GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Push — develop ( 32b66f...80cd91 )
by gyeong-won
11:21
created

Password::bcrypt()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 2
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/* Copyright (C) NAVER <http://www.navercorp.com> */
3
4
/**
5
 * This class can be used to hash passwords using various algorithms and check their validity.
6
 * It is fully compatible with previous defaults, while also supporting bcrypt and pbkdf2.
7
 *
8
 * @file Password.class.php
9
 * @author Kijin Sung ([email protected])
10
 * @package /classes/security
11
 * @version 1.1
12
 */
13
class Password
14
{
15
	/**
16
	 * @brief Return the list of hashing algorithms supported by this server
17
	 * @return array
18
	 */
19
	public function getSupportedAlgorithms()
20
	{
21
		$retval = array();
22
		if(function_exists('hash_hmac') && in_array('sha256', hash_algos()))
23
		{
24
			$retval['pbkdf2'] = 'pbkdf2';
25
		}
26
		if(version_compare(PHP_VERSION, '5.3.7', '>=') && defined('CRYPT_BLOWFISH'))
27
		{
28
			$retval['bcrypt'] = 'bcrypt';
29
		}
30
		$retval['md5'] = 'md5';
31
		return $retval;
32
	}
33
34
	/**
35
	 * @brief Return the best hashing algorithm supported by this server
36
	 * @return string
37
	 */
38
	public function getBestAlgorithm()
39
	{
40
		$algos = $this->getSupportedAlgorithms();
41
		return key($algos);
42
	}
43
44
	/**
45
	 * @brief Return the currently selected hashing algorithm
46
	 * @return string
47
	 */
48
	public function getCurrentlySelectedAlgorithm()
49
	{
50
		if(function_exists('getModel'))
51
		{
52
			$config = getModel('member')->getMemberConfig();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class ModuleObject as the method getMemberConfig() does only exist in the following sub-classes of ModuleObject: memberModel. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
53
			$algorithm = $config->password_hashing_algorithm;
54
			if(strval($algorithm) === '')
55
			{
56
				$algorithm = 'md5';  // Historical default for XE
57
			}
58
		}
59
		else
60
		{
61
			$algorithm = 'md5';
62
		}
63
		return $algorithm;
64
	}
65
66
	/**
67
	 * @brief Return the currently configured work factor for bcrypt and other adjustable algorithms
68
	 * @return int
69
	 */
70
	public function getWorkFactor()
71
	{
72
		if(function_exists('getModel'))
73
		{
74
			$config = getModel('member')->getMemberConfig();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class ModuleObject as the method getMemberConfig() does only exist in the following sub-classes of ModuleObject: memberModel. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
75
			$work_factor = $config->password_hashing_work_factor;
76
			if(!$work_factor || $work_factor < 4 || $work_factor > 31)
77
			{
78
				$work_factor = 8;  // Reasonable default
79
			}
80
		}
81
		else
82
		{
83
			$work_factor = 8;
84
		}
85
		return $work_factor;
86
	}
87
88
	/**
89
	 * @brief Create a hash using the specified algorithm
90
	 * @param string $password The password
91
	 * @param string $algorithm The algorithm (optional)
92
	 * @return string
93
	 */
94
	public function createHash($password, $algorithm = null)
95
	{
96
		if($algorithm === null)
97
		{
98
			$algorithm = $this->getCurrentlySelectedAlgorithm();
99
		}
100
		if(!array_key_exists($algorithm, $this->getSupportedAlgorithms()))
101
		{
102
			return false;
103
		}
104
105
		$password = trim($password);
106
107
		switch($algorithm)
108
		{
109
			case 'md5':
110
				return md5($password);
111
112
			case 'pbkdf2':
113
				$iterations = pow(2, $this->getWorkFactor() + 5);
114
				$salt = $this->createSecureSalt(12, 'alnum');
115
				$hash = base64_encode($this->pbkdf2($password, $salt, 'sha256', $iterations, 24));
116
				return 'sha256:'.sprintf('%07d', $iterations).':'.$salt.':'.$hash;
117
118
			case 'bcrypt':
119
				return $this->bcrypt($password);
120
121
			default:
122
				return false;
123
		}
124
	}
125
126
	/**
127
	 * @brief Check if a password matches a hash
128
	 * @param string $password The password
129
	 * @param string $hash The hash
130
	 * @param string $algorithm The algorithm (optional)
131
	 * @return bool
132
	 */
133
	public function checkPassword($password, $hash, $algorithm = null)
134
	{
135
		if($algorithm === null)
136
		{
137
			$algorithm = $this->checkAlgorithm($hash);
138
		}
139
140
		$password = trim($password);
141
142
		switch($algorithm)
143
		{
144
			case 'md5':
145
				return md5($password) === $hash || md5(sha1(md5($password))) === $hash;
146
147
			case 'mysql_old_password':
148
				return (class_exists('Context') && substr(Context::getDBType(), 0, 5) === 'mysql') ?
149
					DB::getInstance()->isValidOldPassword($password, $hash) : false;
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class DB as the method isValidOldPassword() does only exist in the following sub-classes of DB: DBMysql, DBMysqlConnectWrapper, DBMysql_innodb, DBMysqli, DBMysqliConnectWrapper, DBMysqli_innodb. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
150
151
			case 'mysql_password':
152
				return $hash[0] === '*' && substr($hash, 1) === strtoupper(sha1(sha1($password, true)));
153
154
			case 'pbkdf2':
155
				$hash = explode(':', $hash);
156
				$hash[3] = base64_decode($hash[3]);
157
				$hash_to_compare = $this->pbkdf2($password, $hash[2], $hash[0], intval($hash[1], 10), strlen($hash[3]));
158
				return $this->strcmpConstantTime($hash_to_compare, $hash[3]);
159
160
			case 'bcrypt':
161
				$hash_to_compare = $this->bcrypt($password, $hash);
162
				return $this->strcmpConstantTime($hash_to_compare, $hash);
163
164
			default:
165
				return false;
166
		}
167
	}
168
169
	/**
170
	 * @brief Check the algorithm used to create a hash
171
	 * @param string $hash The hash
172
	 * @return string
173
	 */
174
	function checkAlgorithm($hash)
175
	{
176
		if(preg_match('/^\$2[axy]\$([0-9]{2})\$/', $hash, $matches))
177
		{
178
			return 'bcrypt';
179
		}
180
		elseif(preg_match('/^sha[0-9]+:([0-9]+):/', $hash, $matches))
181
		{
182
			return 'pbkdf2';
183
		}
184
		elseif(strlen($hash) === 32 && ctype_xdigit($hash))
185
		{
186
			return 'md5';
187
		}
188
		elseif(strlen($hash) === 16 && ctype_xdigit($hash))
189
		{
190
			return 'mysql_old_password';
191
		}
192
		elseif(strlen($hash) === 41 && $hash[0] === '*')
193
		{
194
			return 'mysql_password';
195
		}
196
		else
197
		{
198
			return false;
199
		}
200
	}
201
202
	/**
203
	 * @brief Check the work factor of a hash
204
	 * @param string $hash The hash
205
	 * @return int
206
	 */
207
	function checkWorkFactor($hash)
208
	{
209
		if(preg_match('/^\$2[axy]\$([0-9]{2})\$/', $hash, $matches))
210
		{
211
			return intval($matches[1], 10);
212
		}
213
		elseif(preg_match('/^sha[0-9]+:([0-9]+):/', $hash, $matches))
214
		{
215
			return max(0, round(log($matches[1], 2)) - 5);
216
		}
217
		else
218
		{
219
			return false;
220
		}
221
	}
222
223
	/**
224
	 * @brief Generate a cryptographically secure random string to use as a salt
225
	 * @param int $length The number of bytes to return
226
	 * @param string $format hex or alnum
227
	 * @return string
228
	 */
229
	public function createSecureSalt($length, $format = 'hex')
230
	{
231
		// Find out how many bytes of entropy we really need
232
		switch($format)
233
		{
234
			case 'hex':
235
				$entropy_required_bytes = ceil($length / 2);
236
				break;
237
			case 'alnum':
238
			case 'printable':
239
				$entropy_required_bytes = ceil($length * 3 / 4);
240
				break;
241
			default:
242
				$entropy_required_bytes = $length;
243
		}
244
245
		// Cap entropy to 256 bits from any one source, because anything more is meaningless
246
		$entropy_capped_bytes = min(32, $entropy_required_bytes);
247
248
		// Find and use the most secure way to generate a random string
249
		$is_windows = (defined('PHP_OS') && strtoupper(substr(PHP_OS, 0, 3)) === 'WIN');
250
		if(function_exists('openssl_random_pseudo_bytes') && (!$is_windows || version_compare(PHP_VERSION, '5.4', '>=')))
251
		{
252
			$entropy = openssl_random_pseudo_bytes($entropy_capped_bytes);
253
		}
254
		elseif(function_exists('mcrypt_create_iv') && (!$is_windows || version_compare(PHP_VERSION, '5.3.7', '>=')))
255
		{
256
			$entropy = mcrypt_create_iv($entropy_capped_bytes, MCRYPT_DEV_URANDOM);
257
		}
258
		elseif(function_exists('mcrypt_create_iv') && $is_windows)
259
		{
260
			$entropy = mcrypt_create_iv($entropy_capped_bytes, MCRYPT_RAND);
261
		}
262
		elseif(!$is_windows && @is_readable('/dev/urandom'))
263
		{
264
			$fp = fopen('/dev/urandom', 'rb');
265
			$entropy = fread($fp, $entropy_capped_bytes);
266
			fclose($fp);
267
		}
268
		else
269
		{
270
			$entropy = '';
271
			for($i = 0; $i < $entropy_capped_bytes; $i += 2)
272
			{
273
				$entropy .= pack('S', rand(0, 65536) ^ mt_rand(0, 65535));
274
			}
275
		}
276
277
		// Mixing (see RFC 4086 section 5)
278
		$output = '';
279
		for($i = 0; $i < $entropy_required_bytes; $i += 32)
280
		{
281
			$output .= hash('sha256', $entropy . $i . rand(), true);
282
		}
283
284
		// Encode and return the random string
285
		switch($format)
286
		{
287
			case 'hex':
288
				return substr(bin2hex($output), 0, $length);
289
			case 'binary':
290
				return substr($output, 0, $length);
291
			case 'printable':
292
				$salt = '';
293
				for($i = 0; $i < $length; $i++)
294
				{
295
					$salt .= chr(33 + (crc32(sha1($i . $output)) % 94));
296
				}
297
				return $salt;
298
			case 'alnum':
299
			default:
300
				$salt = substr(base64_encode($output), 0, $length);
301
				$replacements = chr(rand(65, 90)) . chr(rand(97, 122)) . rand(0, 9);
302
				return strtr($salt, '+/=', $replacements);
303
		}
304
	}
305
306
	/**
307
	 * @brief Generate a temporary password using the secure salt generator
308
	 * @param int $length The number of bytes to return
309
	 * @return string
310
	 */
311
	public function createTemporaryPassword($length = 16)
312
	{
313
		while(true)
314
		{
315
			$source = base64_encode($this->createSecureSalt(64, 'binary'));
316
			$source = strtr($source, 'iIoOjl10/', '@#$%&*-!?');
317
			$source_length = strlen($source);
318
			for($i = 0; $i < $source_length - $length; $i++)
319
			{
320
				$candidate = substr($source, $i, $length);
321
				if(preg_match('/[a-z]/', $candidate) && preg_match('/[A-Z]/', $candidate) &&
322
					preg_match('/[0-9]/', $candidate) && preg_match('/[^a-zA-Z0-9]/', $candidate))
323
				{
324
					return $candidate;
325
				}
326
			}
327
		}
328
	}
329
330
	/**
331
	 * @brief Create a digital signature to verify the authenticity of a string
332
	 * @param string $string
333
	 * @return string
334
	 */
335
	public static function createSignature($string)
336
	{
337
		$key = self::getSecretKey();
338
		$salt = self::createSecureSalt(8, 'alnum');
339
		$hash = substr(base64_encode(hash_hmac('sha256', hash_hmac('sha256', $string, $salt), $key, true)), 0, 32);
340
		return $salt . strtr($hash, '+/', '-_');
341
	}
342
	
343
	/**
344
	 * @brief Check whether a signature is valid
345
	 * @param string $string
346
	 * @param string $signature
347
	 * @return bool
348
	 */
349
	public static function checkSignature($string, $signature)
350
	{
351
		if(strlen($signature) !== 40)
352
		{
353
			return false;
354
		}
355
		
356
		$key = self::getSecretKey();
357
		$salt = substr($signature, 0, 8);
358
		$hash = substr(base64_encode(hash_hmac('sha256', hash_hmac('sha256', $string, $salt), $key, true)), 0, 32);
359
		return self::strcmpConstantTime(substr($signature, 8), strtr($hash, '+/', '-_'));
360
	}
361
	
362
	/**
363
	 * @brief Get the secret key for this site
364
	 * @return bool
365
	 */
366
	public static function getSecretKey()
367
	{
368
		// If the secret key does not exist, the config file needs to be updated
369
		$db_info = Context::getDbInfo();
370
		if(!isset($db_info->secret_key))
371
		{
372
			$db_info->secret_key = self::createSecureSalt(48, 'alnum');
373
			Context::setDBInfo($db_info);
374
			getController('install')->makeConfigFile();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class ModuleObject as the method makeConfigFile() does only exist in the following sub-classes of ModuleObject: installController. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
375
		}
376
		return $db_info->secret_key;
377
	}
378
	
379
	/**
380
	 * @brief Generate the PBKDF2 hash of a string using a salt
381
	 * @param string $password The password
382
	 * @param string $salt The salt
383
	 * @param string $algorithm The algorithm (optional, default is sha256)
384
	 * @param int $iterations Iteration count (optional, default is 8192)
385
	 * @param int $length The length of the hash (optional, default is 32)
386
	 * @return string
387
	 */
388
	public function pbkdf2($password, $salt, $algorithm = 'sha256', $iterations = 8192, $length = 24)
389
	{
390
		if(function_exists('hash_pbkdf2'))
391
		{
392
			return hash_pbkdf2($algorithm, $password, $salt, $iterations, $length, true);
393
		}
394
		else
395
		{
396
			$output = '';
397
			$block_count = ceil($length / strlen(hash($algorithm, '', true)));  // key length divided by the length of one hash
398
			for($i = 1; $i <= $block_count; $i++)
399
			{
400
				$last = $salt . pack('N', $i);  // $i encoded as 4 bytes, big endian
401
				$last = $xorsum = hash_hmac($algorithm, $last, $password, true);  // first iteration
402
				for($j = 1; $j < $iterations; $j++)  // The other $count - 1 iterations
403
				{
404
					$xorsum ^= ($last = hash_hmac($algorithm, $last, $password, true));
405
				}
406
				$output .= $xorsum;
407
			}
408
			return substr($output, 0, $length);
409
		}
410
	}
411
412
	/**
413
	 * @brief Generate the bcrypt hash of a string using a salt
414
	 * @param string $password The password
415
	 * @param string $salt The salt (optional, auto-generated if empty)
416
	 * @return string
417
	 */
418
	public function bcrypt($password, $salt = null)
419
	{
420
		if($salt === null)
421
		{
422
			$salt = '$2y$'.sprintf('%02d', $this->getWorkFactor()).'$'.$this->createSecureSalt(22, 'alnum');
423
		}
424
		return crypt($password, $salt);
425
	}
426
427
	/**
428
	 * @brief Compare two strings in constant time
429
	 * @param string $a The first string
430
	 * @param string $b The second string
431
	 * @return bool
432
	 */
433
	function strcmpConstantTime($a, $b)
434
	{
435
		$diff = strlen($a) ^ strlen($b);
436
		$maxlen = min(strlen($a), strlen($b));
437
		for($i = 0; $i < $maxlen; $i++)
438
		{
439
			$diff |= ord($a[$i]) ^ ord($b[$i]);
440
		}
441
		return $diff === 0;
442
	}
443
}
444
/* End of file : Password.class.php */
445
/* Location: ./classes/security/Password.class.php */
446