Signature
hash_hkdf(
string $algo,
#[\SensitiveParameter] string $key,
int $length = 0,
string $info = "",
string $salt = ""
): stringPurpose
hash_hkdf() implements HKDF, the HMAC-based key derivation function described in RFC 5869. You give it a secret that already has good entropy, called input keying material, and it produces one or more new keys of the length you request. The usual reason is key separation: one master secret yields an encryption key, a signing key and so on, and compromising one derived key does not reveal the others.
HKDF is not a password hashing function. It is fast by design, so it does not protect weak, human-chosen passwords. For passwords use password_hash(), or hash_pbkdf2() where PBKDF2 is required.
Signature
Parameters and defaults
$algo: the hash algorithm name, such as"sha256"or"sha512". It must be a cryptographic algorithm that HMAC supports. Non-cryptographic hashes are rejected.$key: the input keying material as raw binary. It cannot be empty. The#[\SensitiveParameter]attribute keeps its value out of stack traces.$length: the desired output size in bytes. The default0means the digest size of the chosen algorithm, so 32 for SHA-256 and 64 for SHA-512. The maximum is 255 times the digest size.$info: an optional context string that binds the derived key to a purpose, such as"encryption". The default is an empty string.$salt: an optional salt. The default is an empty string. The manual notes that a random salt significantly improves the strength of HKDF.
Return value and failures
The function returns a raw binary string of exactly $length bytes, or the digest size when $length is 0. Raw binary is usually not printable, so convert it with bin2hex() for display or logging.
Since PHP 8.0.0 a ValueError is thrown when $key is empty, when $algo is unknown or non-cryptographic, or when $length is negative or greater than 255 times the digest size. Before 8.0.0 the function returned false and emitted a warning.
Example 1: a published test vector
RFC 5869 lists known inputs and outputs, which makes it a good way to confirm that you are calling the function correctly.
$ikm = str_repeat("\x0b", 22);
$salt = hex2bin('000102030405060708090a0b0c');
$info = hex2bin('f0f1f2f3f4f5f6f7f8f9');
$okm = hash_hkdf('sha256', $ikm, 42, $info, $salt);
echo bin2hex($okm), "\n";3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865
The 42-byte result is printed as 84 hexadecimal characters.
Example 2: output lengths
With $length left at 0, the output matches the digest size of the algorithm. An explicit length overrides that.
$key = str_repeat('k', 32);
echo json_encode([
'sha256' => strlen(hash_hkdf('sha256', $key)),
'sha512' => strlen(hash_hkdf('sha512', $key)),
'ten' => strlen(hash_hkdf('sha256', $key, 10)),
]), "\n";{"sha256":32,"sha512":64,"ten":10}
Example 3: key separation and errors
The same inputs always give the same output. Changing only $info gives an unrelated key. Invalid arguments throw ValueError.
$master = str_repeat("\x01", 32);
$salt = 'fixed-salt';
$enc1 = hash_hkdf('sha256', $master, 32, 'encryption', $salt);
$enc2 = hash_hkdf('sha256', $master, 32, 'encryption', $salt);
$mac = hash_hkdf('sha256', $master, 32, 'authentication', $salt);
$errors = [];
try {
hash_hkdf('sha256', '');
} catch (ValueError $e) {
$errors[] = 'empty key';
}
try {
hash_hkdf('sha256', $master, 255 * 32 + 1);
} catch (ValueError $e) {
$errors[] = 'too long';
}
try {
hash_hkdf('crc32', $master);
} catch (ValueError $e) {
$errors[] = 'crc32';
}
echo json_encode([
'repeatable' => $enc1 === $enc2,
'separate' => $enc1 !== $mac,
'errors' => $errors,
]), "\n";{"repeatable":true,"separate":true,"errors":["empty key","too long","crc32"]}
Edge cases
- For SHA-256 the largest legal length is 8160 bytes, because 255 × 32 = 8160. Asking for 8161 throws.
- An empty salt is allowed. RFC 5869 treats it as a string of zero bytes the size of the hash output. A unique salt is still better when you can supply one.
- The
$infovalue is not secret. Its job is to label the purpose of the key, so use distinct, stable labels for each derived key. - Derived bytes are binary and may contain null bytes. Do not print them directly or compare them with
==. Usehash_equals()when comparing secrets.
Common mistakes
- Feeding it a password. HKDF does not slow down guessing. It assumes the input is already high-entropy.
- Reusing one derived key for several purposes. Derive a separate key per purpose by varying
$info. - Treating the result as text. It is raw binary, so use
bin2hex()orbase64_encode()before storing or showing it. - Expecting
falseon error. Modern PHP throwsValueError, so wrap risky input intry/catchwhere the values come from outside. - Generating a new random salt and forgetting to store it. If the derivation must be reproduced later, keep the salt with the data.
- Choosing a non-cryptographic algorithm. Names such as
crc32are rejected. Usehash_hmac_algos()to see what is accepted.
Related functions
hash_pbkdf2()derives keys from passwords using many iterations.hash_hmac()computes a keyed message authentication code.hash_hmac_algos()lists algorithms valid for HMAC-based functions.hash_equals()compares secret strings in constant time.bin2hex()makes binary output readable.
Recap
Use hash_hkdf() to expand or split a strong secret into purpose-specific binary keys. Pick a cryptographic algorithm, pass a non-empty key, set $length explicitly when you need a specific size, vary $info for each purpose, and include a salt when possible. Expect a ValueError for invalid arguments.
Tested examples
$ikm = str_repeat("\x0b", 22);
$salt = hex2bin('000102030405060708090a0b0c');
$info = hex2bin('f0f1f2f3f4f5f6f7f8f9');
$okm = hash_hkdf('sha256', $ikm, 42, $info, $salt);
echo bin2hex($okm), "\n";3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865
$key = str_repeat('k', 32);
echo json_encode([
'sha256' => strlen(hash_hkdf('sha256', $key)),
'sha512' => strlen(hash_hkdf('sha512', $key)),
'ten' => strlen(hash_hkdf('sha256', $key, 10)),
]), "\n";{"sha256":32,"sha512":64,"ten":10}
$master = str_repeat("\x01", 32);
$salt = 'fixed-salt';
$enc1 = hash_hkdf('sha256', $master, 32, 'encryption', $salt);
$enc2 = hash_hkdf('sha256', $master, 32, 'encryption', $salt);
$mac = hash_hkdf('sha256', $master, 32, 'authentication', $salt);
$errors = [];
try {
hash_hkdf('sha256', '');
} catch (ValueError $e) {
$errors[] = 'empty key';
}
try {
hash_hkdf('sha256', $master, 255 * 32 + 1);
} catch (ValueError $e) {
$errors[] = 'too long';
}
try {
hash_hkdf('crc32', $master);
} catch (ValueError $e) {
$errors[] = 'crc32';
}
echo json_encode([
'repeatable' => $enc1 === $enc2,
'separate' => $enc1 !== $mac,
'errors' => $errors,
]), "\n";{"repeatable":true,"separate":true,"errors":["empty key","too long","crc32"]}