The following code is a secure implementation of PBKDF2 hashing in PHP.
<?php
class InvalidHashException extends Exception
{
}
class CannotPerformOperationException extends Exception
{
}
class PasswordStorage
{
//
// These constants may be changed without breaking existing hashes.
const PBKDF2_HASH_ALGORITHM = 'sha1';
const PBKDF2_ITERATIONS = 64000;
const PBKDF2_SALT_BYTES = 24;
const PBKDF2_OUTPUT_BYTES = 18;
//
// These constants define the encoding and may not be changed.
const HASH_SECTIONS = 5;
const HASH_ALGORITHM_INDEX = 0;
const HASH_ITERATION_INDEX = 1;
const HASH_SIZE_INDEX = 2;
const HASH_SALT_INDEX = 3;
const HASH_PBKDF2_INDEX = 4;
/**
* Hash a password with PBKDF2.
*
* @param string $password
* @throws InvalidArgumentException Thrown if $password is not a
* string.
*
* @throws CannotPerformOperationException Throw if random number generator
* failed. Not safe to proceed.
*
* @return string The hashed password.
*/
public static function createHash($password)
{
// format: algorithm:iterations:outputSize:salt:pbkdf2output
if (!\is_string($password)) {
throw new InvalidArgumentException(
'createHash(): Expected a string'
);
}
if (\function_exists('random_bytes')) {
try {
$salt_raw = \random_bytes(self::PBKDF2_SALT_BYTES);
} catch (Error $e) {
$salt_raw = false;
} catch (Exception $e) {
$salt_raw = false;
} catch (TypeError $e) {
$salt_raw = false;
}
} else {
$salt_raw = \mcrypt_create_iv(self::PBKDF2_SALT_BYTES, MCRYPT_DEV_URANDOM);
}
if ($salt_raw === false) {
throw new CannotPerformOperationException(
'Random number generator failed. Not safe to proceed.'
);
}
$PBKDF2_Output = self::pbkdf2(
self::PBKDF2_HASH_ALGORITHM,
$password,
$salt_raw,
self::PBKDF2_ITERATIONS,
self::PBKDF2_OUTPUT_BYTES,
true
);
return self::PBKDF2_HASH_ALGORITHM.
':'.
self::PBKDF2_ITERATIONS.
':'.
self::PBKDF2_OUTPUT_BYTES.
':'.
\base64_encode($salt_raw).
':'.
\base64_encode($PBKDF2_Output);
}
/**
* Verify that a password matches the stored hash.
*
* @param string $password
* @param string $hash
*
* @throws InvalidArgumentException
* @throws InvalidHashException
*
* @return bool
*/
public static function verifyPassword($password, $hash)
{
if (!\is_string($password) || !\is_string($hash)) {
throw new InvalidArgumentException(
'verifyPassword(): Expected two strings'
);
}
$params = \explode(':', $hash);
if (\count($params) !== self::HASH_SECTIONS) {
throw new InvalidHashException(
'Fields are missing from the password hash.'
);
}
$pbkdf2 = \base64_decode($params[self::HASH_PBKDF2_INDEX], true);
if ($pbkdf2 === false) {
throw new InvalidHashException(
'Base64 decoding of pbkdf2 output failed.'
);
}
$salt_raw = \base64_decode($params[self::HASH_SALT_INDEX], true);
if ($salt_raw === false) {
throw new InvalidHashException(
'Base64 decoding of salt failed.'
);
}
$storedOutputSize = (int) $params[self::HASH_SIZE_INDEX];
if (self::ourStrlen($pbkdf2) !== $storedOutputSize) {
throw new InvalidHashException(
"PBKDF2 output length doesn't match stored output length."
);
}
$iterations = (int) $params[self::HASH_ITERATION_INDEX];
if ($iterations < 1) {
throw new InvalidHashException(
'Invalid number of iterations. Must be >= 1.'
);
}
return self::slowEquals(
$pbkdf2,
self::pbkdf2(
$params[self::HASH_ALGORITHM_INDEX],
$password,
$salt_raw,
$iterations,
self::ourStrlen($pbkdf2),
true
)
);
}
/**
* Compares two strings in length-constant time.
*
* @param string $firstStr
* @param string $secondStr
* @throws InvalidArgumentException Thrown if arguments are not strings.
* @return bool Returns true if strings have the same length, otherwise
* false.
*/
private static function slowEquals($firstStr, $secondStr)
{
if (!\is_string($firstStr) || !\is_string($secondStr)) {
throw new InvalidArgumentException(
'slowEquals(): expected two strings'
);
}
if (\function_exists('hash_equals')) {
return \hash_equals($firstStr, $secondStr);
}
// PHP < 5.6 polyfill:
$diff = self::ourStrlen($firstStr) ^ self::ourStrlen($secondStr);
for ($i = 0; $i < self::ourStrlen($firstStr) &&
$i < self::ourStrlen($secondStr); ++$i) {
$diff |= \ord($firstStr[$i]) ^ \ord($secondStr[$i]);
}
return $diff === 0;
}
/**
* PBKDF2 key derivation function as defined by RSA's PKCS #5:
* https://www.ietf.org/rfc/rfc2898.txt.
*
* Test vectors can be found here: https://www.ietf.org/rfc/rfc6070.txt
*
* This implementation of PBKDF2 was originally created by https://defuse.ca
* With improvements by http://www.variations-of-shadow.com
*
* @param string $algorithm The hash algorithm to use. Recommended: SHA256.
* @param string $password The password.
* @param string $salt A salt that is unique to the password.
* @param int $count Iteration count. Higher is better, but slower.
* Recommended: At least 1000.
*
* @param int $key_length The length of the derived key in bytes.
* @param bool $raw_output If true, the key is returned in raw binary
* format. Hex encoded otherwise.
*
* @throws InvalidArgumentException Thrown when the $algorithm or
* $salt arguments are not a
* string.
*
* @throws CannotPerformOperationException Thrown when the $algorithm
* argument is invalid, or
* unsupported hash algorithm.
*
* @return string A $key_length-byte key derived from the password and salt.
*/
public static function pbkdf2($algorithm, $password, $salt, $count, $key_length, $raw_output = false)
{
// Type checks:
if (!\is_string($algorithm)) {
throw new InvalidArgumentException(
'pbkdf2(): algorithm must be a string'
);
}
if (!\is_string($password)) {
throw new InvalidArgumentException(
'pbkdf2(): password must be a string'
);
}
if (!\is_string($salt)) {
throw new InvalidArgumentException(
'pbkdf2(): salt must be a string'
);
}
// Coerce strings to integers with no information loss or overflow
$count += 0;
$key_length += 0;
$algorithm = \strtolower($algorithm);
if (!\in_array($algorithm, \hash_algos(), true)) {
throw new CannotPerformOperationException(
'Invalid or unsupported hash algorithm.'
);
}
// Whitelist, or we could end up with people using CRC32.
$ok_algorithms = array(
'sha1', 'sha224', 'sha256', 'sha384', 'sha512',
'ripemd160', 'ripemd256', 'ripemd320', 'whirlpool',
);
if (!\in_array($algorithm, $ok_algorithms, true)) {
throw new CannotPerformOperationException(
'Algorithm is not a secure cryptographic hash function.'
);
}
if ($count <= 0 || $key_length <= 0) {
throw new CannotPerformOperationException(
'Invalid PBKDF2 parameters.'
);
}
if (\function_exists('hash_pbkdf2')) {
// The output length is in NIBBLES (4-bits) if $raw_output is false!
if (!$raw_output) {
$key_length = $key_length * 2;
}
// Generate a PBKDF2 key derivation of a supplied password:
return \hash_pbkdf2($algorithm, $password, $salt, $count, $key_length, $raw_output);
}
$hash_length = self::ourStrlen(\hash($algorithm, '', true));
$block_count = \ceil($key_length / $hash_length);
$output = '';
for ($i = 1; $i <= $block_count; ++$i) {
// $i encoded as 4 bytes, big endian.
$last = $salt.\pack('N', $i);
// first iteration
$last = $xorsum = \hash_hmac($algorithm, $last, $password, true);
// perform the other $count - 1 iterations
for ($j = 1; $j < $count; ++$j) {
$xorsum ^= ($last = \hash_hmac($algorithm, $last, $password, true));
}
$output .= $xorsum;
}
if ($raw_output) {
return self::ourSubstr($output, 0, $key_length);
} else {
return \bin2hex(self::ourSubstr($output, 0, $key_length));
}
}
/*
* We need these strlen() and substr() functions because when
* 'mbstring.func_overload' is set in php.ini, the standard strlen() and
* substr() are replaced by mb_strlen() and mb_substr().
*/
/**
* Calculate the length of a string.
*
* @param string $str
* @throws InvalidArgumentException
* @throws CannotPerformOperationException
* @return int The length of the string.
*/
private static function ourStrlen($str)
{
static $exists = null;
if ($exists === null) {
$exists = \function_exists('mb_strlen');
}
if (!\is_string($str)) {
throw new InvalidArgumentException(
'ourStrlen() expects a string'
);
}
if ($exists) {
$length = \mb_strlen($str, '8bit');
if ($length === false) {
throw new CannotPerformOperationException();
}
return $length;
} else {
return \strlen($str);
}
}
/**
* Substring.
*
* @param string $str
* @param int $start
* @param int $length
* @throws InvalidArgumentException
* @return string
*/
private static function ourSubstr($str, $start, $length = null)
{
static $exists = null;
if ($exists === null) {
$exists = \function_exists('mb_substr');
}
// Type validation:
if (!\is_string($str)) {
throw new InvalidArgumentException(
'ourSubstr() expects a string'
);
}
if ($exists) {
// mb_substr($str, 0, NULL, '8bit') returns an empty string on PHP
// 5.3, so we have to find the length ourselves.
if (!isset($length)) {
if ($start >= 0) {
$length = self::ourStrlen($str) - $start;
} else {
$length = -$start;
}
}
return \mb_substr($str, $start, $length, '8bit');
}
// Unlike mb_substr(), substr() doesn't accept NULL for length
if (isset($length)) {
return \substr($str, $start, $length);
} else {
return \substr($str, $start);
}
}
}