Symmetric Ciphers
Symmetric crypto uses one shared key for encrypt + decrypt. Modern choices: AES-256-GCM (hardware-accelerated, FIPS) or ChaCha20-Poly1305 (faster on mobile). Both are authenticated — tampering breaks decryption.
AES-GCM, ChaCha20, key derivation
EXAMPLE
// 1) AES-256-GCM in Node — the daily-driver
import crypto from 'node:crypto';
function aesGcmEncrypt(plaintext, key, aad) {
const iv = crypto.randomBytes(12); // 96-bit nonce — MUST be unique per encryption
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
if (aad) cipher.setAAD(aad); // authenticated additional data
const ct = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
const tag = cipher.getAuthTag(); // 16 bytes
// Pack: iv || ct || tag
return Buffer.concat([iv, ct, tag]).toString('base64');
}
function aesGcmDecrypt(packed, key, aad) {
const buf = Buffer.from(packed, 'base64');
const iv = buf.subarray(0, 12);
const tag = buf.subarray(buf.length - 16);
const ct = buf.subarray(12, buf.length - 16);
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
if (aad) decipher.setAAD(aad);
decipher.setAuthTag(tag);
return Buffer.concat([decipher.update(ct), decipher.final()]).toString('utf8');
}
const key = crypto.randomBytes(32); // 256-bit key — STORE THIS SAFELY
const packed = aesGcmEncrypt('hello world', key);
console.log(aesGcmDecrypt(packed, key));
// 2) ChaCha20-Poly1305 — preferred when AES hw is uncertain (mobile, IoT)
import sodium from 'libsodium-wrappers';
await sodium.ready;
const key2 = sodium.crypto_aead_chacha20poly1305_ietf_keygen();
const nonce = sodium.randombytes_buf(sodium.crypto_aead_chacha20poly1305_ietf_NPUBBYTES);
const ct2 = sodium.crypto_aead_chacha20poly1305_ietf_encrypt('hello', null, null, nonce, key2);
const pt2 = sodium.crypto_aead_chacha20poly1305_ietf_decrypt(null, ct2, null, nonce, key2);
// 3) Python — cryptography (AESGCM)
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import os
key = AESGCM.generate_key(bit_length=256)
aes = AESGCM(key)
nonce = os.urandom(12)
ct = aes.encrypt(nonce, b'hello', associated_data=b'ctx')
pt = aes.decrypt(nonce, ct, associated_data=b'ctx')
// 4) PHP — libsodium (recommended)
$key = sodium_crypto_aead_chacha20poly1305_ietf_keygen();
$nonce = random_bytes(SODIUM_CRYPTO_AEAD_CHACHA20POLY1305_IETF_NPUBBYTES);
$ct = sodium_crypto_aead_chacha20poly1305_ietf_encrypt('hello', '', $nonce, $key);
$pt = sodium_crypto_aead_chacha20poly1305_ietf_decrypt($ct, '', $nonce, $key);
// 5) Java — javax.crypto with AES/GCM/NoPadding
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
byte[] iv = new byte[12]; new SecureRandom().nextBytes(iv);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, iv));
byte[] ct = cipher.doFinal(plaintext);
// === Key derivation — when the key comes from a password / shared secret ===
// 6) Argon2id — derive a strong key from a low-entropy password
import argon2 from 'argon2';
const derived = await argon2.hash(password, {
type: argon2.argon2id,
raw: true,
salt: crypto.randomBytes(16),
memoryCost: 19 * 1024,
timeCost: 2,
hashLength: 32,
});
// derived = 32 bytes — use as a symmetric key
// 7) HKDF — derive a key from a high-entropy shared secret (e.g. ECDH output)
const sharedSecret = crypto.diffieHellman({ privateKey: myPriv, publicKey: theirPub });
const aesKey = crypto.hkdfSync('sha256', sharedSecret, salt, info, 32);
// === Operational essentials ===
// 8) Nonce rules — non-negotiable
// • Never REUSE a (key, nonce) pair. Same pair + GCM = leaks plaintext XOR. Catastrophic.
// • Random 96-bit nonces are safe for ~2^32 messages per key. After that, ROTATE.
// • For more messages on one key: deterministic counter nonces, or XChaCha20 (192-bit).
// 9) Authenticated encryption (AEAD) — ALWAYS
// AES-GCM and ChaCha20-Poly1305 verify integrity automatically.
// AES-CBC + HMAC by hand: easy to mess up. Don't.
// AES-ECB: leaks structure (the famous penguin meme). NEVER.
// 10) Key storage
// • KMS / Secrets Manager / Vault — not env vars, not in code
// • Envelope encryption: master key wraps data keys; rotate keys without re-encrypting data
// • Hardware: HSM / TPM / Apple Secure Enclave / Android Keystore for highest assurance
// 11) Common API patterns
// • Encrypt-only: data in DB columns (per-tenant key in KMS, ciphertext in column)
// • Encrypt-then-MAC for legacy (AES-CBC + HMAC-SHA256, constant-time compare)
// • Hybrid: RSA / X25519 wraps an AES key; AES encrypts the payload
// 12) Cryptographic agility
// • Prefix the ciphertext with an algorithm + version byte: '01' = AES-GCM-256
// • Rotation path: bump the version; decrypt old, re-encrypt new
// • Don't hard-code algorithm choice into the API — wrap it
// 13) Things to AVOID
// • Rolling your own crypto (build on libsodium / cryptography / WebCrypto / @noble)
// • Using Math.random / Date.now for nonces (use CSPRNG)
// • Reusing keys across services without rotation
// • Storing both key and ciphertext in the same place without separating access
// • Sending the IV / nonce out of band — pack it with the ciphertext
// • Reinventing AES-GCM — there's always a subtle nonce bug waiting
// 14) When you need PERFECT FORWARD SECRECY
// • Don't reuse a long-term key for messages — derive an ephemeral session key (ECDH + HKDF + AES)
// • This is how Signal / TLS 1.3 / Noise protocol work
// 15) Quick decision
// Encrypt small data (< few KB): AES-256-GCM
// Encrypt streaming / large data: AES-GCM in chunks (one nonce per chunk) OR XChaCha20-Poly1305
// Mobile / embedded: ChaCha20-Poly1305 — no AES hardware dependency
// FIPS / regulated: AES-256-GCM (compliant)
Why it matters
AES-256-GCM + 96-bit random nonces, key from a CSPRNG, keys in a KMS — that’s the modern symmetric stack. Nonce reuse breaks everything, so never invent your own scheme.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// Same key encrypts and decrypts. Use AEAD modes only. // AES-GCM — fastest with hardware AES (most servers) // ChaCha20-Poly1305 — best for mobile / no AES-NI // Avoid bare CBC / ECB / CTR — they don't authenticate.Try it Yourself »
Discussion
Loading…