package encryptx_test import ( "bytes" "errors" "fmt" "os" "path/filepath" "runtime" "strings" "testing" "code.yun.ink/pkg/encryptx" "code.yun.ink/pkg/encryptx/rsax" ) // 所有对称加密算法(含 AEAD),用于表驱动测试。 var symmetricTypes = []encryptx.EncryptType{ encryptx.DES_ECB, encryptx.DES_CBC, encryptx.DES_CFB, encryptx.DES_OFB, encryptx.DES_CTR, encryptx.TDES_ECB, encryptx.TDES_CBC, encryptx.TDES_CFB, encryptx.TDES_OFB, encryptx.TDES_CTR, encryptx.AES_ECB, encryptx.AES_CBC, encryptx.AES_CFB, encryptx.AES_OFB, encryptx.AES_CTR, encryptx.AES_GCM, encryptx.BLOWFISH_ECB, encryptx.BLOWFISH_CBC, encryptx.BLOWFISH_CFB, encryptx.BLOWFISH_OFB, encryptx.BLOWFISH_CTR, encryptx.CHACHA20_POLY1305, encryptx.XCHACHA20_POLY1305, } var hashTypes = []encryptx.EncryptType{ encryptx.MD5, encryptx.SHA1, encryptx.SHA224, encryptx.SHA256, encryptx.SHA384, encryptx.SHA512, encryptx.SHA3_224, encryptx.SHA3_256, encryptx.SHA3_384, encryptx.SHA3_512, encryptx.BLAKE2B_256, encryptx.BLAKE2S_256, encryptx.RIPEMD160, } var hmacTypes = []encryptx.EncryptType{ encryptx.HMAC_SHA1, encryptx.HMAC_SHA256, encryptx.HMAC_SHA384, encryptx.HMAC_SHA512, } var kdfTypes = []encryptx.EncryptType{ encryptx.PBKDF2, encryptx.HKDF, encryptx.SCRYPT, encryptx.ARGON2ID, } // ---- 枚举元数据 ---- func TestEncryptTypeMetadata(t *testing.T) { // 历史数值必须保持不变,否则已落库的配置会指向错误的算法。 legacyValues := map[encryptx.EncryptType]int{ encryptx.MD5: 0, encryptx.SHA1: 1, encryptx.SHA256: 2, encryptx.SHA512: 3, encryptx.SHA3_256: 4, encryptx.SHA3_384: 5, encryptx.SHA3_512: 6, encryptx.DES_ECB: 7, encryptx.DES_CBC: 8, encryptx.DES_CFB: 9, encryptx.TDES_ECB: 10, encryptx.TDES_CBC: 11, encryptx.TDES_CFB: 12, encryptx.AES_ECB: 13, encryptx.AES_CBC: 14, encryptx.AES_CFB: 15, encryptx.AES_GCM: 16, encryptx.BLOWFISH_ECB: 17, encryptx.BLOWFISH_CBC: 18, encryptx.BLOWFISH_CFB: 19, encryptx.RSA: 20, encryptx.HMAC_SHA256: 21, encryptx.HMAC_SHA512: 22, encryptx.PBKDF2: 23, encryptx.BASE64: 24, } for encryptType, want := range legacyValues { if got := int(encryptType); got != want { t.Errorf("%s 的数值 = %d, 期望 %d(破坏兼容性)", encryptType, got, want) } } } func TestGetAllEncryptTypes(t *testing.T) { types := encryptx.GetAllEncryptTypes() if len(types) < 40 { t.Errorf("算法数量 = %d, 期望至少 40 个", len(types)) } for i := 1; i < len(types); i++ { if types[i-1] >= types[i] { t.Errorf("GetAllEncryptTypes 未按升序返回: 位置 %d", i) } } for _, encryptType := range types { if !encryptType.Valid() { t.Errorf("%d.Valid() = false", int(encryptType)) } if encryptType.String() == "" { t.Errorf("%d 的名称为空", int(encryptType)) } } } func TestSecurityMetadata(t *testing.T) { // 已被攻破的算法必须被标记为 SecurityBroken for _, encryptType := range []encryptx.EncryptType{encryptx.MD5, encryptx.SHA1, encryptx.DES_ECB, encryptx.DES_CBC, encryptx.DES_CFB} { if got := encryptType.SecurityLevel(); got != encryptx.SecurityBroken { t.Errorf("%s 的安全等级 = %v, 期望已攻破", encryptType, got) } if !encryptType.IsDeprecated() { t.Errorf("%s 应当被标记为已弃用", encryptType) } } // 推荐的现代算法 for _, encryptType := range []encryptx.EncryptType{encryptx.AES_GCM, encryptx.CHACHA20_POLY1305, encryptx.XCHACHA20_POLY1305, encryptx.SHA256, encryptx.SHA3_256, encryptx.ARGON2ID} { if got := encryptType.SecurityLevel(); got != encryptx.SecurityRecommended { t.Errorf("%s 的安全等级 = %v, 期望推荐", encryptType, got) } } // 只有 AEAD 类算法才应标记完整性保护 if !encryptx.AES_GCM.IsAEAD() || !encryptx.CHACHA20_POLY1305.IsAEAD() { t.Error("AEAD 算法未被标记为提供完整性保护") } if encryptx.AES_CBC.IsAEAD() { t.Error("AES-CBC 不应被标记为提供完整性保护") } // 哈希不可逆、不需要密钥 for _, encryptType := range hashTypes { if encryptType.IsReversible() { t.Errorf("%s 不应可逆", encryptType) } if encryptType.NeedsKey() { t.Errorf("%s 不应需要密钥", encryptType) } } } func TestUnsupportedType(t *testing.T) { bogus := encryptx.EncryptType(9999) if bogus.Valid() { t.Error("EncryptType(9999).Valid() = true") } if _, err := encryptx.Encrypt(bogus, "x", nil, nil); !errors.Is(err, encryptx.ErrUnsupportedType) { t.Errorf("Encrypt: 期望 ErrUnsupportedType, 得到 %v", err) } if _, err := encryptx.Decrypt(bogus, "x", nil, nil); !errors.Is(err, encryptx.ErrUnsupportedType) { t.Errorf("Decrypt: 期望 ErrUnsupportedType, 得到 %v", err) } if _, err := encryptx.GenerateKey(bogus, nil); !errors.Is(err, encryptx.ErrUnsupportedType) { t.Errorf("GenerateKey: 期望 ErrUnsupportedType, 得到 %v", err) } } // ---- 哈希 ---- func TestHashAlgorithms(t *testing.T) { // 权威测试向量(与各子包测试一致,这里验证门面接线正确) vectors := map[encryptx.EncryptType]string{ encryptx.MD5: "900150983cd24fb0d6963f7d28e17f72", encryptx.SHA1: "a9993e364706816aba3e25717850c26c9cd0d89d", encryptx.SHA224: "23097d223405d8228642a477bda255b32aadbce4bda0b3f7e36c9da7", encryptx.SHA256: "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", encryptx.SHA384: "cb00753f45a35e8bb5a03d699ac65007272c32ab0eded1631a8b605a43ff5bed8086072ba1e7cc2358baeca134c825a7", encryptx.SHA512: "ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f", encryptx.SHA3_224: "e642824c3f8cf24ad09234ee7d3c766fc9a3a5168d0c94ad73b46fdf", encryptx.SHA3_256: "3a985da74fe225b2045c172d6bd390bd855f086e3e9d525b46bfe24511431532", encryptx.SHA3_384: "ec01498288516fc926459f58e2c6ad8df9b473cb0fc08c2596da7cf0e49be4b298d88cea927ac7f539f1edf228376d25", encryptx.SHA3_512: "b751850b1a57168a5693cd924b6b096e08f621827444f70d884f5d0240d2712e10e116e9192af3c91a7ec57647e3934057340b4cf408d5a56592f8274eec53f0", encryptx.BLAKE2B_256: "bddd813c634239723171ef3fee98579b94964e3bb1cb3e427262c8c068d52319", encryptx.BLAKE2S_256: "508c5e8c327c14e2e1a72ba34eeb452f37458b209ed63a294d999b4c86675982", encryptx.RIPEMD160: "8eb208f7e05d987a9b044a8e98c6b087f15a0bfc", } for encryptType, want := range vectors { got, err := encryptx.Encrypt(encryptType, "abc", nil, nil) if err != nil { t.Errorf("%s: 计算失败: %v", encryptType, err) continue } if got != want { t.Errorf("%s(\"abc\") = %s, 期望 %s", encryptType, got, want) } } // 哈希不接受任何选项 if _, err := encryptx.Encrypt(encryptx.SHA256, "x", nil, encryptx.Options{"bogus": 1}); !errors.Is(err, encryptx.ErrUnknownOption) { t.Errorf("哈希传入未知选项: 期望 ErrUnknownOption, 得到 %v", err) } } func TestHashAlgorithmsProduceDistinctOutput(t *testing.T) { seen := make(map[string]encryptx.EncryptType) for _, encryptType := range hashTypes { got, err := encryptx.Encrypt(encryptType, "same input", nil, nil) if err != nil { t.Fatalf("%s: %v", encryptType, err) } if prev, dup := seen[got]; dup { t.Errorf("%s 与 %s 产生了相同摘要", encryptType, prev) } seen[got] = encryptType } } // ---- HMAC ---- func TestHMACAlgorithms(t *testing.T) { // RFC 4231 Test Case 1:key = 20 字节 0x0b,data = "Hi There" key := bytes.Repeat([]byte{0x0b}, 20) vectors := map[encryptx.EncryptType]string{ encryptx.HMAC_SHA256: "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7", encryptx.HMAC_SHA512: "87aa7cdea5ef619d4ff0b4241a1d6cb02379f4e2ce4ec2787ad0b30545e17cdedaa833b7d6b8a702038b274eaea3f4e4be9d914eeb61f1702e696c203a126854", } for encryptType, want := range vectors { got, err := encryptx.Encrypt(encryptType, "Hi There", key, nil) if err != nil { t.Fatalf("%s: %v", encryptType, err) } if got != want { t.Errorf("%s = %s, 期望 %s", encryptType, got, want) } } // RFC 2202 未直接给出的 SHA-1/SHA-384 分支至少要有正确长度 for _, tc := range []struct { encryptType encryptx.EncryptType hexLength int }{ {encryptx.HMAC_SHA1, 40}, {encryptx.HMAC_SHA384, 96}, } { got, err := encryptx.Encrypt(tc.encryptType, "Hi There", key, nil) if err != nil { t.Fatalf("%s: %v", tc.encryptType, err) } if len(got) != tc.hexLength { t.Errorf("%s 长度 = %d, 期望 %d", tc.encryptType, len(got), tc.hexLength) } } // HMAC 需要密钥 if _, err := encryptx.Encrypt(encryptx.HMAC_SHA256, "x", nil, nil); !errors.Is(err, encryptx.ErrMissingKey) { t.Errorf("HMAC 无密钥: 期望 ErrMissingKey, 得到 %v", err) } } // ---- 编码 ---- func TestEncodingAlgorithms(t *testing.T) { original := "hello 世界 + / = 编码" cases := []struct { encryptType encryptx.EncryptType }{ {encryptx.BASE64}, {encryptx.BASE64_URL}, {encryptx.HEX}, } for _, c := range cases { encoded, err := encryptx.Encrypt(c.encryptType, original, nil, nil) if err != nil { t.Fatalf("%s 编码失败: %v", c.encryptType, err) } decoded, err := encryptx.Decrypt(c.encryptType, encoded, nil, nil) if err != nil { t.Fatalf("%s 解码失败: %v", c.encryptType, err) } if decoded != original { t.Errorf("%s 往返不一致: 得到 %q, 期望 %q", c.encryptType, decoded, original) } } // 非法输入必须报错而不是返回垃圾 if _, err := encryptx.Decrypt(encryptx.HEX, "这不是十六进制", nil, nil); !errors.Is(err, encryptx.ErrInvalidInput) { t.Errorf("非法 HEX: 期望 ErrInvalidInput, 得到 %v", err) } if _, err := encryptx.Decrypt(encryptx.BASE64, "!!!!", nil, nil); !errors.Is(err, encryptx.ErrInvalidInput) { t.Errorf("非法 Base64: 期望 ErrInvalidInput, 得到 %v", err) } } // ---- 对称加密 ---- // TestGenerateKeyThenEncrypt 是 P0-2 的核心回归测试。 // // 历史缺陷:GenerateKey 返回十六进制文本(32 字节 → 64 字符),而加密函数把它 // 当作原始密钥字节使用,于是「生成密钥 → 加密」必然失败,报 // "crypto/aes: invalid key size 64"。现在密钥统一为 []byte,这条路径必须畅通。 func TestGenerateKeyThenEncrypt(t *testing.T) { const plaintext = "生成密钥后直接加密" for _, encryptType := range symmetricTypes { key, err := encryptx.GenerateKey(encryptType, nil) if err != nil { t.Errorf("%s: GenerateKey 失败: %v", encryptType, err) continue } if len(key) == 0 { t.Errorf("%s: GenerateKey 返回空密钥", encryptType) continue } ciphertext, err := encryptx.Encrypt(encryptType, plaintext, key, nil) if err != nil { t.Errorf("%s: 用生成的密钥加密失败: %v", encryptType, err) continue } decrypted, err := encryptx.Decrypt(encryptType, ciphertext, key, nil) if err != nil { t.Errorf("%s: 解密失败: %v", encryptType, err) continue } if decrypted != plaintext { t.Errorf("%s: 往返不一致, 得到 %q", encryptType, decrypted) } } } func TestSymmetricRoundTripVariousLengths(t *testing.T) { plaintexts := []string{"", "a", "0123456789abcde", "0123456789abcdef", "0123456789abcdefg", strings.Repeat("x", 5000), "中文与 emoji 🔐"} for _, encryptType := range symmetricTypes { key, err := encryptx.GenerateKey(encryptType, nil) if err != nil { t.Fatalf("%s: GenerateKey 失败: %v", encryptType, err) } for i, plaintext := range plaintexts { ciphertext, err := encryptx.Encrypt(encryptType, plaintext, key, nil) if err != nil { t.Fatalf("%s 明文#%d: 加密失败: %v", encryptType, i, err) } decrypted, err := encryptx.Decrypt(encryptType, ciphertext, key, nil) if err != nil { t.Fatalf("%s 明文#%d: 解密失败: %v", encryptType, i, err) } if decrypted != plaintext { t.Errorf("%s 明文#%d: 往返不一致", encryptType, i) } } } } // TestSymmetricOutputHasNoWhitespace 是 P1-6 的回归测试。 // // 旧实现在 AES-GCM 的输出里用 fmt.Sprintf("%x\n", ...) 带上了换行符,再靠忽略 // hex.DecodeString 的错误才勉强往返成功。现在输出必须干净。 func TestSymmetricOutputHasNoWhitespace(t *testing.T) { for _, encryptType := range symmetricTypes { key, _ := encryptx.GenerateKey(encryptType, nil) ciphertext, err := encryptx.Encrypt(encryptType, "check format", key, nil) if err != nil { t.Fatalf("%s: %v", encryptType, err) } if strings.ContainsAny(ciphertext, "\n\r\t ") { t.Errorf("%s 的密文含空白字符: %q", encryptType, ciphertext) } if encryptType == encryptx.AES_GCM || encryptType == encryptx.CHACHA20_POLY1305 || encryptType == encryptx.XCHACHA20_POLY1305 { if parts := strings.Split(ciphertext, ":"); len(parts) != 2 { t.Errorf("%s 的密文格式应为 \"密文:nonce\", 得到 %q", encryptType, ciphertext) } } } } func TestSymmetricWrongKeyDoesNotReturnPlaintext(t *testing.T) { const plaintext = "secret payload" for _, encryptType := range symmetricTypes { key, _ := encryptx.GenerateKey(encryptType, nil) otherKey, _ := encryptx.GenerateKey(encryptType, nil) ciphertext, err := encryptx.Encrypt(encryptType, plaintext, key, nil) if err != nil { t.Fatalf("%s: %v", encryptType, err) } // 允许返回错误,也允许返回垃圾,但绝不能还原出正确明文 decrypted, err := encryptx.Decrypt(encryptType, ciphertext, otherKey, nil) if err == nil && decrypted == plaintext { t.Errorf("%s: 错误密钥解出了正确明文", encryptType) } } } func TestSymmetricWrongKeySizeRejected(t *testing.T) { // 57 字节对 DES(8)、3DES(24)、AES(16/24/32)、Blowfish(≤56) 全都非法。 // 注意不能用"短密钥"来测 Blowfish:它接受 4..56 字节的任意长度。 tooLong := bytes.Repeat([]byte{0x01}, 57) for _, encryptType := range symmetricTypes { if _, err := encryptx.Encrypt(encryptType, "x", tooLong, nil); err == nil { t.Errorf("%s: 57 字节的密钥应当被拒绝", encryptType) } // 空密钥等同于未提供密钥 if _, err := encryptx.Encrypt(encryptType, "x", nil, nil); !errors.Is(err, encryptx.ErrMissingKey) { t.Errorf("%s: 空密钥期望 ErrMissingKey, 得到 %v", encryptType, err) } } } // TestSymmetricTamperDetection 验证带完整性保护的算法能发现篡改。 func TestSymmetricTamperDetection(t *testing.T) { for _, encryptType := range []encryptx.EncryptType{ encryptx.AES_GCM, encryptx.CHACHA20_POLY1305, encryptx.XCHACHA20_POLY1305, } { key, _ := encryptx.GenerateKey(encryptType, nil) ciphertext, err := encryptx.Encrypt(encryptType, "authenticated payload", key, nil) if err != nil { t.Fatalf("%s: %v", encryptType, err) } // 逐个字符翻转,必须全部被检出 for i := 0; i < len(ciphertext); i++ { if ciphertext[i] == ':' { continue } tampered := []byte(ciphertext) if tampered[i] == '0' { tampered[i] = '1' } else { tampered[i] = '0' } if _, err := encryptx.Decrypt(encryptType, string(tampered), key, nil); err == nil { t.Errorf("%s: 篡改第 %d 个字符后仍然解密成功", encryptType, i) } } } } func TestAEADAADBinding(t *testing.T) { for _, encryptType := range []encryptx.EncryptType{encryptx.AES_GCM, encryptx.CHACHA20_POLY1305} { key, _ := encryptx.GenerateKey(encryptType, nil) aad := encryptx.Options{"aad": "user=42"} ciphertext, err := encryptx.Encrypt(encryptType, "payload", key, aad) if err != nil { t.Fatalf("%s: %v", encryptType, err) } if _, err := encryptx.Decrypt(encryptType, ciphertext, key, aad); err != nil { t.Errorf("%s: 正确 AAD 解密失败: %v", encryptType, err) } if _, err := encryptx.Decrypt(encryptType, ciphertext, key, encryptx.Options{"aad": "user=43"}); err == nil { t.Errorf("%s: 错误 AAD 竟然解密成功", encryptType) } if _, err := encryptx.Decrypt(encryptType, ciphertext, key, nil); err == nil { t.Errorf("%s: 缺失 AAD 竟然解密成功", encryptType) } } } func TestExplicitIVOption(t *testing.T) { // 只有 CBC/CFB/OFB/CTR 接受显式 IV;ECB 不接受。 cases := []encryptx.EncryptType{ encryptx.AES_CBC, encryptx.AES_CFB, encryptx.AES_OFB, encryptx.AES_CTR, encryptx.DES_CBC, encryptx.TDES_CBC, } for _, encryptType := range cases { key, _ := encryptx.GenerateKey(encryptType, nil) // AES 的 IV 是 16 字节,DES/3DES 是 8 字节 ivLen := 16 if encryptType == encryptx.DES_CBC || encryptType == encryptx.TDES_CBC { ivLen = 8 } ivHex := strings.Repeat("0011223344556677", 1) if ivLen == 16 { ivHex = strings.Repeat("0011223344556677", 2) } options := encryptx.Options{"iv": ivHex} ciphertext, err := encryptx.Encrypt(encryptType, "fixed iv", key, options) if err != nil { t.Fatalf("%s: 带显式 IV 加密失败: %v", encryptType, err) } decrypted, err := encryptx.Decrypt(encryptType, ciphertext, key, options) if err != nil { t.Fatalf("%s: 带显式 IV 解密失败: %v", encryptType, err) } if decrypted != "fixed iv" { t.Errorf("%s: 往返不一致", encryptType) } // 相同 IV 下相同明文应产生相同密文(这正说明调用方需自行保证 IV 唯一) again, _ := encryptx.Encrypt(encryptType, "fixed iv", key, options) if again != ciphertext { t.Errorf("%s: 固定 IV 下加密结果不确定", encryptType) } // IV 长度不对必须报错 if _, err := encryptx.Encrypt(encryptType, "x", key, encryptx.Options{"iv": "00"}); err == nil { t.Errorf("%s: 非法 IV 长度应当报错", encryptType) } } // ECB 不接受 IV key, _ := encryptx.GenerateKey(encryptx.AES_ECB, nil) if _, err := encryptx.Encrypt(encryptx.AES_ECB, "x", key, encryptx.Options{"iv": strings.Repeat("00", 16)}); err == nil { t.Error("ECB 不应接受 IV") } } // ---- 密钥派生 ---- // TestPBKDF2RequiresSalt 是 P0-4 的回归测试。 // // 历史缺陷:未提供盐值时门面会自动生成随机盐,却只返回密钥、丢弃盐值,导致 // 派生结果永远无法复现。现在要么显式给盐,要么用 DeriveKey 一并拿回盐值。 func TestPBKDF2RequiresSalt(t *testing.T) { if _, err := encryptx.Encrypt(encryptx.PBKDF2, "password", nil, nil); !errors.Is(err, encryptx.ErrMissingOption) { t.Errorf("PBKDF2 缺少盐值: 期望 ErrMissingOption, 得到 %v", err) } } func TestDeriveKeyReproducible(t *testing.T) { password := []byte("correct horse battery staple") for _, encryptType := range kdfTypes { // 用很小的参数,保证测试快速 options := fastKDFOptions(encryptType) key1, salt, err := encryptx.DeriveKey(encryptType, password, options) if err != nil { t.Fatalf("%s: DeriveKey 失败: %v", encryptType, err) } if len(key1) == 0 { t.Fatalf("%s: 派生出的密钥为空", encryptType) } // 用返回的盐值重新派生,结果必须完全一致——这才是"可用"的密钥派生 withSalt := encryptx.Options{} for k, v := range options { withSalt[k] = v } withSalt["salt"] = salt key2, _, err := encryptx.DeriveKey(encryptType, password, withSalt) if err != nil { t.Fatalf("%s: 用返回的盐值重新派生失败: %v", encryptType, err) } if !bytes.Equal(key1, key2) { t.Errorf("%s: 派生结果不可复现", encryptType) } // 不同的盐必须产生不同的密钥(HKDF 允许空盐,故跳过该断言) if encryptType != encryptx.HKDF { otherOptions := encryptx.Options{} for k, v := range options { otherOptions[k] = v } otherOptions["salt"] = bytes.Repeat([]byte{0xAA}, len(salt)+1) key3, _, err := encryptx.DeriveKey(encryptType, password, otherOptions) if err != nil { t.Fatalf("%s: %v", encryptType, err) } if bytes.Equal(key1, key3) { t.Errorf("%s: 不同盐值产生了相同密钥", encryptType) } } } } func TestDeriveKeyRejectsEmptySecret(t *testing.T) { for _, encryptType := range kdfTypes { if _, _, err := encryptx.DeriveKey(encryptType, nil, fastKDFOptions(encryptType)); !errors.Is(err, encryptx.ErrInvalidInput) { t.Errorf("%s: 空口令应被拒绝, 得到 %v", encryptType, err) } } } // TestEncryptKDFWithExplicitSalt 验证 Encrypt 走 KDF 时结果可复现。 // // 不含 ARGON2ID:它在 Encrypt 路径上是 PHC 口令哈希(自带盐),原始密钥派生 // 请走 DeriveKey。 func TestEncryptKDFWithExplicitSalt(t *testing.T) { salt := bytes.Repeat([]byte{0x5A}, 16) for _, encryptType := range []encryptx.EncryptType{encryptx.PBKDF2, encryptx.SCRYPT} { options := fastKDFOptions(encryptType) options["salt"] = salt first, err := encryptx.Encrypt(encryptType, "password", nil, options) if err != nil { t.Fatalf("%s: %v", encryptType, err) } second, err := encryptx.Encrypt(encryptType, "password", nil, options) if err != nil { t.Fatalf("%s: %v", encryptType, err) } if first != second { t.Errorf("%s: 同一盐值下派生结果不可复现", encryptType) } } } // fastKDFOptions 返回各 KDF 的最小可用参数,避免测试耗时过长。 func fastKDFOptions(encryptType encryptx.EncryptType) encryptx.Options { switch encryptType { case encryptx.PBKDF2: return encryptx.Options{"iterations": 10000, "keyLength": 32} case encryptx.SCRYPT: return encryptx.Options{"n": 1 << 10, "r": 8, "p": 1, "keyLength": 32} case encryptx.ARGON2ID: return encryptx.Options{"time": 1, "memory": 8 * 1024, "threads": 1, "keyLength": 32} default: return encryptx.Options{"keyLength": 32} } } // ---- 不可逆算法 ---- func TestDecryptIrreversible(t *testing.T) { irreversible := append([]encryptx.EncryptType{}, hashTypes...) irreversible = append(irreversible, hmacTypes...) irreversible = append(irreversible, kdfTypes...) irreversible = append(irreversible, encryptx.BCRYPT) for _, encryptType := range irreversible { if _, err := encryptx.Decrypt(encryptType, "whatever", nil, nil); !errors.Is(err, encryptx.ErrIrreversible) { t.Errorf("%s: 期望 ErrIrreversible, 得到 %v", encryptType, err) } } } func TestMissingKey(t *testing.T) { for _, encryptType := range symmetricTypes { if _, err := encryptx.Encrypt(encryptType, "x", nil, nil); !errors.Is(err, encryptx.ErrMissingKey) { t.Errorf("%s: 期望 ErrMissingKey, 得到 %v", encryptType, err) } } } func TestGenerateKeyForKeylessAlgorithms(t *testing.T) { for _, encryptType := range append(append([]encryptx.EncryptType{}, hashTypes...), encryptx.BASE64, encryptx.BASE64_URL, encryptx.HEX, encryptx.BCRYPT) { if _, err := encryptx.GenerateKey(encryptType, nil); err == nil { t.Errorf("%s: 不应能生成密钥", encryptType) } } } // ---- 选项严格性 ---- // TestUnknownOptionRejected 是 P2-4 的回归测试。 // // 历史版本用 map[string]interface{} 取参,键名拼错会被静默忽略并回退默认值, // 导致「以为设置了参数、实际用的是默认值」这类安全隐患。现在必须报错。 func TestUnknownOptionRejected(t *testing.T) { key, _ := encryptx.GenerateKey(encryptx.AES_CBC, nil) if _, err := encryptx.Encrypt(encryptx.AES_CBC, "x", key, encryptx.Options{"keyLen": 32}); !errors.Is(err, encryptx.ErrUnknownOption) { t.Errorf("拼错的选项名: 期望 ErrUnknownOption, 得到 %v", err) } if _, err := encryptx.Encrypt(encryptx.PBKDF2, "x", nil, encryptx.Options{"salt": bytes.Repeat([]byte{1}, 16), "iterration": 1000}); !errors.Is(err, encryptx.ErrUnknownOption) { t.Errorf("拼错的 KDF 选项名: 期望 ErrUnknownOption, 得到 %v", err) } if _, err := encryptx.GenerateKey(encryptx.AES_CBC, encryptx.Options{"keylength": 32}); !errors.Is(err, encryptx.ErrUnknownOption) { t.Errorf("GenerateKey 未知选项: 期望 ErrUnknownOption, 得到 %v", err) } } // TestOptionTypeMismatchRejected 验证类型不符时不再静默取默认值。 // // 历史版本 `iterations, ok := options["iterations"].(int)` 在类型不符时 ok 为 // false,于是静默回退默认值——调用方以为设置了参数,实际用的却是默认值。 func TestOptionTypeMismatchRejected(t *testing.T) { aesKey, err := encryptx.GenerateKey(encryptx.AES_CBC, nil) if err != nil { t.Fatalf("GenerateKey 失败: %v", err) } salt := bytes.Repeat([]byte{0x11}, 16) cases := []struct { name string encryptType encryptx.EncryptType key []byte options encryptx.Options }{ {"keyLength 传字符串", encryptx.PBKDF2, nil, encryptx.Options{"salt": salt, "keyLength": "32"}}, {"keyLength 传结构体", encryptx.PBKDF2, nil, encryptx.Options{"salt": salt, "keyLength": struct{}{}}}, {"keyLength 传布尔", encryptx.PBKDF2, nil, encryptx.Options{"salt": salt, "keyLength": true}}, {"aad 传整数", encryptx.AES_GCM, aesKey, encryptx.Options{"aad": 123}}, {"iv 传整数", encryptx.AES_CBC, aesKey, encryptx.Options{"iv": 4242}}, {"salt 传整数", encryptx.PBKDF2, nil, encryptx.Options{"salt": 42}}, {"iterations 传字符串", encryptx.PBKDF2, nil, encryptx.Options{"salt": salt, "iterations": "many"}}, {"aad 传布尔", encryptx.CHACHA20_POLY1305, aesKey, encryptx.Options{"aad": true}}, } for _, c := range cases { _, err := encryptx.Encrypt(c.encryptType, "payload", c.key, c.options) if !errors.Is(err, encryptx.ErrInvalidOption) { t.Errorf("%s: 期望 ErrInvalidOption, 得到 %v", c.name, err) } } // GenerateKey 同样不得静默回退 if _, err := encryptx.GenerateKey(encryptx.AES_CBC, encryptx.Options{"keyLength": "32"}); !errors.Is(err, encryptx.ErrInvalidOption) { t.Errorf("GenerateKey 类型不符: 期望 ErrInvalidOption, 得到 %v", err) } } // TestOptionsAcceptInt64 验证 JSON 解码常见的 int64 能被正确接受。 func TestOptionsAcceptInt64(t *testing.T) { key, err := encryptx.GenerateKey(encryptx.AES_CBC, encryptx.Options{"keyLength": int64(16)}) if err != nil { t.Fatalf("int64 形式的 keyLength 被拒绝: %v", err) } if len(key) != 16 { t.Errorf("密钥长度 = %d, 期望 16", len(key)) } key32, err := encryptx.GenerateKey(encryptx.AES_CBC, encryptx.Options{"keyLength": float64(32)}) if err != nil { t.Fatalf("float64 形式的 keyLength 被拒绝: %v", err) } if len(key32) != 32 { t.Errorf("密钥长度 = %d, 期望 32", len(key32)) } if _, err := encryptx.GenerateKey(encryptx.AES_CBC, encryptx.Options{"keyLength": 16.5}); !errors.Is(err, encryptx.ErrInvalidOption) { t.Errorf("非整数 float64: 期望 ErrInvalidOption, 得到 %v", err) } } // ---- 口令哈希 ---- func TestPasswordHashing(t *testing.T) { password := []byte("S3cr3t-P@ssw0rd") cases := []struct { encryptType encryptx.EncryptType options encryptx.Options prefix string }{ {encryptx.BCRYPT, encryptx.Options{"cost": 4}, "$2"}, {encryptx.ARGON2ID, encryptx.Options{"time": 1, "memory": 8 * 1024, "threads": 1}, "$argon2id$"}, } for _, c := range cases { encoded, err := encryptx.HashPassword(c.encryptType, password, c.options) if err != nil { t.Fatalf("%s: HashPassword 失败: %v", c.encryptType, err) } if !strings.HasPrefix(encoded, c.prefix) { t.Errorf("%s: 哈希串前缀 = %q, 期望以 %q 开头", c.encryptType, encoded, c.prefix) } ok, err := encryptx.VerifyPassword(c.encryptType, password, encoded, nil) if err != nil { t.Fatalf("%s: VerifyPassword 返回错误: %v", c.encryptType, err) } if !ok { t.Errorf("%s: 正确口令校验失败", c.encryptType) } ok, err = encryptx.VerifyPassword(c.encryptType, []byte("wrong"), encoded, nil) if err != nil { t.Errorf("%s: 错误口令不应返回 error, 得到 %v", c.encryptType, err) } if ok { t.Errorf("%s: 错误口令通过了校验", c.encryptType) } // 非法哈希串应返回 error if _, err := encryptx.VerifyPassword(c.encryptType, password, "not-a-hash", nil); err == nil { t.Errorf("%s: 非法哈希串期望 error", c.encryptType) } // Encrypt 也应支持口令哈希 viaEncrypt, err := encryptx.Encrypt(c.encryptType, string(password), nil, c.options) if err != nil { t.Fatalf("%s: Encrypt 形式的口令哈希失败: %v", c.encryptType, err) } if ok, _ := encryptx.VerifyPassword(c.encryptType, password, viaEncrypt, nil); !ok { t.Errorf("%s: Encrypt 产生的哈希无法校验通过", c.encryptType) } // 同一口令两次哈希必须不同(随机盐) second, _ := encryptx.HashPassword(c.encryptType, password, c.options) if second == encoded { t.Errorf("%s: 两次哈希结果相同, 随机盐可能未生效", c.encryptType) } } } // ---- 非对称加密与签名 ---- func TestRSARoundTrip(t *testing.T) { privatePEM, err := encryptx.GenerateKey(encryptx.RSA, encryptx.Options{"bits": 2048}) if err != nil { t.Fatalf("GenerateKey(RSA) 失败: %v", err) } priv, err := rsax.ParsePrivateKeyPEM(privatePEM) if err != nil { t.Fatalf("解析私钥失败: %v", err) } publicPEM, err := rsax.MarshalPublicKeyPEM(&priv.PublicKey) if err != nil { t.Fatalf("导出公钥失败: %v", err) } // GenerateKey 返回的是密钥材料本身,而不是像旧版本那样返回一句提示文本 if !bytes.Contains(privatePEM, []byte("PRIVATE KEY")) { t.Error("GenerateKey(RSA) 未返回 PEM 密钥") } for _, plaintext := range []string{"short", strings.Repeat("long plaintext ", 400)} { ciphertext, err := encryptx.Encrypt(encryptx.RSA, plaintext, publicPEM, nil) if err != nil { t.Fatalf("RSA 加密失败: %v", err) } decrypted, err := encryptx.Decrypt(encryptx.RSA, ciphertext, privatePEM, nil) if err != nil { t.Fatalf("RSA 解密失败: %v", err) } if decrypted != plaintext { t.Errorf("RSA 往返不一致(明文长度 %d)", len(plaintext)) } } // 弱密钥位数必须被拒绝 if _, err := encryptx.GenerateKey(encryptx.RSA, encryptx.Options{"bits": 1024}); err == nil { t.Error("1024 位 RSA 密钥应当被拒绝") } // 篡改密文必须失败 ciphertext, _ := encryptx.Encrypt(encryptx.RSA, "tamper me", publicPEM, nil) tampered := []byte(ciphertext) tampered[0] ^= 0x01 if _, err := encryptx.Decrypt(encryptx.RSA, string(tampered), privatePEM, nil); err == nil { t.Error("篡改后的 RSA 密文竟然解密成功") } } func TestRSAKeyFileWorkflow(t *testing.T) { dir := t.TempDir() privatePath := filepath.Join(dir, "private.pem") publicPath := filepath.Join(dir, "public.pem") _, err := encryptx.GenerateKey(encryptx.RSA, encryptx.Options{ "bits": 2048, "privateKeyFile": privatePath, "publicKeyFile": publicPath, }) if err != nil { t.Fatalf("生成并落盘 RSA 密钥失败: %v", err) } // 只给路径、不给密钥内容 ciphertext, err := encryptx.Encrypt(encryptx.RSA, "via files", nil, encryptx.Options{"publicKeyFile": publicPath}) if err != nil { t.Fatalf("按文件路径加密失败: %v", err) } decrypted, err := encryptx.Decrypt(encryptx.RSA, ciphertext, nil, encryptx.Options{"privateKeyFile": privatePath}) if err != nil { t.Fatalf("按文件路径解密失败: %v", err) } if decrypted != "via files" { t.Errorf("往返不一致: %q", decrypted) } // 文件不存在必须返回 error,而不是像旧版本那样 panic if _, err := encryptx.Encrypt(encryptx.RSA, "x", nil, encryptx.Options{"publicKeyFile": filepath.Join(dir, "missing.pem")}); err == nil { t.Error("公钥文件不存在时应当返回错误") } if runtime.GOOS != "windows" { // 私钥文件权限应为 0600 info, err := os.Stat(privatePath) if err != nil { t.Fatalf("stat 私钥文件失败: %v", err) } if perm := info.Mode().Perm(); perm != 0o600 { t.Errorf("私钥文件权限 = %o, 期望 600", perm) } } } // keyPairOptions 返回生成该签名算法密钥对所需的选项。 // // 只有 RSA 有 bits 参数;Ed25519 与 ECDSA 的强度由算法/曲线决定,传入 bits // 会被判为未知选项(这正是选项严格校验的体现)。 func keyPairOptions(signType encryptx.SignType) encryptx.Options { if signType.UsesRSA() { return encryptx.Options{"bits": 2048} } return nil } func TestSignAndVerify(t *testing.T) { const message = "待签名的消息 with unicode 🔏" for _, signType := range encryptx.GetAllSignTypes() { privatePEM, publicPEM, err := encryptx.GenerateKeyPair(signType, keyPairOptions(signType)) if err != nil { t.Fatalf("%s: GenerateKeyPair 失败: %v", signType, err) } signature, err := encryptx.Sign(signType, message, privatePEM, nil) if err != nil { t.Fatalf("%s: 签名失败: %v", signType, err) } if err := encryptx.Verify(signType, message, signature, publicPEM, nil); err != nil { t.Errorf("%s: 正确签名验签失败: %v", signType, err) } // 篡改消息 if err := encryptx.Verify(signType, message+"!", signature, publicPEM, nil); err == nil { t.Errorf("%s: 篡改消息后验签竟然通过", signType) } // 篡改签名 badSignature := []byte(signature) badSignature[0] ^= 0x01 if err := encryptx.Verify(signType, message, string(badSignature), publicPEM, nil); err == nil { t.Errorf("%s: 篡改签名后验签竟然通过", signType) } // 错误公钥 _, otherPublicPEM, err := encryptx.GenerateKeyPair(signType, keyPairOptions(signType)) if err != nil { t.Fatalf("%s: %v", signType, err) } if err := encryptx.Verify(signType, message, signature, otherPublicPEM, nil); err == nil { t.Errorf("%s: 用错误公钥验签竟然通过", signType) } // 非法的签名 Base64 if err := encryptx.Verify(signType, message, "!!!not-base64!!!", publicPEM, nil); err == nil { t.Errorf("%s: 非法签名 Base64 期望错误", signType) } // 把加密用的私钥当公钥传入应当报错 if err := encryptx.Verify(signType, message, signature, privatePEM, nil); err == nil { t.Errorf("%s: 用私钥验签竟然通过", signType) } } } func TestSignInvalidInputs(t *testing.T) { bogus := encryptx.SignType(9999) if bogus.Valid() { t.Error("SignType(9999).Valid() = true") } if _, err := encryptx.Sign(bogus, "x", nil, nil); !errors.Is(err, encryptx.ErrUnsupportedType) { t.Errorf("Sign: 期望 ErrUnsupportedType, 得到 %v", err) } if err := encryptx.Verify(bogus, "x", "sig", nil, nil); !errors.Is(err, encryptx.ErrUnsupportedType) { t.Errorf("Verify: 期望 ErrUnsupportedType, 得到 %v", err) } // 缺少密钥 if _, err := encryptx.Sign(encryptx.ED25519, "x", nil, nil); !errors.Is(err, encryptx.ErrMissingKey) { t.Errorf("Sign 缺少密钥: 期望 ErrMissingKey, 得到 %v", err) } if err := encryptx.Verify(encryptx.ED25519, "x", "sig", nil, nil); !errors.Is(err, encryptx.ErrMissingKey) { t.Errorf("Verify 缺少密钥: 期望 ErrMissingKey, 得到 %v", err) } // 垃圾 PEM if _, err := encryptx.Sign(encryptx.ED25519, "x", []byte("not a pem"), nil); err == nil { t.Error("垃圾 PEM 期望错误") } if err := encryptx.Verify(encryptx.ED25519, "x", "c2ln", []byte("not a pem"), nil); err == nil { t.Error("垃圾 PEM 期望错误") } // 算法与密钥类型不匹配:把 Ed25519 密钥当 RSA 用 privatePEM, _, err := encryptx.GenerateKeyPair(encryptx.ED25519, nil) if err != nil { t.Fatalf("GenerateKeyPair 失败: %v", err) } if _, err := encryptx.Sign(encryptx.RSA_PSS, "x", privatePEM, nil); err == nil { t.Error("用 Ed25519 私钥做 RSA 签名应当报错") } } func TestGenerateKeyPairECDSA(t *testing.T) { // ECDSA 的曲线必须与 SignType 匹配 privatePEM, publicPEM, err := encryptx.GenerateKeyPair(encryptx.ECDSA_P256, nil) if err != nil { t.Fatalf("GenerateKeyPair(ECDSA_P256) 失败: %v", err) } signature, err := encryptx.Sign(encryptx.ECDSA_P256, "msg", privatePEM, nil) if err != nil { t.Fatalf("签名失败: %v", err) } if err := encryptx.Verify(encryptx.ECDSA_P256, "msg", signature, publicPEM, nil); err != nil { t.Fatalf("验签失败: %v", err) } // 换一个曲线标识去验签,必须因曲线不匹配而失败 if err := encryptx.Verify(encryptx.ECDSA_P384, "msg", signature, publicPEM, nil); err == nil { t.Error("曲线不匹配时验签应当失败") } } // ---- 健壮性 ---- // TestDecryptGarbageNeverPanics 用大量畸形输入轰炸所有可逆算法。 // // 历史版本在 ECB 解密时对非分组对齐的密文会触发 // "slice bounds out of range" panic,攻击者用一串伪造密文即可让服务崩溃。 func TestDecryptGarbageNeverPanics(t *testing.T) { garbage := []string{ "", "0", "00", "zz", "!!!", "::::", "0:0", strings.Repeat("A", 33), strings.Repeat("AB", 7), strings.Repeat("AB", 8), strings.Repeat("AB", 9), strings.Repeat("ff", 100), "not-hex-at-all", strings.Repeat("=", 10), "\n\n\n", } reversible := append([]encryptx.EncryptType{}, symmetricTypes...) reversible = append(reversible, encryptx.BASE64, encryptx.BASE64_URL, encryptx.HEX) for _, encryptType := range reversible { var key []byte if encryptType.NeedsKey() { var err error key, err = encryptx.GenerateKey(encryptType, nil) if err != nil { t.Fatalf("%s: GenerateKey 失败: %v", encryptType, err) } } for i, input := range garbage { // 不应 panic _, _ = encryptx.Decrypt(encryptType, input, key, nil) _ = i } } } func TestEncryptNeverPanicsOnNilOptions(t *testing.T) { key, _ := encryptx.GenerateKey(encryptx.AES_CBC, nil) if _, err := encryptx.Encrypt(encryptx.AES_CBC, "x", key, nil); err != nil { t.Errorf("nil options 应当被接受, 得到 %v", err) } if _, err := encryptx.GenerateKey(encryptx.AES_CBC, nil); err != nil { t.Errorf("nil options 应当被接受, 得到 %v", err) } } // TestConcurrentUse 验证门面可被并发安全调用(无共享可变状态)。 func TestConcurrentUse(t *testing.T) { key, err := encryptx.GenerateKey(encryptx.AES_GCM, nil) if err != nil { t.Fatalf("GenerateKey 失败: %v", err) } const goroutines = 16 done := make(chan error, goroutines) for i := 0; i < goroutines; i++ { go func(n int) { plaintext := fmt.Sprintf("goroutine %d payload", n) ciphertext, err := encryptx.Encrypt(encryptx.AES_GCM, plaintext, key, nil) if err != nil { done <- err return } decrypted, err := encryptx.Decrypt(encryptx.AES_GCM, ciphertext, key, nil) if err != nil { done <- err return } if decrypted != plaintext { done <- fmt.Errorf("往返不一致: %q", decrypted) return } done <- nil }(i) } for i := 0; i < goroutines; i++ { if err := <-done; err != nil { t.Errorf("并发调用失败: %v", err) } } }