Merge branch '5.2' into 5.3
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / util / PasswordUtil.class.php
index 49d488c7671b19836c9e47f231b2c1014899a4dd..806712476ebadf1b8fbc25c525da7b167c6ad69e 100644 (file)
@@ -8,7 +8,7 @@ use wcf\util\exception\CryptoException;
  * Provides functions to compute password hashes.
  * 
  * @author     Alexander Ebert
- * @copyright  2001-2018 WoltLab GmbH
+ * @copyright  2001-2019 WoltLab GmbH
  * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
  * @package    WoltLabSuite\Core\Util
  */
@@ -30,6 +30,7 @@ final class PasswordUtil {
         * @var string[]
         */
        private static $supportedEncryptionTypes = [
+               'argon2',       // vBulletin 5.x
                'ipb2',         // Invision Power Board 2.x
                'ipb3',         // Invision Power Board 3.x
                'mybb1',        // MyBB 1.x
@@ -237,17 +238,11 @@ final class PasswordUtil {
         * @deprecated  Use \wcf\util\CryptoUtil::secureCompare()
         */
        public static function secureCompare($hash1, $hash2) {
-               return CryptoUtil::secureCompare($hash1, $hash2);
+               return \hash_equals($hash1, $hash2);
        }
        
        /**
-        * Generates secure random numbers using OpenSSL.
-        * 
-        * @see         http://de1.php.net/manual/en/function.openssl-random-pseudo-bytes.php#104322
-        * @param       integer         $min
-        * @param       integer         $max
-        * @return      integer
-        * @throws      SystemException
+        * @deprecated  Use random_int()
         */
        public static function secureRandomNumber($min, $max) {
                $range = $max - $min;
@@ -278,7 +273,7 @@ final class PasswordUtil {
        }
        
        /**
-        * Validates the password hash for Invision Power Board 2.x (ipb2).
+        * Validates the password hash for Argon2 (argon2).
         * 
         * @param       string          $username
         * @param       string          $password
@@ -286,12 +281,12 @@ final class PasswordUtil {
         * @param       string          $dbHash
         * @return      boolean
         */
-       protected static function ipb2($username, $password, $salt, $dbHash) {
-               return self::vb3($username, $password, $salt, $dbHash);
+       protected static function argon2($username, $password, $salt, $dbHash) {
+               return password_verify($password, $dbHash);
        }
        
        /**
-        * Validates the password hash for Invision Power Board 3.x (ipb3).
+        * Validates the password hash for Invision Power Board 2.x (ipb2).
         * 
         * @param       string          $username
         * @param       string          $password
@@ -299,12 +294,12 @@ final class PasswordUtil {
         * @param       string          $dbHash
         * @return      boolean
         */
-       protected static function ipb3($username, $password, $salt, $dbHash) {
-               return CryptoUtil::secureCompare($dbHash, md5(md5($salt) . md5($password)));
+       protected static function ipb2($username, $password, $salt, $dbHash) {
+               return self::vb3($username, $password, $salt, $dbHash);
        }
        
        /**
-        * Validates the password hash for MyBB 1.x (mybb1).
+        * Validates the password hash for Invision Power Board 3.x (ipb3).
         * 
         * @param       string          $username
         * @param       string          $password
@@ -312,11 +307,12 @@ final class PasswordUtil {
         * @param       string          $dbHash
         * @return      boolean
         */
-       protected static function mybb1($username, $password, $salt, $dbHash) {
-               return CryptoUtil::secureCompare($dbHash, md5(md5($salt) . md5($password)));
+       protected static function ipb3($username, $password, $salt, $dbHash) {
+               return \hash_equals($dbHash, md5(md5($salt) . md5($password)));
        }
+       
        /**
-        * Validates the password hash for phpBB 3.x (phpbb3).
+        * Validates the password hash for MyBB 1.x (mybb1).
         * 
         * @param       string          $username
         * @param       string          $password
@@ -324,12 +320,11 @@ final class PasswordUtil {
         * @param       string          $dbHash
         * @return      boolean
         */
-       protected static function phpbb3($username, $password, $salt, $dbHash) {
-               return self::phpass($username, $password, $salt, $dbHash);
+       protected static function mybb1($username, $password, $salt, $dbHash) {
+               return \hash_equals($dbHash, md5(md5($salt) . md5($password)));
        }
-       
        /**
-        * Validates the password hash for phpass portable hashes (phpass).
+        * Validates the password hash for phpBB 3.x (phpbb3).
         * 
         * @param       string          $username
         * @param       string          $password
@@ -337,82 +332,134 @@ final class PasswordUtil {
         * @param       string          $dbHash
         * @return      boolean
         */
-       protected static function phpass($username, $password, $salt, $dbHash) {
-               if (mb_strlen($dbHash) !== 34) {
-                       return CryptoUtil::secureCompare(md5($password), $dbHash);
+       public static function phpbb3($username, $password, $salt, $dbHash) {
+               $phpassResult = self::phpass($username, $password, $salt, $dbHash);
+               
+               if ($phpassResult) {
+                       return true;
                }
                
-               $hash_crypt_private = function ($password, $setting) {
-                       static $itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
-                       
-                       $output = '*';
-                       
-                       // Check for correct hash
-                       if (substr($setting, 0, 3) !== '$H$' && substr($setting, 0, 3) !== '$P$') {
-                               return $output;
-                       }
-                       
-                       $count_log2 = strpos($itoa64, $setting[3]);
-                       
-                       if ($count_log2 < 7 || $count_log2 > 30) {
-                               return $output;
+               if (!preg_match('/^\$([^$]+)\$/', $dbHash, $matches)) {
+                       return false;
+               }
+               
+               $algorithms = explode('\\', $matches[1]);
+               // Strip the type prefix.
+               $dbHash = substr($dbHash, strlen($matches[0]));
+               
+               // The following loop only supports the multi-hash variant.
+               // Everything else should already be handled at this point.
+               if (count($algorithms) == 1) {
+                       return false;
+               }
+               foreach ($algorithms as $algorithm) {
+                       $dollar = strpos($dbHash, '$');
+                       if ($dollar === false) {
+                               return false;
                        }
+                       $settings = '$'.$algorithm.'$'.str_replace('\\', '$', substr($dbHash, 0, $dollar));
+                       $dbHash = substr($dbHash, $dollar + 1);
                        
-                       $count = 1 << $count_log2;
-                       $salt = substr($setting, 4, 8);
-                       
-                       if (strlen($salt) != 8) {
-                               return $output;
+                       switch ($algorithm) {
+                               case 'H':
+                               case 'P':
+                                       $password = str_replace($settings, '', self::phpass_hash_crypt_private($password, $settings));
+                               break;
+                               case '2a':
+                               case '2y':
+                                       $password = str_replace($settings, '', self::getSaltedHash($password, $settings));
+                               break;
                        }
+               }
+               
+               return \hash_equals($dbHash, $password);
+       }
+       
+       private static function phpass_hash_crypt_private($password, $setting) {
+               static $itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
+               
+               $output = '*';
+               
+               // Check for correct hash
+               if (substr($setting, 0, 3) !== '$H$' && substr($setting, 0, 3) !== '$P$') {
+                       return $output;
+               }
+               
+               $count_log2 = strpos($itoa64, $setting[3]);
+               
+               if ($count_log2 < 7 || $count_log2 > 30) {
+                       return $output;
+               }
+               
+               $count = 1 << $count_log2;
+               $salt = substr($setting, 4, 8);
+               
+               if (strlen($salt) != 8) {
+                       return $output;
+               }
+               
+               $hash = md5($salt . $password, true);
+               do {
+                       $hash = md5($hash . $password, true);
+               }
+               while (--$count);
+               
+               $output = substr($setting, 0, 12);
+               $hash_encode64 = function ($input, $count, &$itoa64) {
+                       $output = '';
+                       $i = 0;
                        
-                       $hash = md5($salt . $password, true);
                        do {
-                               $hash = md5($hash . $password, true);
-                       }
-                       while (--$count);
-                       
-                       $output = substr($setting, 0, 12);
-                       $hash_encode64 = function ($input, $count, &$itoa64) {
-                               $output = '';
-                               $i = 0;
+                               $value = ord($input[$i++]);
+                               $output .= $itoa64[$value & 0x3f];
                                
-                               do {
-                                       $value = ord($input[$i++]);
-                                       $output .= $itoa64[$value & 0x3f];
-                                       
-                                       if ($i < $count) {
-                                               $value |= ord($input[$i]) << 8;
-                                       }
-                                       
-                                       $output .= $itoa64[($value >> 6) & 0x3f];
-                                       
-                                       if ($i++ >= $count) {
-                                               break;
-                                       }
-                                       
-                                       if ($i < $count) {
-                                               $value |= ord($input[$i]) << 16;
-                                       }
-                                       
-                                       $output .= $itoa64[($value >> 12) & 0x3f];
-                                       
-                                       if ($i++ >= $count) {
-                                               break;
-                                       }
-                                       
-                                       $output .= $itoa64[($value >> 18) & 0x3f];
+                               if ($i < $count) {
+                                       $value |= ord($input[$i]) << 8;
                                }
-                               while ($i < $count);
                                
-                               return $output;
-                       };
-                       
-                       $output .= $hash_encode64($hash, 16, $itoa64);
+                               $output .= $itoa64[($value >> 6) & 0x3f];
+                               
+                               if ($i++ >= $count) {
+                                       break;
+                               }
+                               
+                               if ($i < $count) {
+                                       $value |= ord($input[$i]) << 16;
+                               }
+                               
+                               $output .= $itoa64[($value >> 12) & 0x3f];
+                               
+                               if ($i++ >= $count) {
+                                       break;
+                               }
+                               
+                               $output .= $itoa64[($value >> 18) & 0x3f];
+                       }
+                       while ($i < $count);
                        
                        return $output;
                };
                
-               return CryptoUtil::secureCompare($hash_crypt_private($password, $dbHash), $dbHash);
+               $output .= $hash_encode64($hash, 16, $itoa64);
+               
+               return $output;
+       }
+       
+       /**
+        * Validates the password hash for phpass portable hashes (phpass).
+        * 
+        * @param       string          $username
+        * @param       string          $password
+        * @param       string          $salt
+        * @param       string          $dbHash
+        * @return      boolean
+        */
+       protected static function phpass($username, $password, $salt, $dbHash) {
+               if (mb_strlen($dbHash) !== 34) {
+                       return \hash_equals(md5($password), $dbHash);
+               }
+               
+               return \hash_equals(self::phpass_hash_crypt_private($password, $dbHash), $dbHash);
        }
        
        /**
@@ -425,7 +472,7 @@ final class PasswordUtil {
         * @return      boolean
         */
        protected static function smf1($username, $password, $salt, $dbHash) {
-               return CryptoUtil::secureCompare($dbHash, sha1(mb_strtolower($username) . $password));
+               return \hash_equals($dbHash, sha1(mb_strtolower($username) . $password));
        }
        
        /**
@@ -451,7 +498,7 @@ final class PasswordUtil {
         * @return      boolean
         */
        protected static function vb3($username, $password, $salt, $dbHash) {
-               return CryptoUtil::secureCompare($dbHash, md5(md5($password) . $salt));
+               return \hash_equals($dbHash, md5(md5($password) . $salt));
        }
        
        /**
@@ -490,10 +537,10 @@ final class PasswordUtil {
         * @return      boolean
         */
        protected static function wbb2($username, $password, $salt, $dbHash) {
-               if (CryptoUtil::secureCompare($dbHash, md5($password))) {
+               if (\hash_equals($dbHash, md5($password))) {
                        return true;
                }
-               else if (CryptoUtil::secureCompare($dbHash, sha1($password))) {
+               else if (\hash_equals($dbHash, sha1($password))) {
                        return true;
                }
                
@@ -510,7 +557,7 @@ final class PasswordUtil {
         * @return      boolean
         */
        protected static function wcf1($username, $password, $salt, $dbHash) {
-               return CryptoUtil::secureCompare($dbHash, sha1($salt . sha1($salt . sha1($password))));
+               return \hash_equals($dbHash, sha1($salt . sha1($salt . sha1($password))));
        }
        
        /**
@@ -567,7 +614,7 @@ final class PasswordUtil {
                }
                $hash = $encryptionMethod($salt . $hash);
                
-               return CryptoUtil::secureCompare($dbHash, $hash);
+               return \hash_equals($dbHash, $hash);
        }
        
        /**
@@ -580,7 +627,7 @@ final class PasswordUtil {
         * @return      boolean
         */
        protected static function wcf2($username, $password, $salt, $dbHash) {
-               return CryptoUtil::secureCompare($dbHash, self::getDoubleSaltedHash($password, $dbHash));
+               return \hash_equals($dbHash, self::getDoubleSaltedHash($password, $dbHash));
        }
        
        /**
@@ -593,11 +640,11 @@ final class PasswordUtil {
         * @return      boolean
         */
        protected static function xf1($username, $password, $salt, $dbHash) {
-               if (CryptoUtil::secureCompare($dbHash, sha1(sha1($password) . $salt))) {
+               if (\hash_equals($dbHash, sha1(sha1($password) . $salt))) {
                        return true;
                }
                
-               return CryptoUtil::secureCompare($dbHash, hash('sha256', hash('sha256', $password) . $salt));
+               return \hash_equals($dbHash, hash('sha256', hash('sha256', $password) . $salt));
        }
        
        /**
@@ -610,7 +657,7 @@ final class PasswordUtil {
         * @return      boolean
         */
        protected static function xf12($username, $password, $salt, $dbHash) {
-               if (CryptoUtil::secureCompare($dbHash, self::getSaltedHash($password, $dbHash))) {
+               if (\hash_equals($dbHash, self::getSaltedHash($password, $dbHash))) {
                        return true;
                }
                
@@ -627,7 +674,7 @@ final class PasswordUtil {
         * @return      boolean
         */
        protected static function joomla1($username, $password, $salt, $dbHash) {
-               if (CryptoUtil::secureCompare($dbHash, md5($password . $salt))) {
+               if (\hash_equals($dbHash, md5($password . $salt))) {
                        return true;
                }
                
@@ -672,7 +719,7 @@ final class PasswordUtil {
         * @return      boolean
         */
         protected static function phpfox3($username, $password, $salt, $dbHash) {
-                if (CryptoUtil::secureCompare($dbHash, md5(md5($password) . md5($salt)))) {
+                if (\hash_equals($dbHash, md5(md5($password) . md5($salt)))) {
                         return true;
                 }
                 
@@ -689,7 +736,7 @@ final class PasswordUtil {
         * @return      boolean
         */
        protected static function cryptMD5($username, $password, $salt, $dbHash) {
-               if (CryptoUtil::secureCompare($dbHash, self::getSaltedHash($password, $dbHash))) {
+               if (\hash_equals($dbHash, self::getSaltedHash($password, $dbHash))) {
                        return true;
                }