Issues (1061)

Sources/Subs-Password.php (5 issues)

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
The condition is_string($password) is always true.
Loading history...
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
The condition is_int($algo) is always true.
Loading history...
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
Deprecated Code introduced by
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 ignore-deprecated  annotation

153
					$buffer = /** @scrutinizer ignore-deprecated */ mcrypt_create_iv($raw_salt_len, MCRYPT_DEV_URANDOM);

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.

Loading history...
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 ignore-type  annotation

173
						$buffer .= fread(/** @scrutinizer ignore-type */ $f, $raw_salt_len - $read);
Loading history...
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 ignore-type  annotation

176
					fclose(/** @scrutinizer ignore-type */ $f);
Loading history...
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
?>