Symmetric encryption
Use envelope encryption
Encrypt data with a data key and protect that key with a separate key-encryption service so rotation and access can be controlled centrally.
Encrypting every record directly with one long-lived master key makes rotation and access harder. Think about what rotating that key means: one long-lived master key directly encrypts millions of records, so rotating it requires reading and rewriting every record while the old key remains broadly available. Most teams quietly never rotate.
Envelope encryption separates roles so that problem disappears.
Two keys, two jobs
Generate a fresh data-encryption key (DEK) for each object or group of records, encrypt the data with it, then encrypt — “wrap” — that key with a key-encryption key (KEK) held by a managed KMS or vault. Store the wrapped data key with the ciphertext. The KEK never leaves the key service; the plaintext DEK exists only in application memory during the operation.
import { randomBytes, createCipheriv } from 'node:crypto'
const dek = randomBytes(32) // fresh per record
const nonce = randomBytes(12)
const cipher = createCipheriv('aes-256-gcm', dek, nonce)
const ciphertext = Buffer.concat([cipher.update(document), cipher.final()])
const wrappedDek = await kms.encrypt({ keyId: 'kek-prod-v2', plaintext: dek })
// the plaintext dek is discarded after this point
A stored record then carries everything needed to decrypt later, except the authority to do it:
{
"v": 2,
"kek_id": "kek-prod-v2",
"wrapped_dek": "AQIDAHh3...",
"nonce": "8kQzL1Xb9mPq",
"tag": "5tR8...",
"ciphertext": "..."
}
To read, the application asks the KMS to unwrap wrapped_dek, decrypts locally, and discards the DEK again. The KMS sees one small unwrap call per read, not your data.
Why this structure pays off
Rotating the wrapping key may avoid re-encrypting all data. You unwrap each DEK with the old KEK and rewrap with the new one. The bulk ciphertext bytes never move and the plaintext is never exposed during rotation — you rewrite a 32-byte field per record instead of the record itself.
Access control moves to one place. The KMS decides who may unwrap keys, and its audit log shows every decryption request. Losing a database backup exposes wrapped keys only, which are useless without the KEK.
One caution: bind each wrapped key to its record with authenticated context, so a valid wrapped DEK cannot be moved onto a different record unnoticed.
Practice
Draw and save one encrypted record containing ciphertext, nonce, wrapped data key, key identifier, version, and authenticated context. Simulate rotating the wrapping key by rewrapping the data key and prove the ciphertext bytes stay unchanged. Then use the wrong record context and capture the expected decryption failure.
Lesson completed