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

479 lines
15 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 aesx_test
import (
"bytes"
"errors"
"testing"
"code.yun.ink/pkg/encryptx/aesx"
"code.yun.ink/pkg/encryptx/cipherx"
)
func testKey(t *testing.T, size int) []byte {
t.Helper()
key, err := aesx.GenerateKey(size)
if err != nil {
t.Fatalf("GenerateKey(%d) 失败: %v", size, err)
}
if len(key) != size {
t.Fatalf("GenerateKey(%d) 返回长度 %d", size, len(key))
}
return key
}
func TestGenerateKey(t *testing.T) {
for _, size := range []int{aesx.KeySize128, aesx.KeySize192, aesx.KeySize256} {
key := testKey(t, size)
if !aesx.ValidKeySize(len(key)) {
t.Errorf("ValidKeySize(%d) = false, 期望 true", len(key))
}
}
// 非法长度必须被拒绝
for _, size := range []int{-1, 0, 1, 15, 17, 31, 33, 64} {
if _, err := aesx.GenerateKey(size); err == nil {
t.Errorf("GenerateKey(%d) 期望错误, 得到 nil", size)
}
if aesx.ValidKeySize(size) {
t.Errorf("ValidKeySize(%d) = true, 期望 false", size)
}
}
// 两次生成的密钥不得相同
k1, _ := aesx.GenerateKey(aesx.KeySize256)
k2, _ := aesx.GenerateKey(aesx.KeySize256)
if bytes.Equal(k1, k2) {
t.Error("两次 GenerateKey 产生了相同密钥")
}
hexKey, err := aesx.GenerateKeyHex(aesx.KeySize256)
if err != nil {
t.Fatalf("GenerateKeyHex 失败: %v", err)
}
if len(hexKey) != 64 {
t.Errorf("GenerateKeyHex(32) 长度 = %d, 期望 64", len(hexKey))
}
if _, err := aesx.GenerateKeyHex(7); err == nil {
t.Error("GenerateKeyHex(7) 期望错误")
}
}
// TestKeySizeValidation 是 P0-2 的回归测试。
//
// 旧实现的 GenerateKey 返回十六进制字符串(32 字节 → 64 字符),而加密函数把
// 入参直接当原始密钥字节用,导致「生成密钥 → 加密」这条最基本的路径必然失败
// (invalid key size 64)。现在密钥统一为 []byte,两者天然一致。
func TestKeySizeValidation(t *testing.T) {
plaintext := []byte("hello world")
for _, wrongLen := range []int{5, 15, 17, 31, 33, 64} {
key := bytes.Repeat([]byte{0x01}, wrongLen)
if _, err := aesx.EncryptCBC(plaintext, key); !errors.Is(err, cipherx.ErrInvalidKeySize) {
t.Errorf("密钥长度 %d: 期望 ErrInvalidKeySize, 得到 %v", wrongLen, err)
}
}
// GenerateKey 的返回值必须能直接用于加密(旧实现做不到)
key, err := aesx.GenerateKey(aesx.KeySize256)
if err != nil {
t.Fatalf("GenerateKey 失败: %v", err)
}
ciphertext, err := aesx.EncryptCBC(plaintext, key)
if err != nil {
t.Fatalf("用 GenerateKey 生成的密钥加密失败: %v", err)
}
decrypted, err := aesx.DecryptCBC(ciphertext, key)
if err != nil {
t.Fatalf("解密失败: %v", err)
}
if !bytes.Equal(decrypted, plaintext) {
t.Errorf("往返不一致: 得到 %q, 期望 %q", decrypted, plaintext)
}
}
func TestAllModesRoundTrip(t *testing.T) {
key := testKey(t, aesx.KeySize256)
encryptors := map[string]func([]byte, []byte) ([]byte, error){
"ECB": aesx.EncryptECB, "CBC": aesx.EncryptCBC, "CFB": aesx.EncryptCFB,
"OFB": aesx.EncryptOFB, "CTR": aesx.EncryptCTR,
}
decryptors := map[string]func([]byte, []byte) ([]byte, error){
"ECB": aesx.DecryptECB, "CBC": aesx.DecryptCBC, "CFB": aesx.DecryptCFB,
"OFB": aesx.DecryptOFB, "CTR": aesx.DecryptCTR,
}
plaintexts := [][]byte{
{}, []byte("a"), []byte("0123456789abcde"), []byte("0123456789abcdef"),
[]byte("0123456789abcdefg"), bytes.Repeat([]byte("AES"), 500),
}
for name, encrypt := range encryptors {
for i, plaintext := range plaintexts {
ciphertext, err := encrypt(plaintext, key)
if err != nil {
t.Fatalf("%s 明文#%d 加密失败: %v", name, i, err)
}
decrypted, err := decryptors[name](ciphertext, key)
if err != nil {
t.Fatalf("%s 明文#%d 解密失败: %v", name, i, err)
}
if !bytes.Equal(decrypted, plaintext) {
t.Errorf("%s 明文#%d 往返不一致", name, i)
}
}
}
}
func TestHexAPIRoundTrip(t *testing.T) {
key := testKey(t, aesx.KeySize128)
plaintext := []byte("十六进制 API 往返测试")
cases := []struct {
name string
encrypt func([]byte, []byte) (string, error)
decrypt func(string, []byte) ([]byte, error)
}{
{"ECB", aesx.EncryptECBHex, aesx.DecryptECBHex},
{"CBC", aesx.EncryptCBCHex, aesx.DecryptCBCHex},
{"CFB", aesx.EncryptCFBHex, aesx.DecryptCFBHex},
{"OFB", aesx.EncryptOFBHex, aesx.DecryptOFBHex},
{"CTR", aesx.EncryptCTRHex, aesx.DecryptCTRHex},
}
for _, c := range cases {
hexStr, err := c.encrypt(plaintext, key)
if err != nil {
t.Fatalf("%s Hex 加密失败: %v", c.name, err)
}
// 输出必须是大写十六进制,且不含换行符(旧实现会带上 \n)
if bytes.ContainsAny([]byte(hexStr), "\n\r \t") {
t.Errorf("%s Hex 输出含空白字符: %q", c.name, hexStr)
}
for _, r := range hexStr {
if (r < '0' || r > '9') && (r < 'A' || r > 'F') {
t.Fatalf("%s Hex 输出含非大写十六进制字符: %q", c.name, hexStr)
}
}
decrypted, err := c.decrypt(hexStr, key)
if err != nil {
t.Fatalf("%s Hex 解密失败: %v", c.name, err)
}
if !bytes.Equal(decrypted, plaintext) {
t.Errorf("%s Hex 往返不一致", c.name)
}
}
}
func TestWithIVRoundTrip(t *testing.T) {
key := testKey(t, aesx.KeySize256)
iv := bytes.Repeat([]byte{0x5A}, aesx.BlockSize)
plaintext := []byte("fixed iv interop")
cases := []struct {
name string
encrypt func([]byte, []byte, []byte) ([]byte, error)
decrypt func([]byte, []byte, []byte) ([]byte, error)
}{
{"CBC", aesx.EncryptCBCWithIV, aesx.DecryptCBCWithIV},
{"CFB", aesx.EncryptCFBWithIV, aesx.DecryptCFBWithIV},
{"OFB", aesx.EncryptOFBWithIV, aesx.DecryptOFBWithIV},
{"CTR", aesx.EncryptCTRWithIV, aesx.DecryptCTRWithIV},
}
for _, c := range cases {
ciphertext, err := c.encrypt(plaintext, key, iv)
if err != nil {
t.Fatalf("%s WithIV 加密失败: %v", c.name, err)
}
decrypted, err := c.decrypt(ciphertext, key, iv)
if err != nil {
t.Fatalf("%s WithIV 解密失败: %v", c.name, err)
}
if !bytes.Equal(decrypted, plaintext) {
t.Errorf("%s WithIV 往返不一致", c.name)
}
// 错误 IV 下,CBC 通常会因填充校验失败报错,流式模式则会静默返回垃圾
// 明文(这正是这些模式不提供完整性保护的体现)。无论哪种情况,都绝不
// 允许还原出正确明文。
wrongIV, err := c.decrypt(ciphertext, key, bytes.Repeat([]byte{0x00}, aesx.BlockSize))
if err == nil && bytes.Equal(wrongIV, plaintext) {
t.Errorf("%s 使用错误 IV 却解出了正确明文", c.name)
}
}
}
// TestECBIsDeterministic 记录 ECB 的固有弱点(相同明文块 → 相同密文块),
// 与 CBC 对比以证明文档中的安全警告属实。
func TestECBIsDeterministic(t *testing.T) {
key := testKey(t, aesx.KeySize256)
plaintext := bytes.Repeat([]byte("0123456789abcdef"), 4)
ecb1, err := aesx.EncryptECB(plaintext, key)
if err != nil {
t.Fatalf("ECB 加密失败: %v", err)
}
ecb2, err := aesx.EncryptECB(plaintext, key)
if err != nil {
t.Fatalf("ECB 加密失败: %v", err)
}
if !bytes.Equal(ecb1, ecb2) {
t.Error("ECB 应当是确定性的(这正是它的弱点)")
}
cbc1, err := aesx.EncryptCBC(plaintext, key)
if err != nil {
t.Fatalf("CBC 加密失败: %v", err)
}
cbc2, err := aesx.EncryptCBC(plaintext, key)
if err != nil {
t.Fatalf("CBC 加密失败: %v", err)
}
if bytes.Equal(cbc1, cbc2) {
t.Error("CBC 使用了随机 IV,两次加密结果不应相同")
}
}
func TestMalformedCiphertextNeverPanics(t *testing.T) {
key := testKey(t, aesx.KeySize256)
decryptors := map[string]func([]byte, []byte) ([]byte, error){
"ECB": aesx.DecryptECB, "CBC": aesx.DecryptCBC, "CFB": aesx.DecryptCFB,
"OFB": aesx.DecryptOFB, "CTR": aesx.DecryptCTR,
}
for _, decrypt := range decryptors {
for n := 0; n <= 40; n++ {
// 不应 panic
_, _ = decrypt(bytes.Repeat([]byte{0x7F}, n), key)
}
}
// ECB / CBC 的非对齐密文必须返回 ErrNotBlockAligned
nonAligned := bytes.Repeat([]byte{0xAB}, 20)
if _, err := aesx.DecryptECB(nonAligned, key); !errors.Is(err, cipherx.ErrNotBlockAligned) {
t.Errorf("ECB 非对齐: 期望 ErrNotBlockAligned, 得到 %v", err)
}
if _, err := aesx.DecryptCBC(nonAligned, key); !errors.Is(err, cipherx.ErrNotBlockAligned) {
t.Errorf("CBC 非对齐: 期望 ErrNotBlockAligned, 得到 %v", err)
}
}
// ---- GCM ----
func TestGCMRoundTrip(t *testing.T) {
key := testKey(t, aesx.KeySize256)
plaintexts := [][]byte{
{}, []byte("a"), []byte("GCM 往返测试"), bytes.Repeat([]byte("x"), 100000),
}
for i, plaintext := range plaintexts {
ciphertext, nonce, err := aesx.EncryptGCM(plaintext, key, nil)
if err != nil {
t.Fatalf("GCM 加密#%d 失败: %v", i, err)
}
if len(nonce) != aesx.GCMNonceSize {
t.Fatalf("nonce 长度 = %d, 期望 %d", len(nonce), aesx.GCMNonceSize)
}
// 密文应等于明文长度 + 认证标签
if len(ciphertext) != len(plaintext)+aesx.GCMTagSize {
t.Errorf("密文长度 = %d, 期望 %d", len(ciphertext), len(plaintext)+aesx.GCMTagSize)
}
decrypted, err := aesx.DecryptGCM(ciphertext, key, nonce, nil)
if err != nil {
t.Fatalf("GCM 解密#%d 失败: %v", i, err)
}
if !bytes.Equal(decrypted, plaintext) {
t.Errorf("GCM 往返#%d 不一致", i)
}
}
}
func TestGCMNonceIsRandom(t *testing.T) {
key := testKey(t, aesx.KeySize256)
seen := make(map[string]bool)
for i := 0; i < 64; i++ {
_, nonce, err := aesx.EncryptGCM([]byte("same"), key, nil)
if err != nil {
t.Fatalf("GCM 加密失败: %v", err)
}
if seen[string(nonce)] {
t.Fatal("GCM nonce 重复,会导致灾难性的密钥恢复攻击")
}
seen[string(nonce)] = true
}
}
func TestGCMBindsAAD(t *testing.T) {
key := testKey(t, aesx.KeySize256)
plaintext := []byte("authenticated data test")
aad := []byte("v1|user=42")
ciphertext, nonce, err := aesx.EncryptGCM(plaintext, key, aad)
if err != nil {
t.Fatalf("GCM 加密失败: %v", err)
}
// 正确的 AAD 可解密
if _, err := aesx.DecryptGCM(ciphertext, key, nonce, aad); err != nil {
t.Fatalf("正确 AAD 解密失败: %v", err)
}
// 不同的 AAD 必须失败
if _, err := aesx.DecryptGCM(ciphertext, key, nonce, []byte("v1|user=43")); !errors.Is(err, aesx.ErrAuthenticationFailed) {
t.Errorf("错误 AAD: 期望 ErrAuthenticationFailed, 得到 %v", err)
}
// 缺少 AAD 必须失败
if _, err := aesx.DecryptGCM(ciphertext, key, nonce, nil); !errors.Is(err, aesx.ErrAuthenticationFailed) {
t.Errorf("缺失 AAD: 期望 ErrAuthenticationFailed, 得到 %v", err)
}
}
// TestGCMTamperDetection 验证任何一处篡改都会导致解密失败,且不返回明文。
func TestGCMTamperDetection(t *testing.T) {
key := testKey(t, aesx.KeySize256)
plaintext := []byte("tamper detection")
ciphertext, nonce, err := aesx.EncryptGCM(plaintext, key, nil)
if err != nil {
t.Fatalf("GCM 加密失败: %v", err)
}
// 翻转密文(含标签)中每一个字节
for i := range ciphertext {
tampered := bytes.Clone(ciphertext)
tampered[i] ^= 0x01
got, err := aesx.DecryptGCM(tampered, key, nonce, nil)
if !errors.Is(err, aesx.ErrAuthenticationFailed) {
t.Errorf("篡改密文第 %d 字节: 期望认证失败, 得到 %v", i, err)
}
if got != nil {
t.Errorf("篡改密文第 %d 字节: 认证失败却返回了明文 %q", i, got)
}
}
// 翻转 nonce 中每一个字节
for i := range nonce {
tampered := bytes.Clone(nonce)
tampered[i] ^= 0x01
if _, err := aesx.DecryptGCM(ciphertext, key, tampered, nil); !errors.Is(err, aesx.ErrAuthenticationFailed) {
t.Errorf("篡改 nonce 第 %d 字节: 期望认证失败, 得到 %v", i, err)
}
}
// 错误密钥
wrongKey := testKey(t, aesx.KeySize256)
if _, err := aesx.DecryptGCM(ciphertext, wrongKey, nonce, nil); !errors.Is(err, aesx.ErrAuthenticationFailed) {
t.Errorf("错误密钥: 期望认证失败, 得到 %v", err)
}
}
func TestGCMInvalidInputs(t *testing.T) {
key := testKey(t, aesx.KeySize256)
ciphertext, nonce, err := aesx.EncryptGCM([]byte("data"), key, nil)
if err != nil {
t.Fatalf("GCM 加密失败: %v", err)
}
// nonce 长度错误
for _, badNonce := range [][]byte{nil, {}, bytes.Repeat([]byte{1}, 8), bytes.Repeat([]byte{1}, 16)} {
if _, err := aesx.DecryptGCM(ciphertext, key, badNonce, nil); !errors.Is(err, aesx.ErrInvalidNonceSize) {
t.Errorf("nonce 长度 %d: 期望 ErrInvalidNonceSize, 得到 %v", len(badNonce), err)
}
}
// 密文短于认证标签
if _, err := aesx.DecryptGCM(bytes.Repeat([]byte{1}, aesx.GCMTagSize-1), key, nonce, nil); !errors.Is(err, aesx.ErrCiphertextTooShort) {
t.Errorf("超短密文: 期望 ErrCiphertextTooShort, 得到 %v", err)
}
// 空密文
if _, err := aesx.DecryptGCM(nil, key, nonce, nil); !errors.Is(err, aesx.ErrCiphertextTooShort) {
t.Errorf("空密文: 期望 ErrCiphertextTooShort, 得到 %v", err)
}
// 密钥长度错误
if _, err := aesx.DecryptGCM(ciphertext, []byte("short"), nonce, nil); !errors.Is(err, cipherx.ErrInvalidKeySize) {
t.Errorf("错误密钥长度: 期望 ErrInvalidKeySize, 得到 %v", err)
}
}
func TestSealOpenGCM(t *testing.T) {
key := testKey(t, aesx.KeySize128)
plaintext := []byte("single field storage")
aad := []byte("ctx")
sealed, err := aesx.SealGCM(plaintext, key, aad)
if err != nil {
t.Fatalf("SealGCM 失败: %v", err)
}
if len(sealed) != aesx.GCMNonceSize+len(plaintext)+aesx.GCMTagSize {
t.Errorf("SealGCM 长度 = %d, 期望 %d", len(sealed), aesx.GCMNonceSize+len(plaintext)+aesx.GCMTagSize)
}
opened, err := aesx.OpenGCM(sealed, key, aad)
if err != nil {
t.Fatalf("OpenGCM 失败: %v", err)
}
if !bytes.Equal(opened, plaintext) {
t.Errorf("Seal/Open 往返不一致")
}
// AAD 不匹配
if _, err := aesx.OpenGCM(sealed, key, []byte("other")); !errors.Is(err, aesx.ErrAuthenticationFailed) {
t.Errorf("错误 AAD: 期望认证失败, 得到 %v", err)
}
// 数据过短
for n := 0; n < aesx.GCMNonceSize+aesx.GCMTagSize; n++ {
if _, err := aesx.OpenGCM(bytes.Repeat([]byte{0x01}, n), key, aad); err == nil {
t.Errorf("长度 %d 的数据: 期望错误, 得到 nil", n)
}
}
// 篡改
tampered := bytes.Clone(sealed)
tampered[len(tampered)-1] ^= 0x01
if _, err := aesx.OpenGCM(tampered, key, aad); !errors.Is(err, aesx.ErrAuthenticationFailed) {
t.Errorf("篡改数据: 期望认证失败, 得到 %v", err)
}
}
func TestGCMHexRoundTrip(t *testing.T) {
key := testKey(t, aesx.KeySize256)
plaintext := []byte("hex gcm")
ciphertextHex, nonceHex, err := aesx.EncryptGCMHex(plaintext, key, nil)
if err != nil {
t.Fatalf("EncryptGCMHex 失败: %v", err)
}
decrypted, err := aesx.DecryptGCMHex(ciphertextHex, nonceHex, key, nil)
if err != nil {
t.Fatalf("DecryptGCMHex 失败: %v", err)
}
if !bytes.Equal(decrypted, plaintext) {
t.Error("GCM Hex 往返不一致")
}
if _, err := aesx.DecryptGCMHex("not hex", nonceHex, key, nil); err == nil {
t.Error("非法密文十六进制: 期望错误")
}
if _, err := aesx.DecryptGCMHex(ciphertextHex, "not hex", key, nil); err == nil {
t.Error("非法 nonce 十六进制: 期望错误")
}
}
func TestGCMWithNonceInterop(t *testing.T) {
key := testKey(t, aesx.KeySize256)
nonce := bytes.Repeat([]byte{0x42}, aesx.GCMNonceSize)
plaintext := []byte("interop")
ciphertext, err := aesx.EncryptGCMWithNonce(plaintext, key, nonce, nil)
if err != nil {
t.Fatalf("EncryptGCMWithNonce 失败: %v", err)
}
decrypted, err := aesx.DecryptGCM(ciphertext, key, nonce, nil)
if err != nil {
t.Fatalf("解密失败: %v", err)
}
if !bytes.Equal(decrypted, plaintext) {
t.Error("往返不一致")
}
if _, err := aesx.EncryptGCMWithNonce(plaintext, key, []byte("bad"), nil); !errors.Is(err, aesx.ErrInvalidNonceSize) {
t.Errorf("非法 nonce 长度: 期望 ErrInvalidNonceSize, 得到 %v", err)
}
}