package encryptx_test import ( "strings" "testing" "code.yun.ink/pkg/encryptx" ) // 本文件提供 Go 原生 fuzz 目标。它们有两个作用: // // 1. 普通 `go test` 时以种子语料运行,作为常规回归测试; // 2. `go test -fuzz=FuzzXxx` 时持续变异输入,穷举解码/解密路径上的边界。 // // 之所以重点 fuzz 解密路径:这些函数的输入直接来自网络或数据库等不可信来源, // 且历史上正是在这里出现过 "slice bounds out of range" 崩溃。 // // 有意排除 RSA 与 PBKDF2/scrypt/Argon2:它们单次调用开销过大,会让 fuzz 的 // 变异吞吐低到没有意义。这些算法的错误路径由各自的单元测试覆盖。 // fuzzTargets 是适合高频 fuzz 的算法:对称加密与编码。 func fuzzTargets() []encryptx.EncryptType { targets := make([]encryptx.EncryptType, 0, len(symmetricTypes)+3) targets = append(targets, symmetricTypes...) targets = append(targets, encryptx.BASE64, encryptx.BASE64_URL, encryptx.HEX) return targets } // precomputedKeys 预生成各算法的密钥,避免每次 fuzz 迭代重复生成。 // 不需要密钥的算法(编码类)不参与。 func precomputedKeys(f *testing.F) map[encryptx.EncryptType][]byte { f.Helper() keys := make(map[encryptx.EncryptType][]byte) for _, encryptType := range fuzzTargets() { if !encryptType.NeedsKey() { continue } key, err := encryptx.GenerateKey(encryptType, nil) if err != nil { f.Fatalf("%s: GenerateKey 失败: %v", encryptType, err) } keys[encryptType] = key } return keys } // FuzzDecryptNeverPanics 是 P0-3 的模糊测试形态。 // // 无论喂进什么字符串,解密都必须安全返回,绝不能因为切片越界、除零或空指针 // 而崩溃——攻击者只要能构造一段畸形的十六进制密文,就能用 panic 打掉服务。 func FuzzDecryptNeverPanics(f *testing.F) { keys := precomputedKeys(f) targets := fuzzTargets() // 种子语料:空、奇数长度、非十六进制、非分组对齐、超长、含分隔符 f.Add("") f.Add("A") f.Add("AB") f.Add("zz") f.Add(":::") f.Add("0:0") f.Add(strings.Repeat("AB", 7)) f.Add(strings.Repeat("AB", 8)) f.Add(strings.Repeat("AB", 9)) f.Add(strings.Repeat("AB", 17)) f.Add(strings.Repeat("FF", 64)) f.Add("not-hex-at-all") f.Fuzz(func(t *testing.T, data string) { for _, encryptType := range targets { // 只要求"不 panic";返回值无论是什么都可以接受。 _, _ = encryptx.Decrypt(encryptType, data, keys[encryptType], nil) } }) } // FuzzDecryptWithIVNeverPanics 覆盖带显式 IV 的解密路径。 // // IV 由选项传入,长度可能任意;这条路径额外接受一个十六进制字符串, // 是另一个容易越界的入口。 func FuzzDecryptWithIVNeverPanics(f *testing.F) { keys := precomputedKeys(f) // 只挑接受 iv 选项的模式(ECB 不接受) targets := []encryptx.EncryptType{ encryptx.AES_CBC, encryptx.AES_CFB, encryptx.AES_OFB, encryptx.AES_CTR, encryptx.DES_CBC, encryptx.TDES_CBC, } f.Add("AB", "0011223344556677") f.Add("", "") f.Add(strings.Repeat("AB", 20), "zz") f.Fuzz(func(t *testing.T, data, iv string) { for _, encryptType := range targets { _, _ = encryptx.Decrypt(encryptType, data, keys[encryptType], encryptx.Options{"iv": iv}) } }) } // FuzzSymmetricRoundTrip 验证「加密再解密必然还原」这一基本性质对任意明文成立, // 包括空串、跨分组边界长度与多字节 UTF-8。 func FuzzSymmetricRoundTrip(f *testing.F) { keys := precomputedKeys(f) f.Add("") f.Add("a") f.Add(strings.Repeat("x", 15)) f.Add(strings.Repeat("x", 16)) f.Add(strings.Repeat("x", 17)) f.Add("中文与 emoji 🔐") f.Add(strings.Repeat("payload", 100)) f.Fuzz(func(t *testing.T, plaintext string) { for _, encryptType := range symmetricTypes { key := keys[encryptType] ciphertext, err := encryptx.Encrypt(encryptType, plaintext, key, nil) if err != nil { t.Fatalf("%s: 加密失败: %v", encryptType, err) } decrypted, err := encryptx.Decrypt(encryptType, ciphertext, key, nil) if err != nil { t.Fatalf("%s: 解密失败: %v", encryptType, err) } if decrypted != plaintext { t.Fatalf("%s: 往返不一致(明文长度 %d)", encryptType, len(plaintext)) } } }) } // FuzzAEADTamperDetection 验证 AEAD 的认证性质:对密文做任何单字节改动, // 解密都必须失败。 func FuzzAEADTamperDetection(f *testing.F) { keys := precomputedKeys(f) aeads := []encryptx.EncryptType{ encryptx.AES_GCM, encryptx.CHACHA20_POLY1305, encryptx.XCHACHA20_POLY1305, } f.Add("payload", 0) f.Add("", 3) f.Add(strings.Repeat("x", 200), 42) f.Fuzz(func(t *testing.T, plaintext string, offset int) { for _, encryptType := range aeads { key := keys[encryptType] ciphertext, err := encryptx.Encrypt(encryptType, plaintext, key, encryptx.Options{"aad": "ctx"}) if err != nil { t.Fatalf("%s: 加密失败: %v", encryptType, err) } if len(ciphertext) == 0 { continue } // 把 offset 映射到合法下标,并避开分隔符 idx := offset % len(ciphertext) if idx < 0 { idx += len(ciphertext) } if ciphertext[idx] == ':' { continue } tampered := []byte(ciphertext) if tampered[idx] == '0' { tampered[idx] = '1' } else { tampered[idx] = '0' } if _, err := encryptx.Decrypt(encryptType, string(tampered), key, encryptx.Options{"aad": "ctx"}); err == nil { t.Fatalf("%s: 篡改第 %d 个字符后仍然解密成功", encryptType, idx) } } }) }