Watch
1
0
Fork
You've already forked RedFlag
0
RedFlag/server/internal/services/security_settings_service_test.go
Fimeg cf061a13a1 seal the settings path so secrets land encrypted
the encrypt/decrypt hooks existed but the write path wrote values straight to
the column, so a sensitive setting would have gone in as plaintext. closed it:

- sensitive values serialize + encrypt before they persist (base64 aes-gcm over
  the json), and updates now carry the is_encrypted flag through
- non-sensitive settings are untouched — still plain json, same as before
- audit log redacts sensitive old/new values instead of recording them raw

round-trip test proves sensitive values never hit the column in plaintext and
still decrypt back; non-sensitive stay readable. no backfill needed — nothing
writes secrets through this path yet, the defaults are empty.

builds clean, vet quiet, tests pass.
2026-06-07 21:16:39 -04:00

70 lines
2.1 KiB
Go

package services
import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"strings"
"testing"
)
// newTestSettingsService builds a service with a real random AES-256 key and no
// DB (serializeForStorage / encrypt / decrypt don't touch the store).
func newTestSettingsService(t *testing.T) *SecuritySettingsService {
t.Helper()
key := make([]byte, 32)
if _, err := rand.Read(key); err != nil {
t.Fatalf("rand key: %v", err)
}
svc, err := NewSecuritySettingsService(nil, nil, base64.StdEncoding.EncodeToString(key))
if err != nil {
t.Fatalf("new service: %v", err)
}
return svc
}
// SEC-024: a non-sensitive setting stores plain JSON exactly as before.
func TestSerializeForStorageNonSensitivePlaintext(t *testing.T) {
svc := newTestSettingsService(t)
stored, encrypted, err := svc.serializeForStorage("nonce_validation", "timeout_seconds", 600)
if err != nil {
t.Fatalf("serialize: %v", err)
}
if encrypted {
t.Fatalf("non-sensitive setting must not be marked encrypted")
}
if stored != "600" {
t.Fatalf("non-sensitive setting should store plain JSON, got %q", stored)
}
}
// SEC-024: a sensitive setting must persist as ciphertext (never plaintext) and
// must round-trip through the same decrypt-then-unmarshal path GetSetting uses.
func TestSerializeForStorageSensitiveEncryptsAndRoundTrips(t *testing.T) {
svc := newTestSettingsService(t)
secret := "super-secret-ed25519-private-key"
stored, encrypted, err := svc.serializeForStorage("command_signing", "private_key", secret)
if err != nil {
t.Fatalf("serialize: %v", err)
}
if !encrypted {
t.Fatalf("sensitive setting must be marked encrypted")
}
if strings.Contains(stored, secret) {
t.Fatalf("sensitive value persisted in plaintext: %q", stored)
}
// GetSetting read path: decrypt then json-unmarshal must recover the original.
decrypted, err := svc.decrypt(stored)
if err != nil {
t.Fatalf("decrypt: %v", err)
}
var got string
if err := json.Unmarshal([]byte(decrypted), &got); err != nil {
t.Fatalf("unmarshal decrypted value: %v", err)
}
if got != secret {
t.Fatalf("round-trip mismatch: got %q want %q", got, secret)
}
}