Block Ciphers and Modes: Why GCM Matters More Than AES
AES, DES, 3DES, and ChaCha20 compared by structure — then why the mode of operation is where deployed cryptography actually breaks.
By Toolery Team · September 2, 2026
Somebody on your team is going to look at two TLS cipher suites, see AES256 in one and AES128 in the other, and pick the bigger number. It's the obvious move. It is also, in the two configurations below, the wrong one:
ECDHE-RSA-AES256-SHA # 256-bit key, CBC mode, SHA-1 MAC
ECDHE-RSA-AES128-GCM-SHA256 # 128-bit key, GCM mode, authenticatedThe first has twice the key material and has been publicly attacked, in deployed form, at least three separate times. The second has never been broken. AES is identical in both, so everything that differs is in the mode.
A block cipher does one narrow thing: given a key, it maps a fixed-size block of bits to another block of the same size, reversibly. AES takes 16 bytes and returns 16 bytes, and for a fixed key it's a bijection on all 2128 possible blocks. It has no notion of a message, a file, or a TLS record. Encrypting 17 bytes is already outside its job description.
Everything that bridges the gap between "scrambles 16 bytes" and "encrypts an HTTP request" is the mode of operation. It decides four things:
- How blocks are chained to each other
- What randomness gets mixed in, and where it comes from
- How a message that isn't a multiple of 16 bytes is handled
- Whether the recipient can tell that someone tampered with the ciphertext
2126.1
best published attack on full AES-128, against 2128 for brute force
785 GB
traffic that pulled a cookie out of a 3DES connection (Sweet32)
184
public HTTPS servers found repeating a GCM nonce, which forfeits the key
How DES and AES build that permutation differently
DES uses a Feistel network. Split the 64-bit block into two 32-bit halves. Run the right half through a round function F along with a subkey, XOR the result into the left half, then swap the halves. Repeat 16 times.
The clever property is that F never has to be invertible. Decryption is the identical circuit with the subkeys applied in reverse order, because XOR undoes itself. In 1977, when DES was standardized as FIPS 46, that meant one piece of silicon did both directions. Cheap hardware was the entire point.
AES uses a substitution-permutation network. The full 16-byte block moves through four steps every round:
SubBytesreplaces each byte using an S-box built from multiplicative inversion in GF(28). This is the only nonlinear step, and it's what stops the whole cipher from collapsing into solvable linear algebra.ShiftRowsrotates the rows of the 4×4 byte state by 0, 1, 2, and 3 positions, spreading bytes across column boundaries.MixColumnsmultiplies each column by a fixed matrix over GF(28), so every output byte in a column depends on all four input bytes.AddRoundKeyXORs in the round key.
Every step has to be invertible, which means AES decryption needs a genuinely separate inverse circuit. The payoff is diffusion speed: after two AES rounds, every output byte depends on every input byte, while a Feistel round only ever touches half the block. That's why AES needs 10 rounds where DES needed 16.
AES-256's key schedule is, relative to its key size, weaker than AES-128's, and the best known related-key attacks are consequently better against AES-256 than against AES-128. None of them apply to TLS, which never lets an attacker choose relationships between keys, but "bigger key means strictly stronger cipher" is not a statement that survives contact with the literature.
For the single-key case that actually matters, the best published key recovery on the full cipher remains the 2011 biclique cryptanalysis by Bogdanov, Khovratovich and Rechberger, at 2126.1 operations for AES-128. Fifteen years of trying has bought a factor of two against a number that was already unreachable.
Why 3DES died from its block size, not its key
DES's 56-bit key was already uncomfortable when it shipped and indefensible by 1998, when the EFF's purpose-built Deep Crack machine recovered a key by brute force in under three days, on hardware the foundation says cost less than $250,000. The retrofit was Triple DES: encrypt, decrypt, encrypt with three separate keys.
C = E(K3, D(K2, E(K1, P))) # 168 bits of key material
# ~112 bits of actual securityThe middle decrypt exists purely for backward compatibility: set all three keys equal and you get plain DES. The gap between 168 and 112 comes from a meet-in-the-middle attack that trades memory for key search, which is also why double DES was never worth standardizing.
What actually killed 3DES was DES's 64-bit block, which it inherited unchanged. In CBC mode, ciphertext blocks are essentially random values, so the birthday bound says two will collide, and with a 64-bit block that becomes likely around 232 blocks, or 32 GB. A CBC collision leaks the XOR of the two corresponding plaintext blocks. If one of them is a session cookie repeated in every request on a long-lived connection, the attacker recovers it.
That's Sweet32, and 32 GB is the theoretical bound rather than the demonstrated one. Bhargavan and Leurent's 2016 paper recovered an HTTP cookie from a Firefox-to-IIS 3DES connection after capturing 785 GB over 38 hours: impractical against a casual target, entirely practical against a long-lived VPN tunnel. NIST disallowed three-key TDEA after 2023 and withdrew the specification in January 2024. The same arithmetic on AES's 128-bit block puts the birthday bound at 264 blocks, roughly 256 exabytes. Doubling the block size moved a real attack out of reach; tripling the key size did not.
| Cipher | Structure | Block | Key | Status |
|---|---|---|---|---|
| DES | Feistel, 16 rounds | 64 bit | 56 bit | Brute-forceable since 1998 |
| 3DES | Feistel ×3 (EDE) | 64 bit | 168 bit (~112 effective) | Disallowed by NIST after 2023; Sweet32 |
| AES-128 | SPN, 10 rounds | 128 bit | 128 bit | Unbroken; hardware-accelerated |
| AES-256 | SPN, 14 rounds | 128 bit | 256 bit | Unbroken; ~40% slower than AES-128 |
| ChaCha20 | ARX stream cipher, 20 rounds | n/a (64-byte keystream) | 256 bit | Unbroken; fast without AES hardware |
ChaCha20 is a stream cipher, and that turns out not to matter
ChaCha20 gets listed alongside AES constantly and it is not a block cipher. Why it can still occupy the same slot in a TLS cipher suite is the cleanest illustration of what a mode of operation does.
ChaCha20 keeps a 512-bit state as sixteen 32-bit words: four constants, eight key words, one block counter, three nonce words. It mixes them with a quarter-round built from nothing but addition, XOR, and bitwise rotation. That runs as ten double-rounds, alternating column and diagonal patterns, for twenty rounds total. Then it adds the original state back to the mixed state and emits the 64 bytes as keystream, which you XOR against your plaintext. That final addition is the structural difference from AES: it makes the core non-invertible, so ChaCha20's core is a pseudorandom function rather than a permutation. There is no "decrypt" direction — you regenerate the same keystream and XOR again.
AES-GCM uses AES in counter mode, and counter mode also only ever generates a keystream to XOR against the plaintext. AES-GCM never invokes AES's inverse direction, not even when decrypting. Both constructions end up as "keystream generator plus polynomial MAC," which is why TLS 1.3 can offer TLS_AES_128_GCM_SHA256 and TLS_CHACHA20_POLY1305_SHA256 as drop-in alternatives.
When ChaCha20 is the better pick
Hardware acceleration. Intel added the AES-NI instruction set with Westmere in 2010. Given AESENC plus PCLMULQDQ for the GHASH multiply, AES-GCM lands around 2.5 cycles per byte; Intel puts the general speedup over pure software AES at 3 to 10x. ChaCha20's add-rotate-XOR core needs no such help; it maps onto any general-purpose register file. Google's stated reason for pushing ChaCha20-Poly1305 into mobile TLS in 2014 was that it ran three times faster than AES-GCM on devices without AES hardware, which at the time meant most Android phones. Adam Langley's Galaxy Nexus numbers were starker: 92 MB/s for ChaCha20-Poly1305 against 20 MB/s for AES-GCM.
Constant-time behavior. A straightforward software AES implementation looks up S-box entries in a table using secret-dependent indices, and cache access patterns leak. That's not hypothetical. Bernstein demonstrated a remote cache-timing attack recovering an OpenSSL AES key in 2005, and Osvik, Shamir and Tromer turned it into practical local cache attacks presented at CT-RSA 2006. Writing bitsliced constant-time AES is possible and unpleasant. ChaCha20 has no tables, no branches on secret data, and no secret-dependent memory access, so it is constant-time by construction.
If you have AES-NI, use AES-GCM; it is faster and the ecosystem support is deeper. If you are shipping to hardware you don't control, or writing crypto in a language where you can't trust the compiler not to introduce a timing leak, ChaCha20-Poly1305 is the safer default.
One block cipher call encrypts exactly 16 bytes
A 4 KB HTTP request is 256 blocks and you have one cipher; how you connect them is the mode. ECB encrypts each block independently. Nothing is chained, nothing random is mixed in, and identical plaintext blocks therefore produce identical ciphertext blocks. The canonical demonstration is encrypting a bitmap of the Linux mascot in ECB mode: the outline of the penguin stays perfectly visible in the ciphertext, because the flat color regions repeat. Beyond leaking structure, ECB ciphertext is freely reorderable, so an attacker can cut and paste blocks between messages.
CBC fixes the repetition by chaining. Each plaintext block is XORed with the previous ciphertext block before encryption, and the first one is XORed with a random initialization vector. Identical plaintext now produces different ciphertext, both within a message and between messages.
CTR takes a different route entirely. It builds a counter block from a nonce and a counter, encrypts that, and XORs the result against the plaintext. The plaintext never enters the cipher at all, which turns the block cipher into a stream cipher.
CBC and twenty years of padding oracles
Because CBC feeds plaintext into the cipher directly, the message length has to be a multiple of the block size. PKCS#7 handles this by appending n bytes each of value n, and decryption strips them.
Stripping padding means inspecting attacker-controlled bytes. Since plain CBC has no authentication, the receiver has to do that inspection before it knows whether the ciphertext is legitimate. Serge Vaudenay published the consequence in 2002: if a server behaves differently for invalid padding than for other failures, an attacker who can submit modified ciphertext learns one plaintext byte per roughly 128 queries. That's the padding oracle, and it recovers entire messages without ever attacking AES.
TLS spent a decade patching around it rather than fixing the structure:
- BEAST (Duong and Rizzo, 2011, CVE-2011-3389) exploited TLS 1.0's choice to reuse the last ciphertext block of one record as the IV of the next. That made the IV predictable, and a predictable IV in CBC lets an attacker who can inject chosen plaintext confirm guesses about adjacent secrets. TLS 1.1 added an explicit random IV per record.
- Lucky 13 (AlFardan and Paterson, IEEE S&P 2013) defeated the standard mitigation of returning a uniform error message. The amount of data fed into the MAC computation depends on how many padding bytes were removed, so the timing of the response still leaks the padding length.
- POODLE (2014) did the same thing again against SSL 3.0's even weaker padding rules, which didn't specify the content of padding bytes at all.
The actual root cause was ordering, not CBC
TLS used MAC-then-encrypt: compute the MAC over the plaintext, then encrypt plaintext and MAC together. To verify the MAC, the receiver must first decrypt and unpad, so it always touches unauthenticated attacker data before it can reject anything. Encrypt-then-MAC inverts the order and kills the entire attack family, which is what RFC 7366 retrofitted onto TLS. AEAD modes make the correct order structural instead of optional.
CBC used correctly, with a random IV and encrypt-then-MAC over the ciphertext, is fine. That is also three separate things a developer has to get right, in the correct order, with a constant-time comparison on the MAC check. TLS 1.3 removed CBC entirely and permits only AEAD constructions.
GCM: counter mode with a MAC bolted to the same key
Galois/Counter Mode does two jobs with one key, which is what makes it an AEAD (authenticated encryption with associated data) rather than just an encryption mode.
The confidentiality half is plain CTR mode. With a 96-bit IV, GCM forms a counter block J0 = IV ‖ 0…01, and the keystream for the actual data starts at the next counter value. Counter block J0 itself is reserved for the tag.
The authentication half is GHASH, a polynomial MAC over GF(2128). Its key is H = EK(0128), derived by encrypting an all-zero block with the same AES key. GHASH treats the associated data and the ciphertext blocks as coefficients of a polynomial and evaluates it at H using Horner's method: each step XORs in the next block and multiplies by H in the field. The final tag is that accumulator XORed with EK(J0). Stripped of the algebra: GHASH is a checksum whose arithmetic is chosen so that forging it needs the key, not just the message. Because GHASH runs over the ciphertext, GCM authenticates after encrypting, which is the ordering CBC-based TLS got wrong for a decade. The tag also covers the associated data, so hardware can verify a packet header and its payload in one pass.
The associated-data slot is the part people forget to use. It authenticates bytes that travel in the clear, which is how TLS binds a record's header to its payload and how you stop an attacker from replaying a valid encrypted blob into a different context. If you're encrypting a database column, the row ID belongs in the AAD. Otherwise a ciphertext lifted from row 5 will decrypt and authenticate perfectly well in row 900.
The one input that destroys GCM
Everything above holds only while each (key, nonce) pair is used once.
The confidentiality loss is the ordinary CTR-mode consequence: the same key and nonce means the same keystream, so C₁ ⊕ C₂ = P₁ ⊕ P₂ and the plaintexts fall out with a bit of statistics. That much is bounded to the two affected messages.
The integrity loss is not bounded, and it runs in four steps:
- Both tags were masked with the same EK(J0). XOR the two tags together and the mask cancels, leaving only GHASH outputs.
- GHASH is linear in H, so what remains is a polynomial equation in H.
- Its coefficients are the ciphertext blocks, which the attacker already has. Solve for the roots and H is recovered.
- H is the authentication key. With it, the attacker can forge a valid tag for any ciphertext under that key, on every future message.
So a single nonce repeat turns a confidentiality bug into permanent forgery capability. NIST SP 800-38D states the asymmetry directly: CTR nonce reuse costs you confidentiality, GCM nonce reuse costs you both. Antoine Joux raised it in a 2006 comment to NIST, "Authentication Failures in NIST version of GCM," now informally called the forbidden attack.
This is a live bug class, not a textbook footnote
Böck, Zauner, Devlin, Somorovsky and Jovanovic scanned the public HTTPS population in 2016 and published the results as Nonce-Disrespecting Adversaries at WOOT. They confirmed 184 servers actually repeating GCM nonces, which is a complete authenticity break on those hosts, and found over 70,000 more generating nonces randomly and therefore standing on the wrong side of a birthday bound. These were shipped load balancers and appliances, not student projects. ChaCha20-Poly1305 has the identical weakness: repeat a nonce and the Poly1305 key r falls out.
Practical nonce rules
- Prefer a deterministic counter over a random nonce. A monotonic counter with a per-key prefix is unique by construction. Randomness only gives you probabilistic uniqueness, and 96 bits is not as much room as it looks.
- Respect the invocation limit. NIST SP 800-38D caps a single key at 232 invocations when IVs are generated randomly, precisely to keep the collision probability negligible. Rotate the key before you get there.
- Respect the per-message limit. A single GCM invocation is capped at 239 − 256 bits of plaintext, about 64 GiB, because the 32-bit counter wraps past that. Streaming a large backup through one GCM call is a real way to reuse keystream.
- If uniqueness is genuinely hard, change the mode. Uniqueness breaks in ways that are painful to fix whenever there are multiple writers with no shared counter, stateless functions with no memory between invocations, or VM snapshots that get restored and replay a counter. AES-GCM-SIV (RFC 8452) derives its nonce synthetically from the message, so a repeat leaks only that two plaintexts were equal, rather than surrendering the key.
GCM is the stronger construction with the sharper failure mode. CBC's classic breaks needed an oracle and thousands of queries; GCM's needs one duplicated integer.
What to actually pick
For TLS, don't pick. TLS 1.3 registers five cipher suites and all five are acceptable; the negotiation exists so that clients and servers can agree, not so that you can tune it. The work is entirely in what you turn off:
# nginx: TLS 1.3 suites are fixed by the protocol.
# Everything below only shapes the 1.2 fallback.
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;No CBC suites, no 3DES, no TLS 1.0 or 1.1. Leaving ssl_prefer_server_ciphers off is deliberate: it lets a phone without AES hardware choose ChaCha20 for itself, which it can judge better than your server can.
For encryption you're writing yourself, the decision is narrower than it looks:
Choosing an AEAD for application-level encryption
- AES-256-GCM when you're on server hardware with AES-NI and can guarantee unique nonces from a counter.
- ChaCha20-Poly1305 when you can't count on AES instructions, or when you're implementing in a runtime where constant-time table lookups aren't realistic.
- AES-GCM-SIV when nonce uniqueness depends on coordination you don't control.
- Never ECB. Never unauthenticated CBC or CTR. Never 3DES on a connection that carries repeated secrets.
- Put every byte that travels in the clear but must not be swapped into the AAD: row IDs, tenant IDs, record headers.
Two limits on all of the above
Everything here is symmetric cryptography, which assumes both sides already share a key. Getting the key there is the asymmetric half of TLS, with a separate set of failure modes — JWKS and OIDC covers why symmetric keys stop working once a token crosses a service boundary, and password entropy covers the other place a big number gets mistaken for security. And "AES has never been broken" describes the cipher in isolation. Side-channel attacks against specific implementations succeed regularly, and they don't care that the mathematics is sound. A correct algorithm compiled into a binary that leaks timing or power is still a break, just not one that shows up in a cipher suite string.
Primary sources, if you're implementing any of this: FIPS 197 for AES, NIST SP 800-38D for GCM and its exact data limits, RFC 8439 for ChaCha20-Poly1305, and RFC 8446 for what TLS 1.3 deleted. For the encodings ciphertext and keys arrive wrapped in, our Base64 encoder is on hand.
Reviewing crypto code, the key length is the first thing you see and the last thing that matters. Read the mode, then find the nonce.