Learning Goals
3 minBy the end of this lesson you can:
- Explain what a block-cipher "mode of operation" is and why it matters.
- Show why ECB is broken (the penguin) and must never be used.
- Compare CBC (needs IV + separate MAC) with GCM (authenticated in one step).
- Choose GCM as the default — and know when to just use Fernet instead.
Warm-Up · The Cipher Is Fine; the Mode Isn't
5 minAES encrypts data one fixed-size block (16 bytes) at a time. But messages are bigger than one block — so how do you chain the blocks together? That choice is the mode of operation, and it's where security is won or lost. Same strong cipher, wildly different safety.
AES itself is excellent. The mistakes happen in the mode: ECB encrypts each block independently (so identical plaintext → identical ciphertext — patterns leak). CBC chains blocks with an IV (better, but needs a separate integrity check). GCM chains and authenticates in one step (confidentiality + integrity). The lesson: never pick ECB; prefer authenticated modes like GCM; and honestly, prefer a high-level library (Fernet) that picks correctly for you.
New Concept · ECB vs CBC vs GCM
14 minECB — Electronic CodeBook (never use)
Each 16-byte block encrypted INDEPENDENTLY with the same key: block A → cipher X block A → cipher X ← identical input ALWAYS gives identical output So repeated patterns in the plaintext survive into the ciphertext.
The classic demonstration: encrypt a bitmap of the Linux penguin (Tux) with AES-ECB and you can still see the penguin in the ciphertext — because identical-coloured regions encrypt identically. ECB leaks structure. It must never be used for real data; if you see MODE_ECB in code, that's a bug.
CBC — Cipher Block Chaining (legacy, careful)
Each block is XORed with the PREVIOUS ciphertext before encrypting: block 1 ⊕ IV → cipher 1 block 2 ⊕ cipher 1 → cipher 2 ← chaining hides patterns Needs: a random IV (never reused), correct padding, AND a separate MAC.
CBC fixes ECB's pattern leak via chaining and a random initialization vector (IV). But CBC alone gives no integrity — you must add a separate HMAC (encrypt-then-MAC), and mistakes here cause real attacks (padding oracles). CBC is legacy; only use it if you must, and always with a MAC.
GCM — Galois/Counter Mode (modern default)
Encrypts (counter mode) AND produces an authentication TAG in one pass. output: ciphertext + 16-byte tag decrypt verifies the tag first → tampering = rejected, like Fernet. Needs: a unique nonce per message (NEVER reuse a nonce with the same key).
GCM is authenticated encryption: confidentiality and integrity together, no separate MAC step. It's fast and the modern standard (TLS uses AES-GCM). The one rule: the nonce must be unique for each message under a given key — reuse is catastrophic.
Using AES-GCM correctly (when you must use raw AES)
import os from cryptography.hazmat.primitives.ciphers.aead import AESGCM key = AESGCM.generate_key(bit_length=256) # 256-bit AES aesgcm = AESGCM(key) nonce = os.urandom(12) # 12-byte nonce, UNIQUE per message ct = aesgcm.encrypt(nonce, b"secret data", associated_data=None) # store nonce + ct together (nonce isn't secret, but must never repeat) plain = aesgcm.decrypt(nonce, ct, associated_data=None) # raises on tamper print(plain.decode())
AESGCM from cryptography gives you authenticated AES with a clean API. You manage the nonce (12 random bytes per message, stored alongside the ciphertext). associated_data lets you authenticate (but not encrypt) extra context like a header.
...but usually, just use Fernet
For 95% of needs, use Fernet (Lesson 14) — it's AES-CBC + HMAC assembled correctly, with nonce/IV and integrity handled for you, and you can't misuse it. Reach for raw AESGCM only when you need GCM specifically (e.g. interoperating with another system, or authenticating associated data). The deeper lesson: knowing modes lets you recognise dangerous code ("why is this ECB?"), not necessarily hand-roll AES yourself.
Worked Example · See ECB Leak, Watch GCM Protect
12 minGoal: a hands-on demo that makes ECB's flaw visible and shows GCM detecting tampering — so the "which mode" choice is concrete, not abstract.
import os from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.primitives.ciphers.aead import AESGCM key = os.urandom(32) # --- ECB leaks patterns: identical blocks → identical ciphertext --- def aes_ecb(data: bytes) -> bytes: # 16-byte blocks; pad to a multiple of 16 for the demo data = data + b"\x00" * (-len(data) % 16) enc = Cipher(algorithms.AES(key), modes.ECB()).encryptor() # NEVER in real code return enc.update(data) + enc.finalize() # two identical 16-byte blocks of plaintext: plaintext = b"AAAAAAAAAAAAAAAA" + b"AAAAAAAAAAAAAAAA" ct = aes_ecb(plaintext) print("ECB block 1 == block 2 ciphertext:", ct[:16] == ct[16:32]) # True → LEAK! # --- GCM hides patterns AND detects tampering --- aesgcm = AESGCM(key) nonce = os.urandom(12) gcm_ct = aesgcm.encrypt(nonce, plaintext, None) print("GCM block 1 == block 2 ciphertext:", gcm_ct[:16] == gcm_ct[16:32]) # False # tampering is caught by the auth tag: from cryptography.exceptions import InvalidTag tampered = bytearray(gcm_ct); tampered[0] ^= 1 try: aesgcm.decrypt(nonce, bytes(tampered), None) except InvalidTag: print("GCM detected tampering — refused to decrypt ✓")
ECB block 1 == block 2 ciphertext: True ← identical blocks leak! GCM block 1 == block 2 ciphertext: False ← patterns hidden GCM detected tampering — refused to decrypt ✓
Read the code
The ECB output proves the flaw numerically: two identical 16-byte plaintext blocks produce byte-identical ciphertext, so any structure in the data shows through (the penguin). GCM, by contrast, makes them different (chaining/counter) and — like Fernet — rejects tampering via its authentication tag. This is why mode choice matters more than "is it AES?": AES-ECB is broken-by-mode, AES-GCM is solid. For real code you'd still reach for Fernet unless you specifically need GCM.
Try It Yourself
13 minEncrypt a plaintext made of several repeated 16-byte blocks with ECB, and confirm the repeated blocks produce repeated ciphertext. Then do the same with GCM and confirm they don't repeat.
Encrypt and decrypt with AES-GCM, storing nonce + ciphertext. Then write a comment explaining what goes wrong if you reuse the same nonce for two messages under the same key.
Hint
# Reusing a nonce with the same key in GCM is CATASTROPHIC: an # attacker can recover the authentication key and forge messages, # and XOR of two ciphertexts leaks plaintext relationships. # Rule: 12 fresh random bytes per message, every time.
Given three snippets — one using MODE_ECB, one using CBC with a hard-coded/reused IV, and one using GCM correctly — identify the vulnerability in each of the first two and explain the fix. This is the real-world skill: reviewing crypto code for mode mistakes.
Hint
1) MODE_ECB → bug: patterns leak. Fix: use GCM/Fernet.
2) CBC + fixed IV → bug: reused IV makes identical messages produce
identical first blocks (pattern leak + attacks).
Fix: random IV per message; and add a MAC.
3) GCM + random nonce per msg → correct.Mini-Challenge · A Mode-Choice Cheat Sheet Tool
8 minBuild recommend(scenario) that, given a use case ("encrypt a file at rest," "authenticated API tokens," "interop with a system that requires AES-GCM"), returns the recommended approach (Fernet vs AES-GCM vs "never ECB") with a one-line reason. Encode the decision logic you'd give a teammate doing a code review.
Show a sample solution
RULES = [ ("ecb", "🚫 NEVER use ECB — it leaks plaintext patterns. Use GCM/Fernet."), ("interop", "Use AES-GCM (cryptography.AESGCM) to match the other system; " "manage a unique 12-byte nonce per message."), ("associated data", "AES-GCM — it can authenticate extra context (headers) " "alongside the ciphertext."), ("file at rest", "Use Fernet — safe defaults, integrity built in, hard to misuse."), ("token", "Use Fernet — authenticated, timestamped tokens out of the box."), ] def recommend(scenario: str) -> str: s = scenario.lower() for key, advice in RULES: if key in s: return advice return "Default to Fernet unless you specifically need raw AES-GCM." for case in ["encrypt a file at rest", "must interop with AES-GCM", "someone wrote MODE_ECB", "authenticate a header (associated data)"]: print(f"{case:35} → {recommend(case)}")
Non-negotiables: ECB always rejected, Fernet as the default, AES-GCM for interop/associated-data, each with a reason.
Recap
3 minAES is a block cipher; the mode determines how blocks combine, and that's where security lives. ECB encrypts blocks independently so patterns leak (the penguin) — never use it. CBC chains with a random IV but needs a separate MAC and careful handling (legacy). GCM is authenticated encryption — confidentiality + integrity in one pass, the modern default — with the absolute rule that the nonce must never repeat under a key. In practice, prefer Fernet unless you specifically need GCM; the real-world skill is recognising dangerous mode choices in code reviews.
Vocabulary Card
- mode of operation
- How a block cipher processes data longer than one block.
- ECB
- Encrypts each block independently — leaks patterns; never use.
- IV / nonce
- A random/unique value per message; reuse breaks security.
- GCM
- Authenticated AES mode — confidentiality + integrity together.
Homework
4 minReproduce the ECB-vs-GCM demo and write it up with the "penguin" explanation in your own words. Build the mode-choice recommender. Then write a paragraph a code reviewer could use: the three red flags that mean "reject this crypto code" (ECB, reused IV/nonce, missing integrity/MAC) and the safe default to suggest instead.
Sample · crypto code-review red flags
Reject the crypto code if you see any of these: 1. ECB anywhere (MODE_ECB / modes.ECB()) — patterns leak (the penguin stays visible). No legitimate use for real data. 2. A hard-coded or reused IV/nonce — destroys CBC and is CATASTROPHIC for GCM (key recovery, forgery). Must be fresh and random per message. 3. Encryption with NO integrity check — raw CBC without an HMAC lets attackers tamper (padding-oracle attacks). Encryption ≠ integrity unless the mode is authenticated. Safe default to suggest: "Use Fernet — it's authenticated, handles IV and integrity for you, and is hard to misuse. Only use raw AES-GCM if you specifically need GCM, with a unique nonce per message." Never hand-roll AES modes.
Non-negotiables: working demo + penguin explanation, the recommender, and the three reviewer red flags with the safe default.