3 use wcf\system\exception\SystemException
;
5 use wcf\util\exception\CryptoException
;
8 * Provides functions to compute password hashes.
10 * @author Alexander Ebert
11 * @copyright 2001-2019 WoltLab GmbH
12 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
13 * @package WoltLabSuite\Core\Util
15 final class PasswordUtil
{
17 * list of possible characters in generated passwords
20 const PASSWORD_CHARSET
= 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
23 * concated list of valid blowfish salt characters
26 private static $blowfishCharacters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789./';
29 * list of supported encryption type by software identifier
32 private static $supportedEncryptionTypes = [
33 'ipb2', // Invision Power Board 2.x
34 'ipb3', // Invision Power Board 3.x
36 'phpbb3', // phpBB 3.x
37 'phpass', // phpass Portable Hashes
38 'smf1', // Simple Machines Forum 1.x
39 'smf2', // Simple Machines Forum 2.x
40 'vb3', // vBulletin 3.x
41 'vb4', // vBulletin 4.x
42 'vb5', // vBulletin 5.x
43 'wbb2', // WoltLab Burning Board 2.x
44 'wcf1', // WoltLab Community Framework 1.x
45 'wcf2', // WoltLab Suite 3.x / WoltLab Community Framework 2.x
46 'xf1', // XenForo 1.0 / 1.1
47 'xf12', // XenForo 1.2+
48 'joomla1', // Joomla 1.x
49 'joomla2', // Joomla 2.x
50 'joomla3', // Joomla 3.x
51 'phpfox3', // phpFox 3.x
53 'invalid', // Never going to match anything
57 * blowfish cost factor
60 const BCRYPT_COST
= '08';
63 * blowfish encryption type
66 const BCRYPT_TYPE
= '2a';
69 * Returns true if given encryption type is supported.
74 public static function isSupported($type) {
75 if (in_array($type, self
::$supportedEncryptionTypes)) {
79 if (preg_match('~^wcf1e[cms][01][ab][01]$~', $type)) {
87 * Returns true if given hash looks like a valid bcrypt hash.
92 public static function isBlowfish($hash) {
93 return (Regex
::compile('^\$2[afxy]\$')->match($hash) ?
true : false);
97 * Returns true if given bcrypt hash uses a different cost factor and should be re-computed.
102 public static function isDifferentBlowfish($hash) {
103 $currentCost = intval(self
::BCRYPT_COST
);
104 $hashCost = intval(substr($hash, 4, 2));
106 if ($currentCost != $hashCost) {
114 * Validates password against stored hash, encryption type is automatically resolved.
116 * @param string $username
117 * @param string $password
118 * @param string $dbHash
120 * @throws SystemException
122 public static function checkPassword($username, $password, $dbHash) {
123 $type = self
::detectEncryption($dbHash);
124 if ($type === 'unknown') {
125 throw new SystemException("Unable to determine password encryption");
128 // drop type from hash
129 $dbHash = substr($dbHash, strlen($type) +
1);
132 $parts = explode(':', $dbHash, 2);
133 if (count($parts) == 2) {
134 list($dbHash, $salt) = $parts;
142 if (in_array($type, self
::$supportedEncryptionTypes)) {
143 return call_user_func('\wcf\util\PasswordUtil::'.$type, $username, $password, $salt, $dbHash);
146 // WCF 1.x with different encryption
147 return self
::wcf1e($type, $password, $salt, $dbHash);
152 * Returns encryption type if possible.
154 * @param string $hash
157 public static function detectEncryption($hash) {
158 if (($pos = strpos($hash, ':')) !== false) {
159 $type = substr($hash, 0, $pos);
160 if (self
::isSupported($type)) {
169 * Returns a double salted bcrypt hash.
171 * @param string $password
172 * @param string $salt
175 public static function getDoubleSaltedHash($password, $salt = null) {
176 if ($salt === null) {
177 $salt = self
::getRandomSalt();
180 return self
::getSaltedHash(self
::getSaltedHash($password, $salt), $salt);
184 * Returns a simple salted bcrypt hash.
186 * @param string $password
187 * @param string $salt
190 public static function getSaltedHash($password, $salt = null) {
191 if ($salt === null) {
192 $salt = self
::getRandomSalt();
195 return crypt($password, $salt);
199 * Returns a random blowfish-compatible salt.
203 public static function getRandomSalt() {
206 for ($i = 0, $maxIndex = (strlen(self
::$blowfishCharacters) - 1); $i < 22; $i++
) {
207 $salt .= self
::$blowfishCharacters[self
::secureRandomNumber(0, $maxIndex)];
210 return self
::getSalt($salt);
214 * Generates a random alphanumeric user password with the given character length.
216 * @param integer $length
219 public static function getRandomPassword($length = 12) {
220 $charset = self
::PASSWORD_CHARSET
;
223 for ($i = 0, $maxIndex = (strlen($charset) - 1); $i < $length; $i++
) {
224 $password .= $charset[self
::secureRandomNumber(0, $maxIndex)];
231 * Compares two strings in a constant time manner.
232 * This function effectively is a polyfill for the PHP 5.6 `hash_equals`.
234 * @param string $hash1
235 * @param string $hash2
237 * @deprecated Use \wcf\util\CryptoUtil::secureCompare()
239 public static function secureCompare($hash1, $hash2) {
240 return \
hash_equals($hash1, $hash2);
244 * @deprecated Use random_int()
246 public static function secureRandomNumber($min, $max) {
247 $range = $max - $min;
250 throw new SystemException("Cannot generate a secure random number, min and max are the same");
254 return CryptoUtil
::randomInt($min, $max);
256 catch (CryptoException
$e) {
257 // Backwards compatibility: This function never did throw.
258 return mt_rand($min, $max);
263 * Returns a blowfish salt, e.g. $2a$07$usesomesillystringforsalt$
265 * @param string $salt
268 protected static function getSalt($salt) {
269 $salt = mb_substr($salt, 0, 22);
271 return '$' . self
::BCRYPT_TYPE
. '$' . self
::BCRYPT_COST
. '$' . $salt;
275 * Validates the password hash for Invision Power Board 2.x (ipb2).
277 * @param string $username
278 * @param string $password
279 * @param string $salt
280 * @param string $dbHash
283 protected static function ipb2($username, $password, $salt, $dbHash) {
284 return self
::vb3($username, $password, $salt, $dbHash);
288 * Validates the password hash for Invision Power Board 3.x (ipb3).
290 * @param string $username
291 * @param string $password
292 * @param string $salt
293 * @param string $dbHash
296 protected static function ipb3($username, $password, $salt, $dbHash) {
297 return \
hash_equals($dbHash, md5(md5($salt) . md5($password)));
301 * Validates the password hash for MyBB 1.x (mybb1).
303 * @param string $username
304 * @param string $password
305 * @param string $salt
306 * @param string $dbHash
309 protected static function mybb1($username, $password, $salt, $dbHash) {
310 return \
hash_equals($dbHash, md5(md5($salt) . md5($password)));
313 * Validates the password hash for phpBB 3.x (phpbb3).
315 * @param string $username
316 * @param string $password
317 * @param string $salt
318 * @param string $dbHash
321 protected static function phpbb3($username, $password, $salt, $dbHash) {
322 return self
::phpass($username, $password, $salt, $dbHash);
326 * Validates the password hash for phpass portable hashes (phpass).
328 * @param string $username
329 * @param string $password
330 * @param string $salt
331 * @param string $dbHash
334 protected static function phpass($username, $password, $salt, $dbHash) {
335 if (mb_strlen($dbHash) !== 34) {
336 return \
hash_equals(md5($password), $dbHash);
339 $hash_crypt_private = function ($password, $setting) {
340 static $itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
344 // Check for correct hash
345 if (substr($setting, 0, 3) !== '$H$' && substr($setting, 0, 3) !== '$P$') {
349 $count_log2 = strpos($itoa64, $setting[3]);
351 if ($count_log2 < 7 ||
$count_log2 > 30) {
355 $count = 1 << $count_log2;
356 $salt = substr($setting, 4, 8);
358 if (strlen($salt) != 8) {
362 $hash = md5($salt . $password, true);
364 $hash = md5($hash . $password, true);
368 $output = substr($setting, 0, 12);
369 $hash_encode64 = function ($input, $count, &$itoa64) {
374 $value = ord($input[$i++
]);
375 $output .= $itoa64[$value & 0x3f];
378 $value |
= ord($input[$i]) << 8;
381 $output .= $itoa64[($value >> 6) & 0x3f];
383 if ($i++
>= $count) {
388 $value |
= ord($input[$i]) << 16;
391 $output .= $itoa64[($value >> 12) & 0x3f];
393 if ($i++
>= $count) {
397 $output .= $itoa64[($value >> 18) & 0x3f];
404 $output .= $hash_encode64($hash, 16, $itoa64);
409 return \
hash_equals($hash_crypt_private($password, $dbHash), $dbHash);
413 * Validates the password hash for Simple Machines Forums 1.x (smf1).
415 * @param string $username
416 * @param string $password
417 * @param string $salt
418 * @param string $dbHash
421 protected static function smf1($username, $password, $salt, $dbHash) {
422 return \
hash_equals($dbHash, sha1(mb_strtolower($username) . $password));
426 * Validates the password hash for Simple Machines Forums 2.x (smf2).
428 * @param string $username
429 * @param string $password
430 * @param string $salt
431 * @param string $dbHash
434 protected static function smf2($username, $password, $salt, $dbHash) {
435 return self
::smf1($username, $password, $salt, $dbHash);
439 * Validates the password hash for vBulletin 3 (vb3).
441 * @param string $username
442 * @param string $password
443 * @param string $salt
444 * @param string $dbHash
447 protected static function vb3($username, $password, $salt, $dbHash) {
448 return \
hash_equals($dbHash, md5(md5($password) . $salt));
452 * Validates the password hash for vBulletin 4 (vb4).
454 * @param string $username
455 * @param string $password
456 * @param string $salt
457 * @param string $dbHash
460 protected static function vb4($username, $password, $salt, $dbHash) {
461 return self
::vb3($username, $password, $salt, $dbHash);
465 * Validates the password hash for vBulletin 5 (vb5).
467 * @param string $username
468 * @param string $password
469 * @param string $salt
470 * @param string $dbHash
473 protected static function vb5($username, $password, $salt, $dbHash) {
474 return self
::vb3($username, $password, $salt, $dbHash);
478 * Validates the password hash for WoltLab Burning Board 2 (wbb2).
480 * @param string $username
481 * @param string $password
482 * @param string $salt
483 * @param string $dbHash
486 protected static function wbb2($username, $password, $salt, $dbHash) {
487 if (\
hash_equals($dbHash, md5($password))) {
490 else if (\
hash_equals($dbHash, sha1($password))) {
498 * Validates the password hash for WoltLab Community Framework 1.x (wcf1).
500 * @param string $username
501 * @param string $password
502 * @param string $salt
503 * @param string $dbHash
506 protected static function wcf1($username, $password, $salt, $dbHash) {
507 return \
hash_equals($dbHash, sha1($salt . sha1($salt . sha1($password))));
511 * Validates the password hash for WoltLab Community Framework 1.x with different encryption (wcf1e).
513 * @param string $type
514 * @param string $password
515 * @param string $salt
516 * @param string $dbHash
519 protected static function wcf1e($type, $password, $salt, $dbHash) {
520 preg_match('~^wcf1e([cms])([01])([ab])([01])$~', $type, $matches);
521 $enableSalting = $matches[2];
522 $saltPosition = $matches[3];
523 $encryptBeforeSalting = $matches[4];
525 $encryptionMethod = '';
526 switch ($matches[1]) {
528 $encryptionMethod = 'crc32';
532 $encryptionMethod = 'md5';
536 $encryptionMethod = 'sha1';
541 if ($enableSalting) {
542 if ($saltPosition == 'b') {
546 if ($encryptBeforeSalting) {
547 $hash .= $encryptionMethod($password);
553 if ($saltPosition == 'a') {
557 $hash = $encryptionMethod($hash);
560 $hash = $encryptionMethod($password);
562 $hash = $encryptionMethod($salt . $hash);
564 return \
hash_equals($dbHash, $hash);
568 * Validates the password hash for Woltlab Suite 3.x / WoltLab Community Framework 2.x (wcf2).
570 * @param string $username
571 * @param string $password
572 * @param string $salt
573 * @param string $dbHash
576 protected static function wcf2($username, $password, $salt, $dbHash) {
577 return \
hash_equals($dbHash, self
::getDoubleSaltedHash($password, $dbHash));
581 * Validates the password hash for XenForo 1.0 / 1.1 (xf1).
583 * @param string $username
584 * @param string $password
585 * @param string $salt
586 * @param string $dbHash
589 protected static function xf1($username, $password, $salt, $dbHash) {
590 if (\
hash_equals($dbHash, sha1(sha1($password) . $salt))) {
594 return \
hash_equals($dbHash, hash('sha256', hash('sha256', $password) . $salt));
598 * Validates the password hash for XenForo 1.2+ (xf12).
600 * @param string $username
601 * @param string $password
602 * @param string $salt
603 * @param string $dbHash
606 protected static function xf12($username, $password, $salt, $dbHash) {
607 if (\
hash_equals($dbHash, self
::getSaltedHash($password, $dbHash))) {
615 * Validates the password hash for Joomla 1.x (kunea)
617 * @param string $username
618 * @param string $password
619 * @param string $salt
620 * @param string $dbHash
623 protected static function joomla1($username, $password, $salt, $dbHash) {
624 if (\
hash_equals($dbHash, md5($password . $salt))) {
632 * Validates the password hash for Joomla 2.x (kunea)
634 * @param string $username
635 * @param string $password
636 * @param string $salt
637 * @param string $dbHash
640 protected static function joomla2($username, $password, $salt, $dbHash) {
641 return self
::joomla1($username, $password, $salt, $dbHash);
645 * Validates the password hash for Joomla 3.x (kunea)
647 * @param string $username
648 * @param string $password
649 * @param string $salt
650 * @param string $dbHash
653 protected static function joomla3($username, $password, $salt, $dbHash) {
654 return self
::joomla1($username, $password, $salt, $dbHash);
658 * Validates the password hash for phpFox 3.x
659 * Merge phpfox_user.password and phpfox_user.password_salt with ':' before importing all data row values
660 * See PasswordUtil::checkPassword() for more info
662 * @param string $username
663 * @param string $password
664 * @param string $salt
665 * @param string $dbHash
668 protected static function phpfox3($username, $password, $salt, $dbHash) {
669 if (\
hash_equals($dbHash, md5(md5($password) . md5($salt)))) {
677 * Validates the password hash for MD5 mode of crypt()
679 * @param string $username
680 * @param string $password
681 * @param string $salt
682 * @param string $dbHash
685 protected static function cryptMD5($username, $password, $salt, $dbHash) {
686 if (\
hash_equals($dbHash, self
::getSaltedHash($password, $dbHash))) {
696 * @param string $username
697 * @param string $password
698 * @param string $salt
699 * @param string $dbHash
702 protected static function invalid($username, $password, $salt, $dbHash) {
707 * Forbid creation of PasswordUtil objects.
709 private function __construct() {