CHAPTER 12 · Secrets and Bring-Your-Own-Key · 3 / 10
Storing user secrets: encryption at rest
Never store a user secret in plaintext. Use authenticated symmetric encryption (AES-256-GCM is the standard choice):
- Derive the encryption key from an operator secret held in the environment (e.g. hash a long random
ENCRYPTION_SECRETinto a 32-byte key). The encryption key lives in the environment, not in the database, so a database dump alone is useless to an attacker. - Encrypt with a fresh random IV per record. Store the ciphertext, the IV, and the authentication tag: the three outputs of GCM. Reusing an IV with GCM is catastrophic, so generate a new one every time you encrypt.
- Authenticated encryption gives integrity, not just secrecy. A tampered ciphertext fails the auth-tag check on decryption rather than silently producing attacker-influenced plaintext.
- Fail closed on decryption. If the auth tag doesn't verify (corruption, wrong key, tampering), return nothing and log; never return garbage that might be used as a credential.
Store each secret type in an appropriately-constrained table (e.g. a provider key per (user, provider); a separate table for a different token type so a check constraint reserved for providers isn't stretched).