CS 161, Lesson 33, in 51 slides. It explains why storing H(w) is not enough, in section 14.7, then covers the offline guessing attack and the amortized precomputation attack, and the two defenses given in section 14.8: a per-user salt, and slow iterated hashing. It closes with what this implies for key derivation, in section 14.9. It is anchored to textbook sections 14.7 to 14.9.
Subject: Computer Security · 82 slides · applied lesson
Open the interactive version of this deck · Homework for this lesson
Title
CS 161 · Lesson 33 of 45
why storing H(w) isn't enough · offline & precomputation attacks · salt · slow iterated hashing · and key derivation
Objectives
Warm-up
Discussion prompt
Before we open L33 · Password Hashing: Salt, Slow Hashes & Key Derivation: without looking back, what was the main idea of L32 · Password Risks & Mitigations, 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 32, in 54 slides. It covers the five password risk vectors in section 14.1, eavesdropping and SSL/TLS in section 14.2, client-side malware and two-factor authentication in section 14.3, and the statistics of online guessing in section 14.4. It then covers the mitigations - rate limiting, CAPTCHAs, and password requirements - in section 14.5, and server compromise and why you must never store cleartext, in section 14.6. It is anchored to textbook sections 14.1 to 14.6.
Concept
Last lesson (L32) warned that a server compromise leaks whatever the server stores. So how should a server store passwords so that a stolen database doesn't hand the attacker everyone's password?
Matching
Match the pairs
From Three questions this lesson answers — match each one to what it actually does. The descriptions have been shuffled.
Why: Why not store H(w)?, What are the fixes?, What about keys? 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 · §14.7 hash, don't store cleartext
Concept
Scenario first: a web service must check the password a user types at login. The naive design stores each user's password w in cleartext — and L32 showed why that's a disaster: one database breach leaks every password directly.
The first improvement: never store w. Instead store its hash H(w) (e.g. SHA-256). The server keeps the hashes; it never keeps the passwords themselves.
Counterexample
Discussion prompt
The first improvement: never store w. Instead store its hash H(w) (e.g. SHA-256). The server keeps the hashes; it never keeps the passwords themselves.
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.
Concept
At registration the server computes and stores H(w). At login it hashes the entered password w′ and compares to the stored value — equal hashes mean the right password.
\[ \text{store } H(w); \qquad \text{on login, accept} \iff H(w') = H(w) \]
Hashes are one-way, so a stolen database of H(w) values doesn't directly reveal any password. This sounds like it solves the problem — but it doesn't.
Analogy
Discussion prompt
Explain §14.7 Store the hash, verify on login 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:
At registration the server computes and stores H(w). At login it hashes the entered password w′ and compares to the stored value — equal hashes mean the right password.
Intuition
A one-way hash can't be run backward — but the attacker doesn't need to. Real passwords aren't random: a huge fraction are drawn from a small, predictable pool ('password123', a pet's name, a dictionary word).
So the attacker doesn't invert H. She guesses forward: hash a likely password, see if it matches a stored value. The weakness isn't the hash — it's that passwords are guessable and the hash is fast.
Ask yourself: what makes guessing forward cheap? (A fast hash + a small space of likely passwords. The next two attacks exploit exactly that.)
Explain it
Discussion prompt
Explain §14.7 One-way ≠ unguessable 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:
Ask yourself: what makes guessing forward cheap? (A fast hash + a small space of likely passwords. The next two attacks exploit exactly that.)
Concept
There are two settings for a guessing attack. Online: the attacker submits guesses to the live login form — slow, and easily throttled by rate limits and lockouts. The server is the bottleneck.
Offline: the attacker has stolen the hash database and tests guesses on her OWN hardware. No server, no throttle. This lesson is about defending against the offline case — the one that survives a breach.
Anomaly
Predict first
A student writes this, and it looks reasonable:
A student: 'SHA-256 is one-way, so once I store H(w) a stolen database is useless to the attacker.'
It is wrong. Say what breaks — and say it before you turn the page.
Correct: One-way means the attacker can't INVERT H — but she can GUESS forward.
A student: what does storing H(w) actually protect against, and what does it not?
Why: One-way means the attacker can't INVERT H — but she can GUESS forward. A fast hash lets her test billions of candidate passwords per second against the stolen hashes. Speed is the enemy here.
Trap
A student: 'SHA-256 is one-way, so once I store H(w) a stolen database is useless to the attacker.'
Conclude that storing a fast cryptographic hash is sufficient
Why: Wrong. One-way means the attacker can't INVERT H — but she can GUESS forward. A fast hash lets her test billions of candidate passwords per second against the stolen hashes. Speed is the enemy here.
A student: what does storing H(w) actually protect against, and what does it not?
Recognize H(w) blocks DIRECT reading, not guessing
Why: §14.7: storing H(w) stops the attacker from reading passwords straight off disk, but a fast hash invites offline and precomputation guessing attacks. We need salt AND a slow hash on top.
Section
Part 2 · §14.7 testing guesses locally
Concept
Mallory has stolen the database of stored hashes. She no longer needs to talk to the website at all. For each candidate guess g, she computes H(g) on her own machine and checks whether it matches any stored hash.
\[ \text{for each guess } g:\quad \text{compute } H(g),\ \text{check } H(g) \stackrel{?}{=} H(w) \]
Offline guessing attack — An attack run entirely on the attacker's own hardware against a stolen hash database. There is no rate limit, no lockout, and no server interaction — the only cost is the time to hash each guess.
Intuition
If every password were a uniformly random 256-bit string, guessing would be hopeless. But humans pick MEMORABLE passwords, so the real distribution is wildly skewed toward a tiny set of likely choices.
Studies and leaked databases show a large fraction of users pick from the same few million strings. That's why testing only 2²⁰ ≈ a million guesses per user already finds about half of all passwords.
Ask yourself: why does the attacker bother with common passwords first? (Because the password distribution is concentrated — a small ordered guess list covers most users for minimal work.)
Socratic
Discussion prompt
Studies and leaked databases show a large fraction of users pick from the same few million strings. That's why testing only 2²⁰ ≈ a million guesses per user already finds about half of all passwords.
Suppose that were not true. What is the first thing in L33 · Password Hashing: Salt, Slow Hashes & Key Derivation that would stop working?
Hint: Follow it one step downstream. The answer is whatever was quietly relying on it.
Answer:
Ask yourself: why does the attacker bother with common passwords first? (Because the password distribution is concentrated — a small ordered guess list covers most users for minimal work.)
Concept
SHA-256 was designed to be fast. On modern hardware an attacker can compute on the order of one billion (10⁹) hashes per second — and far more with GPUs or ASICs.
\[ \text{rate} \approx 10^9 \ \text{hashes/second} = 1\ \text{billion/sec} \]
That speed is a feature for verifying file integrity — but a liability for passwords, where you WANT the attacker's per-guess cost to be high.
Ranking
Put in order
Put the moves of §14.7 The cost of cracking 100 million users 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. The textbook's numbers: testing about 2²⁰ ≈ one million common guesses per user is enough to find roughly half of all passwords, since so many people pick from a small pool.
Worked example
Set up the textbook scenario: a site with 100 million users, testing 2²⁰ guesses per user
Why: The textbook's numbers: testing about 2²⁰ ≈ one million common guesses per user is enough to find roughly half of all passwords, since so many people pick from a small pool.
\[ \text{users} = 10^8, \qquad \text{guesses/user} = 2^{20} \approx 10^6 \]
Count total hashes: 100 million users × 2²⁰ guesses each
Why: Without salt this is the upper bound on work; the total is the product of users and per-user guesses.
| quantity | value | ≈ |
|---|---|---|
| users | 10⁸ | 100 million |
| guesses per user | 2²⁰ | ≈ 10⁶ |
| total hashes | 10⁸ × 2²⁰ | ≈ 10¹⁴ (100 trillion) |
| hash rate | 10⁹ / sec | 1 billion / sec |
| time | 10¹⁴ / 10⁹ | ≈ 10⁵ sec ≈ ~1 day |
\[ \frac{10^8 \times 2^{20}}{10^9\ \text{hash/s}} \approx \frac{10^{14}}{10^9} = 10^{5}\ \text{s} \approx 1\ \text{day} \]
Verify the order of magnitude: 10¹⁴ hashes ÷ 10⁹ hashes/sec ≈ 10⁵ seconds ≈ ~1 day
Why: §14.7: 10⁵ seconds is about 27.8 hours — roughly a single day finds about half of 100 million users' passwords. A fast hash makes mass cracking cheap.
Comparison
Comparison matrix
From §14.7 The cost of cracking 100 million users: refill the value column from what you know. The rest of the table is as it appeared.
| quantity | value | ≈ |
|---|---|---|
| users | 10⁸ | 100 million |
| guesses per user | 2²⁰ | ≈ 10⁶ |
| total hashes | 10⁸ × 2²⁰ | ≈ 10¹⁴ (100 trillion) |
| hash rate | 10⁹ / sec | 1 billion / sec |
| time | 10¹⁴ / 10⁹ | ≈ 10⁵ sec ≈ ~1 day |
Anomaly
Predict first
A student writes this, and it looks reasonable:
A student: 'My login form locks out after 5 wrong tries, so guessing is rate-limited and slow.'
It is wrong. Say what breaks — and say it before you turn the page.
Correct: Lockouts only limit ONLINE guessing at the live login form.
A student: where does the offline attacker actually do the work?
Why: Lockouts only limit ONLINE guessing at the live login form. Once Mallory has the stolen hash database, she guesses OFFLINE on her own machine — no server, no rate limit, no lockout. That's the whole point of an offline attack.
Trap
A student: 'My login form locks out after 5 wrong tries, so guessing is rate-limited and slow.'
Assume lockouts and rate limits stop the guessing attack
Why: Wrong. Lockouts only limit ONLINE guessing at the live login form. Once Mallory has the stolen hash database, she guesses OFFLINE on her own machine — no server, no rate limit, no lockout. That's the whole point of an offline attack.
A student: where does the offline attacker actually do the work?
Realize the work happens entirely on the attacker's hardware
Why: §14.7: with the stolen hashes in hand, H(g) is computed locally and compared locally. The server never sees a single guess, so its rate limits and lockouts are irrelevant.
Section
Part 3 · §14.7 one table cracks everyone
Concept
Here's a deeper problem: H(w) depends only on w, not on WHO chose it. So if Alice and Bob both pick 'password123', their stored hashes are byte-for-byte identical.
\[ w_A = w_B \ \Rightarrow\ H(w_A) = H(w_B) \]
That means Mallory's hashing work isn't tied to any one user — a hash she computes once can be matched against the WHOLE database at once.
Concept
Mallory precomputes a table of (H(g), g) pairs for the 2²⁰ most common passwords, sorts it by hash, then binary-searches each user's stored hash in it.
\[ \text{build sorted } \{(H(g),\, g)\}; \quad \text{lookup cost} = \lg(2^{20}) = 20 \text{ comparisons} \]
Amortized (precomputation) attack — Do the expensive hashing ONCE, store the results, and reuse them against every user. The cost of hashing is amortized across the whole user base, so cracking the second, third, … millionth user is nearly free.
Definition probe
Sort into buckets
Every line below is part of the definition of Offline guessing attack or of Amortized (precomputation) attack — one or the other, never both. Put each where it belongs.
Concept
Storing every (H(g), g) pair for a huge guess list takes a lot of disk. Rainbow tables compress this with a chaining trick: store only the endpoints of long hash chains and regenerate the middle on demand.
The effect is the same threat: a one-time precomputation that then cracks many unsalted users cheaply. Crucially, a salt (Part 4) breaks rainbow tables exactly as it breaks plain precomputation — there's no shared work left to compress.
Sorting
Sort into buckets
These are the pieces of L33 · Password Hashing: Salt, Slow Hashes & Key Derivation, out of order. Put each one back under the part of the lesson it belongs to.
Intuition
Per-user offline guessing redid the same hashes for every user. Precomputation hashes each common password once — about a millisecond of work for 2²⁰ guesses plus a quick sort — and then every user is just a fast lookup.
One precomputed table cracks all users who share a common password. This is the idea behind rainbow tables, which use a clever time/memory trade-off to shrink the stored table.
Ask yourself: what single fact makes this possible? (That H(w) doesn't depend on the user — identical passwords collide, so one table serves everyone.)
Step zero
Discussion prompt
§14.7 Precompute-and-lookup, counted — before any calculation: what is the plan? Name the moves in order, in plain English, without doing the arithmetic.
Hint: It starts with: Precompute: hash the 2²⁰ common passwords once and sort by hash value
Answer:
Worked example
Precompute: hash the 2²⁰ common passwords once and sort by hash value
Why: Hashing 2²⁰ ≈ 10⁶ guesses at 10⁹/sec takes about a millisecond; sorting 10⁶ entries is fast. This cost is paid ONCE for the entire attack.
| phase | cost (once) | cost per user |
|---|---|---|
| per-user guessing | — | 2²⁰ hashes each |
| precompute table | 2²⁰ hashes + sort | — |
| lookup per user | — | lg(2²⁰) = 20 lookups |
Crack each user with a binary search: ~20 comparisons against the sorted table
Why: lg(2²⁰) = 20, so finding a user's password (if common) costs about 20 lookups — no hashing at all per user.
\[ \text{total} \approx \underbrace{2^{20}}_{\text{hash once}} + \underbrace{10^8 \times 20}_{\text{lookups}} \ \ll\ \underbrace{10^8 \times 2^{20}}_{\text{per-user guessing}} \]
Verify: the amortized total is dramatically smaller than per-user guessing
Why: §14.7: per-user guessing pays 2²⁰ hashes for EACH of 10⁸ users; precomputation pays 2²⁰ hashes ONCE then cheap lookups. The hashing work is shared across all users — a huge win for the attacker.
Trade off
Comparison matrix
From §14.7 Precompute-and-lookup, counted: every row here is a choice with a cost. Fill the cost (once) column, then say which row you would actually pick and what you give up for it.
| phase | cost (once) | cost per user |
|---|---|---|
| per-user guessing | — | 2²⁰ hashes each |
| precompute table | 2²⁰ hashes + sort | — |
| lookup per user | — | lg(2²⁰) = 20 lookups |
Explain it to yourself
Discussion prompt
In Trap: 'every user must be attacked separately' this move is made:
Assume the attack cost scales linearly with the number of users
Why is that legal? Name the rule or definition it rests on before you read on.
Hint: If you can only say "because that is what you do", the rule is the thing to go and find.
Answer:
Wrong. Without salt, the hash of a common password is the same for everyone, so ONE precomputed table covers the whole database. Adding more users barely adds cost — only a cheap lookup apiece.
Trap
A student: 'Cracking a million users takes a million times as long as cracking one.'
Assume the attack cost scales linearly with the number of users
Why: Wrong. Without salt, the hash of a common password is the same for everyone, so ONE precomputed table covers the whole database. Adding more users barely adds cost — only a cheap lookup apiece.
A student: how does the attack cost actually scale with users?
Recognize precomputation amortizes the hashing across all users
Why: §14.7: hash each common password once, reuse against everyone. The dominant cost (hashing) is paid once, not per user — exactly what salt will break in Part 4.
Section
Part 4 · §14.8 a unique per-user value
Concept
When a user creates an account, the server picks a fresh random salt s for that user. The salt is NOT secret. The server stores the pair ⟨s, H(w, s)⟩ — the salt in the clear, alongside the salted hash.
\[ \text{at registration: pick random } s; \quad \text{store } \langle s,\ H(w, s) \rangle \]
Salt — A random, per-user, NON-secret value mixed into the hash input. Its only job is to be unique per user so that identical passwords produce different stored hashes. It is stored in cleartext right next to the hash.
Concept
The salt has to be drawn from a good random source (callback to the L25 randomness unit). A long, unpredictable salt guarantees each user's salt is unique — that uniqueness is what blocks shared precomputation.
A predictable or REPEATED salt fails: if many users share salt s, the attacker can again build one table for s and reuse it across all of them — reopening the very attack salt was meant to close.
Concept
Now if Alice and Bob both choose the same password w, they still get DIFFERENT stored hashes, because each gets a different salt:
\[ H(w,\ s_A) \ne H(w,\ s_B) \quad\text{even though}\quad w_A = w_B \]
On login the server reads the user's stored salt s, recomputes H(w′, s), and compares. The salt being public doesn't matter for this — it just has to be there.
Intuition
Precomputation worked because one hash of 'password123' matched every user who chose it. With salt, the attacker would have to build a SEPARATE table for each user's salt — there's no shared work left to amortize.
Rainbow tables and precomputed dictionaries are defeated outright: a table built for salt s_A is useless against salt s_B. The attacker is forced back to attacking each user individually.
Ask yourself: does salt slow down a SINGLE-user guess? (No — salt removes the cross-user shortcut. It does nothing about the speed of one guess; that's what the slow hash in Part 5 fixes.)
Ranking
Put in order
Put the moves of §14.8 Same password, two salts, two hashes 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. Salts are drawn independently at random per user, so s_A and s_B differ with overwhelming probability.
Worked example
Alice and Bob both pick the password w = 'sunshine'; the server assigns each a random salt
Why: Salts are drawn independently at random per user, so s_A and s_B differ with overwhelming probability.
| user | password | salt | stored hash |
|---|---|---|---|
| Alice | sunshine | s_A | H(sunshine, s_A) |
| Bob | sunshine | s_B | H(sunshine, s_B) |
| — | (same w) | (s_A ≠ s_B) | (hashes differ!) |
Note the attacker cannot tell Alice and Bob share a password, and cannot reuse any precomputed table
Why: Because the salts differ, the stored hashes differ, so a table built for one salt never matches the other.
\[ H(\text{sunshine},\, s_A) \ne H(\text{sunshine},\, s_B) \]
Verify the defense: the amortized / rainbow-table attack is defeated, even though the salt is public
Why: §14.8: salt forces per-user work; the attacker can no longer share hashing across users. The salt's publicness is fine — uniqueness, not secrecy, is what does the job.
Comparison
Comparison matrix
From §14.8 Same password, two salts, two hashes: refill the stored hash column from what you know. The rest of the table is as it appeared.
| user | password | salt | stored hash |
|---|---|---|---|
| Alice | sunshine | s_A | H(sunshine, s_A) |
| Bob | sunshine | s_B | H(sunshine, s_B) |
| — | (same w) | (s_A ≠ s_B) | (hashes differ!) |
Concept
In 2012 LinkedIn's password database was stolen. It used unsalted SHA-256 — exactly the broken design from Part 1.
Because there was no salt, precomputation applied at full force: a researcher recovered about 90% of the passwords in roughly 6 days. A salt would have blocked the precomputation entirely.
Anomaly
Predict first
A student writes this, and it looks reasonable:
A student: 'The salt is part of the defense, so it must be a secret — store it encrypted or in a separate vault.'
It is wrong. Say what breaks — and say it before you turn the page.
Correct: The salt is NOT secret — it's stored in cleartext right next to the hash, and the attacker who steals the database gets it too.
A student: what property of the salt actually matters?
Why: The salt is NOT secret — it's stored in cleartext right next to the hash, and the attacker who steals the database gets it too. Its only job is to be UNIQUE per user, not hidden.
Trap
A student: 'The salt is part of the defense, so it must be a secret — store it encrypted or in a separate vault.'
Treat the salt as a secret that must be hidden from the attacker
Why: Wrong. The salt is NOT secret — it's stored in cleartext right next to the hash, and the attacker who steals the database gets it too. Its only job is to be UNIQUE per user, not hidden.
A student: what property of the salt actually matters?
Make the salt random and unique per user — public is fine
Why: §14.8: salt defeats precomputation by making identical passwords hash differently. Even knowing every salt, the attacker can't share hashing across users — that's all salt needs to do.
Section
Part 5 · §14.8 iterate to slow it down
Concept
Salt killed precomputation, but it did nothing about raw speed. Per-user offline guessing still works: 2²⁰ guesses for each of 100 million salted users is still about 10¹⁴ fast hashes — still roughly a day.
The remaining problem is that each hash is too CHEAP. We need to make computing the hash deliberately slow.
Concept
Build a slow function F by applying the hash to itself n times in a row. Each call is fast, but n of them in sequence is n times slower — and n is a tunable knob.
\[ F(x) = \underbrace{H(H(H(\cdots H(x)\cdots)))}_{n\ \text{iterations}} \]
\[ \text{store } \langle s,\ F(w, s) \rangle \quad\text{instead of}\quad \langle s,\ H(w, s) \rangle \]
Concept
Because n is just an iteration count, you can RAISE it as hardware gets faster — keeping the honest cost near 1 ms while the attacker's per-guess cost climbs with it. The defense ages gracefully.
This is why production schemes expose a tunable 'work factor' or 'cost' parameter. You re-hash at a higher cost the next time a user logs in, with no change to the user's password.
Concept
Pick n so that F takes about 1 millisecond to compute. For an honest server seeing at most ~10 logins/second, that's roughly 1% CPU overhead — completely affordable.
But it multiplies the attacker's cost by the same factor. A fast SHA-256 hash is ~1 nanosecond; a 1 ms slow hash is about a million times slower, turning the attacker's 1-day job into thousands of machine-years.
Intuition
The defender computes F once per login — a sprinkle of extra milliseconds nobody notices. The attacker computes F once per guess, across trillions of guesses — so the same slowdown that costs the defender 1% costs the attacker everything.
Slow hashing is why a breach buys you TIME rather than instant compromise: while the attacker grinds through machine-years, users can be warned and forced to reset.
Ask yourself: why doesn't a slow hash hurt the honest server much? (It runs F rarely — once per login — whereas the attacker runs it once per guess, trillions of times.)
Socratic
Discussion prompt
Slow hashing is why a breach buys you TIME rather than instant compromise: while the attacker grinds through machine-years, users can be warned and forced to reset.
Suppose that were not true. What is the first thing in L33 · Password Hashing: Salt, Slow Hashes & Key Derivation that would stop working?
Hint: Follow it one step downstream. The answer is whatever was quietly relying on it.
Answer:
Ask yourself: why doesn't a slow hash hurt the honest server much? (It runs F rarely — once per login — whereas the attacker runs it once per guess, trillions of times.)
Ranking
Put in order
Put the moves of §14.8 Attacker time: fast hash vs slow hash 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. This is the baseline from the offline-guessing worked example: 10¹⁴ hashes at 10⁹/sec ≈ 10⁵ sec ≈ ~1 day.
Worked example
Start from Part 2: with fast SHA-256, cracking 100M users (2²⁰ guesses each) was ~10¹⁴ hashes ≈ ~1 day
Why: This is the baseline from the offline-guessing worked example: 10¹⁴ hashes at 10⁹/sec ≈ 10⁵ sec ≈ ~1 day.
Switch to a 1 ms slow hash: every one of those hashes is now about a million times slower
Why: A 1 ms slow hash vs a ~1 ns fast hash is roughly a 10⁶× slowdown, applied to every guess the attacker makes.
| hash | per-hash cost | attacker time for 10¹⁴ hashes |
|---|---|---|
| fast SHA-256 | ~1 ns | ≈ 10⁵ sec ≈ ~1 day |
| slow hash (1 ms) | ~1 ms | ≈ 10¹¹ sec ≈ ~3000 machine-years |
| honest server | ~1 ms / login | ≈ 1% CPU at 10 logins/sec |
\[ 10^{14}\ \text{hashes} \times 10^{-3}\ \text{s/hash} = 10^{11}\ \text{s} \approx 3000\ \text{machine-years} \]
Verify: a 1 ms slow hash turns the attacker's ~1-day job into ~3000 machine-years, at ~1% server overhead
Why: §14.8: 10¹¹ seconds is about 3000 years on one machine. The same slowdown costs the honest server only ~1% CPU — the asymmetry is the entire point.
Comparison
Comparison matrix
From §14.8 Attacker time: fast hash vs slow hash: refill the attacker time for 10¹⁴ hashes column from what you know. The rest of the table is as it appeared.
| hash | per-hash cost | attacker time for 10¹⁴ hashes |
|---|---|---|
| fast SHA-256 | ~1 ns | ≈ 10⁵ sec ≈ ~1 day |
| slow hash (1 ms) | ~1 ms | ≈ 10¹¹ sec ≈ ~3000 machine-years |
| honest server | ~1 ms / login | ≈ 1% CPU at 10 logins/sec |
Concept
Don't invent your own. Production password hashing uses standardized iterated schemes, all built on the slow-hash idea above:
Concept
⊕ Supplemental — beyond §14.7–14.9. Attackers parallelize cheap iterated hashes on GPUs/ASICs. The modern answer is memory-hardness: force each guess to use lots of MEMORY, not just time, which custom hardware can't cheaply replicate.
Anomaly
Predict first
A student writes this, and it looks reasonable:
A student: 'SHA-256 is too weak — switch to SHA-512 for a longer output and offline guessing goes away.'
It is wrong. Say what breaks — and say it before you turn the page.
Correct: A longer output doesn't make the hash SLOWER to compute — SHA-512 is still a fast hash, ~10⁹/sec.
A student: what actually raises the attacker's per-guess cost?
Why: A longer output doesn't make the hash SLOWER to compute — SHA-512 is still a fast hash, ~10⁹/sec. The attacker guesses just as fast. Output length resists nothing here; you need a deliberately SLOW (iterated, ideally memory-hard) hash.
Trap
A student: 'SHA-256 is too weak — switch to SHA-512 for a longer output and offline guessing goes away.'
Swap SHA-256 for SHA-512 to stop fast offline guessing
Why: Wrong. A longer output doesn't make the hash SLOWER to compute — SHA-512 is still a fast hash, ~10⁹/sec. The attacker guesses just as fast. Output length resists nothing here; you need a deliberately SLOW (iterated, ideally memory-hard) hash.
A student: what actually raises the attacker's per-guess cost?
Use a slow, iterated hash (scrypt / bcrypt / PBKDF2), not a longer fast one
Why: §14.8: offline guessing is beaten by making each hash take ~1 ms, not by lengthening the digest. Tune n (or a memory cost) so one guess is expensive; output size is irrelevant to guessing speed.
Section
Part 6 · §14.9 from passwords to keys
Concept
Scenario: you want to encrypt a file under a password the user types. You need an encryption key k. The tempting shortcut is to hash the password directly with a fast hash:
\[ k = \mathrm{SHA\text{-}256}(\text{password}) \quad\text{— tempting, but weak} \]
This is the SAME mistake as storing a fast unsalted hash — now in the key-derivation setting.
Explain it
Discussion prompt
Explain §14.9 Deriving an encryption key from a password 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:
Scenario: you want to encrypt a file under a password the user types. You need an encryption key k. The tempting shortcut is to hash the password directly with a fast hash:
Concept
An attacker with the ciphertext tries the 2²⁰ common passwords: for each, she computes k = SHA-256(guess) (fast), trial-decrypts, and checks for valid plaintext. She wins in milliseconds.
The fix is identical to password storage: use a slow key-derivation function with a salt — PBKDF2, scrypt, or Argon2 — so each guessed key costs ~1 ms, not ~1 ns.
Analogy
Discussion prompt
Explain §14.9 Why k = SHA-256(password) is weak 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:
An attacker with the ciphertext tries the 2²⁰ common passwords: for each, she computes k = SHA-256(guess) (fast), trial-decrypts, and checks for valid plaintext. She wins in milliseconds.
Intuition
Whether you're STORING a password or DERIVING a key from one, the attacker's move is the same: guess the password, run the function forward, check. So the defense is the same: make the function slow and salt it.
That's why the very tools used to hash passwords — PBKDF2, scrypt, Argon2 — are literally called key-derivation functions. They're the same primitive doing the same job.
Ask yourself: what does the salt do for KEY derivation? (Same as before — defeats precomputation, so one table can't crack many encrypted files that share a password.)
Counterexample
Discussion prompt
Ask yourself: what does the salt do for KEY derivation? (Same as before — defeats precomputation, so one table can't crack many encrypted files that share a password.)
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.
Anomaly
Predict first
A student writes this, and it looks reasonable:
A student: 'SHA-256 outputs 256 bits, which is a full-size key, so k = SHA-256(password) is a strong key.'
It is wrong. Say what breaks — and say it before you turn the page.
Correct: The key is only as unguessable as the password, and a fast hash lets the attacker try 2²⁰ common passwords in milliseconds — compute SHA-256(guess), trial-decrypt, win.
A student: how should you turn a password into an encryption key?
Why: The key is only as unguessable as the password, and a fast hash lets the attacker try 2²⁰ common passwords in milliseconds — compute SHA-256(guess), trial-decrypt, win. A full-length output doesn't help when the input is guessable and the hash is fast.
Trap
A student: 'SHA-256 outputs 256 bits, which is a full-size key, so k = SHA-256(password) is a strong key.'
Use a single fast hash of the password as the encryption key
Why: Wrong. The key is only as unguessable as the password, and a fast hash lets the attacker try 2²⁰ common passwords in milliseconds — compute SHA-256(guess), trial-decrypt, win. A full-length output doesn't help when the input is guessable and the hash is fast.
A student: how should you turn a password into an encryption key?
Derive k with a salted, slow KDF (PBKDF2 / scrypt / Argon2)
Why: §14.9: a slow, salted key-derivation function makes each guessed key cost ~1 ms, turning a millisecond attack into machine-years — the same slow-hash + salt defense as password storage.
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 password-storage playbook with this step confiscated:
Slow the hash — iterate F(x) = H(H(…H(x)…)) n times so one hash ≈ 1 ms; store ⟨s, F(w, s)⟩. Honest server ≈ 1% overhead; attacker's day becomes ~3000 machine-years (§14.8).
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 password-storage 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
Which statement about this scheme is TRUE?
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: B
Why: §14.7–14.8: the salt's job is to be unique per user (it is stored in cleartext and the attacker is expected to have it). Uniqueness makes identical passwords hash differently, so no single precomputed table — and no rainbow table — can be reused across users; the amortized attack is defeated. But salt does nothing about SPEED: each user can still be attacked individually with fast offline guessing (~10⁹ guesses/sec). To stop that you must also make the hash SLOW — iterate it so one guess costs ~1 ms, turning the attacker's ~1-day job into thousands of machine-years. Salt and slow hashing fix two different problems; you need both.
Check
A site stores ⟨s, H(w, s)⟩ using a single fast SHA-256 and a random per-user salt. Mallory steals the whole database, salts included. Think it through before choosing.
Check your understanding
Which statement about this scheme is TRUE?
Answer: B
Why: §14.7–14.8: the salt's job is to be unique per user (it is stored in cleartext and the attacker is expected to have it). Uniqueness makes identical passwords hash differently, so no single precomputed table — and no rainbow table — can be reused across users; the amortized attack is defeated. But salt does nothing about SPEED: each user can still be attacked individually with fast offline guessing (~10⁹ guesses/sec). To stop that you must also make the hash SLOW — iterate it so one guess costs ~1 ms, turning the attacker's ~1-day job into thousands of machine-years. Salt and slow hashing fix two different problems; you need both.
Concept
Concept
Concept
Connect it up
Draw it
One page, no notation unless you need it: draw how these connect — Storing H(w) — the First Idea · Attack 1 — Offline Guessing · Attack 2 — Amortized Precomputation · Fix 1 — Salt (Defeats Precomputation) · Fix 2 — Slow Hashing (Defeats Fast Guessing) · Key Derivation — the Same Lesson. Put an arrow wherever one of them is what makes another possible, and label the arrow with why.
Recap
You can now explain why storing a fast hash H(w) isn't enough, estimate the cost of offline and amortized guessing, apply a per-user salt to defeat precomputation and a slow iterated hash to defeat fast guessing, and carry the same recipe over to password-based key derivation.
| Idea | § | The one-line version |
|---|---|---|
| Store H(w) | 14.7 | Never store cleartext; hashes are one-way |
| Offline guessing | 14.7 | Stolen DB attacked locally; fast hash ≈ 10⁹/sec ≈ ~1 day for 100M |
| Amortized attack | 14.7 | Unsalted: one precomputed/rainbow table cracks everyone |
| Salt | 14.8 | Random, public, per-user → identical pwds hash differently |
| Slow hash | 14.8 | Iterate n× to ~1 ms; attacker's day → ~3000 machine-years |
| Real schemes | 14.8 | scrypt / bcrypt / PBKDF2; ⊕ memory-hard Argon2id |
| Key derivation | 14.9 | Not SHA-256(pwd); use a salted slow KDF |
Want this taught 1-on-1? Alexander tutors Computer Security — $55/session, free consultation.