A Diffie-Hellman output is not a key
You ran a Diffie-Hellman exchange and both sides now hold the same 32 bytes. The temptation is to drop those bytes straight into AES and call it a day. Don't. A Diffie-Hellman result is high-entropy but not uniformly random: the bits sit on a curve, not in a flat distribution. And you almost never need just one key. You need one to encrypt and a separate one to authenticate, often a distinct key per direction on top of that. Reuse the same raw secret across all of them and a single slip in one context can sink the rest.
HKDF (RFC 5869) is the function that fixes this. It takes one secret and hands back as many independent, purpose-bound keys as you ask for. Hugo Krawczyk designed it, it's HMAC underneath, and it's the key-derivation step inside TLS 1.3, the Signal protocol, the Noise framework, and SSH.
Extract, then expand
HKDF is two operations bolted together, and the split is the entire point.
Extract takes your input keying material (the Diffie-Hellman output) plus an optional salt and computes HMAC(salt, IKM) to produce a fixed-length pseudo-random key, the PRK. Its job is condensation. Whatever non-uniform structure the source had, extract launders it into something indistinguishable from random. With HMAC-SHA-256 the PRK is 32 bytes no matter how big or weird the input was.
Expand takes that PRK plus an info string and a target length L, then stretches it into output keying material by chaining HMAC calls with a one-byte counter. Block 1 is HMAC(PRK, info ‖ 0x01); block 2 feeds block 1 back in as HMAC(PRK, T(1) ‖ info ‖ 0x02), and so on until you have L bytes. That single-byte counter is also where HKDF's one hard limit lives. It runs 0x01 through 0xFF, so a single call yields at most 255 × HashLen bytes — 8160 bytes with SHA-256. Plenty for keys. Not a stream cipher.
The reason to separate the two is that they have different security goals. Extract needs a good condenser; expand needs a good pseudorandom generator. Splitting them maps HKDF cleanly onto NIST SP 800-56C's two-step KDF, and it lets you skip extract entirely when the input is already a uniform key. TLS 1.3 calls HKDF-Expand directly in parts of its key schedule for exactly that reason.
The info parameter is the part people skip
Here is the mechanism that turns one PRK into a dozen safe keys. The info argument binds each derived key to a specific purpose. Same PRK, different info, completely unrelated output:
HKDF-Expand(PRK, "encryption key v1", 32)gives you an AES-256-GCM keyHKDF-Expand(PRK, "auth token v1", 32)gives you a MAC key with no relationship to the firstHKDF-Expand(PRK, "client→server", 32)gives you a directional key, distinct from its reverse
This is domain separation, and skipping it is the bug. Without distinct info contexts, two parts of your system that both derive a "32-byte key" from the same PRK get the same bytes. Your encryption key and your authentication key are now identical, and an attacker who pries one loose has both. Give each a unique context string and the keys become cryptographically unrelated: leaking one tells an attacker nothing about the rest. The cost is one constant string per call. The payoff is that a compromise stays boxed into the single purpose it touched.
One habit that saves real pain later: version your info strings. "messages-key:v2" is cheap insurance for the day you need to rotate a derivation without colliding with old material.
Salt and info are not the same thing
Both are optional, both feed HMAC, and that's where the resemblance stops.
| Parameter | Phase | Job | Needs to be secret? |
|---|---|---|---|
salt | Extract | Decorrelate the PRK across runs; strengthen extraction | No. Public is fine, even a fixed value |
info | Expand | Bind each output key to a distinct purpose | No. It's a context label, not a key |
Salt acts on the input side, before you have a PRK. info acts on the output side, after. Omit salt and HKDF defaults it to a string of zero bytes the length of the hash output; extraction still works, just weaker against certain analyses. Omit info and you lose domain separation, which is the more dangerous of the two mistakes. Neither value needs to be secret, and both belong in any serious design.
HKDF is not a password hash
This trips people up constantly, so plainly: HKDF assumes its input already has high entropy. It's fast on purpose. A Diffie-Hellman result, a hardware RNG output, a pre-shared key — those are all fine inputs. Feed it a human password and you've built something dangerous, because HKDF does nothing to slow down an attacker guessing low-entropy inputs. A handful of HMAC calls is exactly what you don't want when someone is throwing a billion guesses a second at "hunter2".
That's a different tool's mandate. Passwords want a deliberately slow, memory-hard function — Argon2id tuned to something like 256 MiB of memory, so every guess costs an attacker real hardware. Argon2id stretches weak secrets; HKDF organizes strong ones. Same family, opposite job. It's the same line we draw across how beebeeb derives and protects keys: Argon2id guards the password boundary — the one-time key-stretch you pay at login and during recovery, not per file — and HKDF-style derivation gives each distinct purpose its own key downstream, so one exposure never unlocks the rest. The file encryption itself is AES-256-GCM, with key agreement over X25519 and OPAQUE on the login path, and keys are zeroized from memory the moment they're done being used.
The shape to copy
Wiring HKDF into a protocol comes down to four steps:
- Get high-entropy input — a Diffie-Hellman output, an RNG, a PSK. Never a password.
- Compute
PRK = HKDF-Extract(salt, IKM), skipping this only when the IKM is already a uniform key. - For every key you need, call
HKDF-Expand(PRK, info, L)with a unique, versionedinfostring. - Never reuse one derived key for two purposes. Deriving a fresh one is nearly free.
The discipline isn't in the algorithm; it's in the contexts you feed it. HKDF buys you independent keys for the price of a label, which is why the systems that take end-to-end encryption seriously all lean on it. If you want to see what that discipline buys at the product level,