| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219 |
- //go:build unit
- package repository
- import (
- "encoding/base64"
- "encoding/hex"
- "strings"
- "testing"
- "github.com/Wei-Shaw/sub2api/internal/config"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- )
- // ── 测试辅助 ─────────────────────────────────────────────────────────────────
- // aesHexKey 构造一个全填充为 b 的 n 字节密钥并以 hex 编码返回。
- func aesHexKey(n int, b byte) string {
- raw := make([]byte, n)
- for i := range raw {
- raw[i] = b
- }
- return hex.EncodeToString(raw)
- }
- // aesTestCfg 用给定 hex 密钥字符串构造最小 Config。
- func aesTestCfg(keyHex string) *config.Config {
- return &config.Config{
- Totp: config.TotpConfig{EncryptionKey: keyHex},
- }
- }
- // aesEncryptor 创建一个持有合法 32 字节密钥的加密器,测试失败时立即终止。
- func aesEncryptor(t *testing.T) *AESEncryptor {
- t.Helper()
- enc, err := NewAESEncryptor(aesTestCfg(aesHexKey(32, 0x42)))
- require.NoError(t, err)
- require.NotNil(t, enc)
- return enc.(*AESEncryptor)
- }
- // ── NewAESEncryptor ──────────────────────────────────────────────────────────
- func TestNewAESEncryptor_ValidKey32Bytes(t *testing.T) {
- enc, err := NewAESEncryptor(aesTestCfg(aesHexKey(32, 0x01)))
- require.NoError(t, err)
- require.NotNil(t, enc)
- }
- // 16 / 24 字节密钥在 AES 体系内合法,但本实现仅接受 AES-256(32 字节)。
- func TestNewAESEncryptor_WrongKeyLength(t *testing.T) {
- tests := []struct {
- name string
- keySize int
- }{
- {"16_bytes_AES128", 16},
- {"24_bytes_AES192", 24},
- {"1_byte", 1},
- {"31_bytes", 31},
- {"33_bytes", 33},
- {"64_bytes", 64},
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- _, err := NewAESEncryptor(aesTestCfg(aesHexKey(tt.keySize, 0x00)))
- require.Error(t, err)
- assert.Contains(t, err.Error(), "32 bytes")
- })
- }
- }
- // "配置缺失"场景:空字符串与非法 hex 编码。
- func TestNewAESEncryptor_MissingOrInvalidConfig(t *testing.T) {
- tests := []struct {
- name string
- keyHex string
- wantContain string
- }{
- {"empty_key", "", "32 bytes"},
- {"invalid_hex_odd_length", "abcde", "invalid totp encryption key"},
- {"invalid_hex_chars", "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", "invalid totp encryption key"},
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- _, err := NewAESEncryptor(aesTestCfg(tt.keyHex))
- require.Error(t, err)
- assert.Contains(t, err.Error(), tt.wantContain)
- })
- }
- }
- // ── 加解密往返(Roundtrip)───────────────────────────────────────────────────
- func TestAESEncryptor_RoundTrip(t *testing.T) {
- enc := aesEncryptor(t)
- tests := []struct {
- name string
- plaintext string
- }{
- {"ascii", "Hello, Sub2API!"},
- {"chinese_multibyte", "你好,世界!这是多字节 UTF-8 文本。"},
- {"empty_string", ""},
- {"long_string_gt_1KB", strings.Repeat("x", 2048)},
- {"special_chars", "!@#$%^&*()_+-=[]{}|;':\",./<>?"},
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- ct, err := enc.Encrypt(tt.plaintext)
- require.NoError(t, err)
- require.NotEmpty(t, ct, "密文不应为空(即便明文为空字符串)")
- got, err := enc.Decrypt(ct)
- require.NoError(t, err)
- assert.Equal(t, tt.plaintext, got)
- })
- }
- }
- // ── IV/Nonce 随机性 ──────────────────────────────────────────────────────────
- func TestAESEncryptor_Encrypt_NonceRandomness(t *testing.T) {
- enc := aesEncryptor(t)
- const iterations = 30
- plaintext := "same plaintext for every iteration"
- seen := make(map[string]struct{}, iterations)
- for i := 0; i < iterations; i++ {
- ct, err := enc.Encrypt(plaintext)
- require.NoError(t, err)
- seen[ct] = struct{}{}
- }
- // 30 次加密相同明文,每次因随机 Nonce 应产生不同密文。
- assert.Len(t, seen, iterations,
- "每次加密应因随机 Nonce 产生唯一密文,共 %d 次", iterations)
- }
- // ── Decrypt 错误路径 ──────────────────────────────────────────────────────────
- func TestAESDecrypt_InvalidBase64(t *testing.T) {
- enc := aesEncryptor(t)
- _, err := enc.Decrypt("!!!not-valid-base64!!!")
- require.Error(t, err)
- assert.Contains(t, err.Error(), "decode base64")
- }
- func TestAESDecrypt_TooShort(t *testing.T) {
- enc := aesEncryptor(t)
- // GCM Nonce 为 12 字节;仅提供 2 字节,必然短于 NonceSize。
- short := base64.StdEncoding.EncodeToString([]byte{0x01, 0x02})
- _, err := enc.Decrypt(short)
- require.Error(t, err)
- assert.Contains(t, err.Error(), "too short")
- }
- func TestAESDecrypt_TamperedCiphertext(t *testing.T) {
- enc := aesEncryptor(t)
- ct, err := enc.Encrypt("sensitive payload")
- require.NoError(t, err)
- raw, err := base64.StdEncoding.DecodeString(ct)
- require.NoError(t, err)
- // Nonce 占前 12 字节;翻转其后第一个字节(密文体)。
- raw[12] ^= 0xFF
- _, err = enc.Decrypt(base64.StdEncoding.EncodeToString(raw))
- require.Error(t, err, "篡改密文体后解密应失败")
- }
- func TestAESDecrypt_TamperedTag(t *testing.T) {
- enc := aesEncryptor(t)
- ct, err := enc.Encrypt("sensitive payload")
- require.NoError(t, err)
- raw, err := base64.StdEncoding.DecodeString(ct)
- require.NoError(t, err)
- // GCM 认证标签占最后 16 字节;翻转最后一个字节。
- raw[len(raw)-1] ^= 0xFF
- _, err = enc.Decrypt(base64.StdEncoding.EncodeToString(raw))
- require.Error(t, err, "篡改 GCM 标签后解密应失败")
- }
- // ── 跨实例(Cross-instance)──────────────────────────────────────────────────
- func TestAESEncryptor_CrossInstance_SameKey_CanDecrypt(t *testing.T) {
- keyHex := aesHexKey(32, 0xDE)
- enc1, err := NewAESEncryptor(aesTestCfg(keyHex))
- require.NoError(t, err)
- enc2, err := NewAESEncryptor(aesTestCfg(keyHex))
- require.NoError(t, err)
- plaintext := "cross-instance roundtrip"
- ct, err := enc1.Encrypt(plaintext)
- require.NoError(t, err)
- got, err := enc2.Decrypt(ct)
- require.NoError(t, err)
- assert.Equal(t, plaintext, got, "相同密钥构造的两个实例应可互相解密")
- }
- func TestAESEncryptor_CrossInstance_DifferentKey_CannotDecrypt(t *testing.T) {
- enc1, err := NewAESEncryptor(aesTestCfg(aesHexKey(32, 0xAA)))
- require.NoError(t, err)
- enc2, err := NewAESEncryptor(aesTestCfg(aesHexKey(32, 0xBB)))
- require.NoError(t, err)
- ct, err := enc1.Encrypt("secret message")
- require.NoError(t, err)
- _, err = enc2.Decrypt(ct)
- require.Error(t, err, "不同密钥的实例不应能解密对方的密文")
- }
|