axe/internal/crypt/cipher_test.go

129 lines
3.8 KiB
Go
Raw Normal View History

2026-10-18 19:07:25 +00:00
package crypt
import (
"encoding/base64"
"encoding/hex"
"errors"
"strings"
"testing"
)
// testKey — 32 байта в base64 (та же строка ниже и в hex).
var (
testKeyB64 = base64.StdEncoding.EncodeToString(make([]byte, 32))
testKeyHex = hex.EncodeToString(make([]byte, 32))
)
// TestNewKeyFormat — принимаются base64 и hex, отклоняется прочее:
// пустой ключ, короткий, мусор и неверный размер.
func TestNewKeyFormat(t *testing.T) {
for _, key := range []string{testKeyB64, testKeyHex, strings.TrimRight(testKeyB64, "=")} {
if _, err := New(key); err != nil {
t.Errorf("New(%q): %v", key[:12], err)
}
}
for _, key := range []string{
"",
"short",
base64.StdEncoding.EncodeToString(make([]byte, 16)), // 16 байт — AES-128, не наш формат
strings.Repeat("z", 44), // base64 от 33 байт
} {
if _, err := New(key); !errors.Is(err, ErrKeySize) {
t.Errorf("New(%q): err = %v, want ErrKeySize", key, err)
}
}
}
// TestSealOpen — основной путь: шифрование обратимо, ciphertext не
// содержит исходный текст и меняется от прогона к прогону (nonce).
func TestSealOpen(t *testing.T) {
c, err := New(testKeyB64)
if err != nil {
t.Fatalf("New: %v", err)
}
const secret = "123456789:AAFakeBotTokenForTests0123456789"
enc, err := c.Seal(secret)
if err != nil {
t.Fatalf("Seal: %v", err)
}
if strings.Contains(enc, secret) || strings.Contains(enc, "AAFake") {
t.Errorf("ciphertext leaks plaintext: %q", enc)
}
got, err := c.Open(enc)
if err != nil {
t.Fatalf("Open: %v", err)
}
if got != secret {
t.Errorf("roundtrip = %q, want %q", got, secret)
}
// Та же строка шифруется заново: nonce другой, результат другой —
// иначе два пользователя с одинаковым токеном дали бы одинаковый blob.
again, err := c.Seal(secret)
if err != nil {
t.Fatalf("Seal again: %v", err)
}
if again == enc {
t.Error("two seals of the same value are equal (nonce reuse?)")
}
}
// TestOpenRejects — битые и чужие данные: обрезанный результат, битый
// base64, подделанный байт (GCM поймает), другой ключ.
func TestOpenRejects(t *testing.T) {
c, err := New(testKeyB64)
if err != nil {
t.Fatalf("New: %v", err)
}
other, err := New(hex.EncodeToString(append(make([]byte, 31), 1)))
if err != nil {
t.Fatalf("New other: %v", err)
}
enc, err := c.Seal("topsecret")
if err != nil {
t.Fatalf("Seal: %v", err)
}
raw, err := base64.StdEncoding.DecodeString(enc)
if err != nil {
t.Fatalf("decode: %v", err)
}
raw[len(raw)-1] ^= 0xFF // битый тэг GCM
tampered := base64.StdEncoding.EncodeToString(raw)
for name, tc := range map[string]struct {
c *Cipher
in string
}{
"not base64": {c, "не base64"},
"too short": {c, base64.StdEncoding.EncodeToString([]byte("abc"))},
"tampered": {c, tampered},
"another key": {other, enc},
"empty": {c, ""},
} {
t.Run(name, func(t *testing.T) {
if _, err := tc.c.Open(tc.in); !errors.Is(err, ErrCorrupt) {
t.Errorf("err = %v, want ErrCorrupt", err)
}
})
}
}
// TestSealEmpty — пустая строка шифруется и расшифровывается (нужно,
// когда пустое значение хранят сознательно).
func TestSealEmpty(t *testing.T) {
c, err := New(testKeyHex)
if err != nil {
t.Fatalf("New: %v", err)
}
enc, err := c.Seal("")
if err != nil {
t.Fatalf("Seal: %v", err)
}
got, err := c.Open(enc)
if err != nil || got != "" {
t.Errorf("got %q, err %v, want empty", got, err)
}
}