完善不同加密方法的使用

This commit is contained in:
yun
2026-09-30 15:49:12 +08:00
parent 034315ddbd
commit 7db722f8b9
69 changed files with 21362 additions and 2563 deletions
+214
View File
@@ -0,0 +1,214 @@
package encryptx_test
import (
"bytes"
"errors"
"strings"
"testing"
"code.yun.ink/pkg/encryptx"
"code.yun.ink/pkg/encryptx/aesx"
"code.yun.ink/pkg/encryptx/chachax"
)
// 本文件固化第二轮复查中发现的问题,防止回归。
// TestUnexpectedKeyRejected 验证给不使用密钥的算法传密钥会被明确拒绝。
//
// 旧行为是静默忽略:调用方把 BASE64 或哈希误当成加密算法时,会以为数据已经
// 受到密钥保护,实际上完全没有。这类"看起来做了加密"的误用必须报错。
func TestUnexpectedKeyRejected(t *testing.T) {
key := bytes.Repeat([]byte{0x01}, 32)
keyless := []encryptx.EncryptType{
encryptx.MD5, encryptx.SHA256, encryptx.SHA3_512, encryptx.BLAKE2B_256,
encryptx.BASE64, encryptx.BASE64_URL, encryptx.HEX,
encryptx.BCRYPT, encryptx.ARGON2ID,
encryptx.PBKDF2,
}
for _, encryptType := range keyless {
options := encryptx.Options{}
if encryptType == encryptx.PBKDF2 {
// PBKDF2 需要盐值,否则会先报缺参,掩盖我们要测的分支
options["salt"] = bytes.Repeat([]byte{0x02}, 16)
options["iterations"] = 10000
}
if encryptType == encryptx.ARGON2ID {
options["time"] = 1
options["memory"] = 8 * 1024
options["threads"] = 1
}
_, err := encryptx.Encrypt(encryptType, "payload", key, options)
if !errors.Is(err, encryptx.ErrUnexpectedKey) {
t.Errorf("%s 传入密钥: 期望 ErrUnexpectedKey, 得到 %v", encryptType, err)
}
}
// 反向:真正需要密钥的算法仍然照常工作
aesKey, err := encryptx.GenerateKey(encryptx.AES_GCM, nil)
if err != nil {
t.Fatalf("GenerateKey 失败: %v", err)
}
if _, err := encryptx.Encrypt(encryptx.AES_GCM, "payload", aesKey, nil); err != nil {
t.Errorf("AES-GCM 带密钥加密失败: %v", err)
}
}
// TestMalformedCiphertextClassifiedAsInvalidInput 验证门面的错误分类自洽。
//
// 密文的编码与长度问题都应该能通过 errors.Is(err, ErrInvalidInput) 命中,
// 这样调用方才有一套统一的"输入不合法"判定标准。
func TestMalformedCiphertextClassifiedAsInvalidInput(t *testing.T) {
aesKey, _ := encryptx.GenerateKey(encryptx.AES_CBC, nil)
gcmKey, _ := encryptx.GenerateKey(encryptx.AES_GCM, nil)
chachaKey, _ := encryptx.GenerateKey(encryptx.CHACHA20_POLY1305, nil)
cases := []struct {
name string
encryptType encryptx.EncryptType
key []byte
input string
}{
{"CBC 空密文", encryptx.AES_CBC, aesKey, ""},
{"CBC 过短密文", encryptx.AES_CBC, aesKey, "AB"},
{"CBC 非分组对齐", encryptx.AES_CBC, aesKey, strings.Repeat("AB", 17)},
{"CBC 非法十六进制", encryptx.AES_CBC, aesKey, "zz"},
{"ECB 非分组对齐", encryptx.AES_ECB, aesKey, strings.Repeat("AB", 17)},
{"GCM 缺少分隔符", encryptx.AES_GCM, gcmKey, "abcd"},
{"GCM nonce 长度错误", encryptx.AES_GCM, gcmKey, "abcd:ef"},
{"ChaCha 缺少分隔符", encryptx.CHACHA20_POLY1305, chachaKey, "abcd"},
{"Base64 非法", encryptx.BASE64, nil, "!!!!"},
{"Hex 奇数长度", encryptx.HEX, nil, "ABC"},
}
for _, c := range cases {
_, err := encryptx.Decrypt(c.encryptType, c.input, c.key, nil)
if err == nil {
t.Errorf("%s: 期望错误, 得到 nil", c.name)
continue
}
if !errors.Is(err, encryptx.ErrInvalidInput) {
t.Errorf("%s: 期望 ErrInvalidInput, 得到 %v", c.name, err)
}
}
}
// TestAuthenticationFailureStaysDistinguishable 验证认证失败不会被混进
// "输入格式错误"里。
//
// 若把"密文被篡改"也归类为 ErrInvalidInput,调用方就无法把它和参数写错区分开,
// 真正的安全告警会被淹没在普通错误中——这比分类不统一更危险。
func TestAuthenticationFailureStaysDistinguishable(t *testing.T) {
cases := []struct {
encryptType encryptx.EncryptType
sentinel error
}{
{encryptx.AES_GCM, aesx.ErrAuthenticationFailed},
{encryptx.CHACHA20_POLY1305, chachax.ErrAuthenticationFailed},
{encryptx.XCHACHA20_POLY1305, chachax.ErrAuthenticationFailed},
}
for _, c := range cases {
key, _ := encryptx.GenerateKey(c.encryptType, nil)
ciphertext, err := encryptx.Encrypt(c.encryptType, "authenticated payload", key, nil)
if err != nil {
t.Fatalf("%s: %v", c.encryptType, err)
}
// 翻转密文中的一个字符,制造认证失败
tampered := []byte(ciphertext)
for i := range tampered {
if tampered[i] != ':' {
if tampered[i] == '0' {
tampered[i] = '1'
} else {
tampered[i] = '0'
}
break
}
}
_, err = encryptx.Decrypt(c.encryptType, string(tampered), key, nil)
if !errors.Is(err, c.sentinel) {
t.Errorf("%s 篡改: 期望 %v, 得到 %v", c.encryptType, c.sentinel, err)
}
if errors.Is(err, encryptx.ErrInvalidInput) {
t.Errorf("%s 篡改被错误地归类为 ErrInvalidInput: %v", c.encryptType, err)
}
}
}
// TestHKDFConsistentBetweenEncryptAndDeriveKey 验证两条 HKDF 路径对空盐的态度一致。
//
// 复查发现 Encrypt 强制要求 salt 选项,而 DeriveKey 允许缺省(HKDF 的空盐在标准
// 中等价于全零盐),同一算法两条入口行为不一致会误导调用方。
func TestHKDFConsistentBetweenEncryptAndDeriveKey(t *testing.T) {
secret := []byte("high-entropy-secret-material")
// DeriveKey 允许空盐,并返回 nil 盐
key, salt, err := encryptx.DeriveKey(encryptx.HKDF, secret, nil)
if err != nil {
t.Fatalf("DeriveKey(HKDF) 无盐失败: %v", err)
}
if salt != nil {
t.Errorf("HKDF 空盐应当返回 nil, 得到 %v", salt)
}
// Encrypt 也必须允许空盐,且结果与 DeriveKey 一致
derived, err := encryptx.Encrypt(encryptx.HKDF, string(secret), nil, nil)
if err != nil {
t.Fatalf("Encrypt(HKDF) 无盐失败: %v", err)
}
if !strings.EqualFold(derived, hexEncode(key)) {
t.Errorf("Encrypt(HKDF) 与 DeriveKey(HKDF) 结果不一致: %q vs %q", derived, hexEncode(key))
}
// 其余 KDF 仍然必须显式给盐
for _, encryptType := range []encryptx.EncryptType{encryptx.PBKDF2, encryptx.SCRYPT} {
if _, err := encryptx.Encrypt(encryptType, "password", nil,
fastKDFOptions(encryptType)); !errors.Is(err, encryptx.ErrMissingOption) {
t.Errorf("%s 缺盐: 期望 ErrMissingOption, 得到 %v", encryptType, err)
}
}
}
// TestGenerateKeyForAllKDFs 验证四种 KDF 都能生成盐值。
//
// 复查发现 ARGON2ID 被排除在盐值生成之外,而它同样是需要盐的 KDF(DeriveKey
// 路径),同一类算法行为不一致。
func TestGenerateKeyForAllKDFs(t *testing.T) {
for _, encryptType := range kdfTypes {
salt, err := encryptx.GenerateKey(encryptType, encryptx.Options{"saltLength": 16})
if err != nil {
t.Errorf("%s: GenerateKey 失败: %v", encryptType, err)
continue
}
if len(salt) != 16 {
t.Errorf("%s: 盐值长度 = %d, 期望 16", encryptType, len(salt))
}
// 生成的盐必须能用于派生
options := fastKDFOptions(encryptType)
options["salt"] = salt
if _, _, err := encryptx.DeriveKey(encryptType, []byte("password"), options); err != nil {
t.Errorf("%s: 用生成的盐派生失败: %v", encryptType, err)
}
}
// bcrypt 的盐内嵌在哈希结果中,仍然应当拒绝生成
if _, err := encryptx.GenerateKey(encryptx.BCRYPT, nil); err == nil {
t.Error("GenerateKey(BCRYPT) 应当返回错误")
}
}
// hexEncode 是测试用的小工具,避免为一行格式化再引入依赖。
func hexEncode(data []byte) string {
const digits = "0123456789abcdef"
out := make([]byte, 0, len(data)*2)
for _, b := range data {
out = append(out, digits[b>>4], digits[b&0x0f])
}
return string(out)
}