All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Add internal/crypto package: AES-256-GCM encrypt/decrypt with migration passthrough for existing plain-text records (no "enc:" prefix = plain text) - Store.NewStoreWithCipher injects cipher; SaveToken/UpdateLocationConfig encrypt cast_api_key before write; GetToken/ListTokens decrypt on read - Add CREDENTIALS_ENCRYPTION_KEY env var (64-hex / 32-byte); warns if unset - Add MongoDB authentication: MONGO_ROOT_USERNAME / MONGO_ROOT_PASSWORD via docker-compose MONGO_INITDB_ROOT_USERNAME/PASSWORD; MONGO_URI now requires credentials in .env.example - Update .env.example with generation instructions for all secrets Co-Authored-By: SideKx <sidekx.ai@sds.dev>
95 lines
2.3 KiB
Go
95 lines
2.3 KiB
Go
package crypto
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
const testKeyHex = "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"
|
|
|
|
func TestEncryptDecryptRoundtrip(t *testing.T) {
|
|
c, err := NewCipher(testKeyHex)
|
|
if err != nil {
|
|
t.Fatalf("NewCipher: %v", err)
|
|
}
|
|
|
|
plaintext := "cast_abc123def456"
|
|
encrypted, err := c.Encrypt(plaintext)
|
|
if err != nil {
|
|
t.Fatalf("Encrypt: %v", err)
|
|
}
|
|
if !strings.HasPrefix(encrypted, encPrefix) {
|
|
t.Errorf("encrypted value missing %q prefix: %q", encPrefix, encrypted)
|
|
}
|
|
if encrypted == plaintext {
|
|
t.Errorf("encrypted value equals plaintext — no encryption occurred")
|
|
}
|
|
|
|
decrypted, err := c.Decrypt(encrypted)
|
|
if err != nil {
|
|
t.Fatalf("Decrypt: %v", err)
|
|
}
|
|
if decrypted != plaintext {
|
|
t.Errorf("roundtrip mismatch: got %q, want %q", decrypted, plaintext)
|
|
}
|
|
}
|
|
|
|
func TestEncryptProducesUniqueNonce(t *testing.T) {
|
|
c, err := NewCipher(testKeyHex)
|
|
if err != nil {
|
|
t.Fatalf("NewCipher: %v", err)
|
|
}
|
|
a, _ := c.Encrypt("same-value")
|
|
b, _ := c.Encrypt("same-value")
|
|
if a == b {
|
|
t.Error("two encryptions of the same plaintext produced identical ciphertext (nonce reuse)")
|
|
}
|
|
}
|
|
|
|
func TestDecryptPlainText(t *testing.T) {
|
|
c, err := NewCipher(testKeyHex)
|
|
if err != nil {
|
|
t.Fatalf("NewCipher: %v", err)
|
|
}
|
|
// Plain text (no "enc:" prefix) should pass through unchanged for migration.
|
|
plain := "legacy_unencrypted_key"
|
|
result, err := c.Decrypt(plain)
|
|
if err != nil {
|
|
t.Fatalf("Decrypt plain text: %v", err)
|
|
}
|
|
if result != plain {
|
|
t.Errorf("plain text passthrough: got %q, want %q", result, plain)
|
|
}
|
|
}
|
|
|
|
func TestEncryptEmptyString(t *testing.T) {
|
|
c, err := NewCipher(testKeyHex)
|
|
if err != nil {
|
|
t.Fatalf("NewCipher: %v", err)
|
|
}
|
|
enc, err := c.Encrypt("")
|
|
if err != nil {
|
|
t.Fatalf("Encrypt empty: %v", err)
|
|
}
|
|
if enc != "" {
|
|
t.Errorf("encrypt empty: expected empty result, got %q", enc)
|
|
}
|
|
}
|
|
|
|
func TestNewCipherInvalidKey(t *testing.T) {
|
|
cases := []struct{ name, key string }{
|
|
{"not hex", "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"},
|
|
{"too short", "0102030405060708"},
|
|
{"too long", testKeyHex + "ff"},
|
|
{"empty", ""},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
_, err := NewCipher(tc.key)
|
|
if err == nil {
|
|
t.Errorf("expected error for key %q, got nil", tc.key)
|
|
}
|
|
})
|
|
}
|
|
}
|