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

178 lines
5.5 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 (
"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)
}
}
})
}