Chapter 14 — Modes of operation (and why ECB is a meme)
In one sentence
A block cipher only encrypts 16 bytes; a mode defines how to chain blocks together to encrypt a real message — and choosing the wrong mode breaks everything.
The problem
AES encrypts exactly one 16-byte block. Your message is 4,000 bytes. Now what?
ECB — Electronic Codebook (never use this)
The obvious approach: encrypt each block independently.
P1 -> [AES] -> C1
P2 -> [AES] -> C2
P3 -> [AES] -> C3
The fatal flaw: identical plaintext blocks produce identical ciphertext blocks. Structure in the plaintext survives into the ciphertext.
THE FAMOUS DEMONSTRATION
Take a bitmap image of the Linux penguin. Encrypt it with AES-ECB.
The result is still visibly a penguin -- because large areas of identical
colour become large areas of identical ciphertext.
original AES-256-ECB
+-------------+ +-------------+
| (o_o) | | ::::(o_o):: | <-- you can still SEE it
| <penguin> | ==> | :<penguin>: |
| | | ::::::::::: |
+-------------+ +-------------+
This isn't a toy concern. ECB leaks:
- Repeated records in an encrypted database
- Whether two encrypted messages are the same
- Structure in encrypted network protocols
Dangerous: ECB is often the default in older libraries and in naive code (
Cipher.getInstance("AES")in Java historically defaulted to ECB). If you see ECB in a code review, it is a finding.
CBC — Cipher Block Chaining
XOR each plaintext block with the previous ciphertext block before encrypting. A random IV (initialisation vector) seeds the chain.
IV P1 P2 P3
| | | |
+--XOR----+ +--XOR----+ +--XOR----+
| | | | |
[AES] --+ [AES] --+ [AES]
| | |
C1 C2 C3
Now identical plaintext blocks encrypt differently. Properties:
+ hides patterns
- SEQUENTIAL encryption (can't parallelise) decryption CAN parallelise
- needs padding -> padding oracle risk
- NO integrity protection
- IV must be random and unpredictable (not just unique)
The padding oracle attack. If an attacker can tell the difference between "padding was invalid" and "padding was valid but the message was wrong" — through error messages, response codes, or timing — they can decrypt the entire ciphertext without the key, one byte at a time. This broke ASP.NET (2010), Ruby on Rails, JSF, and many others.
The fix is not "hide the error". The fix is to authenticate the ciphertext (Chapter 15) so invalid data is rejected before decryption is even attempted.
CTR — Counter mode
Turn the block cipher into a stream cipher: encrypt a counter, then XOR the result with the plaintext.
nonce||counter=1 -> [AES] -> keystream1 -> XOR P1 -> C1
nonce||counter=2 -> [AES] -> keystream2 -> XOR P2 -> C2
nonce||counter=3 -> [AES] -> keystream3 -> XOR P3 -> C3
+ fully PARALLELISABLE both ways
+ NO padding needed (works on any length)
+ random access -- decrypt block 500 without blocks 1-499
- NO integrity protection
- CATASTROPHIC if a nonce+key pair is ever reused
Nonce reuse in CTR is exactly the one-time-pad key reuse from Chapter 12. Same keystream, XOR the ciphertexts, key cancels out. This is the single most dangerous footgun in symmetric cryptography.
The mode comparison
| Mode | Parallel | Padding | Integrity | Nonce reuse | Verdict |
|---|---|---|---|---|---|
| ECB | yes | yes | no | n/a | Never |
| CBC | decrypt only | yes | no | reveals if plaintexts match | Legacy only, with a MAC |
| CTR | yes | no | no | catastrophic | Only inside AEAD |
| GCM | yes | no | yes | catastrophic | Default (Ch 15) |
| ChaCha20-Poly1305 | yes | no | yes | catastrophic | Default (Ch 15) |
| XTS | yes | no | no | n/a | Disk encryption only |
Practice (30 min) — see ECB fail with your own eyes
This is the most memorable exercise in Part 4. Do it.
cd ~/sec-lab/scratch
python3 - <<'PY'
# Build a simple image with large blocks of identical colour (a PPM file --
# a trivial uncompressed format, so no compression hides the effect).
W, H = 256, 256
px = bytearray()
for y in range(H):
for x in range(W):
# a white circle on a black background
inside = (x-128)**2 + (y-128)**2 < 90**2
px += bytes([255,255,255] if inside else [0,0,0])
open("plain.ppm","wb").write(b"P6\n%d %d\n255\n" % (W,H) + bytes(px))
print("wrote plain.ppm", W, "x", H)
PY
# Encrypt the PIXEL DATA with ECB, keeping the header readable
KEY=$(openssl rand -hex 32)
head -c 15 plain.ppm > header.bin # "P6\n256 256\n255\n"
tail -c +16 plain.ppm > body.bin
openssl enc -aes-256-ecb -K $KEY -in body.bin -out body.ecb -nopad
cat header.bin body.ecb > ecb.ppm
# And with CBC for comparison
IV=$(openssl rand -hex 16)
openssl enc -aes-256-cbc -K $KEY -iv $IV -in body.bin -out body.cbc -nopad
cat header.bin body.cbc > cbc.ppm
echo "Now open plain.ppm, ecb.ppm and cbc.ppm in an image viewer."
echo "ECB: you can still see the circle. CBC: pure noise."
On macOS: open -a Preview *.ppm. On Linux: xdg-open ecb.ppm or use GIMP.
If your viewer can't read PPM: python3 -c "from PIL import Image; [Image.open(f).save(f.replace('.ppm','.png')) for f in ['plain.ppm','ecb.ppm','cbc.ppm']]" (needs pip install pillow).
Now demonstrate CTR nonce reuse:
python3 - <<'PY'
import os
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
key = os.urandom(32)
nonce = os.urandom(16) # the SAME nonce, reused -- the bug
def ctr(data):
c = Cipher(algorithms.AES(key), modes.CTR(nonce)).encryptor()
return c.update(data) + c.finalize()
p1 = b"TRANSFER 100 USD TO ALICE"
p2 = b"TRANSFER 999 USD TO EVE!!"
c1, c2 = ctr(p1), ctr(p2)
x = bytes(a ^ b for a, b in zip(c1, c2))
y = bytes(a ^ b for a, b in zip(p1, p2))
print("C1 XOR C2 == P1 XOR P2 :", x == y)
# Knowing ONE plaintext reveals the other:
print("recovered p2:", bytes(a ^ b for a, b in zip(x, p1)))
PY
(Requires pip install cryptography inside a virtualenv.)
Common confusions
- "CBC is broken." CBC itself is fine; unauthenticated CBC is the problem. TLS 1.2 with CBC needed careful Encrypt-then-MAC. Just use AEAD.
- "A nonce is the same as an IV." Similar role, different requirements. A CBC IV must be unpredictable; a CTR/GCM nonce must be unique (predictable is fine). Both must never repeat with the same key.
- "I'll just use a counter as my nonce." That's correct if you can guarantee it never resets — which fails on VM snapshots, restores, and multi-writer systems. Random 96-bit nonces are safer for most applications.
Check yourself
- Why does ECB leak the structure of an image?
- What does the IV do in CBC, and what property must it have?
- What happens if you reuse a nonce in CTR mode?
- Which modes require padding, and why is that a risk?
- Which of these modes provide integrity? (Trick question.)
Further reading
- Wikipedia: "Block cipher mode of operation" — unusually good, with the canonical ECB penguin image.
- Article: "Padding Oracles and the Decline of CBC-mode Cipher Suites" (Cloudflare blog) — clear explanation of a real, important attack.
- Standard: NIST SP 800-38A (modes) and SP 800-38D (GCM).