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:
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -290,19 +291,29 @@ func (s *PaymentConfigService) mergeConfig(ctx context.Context, id int64, newCon
|
||||
return existing, nil
|
||||
}
|
||||
|
||||
func (s *PaymentConfigService) decryptConfig(encrypted string) (map[string]string, error) {
|
||||
if encrypted == "" {
|
||||
// decryptConfig parses a stored provider config.
|
||||
// New records are plaintext JSON; legacy records are AES-256-GCM ciphertext
|
||||
// ("iv:authTag:ciphertext"). Values that cannot be parsed as either — including
|
||||
// legacy ciphertext with no/invalid TOTP_ENCRYPTION_KEY — are treated as empty,
|
||||
// letting the admin re-enter the config via the UI to complete the migration.
|
||||
func (s *PaymentConfigService) decryptConfig(stored string) (map[string]string, error) {
|
||||
if stored == "" {
|
||||
return nil, nil
|
||||
}
|
||||
decrypted, err := payment.Decrypt(encrypted, s.encryptionKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decrypt config: %w", err)
|
||||
var cfg map[string]string
|
||||
if err := json.Unmarshal([]byte(stored), &cfg); err == nil {
|
||||
return cfg, nil
|
||||
}
|
||||
var raw map[string]string
|
||||
if err := json.Unmarshal([]byte(decrypted), &raw); err != nil {
|
||||
return nil, fmt.Errorf("unmarshal decrypted config: %w", err)
|
||||
if len(s.encryptionKey) == payment.AES256KeySize {
|
||||
if plaintext, err := payment.Decrypt(stored, s.encryptionKey); err == nil {
|
||||
if err := json.Unmarshal([]byte(plaintext), &cfg); err == nil {
|
||||
return cfg, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return raw, nil
|
||||
slog.Warn("payment provider config unreadable, treating as empty for re-entry",
|
||||
"stored_len", len(stored))
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *PaymentConfigService) DeleteProviderInstance(ctx context.Context, id int64) error {
|
||||
@@ -317,14 +328,13 @@ func (s *PaymentConfigService) DeleteProviderInstance(ctx context.Context, id in
|
||||
return s.entClient.PaymentProviderInstance.DeleteOneID(id).Exec(ctx)
|
||||
}
|
||||
|
||||
// encryptConfig serialises a provider config for storage.
|
||||
// New records are written as plaintext JSON; the historical AES-GCM wrapping
|
||||
// has been dropped but decryptConfig still accepts old ciphertext during migration.
|
||||
func (s *PaymentConfigService) encryptConfig(cfg map[string]string) (string, error) {
|
||||
data, err := json.Marshal(cfg)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("marshal config: %w", err)
|
||||
}
|
||||
enc, err := payment.Encrypt(string(data), s.encryptionKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("encrypt config: %w", err)
|
||||
}
|
||||
return enc, nil
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user