Files
encryptx/encryptx_test.go
T
2026-09-30 15:49:12 +08:00

1069 lines
38 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}
}
}