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:
erio
2026-04-17 17:00:29 +08:00
parent 6cfdf4ec05
commit fd0c9a1305
4 changed files with 151 additions and 25 deletions

View File

@@ -10,12 +10,15 @@ import (
"strings"
)
// AES256KeySize is the required key length (in bytes) for AES-256-GCM.
const AES256KeySize = 32
// Encrypt encrypts plaintext using AES-256-GCM with the given 32-byte key.
// The output format is "iv:authTag:ciphertext" where each component is base64-encoded,
// matching the Node.js crypto.ts format for cross-compatibility.
func Encrypt(plaintext string, key []byte) (string, error) {
if len(key) != 32 {
return "", fmt.Errorf("encryption key must be 32 bytes, got %d", len(key))
if len(key) != AES256KeySize {
return "", fmt.Errorf("encryption key must be %d bytes, got %d", AES256KeySize, len(key))
}
block, err := aes.NewCipher(key)
@@ -52,8 +55,8 @@ func Encrypt(plaintext string, key []byte) (string, error) {
// Decrypt decrypts a ciphertext string produced by Encrypt.
// The input format is "iv:authTag:ciphertext" where each component is base64-encoded.
func Decrypt(ciphertext string, key []byte) (string, error) {
if len(key) != 32 {
return "", fmt.Errorf("encryption key must be 32 bytes, got %d", len(key))
if len(key) != AES256KeySize {
return "", fmt.Errorf("encryption key must be %d bytes, got %d", AES256KeySize, len(key))
}
parts := strings.SplitN(ciphertext, ":", 3)