In crypto/hkdf, when would you call Extract and Expand yourself instead of hkdf.Key?
answer
- two stages, one convenience wrapper
- the salt belongs to the first stage
- info belongs to the second stage
- extract once when you expand many times
basics
~20 shkdf.Key is Extract followed by Expand in one call. Call the halves yourself when one secret feeds many keys: Extract once to get a pseudorandom key, then Expand it repeatedly with different info strings, skipping the repeated extraction.
solid answer
~40 sHKDF is two stages and `crypto/hkdf` exposes both. `hkdf.Extract(sha256.New, secret, salt)` compresses possibly-uneven input keying material together with a non-secret salt into a uniformly pseudorandom key. `hkdf.Expand(sha256.New, prk, info, keyLength)` stretches that pseudorandom key into as many bytes as you ask for, bound to the `info` label. `hkdf.Key` is exactly `Extract` then `Expand`, and the package documentation says to prefer it for common cases. I reach for the split only when one secret backs many keys and I want to extract once and expand per purpose. It is also the cleanest way to remember which argument goes where: the salt belongs to extraction, the info string to expansion. And `Expand` assumes its input is already a uniform key — handing it a raw secret skips the step that fixes that.
code
go · 14 linesprk, err := hkdf.Extract(sha256.New, master, salt) // once
if err != nil {
return err
}
cookieKey, err := hkdf.Expand(sha256.New, prk, "acme/session-cookie/v1", 32)
if err != nil {
return err
}
urlKey, err := hkdf.Expand(sha256.New, prk, "acme/download-url/v1", 32)
if err != nil {
return err
}go deeper
Know that hkdf.Key is the one-call form and that Extract and Expand are its two halves. You are not expected to split them yourself yet, only to recognise the three functions.
Explain what each stage is for — extraction makes the input uniform, expansion produces the length you need — and place the salt and the info argument on the correct stage without hesitating.
Justify when splitting is actually worth it, and be ready to say what a nil salt becomes inside the package and why that determinism matters across environments and replicas.
Decide whether your library exposes Extract and Expand at all, or only named per-purpose accessors that no caller can stage wrongly in the first place.
## Two stages, and why there are two HKDF, as defined in RFC 5869, is deliberately split into *extract* and *expand*, and `crypto/hkdf` mirrors that split exactly. **Extract** takes input keying material that may be high-entropy but unevenly distributed — the raw output of a Diffie-Hellman style key agreement is the canonical example, where every bit is unpredictable but the bits are not uniformly spread across the value space — and condenses it, together with a salt, into a fixed-size *pseudorandom key* (conventionally abbreviated `prk`). The output is exactly the hash's size: 32 bytes with SHA-256. ```go func Extract[H hash.Hash](h func() H, secret, salt []byte) ([]byte, error) ``` **Expand** takes an already-uniform pseudorandom key and stretches it to whatever length you need, mixing in a context label: ```go func Expand[H hash.Hash](h func() H, pseudorandomKey []byte, info string, keyLength int) ([]byte, error) ``` Note the asymmetry in the parameter types, which is a useful memory aid: the salt is `[]byte` and belongs to extraction; the info label is a `string` and belongs to expansion. **Key** is the composition: ```go func Key[Hash hash.Hash](h func() Hash, secret, salt []byte, info string, keyLength int) ([]byte, error) ``` Its implementation is literally `Expand(h, Extract(h, secret, salt), info, keyLength)`. The package doc on `Extract` says as much: use the split *only* if you need to reuse the extracted key with multiple `Expand` invocations and different context values; most scenarios, including generating multiple keys, should use `Key`. ## When the split earns its place The honest answer is: rarely, and for one reason. If a library derives ten keys from one master secret at startup, calling `Key` ten times repeats the extract step ten times. Extracting once and expanding ten times skips that. It is a small saving — extraction is one hash-based operation — so this matters when derivation is on a hot path, not at process start. The second, better reason is structural clarity. A library that holds a `prk` field makes it obvious that the salt is a property of the deriver as a whole and the info string a property of each call site. That maps cleanly onto how the two values are actually governed: one salt, chosen once and shipped in configuration; many labels, allocated per purpose. ## What each stage assumes The trap is calling `Expand` directly on a value that has not been extracted. `Expand` treats its `pseudorandomKey` argument as already uniformly random. Feeding it a value that is merely *secret* — a coordination-service token, a base64 blob someone pasted, a passphrase — skips precisely the step that would have made it uniform, and the package will not stop you. The only inputs that are legitimately safe to pass straight to `Expand` are values that already came out of an `Extract` call, or out of a previous `Expand`, or from a cryptographically secure random source. Going the other way is harmless: calling `Extract` on a value that is already uniform costs one extra operation and changes nothing about the security of the result. When in doubt, use `Key`. ## What a nil salt does `Extract` accepts a `nil` salt. Internally the package substitutes a slice of zero bytes as long as the hash's output — 32 zero bytes for SHA-256 — which is exactly what RFC 5869 prescribes for the salt-less case. So a `nil` salt is valid, defined and deterministic. It is not an error and never will be. That determinism is worth stating out loud, because it is where a subtle production surprise comes from. Two services that pass the same secret, `nil` salt and the same info string derive byte-identical keys. If that is intended — two replicas of the same service that must agree — it is exactly right. If it is not intended, because staging and production were both provisioned with `nil` salt and a copied secret, nothing errors and the two environments quietly share keys. ## Errors Both `Extract` and `Expand` return an error, and in practice the only condition either reports outside FIPS 140-only mode is on `Expand`: `keyLength` may not exceed the hash's `Size()` times 255, because the expand stage numbers its output blocks with a single byte. With SHA-256 that ceiling is 8160 bytes. Nothing validates the salt, the info string or the length of the secret. ## Practical shape ```go prk, err := hkdf.Extract(sha256.New, master, salt) if err != nil { return err } cookieKey, err := hkdf.Expand(sha256.New, prk, "acme/session-cookie/v1", 32) ``` If you find yourself writing that and only ever calling `Expand` once, replace both lines with a single `hkdf.Key` call. The split is a specialisation, not the default.
- What does `crypto/hkdf` do when the salt passed to Extract is nil?It substitutes a salt of zero bytes as long as the hash's output — 32 zero bytes for SHA-256 — so the call succeeds and stays fully deterministic. RFC 5869 explicitly permits a salt-less HKDF. The consequence is that two systems passing the same secret, a nil salt and the same info string derive byte-identical keys, which is correct when intended and a quiet defect when it is not.
- Can you skip Extract and pass the master secret straight to `hkdf.Expand`?Only when that secret is already a uniformly random or pseudorandom key. Expand assumes its input is uniform and does nothing to make it so; extraction is the stage that fixes an uneven distribution. A key from a secure random source, or one that already came out of Extract or Expand, is fine. Anything else should go through `hkdf.Key`.
- Does the salt have to be identical on every machine that derives the same key?Yes. HKDF is deterministic in all of its inputs, so the salt is part of the key's identity rather than a per-run nonce. Treat it like the info labels: fixed, non-secret, held in configuration or a constant. Generating a fresh random salt at startup means a fresh key at startup, which almost never is what you want.
saying these in an interview costs you the question
- Thinks Extract encrypts and Expand decrypts
- Passes the salt to Expand and the info string to Extract
- Believes Extract must run before every Expand call
- Feeds an unevenly distributed secret straight to Expand
- Treats HKDF's salt as a per-call random nonce