Cryptography you actually need

Not the maths. The decisions: which primitive for which job, the parameters that matter, and the five mistakes that account for most crypto bugs in production.

● Must know for interviewsFREELesson·13 min read·Beginner → Intermediate
cryptographyhashingAEADkey management

01The primitive map

✓What you will learn
  • Pick the right primitive for hashing, MACs, passwords, encryption, signatures and key exchange
  • Explain AEAD and why unauthenticated encryption is a bug
  • Use HKDF to derive keys and constant-time comparison to check MACs
  • Describe envelope encryption and why KMS designs use it
  • Recognise the classic mistakes: ECB, nonce reuse, fast password hashes, rolling your own

Production cryptography is a handful of decisions made correctly and then hidden behind a good library. You do not need the maths; you need to know which tool does which job, which parameters matter and how keys are looked after. Each primitive answers a different question: is this the same data (hash), did someone with the key produce it (MAC), can only key holders read it (encryption), did this specific party produce it (signature), and how do two strangers agree a key (key exchange).

JobUseNot
Integrity / fingerprintSHA-256, SHA-3, BLAKE3MD5, SHA-1
Password storageArgon2id, scrypt, bcrypt (cost tuned)Plain SHA-256, even salted
Message authenticationHMAC-SHA256hash(secret + msg)
Symmetric encryptionAES-256-GCM, ChaCha20-Poly1305 (AEAD)AES-CBC without MAC, AES-ECB
SignaturesEd25519, ECDSA P-256, RSA-PSSRSA PKCS#1 v1.5 for new systems
Key exchangeX25519, ECDHEStatic RSA
Key derivationHKDF-SHA256 (from a key), Argon2id (from a password)Truncating or concatenating keys by hand
RandomOS CSPRNG (crypto.randomBytes)Math.random, time seeds
◎Interview radar

Interviewers probe whether you can tell these apart under pressure: "is a hash enough to protect this webhook?" (no, no key), "is encryption enough to stop tampering?" (only if it is authenticated), "why not sign with the shared secret?" (that is a MAC; anyone with the secret can forge it). Name the property each one gives.

If you remember one rule: modern, boring, authenticated. The library should make the safe choice the easy choice. If you find yourself choosing an IV length or a padding mode, step back and use a higher-level API (libsodium, Tink, your cloud KMS).

02Hashes and MACs

A hash maps any input to a fixed-size digest; it is collision resistant (hard to find two inputs with the same digest) and preimage resistant (hard to go backwards). It has no key, so it proves nothing about who made the data. An attacker who changes a file can recompute its SHA-256. Hashes are for fingerprints, deduplication, content addressing and as building blocks.

Why MD5 and SHA-1 are on the "not" list: collisions are practical. Researchers used an MD5 collision to forge a rogue CA certificate in 2008, and the SHAttered attack produced two different PDFs with the same SHA-1 digest in 2017. If an attacker can get you to sign or allow-list one document and then swap in its twin, collision resistance is the whole game.

A MAC (message authentication code) mixes in a secret key, so only key holders can produce a valid tag. HMAC-SHA256 is the default: webhook signatures, signed cookies, CSRF tokens, internal request signing. Two mistakes recur in reviews: building a MAC as sha256(secret + message), which length-extension attacks break, and comparing tags with ===, which leaks how many leading bytes matched through timing.

HMAC verification done properlyts
import { createHmac, timingSafeEqual } from "node:crypto";

// Verify a webhook: header "X-Signature: t=1731600000,v1=<hex hmac>"
export function verifyWebhook(rawBody: Buffer, header: string, secret: Buffer, nowSec: number): boolean {
  const m = /^t=(\d{10}),v1=([0-9a-f]{64})$/.exec(header);
  if (!m) return false;
  const ts = Number(m[1]);
  if (Math.abs(nowSec - ts) > 300) return false;            // 5 minute window stops replays of old events
  const expected = createHmac("sha256", secret).update(m[1] + ".").update(rawBody).digest();
  const given = Buffer.from(m[2], "hex");
  return given.length === expected.length && timingSafeEqual(given, expected);
}
  • Sign the raw bytes you received, not a re-serialised JSON object; key order and whitespace change between libraries.
  • Include a timestamp inside the signed data and reject old ones, or an attacker can replay a captured request forever.
  • timingSafeEqual throws on unequal lengths, so check the length first (as above) rather than letting an exception leak information or crash the handler.

Randomness is a primitive too

Session IDs, API keys, reset tokens, nonces and salts are only as strong as the generator behind them. Use the operating system CSPRNG (crypto.randomBytes, crypto.getRandomValues, secrets.token_urlsafe in Python) and take at least 128 bits for anything an attacker might guess. Math.random is a fast statistical generator whose internal state can be recovered from a handful of outputs, after which every future "random" token is predictable. Password reset links built from a timestamp or a user ID plus a counter fall to the same attack: the attacker requests a reset for themselves, sees the pattern and computes the victim's link.

03Why authenticated encryption

Encryption without authentication tells you nothing about who produced the ciphertext or whether it was altered. Padding-oracle attacks against CBC, bit-flipping attacks that change role=user to role=admin in an encrypted cookie: both come from decrypting data that was never authenticated. AEAD (authenticated encryption with associated data) gives confidentiality and integrity in one call, and lets you bind unencrypted context, the associated data, into the tag.

AES-GCM with associated datats
import { createCipheriv, randomBytes } from "node:crypto";

const key = loadKeyFromKms();           // 32 bytes, never in source
const nonce = randomBytes(12);          // unique per message, never reused with the same key
const cipher = createCipheriv("aes-256-gcm", key, nonce);
cipher.setAAD(Buffer.from("tenant:42")); // bind context: ciphertext cannot be replayed for tenant 43
const ct = Buffer.concat([cipher.update(plaintext), cipher.final()]);
const tag = cipher.getAuthTag();        // 16-byte MAC; decrypt MUST verify this before using data
store(Buffer.concat([nonce, tag, ct]));
!Nonce reuse in GCM is catastrophic

Reusing a nonce with the same key leaks the XOR of plaintexts and the authentication key. With random 96-bit nonces, rotate the key well before 2^32 messages. With counters, make the counter durable across restarts, or use a misuse-resistant mode (AES-GCM-SIV) or XChaCha20-Poly1305 with its 192-bit nonce. The AEAD and nonce discipline lesson works through the limits.

04Signatures, key exchange and derivation

Symmetric keys need both sides to share a secret in advance. Public key cryptography removes that. A signature is made with a private key and checked with the public key, so anyone can verify but only one party can sign: software releases, JWTs, certificates, audit log checkpoints. Ed25519 (RFC 8032) is the default for new systems: fast, small 64-byte signatures, deterministic, so a weak or repeated per-signature random value cannot leak the private key. That exact failure, a constant ECDSA nonce, exposed Sony's PlayStation 3 signing key in 2010.

Ed25519 sign and verifyts
import { generateKeyPairSync, sign, verify } from "node:crypto";

const { publicKey, privateKey } = generateKeyPairSync("ed25519");   // in production: KMS or HSM
const manifest = Buffer.from(JSON.stringify({ artefact: "academy-1.42.0.tgz", sha256: digestHex }));
const sig = sign(null, manifest, privateKey);   // Ed25519 takes no separate hash algorithm
const ok = verify(null, manifest, publicKey, sig);

Key exchange lets two parties agree a shared secret over a public channel. TLS 1.3 uses ephemeral X25519 (RFC 7748): both sides generate a fresh key pair per connection and throw it away afterwards, which gives forward secrecy. Stealing the server's long-term key later does not decrypt recorded traffic, because that key only signed the handshake. Static RSA key exchange had no such property and is gone from TLS 1.3.

Key exchange output is not a key you should use directly. HKDF (RFC 5869) turns a shared secret or master key into as many independent keys as you need, each labelled by an info string. Use it to derive per-tenant or per-purpose keys from one root so that the encryption key and the MAC key are never the same bytes.

Domain separation with HKDFts
import { hkdfSync } from "node:crypto";
// One master key from KMS, separate keys per purpose and tenant
const encKey = Buffer.from(hkdfSync("sha256", masterKey, salt, "academy/v1/enc/tenant:42", 32));
const macKey = Buffer.from(hkdfSync("sha256", masterKey, salt, "academy/v1/mac/tenant:42", 32));
iPost-quantum, briefly

A large quantum computer would break X25519, ECDSA and RSA, but not AES-256 or SHA-256. Because recorded traffic can be decrypted later, key exchange migrates first: hybrid X25519 + ML-KEM-768 is already default in major browsers. See the post-quantum migration lesson.

05Keys are the hard part

Algorithms are rarely the weak point. Keys are: where they live, who can read them, how they rotate. The pattern every cloud KMS uses is envelope encryption.

  1. A root key lives in an HSM or KMS and never leaves it.
  2. For each object or tenant, generate a data encryption key (DEK), encrypt the data with it.
  3. Encrypt the DEK with the root key (wrap it) and store the wrapped DEK next to the ciphertext.
  4. To read: send the wrapped DEK to KMS, get the DEK back, decrypt locally. KMS logs every unwrap.
  5. To rotate the root key: re-wrap DEKs, not re-encrypt terabytes. To revoke a tenant: delete its DEKs (crypto-shredding).

Put a key ID and algorithm identifier in every ciphertext header from day one. Without them, the first rotation or algorithm change becomes a migration crisis, because you cannot tell which key encrypted which record. The key rotation and crypto agility lesson covers formats and re-encryption strategies.

“How to say it in the room

"I would not encrypt rows directly with the KMS key: too slow and too many KMS calls. I'd use envelope encryption, one DEK per tenant, wrapped by a KMS key. Rotation re-wraps, deletion is crypto-shredding, and the KMS audit log tells me every time a tenant's data was unlocked."

06Passwords, properly

Fast hashes are the wrong tool for passwords because attackers compute billions per second on GPUs. Password hashing functions are deliberately slow and memory-hard: each guess costs real memory, which GPUs and ASICs have little of per core.

Parameters that are sane in 2026txt
Argon2id  memory=64 MiB  iterations=3  parallelism=1   (~50-100 ms server-side)
bcrypt    cost=12                                       (legacy, 72-byte input limit)
Store: $argon2id$v=19$m=65536,t=3,p=1$<salt>$<hash>
Upgrade: on successful login, if params are weaker than current policy, rehash.
  • The encoded string carries its own parameters and salt, so you can raise the cost later without a flag day.
  • Add a server-side pepper from KMS so a database dump alone is not crackable.
  • Check new passwords against breach corpora (k-anonymity API) instead of enforcing symbol rules.
  • Rate limit by account and by IP, and alert on distributed low-and-slow attempts.
Test yourself

You find `sha256(salt + password)` in a legacy service. What is the actual problem?

07The mistakes that ship

MistakeWhat breaksFix
AES-ECBIdentical blocks encrypt identically, so patterns leakAEAD mode
Nonce reuse in GCMPlaintext XOR and forgeryRandom nonces under the limit, or SIV / XChaCha
Unauthenticated CBCPadding oracles, bit flippingAEAD, or encrypt-then-MAC if stuck
Fast hash for passwordsOffline cracking at GPU speedArgon2id with tuned memory
=== on MACs or tokensTiming side channeltimingSafeEqual after a length check
Keys in source or env filesEvery clone and CI log has themKMS, secrets manager, workload identity
Home-made protocolSubtle composition bugs nobody reviewedTLS, Noise, libsodium boxes, Tink
Q1Encrypt or hash a credit card number?+

Encrypt if you need it back (envelope encryption, tokenise where possible). Hash if you only need to match (HMAC with a secret key, so the hash cannot be brute forced over the 16-digit space).

Q2Why HMAC instead of hash(secret || message)?+

Length-extension attacks on Merkle-Damgard hashes (SHA-256) let an attacker append data and compute a valid tag without the secret. HMAC is designed to prevent this.

Q3What does post-quantum change for you today?+

Harvest-now-decrypt-later means key exchange matters first. Hybrid X25519 + ML-KEM is shipping in TLS; signatures (ML-DSA) matter later for long-lived roots.

Q4Why is encrypting a JWT not a substitute for signing it?+

Encryption hides claims but, unless it is authenticated, does not prove who issued them. JWE with an AEAD algorithm authenticates the content to the key holder, but anyone holding a shared key could mint one. Sign for authenticity; encrypt only if claims are sensitive.

Finished this one?
Tick it and it shows as done on the roadmap and the course.