fix(payment): store provider config as plaintext JSON with legacy ciphertext fallback
Without TOTP_ENCRYPTION_KEY, saved payment configs were lost on restart because the AES round-trip failed silently. Write new records as plaintext JSON; read path tries JSON first, falls back to legacy AES decrypt when a key is present, and treats unreadable values as empty so admins can re-enter them via the UI.
This commit is contained in:
@@ -452,6 +452,103 @@ func TestStartOfDay(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecryptConfig_PlaintextAndLegacyCompat(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
key := make([]byte, AES256KeySize)
|
||||
for i := range key {
|
||||
key[i] = byte(i + 1)
|
||||
}
|
||||
wrongKey := make([]byte, AES256KeySize)
|
||||
for i := range wrongKey {
|
||||
wrongKey[i] = byte(0xFF - i)
|
||||
}
|
||||
|
||||
plaintextJSON := `{"appId":"app-123","secret":"sec-xyz"}`
|
||||
|
||||
legacyEncrypted, err := Encrypt(plaintextJSON, key)
|
||||
if err != nil {
|
||||
t.Fatalf("seed Encrypt: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
stored string
|
||||
key []byte
|
||||
want map[string]string
|
||||
}{
|
||||
{
|
||||
name: "empty stored returns nil map",
|
||||
stored: "",
|
||||
key: key,
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "plaintext JSON parses directly",
|
||||
stored: plaintextJSON,
|
||||
key: nil,
|
||||
want: map[string]string{"appId": "app-123", "secret": "sec-xyz"},
|
||||
},
|
||||
{
|
||||
name: "plaintext JSON works even with key present",
|
||||
stored: plaintextJSON,
|
||||
key: key,
|
||||
want: map[string]string{"appId": "app-123", "secret": "sec-xyz"},
|
||||
},
|
||||
{
|
||||
name: "legacy ciphertext with correct key decrypts",
|
||||
stored: legacyEncrypted,
|
||||
key: key,
|
||||
want: map[string]string{"appId": "app-123", "secret": "sec-xyz"},
|
||||
},
|
||||
{
|
||||
name: "legacy ciphertext with no key treated as empty",
|
||||
stored: legacyEncrypted,
|
||||
key: nil,
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "legacy ciphertext with wrong key treated as empty",
|
||||
stored: legacyEncrypted,
|
||||
key: wrongKey,
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "garbage data treated as empty",
|
||||
stored: "not-json-and-not-ciphertext",
|
||||
key: key,
|
||||
want: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
lb := NewDefaultLoadBalancer(nil, tt.key)
|
||||
got, err := lb.decryptConfig(tt.stored)
|
||||
if err != nil {
|
||||
t.Fatalf("decryptConfig unexpected error: %v", err)
|
||||
}
|
||||
if !stringMapEqual(got, tt.want) {
|
||||
t.Fatalf("decryptConfig = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// stringMapEqual compares two map[string]string values; nil and empty are equal.
|
||||
func stringMapEqual(a, b map[string]string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for k, v := range a {
|
||||
if bv, ok := b[k]; !ok || bv != v {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user