password.php 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. <?php
  2. /**
  3. * A Compatibility library with PHP 5.5's simplified password hashing API.
  4. *
  5. * @author Anthony Ferrara <ircmaxell@php.net>
  6. * @license http://www.opensource.org/licenses/mit-license.html MIT License
  7. * @copyright 2012 The Authors
  8. */
  9. if (!defined('PASSWORD_BCRYPT')) {
  10. define('PASSWORD_BCRYPT', 1);
  11. define('PASSWORD_DEFAULT', PASSWORD_BCRYPT);
  12. /**
  13. * Hash the password using the specified algorithm
  14. *
  15. * @param string $password The password to hash
  16. * @param int $algo The algorithm to use (Defined by PASSWORD_* constants)
  17. * @param array $options The options for the algorithm to use
  18. *
  19. * @return string|false The hashed password, or false on error.
  20. */
  21. function password_hash($password, $algo, array $options = array()) {
  22. if (!function_exists('crypt')) {
  23. trigger_error("Crypt must be loaded for password_hash to function", E_USER_WARNING);
  24. return null;
  25. }
  26. if (!is_string($password)) {
  27. trigger_error("password_hash(): Password must be a string", E_USER_WARNING);
  28. return null;
  29. }
  30. if (!is_int($algo)) {
  31. trigger_error("password_hash() expects parameter 2 to be long, " . gettype($algo) . " given", E_USER_WARNING);
  32. return null;
  33. }
  34. switch ($algo) {
  35. case PASSWORD_BCRYPT:
  36. // Note that this is a C constant, but not exposed to PHP, so we don't define it here.
  37. $cost = 10;
  38. if (isset($options['cost'])) {
  39. $cost = $options['cost'];
  40. if ($cost < 4 || $cost > 31) {
  41. trigger_error(sprintf("password_hash(): Invalid bcrypt cost parameter specified: %d", $cost), E_USER_WARNING);
  42. return null;
  43. }
  44. }
  45. // The length of salt to generate
  46. $raw_salt_len = 16;
  47. // The length required in the final serialization
  48. $required_salt_len = 22;
  49. $hash_format = sprintf("$2y$%02d$", $cost);
  50. break;
  51. default:
  52. trigger_error(sprintf("password_hash(): Unknown password hashing algorithm: %s", $algo), E_USER_WARNING);
  53. return null;
  54. }
  55. if (isset($options['salt'])) {
  56. switch (gettype($options['salt'])) {
  57. case 'NULL':
  58. case 'boolean':
  59. case 'integer':
  60. case 'double':
  61. case 'string':
  62. $salt = (string) $options['salt'];
  63. break;
  64. case 'object':
  65. if (method_exists($options['salt'], '__tostring')) {
  66. $salt = (string) $options['salt'];
  67. break;
  68. }
  69. case 'array':
  70. case 'resource':
  71. default:
  72. trigger_error('password_hash(): Non-string salt parameter supplied', E_USER_WARNING);
  73. return null;
  74. }
  75. if (strlen($salt) < $required_salt_len) {
  76. trigger_error(sprintf("password_hash(): Provided salt is too short: %d expecting %d", strlen($salt), $required_salt_len), E_USER_WARNING);
  77. return null;
  78. } elseif (0 == preg_match('#^[a-zA-Z0-9./]+$#D', $salt)) {
  79. $salt = str_replace('+', '.', base64_encode($salt));
  80. }
  81. } else {
  82. $buffer = '';
  83. $buffer_valid = false;
  84. if (function_exists('mcrypt_create_iv') && !defined('PHALANGER')) {
  85. $buffer = mcrypt_create_iv($raw_salt_len, MCRYPT_DEV_URANDOM);
  86. if ($buffer) {
  87. $buffer_valid = true;
  88. }
  89. }
  90. if (!$buffer_valid && function_exists('openssl_random_pseudo_bytes')) {
  91. $buffer = openssl_random_pseudo_bytes($raw_salt_len);
  92. if ($buffer) {
  93. $buffer_valid = true;
  94. }
  95. }
  96. if (!$buffer_valid && is_readable('/dev/urandom')) {
  97. $f = fopen('/dev/urandom', 'r');
  98. $read = strlen($buffer);
  99. while ($read < $raw_salt_len) {
  100. $buffer .= fread($f, $raw_salt_len - $read);
  101. $read = strlen($buffer);
  102. }
  103. fclose($f);
  104. if ($read >= $raw_salt_len) {
  105. $buffer_valid = true;
  106. }
  107. }
  108. if (!$buffer_valid || strlen($buffer) < $raw_salt_len) {
  109. $bl = strlen($buffer);
  110. for ($i = 0; $i < $raw_salt_len; $i++) {
  111. if ($i < $bl) {
  112. $buffer[$i] = $buffer[$i] ^ chr(mt_rand(0, 255));
  113. } else {
  114. $buffer .= chr(mt_rand(0, 255));
  115. }
  116. }
  117. }
  118. $salt = str_replace('+', '.', base64_encode($buffer));
  119. }
  120. $salt = substr($salt, 0, $required_salt_len);
  121. $hash = $hash_format . $salt;
  122. $ret = crypt($password, $hash);
  123. if (!is_string($ret) || strlen($ret) <= 13) {
  124. return false;
  125. }
  126. return $ret;
  127. }
  128. /**
  129. * Get information about the password hash. Returns an array of the information
  130. * that was used to generate the password hash.
  131. *
  132. * array(
  133. * 'algo' => 1,
  134. * 'algoName' => 'bcrypt',
  135. * 'options' => array(
  136. * 'cost' => 10,
  137. * ),
  138. * )
  139. *
  140. * @param string $hash The password hash to extract info from
  141. *
  142. * @return array The array of information about the hash.
  143. */
  144. function password_get_info($hash) {
  145. $return = array(
  146. 'algo' => 0,
  147. 'algoName' => 'unknown',
  148. 'options' => array(),
  149. );
  150. if (substr($hash, 0, 4) == '$2y$' && strlen($hash) == 60) {
  151. $return['algo'] = PASSWORD_BCRYPT;
  152. $return['algoName'] = 'bcrypt';
  153. list($cost) = sscanf($hash, "$2y$%d$");
  154. $return['options']['cost'] = $cost;
  155. }
  156. return $return;
  157. }
  158. /**
  159. * Determine if the password hash needs to be rehashed according to the options provided
  160. *
  161. * If the answer is true, after validating the password using password_verify, rehash it.
  162. *
  163. * @param string $hash The hash to test
  164. * @param int $algo The algorithm used for new password hashes
  165. * @param array $options The options array passed to password_hash
  166. *
  167. * @return boolean True if the password needs to be rehashed.
  168. */
  169. function password_needs_rehash($hash, $algo, array $options = array()) {
  170. $info = password_get_info($hash);
  171. if ($info['algo'] != $algo) {
  172. return true;
  173. }
  174. switch ($algo) {
  175. case PASSWORD_BCRYPT:
  176. $cost = isset($options['cost']) ? $options['cost'] : 10;
  177. if ($cost != $info['options']['cost']) {
  178. return true;
  179. }
  180. break;
  181. }
  182. return false;
  183. }
  184. /**
  185. * Verify a password against a hash using a timing attack resistant approach
  186. *
  187. * @param string $password The password to verify
  188. * @param string $hash The hash to verify against
  189. *
  190. * @return boolean If the password matches the hash
  191. */
  192. function password_verify($password, $hash) {
  193. if (!function_exists('crypt')) {
  194. trigger_error("Crypt must be loaded for password_verify to function", E_USER_WARNING);
  195. return false;
  196. }
  197. $ret = crypt($password, $hash);
  198. if (!is_string($ret) || strlen($ret) != strlen($hash) || strlen($ret) <= 13) {
  199. return false;
  200. }
  201. $status = 0;
  202. for ($i = 0; $i < strlen($ret); $i++) {
  203. $status |= (ord($ret[$i]) ^ ord($hash[$i]));
  204. }
  205. return $status === 0;
  206. }
  207. }