/src/Phpass/Hash/Adapter/Pbkdf2.php
PHP | 290 lines | 142 code | 32 blank | 116 comment | 33 complexity | c64d1ab3e1e56b236d8e26461b4c3e10 MD5 | raw file
- <?php
- /**
- * PHP Password Library
- *
- * @package PHPass\Hashes
- * @category Cryptography
- * @author Ryan Chouinard <rchouinard at gmail.com>
- * @license http://www.opensource.org/licenses/mit-license.html MIT License
- * @link https://github.com/rchouinard/phpass Project at GitHub
- */
- namespace Phpass\Hash\Adapter;
- use Phpass\Exception\InvalidArgumentException;
- use Phpass\Exception\RuntimeException;
- /**
- * PBKDF2 hash adapter
- *
- * @package PHPass\Hashes
- * @category Cryptography
- * @author Ryan Chouinard <rchouinard at gmail.com>
- * @license http://www.opensource.org/licenses/mit-license.html MIT License
- * @link https://github.com/rchouinard/phpass Project at GitHub
- */
- class Pbkdf2 extends Base
- {
- const DIGEST_SHA1 = 'sha1';
- const DIGEST_SHA256 = 'sha256';
- const DIGEST_SHA512 = 'sha512';
- /**
- * Hashing algorithm used by the PBKDF2 implementation.
- *
- * @var string
- */
- protected $_algo = self::DIGEST_SHA512;
- /**
- * Cost value used to generate new hash values.
- *
- * @var integer
- */
- protected $_iterationCount = 12000;
- /**
- * Return a hashed string.
- *
- * @param string $password
- * The string to be hashed.
- * @param string $salt
- * An optional salt string to base the hashing on. If not provided, a
- * suitable string is generated by the adapter.
- * @return string
- * Returns the hashed string. On failure, a standard crypt error string
- * is returned which is guaranteed to differ from the salt.
- * @throws RuntimeException
- * A RuntimeException is thrown on failure if
- * self::$_throwExceptionOnFailure is true.
- */
- public function crypt($password, $salt = null)
- {
- if (!$salt) {
- $salt = $this->genSalt();
- }
- $hash = '*0';
- if ($this->verify($salt)) {
- $matches = array ();
- preg_match('/^\$pbkdf2(?:-(?P<digest>sha256|sha512))?\$(?P<rounds>\d+)\$(?P<salt>[\.\/0-9A-Za-z]{0,1366})\$?/', $salt, $matches);
- if ($matches['digest'] == '') {
- $matches['digest'] = $matches[1] = self::DIGEST_SHA1;
- }
- $keySize = 64;
- if ($matches['digest'] == self::DIGEST_SHA256) {
- $keySize = 32;
- } elseif ($matches['digest'] == self::DIGEST_SHA1) {
- $keySize = 20;
- }
- $salt = '';
- if ($matches['salt'] != '') {
- $salt = str_replace('.', '+', $matches['salt']);
- switch (strlen($salt) & 0x03) {
- case 0:
- $salt = base64_decode($salt);
- break;
- case 2:
- $salt = base64_decode($salt . '==');
- break;
- case 3:
- $salt = base64_decode($salt . '=');
- break;
- default:
- return $hash;
- }
- }
- $checksum = $this->_pbkdf2($password, $salt, $matches['rounds'], $keySize, $matches['digest']);
- $hash = '$pbkdf2';
- if ($matches['digest'] != self::DIGEST_SHA1) {
- $hash .= '-' . $matches['digest'];
- }
- $hash .= '$' . $matches['rounds'] . '$' .
- str_replace(array ('+', '=', "\n"), array ('.', '', ''), base64_encode($salt)) . '$' .
- str_replace(array ('+', '=', "\n"), array ('.', '', ''), base64_encode($checksum));
- }
- if (!$this->verifyHash($hash)) {
- $hash = ($salt != '*0') ? '*0' : '*1';
- if ($this->_throwExceptionOnFailure) {
- throw new RuntimeException('Failed generating a valid hash', $hash);
- }
- }
- return $hash;
- }
- /**
- * Generate a salt string compatible with this adapter.
- *
- * @param string $input
- * Optional random 48-bit string to use when generating the salt.
- * @return string
- * Returns the generated salt string.
- */
- public function genSalt($input = null)
- {
- if (!$input) {
- $input = $this->_getRandomBytes(16);
- }
- $identifier = 'pbkdf2';
- if ($this->_algo === self::DIGEST_SHA256 || $this->_algo === self::DIGEST_SHA512) {
- $identifier .= '-' . $this->_algo;
- }
- $count = min(max($this->_iterationCount, 1), 4294967296);
- $salt = str_replace(array ('+', '=', "\n"), array ('.', '', ''), base64_encode($input));
- // $pbkdf2-<digest>$<rounds>$<salt>$
- return '$' . $identifier . '$' . $count . '$' . $salt . '$';
- }
- /**
- * Set adapter options.
- *
- * Expects an associative array of option keys and values used to configure
- * this adapter.
- *
- * <dl>
- * <dt>digest</dt>
- * <dd>Hash digest to use when calculating the checksum. Must be one
- * of sha1, sha256, or sha512. Defaults to sha512.</dd>
- * <dt>iterationCount</dt>
- * <dd>Iteration count for the underlying PBKDF2 hashing algorithm.
- * Must be in range 1 - 4294967296. Defaults to 12000.</dd>
- * </dl>
- *
- * @param Array $options
- * Associative array of adapter options.
- * @return self
- * Returns an instance of self to support method chaining.
- * @throws InvalidArgumentException
- * Throws an InvalidArgumentException if a provided option key contains
- * an invalid value.
- * @see Base::setOptions()
- */
- public function setOptions(Array $options)
- {
- parent::setOptions($options);
- $options = array_change_key_case($options, CASE_LOWER);
- foreach ($options as $key => $value) {
- switch ($key) {
- case 'digest':
- $value = strtolower($value);
- if (!in_array($value, array (self::DIGEST_SHA1, self::DIGEST_SHA256, self::DIGEST_SHA512))) {
- throw new InvalidArgumentException('Digest must be one of sha1, sha256, or sha512');
- }
- $this->_algo = $value;
- break;
- case 'iterationcount':
- if ($value < 1 || $value > 4294967296) {
- throw new InvalidArgumentException('Iteration count must be between 1 and 4294967296');
- }
- $this->_iterationCount = $value;
- break;
- default:
- break;
- }
- }
- return $this;
- }
- /**
- * Check if a hash string is valid for the current adapter.
- *
- * @since 2.1.0
- * @param string $input
- * Hash string to verify.
- * @return boolean
- * Returns true if the input string is a valid hash value, false
- * otherwise.
- */
- public function verifyHash($input)
- {
- return ($this->verifySalt($input) && 1 === preg_match('/^\$pbkdf2(?:-(?P<digest>sha256|sha512))?\$(?P<rounds>\d+)\$(?P<salt>[\.\/0-9A-Za-z]{0,1366})\$(?P<checksum>[\.\/0-9A-Za-z]{27,86})$/', $input));
- }
- /**
- * Check if a salt string is valid for the current adapter.
- *
- * @since 2.1.0
- * @param string $input
- * Salt string to verify.
- * @return boolean
- * Returns true if the input string is a valid salt value, false
- * otherwise.
- */
- public function verifySalt($input)
- {
- $valid = false;
- $matches = array ();
- if (1 === preg_match('/^\$pbkdf2(?:-(?P<digest>sha256|sha512))?\$(?P<rounds>\d+)\$(?P<salt>[\.\/0-9A-Za-z]{0,1366})\$?/', $input, $matches)) {
- $digest = $matches['digest'] ?: self::DIGEST_SHA1;
- $rounds = $matches['rounds'];
- $salt = $matches['salt'];
- $digestValid = false;
- if (in_array($digest, array (self::DIGEST_SHA1, self::DIGEST_SHA256, self::DIGEST_SHA512))) {
- $digestValid = true;
- }
- $roundsValid = false;
- if ($rounds[0] != '0' && $rounds >= 1 && $rounds <= 4294967296) {
- $roundsValid = true;
- }
- if ($digestValid && $roundsValid) {
- $valid = true;
- }
- }
- return $valid;
- }
- /**
- * Internal implementation of PKCS #5 v2.0.
- *
- * This implementation passes tests using vectors given in RFC 6070 s.2,
- * PBKDF2 HMAC-SHA1 Test Vectors. Vectors given for PBKDF2 HMAC-SHA2 at
- * http://stackoverflow.com/questions/5130513 also pass.
- *
- * @param string $password
- * The string to be hashed.
- * @param string $salt
- * Salt value used by the HMAC function.
- * @param integer $iterationCount
- * Number of iterations for key stretching.
- * @param integer $keyLength
- * Length of derived key.
- * @param string $algo
- * Algorithm to use when generating HMAC digest.
- * @return string
- * Returns the raw hash string.
- */
- protected function _pbkdf2($password, $salt, $iterationCount = 1000, $keyLength = 20, $algo = 'sha1')
- {
- $hashLength = strlen(hash($algo, null, true));
- $keyBlocks = ceil($keyLength / $hashLength);
- $derivedKey = '';
- for ($block = 1; $block <= $keyBlocks; ++$block) {
- $iteratedBlock = $currentBlock = hash_hmac($algo, $salt . pack('N', $block), $password, true);
- for ($iteration = 1; $iteration < $iterationCount; ++$iteration) {
- $iteratedBlock ^= $currentBlock = hash_hmac($algo, $currentBlock, $password, true);
- }
- $derivedKey .= $iteratedBlock;
- }
- return substr($derivedKey, 0, $keyLength);
- }
- }