package chachax_test import ( "bytes" "crypto/rand" "encoding/hex" "testing" "golang.org/x/crypto/chacha20poly1305" "code.yun.ink/pkg/encryptx/chachax" ) // testKey 是测试专用的固定 32 字节密钥。测试密钥可以硬编码,生产密钥绝不能。 var testKey = []byte("0123456789abcdef0123456789abcdef") // mustHex 把十六进制字符串解码为字节,解码失败直接终止测试。 func mustHex(t *testing.T, s string) []byte { t.Helper() b, err := hex.DecodeString(s) if err != nil { t.Fatalf("测试向量十六进制解码失败: %v", err) } return b } // wantDecryptError 断言解密失败,且失败时返回的明文必须为 nil(绝不返回未认证数据)。 func wantDecryptError(t *testing.T, name string, got []byte, err error) { t.Helper() if err == nil { t.Fatalf("%s: 期望解密失败,实际成功且返回 %d 字节明文", name, len(got)) } if got != nil { t.Fatalf("%s: 解密失败时明文必须为 nil,实际为 %d 字节", name, len(got)) } } // fillPlaintext 生成确定性的、非全零的测试明文,便于定位篡改位置。 func fillPlaintext(n int) []byte { b := make([]byte, n) for i := range b { b[i] = byte(i*7 + 13) } return b } // --------------------------------------------------------------------------- // 1. 加解密往返(空明文、1 字节、分组边界附近、100 KB) // --------------------------------------------------------------------------- func TestRoundTripSizes(t *testing.T) { sizes := []int{0, 1, 15, 16, 17, 63, 64, 65, 127, 128, 129, 1024, 100 * 1024} for _, size := range sizes { size := size t.Run(itoa(size), func(t *testing.T) { plaintext := fillPlaintext(size) ciphertext, nonce, err := chachax.Encrypt(testKey, plaintext, nil) if err != nil { t.Fatalf("Encrypt 失败: %v", err) } if len(nonce) != chachax.NonceSize { t.Fatalf("nonce 长度 = %d,期望 %d", len(nonce), chachax.NonceSize) } if len(ciphertext) != size+chachax.Overhead { t.Fatalf("密文长度 = %d,期望 %d", len(ciphertext), size+chachax.Overhead) } got, err := chachax.Decrypt(testKey, ciphertext, nonce, nil) if err != nil { t.Fatalf("Decrypt 失败: %v", err) } if !bytes.Equal(got, plaintext) { t.Fatalf("往返结果不一致: 得到 %d 字节,期望 %d 字节", len(got), len(plaintext)) } }) } } func TestRoundTripXSizes(t *testing.T) { sizes := []int{0, 1, 15, 16, 17, 63, 64, 65, 127, 128, 129, 1024, 100 * 1024} for _, size := range sizes { size := size t.Run(itoa(size), func(t *testing.T) { plaintext := fillPlaintext(size) ciphertext, nonce, err := chachax.EncryptX(testKey, plaintext, nil) if err != nil { t.Fatalf("EncryptX 失败: %v", err) } if len(nonce) != chachax.NonceSizeX { t.Fatalf("XChaCha20 nonce 长度 = %d,期望 %d", len(nonce), chachax.NonceSizeX) } if len(nonce) != 24 { t.Fatalf("XChaCha20 nonce 长度 = %d,期望 24", len(nonce)) } if len(ciphertext) != size+chachax.Overhead { t.Fatalf("密文长度 = %d,期望 %d", len(ciphertext), size+chachax.Overhead) } got, err := chachax.DecryptX(testKey, ciphertext, nonce, nil) if err != nil { t.Fatalf("DecryptX 失败: %v", err) } if !bytes.Equal(got, plaintext) { t.Fatalf("往返结果不一致: 得到 %d 字节,期望 %d 字节", len(got), len(plaintext)) } }) } } func TestEncryptUsesFreshNonce(t *testing.T) { c1, n1, err := chachax.Encrypt(testKey, []byte("same plaintext"), nil) if err != nil { t.Fatalf("Encrypt 失败: %v", err) } c2, n2, err := chachax.Encrypt(testKey, []byte("same plaintext"), nil) if err != nil { t.Fatalf("Encrypt 失败: %v", err) } if bytes.Equal(n1, n2) { t.Fatal("两次 Encrypt 返回了相同的 nonce,同一密钥下 nonce 绝不能重复") } if bytes.Equal(c1, c2) { t.Fatal("两次 Encrypt 相同明文得到了相同密文,说明没有随机 nonce") } _, xn1, err := chachax.EncryptX(testKey, []byte("same plaintext"), nil) if err != nil { t.Fatalf("EncryptX 失败: %v", err) } _, xn2, err := chachax.EncryptX(testKey, []byte("same plaintext"), nil) if err != nil { t.Fatalf("EncryptX 失败: %v", err) } if bytes.Equal(xn1, xn2) { t.Fatal("两次 EncryptX 返回了相同的 nonce") } } // --------------------------------------------------------------------------- // 2. AAD 绑定 // --------------------------------------------------------------------------- func TestAADBinding(t *testing.T) { plaintext := []byte("payload that must be bound to its context") aad := []byte("v1|tenant=42|") ciphertext, nonce, err := chachax.Encrypt(testKey, plaintext, aad) if err != nil { t.Fatalf("Encrypt 失败: %v", err) } got, err := chachax.Decrypt(testKey, ciphertext, nonce, aad) if err != nil { t.Fatalf("相同 AAD 解密应当成功,实际失败: %v", err) } if !bytes.Equal(got, plaintext) { t.Fatal("相同 AAD 解密结果不一致") } // 用非空 AAD 加密后,任何不同的 AAD 都必须认证失败; // nil / 空切片与 aad 不等价(它们彼此等价,见 TestNilAndEmptyAADEquivalent)。 badAADs := map[string][]byte{ "不同 AAD": []byte("v1|tenant=43|"), "多一个字节": append(append([]byte{}, aad...), 'x'), "少一个字节": aad[:len(aad)-1], "nil AAD": nil, "空 AAD": {}, "大小写不同": []byte("V1|TENANT=42|"), } for name, bad := range badAADs { got, err := chachax.Decrypt(testKey, ciphertext, nonce, bad) wantDecryptError(t, "AAD "+name, got, err) } // 反向:用 nil AAD 加密,用非空 AAD 解密必须失败。 ct2, n2, err := chachax.Encrypt(testKey, plaintext, nil) if err != nil { t.Fatalf("Encrypt 失败: %v", err) } got, err = chachax.Decrypt(testKey, ct2, n2, aad) wantDecryptError(t, "nil 加密 + 非空 AAD 解密", got, err) } func TestAADBindingX(t *testing.T) { plaintext := []byte("payload that must be bound to its context") aad := []byte("v1|tenant=42|") ciphertext, nonce, err := chachax.EncryptX(testKey, plaintext, aad) if err != nil { t.Fatalf("EncryptX 失败: %v", err) } got, err := chachax.DecryptX(testKey, ciphertext, nonce, aad) if err != nil { t.Fatalf("相同 AAD 解密应当成功,实际失败: %v", err) } if !bytes.Equal(got, plaintext) { t.Fatal("相同 AAD 解密结果不一致") } got, err = chachax.DecryptX(testKey, ciphertext, nonce, []byte("v1|tenant=43|")) wantDecryptError(t, "XChaCha20 不同 AAD", got, err) got, err = chachax.DecryptX(testKey, ciphertext, nonce, nil) wantDecryptError(t, "XChaCha20 nil AAD", got, err) } // TestNilAndEmptyAADEquivalent 断言 nil AAD 与 []byte{} AAD 行为完全一致: // 用其中一个加密,用另一个也能解密,且密文逐字节相同。 func TestNilAndEmptyAADEquivalent(t *testing.T) { plaintext := []byte("nil and empty aad must behave the same") ctNil, nonceNil, err := chachax.Encrypt(testKey, plaintext, nil) if err != nil { t.Fatalf("Encrypt(nil AAD) 失败: %v", err) } ctEmpty, nonceEmpty, err := chachax.Encrypt(testKey, plaintext, []byte{}) if err != nil { t.Fatalf("Encrypt(空 AAD) 失败: %v", err) } // nonce 随机,所以用固定 nonce 的底层 API 直接比较密文更有意义: // 这里改为断言「交叉解密成功」这一可观察的等价行为。 got, err := chachax.Decrypt(testKey, ctEmpty, nonceEmpty, nil) if err != nil { t.Fatalf("空 AAD 加密 + nil AAD 解密应当成功: %v", err) } if !bytes.Equal(got, plaintext) { t.Fatal("空 AAD 加密 + nil AAD 解密结果不一致") } got, err = chachax.Decrypt(testKey, ctNil, nonceNil, []byte{}) if err != nil { t.Fatalf("nil AAD 加密 + 空 AAD 解密应当成功: %v", err) } if !bytes.Equal(got, plaintext) { t.Fatal("nil AAD 加密 + 空 AAD 解密结果不一致") } // 用同一个固定 nonce 直接比较 nil 与空 AAD 的密文,逐字节必须相同。 aead, err := chacha20poly1305.New(testKey) if err != nil { t.Fatalf("构造参考 AEAD 失败: %v", err) } fixedNonce := make([]byte, chachax.NonceSize) if _, err := rand.Read(fixedNonce); err != nil { t.Fatalf("生成固定 nonce 失败: %v", err) } if !bytes.Equal(aead.Seal(nil, fixedNonce, plaintext, nil), aead.Seal(nil, fixedNonce, plaintext, []byte{})) { t.Fatal("nil AAD 与空 AAD 的密文不相同,两者行为不等价") } // XChaCha20 同理。 xNil, xnNil, err := chachax.EncryptX(testKey, plaintext, nil) if err != nil { t.Fatalf("EncryptX(nil AAD) 失败: %v", err) } if _, err := chachax.DecryptX(testKey, xNil, xnNil, []byte{}); err != nil { t.Fatalf("XChaCha20 nil AAD 加密 + 空 AAD 解密应当成功: %v", err) } xEmpty, xnEmpty, err := chachax.EncryptX(testKey, plaintext, []byte{}) if err != nil { t.Fatalf("EncryptX(空 AAD) 失败: %v", err) } if _, err := chachax.DecryptX(testKey, xEmpty, xnEmpty, nil); err != nil { t.Fatalf("XChaCha20 空 AAD 加密 + nil AAD 解密应当成功: %v", err) } } // --------------------------------------------------------------------------- // 3. 篡改检测:翻转密文、翻转标签、截断、追加 // --------------------------------------------------------------------------- func TestTamperDetection(t *testing.T) { plaintext := fillPlaintext(96) aad := []byte("tamper-aad") ciphertext, nonce, err := chachax.Encrypt(testKey, plaintext, aad) if err != nil { t.Fatalf("Encrypt 失败: %v", err) } tagStart := len(ciphertext) - chachax.Overhead // 逐字节翻转密文主体 + 逐字节翻转标签,每一处都必须被检出。 for i := 0; i < len(ciphertext); i++ { tampered := append([]byte{}, ciphertext...) tampered[i] ^= 0x01 region := "密文主体" if i >= tagStart { region = "认证标签" } got, err := chachax.Decrypt(testKey, tampered, nonce, aad) wantDecryptError(t, "翻转 "+region+" 第 "+itoa(i)+" 字节", got, err) } // 翻转密文最后 1 字节(标签末字节)单独再确认一次。 tampered := append([]byte{}, ciphertext...) tampered[len(tampered)-1] ^= 0x80 got, err := chachax.Decrypt(testKey, tampered, nonce, aad) wantDecryptError(t, "翻转标签末字节", got, err) // 截断:从合法的空明文长度一直到只剩 1 字节都必须报错。 for cut := 1; cut <= len(ciphertext); cut++ { got, err := chachax.Decrypt(testKey, ciphertext[:len(ciphertext)-cut], nonce, aad) wantDecryptError(t, "截断 "+itoa(cut)+" 字节", got, err) } // 追加任意字节必须报错。 for _, extra := range [][]byte{{0x00}, {0xff}, []byte("extra")} { got, err := chachax.Decrypt(testKey, append(append([]byte{}, ciphertext...), extra...), nonce, aad) wantDecryptError(t, "追加 "+itoa(len(extra))+" 字节", got, err) } // 原始数据仍然可解,确认上面的篡改没有破坏基线。 if _, err := chachax.Decrypt(testKey, ciphertext, nonce, aad); err != nil { t.Fatalf("未被篡改的密文解密失败: %v", err) } } func TestTamperDetectionX(t *testing.T) { plaintext := fillPlaintext(96) aad := []byte("tamper-aad-x") ciphertext, nonce, err := chachax.EncryptX(testKey, plaintext, aad) if err != nil { t.Fatalf("EncryptX 失败: %v", err) } for i := 0; i < len(ciphertext); i++ { tampered := append([]byte{}, ciphertext...) tampered[i] ^= 0x01 got, err := chachax.DecryptX(testKey, tampered, nonce, aad) wantDecryptError(t, "XChaCha20 翻转第 "+itoa(i)+" 字节", got, err) } for cut := 1; cut <= len(ciphertext); cut++ { got, err := chachax.DecryptX(testKey, ciphertext[:len(ciphertext)-cut], nonce, aad) wantDecryptError(t, "XChaCha20 截断 "+itoa(cut)+" 字节", got, err) } got, err := chachax.DecryptX(testKey, append(append([]byte{}, ciphertext...), 0x00), nonce, aad) wantDecryptError(t, "XChaCha20 追加 1 字节", got, err) } func TestTamperDetectionSealed(t *testing.T) { plaintext := fillPlaintext(64) sealed, err := chachax.Seal(testKey, plaintext, nil) if err != nil { t.Fatalf("Seal 失败: %v", err) } for i := 0; i < len(sealed); i++ { tampered := append([]byte{}, sealed...) tampered[i] ^= 0x01 got, err := chachax.Open(testKey, tampered, nil) wantDecryptError(t, "sealed 翻转第 "+itoa(i)+" 字节", got, err) } sealedX, err := chachax.SealX(testKey, plaintext, nil) if err != nil { t.Fatalf("SealX 失败: %v", err) } for i := 0; i < len(sealedX); i++ { tampered := append([]byte{}, sealedX...) tampered[i] ^= 0x01 got, err := chachax.OpenX(testKey, tampered, nil) wantDecryptError(t, "sealedX 翻转第 "+itoa(i)+" 字节", got, err) } } // --------------------------------------------------------------------------- // 4. nonce 错误 // --------------------------------------------------------------------------- func TestNonceErrors(t *testing.T) { plaintext := []byte("nonce must be exact") ciphertext, nonce, err := chachax.Encrypt(testKey, plaintext, nil) if err != nil { t.Fatalf("Encrypt 失败: %v", err) } badNonces := map[string][]byte{ "nil nonce": nil, "空 nonce": {}, "11 字节": nonce[:11], "13 字节": append(append([]byte{}, nonce...), 0x00), "24 字节": make([]byte, chachax.NonceSizeX), "32 字节": make([]byte, 32), } for name, bad := range badNonces { got, err := chachax.Decrypt(testKey, ciphertext, bad, nil) wantDecryptError(t, "ChaCha20 错误 nonce "+name, got, err) } // nonce 被篡改(长度合法但内容不同)必须认证失败。 for i := 0; i < len(nonce); i++ { tampered := append([]byte{}, nonce...) tampered[i] ^= 0x01 got, err := chachax.Decrypt(testKey, ciphertext, tampered, nil) wantDecryptError(t, "ChaCha20 篡改 nonce 第 "+itoa(i)+" 字节", got, err) } // XChaCha20:错误长度 nonce 必须报错。 xCT, xNonce, err := chachax.EncryptX(testKey, plaintext, nil) if err != nil { t.Fatalf("EncryptX 失败: %v", err) } badXNonces := map[string][]byte{ "nil nonce": nil, "空 nonce": {}, "12 字节": make([]byte, chachax.NonceSize), "23 字节": xNonce[:23], "25 字节": append(append([]byte{}, xNonce...), 0x00), } for name, bad := range badXNonces { got, err := chachax.DecryptX(testKey, xCT, bad, nil) wantDecryptError(t, "XChaCha20 错误 nonce "+name, got, err) } for i := 0; i < len(xNonce); i++ { tampered := append([]byte{}, xNonce...) tampered[i] ^= 0x01 got, err := chachax.DecryptX(testKey, xCT, tampered, nil) wantDecryptError(t, "XChaCha20 篡改 nonce 第 "+itoa(i)+" 字节", got, err) } } // --------------------------------------------------------------------------- // 5. 密钥错误 // --------------------------------------------------------------------------- func TestKeyLengthErrors(t *testing.T) { plaintext := []byte("key length must be exact") ciphertext, nonce, err := chachax.Encrypt(testKey, plaintext, nil) if err != nil { t.Fatalf("Encrypt 失败: %v", err) } lengths := []int{0, 1, 15, 16, 24, 31, 33, 48, 64} for _, n := range lengths { if n == chachax.KeySize { continue } badKey := make([]byte, n) if _, _, err := chachax.Encrypt(badKey, plaintext, nil); err == nil { t.Fatalf("Encrypt 使用 %d 字节密钥应当报错", n) } got, err := chachax.Decrypt(badKey, ciphertext, nonce, nil) wantDecryptError(t, "Decrypt 使用 "+itoa(n)+" 字节密钥", got, err) if _, err := chachax.Seal(badKey, plaintext, nil); err == nil { t.Fatalf("Seal 使用 %d 字节密钥应当报错", n) } if _, _, err := chachax.EncryptX(badKey, plaintext, nil); err == nil { t.Fatalf("EncryptX 使用 %d 字节密钥应当报错", n) } got, err = chachax.DecryptX(badKey, ciphertext, nonce, nil) wantDecryptError(t, "DecryptX 使用 "+itoa(n)+" 字节密钥", got, err) } } func TestNilKeyDoesNotPanic(t *testing.T) { if _, _, err := chachax.Encrypt(nil, []byte("x"), nil); err == nil { t.Fatal("nil 密钥 Encrypt 应当报错") } if _, err := chachax.Decrypt(nil, make([]byte, chachax.Overhead), make([]byte, chachax.NonceSize), nil); err == nil { t.Fatal("nil 密钥 Decrypt 应当报错") } if _, err := chachax.Seal(nil, []byte("x"), nil); err == nil { t.Fatal("nil 密钥 Seal 应当报错") } if _, err := chachax.Open(nil, make([]byte, chachax.NonceSize+chachax.Overhead), nil); err == nil { t.Fatal("nil 密钥 Open 应当报错") } if _, _, err := chachax.EncryptX(nil, []byte("x"), nil); err == nil { t.Fatal("nil 密钥 EncryptX 应当报错") } if _, err := chachax.DecryptX(nil, make([]byte, chachax.Overhead), make([]byte, chachax.NonceSizeX), nil); err == nil { t.Fatal("nil 密钥 DecryptX 应当报错") } if _, err := chachax.SealX(nil, []byte("x"), nil); err == nil { t.Fatal("nil 密钥 SealX 应当报错") } if _, err := chachax.OpenX(nil, make([]byte, chachax.NonceSizeX+chachax.Overhead), nil); err == nil { t.Fatal("nil 密钥 OpenX 应当报错") } } func TestWrongKeyFails(t *testing.T) { other := make([]byte, chachax.KeySize) copy(other, testKey) other[0] ^= 0xff // 只差 1 bit 的另一条 32 字节密钥 plaintext := []byte("wrong key must not decrypt") ciphertext, nonce, err := chachax.Encrypt(testKey, plaintext, nil) if err != nil { t.Fatalf("Encrypt 失败: %v", err) } got, err := chachax.Decrypt(other, ciphertext, nonce, nil) wantDecryptError(t, "另一条 32 字节密钥", got, err) xCT, xNonce, err := chachax.EncryptX(testKey, plaintext, nil) if err != nil { t.Fatalf("EncryptX 失败: %v", err) } got, err = chachax.DecryptX(other, xCT, xNonce, nil) wantDecryptError(t, "XChaCha20 另一条 32 字节密钥", got, err) otherKey, err := chachax.GenerateKey() if err != nil { t.Fatalf("GenerateKey 失败: %v", err) } if bytes.Equal(otherKey, testKey) { t.Fatal("GenerateKey 返回了与测试密钥相同的密钥") } got, err = chachax.Decrypt(otherKey, ciphertext, nonce, nil) wantDecryptError(t, "随机生成的另一条密钥", got, err) } // --------------------------------------------------------------------------- // 6. Seal/Open 与 Encrypt/Decrypt 的格式与兼容性 // --------------------------------------------------------------------------- func TestSealFormatAndCompatibility(t *testing.T) { sizes := []int{0, 1, 31, 32, 33, 1000} for _, size := range sizes { plaintext := fillPlaintext(size) sealed, err := chachax.Seal(testKey, plaintext, nil) if err != nil { t.Fatalf("Seal 失败: %v", err) } wantLen := chachax.NonceSize + size + chachax.Overhead if len(sealed) != wantLen { t.Fatalf("Seal 输出长度 = %d,期望 NonceSize+len(明文)+Overhead = %d", len(sealed), wantLen) } got, err := chachax.Open(testKey, sealed, nil) if err != nil { t.Fatalf("Open 失败: %v", err) } if !bytes.Equal(got, plaintext) { t.Fatal("Seal/Open 往返结果不一致") } // 手工拆分 sealed,验证格式确实是 nonce ‖ 密文‖标签,且能被 Decrypt 解开。 nonce, ciphertext := sealed[:chachax.NonceSize], sealed[chachax.NonceSize:] if len(ciphertext) != size+chachax.Overhead { t.Fatalf("sealed 中密文部分长度 = %d,期望 %d", len(ciphertext), size+chachax.Overhead) } got, err = chachax.Decrypt(testKey, ciphertext, nonce, nil) if err != nil { t.Fatalf("用 Decrypt 解开 Seal 拆出的密文失败: %v", err) } if !bytes.Equal(got, plaintext) { t.Fatal("Seal 拆解后 Decrypt 结果不一致") } // 反向:Encrypt 的输出手工拼成 sealed,必须能被 Open 解开。 ct, n, err := chachax.Encrypt(testKey, plaintext, nil) if err != nil { t.Fatalf("Encrypt 失败: %v", err) } manual := append(append([]byte{}, n...), ct...) got, err = chachax.Open(testKey, manual, nil) if err != nil { t.Fatalf("Open 解开手工拼接的 sealed 失败: %v", err) } if !bytes.Equal(got, plaintext) { t.Fatal("手工拼接 sealed 的 Open 结果不一致") } } } func TestSealFormatAndCompatibilityX(t *testing.T) { sizes := []int{0, 1, 31, 32, 33, 1000} for _, size := range sizes { plaintext := fillPlaintext(size) sealed, err := chachax.SealX(testKey, plaintext, nil) if err != nil { t.Fatalf("SealX 失败: %v", err) } wantLen := chachax.NonceSizeX + size + chachax.Overhead if len(sealed) != wantLen { t.Fatalf("SealX 输出长度 = %d,期望 NonceSizeX+len(明文)+Overhead = %d", len(sealed), wantLen) } got, err := chachax.OpenX(testKey, sealed, nil) if err != nil { t.Fatalf("OpenX 失败: %v", err) } if !bytes.Equal(got, plaintext) { t.Fatal("SealX/OpenX 往返结果不一致") } nonce, ciphertext := sealed[:chachax.NonceSizeX], sealed[chachax.NonceSizeX:] if len(nonce) != 24 { t.Fatalf("sealedX 中 nonce 长度 = %d,期望 24", len(nonce)) } got, err = chachax.DecryptX(testKey, ciphertext, nonce, nil) if err != nil { t.Fatalf("用 DecryptX 解开 SealX 拆出的密文失败: %v", err) } if !bytes.Equal(got, plaintext) { t.Fatal("SealX 拆解后 DecryptX 结果不一致") } ct, n, err := chachax.EncryptX(testKey, plaintext, nil) if err != nil { t.Fatalf("EncryptX 失败: %v", err) } manual := append(append([]byte{}, n...), ct...) got, err = chachax.OpenX(testKey, manual, nil) if err != nil { t.Fatalf("OpenX 解开手工拼接的 sealed 失败: %v", err) } if !bytes.Equal(got, plaintext) { t.Fatal("手工拼接 sealedX 的 OpenX 结果不一致") } } } func TestSealOpenAADBinding(t *testing.T) { plaintext := []byte("sealed with aad") aad := []byte("ctx") sealed, err := chachax.Seal(testKey, plaintext, aad) if err != nil { t.Fatalf("Seal 失败: %v", err) } got, err := chachax.Open(testKey, sealed, aad) if err != nil { t.Fatalf("相同 AAD Open 应当成功: %v", err) } if !bytes.Equal(got, plaintext) { t.Fatal("Open 结果不一致") } bad, err := chachax.Open(testKey, sealed, []byte("ctx2")) wantDecryptError(t, "Open 不同 AAD", bad, err) sealedX, err := chachax.SealX(testKey, plaintext, aad) if err != nil { t.Fatalf("SealX 失败: %v", err) } if _, err := chachax.OpenX(testKey, sealedX, aad); err != nil { t.Fatalf("相同 AAD OpenX 应当成功: %v", err) } bad, err = chachax.OpenX(testKey, sealedX, nil) wantDecryptError(t, "OpenX 不同 AAD", bad, err) } // --------------------------------------------------------------------------- // 7. 常量与密钥生成 // --------------------------------------------------------------------------- func TestConstants(t *testing.T) { cases := []struct { name string got int want int }{ {"KeySize", chachax.KeySize, 32}, {"NonceSize", chachax.NonceSize, 12}, {"NonceSizeX", chachax.NonceSizeX, 24}, {"Overhead", chachax.Overhead, 16}, {"KeySize==x/crypto", chachax.KeySize, chacha20poly1305.KeySize}, {"NonceSize==x/crypto", chachax.NonceSize, chacha20poly1305.NonceSize}, {"NonceSizeX==x/crypto", chachax.NonceSizeX, chacha20poly1305.NonceSizeX}, {"Overhead==x/crypto", chachax.Overhead, chacha20poly1305.Overhead}, } for _, c := range cases { if c.got != c.want { t.Errorf("%s = %d,期望 %d", c.name, c.got, c.want) } } } func TestGenerateKey(t *testing.T) { seen := make(map[string]bool) for i := 0; i < 16; i++ { key, err := chachax.GenerateKey() if err != nil { t.Fatalf("GenerateKey 失败: %v", err) } if len(key) != chachax.KeySize { t.Fatalf("GenerateKey 返回 %d 字节,期望 %d", len(key), chachax.KeySize) } if seen[string(key)] { t.Fatal("GenerateKey 返回了重复的密钥") } seen[string(key)] = true // 生成的密钥必须真的可用于加解密。 plaintext := []byte("generated key must be usable") ciphertext, nonce, err := chachax.Encrypt(key, plaintext, nil) if err != nil { t.Fatalf("用生成的密钥 Encrypt 失败: %v", err) } got, err := chachax.Decrypt(key, ciphertext, nonce, nil) if err != nil { t.Fatalf("用生成的密钥 Decrypt 失败: %v", err) } if !bytes.Equal(got, plaintext) { t.Fatal("用生成的密钥往返结果不一致") } // XChaCha20 也应接受同一条密钥。 xCT, xNonce, err := chachax.EncryptX(key, plaintext, nil) if err != nil { t.Fatalf("用生成的密钥 EncryptX 失败: %v", err) } if _, err := chachax.DecryptX(key, xCT, xNonce, nil); err != nil { t.Fatalf("用生成的密钥 DecryptX 失败: %v", err) } } } func TestGenerateKeyHex(t *testing.T) { const wantLen = 2 * chachax.KeySize seen := make(map[string]bool) for i := 0; i < 16; i++ { s, err := chachax.GenerateKeyHex() if err != nil { t.Fatalf("GenerateKeyHex 失败: %v", err) } if len(s) != wantLen { t.Fatalf("GenerateKeyHex 返回长度 = %d,期望 %d", len(s), wantLen) } key, err := hex.DecodeString(s) if err != nil { t.Fatalf("GenerateKeyHex 返回的不是合法十六进制: %v", err) } if len(key) != chachax.KeySize { t.Fatalf("解码后密钥长度 = %d,期望 %d", len(key), chachax.KeySize) } if seen[s] { t.Fatal("GenerateKeyHex 返回了重复的密钥") } seen[s] = true // 解码后的字节必须可直接作为密钥使用。 plaintext := []byte("hex key round trip") ciphertext, nonce, err := chachax.Encrypt(key, plaintext, nil) if err != nil { t.Fatalf("用 hex 解码出的密钥 Encrypt 失败: %v", err) } got, err := chachax.Decrypt(key, ciphertext, nonce, nil) if err != nil { t.Fatalf("用 hex 解码出的密钥 Decrypt 失败: %v", err) } if !bytes.Equal(got, plaintext) { t.Fatal("用 hex 解码出的密钥往返结果不一致") } } } // --------------------------------------------------------------------------- // 8. 非法输入:绝不 panic,一律返回 error // --------------------------------------------------------------------------- func TestMalformedInputDoesNotPanic(t *testing.T) { // 长度边界附近的所有输入都必须安全返回。 type decryptCase struct { name string key []byte ciphertext []byte nonce []byte x bool } var cases []decryptCase for _, n := range []int{0, 1, 15, 16, 17, 27, 28} { cases = append(cases, decryptCase{ name: "ChaCha20 密文 " + itoa(n) + " 字节", key: testKey, ciphertext: make([]byte, n), nonce: make([]byte, chachax.NonceSize), }) } for _, n := range []int{0, 1, 15, 16, 17, 39, 40} { cases = append(cases, decryptCase{ name: "XChaCha20 密文 " + itoa(n) + " 字节", key: testKey, ciphertext: make([]byte, n), nonce: make([]byte, chachax.NonceSizeX), x: true, }) } for _, c := range cases { c := c t.Run(c.name, func(t *testing.T) { var err error var got []byte if c.x { got, err = chachax.DecryptX(c.key, c.ciphertext, c.nonce, nil) } else { got, err = chachax.Decrypt(c.key, c.ciphertext, c.nonce, nil) } if err == nil { t.Fatalf("畸形输入应当报错,实际成功返回 %d 字节", len(got)) } if got != nil { t.Fatalf("畸形输入失败时明文必须为 nil,实际 %d 字节", len(got)) } }) } // Open/OpenX 的超短输入必须在切片之前就被拒绝。 for _, n := range []int{0, 1, 11, 12, 27, 28} { if _, err := chachax.Open(testKey, make([]byte, n), nil); err == nil { t.Fatalf("Open 输入 %d 字节应当报错", n) } } for _, n := range []int{0, 1, 23, 24, 39, 40} { if _, err := chachax.OpenX(testKey, make([]byte, n), nil); err == nil { t.Fatalf("OpenX 输入 %d 字节应当报错", n) } } } // TestMalformedInputTable 用一张表把「nil / 空 / 超短」三类非法输入集中覆盖一遍, // 确保没有一条路径会 panic 或返回未认证明文。 func TestMalformedInputTable(t *testing.T) { emptyish := map[string][]byte{"nil": nil, "空": {}} for name, b := range emptyish { t.Run("ChaCha20 "+name+" 密文", func(t *testing.T) { got, err := chachax.Decrypt(testKey, b, make([]byte, chachax.NonceSize), nil) wantDecryptError(t, "空密文", got, err) }) t.Run("XChaCha20 "+name+" 密文", func(t *testing.T) { got, err := chachax.DecryptX(testKey, b, make([]byte, chachax.NonceSizeX), nil) wantDecryptError(t, "空密文", got, err) }) t.Run("Open "+name+" 输入", func(t *testing.T) { got, err := chachax.Open(testKey, b, nil) wantDecryptError(t, "空 sealed", got, err) }) t.Run("OpenX "+name+" 输入", func(t *testing.T) { got, err := chachax.OpenX(testKey, b, nil) wantDecryptError(t, "空 sealedX", got, err) }) } } // TestSealedShortInputBoundary 逐字节扫过 Seal/Open 与 SealX/OpenX 的长度边界, // 专门验证「先校验长度、再切片」这一步: // // nonce 开销刚好够但密文主体为空(ChaCha20 28 字节 / XChaCha20 40 字节)是可解密的 // 最小长度,短 1 字节就必须被拒绝,绝不能越界切片或 panic。 // // 这条测试能区分「本包的显式长度校验」与「底层库的认证失败」:前者是可解密的边界, // 后者在更短处就会返回错误。 func TestSealedShortInputBoundary(t *testing.T) { plaintext := []byte("boundary") sealed, err := chachax.Seal(testKey, plaintext, nil) if err != nil { t.Fatalf("Seal 失败: %v", err) } minSealed := chachax.NonceSize + chachax.Overhead if len(sealed) != minSealed+len(plaintext) { t.Fatalf("sealed 长度 = %d,期望 %d", len(sealed), minSealed+len(plaintext)) } for n := 0; n < len(sealed); n++ { got, err := chachax.Open(testKey, sealed[:n], nil) if n < minSealed { // 本包的显式长度校验负责这一段(短 1 字节即拒绝)。 wantDecryptError(t, "Open 输入 "+itoa(n)+" 字节(本包长度校验)", got, err) continue } // 长度足够、内容被截断:必须由认证失败拦下。 wantDecryptError(t, "Open 输入 "+itoa(n)+" 字节(认证失败)", got, err) } sealedX, err := chachax.SealX(testKey, plaintext, nil) if err != nil { t.Fatalf("SealX 失败: %v", err) } minSealedX := chachax.NonceSizeX + chachax.Overhead for n := 0; n < len(sealedX); n++ { if n == minSealedX { continue } got, err := chachax.OpenX(testKey, sealedX[:n], nil) if n < minSealedX { wantDecryptError(t, "OpenX 输入 "+itoa(n)+" 字节(本包长度校验)", got, err) continue } wantDecryptError(t, "OpenX 输入 "+itoa(n)+" 字节(认证失败)", got, err) } // 可解密的最小长度本身必须能正常工作:空明文。 minSealedEnc, err := chachax.Seal(testKey, nil, nil) if err != nil { t.Fatalf("Seal 空明文失败: %v", err) } if len(minSealedEnc) != minSealed { t.Fatalf("空明文的 sealed 长度 = %d,期望 %d", len(minSealedEnc), minSealed) } got, err := chachax.Open(testKey, minSealedEnc, nil) if err != nil { t.Fatalf("解开空明文 sealed 失败: %v", err) } if len(got) != 0 { t.Fatalf("空明文解密结果长度 = %d,期望 0", len(got)) } minSealedXEnc, err := chachax.SealX(testKey, nil, nil) if err != nil { t.Fatalf("SealX 空明文失败: %v", err) } if len(minSealedXEnc) != minSealedX { t.Fatalf("空明文的 sealedX 长度 = %d,期望 %d", len(minSealedXEnc), minSealedX) } got, err = chachax.OpenX(testKey, minSealedXEnc, nil) if err != nil { t.Fatalf("解开空明文 sealedX 失败: %v", err) } if len(got) != 0 { t.Fatalf("空明文 OpenX 结果长度 = %d,期望 0", len(got)) } } // --------------------------------------------------------------------------- // 9. 已知测试向量 // --------------------------------------------------------------------------- // TestKnownVectorRFC8439 使用 RFC 8439 §2.8.2 的 ChaCha20-Poly1305 AEAD 测试向量, // 验证本包与标准实现的接线正确(密钥/nonce/AAD 的传递顺序没有搞错)。 // // 向量来源:RFC 8439 "ChaCha20 and Poly1305 for IETF Protocols" 第 2.8.2 节。 // 期望值已用 golang.org/x/crypto/chacha20poly1305 独立复算核对一致 // (明文 114 字节,密文 114 字节,标签 16 字节,合计 130 字节)。 // // 明文以十六进制给出,避免源码编码影响其中的撇号等非 ASCII 字符。 func TestKnownVectorRFC8439(t *testing.T) { key := mustHex(t, "808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f") nonce := mustHex(t, "070000004041424344454647") aad := mustHex(t, "50515253c0c1c2c3c4c5c6c7") plaintext := mustHex(t, "4c616469657320616e642047656e746c656d656e206f662074686520636c617373206f66202739393a204966204920636f756c64206f6666657220796f75206f6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73637265656e20776f756c642062652069742e") wantCiphertext := mustHex(t, "d31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5a736ee62d63dbea45e8ca9671282fafb69da92728b1a71de0a9e060b2905d6a5b67ecd3b3692ddbd7f2d778b8c9803aee328091b58fab324e4fad675945585808b4831d7bc3ff4def08e4b7a9de576d26586cec64b6116") wantTag := mustHex(t, "1ae10b594f09e26a7e902ecbd0600691") if len(plaintext) != 114 { t.Fatalf("测试向量明文长度 = %d,期望 114", len(plaintext)) } if len(wantCiphertext)+len(wantTag) != len(plaintext)+chachax.Overhead { t.Fatalf("向量自检失败: 密文 %d + 标签 %d 与明文 %d + 开销 %d 不匹配", len(wantCiphertext), len(wantTag), len(plaintext), chachax.Overhead) } aead, err := chacha20poly1305.New(key) if err != nil { t.Fatalf("构造参考 AEAD 失败: %v", err) } if got := aead.Seal(nil, nonce, plaintext, aad); !bytes.Equal(got, append(append([]byte{}, wantCiphertext...), wantTag...)) { t.Fatalf("底层实现的向量自检失败,说明测试向量本身写错了\n got=%x", got) } // 1) 本包 Decrypt 必须能解开标准向量。 got, err := chachax.Decrypt(key, append(append([]byte{}, wantCiphertext...), wantTag...), nonce, aad) if err != nil { t.Fatalf("Decrypt 解开 RFC 8439 向量失败: %v", err) } if !bytes.Equal(got, plaintext) { t.Fatalf("RFC 8439 向量解密结果不一致\n got=%x\nwant=%x", got, plaintext) } // 2) 本包 Encrypt 的结果结构必须与向量一致:密文 ‖ 标签,且长度正确。 // nonce 由 Encrypt 随机生成,无法与向量逐字节比对,因此这里显式用 // 底层实现 + 固定 nonce 验证「密文 ‖ 标签」的拼接顺序与向量一致。 pkgAEAD, err := chacha20poly1305.New(key) if err != nil { t.Fatalf("构造 AEAD 失败: %v", err) } sealedFixed := pkgAEAD.Seal(nil, nonce, plaintext, aad) if len(sealedFixed) != chachax.Overhead+len(plaintext) { t.Fatalf("Seal 输出长度 = %d,期望 %d", len(sealedFixed), chachax.Overhead+len(plaintext)) } if !bytes.Equal(sealedFixed[:len(wantCiphertext)], wantCiphertext) { t.Fatalf("密文部分与向量不一致\n got=%x\nwant=%x", sealedFixed[:len(wantCiphertext)], wantCiphertext) } if !bytes.Equal(sealedFixed[len(wantCiphertext):], wantTag) { t.Fatalf("标签部分与向量不一致\n got=%x\nwant=%x", sealedFixed[len(wantCiphertext):], wantTag) } // 3) 用错 AAD 解标准向量必须失败。 badAAD, err := chachax.Decrypt(key, append(append([]byte{}, wantCiphertext...), wantTag...), nonce, []byte("wrong aad")) wantDecryptError(t, "RFC 8439 向量错误 AAD", badAAD, err) // 4) 向量密文被篡改 1 字节必须失败。 tampered := append(append([]byte{}, wantCiphertext...), wantTag...) tampered[0] ^= 0x01 bad, err := chachax.Decrypt(key, tampered, nonce, aad) wantDecryptError(t, "RFC 8439 向量篡改密文", bad, err) } // TestKnownVectorXChaCha20 使用 draft-irtf-cfrg-xchacha-01 附录 A.3.1 的 // XChaCha20-Poly1305 AEAD 测试向量。 // // 向量来源:draft-irtf-cfrg-xchacha-01 "XChaCha20-Poly1305 AEAD" A.3.1。 // 期望值已用 golang.org/x/crypto/chacha20poly1305 的 NewX 独立复算核对一致。 func TestKnownVectorXChaCha20(t *testing.T) { key := mustHex(t, "808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f") nonce := mustHex(t, "404142434445464748494a4b4c4d4e4f5051525354555657") aad := mustHex(t, "50515253c0c1c2c3c4c5c6c7") plaintext := mustHex(t, "4c616469657320616e642047656e746c656d656e206f662074686520636c617373206f66202739393a204966204920636f756c64206f6666657220796f75206f6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73637265656e20776f756c642062652069742e") wantCiphertext := mustHex(t, "bd6d179d3e83d43b9576579493c0e939572a1700252bfaccbed2902c21396cbb731c7f1b0b4aa6440bf3a82f4eda7e39ae64c6708c54c216cb96b72e1213b4522f8c9ba40db5d945b11b69b982c1bb9e3f3fac2bc369488f76b2383565d3fff921f9664c97637da9768812f615c68b13b52e") wantTag := mustHex(t, "c0875924c1c7987947deafd8780acf49") if len(nonce) != chachax.NonceSizeX { t.Fatalf("向量 nonce 长度 = %d,期望 %d", len(nonce), chachax.NonceSizeX) } aead, err := chacha20poly1305.NewX(key) if err != nil { t.Fatalf("构造参考 XChaCha20 AEAD 失败: %v", err) } if got := aead.Seal(nil, nonce, plaintext, aad); !bytes.Equal(got, append(append([]byte{}, wantCiphertext...), wantTag...)) { t.Fatalf("底层实现的 XChaCha20 向量自检失败,说明测试向量本身写错了\n got=%x", got) } got, err := chachax.DecryptX(key, append(append([]byte{}, wantCiphertext...), wantTag...), nonce, aad) if err != nil { t.Fatalf("DecryptX 解开 XChaCha20 向量失败: %v", err) } if !bytes.Equal(got, plaintext) { t.Fatalf("XChaCha20 向量解密结果不一致\n got=%x\nwant=%x", got, plaintext) } bad, err := chachax.DecryptX(key, append(append([]byte{}, wantCiphertext...), wantTag...), nonce, nil) wantDecryptError(t, "XChaCha20 向量错误 AAD", bad, err) // 用 12 字节 nonce 走 XChaCha20 通道必须被拒绝(长度精确匹配)。 bad, err = chachax.DecryptX(key, append(append([]byte{}, wantCiphertext...), wantTag...), nonce[:chachax.NonceSize], aad) wantDecryptError(t, "XChaCha20 用 12 字节 nonce", bad, err) } // TestCrossVariantIncompatible 断言 12 字节与 24 字节两套 API 不会互相误解: // 用 ChaCha20-Poly1305 加密的数据不能被 DecryptX 解开,反之亦然。 func TestCrossVariantIncompatible(t *testing.T) { plaintext := []byte("variants must not be interchangeable") ct, nonce, err := chachax.Encrypt(testKey, plaintext, nil) if err != nil { t.Fatalf("Encrypt 失败: %v", err) } // ChaCha20 的 12 字节 nonce 在 XChaCha20 下长度非法。 xNonce := make([]byte, chachax.NonceSizeX) copy(xNonce, nonce) got, err := chachax.DecryptX(testKey, ct, xNonce, nil) wantDecryptError(t, "DecryptX 解 ChaCha20 密文", got, err) xCT, xNonceReal, err := chachax.EncryptX(testKey, plaintext, nil) if err != nil { t.Fatalf("EncryptX 失败: %v", err) } got, err = chachax.Decrypt(testKey, xCT, xNonceReal[:chachax.NonceSize], nil) wantDecryptError(t, "Decrypt 解 XChaCha20 密文", got, err) } // itoa 避免在测试里反复引入 strconv,仅用于拼接子测试名。 func itoa(n int) string { if n == 0 { return "0" } var buf [20]byte i := len(buf) for n > 0 { i-- buf[i] = byte('0' + n%10) n /= 10 } return string(buf[i:]) }