Merge branch '3.1' into 5.2
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / util / PasswordUtil.class.php
1 <?php
2 namespace wcf\util;
3 use wcf\system\exception\SystemException;
4 use wcf\system\Regex;
5 use wcf\util\exception\CryptoException;
6
7 /**
8 * Provides functions to compute password hashes.
9 *
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
14 */
15 final class PasswordUtil {
16 /**
17 * list of possible characters in generated passwords
18 * @var string
19 */
20 const PASSWORD_CHARSET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
21
22 /**
23 * concated list of valid blowfish salt characters
24 * @var string
25 */
26 private static $blowfishCharacters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789./';
27
28 /**
29 * list of supported encryption type by software identifier
30 * @var string[]
31 */
32 private static $supportedEncryptionTypes = [
33 'ipb2', // Invision Power Board 2.x
34 'ipb3', // Invision Power Board 3.x
35 'mybb1', // MyBB 1.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
52 'cryptMD5',
53 'invalid', // Never going to match anything
54 ];
55
56 /**
57 * blowfish cost factor
58 * @var string
59 */
60 const BCRYPT_COST = '08';
61
62 /**
63 * blowfish encryption type
64 * @var string
65 */
66 const BCRYPT_TYPE = '2a';
67
68 /**
69 * Returns true if given encryption type is supported.
70 *
71 * @param string $type
72 * @return boolean
73 */
74 public static function isSupported($type) {
75 if (in_array($type, self::$supportedEncryptionTypes)) {
76 return true;
77 }
78
79 if (preg_match('~^wcf1e[cms][01][ab][01]$~', $type)) {
80 return true;
81 }
82
83 return false;
84 }
85
86 /**
87 * Returns true if given hash looks like a valid bcrypt hash.
88 *
89 * @param string $hash
90 * @return boolean
91 */
92 public static function isBlowfish($hash) {
93 return (Regex::compile('^\$2[afxy]\$')->match($hash) ? true : false);
94 }
95
96 /**
97 * Returns true if given bcrypt hash uses a different cost factor and should be re-computed.
98 *
99 * @param string $hash
100 * @return boolean
101 */
102 public static function isDifferentBlowfish($hash) {
103 $currentCost = intval(self::BCRYPT_COST);
104 $hashCost = intval(substr($hash, 4, 2));
105
106 if ($currentCost != $hashCost) {
107 return true;
108 }
109
110 return false;
111 }
112
113 /**
114 * Validates password against stored hash, encryption type is automatically resolved.
115 *
116 * @param string $username
117 * @param string $password
118 * @param string $dbHash
119 * @return boolean
120 * @throws SystemException
121 */
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");
126 }
127
128 // drop type from hash
129 $dbHash = substr($dbHash, strlen($type) + 1);
130
131 // check for salt
132 $parts = explode(':', $dbHash, 2);
133 if (count($parts) == 2) {
134 list($dbHash, $salt) = $parts;
135 }
136 else {
137 $dbHash = $parts[0];
138 $salt = '';
139 }
140
141 // compare hash
142 if (in_array($type, self::$supportedEncryptionTypes)) {
143 return call_user_func('\wcf\util\PasswordUtil::'.$type, $username, $password, $salt, $dbHash);
144 }
145 else {
146 // WCF 1.x with different encryption
147 return self::wcf1e($type, $password, $salt, $dbHash);
148 }
149 }
150
151 /**
152 * Returns encryption type if possible.
153 *
154 * @param string $hash
155 * @return string
156 */
157 public static function detectEncryption($hash) {
158 if (($pos = strpos($hash, ':')) !== false) {
159 $type = substr($hash, 0, $pos);
160 if (self::isSupported($type)) {
161 return $type;
162 }
163 }
164
165 return 'unknown';
166 }
167
168 /**
169 * Returns a double salted bcrypt hash.
170 *
171 * @param string $password
172 * @param string $salt
173 * @return string
174 */
175 public static function getDoubleSaltedHash($password, $salt = null) {
176 if ($salt === null) {
177 $salt = self::getRandomSalt();
178 }
179
180 return self::getSaltedHash(self::getSaltedHash($password, $salt), $salt);
181 }
182
183 /**
184 * Returns a simple salted bcrypt hash.
185 *
186 * @param string $password
187 * @param string $salt
188 * @return string
189 */
190 public static function getSaltedHash($password, $salt = null) {
191 if ($salt === null) {
192 $salt = self::getRandomSalt();
193 }
194
195 return crypt($password, $salt);
196 }
197
198 /**
199 * Returns a random blowfish-compatible salt.
200 *
201 * @return string
202 */
203 public static function getRandomSalt() {
204 $salt = '';
205
206 for ($i = 0, $maxIndex = (strlen(self::$blowfishCharacters) - 1); $i < 22; $i++) {
207 $salt .= self::$blowfishCharacters[self::secureRandomNumber(0, $maxIndex)];
208 }
209
210 return self::getSalt($salt);
211 }
212
213 /**
214 * Generates a random alphanumeric user password with the given character length.
215 *
216 * @param integer $length
217 * @return string
218 */
219 public static function getRandomPassword($length = 12) {
220 $charset = self::PASSWORD_CHARSET;
221 $password = '';
222
223 for ($i = 0, $maxIndex = (strlen($charset) - 1); $i < $length; $i++) {
224 $password .= $charset[self::secureRandomNumber(0, $maxIndex)];
225 }
226
227 return $password;
228 }
229
230 /**
231 * Compares two strings in a constant time manner.
232 * This function effectively is a polyfill for the PHP 5.6 `hash_equals`.
233 *
234 * @param string $hash1
235 * @param string $hash2
236 * @return boolean
237 * @deprecated Use \wcf\util\CryptoUtil::secureCompare()
238 */
239 public static function secureCompare($hash1, $hash2) {
240 return \hash_equals($hash1, $hash2);
241 }
242
243 /**
244 * @deprecated Use random_int()
245 */
246 public static function secureRandomNumber($min, $max) {
247 $range = $max - $min;
248 if ($range == 0) {
249 // not random
250 throw new SystemException("Cannot generate a secure random number, min and max are the same");
251 }
252
253 try {
254 return CryptoUtil::randomInt($min, $max);
255 }
256 catch (CryptoException $e) {
257 // Backwards compatibility: This function never did throw.
258 return mt_rand($min, $max);
259 }
260 }
261
262 /**
263 * Returns a blowfish salt, e.g. $2a$07$usesomesillystringforsalt$
264 *
265 * @param string $salt
266 * @return string
267 */
268 protected static function getSalt($salt) {
269 $salt = mb_substr($salt, 0, 22);
270
271 return '$' . self::BCRYPT_TYPE . '$' . self::BCRYPT_COST . '$' . $salt;
272 }
273
274 /**
275 * Validates the password hash for Invision Power Board 2.x (ipb2).
276 *
277 * @param string $username
278 * @param string $password
279 * @param string $salt
280 * @param string $dbHash
281 * @return boolean
282 */
283 protected static function ipb2($username, $password, $salt, $dbHash) {
284 return self::vb3($username, $password, $salt, $dbHash);
285 }
286
287 /**
288 * Validates the password hash for Invision Power Board 3.x (ipb3).
289 *
290 * @param string $username
291 * @param string $password
292 * @param string $salt
293 * @param string $dbHash
294 * @return boolean
295 */
296 protected static function ipb3($username, $password, $salt, $dbHash) {
297 return \hash_equals($dbHash, md5(md5($salt) . md5($password)));
298 }
299
300 /**
301 * Validates the password hash for MyBB 1.x (mybb1).
302 *
303 * @param string $username
304 * @param string $password
305 * @param string $salt
306 * @param string $dbHash
307 * @return boolean
308 */
309 protected static function mybb1($username, $password, $salt, $dbHash) {
310 return \hash_equals($dbHash, md5(md5($salt) . md5($password)));
311 }
312 /**
313 * Validates the password hash for phpBB 3.x (phpbb3).
314 *
315 * @param string $username
316 * @param string $password
317 * @param string $salt
318 * @param string $dbHash
319 * @return boolean
320 */
321 protected static function phpbb3($username, $password, $salt, $dbHash) {
322 return self::phpass($username, $password, $salt, $dbHash);
323 }
324
325 /**
326 * Validates the password hash for phpass portable hashes (phpass).
327 *
328 * @param string $username
329 * @param string $password
330 * @param string $salt
331 * @param string $dbHash
332 * @return boolean
333 */
334 protected static function phpass($username, $password, $salt, $dbHash) {
335 if (mb_strlen($dbHash) !== 34) {
336 return \hash_equals(md5($password), $dbHash);
337 }
338
339 $hash_crypt_private = function ($password, $setting) {
340 static $itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
341
342 $output = '*';
343
344 // Check for correct hash
345 if (substr($setting, 0, 3) !== '$H$' && substr($setting, 0, 3) !== '$P$') {
346 return $output;
347 }
348
349 $count_log2 = strpos($itoa64, $setting[3]);
350
351 if ($count_log2 < 7 || $count_log2 > 30) {
352 return $output;
353 }
354
355 $count = 1 << $count_log2;
356 $salt = substr($setting, 4, 8);
357
358 if (strlen($salt) != 8) {
359 return $output;
360 }
361
362 $hash = md5($salt . $password, true);
363 do {
364 $hash = md5($hash . $password, true);
365 }
366 while (--$count);
367
368 $output = substr($setting, 0, 12);
369 $hash_encode64 = function ($input, $count, &$itoa64) {
370 $output = '';
371 $i = 0;
372
373 do {
374 $value = ord($input[$i++]);
375 $output .= $itoa64[$value & 0x3f];
376
377 if ($i < $count) {
378 $value |= ord($input[$i]) << 8;
379 }
380
381 $output .= $itoa64[($value >> 6) & 0x3f];
382
383 if ($i++ >= $count) {
384 break;
385 }
386
387 if ($i < $count) {
388 $value |= ord($input[$i]) << 16;
389 }
390
391 $output .= $itoa64[($value >> 12) & 0x3f];
392
393 if ($i++ >= $count) {
394 break;
395 }
396
397 $output .= $itoa64[($value >> 18) & 0x3f];
398 }
399 while ($i < $count);
400
401 return $output;
402 };
403
404 $output .= $hash_encode64($hash, 16, $itoa64);
405
406 return $output;
407 };
408
409 return \hash_equals($hash_crypt_private($password, $dbHash), $dbHash);
410 }
411
412 /**
413 * Validates the password hash for Simple Machines Forums 1.x (smf1).
414 *
415 * @param string $username
416 * @param string $password
417 * @param string $salt
418 * @param string $dbHash
419 * @return boolean
420 */
421 protected static function smf1($username, $password, $salt, $dbHash) {
422 return \hash_equals($dbHash, sha1(mb_strtolower($username) . $password));
423 }
424
425 /**
426 * Validates the password hash for Simple Machines Forums 2.x (smf2).
427 *
428 * @param string $username
429 * @param string $password
430 * @param string $salt
431 * @param string $dbHash
432 * @return boolean
433 */
434 protected static function smf2($username, $password, $salt, $dbHash) {
435 return self::smf1($username, $password, $salt, $dbHash);
436 }
437
438 /**
439 * Validates the password hash for vBulletin 3 (vb3).
440 *
441 * @param string $username
442 * @param string $password
443 * @param string $salt
444 * @param string $dbHash
445 * @return boolean
446 */
447 protected static function vb3($username, $password, $salt, $dbHash) {
448 return \hash_equals($dbHash, md5(md5($password) . $salt));
449 }
450
451 /**
452 * Validates the password hash for vBulletin 4 (vb4).
453 *
454 * @param string $username
455 * @param string $password
456 * @param string $salt
457 * @param string $dbHash
458 * @return boolean
459 */
460 protected static function vb4($username, $password, $salt, $dbHash) {
461 return self::vb3($username, $password, $salt, $dbHash);
462 }
463
464 /**
465 * Validates the password hash for vBulletin 5 (vb5).
466 *
467 * @param string $username
468 * @param string $password
469 * @param string $salt
470 * @param string $dbHash
471 * @return boolean
472 */
473 protected static function vb5($username, $password, $salt, $dbHash) {
474 return self::vb3($username, $password, $salt, $dbHash);
475 }
476
477 /**
478 * Validates the password hash for WoltLab Burning Board 2 (wbb2).
479 *
480 * @param string $username
481 * @param string $password
482 * @param string $salt
483 * @param string $dbHash
484 * @return boolean
485 */
486 protected static function wbb2($username, $password, $salt, $dbHash) {
487 if (\hash_equals($dbHash, md5($password))) {
488 return true;
489 }
490 else if (\hash_equals($dbHash, sha1($password))) {
491 return true;
492 }
493
494 return false;
495 }
496
497 /**
498 * Validates the password hash for WoltLab Community Framework 1.x (wcf1).
499 *
500 * @param string $username
501 * @param string $password
502 * @param string $salt
503 * @param string $dbHash
504 * @return boolean
505 */
506 protected static function wcf1($username, $password, $salt, $dbHash) {
507 return \hash_equals($dbHash, sha1($salt . sha1($salt . sha1($password))));
508 }
509
510 /**
511 * Validates the password hash for WoltLab Community Framework 1.x with different encryption (wcf1e).
512 *
513 * @param string $type
514 * @param string $password
515 * @param string $salt
516 * @param string $dbHash
517 * @return boolean
518 */
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];
524
525 $encryptionMethod = '';
526 switch ($matches[1]) {
527 case 'c':
528 $encryptionMethod = 'crc32';
529 break;
530
531 case 'm':
532 $encryptionMethod = 'md5';
533 break;
534
535 case 's':
536 $encryptionMethod = 'sha1';
537 break;
538 }
539
540 $hash = '';
541 if ($enableSalting) {
542 if ($saltPosition == 'b') {
543 $hash .= $salt;
544 }
545
546 if ($encryptBeforeSalting) {
547 $hash .= $encryptionMethod($password);
548 }
549 else {
550 $hash .= $password;
551 }
552
553 if ($saltPosition == 'a') {
554 $hash .= $salt;
555 }
556
557 $hash = $encryptionMethod($hash);
558 }
559 else {
560 $hash = $encryptionMethod($password);
561 }
562 $hash = $encryptionMethod($salt . $hash);
563
564 return \hash_equals($dbHash, $hash);
565 }
566
567 /**
568 * Validates the password hash for Woltlab Suite 3.x / WoltLab Community Framework 2.x (wcf2).
569 *
570 * @param string $username
571 * @param string $password
572 * @param string $salt
573 * @param string $dbHash
574 * @return boolean
575 */
576 protected static function wcf2($username, $password, $salt, $dbHash) {
577 return \hash_equals($dbHash, self::getDoubleSaltedHash($password, $dbHash));
578 }
579
580 /**
581 * Validates the password hash for XenForo 1.0 / 1.1 (xf1).
582 *
583 * @param string $username
584 * @param string $password
585 * @param string $salt
586 * @param string $dbHash
587 * @return boolean
588 */
589 protected static function xf1($username, $password, $salt, $dbHash) {
590 if (\hash_equals($dbHash, sha1(sha1($password) . $salt))) {
591 return true;
592 }
593
594 return \hash_equals($dbHash, hash('sha256', hash('sha256', $password) . $salt));
595 }
596
597 /**
598 * Validates the password hash for XenForo 1.2+ (xf12).
599 *
600 * @param string $username
601 * @param string $password
602 * @param string $salt
603 * @param string $dbHash
604 * @return boolean
605 */
606 protected static function xf12($username, $password, $salt, $dbHash) {
607 if (\hash_equals($dbHash, self::getSaltedHash($password, $dbHash))) {
608 return true;
609 }
610
611 return false;
612 }
613
614 /**
615 * Validates the password hash for Joomla 1.x (kunea)
616 *
617 * @param string $username
618 * @param string $password
619 * @param string $salt
620 * @param string $dbHash
621 * @return boolean
622 */
623 protected static function joomla1($username, $password, $salt, $dbHash) {
624 if (\hash_equals($dbHash, md5($password . $salt))) {
625 return true;
626 }
627
628 return false;
629 }
630
631 /**
632 * Validates the password hash for Joomla 2.x (kunea)
633 *
634 * @param string $username
635 * @param string $password
636 * @param string $salt
637 * @param string $dbHash
638 * @return boolean
639 */
640 protected static function joomla2($username, $password, $salt, $dbHash) {
641 return self::joomla1($username, $password, $salt, $dbHash);
642 }
643
644 /**
645 * Validates the password hash for Joomla 3.x (kunea)
646 *
647 * @param string $username
648 * @param string $password
649 * @param string $salt
650 * @param string $dbHash
651 * @return boolean
652 */
653 protected static function joomla3($username, $password, $salt, $dbHash) {
654 return self::joomla1($username, $password, $salt, $dbHash);
655 }
656
657 /**
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
661 *
662 * @param string $username
663 * @param string $password
664 * @param string $salt
665 * @param string $dbHash
666 * @return boolean
667 */
668 protected static function phpfox3($username, $password, $salt, $dbHash) {
669 if (\hash_equals($dbHash, md5(md5($password) . md5($salt)))) {
670 return true;
671 }
672
673 return false;
674 }
675
676 /**
677 * Validates the password hash for MD5 mode of crypt()
678 *
679 * @param string $username
680 * @param string $password
681 * @param string $salt
682 * @param string $dbHash
683 * @return boolean
684 */
685 protected static function cryptMD5($username, $password, $salt, $dbHash) {
686 if (\hash_equals($dbHash, self::getSaltedHash($password, $dbHash))) {
687 return true;
688 }
689
690 return false;
691 }
692
693 /**
694 * Returns false.
695 *
696 * @param string $username
697 * @param string $password
698 * @param string $salt
699 * @param string $dbHash
700 * @return boolean
701 */
702 protected static function invalid($username, $password, $salt, $dbHash) {
703 return false;
704 }
705
706 /**
707 * Forbid creation of PasswordUtil objects.
708 */
709 private function __construct() {
710 // does nothing
711 }
712 }