string in bytes. * * @param string $str * * @throws Ex\EnvironmentIsBrokenException * * @return int */ public static function ourStrlen($str) { static $exists = null; if ($exists === null) { $exists = \function_exists('mb_strlen'); } if ($exists) { $length = \mb_strlen($str, '8bit'); if ($length === false) { throw new Ex\EnvironmentIsBrokenException(); } return $length; } else { return \strlen($str); } } /** * Behaves roughly like the function substr() in PHP 7 does. * * @param string $str * @param int $start * @param int $length * * @throws Ex\EnvironmentIsBrokenException * * @return string */ public static function ourSubstr($str, $start, $length = null) { static $exists = null; if ($exists === null) { $exists = \function_exists('mb_substr'); } 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 = Core::ourStrlen($str) - $start; } else { $length = -$start; } } // This is required to make mb_substr behavior identical to substr. // Without this, mb_substr() would return false, contra to what the // PHP documentation says (it doesn't say it can return false.) if ($start === Core::ourStrlen($str) && $length === 0) { return ''; } if ($start > Core::ourStrlen($str)) { return false; } $substr = \mb_substr($str, $start, $length, '8bit'); if (Core::ourStrlen($substr) !== $length) { throw new Ex\EnvironmentIsBrokenException( 'Your version of PHP has bug #66797. Its implementation of mb_substr() is incorrect. See the details here: https://bugs.php.net/bug.php?id=66797' ); } return $substr; } // Unlike mb_substr(), substr() doesn't accept NULL for length if (isset($length)) { return \substr($str, $start, $length); } else { return \substr($str, $start); } } /** * Computes the PBKDF2 password-based key derivation function. * * The PBKDF2 function is defined in RFC 2898. Test vectors can be found in * RFC 6070. This implementation of PBKDF2 was originally created by Taylor * Hornby, with improvements from 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 Ex\EnvironmentIsBrokenException * * @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 Ex\EnvironmentIsBrokenException( 'Invalid or unsupported hash algorithm.' ); } // Whitelist, or we could end up with people using CRC32. $ok_algorithms = [ 'sha1', 'sha224', 'sha256', 'sha384', 'sha512', 'ripemd160', 'ripemd256', 'ripemd320', 'whirlpool', ]; if (! \in_array($algorithm, $ok_algorithms, true)) { throw new Ex\EnvironmentIsBrokenException( 'Algorithm is not a secure cryptographic hash function.' ); } if ($count <= 0 || $key_length <= 0) { throw new Ex\EnvironmentIsBrokenException( '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; } return \hash_pbkdf2($algorithm, $password, $salt, $count, $key_length, $raw_output); } $hash_length = Core::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 Core::ourSubstr($output, 0, $key_length); } else { return Encoding::binToHex(Core::ourSubstr($output, 0, $key_length)); } } }