Add delete account endpoint and file encryption at rest
Delete Account (Plan #2): - DELETE /api/auth/account/ with password or "DELETE" confirmation - Cascade delete across 15+ tables in correct FK order - Auth provider detection (email/apple/google) for /auth/me/ - File cleanup after account deletion - Handler + repository tests (12 tests) Encryption at Rest (Plan #3): - AES-256-GCM envelope encryption (per-file DEK wrapped by KEK) - Encrypt on upload, auto-decrypt on serve via StorageService.ReadFile() - MediaHandler serves decrypted files via c.Blob() - TaskService email image loading uses ReadFile() - cmd/migrate-encrypt CLI tool with --dry-run for existing files - Encryption service + storage service tests (18 tests)
This commit is contained in:
218
internal/services/encryption_service_test.go
Normal file
218
internal/services/encryption_service_test.go
Normal file
@@ -0,0 +1,218 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// validTestKey returns a deterministic 64-char hex key for tests.
|
||||
func validTestKey() string {
|
||||
return "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
||||
}
|
||||
|
||||
// randomHexKey generates a random 64-char hex key.
|
||||
func randomHexKey(t *testing.T) string {
|
||||
t.Helper()
|
||||
b := make([]byte, 32)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
func TestNewEncryptionService_Valid(t *testing.T) {
|
||||
svc, err := NewEncryptionService(validTestKey())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if svc == nil {
|
||||
t.Fatal("expected non-nil service")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewEncryptionService_InvalidHex(t *testing.T) {
|
||||
// 64 chars but not valid hex
|
||||
_, err := NewEncryptionService("zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for invalid hex")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewEncryptionService_WrongLength(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
key string
|
||||
}{
|
||||
{"too short", "0123456789abcdef"},
|
||||
{"too long", "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef00"},
|
||||
{"empty", ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := NewEncryptionService(tt.key)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for wrong length key")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncryptDecrypt_RoundTrip(t *testing.T) {
|
||||
svc, err := NewEncryptionService(validTestKey())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
plaintext := []byte("Hello, encryption at rest!")
|
||||
|
||||
ciphertext, err := svc.Encrypt(plaintext)
|
||||
if err != nil {
|
||||
t.Fatalf("encrypt failed: %v", err)
|
||||
}
|
||||
|
||||
decrypted, err := svc.Decrypt(ciphertext)
|
||||
if err != nil {
|
||||
t.Fatalf("decrypt failed: %v", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(plaintext, decrypted) {
|
||||
t.Fatalf("round-trip mismatch: got %q, want %q", decrypted, plaintext)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncryptDecrypt_EmptyPlaintext(t *testing.T) {
|
||||
svc, err := NewEncryptionService(validTestKey())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ciphertext, err := svc.Encrypt([]byte{})
|
||||
if err != nil {
|
||||
t.Fatalf("encrypt failed: %v", err)
|
||||
}
|
||||
|
||||
decrypted, err := svc.Decrypt(ciphertext)
|
||||
if err != nil {
|
||||
t.Fatalf("decrypt failed: %v", err)
|
||||
}
|
||||
|
||||
if len(decrypted) != 0 {
|
||||
t.Fatalf("expected empty plaintext, got %d bytes", len(decrypted))
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncrypt_DifferentCiphertexts(t *testing.T) {
|
||||
svc, err := NewEncryptionService(validTestKey())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
plaintext := []byte("same input")
|
||||
ct1, err := svc.Encrypt(plaintext)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ct2, err := svc.Encrypt(plaintext)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if bytes.Equal(ct1, ct2) {
|
||||
t.Fatal("encrypting the same plaintext twice should produce different ciphertexts")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecrypt_TamperDetection(t *testing.T) {
|
||||
svc, err := NewEncryptionService(validTestKey())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ciphertext, err := svc.Encrypt([]byte("sensitive data"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Flip a byte near the end (in the ciphertext portion)
|
||||
tampered := make([]byte, len(ciphertext))
|
||||
copy(tampered, ciphertext)
|
||||
tampered[len(tampered)-1] ^= 0xFF
|
||||
|
||||
_, err = svc.Decrypt(tampered)
|
||||
if err == nil {
|
||||
t.Fatal("expected error when decrypting tampered ciphertext")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecrypt_WrongKEK(t *testing.T) {
|
||||
svc1, err := NewEncryptionService(validTestKey())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ciphertext, err := svc1.Encrypt([]byte("secret"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create a second service with a different key
|
||||
svc2, err := NewEncryptionService(randomHexKey(t))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = svc2.Decrypt(ciphertext)
|
||||
if err == nil {
|
||||
t.Fatal("expected error when decrypting with wrong KEK")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecrypt_TooShort(t *testing.T) {
|
||||
svc, err := NewEncryptionService(validTestKey())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = svc.Decrypt([]byte("short"))
|
||||
if err == nil {
|
||||
t.Fatal("expected error for too-short ciphertext")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecrypt_BadVersion(t *testing.T) {
|
||||
svc, err := NewEncryptionService(validTestKey())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ciphertext, err := svc.Encrypt([]byte("data"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Change version byte
|
||||
ciphertext[0] = 0xFF
|
||||
|
||||
_, err = svc.Decrypt(ciphertext)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for bad version")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsEnabled(t *testing.T) {
|
||||
svc, err := NewEncryptionService(validTestKey())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !svc.IsEnabled() {
|
||||
t.Fatal("expected IsEnabled() to return true")
|
||||
}
|
||||
|
||||
var nilSvc *EncryptionService
|
||||
if nilSvc.IsEnabled() {
|
||||
t.Fatal("expected IsEnabled() to return false for nil service")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user