Hashing7 min readExamples run on PHP 8.5.10

hash_pbkdf2()

Learn how hash_pbkdf2() derives a key from a password and salt, how its length and binary parameters interact, and which mistakes to avoid.

Signature

hash_pbkdf2(
    string $algo,
    #[\SensitiveParameter] string $password,
    string $salt,
    int $iterations,
    int $length = 0,
    bool $binary = false,
    array $options = []
): string
Available since PHP 5.5.07.48.08.18.28.38.48.5All versions →

Purpose

hash_pbkdf2() implements PBKDF2, a password-based key derivation function. It takes a password and a salt, then repeatedly applies an HMAC built on a hash algorithm of your choice. The result is a derived key. The iteration count makes each guess slower for an attacker, and the salt makes precomputed tables much less useful.

Typical uses are deriving an encryption key from a passphrase, or interoperating with a system that specifies PBKDF2. The manual notes that PBKDF2 can be used for password storage, but password_hash() or crypt() with Blowfish are better suited to that job.

Signature

Parameters and defaults

  • $algo: the name of the hash algorithm, such as sha256. Valid names are listed by hash_hmac_algos(). Non-cryptographic algorithms are not allowed.
  • $password: the secret input. The SensitiveParameter attribute keeps its value out of stack traces.
  • $salt: the salt. It should be generated randomly for real use, and it must be stored so that you can derive the same key again.
  • $iterations: how many internal iterations to run. It must be greater than 0. Larger values cost more time.
  • $length: the output length. The default 0 means the full output of the algorithm. The unit depends on $binary, as described below.
  • $binary: false (the default) returns lowercase hexadecimal. true returns raw bytes.
  • $options: an options array. Only the seed key is supported, and only by the MurmurHash variants, so it is irrelevant for PBKDF2 with cryptographic algorithms. Leave it empty.

Return value and failure cases

The function returns a string: the derived key as lowercase hexits, or raw bytes if $binary is true.

Since PHP 8.0.0 it throws a ValueError on failure. Before that it returned false and emitted a warning. The error is thrown when:

  • the algorithm is unknown,
  • $iterations is 0 or less,
  • $length is below 0,
  • the salt is too long (greater than INT_MAX - 4).

PHP 7.2.0 disabled non-cryptographic algorithms such as crc32 and adler32, so they are rejected as well.

Example 1: known test vectors

RFC 6070 publishes PBKDF2-HMAC-SHA1 test vectors, so this output can be checked against a standard. The $length of 40 hex characters equals 20 bytes, which is the full SHA-1 size.

Example
<?php
$results = [];
foreach ([1, 2, 4096] as $iterations) {
    $results[] = hash_pbkdf2('sha1', 'password', 'salt', $iterations, 40);
}
echo json_encode($results), "\n";
Output
["0c60c80f961f0e71f3a9b524af6012062fe037a6","ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957","4b007901b765489abead49d926f721d065a429c1"]

More iterations give a completely different key, even with the same password and salt.

Example 2: hex length versus binary length

The meaning of $length changes with $binary. In hex mode it counts output characters. In binary mode it counts bytes. To get the same key material, halve the length for binary output.

Example
<?php
$hex = hash_pbkdf2('sha256', 'password', 'salt', 1, 20);
$raw = hash_pbkdf2('sha256', 'password', 'salt', 1, 10, true);

echo json_encode([
    'hex' => $hex,
    'binaryHex' => bin2hex($raw),
    'bytes' => strlen($raw),
    'same' => $hex === bin2hex($raw),
]), "\n";
Output
{"hex":"120fb6cffcf8b32c43e7","binaryHex":"120fb6cffcf8b32c43e7","bytes":10,"same":true}

Example 3: handling invalid arguments

Because invalid input throws, wrap calls in try/catch when the algorithm or iteration count comes from configuration.

Example
<?php
function tryDerive(string $algo, int $iterations): string
{
    try {
        return hash_pbkdf2($algo, 'pw', 'salt', $iterations, 16);
    } catch (ValueError $e) {
        return 'ValueError';
    }
}

echo json_encode([
    tryDerive('sha256', 0),
    tryDerive('crc32', 1000),
    tryDerive('nope', 1),
]), "\n";
Output
["ValueError","ValueError","ValueError"]

Zero iterations, a non-cryptographic algorithm and an unknown algorithm all fail the same way.

Edge cases

  • A $length of 0 returns the algorithm's full output: 64 hex characters for SHA-256, 40 for SHA-1.
  • A $length larger than the hash output is allowed. PBKDF2 produces more blocks to fill the request.
  • The salt is an arbitrary byte string. It may be binary and may contain null bytes.
  • An empty password or salt is accepted, but it is poor practice.

Common mistakes

  1. Treating length as bytes in hex mode. $length = 16 in hex mode gives only 8 bytes of key material.
  2. Using a fixed or empty salt in production. Examples here use salt for repeatable output. Real code should generate a random salt, for instance with random_bytes(), and store it.
  3. Too few iterations. A low count makes brute force cheap. Choose a count that is as high as your performance budget allows, and review it over time.
  4. Comparing hashes with == or ===. Use hash_equals() for timing-safe comparison when verifying derived values.
  5. Choosing PBKDF2 for ordinary password storage. Prefer password_hash() and password_verify().
  6. Passing a non-cryptographic algorithm. Names like crc32 are rejected.
  • password_hash(): the recommended way to store passwords.
  • hash_hkdf(): key derivation from input that is already high-entropy.
  • sodium_crypto_pwhash(): Argon2-based derivation.
  • hash_hmac_algos(): lists algorithm names usable here.
  • hash_equals(): timing-safe string comparison.

Recap

hash_pbkdf2() stretches a password and salt into a derived key using repeated HMAC. Remember that $length counts hex characters unless $binary is true, that the salt must be random and stored, and that bad arguments throw ValueError. For storing login passwords, use password_hash() instead.

Tested examples

RFC 6070 PBKDF2-HMAC-SHA1 test vectors
<?php
$results = [];
foreach ([1, 2, 4096] as $iterations) {
    $results[] = hash_pbkdf2('sha1', 'password', 'salt', $iterations, 40);
}
echo json_encode($results), "\n";
Output
["0c60c80f961f0e71f3a9b524af6012062fe037a6","ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957","4b007901b765489abead49d926f721d065a429c1"]
Hex length versus binary length
<?php
$hex = hash_pbkdf2('sha256', 'password', 'salt', 1, 20);
$raw = hash_pbkdf2('sha256', 'password', 'salt', 1, 10, true);

echo json_encode([
    'hex' => $hex,
    'binaryHex' => bin2hex($raw),
    'bytes' => strlen($raw),
    'same' => $hex === bin2hex($raw),
]), "\n";
Output
{"hex":"120fb6cffcf8b32c43e7","binaryHex":"120fb6cffcf8b32c43e7","bytes":10,"same":true}
Handling invalid arguments with ValueError
<?php
function tryDerive(string $algo, int $iterations): string
{
    try {
        return hash_pbkdf2($algo, 'pw', 'salt', $iterations, 16);
    } catch (ValueError $e) {
        return 'ValueError';
    }
}

echo json_encode([
    tryDerive('sha256', 0),
    tryDerive('crc32', 1000),
    tryDerive('nope', 1),
]), "\n";
Output
["ValueError","ValueError","ValueError"]