aes_encryptor_test.go 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. //go:build unit
  2. package repository
  3. import (
  4. "encoding/base64"
  5. "encoding/hex"
  6. "strings"
  7. "testing"
  8. "github.com/Wei-Shaw/sub2api/internal/config"
  9. "github.com/stretchr/testify/assert"
  10. "github.com/stretchr/testify/require"
  11. )
  12. // ── 测试辅助 ─────────────────────────────────────────────────────────────────
  13. // aesHexKey 构造一个全填充为 b 的 n 字节密钥并以 hex 编码返回。
  14. func aesHexKey(n int, b byte) string {
  15. raw := make([]byte, n)
  16. for i := range raw {
  17. raw[i] = b
  18. }
  19. return hex.EncodeToString(raw)
  20. }
  21. // aesTestCfg 用给定 hex 密钥字符串构造最小 Config。
  22. func aesTestCfg(keyHex string) *config.Config {
  23. return &config.Config{
  24. Totp: config.TotpConfig{EncryptionKey: keyHex},
  25. }
  26. }
  27. // aesEncryptor 创建一个持有合法 32 字节密钥的加密器,测试失败时立即终止。
  28. func aesEncryptor(t *testing.T) *AESEncryptor {
  29. t.Helper()
  30. enc, err := NewAESEncryptor(aesTestCfg(aesHexKey(32, 0x42)))
  31. require.NoError(t, err)
  32. require.NotNil(t, enc)
  33. return enc.(*AESEncryptor)
  34. }
  35. // ── NewAESEncryptor ──────────────────────────────────────────────────────────
  36. func TestNewAESEncryptor_ValidKey32Bytes(t *testing.T) {
  37. enc, err := NewAESEncryptor(aesTestCfg(aesHexKey(32, 0x01)))
  38. require.NoError(t, err)
  39. require.NotNil(t, enc)
  40. }
  41. // 16 / 24 字节密钥在 AES 体系内合法,但本实现仅接受 AES-256(32 字节)。
  42. func TestNewAESEncryptor_WrongKeyLength(t *testing.T) {
  43. tests := []struct {
  44. name string
  45. keySize int
  46. }{
  47. {"16_bytes_AES128", 16},
  48. {"24_bytes_AES192", 24},
  49. {"1_byte", 1},
  50. {"31_bytes", 31},
  51. {"33_bytes", 33},
  52. {"64_bytes", 64},
  53. }
  54. for _, tt := range tests {
  55. t.Run(tt.name, func(t *testing.T) {
  56. _, err := NewAESEncryptor(aesTestCfg(aesHexKey(tt.keySize, 0x00)))
  57. require.Error(t, err)
  58. assert.Contains(t, err.Error(), "32 bytes")
  59. })
  60. }
  61. }
  62. // "配置缺失"场景:空字符串与非法 hex 编码。
  63. func TestNewAESEncryptor_MissingOrInvalidConfig(t *testing.T) {
  64. tests := []struct {
  65. name string
  66. keyHex string
  67. wantContain string
  68. }{
  69. {"empty_key", "", "32 bytes"},
  70. {"invalid_hex_odd_length", "abcde", "invalid totp encryption key"},
  71. {"invalid_hex_chars", "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", "invalid totp encryption key"},
  72. }
  73. for _, tt := range tests {
  74. t.Run(tt.name, func(t *testing.T) {
  75. _, err := NewAESEncryptor(aesTestCfg(tt.keyHex))
  76. require.Error(t, err)
  77. assert.Contains(t, err.Error(), tt.wantContain)
  78. })
  79. }
  80. }
  81. // ── 加解密往返(Roundtrip)───────────────────────────────────────────────────
  82. func TestAESEncryptor_RoundTrip(t *testing.T) {
  83. enc := aesEncryptor(t)
  84. tests := []struct {
  85. name string
  86. plaintext string
  87. }{
  88. {"ascii", "Hello, Sub2API!"},
  89. {"chinese_multibyte", "你好,世界!这是多字节 UTF-8 文本。"},
  90. {"empty_string", ""},
  91. {"long_string_gt_1KB", strings.Repeat("x", 2048)},
  92. {"special_chars", "!@#$%^&*()_+-=[]{}|;':\",./<>?"},
  93. }
  94. for _, tt := range tests {
  95. t.Run(tt.name, func(t *testing.T) {
  96. ct, err := enc.Encrypt(tt.plaintext)
  97. require.NoError(t, err)
  98. require.NotEmpty(t, ct, "密文不应为空(即便明文为空字符串)")
  99. got, err := enc.Decrypt(ct)
  100. require.NoError(t, err)
  101. assert.Equal(t, tt.plaintext, got)
  102. })
  103. }
  104. }
  105. // ── IV/Nonce 随机性 ──────────────────────────────────────────────────────────
  106. func TestAESEncryptor_Encrypt_NonceRandomness(t *testing.T) {
  107. enc := aesEncryptor(t)
  108. const iterations = 30
  109. plaintext := "same plaintext for every iteration"
  110. seen := make(map[string]struct{}, iterations)
  111. for i := 0; i < iterations; i++ {
  112. ct, err := enc.Encrypt(plaintext)
  113. require.NoError(t, err)
  114. seen[ct] = struct{}{}
  115. }
  116. // 30 次加密相同明文,每次因随机 Nonce 应产生不同密文。
  117. assert.Len(t, seen, iterations,
  118. "每次加密应因随机 Nonce 产生唯一密文,共 %d 次", iterations)
  119. }
  120. // ── Decrypt 错误路径 ──────────────────────────────────────────────────────────
  121. func TestAESDecrypt_InvalidBase64(t *testing.T) {
  122. enc := aesEncryptor(t)
  123. _, err := enc.Decrypt("!!!not-valid-base64!!!")
  124. require.Error(t, err)
  125. assert.Contains(t, err.Error(), "decode base64")
  126. }
  127. func TestAESDecrypt_TooShort(t *testing.T) {
  128. enc := aesEncryptor(t)
  129. // GCM Nonce 为 12 字节;仅提供 2 字节,必然短于 NonceSize。
  130. short := base64.StdEncoding.EncodeToString([]byte{0x01, 0x02})
  131. _, err := enc.Decrypt(short)
  132. require.Error(t, err)
  133. assert.Contains(t, err.Error(), "too short")
  134. }
  135. func TestAESDecrypt_TamperedCiphertext(t *testing.T) {
  136. enc := aesEncryptor(t)
  137. ct, err := enc.Encrypt("sensitive payload")
  138. require.NoError(t, err)
  139. raw, err := base64.StdEncoding.DecodeString(ct)
  140. require.NoError(t, err)
  141. // Nonce 占前 12 字节;翻转其后第一个字节(密文体)。
  142. raw[12] ^= 0xFF
  143. _, err = enc.Decrypt(base64.StdEncoding.EncodeToString(raw))
  144. require.Error(t, err, "篡改密文体后解密应失败")
  145. }
  146. func TestAESDecrypt_TamperedTag(t *testing.T) {
  147. enc := aesEncryptor(t)
  148. ct, err := enc.Encrypt("sensitive payload")
  149. require.NoError(t, err)
  150. raw, err := base64.StdEncoding.DecodeString(ct)
  151. require.NoError(t, err)
  152. // GCM 认证标签占最后 16 字节;翻转最后一个字节。
  153. raw[len(raw)-1] ^= 0xFF
  154. _, err = enc.Decrypt(base64.StdEncoding.EncodeToString(raw))
  155. require.Error(t, err, "篡改 GCM 标签后解密应失败")
  156. }
  157. // ── 跨实例(Cross-instance)──────────────────────────────────────────────────
  158. func TestAESEncryptor_CrossInstance_SameKey_CanDecrypt(t *testing.T) {
  159. keyHex := aesHexKey(32, 0xDE)
  160. enc1, err := NewAESEncryptor(aesTestCfg(keyHex))
  161. require.NoError(t, err)
  162. enc2, err := NewAESEncryptor(aesTestCfg(keyHex))
  163. require.NoError(t, err)
  164. plaintext := "cross-instance roundtrip"
  165. ct, err := enc1.Encrypt(plaintext)
  166. require.NoError(t, err)
  167. got, err := enc2.Decrypt(ct)
  168. require.NoError(t, err)
  169. assert.Equal(t, plaintext, got, "相同密钥构造的两个实例应可互相解密")
  170. }
  171. func TestAESEncryptor_CrossInstance_DifferentKey_CannotDecrypt(t *testing.T) {
  172. enc1, err := NewAESEncryptor(aesTestCfg(aesHexKey(32, 0xAA)))
  173. require.NoError(t, err)
  174. enc2, err := NewAESEncryptor(aesTestCfg(aesHexKey(32, 0xBB)))
  175. require.NoError(t, err)
  176. ct, err := enc1.Encrypt("secret message")
  177. require.NoError(t, err)
  178. _, err = enc2.Decrypt(ct)
  179. require.Error(t, err, "不同密钥的实例不应能解密对方的密文")
  180. }