skip to content

In Go, why use crypto/pbkdf2 rather than hkdf.Key to derive a key from a user passphrase?

level: middleimportance: must knowfreq 52%

answer

  1. one of them has a cost dial
  2. the entropy of the input decides
  3. iter versus info
  4. hkdf costs about two hash operations

basics

~20 s

HKDF has no work factor: hkdf.Key runs only a couple of hash operations, so guessing a weak passphrase stays cheap. pbkdf2.Key takes an explicit iteration count that makes every guess as costly as you choose.

solid answer

~40 s

`hkdf.Key` assumes its `secret` argument is already high-entropy — it costs about two hash-based operations, so it does nothing to slow an attacker enumerating passphrases. `crypto/pbkdf2`, which landed alongside `crypto/hkdf` in Go 1.24, exists for exactly that case: `pbkdf2.Key(sha256.New, password, salt, iter, keyLength)` takes the passphrase as a `string` and an explicit `iter` count that multiplies the cost of every attempt. The rule inside my key library is therefore about the *input*: a passphrase or anything else low-entropy goes through `pbkdf2.Key`; a randomly generated master secret, a shared secret from a handshake, or a key that came out of an earlier derivation goes through `hkdf.Key`. Worth knowing that the standard library gives you PBKDF2 only — bcrypt, scrypt and Argon2 still live in `golang.org/x/crypto`, so a memory-hard function means taking that module.

code

go · 12 lines
go
const iterations = 600_000 // a tuned cost, revisited as hardware improves

strong, err := pbkdf2.Key(sha256.New, passphrase, userSalt, iterations, 32)
if err != nil {
	return err
}

// strong is uniform, so HKDF's assumption about its secret argument holds
fileKey, err := hkdf.Key(sha256.New, strong, nil, "acme/backup-file/v1", 32)
if err != nil {
	return err
}

go deeper

for a junior

Remember the split by the input: a human-typed passphrase goes to pbkdf2.Key, a randomly generated secret goes to hkdf.Key. Know that only the first of the two has an iteration count.

for a middle

Explain why an iteration count exists at all and why HKDF deliberately has none, and name pbkdf2.Key's arguments including that the password parameter is a string rather than a byte slice.

for a senior

Show the combined shape — pay PBKDF2 once, then fan out with HKDF — and be ready to say what raising the iteration count costs on your own login path.

for a principal

Own the dependency call: standard-library-only means PBKDF2, and reaching for a memory-hard function means adding golang.org/x/crypto and owning its upgrades across every service that derives keys.

## Two KDFs, two jobs Go 1.24 added `crypto/hkdf` and `crypto/pbkdf2` to the standard library at the same time, and they are not alternatives. They answer different questions about the *input*. `crypto/hkdf` answers: *I already hold a strong secret; give me several independent keys from it.* Its cost is deliberately near zero — the whole computation is roughly two hash-based operations. That is a feature: derivation happens on hot paths and there is nothing to defend against, because guessing a 256-bit random secret is not a threat model. `crypto/pbkdf2` answers: *my input is something a human chose; make guessing expensive.* Its whole point is a tunable cost. ## The signatures ```go // crypto/hkdf func Key[Hash hash.Hash](h func() Hash, secret, salt []byte, info string, keyLength int) ([]byte, error) // crypto/pbkdf2 func Key[Hash hash.Hash](h func() Hash, password string, salt []byte, iter, keyLength int) ([]byte, error) ``` The differences read straight off the parameter lists. - `pbkdf2.Key` takes the password as a **`string`**, which is what you have when a human typed it. `hkdf.Key` takes `[]byte`, which is what you have when a random source produced it. - `pbkdf2.Key` has an **`iter`** parameter and no `info` parameter. `hkdf.Key` has an **`info`** parameter and no cost parameter. Neither omission is an oversight. `iter` is the work factor: PBKDF2 applies its underlying hash-based function that many times in a chain, so raising `iter` raises the attacker's cost per guess linearly — and yours too, once per login. It is a number you tune against your own latency budget and revisit as hardware gets faster, not a constant you copy once and forget. ## Why HKDF cannot substitute Running `hkdf.Key` on a passphrase is not merely weaker; it provides no defence at all against the attack that matters. An attacker with the salt and the derived key can test candidate passphrases at whatever rate their hardware allows, and HKDF's cost per candidate is two hash operations. Against a passphrase with, say, 30 bits of realistic entropy, that is over in a very short time. PBKDF2 with a meaningful iteration count multiplies that time by the iteration count. The inverse mistake is milder but still worth naming: putting a random 32-byte master secret through `pbkdf2.Key` buys nothing. There is no guessing attack to slow, and you have paid the latency anyway. ## The two salts are not the same salt Both functions take a salt and the arguments look alike, but their jobs differ. - **PBKDF2's salt** is per-user and its job is to stop one precomputed table from attacking every user at once. Two users with the same passphrase must get different derived keys, which only happens if their salts differ. It is generated per user and stored alongside the record. - **HKDF's salt** is typically a single, shared, non-secret value for the whole deriver, and it does not separate purposes. The `info` string does that. Collapsing the two in your head is the most common way this material gets answered badly. ## Combining them When you have a passphrase *and* need several keys from it, the shape is: pay the work factor once, then fan out cheaply. ```go strong, err := pbkdf2.Key(sha256.New, passphrase, userSalt, iterations, 32) if err != nil { return err } fileKey, err := hkdf.Key(sha256.New, strong, nil, "acme/backup-file/v1", 32) indexKey, err := hkdf.Key(sha256.New, strong, nil, "acme/backup-index/v1", 32) ``` Calling `pbkdf2.Key` three times instead would triple the deliberate cost on the user's login path for no security benefit at all. The expensive function runs once; HKDF does the multiplication. Note that `strong` is a legitimate input to `hkdf.Key` precisely because PBKDF2 already produced a uniform output — HKDF's assumption about its `secret` argument is satisfied. ## What the standard library does and does not give you As of Go 1.24 the standard library ships HKDF and PBKDF2. It does **not** ship bcrypt, scrypt or Argon2; those remain in `golang.org/x/crypto`. That matters practically: if a team's policy is standard-library-only, the policy itself selects PBKDF2, and reaching for a memory-hard alternative means accepting `golang.org/x/crypto` as a dependency and owning its upgrade path. Knowing which package a function lives in is a genuine Go-catalogue question, and getting it wrong — claiming Argon2 is in the standard library — is the usual tell.

  • Do PBKDF2's salt and HKDF's info string do the same job?
    No. PBKDF2's salt is per-user and stops one precomputed table from attacking every user at once; it does not separate purposes. HKDF's info string is a domain separator that makes two keys from one secret unrelated. HKDF also takes a salt, but that one is usually a single shared, non-secret value belonging to the extract stage.
  • You have a passphrase and need three different keys. How many pbkdf2.Key calls do you make?
    One. Run `pbkdf2.Key` once to pay the work factor and get a uniform key, then feed that to `hkdf.Key` three times with different info strings. Three PBKDF2 calls would triple the deliberate latency on every login for no security gain, and the expensive function is the one you least want in a loop.
  • Does Go's standard library give you Argon2 or bcrypt?
    No. Since Go 1.24 the standard library ships `crypto/hkdf` and `crypto/pbkdf2` only; bcrypt, scrypt and Argon2 remain in `golang.org/x/crypto`. So a standard-library-only policy effectively selects PBKDF2, and choosing a memory-hard function is also a decision to take on that module as a dependency.

saying these in an interview costs you the question

  • Runs hkdf.Key directly on a user passphrase
  • Thinks HKDF's info string adds computational cost
  • Calls pbkdf2.Key once per derived key
  • Believes Argon2 ships in Go's standard library
  • Confuses PBKDF2's per-user salt with HKDF's info label