1 | <?php |
||||
2 | /** |
||||
3 | * A Compatibility library with PHP 5.5's simplified password hashing API. |
||||
4 | * |
||||
5 | * @author Anthony Ferrara <[email protected]> |
||||
6 | * @license https://opensource.org/licenses/mit-license.html MIT License |
||||
7 | * @copyright 2012 The Authors |
||||
8 | * |
||||
9 | * Simple Machines Forum (SMF) |
||||
10 | * |
||||
11 | * @package SMF |
||||
12 | * @author Simple Machines https://www.simplemachines.org |
||||
13 | * @copyright 2020 Simple Machines and individual contributors |
||||
14 | * @license https://www.simplemachines.org/about/smf/license.php BSD |
||||
15 | * |
||||
16 | * @version 2.1 RC2 |
||||
17 | */ |
||||
18 | |||||
19 | namespace |
||||
20 | { |
||||
21 | |||||
22 | if (!defined('PASSWORD_DEFAULT')) |
||||
23 | { |
||||
24 | |||||
25 | define('PASSWORD_BCRYPT', 1); |
||||
26 | define('PASSWORD_DEFAULT', PASSWORD_BCRYPT); |
||||
27 | |||||
28 | /** |
||||
29 | * Hash the password using the specified algorithm |
||||
30 | * Limits the maximum length of password to 72, if a longer |
||||
31 | * string is supplied the first 72 characters are used |
||||
32 | * |
||||
33 | * @param string $password The password to hash |
||||
34 | * @param int $algo The algorithm to use (Defined by PASSWORD_* constants) |
||||
35 | * @param array $options The options for the algorithm to use |
||||
36 | * |
||||
37 | * @return string|false The hashed password, or false on error. |
||||
38 | */ |
||||
39 | function password_hash($password, $algo, array $options = array()) |
||||
40 | { |
||||
41 | global $smcFunc, $sourcedir; |
||||
42 | |||||
43 | if (!function_exists('crypt')) |
||||
44 | { |
||||
45 | trigger_error("Crypt must be loaded for password_hash to function", E_USER_WARNING); |
||||
46 | return null; |
||||
47 | } |
||||
48 | if (!is_string($password)) |
||||
0 ignored issues
–
show
introduced
by
![]() |
|||||
49 | { |
||||
50 | trigger_error("password_hash(): Password must be a string", E_USER_WARNING); |
||||
51 | return null; |
||||
52 | } |
||||
53 | if (!is_int($algo)) |
||||
0 ignored issues
–
show
|
|||||
54 | { |
||||
55 | trigger_error("password_hash() expects parameter 2 to be long, " . gettype($algo) . " given", E_USER_WARNING); |
||||
56 | return null; |
||||
57 | } |
||||
58 | if (PasswordCompat\binary\_strlen($password) > 72) |
||||
59 | { |
||||
60 | $password = PasswordCompat\binary\_substr($password, 0, 72); |
||||
61 | } |
||||
62 | switch ($algo) |
||||
63 | { |
||||
64 | case PASSWORD_BCRYPT: |
||||
65 | // Note that this is a C constant, but not exposed to PHP, so we don't define it here. |
||||
66 | $cost = 10; |
||||
67 | if (isset($options['cost'])) |
||||
68 | { |
||||
69 | $cost = $options['cost']; |
||||
70 | if ($cost < 4 || $cost > 31) |
||||
71 | { |
||||
72 | trigger_error(sprintf("password_hash(): Invalid bcrypt cost parameter specified: %d", $cost), E_USER_WARNING); |
||||
73 | return null; |
||||
74 | } |
||||
75 | } |
||||
76 | // The length of salt to generate |
||||
77 | $raw_salt_len = 16; |
||||
78 | // The length required in the final serialization |
||||
79 | $required_salt_len = 22; |
||||
80 | $hash_format = sprintf("$2y$%02d$", $cost); |
||||
81 | // The expected length of the final crypt() output |
||||
82 | $resultLength = 60; |
||||
83 | break; |
||||
84 | default: |
||||
85 | trigger_error(sprintf("password_hash(): Unknown password hashing algorithm: %s", $algo), E_USER_WARNING); |
||||
86 | return null; |
||||
87 | } |
||||
88 | $salt_requires_encoding = false; |
||||
89 | if (isset($options['salt'])) |
||||
90 | { |
||||
91 | switch (gettype($options['salt'])) |
||||
92 | { |
||||
93 | case 'NULL': |
||||
94 | case 'boolean': |
||||
95 | case 'integer': |
||||
96 | case 'double': |
||||
97 | case 'string': |
||||
98 | $salt = (string) $options['salt']; |
||||
99 | break; |
||||
100 | case 'object': |
||||
101 | if (method_exists($options['salt'], '__tostring')) |
||||
102 | { |
||||
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 (PasswordCompat\binary\_strlen($salt) < $required_salt_len) |
||||
113 | { |
||||
114 | trigger_error(sprintf("password_hash(): Provided salt is too short: %d expecting %d", PasswordCompat\binary\_strlen($salt), $required_salt_len), E_USER_WARNING); |
||||
115 | return null; |
||||
116 | } |
||||
117 | elseif (0 == preg_match('#^[a-zA-Z0-9./]+$#D', $salt)) |
||||
118 | { |
||||
119 | $salt_requires_encoding = true; |
||||
120 | } |
||||
121 | } |
||||
122 | else |
||||
123 | { |
||||
124 | $buffer = ''; |
||||
125 | $buffer_valid = false; |
||||
126 | if (function_exists('random_bytes')) |
||||
127 | { |
||||
128 | $buffer = random_bytes($raw_salt_len); |
||||
129 | if ($buffer) |
||||
130 | { |
||||
131 | $buffer_valid = true; |
||||
132 | } |
||||
133 | } |
||||
134 | if (!$buffer_valid && is_callable(@$smcFunc['random_bytes'])) |
||||
135 | { |
||||
136 | $buffer = $smcFunc['random_bytes']($raw_salt_len); |
||||
137 | if ($buffer) |
||||
138 | { |
||||
139 | $buffer_valid = true; |
||||
140 | } |
||||
141 | } |
||||
142 | if (!$buffer_valid && file_exists((!empty($sourcedir) ? $sourcedir : __DIR__) . '/random_compat/random.php')) |
||||
143 | { |
||||
144 | require_once($sourcedir . '/random_compat/random.php'); |
||||
145 | $buffer = random_bytes($raw_salt_len); |
||||
146 | if ($buffer) |
||||
147 | { |
||||
148 | $buffer_valid = true; |
||||
149 | } |
||||
150 | } |
||||
151 | if (!$buffer_valid && function_exists('mcrypt_create_iv') && !defined('PHALANGER')) |
||||
152 | { |
||||
153 | $buffer = mcrypt_create_iv($raw_salt_len, MCRYPT_DEV_URANDOM); |
||||
0 ignored issues
–
show
The function
mcrypt_create_iv() has been deprecated: 7.1
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This function has been deprecated. The supplier of the function has supplied an explanatory message. The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead. ![]() |
|||||
154 | if ($buffer) |
||||
155 | { |
||||
156 | $buffer_valid = true; |
||||
157 | } |
||||
158 | } |
||||
159 | if (!$buffer_valid && function_exists('openssl_random_pseudo_bytes')) |
||||
160 | { |
||||
161 | $buffer = openssl_random_pseudo_bytes($raw_salt_len); |
||||
162 | if ($buffer) |
||||
163 | { |
||||
164 | $buffer_valid = true; |
||||
165 | } |
||||
166 | } |
||||
167 | if (!$buffer_valid && @is_readable('/dev/urandom')) |
||||
168 | { |
||||
169 | $f = fopen('/dev/urandom', 'r'); |
||||
170 | $read = PasswordCompat\binary\_strlen($buffer); |
||||
171 | while ($read < $raw_salt_len) |
||||
172 | { |
||||
173 | $buffer .= fread($f, $raw_salt_len - $read); |
||||
0 ignored issues
–
show
It seems like
$f can also be of type false ; however, parameter $handle of fread() does only seem to accept resource , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
174 | $read = PasswordCompat\binary\_strlen($buffer); |
||||
175 | } |
||||
176 | fclose($f); |
||||
0 ignored issues
–
show
It seems like
$f can also be of type false ; however, parameter $handle of fclose() does only seem to accept resource , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
177 | if ($read >= $raw_salt_len) |
||||
178 | { |
||||
179 | $buffer_valid = true; |
||||
180 | } |
||||
181 | } |
||||
182 | if (!$buffer_valid || PasswordCompat\binary\_strlen($buffer) < $raw_salt_len) |
||||
183 | { |
||||
184 | if (function_exists('random_int')) |
||||
185 | $random_int = 'random_int'; |
||||
186 | // This is bad, but we're out of options. Alternative would be to trigger an error, maybe? |
||||
187 | else |
||||
188 | $random_int = 'mt_rand'; |
||||
189 | |||||
190 | $bl = PasswordCompat\binary\_strlen($buffer); |
||||
191 | for ($i = 0; $i < $raw_salt_len; $i++) |
||||
192 | { |
||||
193 | if ($i < $bl) |
||||
194 | { |
||||
195 | $buffer[$i] = $buffer[$i] ^ chr($random_int(0, 255)); |
||||
196 | } |
||||
197 | else |
||||
198 | { |
||||
199 | $buffer .= chr($random_int(0, 255)); |
||||
200 | } |
||||
201 | } |
||||
202 | } |
||||
203 | $salt = $buffer; |
||||
204 | $salt_requires_encoding = true; |
||||
205 | } |
||||
206 | if ($salt_requires_encoding) |
||||
207 | { |
||||
208 | // encode string with the Base64 variant used by crypt |
||||
209 | $base64_digits = |
||||
210 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; |
||||
211 | $bcrypt64_digits = |
||||
212 | './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; |
||||
213 | |||||
214 | $base64_string = base64_encode($salt); |
||||
215 | $salt = strtr(rtrim($base64_string, '='), $base64_digits, $bcrypt64_digits); |
||||
216 | } |
||||
217 | $salt = PasswordCompat\binary\_substr($salt, 0, $required_salt_len); |
||||
218 | |||||
219 | $hash = $hash_format . $salt; |
||||
220 | |||||
221 | $ret = crypt($password, $hash); |
||||
222 | |||||
223 | if (!is_string($ret) || PasswordCompat\binary\_strlen($ret) != $resultLength) |
||||
224 | { |
||||
225 | return false; |
||||
226 | } |
||||
227 | |||||
228 | return $ret; |
||||
229 | } |
||||
230 | |||||
231 | /** |
||||
232 | * Get information about the password hash. Returns an array of the information |
||||
233 | * that was used to generate the password hash. |
||||
234 | * |
||||
235 | * array( |
||||
236 | * 'algo' => 1, |
||||
237 | * 'algoName' => 'bcrypt', |
||||
238 | * 'options' => array( |
||||
239 | * 'cost' => 10, |
||||
240 | * ), |
||||
241 | * ) |
||||
242 | * |
||||
243 | * @param string $hash The password hash to extract info from |
||||
244 | * |
||||
245 | * @return array The array of information about the hash. |
||||
246 | */ |
||||
247 | function password_get_info($hash) |
||||
248 | { |
||||
249 | $return = array( |
||||
250 | 'algo' => 0, |
||||
251 | 'algoName' => 'unknown', |
||||
252 | 'options' => array(), |
||||
253 | ); |
||||
254 | if (PasswordCompat\binary\_substr($hash, 0, 4) == '$2y$' && PasswordCompat\binary\_strlen($hash) == 60) |
||||
255 | { |
||||
256 | $return['algo'] = PASSWORD_BCRYPT; |
||||
257 | $return['algoName'] = 'bcrypt'; |
||||
258 | list($cost) = sscanf($hash, "$2y$%d$"); |
||||
259 | $return['options']['cost'] = $cost; |
||||
260 | } |
||||
261 | return $return; |
||||
262 | } |
||||
263 | |||||
264 | /** |
||||
265 | * Determine if the password hash needs to be rehashed according to the options provided |
||||
266 | * |
||||
267 | * If the answer is true, after validating the password using password_verify, rehash it. |
||||
268 | * |
||||
269 | * @param string $hash The hash to test |
||||
270 | * @param int $algo The algorithm used for new password hashes |
||||
271 | * @param array $options The options array passed to password_hash |
||||
272 | * |
||||
273 | * @return boolean True if the password needs to be rehashed. |
||||
274 | */ |
||||
275 | function password_needs_rehash($hash, $algo, array $options = array()) |
||||
276 | { |
||||
277 | $info = password_get_info($hash); |
||||
278 | if ($info['algo'] != $algo) |
||||
279 | { |
||||
280 | return true; |
||||
281 | } |
||||
282 | switch ($algo) |
||||
283 | { |
||||
284 | case PASSWORD_BCRYPT: |
||||
285 | $cost = isset($options['cost']) ? $options['cost'] : 10; |
||||
286 | if ($cost != $info['options']['cost']) |
||||
287 | { |
||||
288 | return true; |
||||
289 | } |
||||
290 | break; |
||||
291 | } |
||||
292 | return false; |
||||
293 | } |
||||
294 | |||||
295 | /** |
||||
296 | * Verify a password against a hash using a timing attack resistant approach |
||||
297 | * |
||||
298 | * @param string $password The password to verify |
||||
299 | * @param string $hash The hash to verify against |
||||
300 | * |
||||
301 | * @return boolean If the password matches the hash |
||||
302 | */ |
||||
303 | function password_verify($password, $hash) |
||||
304 | { |
||||
305 | if (!function_exists('crypt')) |
||||
306 | { |
||||
307 | trigger_error("Crypt must be loaded for password_verify to function", E_USER_WARNING); |
||||
308 | return false; |
||||
309 | } |
||||
310 | if (PasswordCompat\binary\_strlen($password) > 72) |
||||
311 | { |
||||
312 | $password = PasswordCompat\binary\_substr($password, 0, 72); |
||||
313 | } |
||||
314 | $ret = crypt($password, $hash); |
||||
315 | if (!is_string($ret) || PasswordCompat\binary\_strlen($ret) != PasswordCompat\binary\_strlen($hash) || PasswordCompat\binary\_strlen($ret) <= 13) |
||||
316 | { |
||||
317 | return false; |
||||
318 | } |
||||
319 | |||||
320 | $status = 0; |
||||
321 | for ($i = 0; $i < PasswordCompat\binary\_strlen($ret); $i++) |
||||
322 | { |
||||
323 | $status |= (ord($ret[$i]) ^ ord($hash[$i])); |
||||
324 | } |
||||
325 | |||||
326 | return $status === 0; |
||||
327 | } |
||||
328 | } |
||||
329 | } |
||||
330 | |||||
331 | namespace PasswordCompat\binary |
||||
332 | { |
||||
333 | /** |
||||
334 | * Count the number of bytes in a string |
||||
335 | * |
||||
336 | * We cannot simply use strlen() for this, because it might be overwritten by the mbstring extension. |
||||
337 | * In this case, strlen() will count the number of *characters* based on the internal encoding. A |
||||
338 | * sequence of bytes might be regarded as a single multibyte character. |
||||
339 | * |
||||
340 | * @param string $binary_string The input string |
||||
341 | * |
||||
342 | * @internal |
||||
343 | * @return int The number of bytes |
||||
344 | */ |
||||
345 | function _strlen($binary_string) |
||||
346 | { |
||||
347 | if (function_exists('mb_strlen')) |
||||
348 | { |
||||
349 | return mb_strlen($binary_string, '8bit'); |
||||
350 | } |
||||
351 | return strlen($binary_string); |
||||
352 | } |
||||
353 | |||||
354 | /** |
||||
355 | * Get a substring based on byte limits |
||||
356 | * |
||||
357 | * @see _strlen() |
||||
358 | * |
||||
359 | * @param string $binary_string The input string |
||||
360 | * @param int $start |
||||
361 | * @param int $length |
||||
362 | * |
||||
363 | * @internal |
||||
364 | * @return string The substring |
||||
365 | */ |
||||
366 | function _substr($binary_string, $start, $length) |
||||
367 | { |
||||
368 | if (function_exists('mb_substr')) |
||||
369 | { |
||||
370 | return mb_substr($binary_string, $start, $length, '8bit'); |
||||
371 | } |
||||
372 | return substr($binary_string, $start, $length); |
||||
373 | } |
||||
374 | } |
||||
375 | |||||
376 | ?> |