Symmetric Encryption

One key encrypts and decrypts. You reach for this rarely — TLS already protects data in transit — but you need it for fields that must stay unreadable to anyone holding a database dump: a stored API credential, an OAuth refresh token. Use an authenticated cipher, which emits a short tag alongside the ciphertext and refuses to decrypt if either was altered. AES-256-GCM is the default; chacha20-poly1305 is a drop-in replacement for CPUs without AES instructions. Unauthenticated modes such as aes-256-cbc decrypt tampered input into plausible garbage, the root of a long line of padding-oracle vulnerabilities.

Sealing and opening a database field with AES-256-GCMJavaScript
import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';
const key = randomBytes(32);          // 256-bit key: load from a KMS, never hard-code
function seal(plaintext, aad) {
  const iv = randomBytes(12);         // 96-bit nonce: fresh for every single message
  const c = createCipheriv('aes-256-gcm', key, iv);
  c.setAAD(Buffer.from(aad));         // authenticated but not encrypted
  const ct = Buffer.concat([c.update(plaintext, 'utf8'), c.final()]);
  return Buffer.concat([iv, c.getAuthTag(), ct]).toString('base64url');
}
function open(token, aad) {
  const raw = Buffer.from(token, 'base64url');
  const d = createDecipheriv('aes-256-gcm', key, raw.subarray(0, 12));
  d.setAAD(Buffer.from(aad));
  d.setAuthTag(raw.subarray(12, 28)); // must be set before final()
  return Buffer.concat([d.update(raw.subarray(28)), d.final()]).toString('utf8');
}
const token = seal('4111 1111 1111 1111', 'user:4711');
console.log('stored:', token, '\nopened:', open(token, 'user:4711'));
const flip = Buffer.from(token, 'base64url'); flip[30] ^= 1;
for (const [why, t, aad] of [['wrong aad  ', token, 'user:9999'],
  ['flipped bit', flip.toString('base64url'), 'user:4711']]) {
  try { open(t, aad); } catch (e) { console.log(why + ':', e.message); }
}
Output
stored: -YEaXHMMDAlBMuefa1LT9-jR_A2bbJ9Pq6jAinct5-WfMcBw680pcpRzFeY4Epw
opened: 4111 1111 1111 1111
wrong aad  : Unsupported state or unable to authenticate data
flipped bit: Unsupported state or unable to authenticate data

Three rules follow. The IV must be unique per message under a key: GCM repeats its keystream if a nonce repeats, and two messages sharing a key and nonce leak their XOR and the authentication key itself. Store it alongside — an IV is not secret, and that fixed 28-byte overhead is why 19 bytes of plaintext became 47. The tag is not optional: omit setAuthTag and final() throws. AAD binds context, so a ciphertext stolen from one row cannot be pasted into another user's row. Keep the key in a service such as AWS 24 KMS, and prefix ciphertext with a key id like v2: so rotation does not orphan old rows.