PageRenderTime 39ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/framework/utils/CPasswordHelper.php

https://github.com/voltmedia/yii
PHP | 213 lines | 73 code | 17 blank | 123 comment | 21 complexity | 21dface41f334f123ca19c26413f22fe MD5 | raw file
Possible License(s): BSD-3-Clause, LGPL-2.1, BSD-2-Clause, GPL-2.0
  1. <?php
  2. /**
  3. * CPasswordHelper class file.
  4. *
  5. * @author Tom Worster <fsb@thefsb.org>
  6. * @link http://www.yiiframework.com/
  7. * @copyright 2008-2013 Yii Software LLC
  8. * @license http://www.yiiframework.com/license/
  9. */
  10. /**
  11. * CPasswordHelper provides a simple API for secure password hashing and verification.
  12. *
  13. * CPasswordHelper uses the Blowfish hash algorithm available in many PHP runtime
  14. * environments through the PHP {@link http://php.net/manual/en/function.crypt.php crypt()}
  15. * built-in function. As of Dec 2012 it is the strongest algorithm available in PHP
  16. * and the only algorithm without some security concerns surrounding it. For this reason,
  17. * CPasswordHelper fails to initialize when run in and environment that does not have
  18. * crypt() and its Blowfish option. Systems with the option include:
  19. * (1) Most *nix systems since PHP 4 (the algorithm is part of the library function crypt(3));
  20. * (2) All PHP systems since 5.3.0; (3) All PHP systems with the
  21. * {@link http://www.hardened-php.net/suhosin/ Suhosin patch}.
  22. * For more information about password hashing, crypt() and Blowfish, please read
  23. * the Yii Wiki article
  24. * {@link http://www.yiiframework.com/wiki/425/use-crypt-for-password-storage/ Use crypt() for password storage}.
  25. * and the
  26. * PHP RFC {@link http://wiki.php.net/rfc/password_hash Adding simple password hashing API}.
  27. *
  28. * CPasswordHelper throws an exception if the Blowfish hash algorithm is not
  29. * available in the runtime PHP's crypt() function. It can be used as follows
  30. *
  31. * Generate a hash from a password:
  32. * <pre>
  33. * $hash = CPasswordHelper::hashPassword($password);
  34. * </pre>
  35. * This hash can be stored in a database (e.g. CHAR(64) CHARACTER SET latin1). The
  36. * hash is usually generated and saved to the database when the user enters a new password.
  37. * But it can also be useful to generate and save a hash after validating a user's
  38. * password in order to change the cost or refresh the salt.
  39. *
  40. * To verify a password, fetch the user's saved hash from the database (into $hash) and:
  41. * <pre>
  42. * if (CPasswordHelper::verifyPassword($password, $hash)
  43. * // password is good
  44. * else
  45. * // password is bad
  46. * </pre>
  47. *
  48. * @author Tom Worster <fsb@thefsb.org>
  49. * @package system.utils
  50. * @since 1.1.14
  51. */
  52. class CPasswordHelper
  53. {
  54. /**
  55. * Check for availability of PHP crypt() with the Blowfish hash option.
  56. * @throws CException if the runtime system does not have PHP crypt() or its Blowfish hash option.
  57. */
  58. protected static function checkBlowfish()
  59. {
  60. if(!function_exists('crypt'))
  61. throw new CException(Yii::t('yii',
  62. '{class} requires the PHP crypt() function. This system does not have it.',
  63. array("{class}"=>__CLASS__)
  64. ));
  65. if (!defined('CRYPT_BLOWFISH') || !CRYPT_BLOWFISH)
  66. throw new CException(Yii::t('yii',
  67. '{class} requires the Blowfish option of the PHP crypt() function. This system does not have it.',
  68. array("{class}"=>__CLASS__)
  69. ));
  70. }
  71. /**
  72. * Generate a secure hash from a password and a random salt.
  73. *
  74. * Uses the
  75. * PHP {@link http://php.net/manual/en/function.crypt.php crypt()} built-in function
  76. * with the Blowfish hash option.
  77. *
  78. * @param string $password The password to be hashed.
  79. * @param int $cost Cost parameter used by the Blowfish hash algorithm.
  80. * The higher the value of cost,
  81. * the longer it takes to generate the hash and to verify a password against it. Higher cost
  82. * therefore slows down a brute-force attack. For best protection against brute for attacks,
  83. * set it to the highest value that is tolerable on production servers. The time taken to
  84. * compute the hash doubles for every increment by one of $cost. So, for example, if the
  85. * hash takes 1 second to compute when $cost is 14 then then the compute time varies as
  86. * 2^($cost - 14) seconds.
  87. * @throws CException on bad password parameter or if crypt() with Blowfish hash is not available.
  88. * @return string The password hash string, ASCII and not longer than 64 characters.
  89. */
  90. public static function hashPassword($password,$cost=13)
  91. {
  92. self::checkBlowfish();
  93. $salt=self::generateSalt($cost);
  94. $hash=crypt($password,$salt);
  95. if(!is_string($hash) || (function_exists('mb_strlen') ? mb_strlen($hash, '8bit') : strlen($hash))<32)
  96. throw new CException(Yii::t('yii','Internal error while generating hash.'));
  97. return $hash;
  98. }
  99. /**
  100. * Verify a password against a hash.
  101. *
  102. * @param string $password The password to verify.
  103. * @param string $hash The hash to verify the password against.
  104. *
  105. * @return bool True if the password matches the hash.
  106. * @throws CException on bad password or hash parameters or if crypt() with Blowfish hash is not available.
  107. */
  108. public static function verifyPassword($password, $hash)
  109. {
  110. self::checkBlowfish();
  111. if(!is_string($password) || $password === '')
  112. throw new CException(Yii::t('yii','Cannot hash a password that is empty or not a string.'));
  113. if (!$password || !preg_match('{^\$2[axy]\$(\d\d)\$[\./0-9A-Za-z]{22}}',$hash,$matches) || $matches[1] < 4 || $matches[1] > 30)
  114. return false;
  115. $test=crypt($password,$hash);
  116. if(!is_string($test) || strlen($test)<32)
  117. return false;
  118. return self::same($test, $hash);
  119. }
  120. /**
  121. * Check for sameness of two strings using an algorithm with timing
  122. * independent of the string values if the subject strings are of equal length.
  123. *
  124. * The function can be useful to prevent timing attacks. For example, if $a and $b
  125. * are both hash values from the same algirithm, then the timing of this function
  126. * does not reveal whether or not there is a match.
  127. *
  128. * NOTE: timing is affacted if $a and $b are different lengths or either is not a
  129. * string. For the purpose of checking password hash this does not reveal information
  130. * useful to an attacker.
  131. *
  132. * @see http://blog.astrumfutura.com/2010/10/nanosecond-scale-remote-timing-attacks-on-php-applications-time-to-take-them-seriously/
  133. * @see http://codereview.stackexchange.com/questions/13512
  134. * @see https://github.com/ircmaxell/password_compat/blob/master/lib/password.php
  135. *
  136. * @param $a string First subject string to compare.
  137. * @param $b string Second subject string to compare.
  138. *
  139. * @return bool true if the strings are the same, false if they are different or if
  140. * either is not a string.
  141. */
  142. public static function same($a,$b)
  143. {
  144. if(!is_string($a) || !is_string($b))
  145. return false;
  146. $mb=function_exists('mb_strlen');
  147. $length=$mb ? mb_strlen($a,'8bit') : strlen($a);
  148. if($length!==($mb ? mb_strlen($b,'8bit') : strlen($b)))
  149. return false;
  150. $check=0;
  151. for($i=0;$i<$length;$i+=1)
  152. $check|=(ord($a[$i])^ord($b[$i]));
  153. return $check===0;
  154. }
  155. /**
  156. * Generates a salt that can be used to generate a password hash.
  157. *
  158. * The PHP {@link http://php.net/manual/en/function.crypt.php crypt()} built-in function
  159. * requires, for the Blowfish hash algorithm, a salt string in a specific format:
  160. * "$2a$" (in which the "a" may be replaced by "x" or "y" see PHP manual for details),
  161. * a two digit cost parameter,
  162. * "$",
  163. * 22 characters from the alphabet "./0-9A-Za-z".
  164. *
  165. * @param $cost
  166. * @throws CException
  167. * @return string the random salt value.
  168. */
  169. protected static function generateSalt($cost = 13)
  170. {
  171. if(!is_numeric($cost))
  172. throw new CException(Yii::t('yii',
  173. '{class}::$cost must be a number.',
  174. array("{class}"=>__CLASS__)
  175. ));
  176. $cost=(int)$cost;
  177. if($cost<4 || $cost>30)
  178. throw new CException(Yii::t('yii',
  179. '{class}::$cost must be between 4 and 31.',
  180. array("{class}"=>__CLASS__)
  181. ));
  182. // Get 20 * 8bits of pseudo-random entropy from mt_rand().
  183. $rand='';
  184. for($i=0;$i<20;$i+=1)
  185. $rand.=chr(mt_rand(0,255));
  186. // Add the microtime for a little more entropy.
  187. $rand.=microtime();
  188. // Mix the bits cryptographically into a 20-byte binary string.
  189. $rand=sha1($rand,true);
  190. // Form the prefix that specifies Blowfish algo and cost parameter.
  191. $salt=sprintf("$2a$%02d$",$cost);
  192. // Append the random salt data in the required base64 format.
  193. $salt.=str_replace('+','.',substr(base64_encode($rand),0,22));
  194. return $salt;
  195. }
  196. }