Almost every security promise a computer makes, from the padlock in your browser to a software update you can trust, rests on four tools: hash functions, ciphers, key exchange and digital signatures. This chapter is about what each one actually guarantees, what it quietly does not, and the handful of mistakes that break real systems far more often than anyone breaks the mathematics.
Common mix-up: encrypted does not mean tamper-proof. Plain encryption hides what a message says, but on its own it does not stop someone from changing it on the way, sometimes in precise, predictable ways. Secrecy and integrity are separate jobs. Modern systems use authenticated encryption, which does both at once, and the tamper test below shows why that matters.
Background first: VPNs, tunnels & overlays on networks.jelia.nyc shows where encrypted tunnels sit in a network and what they carry. This chapter assumes that picture and opens up the machinery inside the tunnel.
Cryptographers tell the story with three characters. Alice wants to send a message to Bob. Between them is a network neither controls, and on it sits Eve, who can read everything that passes and sometimes change it. Every router, Wi-Fi access point and cable on the path is a possible Eve. The goal is not to stop Eve from seeing packets, which is impossible, but to make what she sees useless and what she changes detectable.
In 1883 the Dutch cryptographer Auguste Kerckhoffs set down a rule that still governs the field: a cipher must stay secure even if everything about it except the key falls into enemy hands. This is Kerckhoffs's principle. It is why AES, SHA-256 and ChaCha20 are published in full, in standards anyone can download, and attacked in public by researchers for years before anyone relies on them. A secret algorithm is not stronger; it is merely unexamined. When it leaks, and algorithms always leak because they ship inside software, you cannot change it the way you can change a key.
The same idea explains the most repeated advice in security engineering: don't roll your own crypto. Designing a cipher is hard, but the bigger danger is subtler. Most real-world breaks come from building a system out of perfectly good primitives and wiring them together wrongly: the right cipher in the wrong mode, a good key generated from a weak random number, a sound signature checked by code that never actually looks at the result. The safe path is a well-reviewed library with a high-level interface, such as libsodium or your platform's TLS stack, that makes these decisions for you.
| Tool | What it guarantees | What it does not do | Names you will meet |
|---|---|---|---|
| Hash function | A short, fixed-size fingerprint; nobody can find two inputs with the same one | Hide anything (no key), or slow down password guessing | SHA-256, SHA-3, BLAKE2 |
| Symmetric cipher (AEAD) | Secrecy and integrity, for anyone holding the shared key | Get the key to the other side in the first place | AES-GCM, ChaCha20-Poly1305 |
| Key exchange | Two strangers agree on a shared secret over a public wire | Tell you who you agreed it with | X25519, ECDH, ML-KEM |
| Digital signature | This exact data was approved by the holder of one private key | Prove that key belongs to the person you think | Ed25519, ECDSA, RSA-PSS, ML-DSA |
Each tool covers the previous one's blind spot, and real protocols stack them. A TLS connection (Chapter 3) uses key exchange to agree on keys, a hash-based function to stretch them, authenticated encryption to protect the data, and signatures to prove the server is genuine. WireGuard does the same with Curve25519, ChaCha20-Poly1305 and BLAKE2s.
A cryptographic hash function takes any input, a word or a whole hard disk, and grinds it into a fixed-size output called a digest. SHA-256, standardized by NIST in FIPS 180-4, always produces 256 bits, usually written as 64 hexadecimal characters. There is no key. Anyone can compute the hash of anything, and the same input always gives the same output.
What makes it cryptographic is three kinds of resistance, from weakest to strongest requirement:
Collisions must exist, since infinitely many inputs share 2256 outputs. The point is that nobody can find one. A good hash behaves like a perfect scrambler: change one character of the input and about half of the output bits flip, with no visible relationship between the two digests. That is the avalanche effect, and you can watch it below.
Type in either box. The page computes the real SHA-256 of both strings on every keystroke and draws the second digest as a 16 × 16 grid of its 256 bits. A filled square is a 1, an empty one a 0; squares marked with an orange ring are bits that differ from the first digest. Change a single letter and count how many flip. It hovers near 128, half of them, every time.
The hash is computed by a SHA-256 written out in this page, so it works offline and over plain http. Where your browser offers the built-in crypto.subtle.digest, the Check readout confirms both give the same answer.
Hash functions don't fail all at once. They erode, and collision resistance goes first. MD5, with its 128-bit output, was already showing cracks in the mid-1990s, and practical collisions were demonstrated in 2004. The IETF's RFC 6151 records that MD5 must not be used where collision resistance is required, such as digital signatures. SHA-1 followed: in 2017 researchers from CWI Amsterdam and Google published two different PDF files with the same SHA-1 digest, the "SHAttered" result. NIST's transition guidance, SP 800-131A, disallows SHA-1 for generating digital signatures.
Why does a collision break signatures? Because systems sign the hash of a document, not the document. If an attacker can prepare two documents with the same hash, one harmless and one not, and get the harmless one signed, the signature is equally valid on the other. The defense is to use SHA-256 or SHA-3 for anything new and to treat any remaining MD5 or SHA-1 in a signing path as a defect to fix. MD5 survives harmlessly as a checksum against accidental corruption, where nobody is trying to collide it.
One more thing a hash is not: a password store. SHA-256 is designed to be fast, billions of hashes a second on a graphics card, which is exactly wrong when the attacker is guessing passwords. Passwords need deliberately slow, salted functions such as Argon2id, scrypt or bcrypt. That story belongs to Chapter 4.
SHA-256 pads the message to a multiple of 512 bits (appending a 1 bit, zeros, and the original length), splits it into 64-byte blocks, and feeds them one at a time through a compression function. The function keeps eight 32-bit words of state. For each block it expands the 16 input words into 64, then runs 64 rounds that mix the state with additions, bitwise choices, majorities and rotations. The eight starting words are the fractional parts of the square roots of the first eight primes, and the 64 round constants come from the cube roots of the first 64 primes, "nothing up my sleeve" numbers that anyone can check were not chosen to hide a weakness.
The construction, called Merkle–Damgård, has a known quirk: knowing H(m) and the length of m lets you compute the hash of m followed by extra data, without knowing m. That is why you must not build a keyed check as H(secret ‖ message). HMAC, below, exists precisely to avoid this. SHA-3, standardized in FIPS 202, uses a different "sponge" design that doesn't have the quirk.
A symmetric cipher uses the same secret key to lock and unlock. It is fast, which is why every byte of bulk data on the internet is protected this way. Ciphers come in two shapes. A block cipher such as AES (FIPS 197) is a keyed scrambler for fixed-size chunks: 128 bits in, 128 bits out, with a 128-, 192- or 256-bit key. A stream cipher such as ChaCha20 (RFC 8439) generates a long, unpredictable keystream from the key and a nonce (a "number used once"), and XORs it with the data.
A block cipher on its own only handles one 16-byte block. To encrypt a whole message you need a mode of operation, a recipe for applying it block after block. The obvious recipe, encrypting each block independently, is called ECB, Electronic Codebook. It is also the classic mistake. Because the same key turns the same plaintext block into the same ciphertext block every time, ECB preserves patterns. Encrypt a picture with large flat areas and the outline of the picture survives, drawn in a different texture.
The fix is to make every block's encryption depend on something that changes. CBC mode XORs each plaintext block with the previous ciphertext block before encrypting, starting from a random initialization vector. CTR mode encrypts a counter (nonce plus block number) and XORs the result with the data, which turns a block cipher into a stream cipher. Either way, identical plaintext blocks no longer produce identical ciphertext. The instrument below lets you see the difference rather than take it on trust.
The left panel is a small padlock image, 96 × 64 pixels, drawn by the page. The right panel is the same pixels encrypted 8 bytes at a time with a keyed block cipher, shown as one shade per byte. Switch modes. In ECB the padlock is still plainly there. In CBC and CTR it dissolves into noise. The last mode encrypts a second picture with the same CTR nonce and XORs the two ciphertexts, which cancels the keystream and shows what nonce reuse gives away.
Toy cipher, for illustration only. To keep the page small and instant, the block cipher here is a four-round 64-bit Feistel network invented for this demonstration. It is not AES, it is not secure, and nothing about it should be reused. The mode behavior it shows is exactly what AES does in the same modes.
CBC and CTR fix the pattern leak, but they still only provide secrecy. Modern protocols use AEAD, authenticated encryption with associated data, which encrypts the message and computes a cryptographic checksum over it in one operation. The two you will meet everywhere are AES-GCM (NIST SP 800-38D), which pairs AES in counter mode with a polynomial authenticator called GHASH, and ChaCha20-Poly1305 (RFC 8439), which pairs the ChaCha20 stream cipher with the Poly1305 authenticator. Both attach a 128-bit tag. On decryption, if a single bit of the ciphertext, the tag or the associated data has changed, the operation fails and returns nothing.
The "associated data" is information that must be authenticated but not hidden, such as a packet header or a record number. Binding it in means an attacker can't take a valid encrypted body and staple it to a different header.
AEAD has one hard rule: never reuse a nonce with the same key. Both GCM and ChaCha20 are counter-mode designs underneath, so the nonce selects the keystream. Encrypt two messages with the same key and nonce and the keystreams are identical; XOR the two ciphertexts and the keystream cancels, leaving the XOR of the two plaintexts, which is often enough to read both. The last mode in Instrument 2 shows exactly that. For GCM it is worse still: a repeated nonce can expose the internal authentication key, after which forged messages pass the check. SP 800-38D therefore limits a key used with random 96-bit nonces to 232 messages.
How do real systems avoid repeats? TLS 1.3 and WireGuard use a counter: each message gets the next number, and the key is replaced long before the counter could wrap. Where a counter is awkward, for example many machines sharing a key, use a nonce-misuse-resistant mode such as AES-GCM-SIV, or a design with a longer random nonce such as XChaCha20-Poly1305, so that collisions are vanishingly unlikely.
| AEAD | Key | Nonce | Tag | Where it shines |
|---|---|---|---|---|
| AES-128-GCM / AES-256-GCM | 128 / 256 bits | 96 bits | 128 bits | CPUs with AES instructions; most server and desktop traffic |
| ChaCha20-Poly1305 | 256 bits | 96 bits | 128 bits | Fast in plain software; phones and devices without AES hardware; WireGuard |
A message authentication code (MAC) is a keyed checksum. Whoever holds the key can compute a short tag for a message; whoever holds the same key can recompute it and compare. Without the key you can't produce a valid tag for any message you haven't seen tagged. The most widely used construction is HMAC (RFC 2104), which wraps a hash function in two passes with the key mixed in differently each time, sidestepping the length-extension quirk mentioned above.
Why does encryption need it? A stream cipher encrypts by XOR, and XOR has no memory of what was intended. If Eve flips bit 3 of byte 5 of the ciphertext, Bob's decryption flips bit 3 of byte 5 of the plaintext, and nothing else. She can't read the message, but if she can guess its layout (and protocols have predictable layouts) she can change a known field to a value of her choosing. That property is called malleability, and it applies even to a perfectly strong cipher. The tamper test lets you try it with real ChaCha20.
Alice sends Bob a 20-character instruction encrypted with ChaCha20, the real RFC 8439 cipher, under a demo key. The 20 boxes are the ciphertext bytes as they cross the wire. Click any box to flip its lowest bit, or use the preset that rewrites the amount. Below, see what Bob decrypts. Then switch on the tag: Alice adds an HMAC-SHA-256 over the ciphertext, Bob recomputes it before decrypting, and any change at all is rejected.
ChaCha20-Poly1305 does this job with the Poly1305 authenticator instead of HMAC; HMAC-SHA-256 is used here because it is built from the hash you met above. The key, nonce and message are made up for this page.
When people combined a cipher and a MAC by hand, there were three orders to choose from: MAC the plaintext and encrypt both, MAC the plaintext and send it alongside, or encrypt first and MAC the ciphertext. Only the last, encrypt-then-MAC, is safe in general, because the receiver can reject a forged message before decrypting anything. Older protocols chose differently and paid for it with a long series of "padding oracle" problems, where tiny differences in how a server rejected bad messages leaked the plaintext. AEAD modes build in the right order, which is the main reason to use them instead of assembling your own.
The final comparison matters too. Comparing the received tag with the expected one byte by byte and stopping at the first mismatch leaks, through timing, how many leading bytes were right. Libraries provide a constant-time comparison (for example hmac.compare_digest in Python or crypto.timingSafeEqual in Node.js); use it.
Symmetric encryption has a chicken-and-egg problem. Alice and Bob need a shared secret key before they can talk privately, and they can't send it over the wire Eve is watching. In 1976 Whitfield Diffie and Martin Hellman published the answer: two parties can agree on a secret in full public view.
The usual picture is paint. Everyone agrees, publicly, on a starting color: yellow. Alice picks a secret color and mixes it into a pot of yellow; Bob does the same with his own secret color. They swap pots in public. Alice adds her secret color to Bob's pot; Bob adds his to Alice's. Both pots now contain yellow plus Alice's secret plus Bob's secret, so they are the same color. Eve saw the yellow and both mixed pots, but to get the final color she would have to un-mix paint, separating a secret color back out of a mixture. That is the one-way step.
In numbers, mixing is exponentiation in modular arithmetic. Everyone knows a prime p and a base g. Alice picks a secret a and sends A = ga mod p; Bob picks b and sends B = gb mod p. Alice computes Ba and Bob computes Ab, and both get gab mod p. Recovering a from A is the discrete logarithm problem: easy to state, and with large enough numbers, no known classical method solves it in any useful time.
Pick the two secrets with the sliders and a prime size with the chips. The page does the real modular arithmetic and shows both sides arriving at the same shared number, alongside the paint picture. It also plays Eve: it tries every possible secret until one matches Alice's public value, and reports how many tries that took. With these toy primes Eve wins instantly. Real key exchange uses numbers so large that the same search would outlast the universe.
| Step | Who knows it | Value |
|---|
Classic Diffie–Hellman needs primes of 3072 bits for 128-bit security. Elliptic-curve Diffie–Hellman does the same job with much smaller numbers, because the discrete logarithm problem on a well-chosen elliptic curve has no known shortcut of the kind that helps with plain modular arithmetic. The dominant choice today is X25519 (RFC 7748), Diffie–Hellman on Curve25519: public keys and the shared secret are 32 bytes, and the design avoids many pitfalls of older curves, such as having to validate that a received point really lies on the curve.
The single most important habit is to use ephemeral keys: a fresh key pair for every connection, thrown away when it ends. That gives forward secrecy. If a server's long-term signing key is stolen next year, the attacker still can't decrypt traffic recorded today, because the keys that protected it no longer exist anywhere. TLS 1.3 requires ephemeral key exchange for exactly this reason; WireGuard rotates its session keys every couple of minutes.
The raw Diffie–Hellman output is not used directly as a key. It is a random-looking group element, not uniformly random bytes, and one exchange usually needs several keys (one per direction, for example). A key derivation function turns it into as many independent keys as needed. The standard one is HKDF (RFC 5869), built on HMAC in two steps: extract concentrates the input's randomness into one strong pseudorandom key, and expand stretches that into labeled output keys, so that "client write key" and "server write key" can never collide.
Diffie–Hellman protects against a passive Eve, who only listens. Against an active attacker who can intercept and replace messages, plain key exchange fails quietly: she runs one exchange with Alice and another with Bob, and relays between them, reading everything. Each side has a perfectly good shared key, just with the wrong party. This is the man-in-the-middle problem, and the cure is authentication: one or both sides sign their key-exchange messages with a long-term key the other can check. That is the job of signatures, next, and of certificates in Chapter 3.
Public-key cryptography splits a key in two. The private key is kept secret by its owner; the public key is published. For signatures, the private key makes the stamp and the public key checks it. Alice signs a document by computing a value from its hash and her private key. Anyone holding her public key can verify that the signature matches that exact document; change a single bit of the document and verification fails.
A valid signature proves two things: integrity (the data is exactly what was signed) and origin (it was signed with the private key matching this public key). Unlike a MAC, the verifier can't create signatures, so a signature can be checked by the whole world and still convince a third party. That is why software updates, firmware, package registries and certificates are signed rather than MACed.
What a signature does not prove is that the public key belongs to who you think. If Eve hands you her public key labeled "Alice," her signatures will verify perfectly. Binding keys to identities is the work of public-key infrastructure and certificates, in Chapter 3; signing the software you install is covered in Chapter 16.
| Algorithm | Based on | Public key | Signature | Note |
|---|---|---|---|---|
| Ed25519 (RFC 8032) | Edwards curve | 32 bytes | 64 bytes | Deterministic: no per-signature random number to get wrong |
| ECDSA P-256 (FIPS 186-5) | Elliptic curve | 65 bytes (33 compressed) | 64 bytes raw (about 72 in DER encoding) | Needs a fresh secret number per signature, or the RFC 6979 deterministic method |
| RSA-3072 (FIPS 186-5) | Factoring | 384 bytes | 384 bytes | Oldest and most widely supported; use PSS padding for new designs |
ECDSA and its predecessor DSA use a secret random number, usually called k, for every signature. If the same k is ever used for two different messages under the same key, simple algebra on the two signatures reveals the private key, and if k is merely predictable or biased, statistical methods can recover the key from many signatures. It is the signature counterpart of nonce reuse in AEAD, and it has caused real key compromises in deployed products.
The defenses are mechanical. RFC 6979 derives k deterministically from the private key and the message hash, so no random number generator is involved. Ed25519 builds the same idea into the algorithm. If you use ECDSA, make sure your library uses RFC 6979 or a vetted random source; if you are choosing fresh, Ed25519 removes the hazard.
A well-designed cipher has no shortcut: the best attack is to try keys until one works. Each extra key bit doubles the work. That doubling is the whole story. A 40-bit key, the legal export limit for some software in the 1990s, has about a trillion possibilities, which a single modern machine can sweep quickly. A 56-bit DES key fell to purpose-built hardware in the late 1990s. A 128-bit key has 272 times as many possibilities as the DES key; no improvement in hardware closes a gap like that.
Different algorithms need very different key lengths for the same strength, because their best attacks differ. NIST's key-management recommendation, SP 800-57 Part 1, expresses everything as a security strength in bits, meaning "as hard as brute-forcing a key of this many bits."
| Security strength | Symmetric | RSA / classic DH modulus | Elliptic curve key |
|---|---|---|---|
| 112 bits | (3-key Triple DES, now retired) | 2048 bits | 224–255 bits |
| 128 bits | AES-128 | 3072 bits | 256–383 bits |
| 192 bits | AES-192 | 7680 bits | 384–511 bits |
| 256 bits | AES-256 | 15360 bits | 512+ bits |
Source: NIST SP 800-57 Part 1 Revision 5, Table 2 (comparable security strengths). X25519 and Ed25519 use a 255-bit curve and are treated as 128-bit strength.
Slide the key length and pick an attacker. The page computes the expected time to find a key by trying them, which on average takes half the key space, and places it on a logarithmic timeline from a microsecond to far beyond the age of the universe. Every step right on the slider doubles the time.
Attacker speeds are round, hypothetical numbers chosen to span the range. The age of the universe is taken as 13.8 billion years.
All of the strength above assumes the key was chosen uniformly at random. A 128-bit key generated from a source with only 32 bits of real unpredictability is a 32-bit key, however long it looks. Keys, nonces, salts and the per-signature numbers just discussed all need a cryptographically secure pseudorandom number generator (CSPRNG): one whose output can't be predicted even by someone who has seen lots of previous output.
Ordinary random functions are not that. JavaScript's Math.random, C's rand and most language "random" modules are built for simulations and games: fast and statistically even, but with internal state that can be reconstructed from a modest sample of output. MITRE catalogs this as CWE-338. The fix is easy, because every operating system already gathers unpredictability from hardware events and exposes a CSPRNG. In a browser that is crypto.getRandomValues; in Node.js, crypto.randomBytes; in Python, the secrets module.
// Wrong: Math.random is not a CSPRNG; its output can be predicted const token = Math.random().toString(36).slice(2);
// Right: 32 bytes from the operating system's CSPRNG const key = crypto.getRandomValues(new Uint8Array(32));
The other common failure is not using a weak generator but reusing a good value: a hard-coded key in source code, the same IV for every message, a test key shipped to production. Treat every key and nonce as something generated at the moment of need, stored in a key-management system or hardware module, and rotated on a schedule. The OWASP Cryptographic Storage Cheat Sheet is a good checklist.
A sufficiently large quantum computer would change the picture for public-key cryptography. In 1994 Peter Shor described a quantum algorithm that factors large numbers and computes discrete logarithms efficiently. That would break RSA, classic Diffie–Hellman and elliptic-curve cryptography alike: X25519, ECDSA and Ed25519 included. No quantum computer today comes close to the size needed, and nobody can say with confidence when one will.
Symmetric cryptography and hashes fare far better. Grover's algorithm can search an unstructured space of N possibilities in about √N quantum steps, which in the idealized worst case halves the effective bit-strength of a key: AES-128 would offer roughly 64-bit security against such a machine, AES-256 roughly 128. In practice Grover's search parallelizes badly and each step is expensive, so the real threat is far smaller; still, long-lived systems increasingly choose 256-bit symmetric keys as cheap insurance.
The urgent risk is harvest now, decrypt later. An adversary can record encrypted traffic today and store it until a quantum computer exists. Anything that must stay secret for ten or twenty years is therefore already exposed if its key exchange is quantum-vulnerable. That is why key exchange is moving first.
In August 2024 NIST published the first three post-quantum standards, built on mathematical problems with no known quantum shortcut:
| Standard | Algorithm | Replaces | Built on |
|---|---|---|---|
| FIPS 203 | ML-KEM (from CRYSTALS-Kyber) | Key exchange (ECDH, RSA key transport) | Module lattices |
| FIPS 204 | ML-DSA (from CRYSTALS-Dilithium) | Signatures (ECDSA, Ed25519, RSA) | Module lattices |
| FIPS 205 | SLH-DSA (from SPHINCS+) | Signatures, as a conservative backup | Hash functions only |
The transition is happening in hybrid form: a connection runs a classical exchange such as X25519 and ML-KEM together and derives its keys from both, so it stays secure unless both are broken. Major browsers and TLS libraries already offer an X25519 plus ML-KEM-768 hybrid. A NIST draft, IR 8547, proposes deprecating the weaker quantum-vulnerable public-key algorithms after 2030 and disallowing all of them after 2035. The practical lesson for defenders is crypto agility: know where your systems use which algorithms, and make sure you can swap them by configuration rather than by rewrite.
Strong algorithms are rarely the weak point. These are, and each has a clean fix. The CWE numbers are MITRE's catalog entries, useful when reading audit reports or scanner output.
| Mistake | What goes wrong | Fix | Catalog |
|---|---|---|---|
| Homegrown or obsolete algorithm | Unreviewed or broken (DES, RC4, MD5 or SHA-1 for signatures) | AES-GCM or ChaCha20-Poly1305; SHA-256 or SHA-3; Ed25519 | CWE-327 |
| ECB mode | Identical blocks encrypt identically; patterns leak | Use an AEAD mode, never raw block encryption | CWE-327 |
| Nonce or IV reuse | Keystream cancels; GCM tags become forgeable | Counter nonces, key rotation, or a misuse-resistant mode | CWE-323 |
| Encryption without authentication | Ciphertext can be edited predictably | AEAD, or encrypt-then-MAC | CWE-327 |
| Weak randomness | Keys and tokens can be predicted | OS CSPRNG: getRandomValues, randomBytes, secrets | CWE-338 |
| Fast hash for passwords | Billions of guesses per second | Argon2id, scrypt or bcrypt (Chapter 4) | CWE-916 |
| Static, ephemeral-free key exchange | One stolen key decrypts all recorded traffic | Ephemeral X25519 (forward secrecy) | – |
| Hard-coded keys | Anyone with the code or binary has the key | Secrets manager or hardware module; rotate | CWE-321 |