π Cryptography You Actually Need to Know
Cryptography You Actually Need to Know
Almost nobody breaks AES. Systems get broken at the joints: a mode of operation chosen badly, a nonce reused, a key derived from a password with a fast hash, a signature verified with a comparison that leaks timing. This module covers the primitives and, more importantly, the rules for wiring them together.
Two families, two different jobs
Symmetric cryptography uses one key for both encryption and decryption. It is fast β a modern CPU with AES-NI instructions encrypts gigabytes per second β but it leaves you with the key distribution problem: both sides must already share a secret.
Asymmetric (public key) cryptography uses a key pair. Anyone can encrypt to your public key or verify your signature; only the private key decrypts or signs. It is orders of magnitude slower and it can only handle small messages, so it is never used to encrypt bulk data directly.
Real protocols are hybrid. A TLS connection uses an asymmetric key agreement (ECDHE) plus a signature to authenticate the server, derives a symmetric key from that exchange, and then encrypts every byte of traffic with AES-GCM or ChaCha20-Poly1305. Asymmetric crypto solves trust and key agreement; symmetric crypto does the work.
AES is not the interesting part β the mode is
AES is a block cipher: it transforms exactly 16 bytes at a time under a 128-, 192- or 256-bit key. Real messages are not 16 bytes, so a mode of operation describes how blocks are chained. The mode, not the cipher, is where systems fail.
| Mode | What it does | Verdict |
|---|---|---|
ECB | Encrypts each block independently | Never use. Identical plaintext blocks produce identical ciphertext blocks, so structure leaks straight through. The classic demonstration is a bitmap of the Linux mascot encrypted in ECB β the penguin is still clearly visible in the "encrypted" image. |
CBC | XORs each plaintext block with the previous ciphertext block; needs an IV and padding | Only with a separate MAC, applied encrypt-then-MAC. Unauthenticated CBC is malleable and vulnerable to padding-oracle attacks (Vaudenay, 2002), the family that produced POODLE and Lucky 13. |
CTR | Encrypts a counter to make a keystream, then XORs it with the plaintext | No padding, parallelisable, but provides zero integrity on its own β and a repeated counter is catastrophic. |
GCM | CTR encryption plus a GHASH authentication tag (AEAD) | Preferred. Confidentiality and integrity in one pass; hardware-accelerated on x86 and ARM. |
ChaCha20-Poly1305 | Stream cipher plus a Poly1305 tag (AEAD, RFC 8439) | Preferred, especially without AES hardware β it is fast and naturally constant-time in software. This is why mobile clients often negotiate it. |
AEAD means Authenticated Encryption with Associated Data. It gives you three things at once: the ciphertext is confidential, any modification is detected (the 128-bit tag fails to verify), and you can bind unencrypted context β a message ID, a record header, a user ID β into the tag as associated data, so an attacker cannot move a valid ciphertext to a different context. If you are choosing a mode in 2026 and it is not AEAD, choose again.
// Browser-native AES-256-GCM. No library, no CDN.
const key = await crypto.subtle.generateKey(
{ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']);
const iv = crypto.getRandomValues(new Uint8Array(12)); // 96-bit nonce
const ct = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv, additionalData: aad }, key, plaintext);
// ct = ciphertext || 128-bit authentication tag (WebCrypto appends it)
openssl enc -aes-256-gcm does not do what people assume: the enc utility has no way to emit or check an AEAD tag, so you get CTR-mode encryption with no integrity at all. Use a library binding (or openssl_encrypt() in PHP, which returns the tag by reference) rather than enc for AEAD.
Nonces and IVs: the single most common fatal bug
A nonce is a number used once. It does not need to be secret and it usually travels alongside the ciphertext in the clear. It must never repeat under the same key.
- CTR / GCM / ChaCha20: reusing a nonce means the same keystream encrypts two messages. XOR the two ciphertexts and the keystream cancels out, leaving the XOR of the two plaintexts β which is routinely solvable. In GCM it is worse: nonce reuse leaks the GHASH authentication subkey, so the attacker can forge valid tags for messages you never sent. This is not theoretical; the "forbidden attack" has been demonstrated against real TLS servers with broken nonce generation.
- CBC: the IV must be unpredictable, not merely unique. A predictable IV was the basis of the BEAST attack against TLS 1.0.
- Counting: NIST SP 800-38D recommends a 96-bit GCM nonce. With random 96-bit nonces, birthday collisions become a real risk past roughly 232 messages under one key β so either use a deterministic counter, rotate the key, or use XChaCha20-Poly1305, whose 192-bit nonce makes random selection safe.
Hash functions: three properties, and what "broken" means
A cryptographic hash maps arbitrary input to a fixed-size digest, and is expected to offer:
- Preimage resistance β given
H(x), you cannot findx. - Second-preimage resistance β given
x, you cannot find a differentywithH(y) = H(x). - Collision resistance β you cannot find any pair
x β ywith the same digest. This is the weakest of the three (the birthday bound means a 256-bit hash offers only 128-bit collision security) and it is the one that falls first.
MD5 collisions have been practical since Wang and Yu's 2004β2005 work, and chosen-prefix collisions are cheap. The Flame malware used an MD5 chosen-prefix collision in 2012 to forge a code-signing certificate that chained to a Microsoft CA. SHA-1 fell publicly in February 2017 when CWI Amsterdam and Google published SHAttered, two different PDFs with the same SHA-1 digest; a chosen-prefix SHA-1 collision followed in 2020.
What that does mean: never use MD5 or SHA-1 anywhere an attacker influences the content being hashed β certificates, code signing, git-style content addressing of untrusted input, deduplication that grants trust. What it does not mean: a collision is not a preimage. Nobody can take the MD5 digest of an unpredictable input and invert it β though a digest of something guessable, such as a password, is still recovered instantly by brute force, which is a different problem entirely. HMAC-MD5 and HMAC-SHA-1 are not broken by these collision results, because HMAC's security rests on the compression function behaving as a pseudorandom function rather than on collision resistance β which is why HMAC-SHA-1 survives inside TOTP. Migrate anyway; there is no reason to stay.
Use SHA-256 or SHA-512 (SHA-2), SHA-3, or BLAKE2/BLAKE3 for new work. Generate digests across algorithms β
$ printf 'hello' | sha256sum
2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 -
HMAC: why you cannot just glue a key to a message
The obvious way to authenticate a message with a shared key is H(key || message). It is broken for every MerkleβDamgΓ₯rd hash β MD5, SHA-1 and the whole SHA-2 family β because of length extension. The digest is the internal state at the end of the input, so an attacker who knows H(key || message) and the length of the key can resume from that state and compute a valid tag for key || message || padding || attacker_data without ever learning the key.
HMAC (RFC 2104, FIPS 198-1) fixes this with a nested construction:
HMAC(K, m) = H( (K β opad) || H( (K β ipad) || m ) )
The outer hash means the attacker never sees a raw internal state. SHA-3 and BLAKE2 are sponge/HAIFA constructions and are not length-extendable, so H(key || message) is safe there β but there is no reason not to use HMAC, which is universally available. Always compare tags with a constant-time comparison (hash_equals() in PHP, hmac.compare_digest() in Python); a byte-by-byte == leaks the correct tag one byte at a time. Compute an HMAC β
Key derivation: two completely different problems
"KDF" covers two jobs that people constantly conflate:
- Deriving keys from a high-entropy secret β for example turning a Diffie-Hellman shared secret into four separate keys. Use HKDF (RFC 5869), which extracts then expands with a context label. It is fast, deliberately.
- Deriving a key from a low-entropy secret a human chose β a password or passphrase. Here you need a deliberately slow function, and ideally a memory-hard one: PBKDF2 (RFC 8018 β an iteration count and nothing else, so GPUs keep their advantage; use it when FIPS demands it), scrypt (RFC 7914, adds a memory cost that blunts GPUs and ASICs) or Argon2id (RFC 9106, the current first choice, tunable in time, memory and parallelism).
Using a plain hash β even SHA-256, even salted β for the second job is the mistake that turns a database leak into a mass account takeover. That is covered in depth in the passwords module.
Public key: RSA, elliptic curves, and equivalent strength
RSA security rests on integer factorisation; elliptic-curve security rests on the discrete logarithm problem in a curve group. ECC reaches the same security level with dramatically smaller keys, which is why every modern protocol defaults to it. NIST SP 800-57 Part 1 Rev 5 gives the comparison:
| Security strength | RSA / finite-field DH modulus | Elliptic curve | Symmetric equivalent |
|---|---|---|---|
| 112 bits | 2048 | 224β255 | 3TDEA |
| 128 bits | 3072 | 256β383 (P-256, Curve25519) | AES-128 |
| 192 bits | 7680 | 384β511 (P-384) | AES-192 |
| 256 bits | 15360 | 512+ (P-521) | AES-256 |
RSA-2048 is the practical floor and is still everywhere; RSA-3072 or ECDSA P-256 is the sensible target for new certificates. Ed25519 (EdDSA over Curve25519, RFC 8032) is the modern default for signing outside the web PKI β 32-byte public keys, 64-byte signatures, roughly 128-bit security, deterministic nonces that remove an entire class of catastrophic implementation bug, and no curve-parameter footguns. X25519 (RFC 7748) is its key-agreement counterpart. Note that RSA padding matters as much as key size: use OAEP for encryption and PSS for signatures, never raw or PKCS#1 v1.5 encryption padding, which has been attackable since Bleichenbacher's 1998 result. Generate keys and secrets β
Diffie-Hellman and forward secrecy
Diffie-Hellman lets two parties who have never met derive a shared secret over a public channel: each sends a public value, each combines it with their own private value, and both arrive at the same result that an eavesdropper cannot compute. When both sides generate a fresh, throwaway key pair for every connection β ephemeral DH, the E in ECDHE β you get forward secrecy: recording today's traffic and stealing the server's long-term private key next year does not decrypt it, because the ephemeral keys were discarded. Static RSA key exchange had no forward secrecy at all, which is precisely why TLS 1.3 removed it.
Signing is not encryption
Encryption uses the recipient's public key and is undone with their private key: it provides confidentiality. Signing uses the signer's private key and is verified with their public key: it provides authenticity and integrity, and no confidentiality whatsoever β a signed message is still fully readable. The two use different padding schemes and, in good designs, different key pairs. "Encrypting with the private key" is a phrase you will see; treat it as a red flag, because a real signature scheme (PSS, ECDSA, Ed25519) is not simply encryption run backwards.
Randomness
Keys, nonces, salts, session identifiers and password-reset tokens must come from a cryptographically secure pseudorandom number generator seeded by the operating system. Math.random() is not one β V8 implements it with xorshift128+, and an observer who sees a handful of outputs can reconstruct the internal state and predict the rest. Use crypto.getRandomValues() in the browser, random_bytes() in PHP, secrets in Python, crypto/rand in Go, or /dev/urandom. Every generator on this site β the key generator, the password generator and the AES tool's nonces β draws from crypto.getRandomValues(), and nothing generated in your browser is transmitted anywhere.
Post-quantum, honestly
Two quantum algorithms matter. Shor's algorithm solves integer factorisation and discrete logarithms efficiently, which would break RSA, finite-field Diffie-Hellman and all elliptic-curve cryptography β every asymmetric algorithm in use on the public internet today. Grover's algorithm gives only a quadratic speed-up on unstructured search, so it nominally halves the effective strength of a symmetric key; AES-256 remains comfortable, and Grover parallelises poorly enough that the practical margin is wider than the headline suggests. Hash functions are similarly bruised but not broken.
No quantum computer capable of running Shor's algorithm against a 2048-bit key exists, and nobody can credibly say when one will. The concern that justifies acting now is harvest now, decrypt later: an adversary who records encrypted traffic today can decrypt it whenever the capability arrives, so anything that must stay secret for a decade is already exposed. Long-lived signatures matter less, because a signature made today can simply be re-made with a new algorithm.
NIST published the first post-quantum standards in August 2024: FIPS 203 (ML-KEM, derived from CRYSTALS-Kyber, for key encapsulation), FIPS 204 (ML-DSA, from CRYSTALS-Dilithium, for signatures) and FIPS 205 (SLH-DSA, from SPHINCS+, a conservative hash-based signature). Deployment on the web has begun with hybrid key exchange β a TLS group that combines X25519 with ML-KEM-768, so the connection stays secure if either component holds. If you see an unfamiliar group name in a modern handshake, that is usually what it is.
Don't roll your own: the practical rules
- Use a vetted high-level library β libsodium, the platform's WebCrypto or
cryptomodule, Tink β not raw primitives glued together. - Encrypt with AEAD. If you genuinely must compose, it is encrypt-then-MAC with independent keys, never MAC-then-encrypt.
- One key, one purpose. Derive per-purpose subkeys with HKDF instead of reusing a master key for encryption, MACs and tokens.
- Never reuse a nonce under a key. Track this explicitly; make it impossible in the API rather than a rule in a comment.
- Compare secrets in constant time. Do not branch or return early on secret data.
- Do not invent a protocol. Handshakes, session resumption and key rotation are where the hard parts live.
- Plan for rotation and algorithm agility on day one: version your ciphertext format so you can change algorithms without a migration crisis.
Practise on the toolkit: AES-GCM encrypt/decrypt β Β· key & secret generator β Β· hash generator β Β· HMAC calculator β
- Hybrid is normal: asymmetric crypto for key agreement and authentication, symmetric AEAD for the data.
- The mode of operation is the decision that matters. Prefer AES-GCM or ChaCha20-Poly1305; never ECB; never unauthenticated CBC or CTR.
- A repeated nonce under the same key breaks confidentiality and, in GCM, authentication too.
- MD5 and SHA-1 are dead for collision resistance β which kills them for certificates and signatures β but a collision is not a preimage.
- HMAC exists because
H(key || message)is length-extendable on MD5, SHA-1 and SHA-2. - Password hashing (Argon2id, scrypt, bcrypt, PBKDF2) and key derivation from strong secrets (HKDF) are different problems with different tools.
- ECC gets 128-bit security from a 256-bit key; RSA needs 3072 bits for the same.
- Randomness must come from a CSPRNG.
Math.random()is predictable. - Post-quantum standards exist (FIPS 203/204/205, 2024); hybrid TLS key exchange is the migration path, and "harvest now, decrypt later" is the reason to care today.