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) }