PageRenderTime 53ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/jelix-legacy/auth/password.php

https://github.com/gmarrot/jelix
PHP | 267 lines | 181 code | 20 blank | 66 comment | 57 complexity | 45bae19a16461ef1e3b71a858cf18f8b MD5 | raw file
Possible License(s): GPL-2.0, LGPL-2.1, BSD-3-Clause
  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. * @contributor Laurent Jouanneau <laurent@jelix.org>
  7. * @license http://www.opensource.org/licenses/mit-license.html MIT License
  8. * @copyright 2012 The Authors
  9. */
  10. /**
  11. * function to check if the password API can be used
  12. * In some PHP version ( <5.3.7), crypt() with blowfish is vulnerable.
  13. * But this issue has been fixed on some older PHP version (php 5.3.3 for most of them) in some
  14. * distro, like Debian squeeze.
  15. * @see http://www.php.net/security/crypt_blowfish.php
  16. */
  17. function can_use_password_API () {
  18. if (version_compare(PHP_VERSION, '5.3.7', '>=')) {
  19. if (!defined('_PASSWORD_CRYPT_HASH_FORMAT'))
  20. define('_PASSWORD_CRYPT_HASH_FORMAT', '$2y$%02d$');
  21. if (!defined('_PASSWORD_CRYPT_PROLOG'))
  22. define('_PASSWORD_CRYPT_PROLOG', '$2y$');
  23. return true;
  24. }
  25. if (version_compare(PHP_VERSION, '5.3.3', '<')) {
  26. return false;
  27. }
  28. // On debian squeeze, crypt() has been fixed in PHP 5.3.3
  29. // http://security-tracker.debian.org/tracker/CVE-2011-2483
  30. // so we can use crypt() securely with $2a$ ($2y$ is not available)
  31. if (preg_match('/squeeze(\d+)$/', PHP_VERSION, $m)) {
  32. if (intval($m[1]) >= 4) {
  33. if (!defined('_PASSWORD_CRYPT_HASH_FORMAT'))
  34. define('_PASSWORD_CRYPT_HASH_FORMAT', '$2a$%02d$');
  35. if (!defined('_PASSWORD_CRYPT_PROLOG'))
  36. define('_PASSWORD_CRYPT_PROLOG', '$2a$');
  37. return true;
  38. }
  39. }
  40. //FIXME crypt() in PHP 5.3.3 is fixed also on other distro like RedHat.
  41. // however I don't know if it supports 2y, and how does PHP_VERSION look like
  42. return false;
  43. }
  44. if (!can_use_password_API()) {
  45. trigger_error("The Password Compatibility Library requires PHP >= 5.3.7 or PHP >= 5.3.3-7+squeeze4 on debian", E_USER_WARNING);
  46. // Prevent defining the functions
  47. return;
  48. }
  49. if (!defined('PASSWORD_BCRYPT')) {
  50. define('PASSWORD_BCRYPT', 1);
  51. define('PASSWORD_DEFAULT', PASSWORD_BCRYPT);
  52. /**
  53. * Hash the password using the specified algorithm
  54. *
  55. * @param string $password The password to hash
  56. * @param int $algo The algorithm to use (Defined by PASSWORD_* constants)
  57. * @param array $options The options for the algorithm to use
  58. *
  59. * @returns string|false The hashed password, or false on error.
  60. */
  61. function password_hash($password, $algo, array $options = array()) {
  62. if (!function_exists('crypt')) {
  63. trigger_error("Crypt must be loaded for password_hash to function", E_USER_WARNING);
  64. return null;
  65. }
  66. if (!is_string($password)) {
  67. trigger_error("password_hash(): Password must be a string", E_USER_WARNING);
  68. return null;
  69. }
  70. if (!is_int($algo)) {
  71. trigger_error("password_hash() expects parameter 2 to be long, " . gettype($algo) . " given", E_USER_WARNING);
  72. return null;
  73. }
  74. switch ($algo) {
  75. case PASSWORD_BCRYPT:
  76. // Note that this is a C constant, but not exposed to PHP, so we don't define it here.
  77. $cost = 10;
  78. if (isset($options['cost'])) {
  79. $cost = $options['cost'];
  80. if ($cost < 4 || $cost > 31) {
  81. trigger_error(sprintf("password_hash(): Invalid bcrypt cost parameter specified: %d", $cost), E_USER_WARNING);
  82. return null;
  83. }
  84. }
  85. $required_salt_len = 22;
  86. $hash_format = sprintf(_PASSWORD_CRYPT_HASH_FORMAT, $cost);
  87. break;
  88. default:
  89. trigger_error(sprintf("password_hash(): Unknown password hashing algorithm: %s", $algo), E_USER_WARNING);
  90. return null;
  91. }
  92. if (isset($options['salt'])) {
  93. switch (gettype($options['salt'])) {
  94. case 'NULL':
  95. case 'boolean':
  96. case 'integer':
  97. case 'double':
  98. case 'string':
  99. $salt = (string) $options['salt'];
  100. break;
  101. case 'object':
  102. if (method_exists($options['salt'], '__tostring')) {
  103. $salt = (string) $options['salt'];
  104. break;
  105. }
  106. case 'array':
  107. case 'resource':
  108. default:
  109. trigger_error('password_hash(): Non-string salt parameter supplied', E_USER_WARNING);
  110. return null;
  111. }
  112. if (strlen($salt) < $required_salt_len) {
  113. trigger_error(sprintf("password_hash(): Provided salt is too short: %d expecting %d", strlen($salt), $required_salt_len), E_USER_WARNING);
  114. return null;
  115. } elseif (0 == preg_match('#^[a-zA-Z0-9./]+$#D', $salt)) {
  116. $salt = str_replace('+', '.', base64_encode($salt));
  117. }
  118. } else {
  119. $buffer = '';
  120. $raw_length = (int) ($required_salt_len * 3 / 4 + 1);
  121. $buffer_valid = false;
  122. if (function_exists('mcrypt_create_iv')) {
  123. $buffer = mcrypt_create_iv($raw_length, MCRYPT_DEV_URANDOM);
  124. if ($buffer) {
  125. $buffer_valid = true;
  126. }
  127. }
  128. if (!$buffer_valid && function_exists('openssl_random_pseudo_bytes')) {
  129. $buffer = openssl_random_pseudo_bytes($raw_length);
  130. if ($buffer) {
  131. $buffer_valid = true;
  132. }
  133. }
  134. if (!$buffer_valid && file_exists('/dev/urandom')) {
  135. $f = @fopen('/dev/urandom', 'r');
  136. if ($f) {
  137. $read = strlen($buffer);
  138. while ($read < $raw_length) {
  139. $buffer .= fread($f, $raw_length - $read);
  140. $read = strlen($buffer);
  141. }
  142. fclose($f);
  143. if ($read >= $raw_length) {
  144. $buffer_valid = true;
  145. }
  146. }
  147. }
  148. if (!$buffer_valid || strlen($buffer) < $raw_length) {
  149. $bl = strlen($buffer);
  150. for ($i = 0; $i < $raw_length; $i++) {
  151. if ($i < $bl) {
  152. $buffer[$i] = $buffer[$i] ^ chr(mt_rand(0, 255));
  153. } else {
  154. $buffer .= chr(mt_rand(0, 255));
  155. }
  156. }
  157. }
  158. $salt = str_replace('+', '.', base64_encode($buffer));
  159. }
  160. $salt = substr($salt, 0, $required_salt_len);
  161. $hash = $hash_format . $salt;
  162. $ret = crypt($password, $hash);
  163. if (!is_string($ret) || strlen($ret) <= 13) {
  164. return false;
  165. }
  166. return $ret;
  167. }
  168. /**
  169. * Get information about the password hash. Returns an array of the information
  170. * that was used to generate the password hash.
  171. *
  172. * array(
  173. * 'algo' => 1,
  174. * 'algoName' => 'bcrypt',
  175. * 'options' => array(
  176. * 'cost' => 10,
  177. * ),
  178. * )
  179. *
  180. * @param string $hash The password hash to extract info from
  181. *
  182. * @return array The array of information about the hash.
  183. */
  184. function password_get_info($hash) {
  185. $return = array(
  186. 'algo' => 0,
  187. 'algoName' => 'unknown',
  188. 'options' => array(),
  189. );
  190. if (substr($hash, 0, 4) == _PASSWORD_CRYPT_PROLOG && strlen($hash) == 60) {
  191. $return['algo'] = PASSWORD_BCRYPT;
  192. $return['algoName'] = 'bcrypt';
  193. list($cost) = sscanf($hash, _PASSWORD_CRYPT_HASH_FORMAT);
  194. $return['options']['cost'] = $cost;
  195. }
  196. return $return;
  197. }
  198. /**
  199. * Determine if the password hash needs to be rehashed according to the options provided
  200. *
  201. * If the answer is true, after validating the password using password_verify, rehash it.
  202. *
  203. * @param string $hash The hash to test
  204. * @param int $algo The algorithm used for new password hashes
  205. * @param array $options The options array passed to password_hash
  206. *
  207. * @return boolean True if the password needs to be rehashed.
  208. */
  209. function password_needs_rehash($hash, $algo, array $options = array()) {
  210. $info = password_get_info($hash);
  211. if ($info['algo'] != $algo) {
  212. return true;
  213. }
  214. switch ($algo) {
  215. case PASSWORD_BCRYPT:
  216. $cost = isset($options['cost']) ? $options['cost'] : 10;
  217. if ($cost != $info['options']['cost']) {
  218. return true;
  219. }
  220. break;
  221. }
  222. return false;
  223. }
  224. /**
  225. * Verify a password against a hash using a timing attack resistant approach
  226. *
  227. * @param string $password The password to verify
  228. * @param string $hash The hash to verify against
  229. *
  230. * @return boolean If the password matches the hash
  231. */
  232. function password_verify($password, $hash) {
  233. if (!function_exists('crypt')) {
  234. trigger_error("Crypt must be loaded for password_verify to function", E_USER_WARNING);
  235. return false;
  236. }
  237. $ret = crypt($password, $hash);
  238. if (!is_string($ret) || strlen($ret) != strlen($hash) || strlen($ret) <= 13) {
  239. return false;
  240. }
  241. $status = 0;
  242. for ($i = 0; $i < strlen($ret); $i++) {
  243. $status |= (ord($ret[$i]) ^ ord($hash[$i]));
  244. }
  245. return $status === 0;
  246. }
  247. }