1074 lines
38 KiB
Go
1074 lines
38 KiB
Go
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:])
|
||
|
|
}
|