178 lines
5.5 KiB
Go
178 lines
5.5 KiB
Go
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)
|
||
}
|
||
}
|
||
})
|
||
}
|