CS 161, Lesson 26, in 51 slides. It covers HMAC-DRBG, a secure pseudorandom generator built from HMAC, along with its Seed and Generate algorithms, its absorption of low-entropy input, and its rollback resistance, in section 9.4, then the Dual_EC_DRBG backdoor as enrichment. It closes with stream ciphers in section 9.5: the keystream used as a one-time pad, the formal Enc and Dec scheme, the roughly 2^64-bit limit on AES-CTR, and ChaCha20's counter-driven random access. It is anchored to textbook sections 9.4 to 9.5.
Subject: Computer Security · 82 slides · applied lesson
Open the interactive version of this deck · Homework for this lesson
Title
CS 161 · Lesson 26 of 45
HMAC-DRBG · seeding & generation · rollback resistance · stream ciphers · AES-CTR · ChaCha20
Objectives
Warm-up
Discussion prompt
Before we open L26 · HMAC-DRBG & Stream Ciphers: without looking back, what was the main idea of L25 · Randomness, Entropy, pRNGs & Rollback Resistance, and what could you do by the end of it that you could not do before?
Hint: One sentence for the idea, one for the skill. If the second one is blank, that is the part to revisit.
Answer:
CS 161, Lesson 25, in 50 slides. It explains what randomness and entropy mean for cryptography, in section 9.1, then covers the pseudorandom generator with its Seed, Reseed, and Generate interface and the computational indistinguishability it aims at, in sections 9.1 and 9.2, and rollback resistance against state compromise in section 9.3. It adds real-world entropy failures - VM snapshots, the Debian OpenSSL bug, and weak entropy at boot - flagged as enrichment. It is anchored to textbook sections 9.1 to 9.3.
Concept
L25 told us what a secure pRNG must do. Now we build one from a tool we already trust — HMAC (L23) — and then turn its output into a working cipher.
Matching
Match the pairs
From Three questions this lesson answers — match each one to what it actually does. The descriptions have been shuffled.
Why: How do we build a pRNG?, Why is it a good one?, How does it encrypt? are easy to tell apart while they are sitting next to their descriptions and much harder afterwards, which is what this checks.
Section
Part 1 · §9.4 the construction
Concept
You have HMAC from L23 — a keyed function whose output is indistinguishable from random to anyone without the key. You need a pRNG: feed it a seed, get back an endless stream of random-looking bits. How do you bridge the two?
HMAC-DRBG does exactly that. DRBG = Deterministic Random Bit Generator: deterministic given its seed, but its output looks random to anyone who can't see the secret internal state.
Concept
HMAC-DRBG — A secure pRNG that builds an output stream by repeatedly applying HMAC. Its entire secret is an internal state of two values, K and V.
The two values are exactly the two arguments HMAC takes. Generate them, feed them back in, and you get a fresh pseudorandom block each round.
Counterexample
Discussion prompt
The two values are exactly the two arguments HMAC takes. Generate them, feed them back in, and you get a fresh pseudorandom block each round.
That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.
Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.
Intuition
HMAC's whole promise (L23): without the key, its output is indistinguishable from random. HMAC-DRBG just leans on that promise over and over.
Each output block is one more HMAC of the previous block under the secret key K. To Eve, who never sees K or V, every block is one more random-looking string — and so is the entire stream.
Ask yourself: what is the one thing that must stay hidden for the stream to look random? (The internal state — K and V. Leak it and Eve can recompute every future output.)
Analogy
Discussion prompt
Explain §9.4 Why repeated HMAC looks random by analogy to something with no Computer Security in it at all — a queue, a recipe, a map, a bank balance, whatever fits. Then say where your analogy breaks.
Hint: An analogy that never breaks is not an analogy, it is the same idea wearing a hat. Find the seam — that is the part that is actually new.
Answer:
HMAC's whole promise (L23): without the key, its output is indistinguishable from random. HMAC-DRBG just leans on that promise over and over.
Concept
Generate(n) produces n bits of output by chaining HMAC on V, then ratchets the state forward so the next call is independent.
Generate(n):
output = ""
while len(output) < n:
V = HMAC(K, V)
output = output ‖ V
# ratchet the state forward
K = HMAC(K, V ‖ 0x00)
V = HMAC(K, V)
return output[0:n]‖ means concatenation. Each loop turn appends one fresh HMAC block to the output; the final two lines refresh K and V so a later Generate can't be linked to this one.
Explain it
Discussion prompt
Explain §9.4 The Generate algorithm to a student a year behind you. No notation, no jargon they have not met — and it still has to be true.
Hint: If your explanation needs a symbol they have never seen, you are describing the notation rather than the idea.
Answer:
Generate(n) produces n bits of output by chaining HMAC on V, then ratchets the state forward so the next call is independent.
Ranking
Put in order
Put the moves of §9.4 Trace two Generate iterations into the order they have to happen.
Why: These are the moves of the worked example in the order it makes them, and each one is set up by the one before it. We want enough output that the while-loop runs at least twice; each turn computes one HMAC block.
Worked example
Start a Generate call with state (K, V) and an empty output
Why: We want enough output that the while-loop runs at least twice; each turn computes one HMAC block.
Round 1: set V = HMAC(K, V), append V to output
Why: Call this block V_1 = HMAC(K, V_0). The output so far is exactly V_1.
Round 2: set V = HMAC(K, V) again, append it
Why: Now V_2 = HMAC(K, V_1). The output is V_1 ‖ V_2 — two chained HMAC blocks under the same K.
| round | V = HMAC(K, V) | output so far |
|---|---|---|
| start | V_0 | (empty) |
| 1 | V_1 = HMAC(K, V_0) | V_1 |
| 2 | V_2 = HMAC(K, V_1) | V_1 ‖ V_2 |
Verify: each block is HMAC of the previous, so each is indistinguishable from random
Why: §9.4: as long as K and V stay secret, V_1 and V_2 each look random — so does their concatenation, which is the returned output.
Comparison
Comparison matrix
From §9.4 Trace two Generate iterations: refill the V = HMAC(K, V) column from what you know. The rest of the table is as it appeared.
| round | V = HMAC(K, V) | output so far |
|---|---|---|
| start | V_0 | (empty) |
| 1 | V_1 = HMAC(K, V_0) | V_1 |
| 2 | V_2 = HMAC(K, V_1) | V_1 ‖ V_2 |
Step zero
Discussion prompt
§9.4 Walk the state ratchet after Generate — before any calculation: what is the plan? Name the moves in order, in plain English, without doing the arithmetic.
Hint: It starts with: After filling the output, refresh K: K = HMAC(K, V ‖ 0x00)
Answer:
Worked example
After filling the output, refresh K: K = HMAC(K, V ‖ 0x00)
Why: The old K is replaced by a new HMAC value, so the key that produced this call's output no longer governs the next one.
Refresh V under the NEW K: V = HMAC(K, V)
Why: V is re-derived using the freshly updated K, decoupling the next Generate's starting point from this one.
| step | K | V |
|---|---|---|
| after output loop | K_old | V_last |
| K = HMAC(K, V ‖ 0x00) | K_new | V_last |
| V = HMAC(K, V) | K_new | V_new |
Verify: the next Generate starts from (K_new, V_new), unlinkable to this one
Why: §9.4: ratcheting both values forward through HMAC means a later call's output can't be tied back to this call — and supports rollback resistance.
Trade off
Comparison matrix
From §9.4 Walk the state ratchet after Generate: every row here is a choice with a cost. Fill the K column, then say which row you would actually pick and what you give up for it.
| step | K | V |
|---|---|---|
| after output loop | K_old | V_last |
| K = HMAC(K, V ‖ 0x00) | K_new | V_last |
| V = HMAC(K, V) | K_new | V_new |
Concept
Seed(s) initializes the state from a seed string s. Reseed(s) mixes in fresh entropy later — it is the same steps but without resetting K and V to 0 first.
Seed(s):
K = 0
V = 0
K = HMAC(K, V ‖ s ‖ 0x00)
V = HMAC(K, V)
K = HMAC(K, V ‖ s ‖ 0x01)
V = HMAC(K, V)
# Reseed(s): identical, but DO NOT reset K, V to 0 firstThe bytes 0x00 and 0x01 are domain separators — their exact values don't matter. The point is the same one throughout: lots of HMAC outputs ⇒ a state that's indistinguishable from random.
Intuition
Seed wipes K and V to 0 and starts fresh. Reseed deliberately skips that wipe — it folds the new seed into the existing state instead of throwing it away.
That's what you want from a long-running generator: as new entropy trickles in (timing jitter, device noise), each Reseed makes the state strictly harder to guess, never weaker.
Ask yourself: if Reseed never discards old entropy and adds new, can mixing in fresh bytes ever hurt? (No — and that 'never hurts' property is the next big idea.)
Section
Part 2 · §9.4 properties
Concept
Real entropy sources are often weak per bit — a noisy sensor might give only a fraction of a bit of true randomness per sampled bit. HMAC-DRBG fixes this by absorbing an arbitrarily long seed.
If each input bit carries about 0.1 bits of real entropy, then 2560 bits of seed yield roughly 256 bits of real entropy in the state — plenty for a secure key. Just feed in enough low-quality bits.
Ranking
Put in order
Put the moves of §9.4 Sizing a low-entropy seed into the order they have to happen.
Why: These are the moves of the worked example in the order it makes them, and each one is set up by the one before it. A 256-bit security level needs about 256 bits of true entropy in the state — the standard target.
Worked example
Decide how much real entropy the state needs
Why: A 256-bit security level needs about 256 bits of true entropy in the state — the standard target.
Measure the entropy rate of the source: ~0.1 bits of real entropy per input bit
Why: A weak source (e.g. lightly-debiased hardware noise) gives a small fraction of a bit per sample.
\[ \text{seed bits needed} = \frac{256\ \text{bits}}{0.1\ \text{bits/bit}} = 2560\ \text{bits} \]
Feed all 2560 bits in as the seed
Why: HMAC-DRBG compresses the whole seed through HMAC, concentrating its scattered entropy into the state.
Verify: the state now holds ~256 bits of real entropy
Why: §9.4: a long low-entropy seed yields a strong state — the generator absorbs as many weak bits as you give it.
Notation
Annotate
From §9.4 Sizing a low-entropy seed — read this one piece at a time. What is each part doing?
On: \( \text{seed bits needed} = \frac{256\ \text{bits}}{0.1\ \text{bits/bit}} = 2560\ \text{bits} \)
Concept
Adding more input to the seed — even input with no entropy at all, like a string of all-zeros — can only ever help or do nothing. It never weakens the state.
So when in doubt, mix more in. There is no penalty for over-seeding, and a real upside if any of those extra bytes turn out to carry entropy you underestimated.
Concept
Rollback resistance — An attacker who compromises the CURRENT state cannot compute any PREVIOUS state — and so cannot recover earlier outputs.
Reversing HMAC-DRBG would mean running the underlying hash backwards — inverting a one-way function. That is infeasible, so past outputs stay safe even after a state compromise.
Definition probe
Sort into buckets
Every line below is part of the definition of HMAC-DRBG or of Rollback resistance — one or the other, never both. Put each where it belongs.
Intuition
Each state update is K = HMAC(K, V‖…) and V = HMAC(K, V). HMAC is built on a one-way hash: easy forwards, infeasible to invert.
So even Eve holding the full current (K, V) hits a wall trying to go back one step — she'd have to invert the hash to find the previous K and V. Yesterday's keystream is out of reach.
Ask yourself: which textbook property of the hash does rollback resistance rely on? (One-wayness — the same property that protects a hashed password.)
Anomaly
Predict first
A student writes this, and it looks reasonable:
A student: 'If I pad the seed with junk like all-zeros, I'm diluting the real entropy — that must make the generator weaker.'
It is wrong. Say what breaks — and say it before you turn the page.
Correct: Backwards. HMAC mixes everything together; adding input — even all-zeros — can only preserve or increase the unpredictability of the state, never reduce it.
A student: what does mixing in extra bytes actually do?
Why: Backwards. HMAC mixes everything together; adding input — even all-zeros — can only preserve or increase the unpredictability of the state, never reduce it.
Trap
A student: 'If I pad the seed with junk like all-zeros, I'm diluting the real entropy — that must make the generator weaker.'
Assume zero-entropy input lowers the state's security
Why: Backwards. HMAC mixes everything together; adding input — even all-zeros — can only preserve or increase the unpredictability of the state, never reduce it.
A student: what does mixing in extra bytes actually do?
Feed in as much as you like — extra input never hurts
Why: §9.4: HMAC-DRBG absorbs arbitrarily long seeds and zero-entropy strings never weaken the state. Over-seeding is safe and often wise.
Concept
⊕ Supplemental — not in this textbook section. Not every standardized DRBG is trustworthy. Dual_EC_DRBG was a NIST-standardized generator later shown to contain a likely NSA backdoor.
Its design used special elliptic-curve constants. Whoever chose those constants could hold a secret value that lets them predict the generator's future output after seeing a little of it — a deliberate trapdoor.
Intuition
⊕ The math of Dual_EC_DRBG wasn't broken by accident — it was built to be breakable by its designer. The defense isn't cleverer math; it's transparency.
This is Kerckhoff and Shannon again: a design should be fully public and analyzable, with no secret constants whose origin you must trust. HMAC-DRBG's pieces (HMAC, a public hash) are exactly that kind of open, well-studied construction.
Ask yourself: why prefer HMAC-DRBG over a generator with unexplained magic constants? (Because every part of HMAC-DRBG is public and analyzable — there's nowhere to hide a backdoor.)
Section
Part 3 · §9.5 the scheme
Concept
A block cipher waits for a full block before it can act. But sometimes data arrives one bit at a time — a live audio feed, a streaming socket. You want to encrypt each bit the instant it shows up.
Stream cipher — A cipher that encrypts and decrypts a message as it arrives, one bit at a time, rather than waiting for a full block as a block cipher does.
Matching
Match the pairs
Match each term to the definition this lesson gave it — not the one you would guess from the word.
Why: These are the working definitions of HMAC-DRBG, Rollback resistance, Stream cipher as L26 · HMAC-DRBG & Stream Ciphers uses them. Pairing them correctly is the test of whether you could state each one with the slide switched off.
Concept
The trick is the one-time pad from L19: XOR each plaintext bit with a secret keystream bit. The only problem with the OTP was needing a truly random pad as long as the message.
A stream cipher solves that: use a secure pRNG's output as the keystream. The secret key is just the pRNG seed — short to share, but it expands into a pad as long as you need.
Intuition
Picture the L19 one-time pad, but instead of pre-sharing a giant random pad, both sides grow the pad on demand from a shared seed by running the same pRNG.
Because the keystream is indistinguishable from random (that's the pRNG's job), XOR-ing it onto the message hides the message just like a real OTP would — but you only had to share a short key.
Ask yourself: what replaced the OTP's true-random pad here? (A pRNG's pseudorandom output — same XOR, same one-time-use rule, far less key to share.)
Sorting
Sort into buckets
These are the pieces of L26 · HMAC-DRBG & Stream Ciphers, out of order. Put each one back under the part of the lesson it belongs to.
Concept
Seed the pRNG with the key K and a fresh random IV, take its output as the keystream, and XOR. The IV is sent in the clear alongside the ciphertext.
\[ \textbf{Enc}(K, M) = \langle\, IV,\ \mathrm{PRNG}(K, IV) \oplus M \,\rangle \]
\[ \textbf{Dec}(K, IV, C) = \mathrm{PRNG}(K, IV) \oplus C \]
Decryption regenerates the same keystream from K and the received IV, then XORs it off — exactly OTP decryption.
Intuition
Seeding only with K would make every message use the identical keystream — and reusing a one-time pad is the two-time-pad disaster from L19/L21.
A fresh random IV per message makes PRNG(K, IV) different each time, so no two messages share a pad. The IV is public; only K is secret.
Ask yourself: what L19 leak returns the instant two messages share a keystream? (C ⊕ C′ = M ⊕ M′ — the two-time-pad break.)
Step zero
Discussion prompt
§9.5 Encrypt then decrypt a 4-bit message — before any calculation: what is the plan? Name the moves in order, in plain English, without doing the arithmetic.
Hint: It starts with: Take M = 1101 and a keystream PRNG(K, IV) = 1011
Answer:
Worked example
Take M = 1101 and a keystream PRNG(K, IV) = 1011
Why: The pRNG, seeded with the secret key K and this message's IV, emitted the 4-bit keystream 1011.
Encrypt: C = keystream ⊕ M, bit by bit
Why: XOR each plaintext bit with the keystream bit below it — exactly a one-time pad with a generated pad.
| bit j | keystream | m_j | c_j = ks_j ⊕ m_j |
|---|---|---|---|
| 1 | 1 | 1 | 0 |
| 2 | 0 | 1 | 1 |
| 3 | 1 | 0 | 1 |
| 4 | 1 | 1 | 0 |
\[ C = 1011 \oplus 1101 = 0110 \]
Decrypt: regenerate the same keystream from K and IV, then XOR it onto C
Why: Dec = PRNG(K, IV) ⊕ C; the receiver derives the identical 1011 keystream from the public IV and secret K.
\[ M = 1011 \oplus 0110 = 1101 \]
Verify: the recovered bits 1101 equal the original M
Why: §9.5: keystream ⊕ keystream = 0 by self-inverse, so XOR-ing the same pad twice returns M — the round trip is lossless.
Comparison
Comparison matrix
From §9.5 Encrypt then decrypt a 4-bit message: refill the c_j = ks_j ⊕ m_j column from what you know. The rest of the table is as it appeared.
| bit j | keystream | m_j | c_j = ks_j ⊕ m_j |
|---|---|---|---|
| 1 | 1 | 1 | 0 |
| 2 | 0 | 1 | 1 |
| 3 | 1 | 0 | 1 |
| 4 | 1 | 1 | 0 |
Concept
Two parties share only the short secret key K. For each message the sender draws a fresh random IV, seeds PRNG(K, IV), and the keystream is born — no giant pre-shared pad required.
The IV rides along in the clear with the ciphertext. The receiver re-seeds with the same K and IV, regenerates the identical keystream, and XORs it off. Secrecy rests entirely on K; the IV only needs to be fresh, not secret.
Anomaly
Predict first
A student writes this, and it looks reasonable:
A student: 'Generating a new IV each time is a hassle — I'll just seed the pRNG with K and reuse the same keystream for every message.'
It is wrong. Say what breaks — and say it before you turn the page.
Correct: This is the two-time pad (L19/L21).
A student: how do I keep every message's keystream distinct?
Why: This is the two-time pad (L19/L21). The shared keystream cancels and C ⊕ C′ = M ⊕ M′ leaks the plaintexts' XOR — confidentiality is gone.
Trap
A student: 'Generating a new IV each time is a hassle — I'll just seed the pRNG with K and reuse the same keystream for every message.'
\[ C \oplus C' = (ks \oplus M) \oplus (ks \oplus M') = M \oplus M' \]
Reuse one keystream across two messages
Why: This is the two-time pad (L19/L21). The shared keystream cancels and C ⊕ C′ = M ⊕ M′ leaks the plaintexts' XOR — confidentiality is gone.
A student: how do I keep every message's keystream distinct?
Seed with K and a FRESH random IV per message
Why: §9.5: PRNG(K, IV) differs for each IV, so no two messages share a pad. The IV travels in the clear; the keystream is one-time, just like the OTP demands.
Notation
Annotate
From Trap: 'reusing the keystream is fine' — read this one piece at a time. What is each part doing?
On: \( C \oplus C' = (ks \oplus M) \oplus (ks \oplus M') = M \oplus M' \)
Section
Part 4 · §9.5 AES-CTR
Concept
Recall CTR mode from L20: encrypt a running counter under AES and XOR the result onto the plaintext. That AES-of-the-counter output is precisely a keystream — so AES-CTR behaves as a stream cipher.
Technically AES is a pseudorandom permutation, not a generator. But in practice its CTR keystream is indistinguishable from a pRNG's output — provided you don't push one key too far.
Concept
The keystream looks random only while you encrypt at most about 2^64 bits under a single key. Beyond that, CTR's block structure starts to show.
Because AES is a permutation, two distinct counter inputs never give the same AES output — but a true pRNG sometimes would. Past ~2^64 bits, the absence of such collisions is itself detectable, leaking that the underlying plaintext blocks differ.
Intuition
A real random generator occasionally repeats an output block — birthday-style collisions. A permutation like AES never repeats for distinct inputs. For a while that difference is invisible.
But after enough blocks (~2^64 bits), an attacker expects to have seen some collisions from a true pRNG and sees none from AES-CTR. That mismatch is a distinguisher — so rotate keys well before the limit.
Ask yourself: what's the fix when you approach the limit? (Re-key — start a fresh AES key so the block count under any one key stays well below ~2^64 bits.)
Intuition
AES alone scrambles one fixed-size block. CTR mode encrypts a running counter (0, 1, 2, …) under AES and uses those outputs as keystream blocks — so the message never passes through AES directly; only the counter does.
That's why CTR is a stream cipher in disguise: the plaintext is only ever XOR-ed with AES(counter). Encryption and decryption are the identical XOR, exactly like the one-time pad.
Ask yourself: in AES-CTR, what plays the role of PRNG(K, IV) from the stream-cipher scheme? (The sequence AES_K(IV), AES_K(IV+1), … — the keystream.)
Anomaly
Predict first
A student writes this, and it looks reasonable:
A student: 'AES-CTR is a stream cipher, so I can stream a petabyte through one key forever — no IV reuse, so no problem.'
It is wrong. Say what breaks — and say it before you turn the page.
Correct: Even with unique counters, past ~2^64 bits the permutation's lack of collisions becomes detectable, leaking that plaintext blocks differ.
A student: how much data is safe under one CTR key?
Why: Even with unique counters, past ~2^64 bits the permutation's lack of collisions becomes detectable, leaking that plaintext blocks differ. The keystream stops looking random.
Trap
A student: 'AES-CTR is a stream cipher, so I can stream a petabyte through one key forever — no IV reuse, so no problem.'
Assume a single CTR key is safe for unbounded data
Why: Even with unique counters, past ~2^64 bits the permutation's lack of collisions becomes detectable, leaking that plaintext blocks differ. The keystream stops looking random.
A student: how much data is safe under one CTR key?
Keep total data under one key below ~2^64 bits, then re-key
Why: §9.5: AES is a permutation, not a generator; the indistinguishability guarantee holds only below ~2^64 bits. Rotate keys before you reach it.
Section
Part 5 · §9.5 ChaCha20
Concept
HMAC-DRBG's output is a forward-only chain: block i comes from block i−1, which came from i−2, and so on. There is no shortcut to block i — you must walk every step before it.
For encrypting a stream that's fine, but it makes seeking expensive: to decrypt one chunk deep in a file you'd regenerate all the keystream before it. A stream cipher built for large files wants something better.
Explain it
Discussion prompt
Explain §9.5 Why a generic pRNG can't seek to a student a year behind you. No notation, no jargon they have not met — and it still has to be true.
Hint: If your explanation needs a symbol they have never seen, you are describing the notation rather than the idea.
Answer:
HMAC-DRBG's output is a forward-only chain: block i comes from block i−1, which came from i−2, and so on. There is no shortcut to block i — you must walk every step before it.
Concept
Dedicated stream ciphers like ChaCha20 are designed to compute their keystream directly from the key, a nonce, and a block counter — the position is an explicit input, not something you reach by iterating.
So keystream block number i is a direct function of i: you can compute it without computing blocks 0 through i−1 first.
Intuition
Say you want to decrypt one chunk in the middle of an encrypted 1 TB file. With a generic pRNG like HMAC-DRBG you'd have to generate all the keystream up to that point — its state only moves forward, one step at a time.
With ChaCha20 you just set the counter to that chunk's block index and compute its keystream directly. That's random access — jump anywhere instantly.
Ask yourself: why can't HMAC-DRBG do the same jump? (Its output is a forward-only chain — block i depends on running the ratchet i times; there's no formula for block i alone.)
Analogy
Discussion prompt
Explain §9.5 Random access: jump into a 1 TB file by analogy to something with no Computer Security in it at all — a queue, a recipe, a map, a bank balance, whatever fits. Then say where your analogy breaks.
Hint: An analogy that never breaks is not an analogy, it is the same idea wearing a hat. Find the seam — that is the part that is actually new.
Answer:
With ChaCha20 you just set the counter to that chunk's block index and compute its keystream directly. That's random access — jump anywhere instantly.
Ranking
Put in order
Put the moves of §9.5 Why the counter beats a forward-only pRNG into the order they have to happen.
Why: These are the moves of the worked example in the order it makes them, and each one is set up by the one before it. We need only the keystream for that one block — ideally without touching the other 999,999.
Worked example
Goal: decrypt block #1,000,000 of an encrypted file
Why: We need only the keystream for that one block — ideally without touching the other 999,999.
HMAC-DRBG approach: run Generate from the start until you reach block 1,000,000
Why: Its state advances one HMAC at a time, so reaching block i means computing all i prior blocks — costly for large i.
ChaCha20 approach: set the block counter = 1,000,000 and compute that keystream block directly
Why: The counter is an input to the keystream function, so block i is a direct computation — no prior blocks needed.
| cipher | work to reach block i | random access? |
|---|---|---|
| HMAC-DRBG | compute all i blocks | no — forward only |
| ChaCha20 / AES-CTR | compute block i directly | yes — set the counter |
Verify: the counter design makes mid-file decryption O(1) instead of O(i)
Why: §9.5: a counter in the keystream computation is exactly what lets a stream cipher seek — the property a generic pRNG lacks.
Comparison
Comparison matrix
From §9.5 Why the counter beats a forward-only pRNG: refill the random access? column from what you know. The rest of the table is as it appeared.
| cipher | work to reach block i | random access? |
|---|---|---|
| HMAC-DRBG | compute all i blocks | no — forward only |
| ChaCha20 / AES-CTR | compute block i directly | yes — set the counter |
Concept
Viewed as a pRNG, AES-CTR and ChaCha20 lack rollback resistance: the 'state' is just the key plus the counter, and from the current state you can compute every earlier block by simply decrementing the counter.
But that's not a flaw here — it's the same property, seen from the other side. The very jump-forward (and jump-back) ability that loses rollback resistance is exactly what makes these good stream ciphers.
Intuition
HMAC-DRBG is built for a generator's job: emit secrets where past outputs must stay safe even after a compromise — so rollback resistance matters and forward-only is a feature.
A stream cipher's job is encryption with random access: seek anywhere in a file cheaply. There, being able to recompute any block (no rollback resistance) is exactly the win.
Ask yourself: is rollback resistance always desirable? (No — for a stream cipher, random access matters more, and that conflicts with rollback resistance.)
Counterexample
Discussion prompt
HMAC-DRBG is built for a generator's job: emit secrets where past outputs must stay safe even after a compromise — so rollback resistance matters and forward-only is a feature.
That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.
Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.
Answer:
A stream cipher's job is encryption with random access: seek anywhere in a file cheaply. There, being able to recompute any block (no rollback resistance) is exactly the win.
Concept
⊕ Beyond the notes. RC4 was once the most popular stream cipher, but it is now broken: its early keystream bytes are biased (not uniformly random), which leaks information about the plaintext. Don't use it.
⊕ ChaCha20's internals are an ARX design — additions, rotations, and XORs in a 'quarter-round' that scrambles its state. The details are beyond this course; what matters is the counter-based keystream interface above.
Anomaly
Predict first
A student writes this, and it looks reasonable:
A student: 'Rollback resistance is a security property, so a good stream cipher like ChaCha20 should have it too.'
It is wrong. Say what breaks — and say it before you turn the page.
Correct: Misapplied goal. A stream cipher needs random access — set the counter, compute any block.
A student: which property does a stream cipher actually want?
Why: Misapplied goal. A stream cipher needs random access — set the counter, compute any block. That ability is the OPPOSITE of rollback resistance; you can't have both.
Trap
A student: 'Rollback resistance is a security property, so a good stream cipher like ChaCha20 should have it too.'
Demand rollback resistance from a stream cipher
Why: Misapplied goal. A stream cipher needs random access — set the counter, compute any block. That ability is the OPPOSITE of rollback resistance; you can't have both.
A student: which property does a stream cipher actually want?
Favor random access; accept no rollback resistance
Why: §9.5: for AES-CTR/ChaCha20 the state is key+counter, so any block is recomputable. That's not a weakness for encryption — it's what enables seeking in a large file.
Two truths and a lie
Sort into buckets
Some of these hold up and some are the exact mistakes this lesson is built to prevent. Sort them.
Constraint
Discussion prompt
Run The HMAC-DRBG & stream-cipher playbook with this step confiscated:
Stream cipher = OTP from a pRNG: Enc(K,M) = ⟨IV, PRNG(K,IV)⊕M⟩; Dec = PRNG(K,IV)⊕C. Fresh IV per message or you get the two-time-pad leak.
Is it still possible? If it is, say what takes its place and what it costs you. If it is not, say exactly what that step was providing that nothing else does.
Hint: A step you can drop for free was never load-bearing. If you cannot drop it, name the thing that goes wrong the moment it is gone.
Answer:
Pattern
Edge cases
Discussion prompt
The HMAC-DRBG & stream-cipher playbook works on the cases you have just seen. Push it to the edge: what is the most degenerate input it still handles — empty, zero, one item, everything equal — and what is the first case where it stops being true? Name the case, not just "it breaks".
Hint: Try the smallest legal input, then the largest, then the one where two things collide. Methods are specified at their edges; the middle takes care of itself.
Answer:
Elimination
Eliminate the wrong options
What has the developer reintroduced, and what can Eve compute from C and C′ alone?
3 of these 4 are wrong. Strike them one at a time, and say what rules each one out before you strike the next. The survivor is the answer.
Survives elimination: A
Why: §9.5: a stream cipher is a one-time pad with a generated keystream. Reusing one keystream makes C ⊕ C′ = (ks ⊕ M) ⊕ (ks ⊕ M′) = M ⊕ M′ because ks ⊕ ks = 0. Eve recovers the XOR of the two plaintexts with no key — the L19/L21 two-time-pad break. The fix is a fresh random IV per message so PRNG(K, IV) differs each time.
Check
A developer seeds a secure pRNG with only the key K (no IV) and encrypts two different messages M and M′ as C = PRNG(K)⊕M and C′ = PRNG(K)⊕M′ — the SAME keystream both times. Eve intercepts C and C′ but not K. Work it out on paper first.
Check your understanding
What has the developer reintroduced, and what can Eve compute from C and C′ alone?
Answer: A
Why: §9.5: a stream cipher is a one-time pad with a generated keystream. Reusing one keystream makes C ⊕ C′ = (ks ⊕ M) ⊕ (ks ⊕ M′) = M ⊕ M′ because ks ⊕ ks = 0. Eve recovers the XOR of the two plaintexts with no key — the L19/L21 two-time-pad break. The fix is a fresh random IV per message so PRNG(K, IV) differs each time.
Concept
Concept
Concept
Connect it up
Draw it
One page, no notation unless you need it: draw how these connect — Building a pRNG: HMAC-DRBG · Why HMAC-DRBG Is Good · Encrypting a Stream: Stream Ciphers · A Block Cipher as a Stream: AES-CTR · A Counter-Native Cipher: ChaCha20. Put an arrow wherever one of them is what makes another possible, and label the arrow with why.
Recap
You can now run HMAC-DRBG's Seed/Reseed/Generate over the (K, V) state, explain why it absorbs low-entropy seeds and is rollback-resistant, build a stream cipher as a pRNG keystream XOR-ed with the message, encrypt and decrypt with Enc(K,M)=⟨IV, PRNG(K,IV)⊕M⟩, and compare AES-CTR's data limit with ChaCha20's counter-driven random access.
| Idea | § | The one-line version |
|---|---|---|
| HMAC-DRBG state | 9.4 | K = HMAC key, V = HMAC message; output is repeated HMAC |
| Generate | 9.4 | V = HMAC(K, V), append; then ratchet K and V forward |
| Good properties | 9.4 | Absorbs low-entropy seeds; extra bytes never hurt; rollback-resistant |
| Dual_EC_DRBG ⊕ | 9.4 | Backdoored DRBG — prefer transparent, analyzable designs |
| Stream cipher | 9.5 | PRNG(K,IV) ⊕ M — a one-time pad with a generated pad |
| Fresh IV | 9.5 | Reuse the keystream ⇒ two-time pad, C⊕C′ = M⊕M′ leaks |
| AES-CTR | 9.5 | Stream cipher; keep below ~2^64 bits per key |
| ChaCha20 | 9.5 | Counter ⇒ random access; trade-off: no rollback resistance |
Want this taught 1-on-1? Alexander tutors Computer Security — $55/session, free consultation.