Skip to content

ADR-0003. AES-256-GCM encryption for provider credentials at rest

FieldValue
StatusAccepted
Date2026-02-12
Deciderspayment-team, security
Supersedes

Context

  • Payment provider credentials (VNPAY appId/secrets, per-merchant tokens) are highly sensitive — leaks enable unauthorized payments.
  • Storing them plaintext in Configuration table is unacceptable.
  • We need a balance between security and operational simplicity (we're not at the scale where HSM/KMS is required).

Decision

All payment credentials are encrypted at rest using AES-256-GCM via CryptoUtility (from @nx/core). The encryption key is derived from APP_ENV_APPLICATION_SECRET.

Specifics:

  • The Configuration.credential column is hidden from standard CRUD reads (drizzle hidden field).
  • PaymentConfigurationService.getPaymentCredential accesses the column directly via the drizzle connector and decrypts inline.
  • Same secret encrypts non-credential payment configs in tValue (e.g., VNPAY's masterMerchantCode, appId).
  • All pods (API + WORKER) MUST share APP_ENV_APPLICATION_SECRET.

Consequences

ProsCons
Database dump alone doesn't leak credentialsSingle-secret model — leaked secret = total compromise
Authenticated encryption (GCM mode) prevents tamperingSecret rotation requires re-encrypting every credential row (manual)
In-process decrypt — sub-millisecondNo per-merchant KMS isolation
Standard primitive, well-auditedApp secret must be stored in K8s secret with strict RBAC

Rotation procedure

  1. Generate new secret.
  2. Read all Configuration rows with credentials, decrypt with old secret.
  3. Re-encrypt with new secret.
  4. Roll deployment with new env.

Rotation MUST be coordinated — no half-rolled state where pods have different secrets.

Alternatives Considered

OptionProsConsWhy rejected
HashiCorp Vault / AWS KMSPer-credential keys, rotation built-inVendor lock-in, ops cost, latencyPremature for our scale
Asymmetric (RSA per merchant)Per-merchant isolationComplex; key explosionUnnecessary for current threat model
Plaintext + DB ACLSimpleUnacceptable — DB ops can read credentialsNo

References

  • core/src/utilities/crypto.utility.ts (CryptoUtility)
  • services/payment-configuration.service.ts:43-176 (decrypt sites)
  • core/src/models/schemas/public/configuration/schema.ts (hidden credential column)

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.