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:
10: namespace {
11:
12: if (!defined('PASSWORD_BCRYPT')) {
13: /**
14: * PHPUnit Process isolation caches constants, but not function declarations.
15: * So we need to check if the constants are defined separately from
16: * the functions to enable supporting process isolation in userland
17: * code.
18: */
19: define('PASSWORD_BCRYPT', 1);
20: define('PASSWORD_DEFAULT', PASSWORD_BCRYPT);
21: define('PASSWORD_BCRYPT_DEFAULT_COST', 10);
22: }
23:
24: if (!function_exists('password_hash')) {
25:
26: /**
27: * Hash the password using the specified algorithm
28: *
29: * @param string $password The password to hash
30: * @param int $algo The algorithm to use (Defined by PASSWORD_* constants)
31: * @param array $options The options for the algorithm to use
32: *
33: * @return string|false The hashed password, or false on error.
34: */
35: function password_hash($password, $algo, array $options = array()) {
36: if (!function_exists('crypt')) {
37: trigger_error("Crypt must be loaded for password_hash to function", E_USER_WARNING);
38: return null;
39: }
40: if (is_null($password) || is_int($password)) {
41: $password = (string) $password;
42: }
43: if (!is_string($password)) {
44: trigger_error("password_hash(): Password must be a string", E_USER_WARNING);
45: return null;
46: }
47: if (!is_int($algo)) {
48: trigger_error("password_hash() expects parameter 2 to be long, " . gettype($algo) . " given", E_USER_WARNING);
49: return null;
50: }
51: $resultLength = 0;
52: switch ($algo) {
53: case PASSWORD_BCRYPT:
54: $cost = PASSWORD_BCRYPT_DEFAULT_COST;
55: if (isset($options['cost'])) {
56: $cost = $options['cost'];
57: if ($cost < 4 || $cost > 31) {
58: trigger_error(sprintf("password_hash(): Invalid bcrypt cost parameter specified: %d", $cost), E_USER_WARNING);
59: return null;
60: }
61: }
62: // The length of salt to generate
63: $raw_salt_len = 16;
64: // The length required in the final serialization
65: $required_salt_len = 22;
66: $hash_format = sprintf("$2y$%02d$", $cost);
67: // The expected length of the final crypt() output
68: $resultLength = 60;
69: break;
70: default:
71: trigger_error(sprintf("password_hash(): Unknown password hashing algorithm: %s", $algo), E_USER_WARNING);
72: return null;
73: }
74: $salt_requires_encoding = false;
75: if (isset($options['salt'])) {
76: switch (gettype($options['salt'])) {
77: case 'NULL':
78: case 'boolean':
79: case 'integer':
80: case 'double':
81: case 'string':
82: $salt = (string) $options['salt'];
83: break;
84: case 'object':
85: if (method_exists($options['salt'], '__tostring')) {
86: $salt = (string) $options['salt'];
87: break;
88: }
89: case 'array':
90: case 'resource':
91: default:
92: trigger_error('password_hash(): Non-string salt parameter supplied', E_USER_WARNING);
93: return null;
94: }
95: if (PasswordCompat\binary\_strlen($salt) < $required_salt_len) {
96: trigger_error(sprintf("password_hash(): Provided salt is too short: %d expecting %d", PasswordCompat\binary\_strlen($salt), $required_salt_len), E_USER_WARNING);
97: return null;
98: } elseif (0 == preg_match('#^[a-zA-Z0-9./]+$#D', $salt)) {
99: $salt_requires_encoding = true;
100: }
101: } else {
102: $buffer = '';
103: $buffer_valid = false;
104: if (function_exists('mcrypt_create_iv') && !defined('PHALANGER')) {
105: $buffer = mcrypt_create_iv($raw_salt_len, MCRYPT_DEV_URANDOM);
106: if ($buffer) {
107: $buffer_valid = true;
108: }
109: }
110: if (!$buffer_valid && function_exists('openssl_random_pseudo_bytes')) {
111: $buffer = openssl_random_pseudo_bytes($raw_salt_len);
112: if ($buffer) {
113: $buffer_valid = true;
114: }
115: }
116: if (!$buffer_valid && @is_readable('/dev/urandom')) {
117: $f = fopen('/dev/urandom', 'r');
118: $read = PasswordCompat\binary\_strlen($buffer);
119: while ($read < $raw_salt_len) {
120: $buffer .= fread($f, $raw_salt_len - $read);
121: $read = PasswordCompat\binary\_strlen($buffer);
122: }
123: fclose($f);
124: if ($read >= $raw_salt_len) {
125: $buffer_valid = true;
126: }
127: }
128: if (!$buffer_valid || PasswordCompat\binary\_strlen($buffer) < $raw_salt_len) {
129: $bl = PasswordCompat\binary\_strlen($buffer);
130: for ($i = 0; $i < $raw_salt_len; $i++) {
131: if ($i < $bl) {
132: $buffer[$i] = $buffer[$i] ^ chr(mt_rand(0, 255));
133: } else {
134: $buffer .= chr(mt_rand(0, 255));
135: }
136: }
137: }
138: $salt = $buffer;
139: $salt_requires_encoding = true;
140: }
141: if ($salt_requires_encoding) {
142: // encode string with the Base64 variant used by crypt
143: $base64_digits =
144: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
145: $bcrypt64_digits =
146: './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
147:
148: $base64_string = base64_encode($salt);
149: $salt = strtr(rtrim($base64_string, '='), $base64_digits, $bcrypt64_digits);
150: }
151: $salt = PasswordCompat\binary\_substr($salt, 0, $required_salt_len);
152:
153: $hash = $hash_format . $salt;
154:
155: $ret = crypt($password, $hash);
156:
157: if (!is_string($ret) || PasswordCompat\binary\_strlen($ret) != $resultLength) {
158: return false;
159: }
160:
161: return $ret;
162: }
163:
164: /**
165: * Get information about the password hash. Returns an array of the information
166: * that was used to generate the password hash.
167: *
168: * array(
169: * 'algo' => 1,
170: * 'algoName' => 'bcrypt',
171: * 'options' => array(
172: * 'cost' => PASSWORD_BCRYPT_DEFAULT_COST,
173: * ),
174: * )
175: *
176: * @param string $hash The password hash to extract info from
177: *
178: * @return array The array of information about the hash.
179: */
180: function password_get_info($hash) {
181: $return = array(
182: 'algo' => 0,
183: 'algoName' => 'unknown',
184: 'options' => array(),
185: );
186: if (PasswordCompat\binary\_substr($hash, 0, 4) == '$2y$' && PasswordCompat\binary\_strlen($hash) == 60) {
187: $return['algo'] = PASSWORD_BCRYPT;
188: $return['algoName'] = 'bcrypt';
189: list($cost) = sscanf($hash, "$2y$%d$");
190: $return['options']['cost'] = $cost;
191: }
192: return $return;
193: }
194:
195: /**
196: * Determine if the password hash needs to be rehashed according to the options provided
197: *
198: * If the answer is true, after validating the password using password_verify, rehash it.
199: *
200: * @param string $hash The hash to test
201: * @param int $algo The algorithm used for new password hashes
202: * @param array $options The options array passed to password_hash
203: *
204: * @return boolean True if the password needs to be rehashed.
205: */
206: function password_needs_rehash($hash, $algo, array $options = array()) {
207: $info = password_get_info($hash);
208: if ($info['algo'] != $algo) {
209: return true;
210: }
211: switch ($algo) {
212: case PASSWORD_BCRYPT:
213: $cost = isset($options['cost']) ? $options['cost'] : PASSWORD_BCRYPT_DEFAULT_COST;
214: if ($cost != $info['options']['cost']) {
215: return true;
216: }
217: break;
218: }
219: return false;
220: }
221:
222: /**
223: * Verify a password against a hash using a timing attack resistant approach
224: *
225: * @param string $password The password to verify
226: * @param string $hash The hash to verify against
227: *
228: * @return boolean If the password matches the hash
229: */
230: function password_verify($password, $hash) {
231: if (!function_exists('crypt')) {
232: trigger_error("Crypt must be loaded for password_verify to function", E_USER_WARNING);
233: return false;
234: }
235: $ret = crypt($password, $hash);
236: if (!is_string($ret) || PasswordCompat\binary\_strlen($ret) != PasswordCompat\binary\_strlen($hash) || PasswordCompat\binary\_strlen($ret) <= 13) {
237: return false;
238: }
239:
240: $status = 0;
241: for ($i = 0; $i < PasswordCompat\binary\_strlen($ret); $i++) {
242: $status |= (ord($ret[$i]) ^ ord($hash[$i]));
243: }
244:
245: return $status === 0;
246: }
247: }
248:
249: }
250:
251: namespace PasswordCompat\binary {
252:
253: if (!function_exists('PasswordCompat\\binary\\_strlen')) {
254:
255: /**
256: * Count the number of bytes in a string
257: *
258: * We cannot simply use strlen() for this, because it might be overwritten by the mbstring extension.
259: * In this case, strlen() will count the number of *characters* based on the internal encoding. A
260: * sequence of bytes might be regarded as a single multibyte character.
261: *
262: * @param string $binary_string The input string
263: *
264: * @internal
265: * @return int The number of bytes
266: */
267: function _strlen($binary_string) {
268: if (function_exists('mb_strlen')) {
269: return mb_strlen($binary_string, '8bit');
270: }
271: return strlen($binary_string);
272: }
273:
274: /**
275: * Get a substring based on byte limits
276: *
277: * @see _strlen()
278: *
279: * @param string $binary_string The input string
280: * @param int $start
281: * @param int $length
282: *
283: * @internal
284: * @return string The substring
285: */
286: function _substr($binary_string, $start, $length) {
287: if (function_exists('mb_substr')) {
288: return mb_substr($binary_string, $start, $length, '8bit');
289: }
290: return substr($binary_string, $start, $length);
291: }
292:
293: /**
294: * Check if current PHP version is compatible with the library
295: *
296: * @return boolean the check result
297: */
298: function check() {
299: static $pass = NULL;
300:
301: if (is_null($pass)) {
302: if (function_exists('crypt')) {
303: $hash = '$2y$04$usesomesillystringfore7hnbRJHxXVLeakoG8K30oukPsA.ztMG';
304: $test = crypt("password", $hash);
305: $pass = $test == $hash;
306: } else {
307: $pass = false;
308: }
309: }
310: return $pass;
311: }
312:
313: }
314: }