password_verify()   B
last analyzed

Complexity

Conditions 7
Paths 7

Size

Total Lines 24
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 12
nop 2
dl 0
loc 24
rs 8.8333
c 0
b 0
f 0
nc 7
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 http://www.simplemachines.org
13
 * @copyright 2019 Simple Machines and individual contributors
14
 * @license http://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;
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
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
introduced by
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 && function_exists('mcrypt_create_iv') && !defined('PHALANGER'))
135
				{
136
					$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

136
					$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...
137
					if ($buffer)
138
					{
139
						$buffer_valid = true;
140
					}
141
				}
142
				if (!$buffer_valid && function_exists('openssl_random_pseudo_bytes'))
143
				{
144
					$buffer = openssl_random_pseudo_bytes($raw_salt_len);
145
					if ($buffer)
146
					{
147
						$buffer_valid = true;
148
					}
149
				}
150
				if (!$buffer_valid && @is_readable('/dev/urandom'))
151
				{
152
					$f = fopen('/dev/urandom', 'r');
153
					$read = PasswordCompat\binary\_strlen($buffer);
154
					while ($read < $raw_salt_len)
155
					{
156
						$buffer .= fread($f, $raw_salt_len - $read);
0 ignored issues
show
Bug introduced by
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

156
						$buffer .= fread(/** @scrutinizer ignore-type */ $f, $raw_salt_len - $read);
Loading history...
157
						$read = PasswordCompat\binary\_strlen($buffer);
158
					}
159
					fclose($f);
0 ignored issues
show
Bug introduced by
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

159
					fclose(/** @scrutinizer ignore-type */ $f);
Loading history...
160
					if ($read >= $raw_salt_len)
161
					{
162
						$buffer_valid = true;
163
					}
164
				}
165
				if (!$buffer_valid || PasswordCompat\binary\_strlen($buffer) < $raw_salt_len)
166
				{
167
					$bl = PasswordCompat\binary\_strlen($buffer);
168
					for ($i = 0; $i < $raw_salt_len; $i++)
169
					{
170
						if ($i < $bl)
171
						{
172
							$buffer[$i] = $buffer[$i] ^ chr($smcFunc['random_int'](0, 255));
173
						}
174
						else
175
						{
176
							$buffer .= chr($smcFunc['random_int'](0, 255));
177
						}
178
					}
179
				}
180
				$salt = $buffer;
181
				$salt_requires_encoding = true;
182
			}
183
			if ($salt_requires_encoding)
184
			{
185
				// encode string with the Base64 variant used by crypt
186
				$base64_digits =
187
					'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
188
				$bcrypt64_digits =
189
					'./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
190
191
				$base64_string = base64_encode($salt);
192
				$salt = strtr(rtrim($base64_string, '='), $base64_digits, $bcrypt64_digits);
193
			}
194
			$salt = PasswordCompat\binary\_substr($salt, 0, $required_salt_len);
195
196
			$hash = $hash_format . $salt;
197
198
			$ret = crypt($password, $hash);
199
200
			if (!is_string($ret) || PasswordCompat\binary\_strlen($ret) != $resultLength)
201
			{
202
				return false;
203
			}
204
205
			return $ret;
206
		}
207
208
		/**
209
		 * Get information about the password hash. Returns an array of the information
210
		 * that was used to generate the password hash.
211
		 *
212
		 * array(
213
		 *    'algo' => 1,
214
		 *    'algoName' => 'bcrypt',
215
		 *    'options' => array(
216
		 *        'cost' => 10,
217
		 *    ),
218
		 * )
219
		 *
220
		 * @param string $hash The password hash to extract info from
221
		 *
222
		 * @return array The array of information about the hash.
223
		 */
224
		function password_get_info($hash)
225
		{
226
			$return = array(
227
				'algo' => 0,
228
				'algoName' => 'unknown',
229
				'options' => array(),
230
			);
231
			if (PasswordCompat\binary\_substr($hash, 0, 4) == '$2y$' && PasswordCompat\binary\_strlen($hash) == 60)
232
			{
233
				$return['algo'] = PASSWORD_BCRYPT;
234
				$return['algoName'] = 'bcrypt';
235
				list($cost) = sscanf($hash, "$2y$%d$");
236
				$return['options']['cost'] = $cost;
237
			}
238
			return $return;
239
		}
240
241
		/**
242
		 * Determine if the password hash needs to be rehashed according to the options provided
243
		 *
244
		 * If the answer is true, after validating the password using password_verify, rehash it.
245
		 *
246
		 * @param string $hash    The hash to test
247
		 * @param int    $algo    The algorithm used for new password hashes
248
		 * @param array  $options The options array passed to password_hash
249
		 *
250
		 * @return boolean True if the password needs to be rehashed.
251
		 */
252
		function password_needs_rehash($hash, $algo, array $options = array())
253
		{
254
			$info = password_get_info($hash);
255
			if ($info['algo'] != $algo)
256
			{
257
				return true;
258
			}
259
			switch ($algo)
260
			{
261
				case PASSWORD_BCRYPT:
262
					$cost = isset($options['cost']) ? $options['cost'] : 10;
263
					if ($cost != $info['options']['cost'])
264
					{
265
						return true;
266
					}
267
					break;
268
			}
269
			return false;
270
		}
271
272
		/**
273
		 * Verify a password against a hash using a timing attack resistant approach
274
		 *
275
		 * @param string $password The password to verify
276
		 * @param string $hash     The hash to verify against
277
		 *
278
		 * @return boolean If the password matches the hash
279
		 */
280
		function password_verify($password, $hash)
281
		{
282
			if (!function_exists('crypt'))
283
			{
284
				trigger_error("Crypt must be loaded for password_verify to function", E_USER_WARNING);
285
				return false;
286
			}
287
			if (PasswordCompat\binary\_strlen($password) > 72)
288
			{
289
				$password = PasswordCompat\binary\_substr($password, 0, 72);
290
			}
291
			$ret = crypt($password, $hash);
292
			if (!is_string($ret) || PasswordCompat\binary\_strlen($ret) != PasswordCompat\binary\_strlen($hash) || PasswordCompat\binary\_strlen($ret) <= 13)
293
			{
294
				return false;
295
			}
296
297
			$status = 0;
298
			for ($i = 0; $i < PasswordCompat\binary\_strlen($ret); $i++)
299
			{
300
				$status |= (ord($ret[$i]) ^ ord($hash[$i]));
301
			}
302
303
			return $status === 0;
304
		}
305
	}
306
}
307
308
namespace PasswordCompat\binary
309
{
310
	/**
311
	 * Count the number of bytes in a string
312
	 *
313
	 * We cannot simply use strlen() for this, because it might be overwritten by the mbstring extension.
314
	 * In this case, strlen() will count the number of *characters* based on the internal encoding. A
315
	 * sequence of bytes might be regarded as a single multibyte character.
316
	 *
317
	 * @param string $binary_string The input string
318
	 *
319
	 * @internal
320
	 * @return int The number of bytes
321
	 */
322
	function _strlen($binary_string)
323
	{
324
		if (function_exists('mb_strlen'))
325
		{
326
			return mb_strlen($binary_string, '8bit');
327
		}
328
		return strlen($binary_string);
329
	}
330
331
	/**
332
	 * Get a substring based on byte limits
333
	 *
334
	 * @see _strlen()
335
	 *
336
	 * @param string $binary_string The input string
337
	 * @param int    $start
338
	 * @param int    $length
339
	 *
340
	 * @internal
341
	 * @return string The substring
342
	 */
343
	function _substr($binary_string, $start, $length)
344
	{
345
		if (function_exists('mb_substr'))
346
		{
347
			return mb_substr($binary_string, $start, $length, '8bit');
348
		}
349
		return substr($binary_string, $start, $length);
350
	}
351
}
352
353
?>