PageRenderTime 40ms CodeModel.GetById 11ms RepoModel.GetById 0ms app.codeStats 0ms

/src/Phpass/Hash/Adapter/Pbkdf2.php

http://github.com/rchouinard/phpass
PHP | 290 lines | 142 code | 32 blank | 116 comment | 33 complexity | c64d1ab3e1e56b236d8e26461b4c3e10 MD5 | raw file
  1. <?php
  2. /**
  3. * PHP Password Library
  4. *
  5. * @package PHPass\Hashes
  6. * @category Cryptography
  7. * @author Ryan Chouinard <rchouinard at gmail.com>
  8. * @license http://www.opensource.org/licenses/mit-license.html MIT License
  9. * @link https://github.com/rchouinard/phpass Project at GitHub
  10. */
  11. namespace Phpass\Hash\Adapter;
  12. use Phpass\Exception\InvalidArgumentException;
  13. use Phpass\Exception\RuntimeException;
  14. /**
  15. * PBKDF2 hash adapter
  16. *
  17. * @package PHPass\Hashes
  18. * @category Cryptography
  19. * @author Ryan Chouinard <rchouinard at gmail.com>
  20. * @license http://www.opensource.org/licenses/mit-license.html MIT License
  21. * @link https://github.com/rchouinard/phpass Project at GitHub
  22. */
  23. class Pbkdf2 extends Base
  24. {
  25. const DIGEST_SHA1 = 'sha1';
  26. const DIGEST_SHA256 = 'sha256';
  27. const DIGEST_SHA512 = 'sha512';
  28. /**
  29. * Hashing algorithm used by the PBKDF2 implementation.
  30. *
  31. * @var string
  32. */
  33. protected $_algo = self::DIGEST_SHA512;
  34. /**
  35. * Cost value used to generate new hash values.
  36. *
  37. * @var integer
  38. */
  39. protected $_iterationCount = 12000;
  40. /**
  41. * Return a hashed string.
  42. *
  43. * @param string $password
  44. * The string to be hashed.
  45. * @param string $salt
  46. * An optional salt string to base the hashing on. If not provided, a
  47. * suitable string is generated by the adapter.
  48. * @return string
  49. * Returns the hashed string. On failure, a standard crypt error string
  50. * is returned which is guaranteed to differ from the salt.
  51. * @throws RuntimeException
  52. * A RuntimeException is thrown on failure if
  53. * self::$_throwExceptionOnFailure is true.
  54. */
  55. public function crypt($password, $salt = null)
  56. {
  57. if (!$salt) {
  58. $salt = $this->genSalt();
  59. }
  60. $hash = '*0';
  61. if ($this->verify($salt)) {
  62. $matches = array ();
  63. preg_match('/^\$pbkdf2(?:-(?P<digest>sha256|sha512))?\$(?P<rounds>\d+)\$(?P<salt>[\.\/0-9A-Za-z]{0,1366})\$?/', $salt, $matches);
  64. if ($matches['digest'] == '') {
  65. $matches['digest'] = $matches[1] = self::DIGEST_SHA1;
  66. }
  67. $keySize = 64;
  68. if ($matches['digest'] == self::DIGEST_SHA256) {
  69. $keySize = 32;
  70. } elseif ($matches['digest'] == self::DIGEST_SHA1) {
  71. $keySize = 20;
  72. }
  73. $salt = '';
  74. if ($matches['salt'] != '') {
  75. $salt = str_replace('.', '+', $matches['salt']);
  76. switch (strlen($salt) & 0x03) {
  77. case 0:
  78. $salt = base64_decode($salt);
  79. break;
  80. case 2:
  81. $salt = base64_decode($salt . '==');
  82. break;
  83. case 3:
  84. $salt = base64_decode($salt . '=');
  85. break;
  86. default:
  87. return $hash;
  88. }
  89. }
  90. $checksum = $this->_pbkdf2($password, $salt, $matches['rounds'], $keySize, $matches['digest']);
  91. $hash = '$pbkdf2';
  92. if ($matches['digest'] != self::DIGEST_SHA1) {
  93. $hash .= '-' . $matches['digest'];
  94. }
  95. $hash .= '$' . $matches['rounds'] . '$' .
  96. str_replace(array ('+', '=', "\n"), array ('.', '', ''), base64_encode($salt)) . '$' .
  97. str_replace(array ('+', '=', "\n"), array ('.', '', ''), base64_encode($checksum));
  98. }
  99. if (!$this->verifyHash($hash)) {
  100. $hash = ($salt != '*0') ? '*0' : '*1';
  101. if ($this->_throwExceptionOnFailure) {
  102. throw new RuntimeException('Failed generating a valid hash', $hash);
  103. }
  104. }
  105. return $hash;
  106. }
  107. /**
  108. * Generate a salt string compatible with this adapter.
  109. *
  110. * @param string $input
  111. * Optional random 48-bit string to use when generating the salt.
  112. * @return string
  113. * Returns the generated salt string.
  114. */
  115. public function genSalt($input = null)
  116. {
  117. if (!$input) {
  118. $input = $this->_getRandomBytes(16);
  119. }
  120. $identifier = 'pbkdf2';
  121. if ($this->_algo === self::DIGEST_SHA256 || $this->_algo === self::DIGEST_SHA512) {
  122. $identifier .= '-' . $this->_algo;
  123. }
  124. $count = min(max($this->_iterationCount, 1), 4294967296);
  125. $salt = str_replace(array ('+', '=', "\n"), array ('.', '', ''), base64_encode($input));
  126. // $pbkdf2-<digest>$<rounds>$<salt>$
  127. return '$' . $identifier . '$' . $count . '$' . $salt . '$';
  128. }
  129. /**
  130. * Set adapter options.
  131. *
  132. * Expects an associative array of option keys and values used to configure
  133. * this adapter.
  134. *
  135. * <dl>
  136. * <dt>digest</dt>
  137. * <dd>Hash digest to use when calculating the checksum. Must be one
  138. * of sha1, sha256, or sha512. Defaults to sha512.</dd>
  139. * <dt>iterationCount</dt>
  140. * <dd>Iteration count for the underlying PBKDF2 hashing algorithm.
  141. * Must be in range 1 - 4294967296. Defaults to 12000.</dd>
  142. * </dl>
  143. *
  144. * @param Array $options
  145. * Associative array of adapter options.
  146. * @return self
  147. * Returns an instance of self to support method chaining.
  148. * @throws InvalidArgumentException
  149. * Throws an InvalidArgumentException if a provided option key contains
  150. * an invalid value.
  151. * @see Base::setOptions()
  152. */
  153. public function setOptions(Array $options)
  154. {
  155. parent::setOptions($options);
  156. $options = array_change_key_case($options, CASE_LOWER);
  157. foreach ($options as $key => $value) {
  158. switch ($key) {
  159. case 'digest':
  160. $value = strtolower($value);
  161. if (!in_array($value, array (self::DIGEST_SHA1, self::DIGEST_SHA256, self::DIGEST_SHA512))) {
  162. throw new InvalidArgumentException('Digest must be one of sha1, sha256, or sha512');
  163. }
  164. $this->_algo = $value;
  165. break;
  166. case 'iterationcount':
  167. if ($value < 1 || $value > 4294967296) {
  168. throw new InvalidArgumentException('Iteration count must be between 1 and 4294967296');
  169. }
  170. $this->_iterationCount = $value;
  171. break;
  172. default:
  173. break;
  174. }
  175. }
  176. return $this;
  177. }
  178. /**
  179. * Check if a hash string is valid for the current adapter.
  180. *
  181. * @since 2.1.0
  182. * @param string $input
  183. * Hash string to verify.
  184. * @return boolean
  185. * Returns true if the input string is a valid hash value, false
  186. * otherwise.
  187. */
  188. public function verifyHash($input)
  189. {
  190. 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));
  191. }
  192. /**
  193. * Check if a salt string is valid for the current adapter.
  194. *
  195. * @since 2.1.0
  196. * @param string $input
  197. * Salt string to verify.
  198. * @return boolean
  199. * Returns true if the input string is a valid salt value, false
  200. * otherwise.
  201. */
  202. public function verifySalt($input)
  203. {
  204. $valid = false;
  205. $matches = array ();
  206. if (1 === preg_match('/^\$pbkdf2(?:-(?P<digest>sha256|sha512))?\$(?P<rounds>\d+)\$(?P<salt>[\.\/0-9A-Za-z]{0,1366})\$?/', $input, $matches)) {
  207. $digest = $matches['digest'] ?: self::DIGEST_SHA1;
  208. $rounds = $matches['rounds'];
  209. $salt = $matches['salt'];
  210. $digestValid = false;
  211. if (in_array($digest, array (self::DIGEST_SHA1, self::DIGEST_SHA256, self::DIGEST_SHA512))) {
  212. $digestValid = true;
  213. }
  214. $roundsValid = false;
  215. if ($rounds[0] != '0' && $rounds >= 1 && $rounds <= 4294967296) {
  216. $roundsValid = true;
  217. }
  218. if ($digestValid && $roundsValid) {
  219. $valid = true;
  220. }
  221. }
  222. return $valid;
  223. }
  224. /**
  225. * Internal implementation of PKCS #5 v2.0.
  226. *
  227. * This implementation passes tests using vectors given in RFC 6070 s.2,
  228. * PBKDF2 HMAC-SHA1 Test Vectors. Vectors given for PBKDF2 HMAC-SHA2 at
  229. * http://stackoverflow.com/questions/5130513 also pass.
  230. *
  231. * @param string $password
  232. * The string to be hashed.
  233. * @param string $salt
  234. * Salt value used by the HMAC function.
  235. * @param integer $iterationCount
  236. * Number of iterations for key stretching.
  237. * @param integer $keyLength
  238. * Length of derived key.
  239. * @param string $algo
  240. * Algorithm to use when generating HMAC digest.
  241. * @return string
  242. * Returns the raw hash string.
  243. */
  244. protected function _pbkdf2($password, $salt, $iterationCount = 1000, $keyLength = 20, $algo = 'sha1')
  245. {
  246. $hashLength = strlen(hash($algo, null, true));
  247. $keyBlocks = ceil($keyLength / $hashLength);
  248. $derivedKey = '';
  249. for ($block = 1; $block <= $keyBlocks; ++$block) {
  250. $iteratedBlock = $currentBlock = hash_hmac($algo, $salt . pack('N', $block), $password, true);
  251. for ($iteration = 1; $iteration < $iterationCount; ++$iteration) {
  252. $iteratedBlock ^= $currentBlock = hash_hmac($algo, $currentBlock, $password, true);
  253. }
  254. $derivedKey .= $iteratedBlock;
  255. }
  256. return substr($derivedKey, 0, $keyLength);
  257. }
  258. }