Hashing7 min readExamples run on PHP 8.5.10

hash_hkdf()

Learn how PHP 8.4 hash_hkdf() derives raw binary keys from input keying material, with parameters, errors, examples and common mistakes.

Signature

hash_hkdf(
    string $algo,
    #[\SensitiveParameter] string $key,
    int $length = 0,
    string $info = "",
    string $salt = ""
): string
Available since PHP 7.1.27.48.08.18.28.38.48.5All versions →

Purpose

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 default 0 means 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.

Example
<?php
$ikm = str_repeat("\x0b", 22);
$salt = hex2bin('000102030405060708090a0b0c');
$info = hex2bin('f0f1f2f3f4f5f6f7f8f9');
$okm = hash_hkdf('sha256', $ikm, 42, $info, $salt);
echo bin2hex($okm), "\n";
Output
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.

Example
<?php
$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";
Output
{"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.

Example
<?php
$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";
Output
{"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 $info value 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 ==. Use hash_equals() when comparing secrets.

Common mistakes

  1. Feeding it a password. HKDF does not slow down guessing. It assumes the input is already high-entropy.
  2. Reusing one derived key for several purposes. Derive a separate key per purpose by varying $info.
  3. Treating the result as text. It is raw binary, so use bin2hex() or base64_encode() before storing or showing it.
  4. Expecting false on error. Modern PHP throws ValueError, so wrap risky input in try/catch where the values come from outside.
  5. Generating a new random salt and forgetting to store it. If the derivation must be reproduced later, keep the salt with the data.
  6. Choosing a non-cryptographic algorithm. Names such as crc32 are rejected. Use hash_hmac_algos() to see what is accepted.
  • 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

RFC 5869 test vector with SHA-256
<?php
$ikm = str_repeat("\x0b", 22);
$salt = hex2bin('000102030405060708090a0b0c');
$info = hex2bin('f0f1f2f3f4f5f6f7f8f9');
$okm = hash_hkdf('sha256', $ikm, 42, $info, $salt);
echo bin2hex($okm), "\n";
Output
3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865
Default and explicit output lengths
<?php
$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";
Output
{"sha256":32,"sha512":64,"ten":10}
Key separation with info and ValueError cases
<?php
$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";
Output
{"repeatable":true,"separate":true,"errors":["empty key","too long","crc32"]}