KDFs & Password Hashing
Work through examples, try the reasoning yourself, then put it into practice.
View SlidesLesson
5 min readKDFs and passwords: preserve entropy, price guesses
A password and a cryptographic shared secret need different treatment. A properly generated key-agreement secret may contain substantial uncertainty for an attacker even if its byte representation is not uniform. A human-chosen password may be among a small number of likely guesses. No deterministic KDF can turn that small guess set into the unpredictability of an independently generated random key.
Start with an attacker's candidate set
Suppose the secret is uniformly selected from 10,000 PINs. The attacker knows the salt and algorithm and has a way to recognize the correct derived key. Producing 32 bytes with HKDF leaves at most 10,000 candidate outputs for that fixed salt: the attacker can compute one for each PIN. A 256-bit output length does not create 256 bits of secret entropy. A salt changes the mapping and prevents reuse across different salts; because it is public, it does not remove guesses.
Password hashing increases the work needed for each guess. It does not create missing entropy either. Keep three questions separate: how many plausible secrets exist, how much work one guess costs, and whether work can be reused across accounts.
HKDF: extract existing uncertainty, then separate uses
For SHA-256, extraction gives a 32-byte intermediate key:
PRK = HMAC-SHA256(salt, IKM)
Extraction concentrates entropy already present in suitable input keying material. Its security depends on the source and cryptographic assumptions; it is not a password-hardening operation. With an absent salt, this exercise uses 32 zero bytes.
Expansion produces requested-length output:
T(0) = empty
T(i) = HMAC-SHA256(PRK, T(i-1) || info || byte(i))
OKM = first L bytes of T(1) || T(2) || ...
For L=42, compute T(1) using empty prefix and counter 01, then T(2) using all 32 bytes of T(1) and counter 02. Return all T(1) plus ten bytes of T(2). Replacing that prefix with the salt or restarting every block with empty input changes the algorithm.
The counter is one byte: at most 255 blocks, or 8160 SHA-256 bytes. The exercise returns nil for negative lengths or L>8160; L=0 returns empty output. Validate before allocating and never wrap counter 255 to zero. These equations and limits follow RFC 5869 sections 2–3.
info identifies the purpose, protocol version, algorithm, or direction. Deriving send and receive keys with identical PRK/info produces identical outputs; distinct unambiguous context labels separate their intended use. Labels are public context, not additional secret entropy. Asking for 16 and 32 bytes with identical PRK/info gives the same first 16 bytes; if length is part of the intended distinction, encode it in the context. Specify a structured encoding so labels such as (ab,c) and (a,bc) cannot concatenate ambiguously.
PBKDF2: make one password guess expensive
For output block i and positive iteration count c:
U1 = HMAC(password, salt || INT_32_BE(i))
Uj = HMAC(password, U(j-1)) for j=2..c
Ti = U1 XOR U2 XOR ... XOR Uc
For c=3 and a 40-byte result, compute three U values for i=1 and three for i=2. XOR within each block; concatenate T1 and the first eight bytes of T2. The block counter uses four big-endian bytes here, unlike HKDF's one-byte counter. Do not return only Uc: all U values participate in the XOR. These are the PBKDF2 construction rules in RFC 8018 section 5.2.
The work is approximately c*ceil(length/32) HMAC evaluations. Increasing c raises attacker and defender costs. PBKDF2 requires little working memory, so many guesses can be run in parallel. Memory-hard functions such as Argon2 also impose memory costs and tradeoffs; they do not make parallel guessing impossible. RFC 9106 describes Argon2 variants and parameter selection. Actual service parameters need benchmarking under concurrency and a latency/resource budget, not a copied iteration count from this exercise.
Salt, pepper, and a verifiable record
A password salt should be independently generated per record and stored openly with it. Distinct salts prevent two equal passwords from automatically sharing a verifier and stop one precomputed table from covering all records. Random generation makes accidental collisions unlikely; it does not mathematically guarantee uniqueness.
A pepper is an additional secret stored separately from the password database. In this exercise only, the PBKDF2 input is the exact byte concatenation password || pepper. Knowing the database alone then does not provide all derivation inputs. This fixed teaching construction is not a general recipe for inventing a password protocol or a reason to store the pepper alongside each record.
The record is pbkdf2-sha256$iter=...$salt=...$dk=.... Verification parses its algorithm and parameters, rejects malformed records, derives exactly len(dk) bytes, then compares every corresponding byte without early exit. Different lengths can be rejected first because length is public. Accumulate byte differences with OR; returning on the first mismatching byte reveals a data-dependent comparison path. This addresses the comparison operation, not constant timing of all parsing, allocation, and password derivation.
On a successful login, a service can rehash with a fresh salt and newer parameters. It cannot recover the password from the old derived key alone. The exercise parser handles syntactic validity; an exposed service additionally needs upper resource limits on iteration counts and record lengths before honoring stored parameters.
Reconstruct and diagnose
- A 6-digit PIN is expanded into a 64-byte key with a public salt. Does key length imply 512-bit guessing security?
- How many HMAC evaluations derive 33 PBKDF2 bytes at c=4?
- Can an attacker reuse the identical derived verifier for two accounts with different salts?
- Why is HKDF info insufficient as a password defense?
Model answers
- No. There are at most one million PIN candidates; each can be derived and tested if a checking oracle is available.
- Eight: two SHA-256 blocks times four iterations.
- Generally no; they must derive for each salt. The salt does not stop the guesses themselves.
- It separates context but is public and cheap to incorporate in every guess. Password defenses need expensive derivation and sufficient secret uncertainty.
Practice sequence
Use the order below for a guided path, or choose an item to revisit. A pass records a successful attempt; try a fresh example later to check what you can reconstruct.
- Step 1
HMAC-SHA256
- Step 2
HKDF-SHA256
- Step 3
PBKDF2-HMAC-SHA256
- Step 4
Password Hash Verification
Verify a PBKDF2-based password hash format with salt and pepper.
- Step 5
KDFs & Password Hashing Checkpoint
Worked scenarios: trace state, diagnose mistakes, and apply the module contract.