From 4e273b1f74ccceec7c69c5a91ba15b2864bb36fd Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Thu, 3 Jan 2013 16:10:27 +0100 Subject: [PATCH] Using bcrypt for passwords and added PasswordUtil --- .../lib/acp/form/MasterPasswordForm.class.php | 6 +- .../acp/form/MasterPasswordInitForm.class.php | 9 +- .../files/lib/data/user/User.class.php | 40 +- .../files/lib/data/user/UserEditor.class.php | 13 +- .../DefaultUserAuthentication.class.php | 6 +- .../files/lib/util/PasswordUtil.class.php | 387 ++++++++++++++++++ .../files/lib/util/StringUtil.class.php | 88 ---- wcfsetup/setup/db/install.sql | 3 +- 8 files changed, 441 insertions(+), 111 deletions(-) create mode 100644 wcfsetup/install/files/lib/util/PasswordUtil.class.php diff --git a/wcfsetup/install/files/lib/acp/form/MasterPasswordForm.class.php b/wcfsetup/install/files/lib/acp/form/MasterPasswordForm.class.php index 46059b02e0..23abbf5131 100755 --- a/wcfsetup/install/files/lib/acp/form/MasterPasswordForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/MasterPasswordForm.class.php @@ -5,13 +5,13 @@ use wcf\system\exception\UserInputException; use wcf\system\request\LinkHandler; use wcf\system\WCF; use wcf\util\HeaderUtil; -use wcf\util\StringUtil; +use wcf\util\PasswordUtil; /** * Shows the master password form. * * @author Marcel Werk - * @copyright 2001-2011 WoltLab GmbH + * @copyright 2001-2013 WoltLab GmbH * @license GNU Lesser General Public License * @package com.woltlab.wcf * @subpackage acp.form @@ -62,7 +62,7 @@ class MasterPasswordForm extends AbstractForm { } // check password - if (StringUtil::getSaltedHash($this->masterPassword, MASTER_PASSWORD_SALT) != MASTER_PASSWORD) { + if (PasswordUtil::getSaltedHash($this->masterPassword, MASTER_PASSWORD_SALT) != MASTER_PASSWORD) { throw new UserInputException('masterPassword', 'invalid'); } } diff --git a/wcfsetup/install/files/lib/acp/form/MasterPasswordInitForm.class.php b/wcfsetup/install/files/lib/acp/form/MasterPasswordInitForm.class.php index be25e9e169..b0c31d7e73 100755 --- a/wcfsetup/install/files/lib/acp/form/MasterPasswordInitForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/MasterPasswordInitForm.class.php @@ -6,13 +6,14 @@ use wcf\system\exception\UserInputException; use wcf\system\io\File; use wcf\system\Regex; use wcf\system\WCF; +use wcf\util\PasswordUtil; use wcf\util\StringUtil; /** * Shows the master password init form. * * @author Marcel Werk - * @copyright 2001-2012 WoltLab GmbH + * @copyright 2001-2013 WoltLab GmbH * @license GNU Lesser General Public License * @package com.woltlab.wcf * @subpackage acp.form @@ -112,14 +113,14 @@ class MasterPasswordInitForm extends MasterPasswordForm { */ public function save() { // generate salt - $salt = StringUtil::getRandomID(); + $salt = PasswordUtil::getRandomSalt(); // write master password file $file = new File(WCF_DIR.'acp/masterPassword.inc.php'); $file->write("masterPassword, $salt)."'); +define('MASTER_PASSWORD', '".PasswordUtil::getSaltedHash($this->masterPassword, $salt)."'); define('MASTER_PASSWORD_SALT', '".$salt."'); ?>"); $file->close(); @@ -136,7 +137,7 @@ define('MASTER_PASSWORD_SALT', '".$salt."'); WCF::getTPL()->assign(array( 'confirmMasterPassword' => $this->confirmMasterPassword, - 'exampleMasterPassword' => StringUtil::getRandomPassword(12) + 'exampleMasterPassword' => PasswordUtil::getRandomPassword(12) )); } } diff --git a/wcfsetup/install/files/lib/data/user/User.class.php b/wcfsetup/install/files/lib/data/user/User.class.php index 7642216043..da9e9cc430 100644 --- a/wcfsetup/install/files/lib/data/user/User.class.php +++ b/wcfsetup/install/files/lib/data/user/User.class.php @@ -9,7 +9,7 @@ use wcf\system\language\LanguageFactory; use wcf\system\request\IRouteController; use wcf\system\user\storage\UserStorageHandler; use wcf\system\WCF; -use wcf\util\StringUtil; +use wcf\util\PasswordUtil; /** * Represents a user. @@ -87,7 +87,37 @@ final class User extends DatabaseObject implements IRESTfulResponse, IRouteContr * @return boolean password correct */ public function checkPassword($password) { - return ($this->password == StringUtil::getDoubleSaltedHash($password, $this->salt)); + $isValid = false; + $rebuild = false; + + // check if password is a valid bcrypt hash + if (PasswordUtil::isBlowfish($this->password)) { + if (PasswordUtil::isDifferentBlowfish($this->password)) { + $rebuild = true; + } + + // password is correct + if ($this->password == PasswordUtil::getDoubleSaltedHash($password, $this->password)) { + $isValid = true; + } + } + else { + // different encryption type + if (PasswordUtil::checkPassword($this->username, $password, $this->password)) { + $isValid = true; + $rebuild = true; + } + } + + // create new password hash, either different encryption or different blowfish cost factor + if ($rebuild) { + $userEditor = new UserEditor($this); + $userEditor->update(array( + 'password' => PasswordUtil::getDoubleSaltedHash($password) + )); + } + + return $isValid; } /** @@ -97,7 +127,11 @@ final class User extends DatabaseObject implements IRESTfulResponse, IRouteContr * @return boolean password correct */ public function checkCookiePassword($passwordHash) { - return ($this->password == StringUtil::encrypt($this->salt . $passwordHash)); + if (PasswordUtil::isBlowfish($this->password) && ($this->password == PasswordUtil::getSaltedHash($passwordHash, $this->password))) { + return true; + } + + return false; } /** diff --git a/wcfsetup/install/files/lib/data/user/UserEditor.class.php b/wcfsetup/install/files/lib/data/user/UserEditor.class.php index 76dc1907df..3da6a73d1b 100644 --- a/wcfsetup/install/files/lib/data/user/UserEditor.class.php +++ b/wcfsetup/install/files/lib/data/user/UserEditor.class.php @@ -7,6 +7,7 @@ use wcf\system\clipboard\ClipboardHandler; use wcf\system\language\LanguageFactory; use wcf\system\session\SessionHandler; use wcf\system\WCF; +use wcf\util\PasswordUtil; use wcf\util\StringUtil; /** @@ -30,8 +31,7 @@ class UserEditor extends DatabaseObjectEditor implements IEditableCachedObject { */ public static function create(array $parameters = array()) { // create salt and password hash - $parameters['salt'] = StringUtil::getRandomID(); - $parameters['password'] = StringUtil::getDoubleSaltedHash($parameters['password'], $parameters['salt']); + $parameters['password'] = PasswordUtil::getDoubleSaltedHash($parameters['password']); // create accessToken for AbstractAuthedPage $parameters['accessToken'] = StringUtil::getRandomID(); @@ -63,17 +63,14 @@ class UserEditor extends DatabaseObjectEditor implements IEditableCachedObject { public function update(array $parameters = array()) { // update salt and create new password hash if (isset($parameters['password']) && $parameters['password'] !== '') { - $parameters['salt'] = StringUtil::getRandomID(); - $parameters['password'] = StringUtil::getDoubleSaltedHash($parameters['password'], $parameters['salt']); - + $parameters['password'] = PasswordUtil::getDoubleSaltedHash($parameters['password']); $parameters['accessToken'] = StringUtil::getRandomID(); - // update salt and accessToken - $this->salt = $parameters['salt']; + // update accessToken $this->accessToken = $parameters['accessToken']; } else { - unset($parameters['password'], $parameters['salt'], $parameters['accessToken']); + unset($parameters['password'], $parameters['accessToken']); } parent::update($parameters); diff --git a/wcfsetup/install/files/lib/system/user/authentication/DefaultUserAuthentication.class.php b/wcfsetup/install/files/lib/system/user/authentication/DefaultUserAuthentication.class.php index f0df14ef52..2c3d831c4b 100644 --- a/wcfsetup/install/files/lib/system/user/authentication/DefaultUserAuthentication.class.php +++ b/wcfsetup/install/files/lib/system/user/authentication/DefaultUserAuthentication.class.php @@ -3,13 +3,13 @@ namespace wcf\system\user\authentication; use wcf\data\user\User; use wcf\system\exception\UserInputException; use wcf\util\HeaderUtil; -use wcf\util\StringUtil; +use wcf\util\PasswordUtil; /** * Default user authentication implementation that uses the username to identify users. * * @author Marcel Werk - * @copyright 2001-2012 WoltLab GmbH + * @copyright 2001-2013 WoltLab GmbH * @license GNU Lesser General Public License * @package com.woltlab.wcf * @subpackage system.user.authentication @@ -28,7 +28,7 @@ class DefaultUserAuthentication extends AbstractUserAuthentication { */ public function storeAccessData(User $user, $username, $password) { HeaderUtil::setCookie('userID', $user->userID, TIME_NOW + 365 * 24 * 3600); - HeaderUtil::setCookie('password', StringUtil::getSaltedHash($password, $user->salt), TIME_NOW + 365 * 24 * 3600); + HeaderUtil::setCookie('password', PasswordUtil::getSaltedHash($password, $user->password), TIME_NOW + 365 * 24 * 3600); } /** diff --git a/wcfsetup/install/files/lib/util/PasswordUtil.class.php b/wcfsetup/install/files/lib/util/PasswordUtil.class.php new file mode 100644 index 0000000000..81be45a84a --- /dev/null +++ b/wcfsetup/install/files/lib/util/PasswordUtil.class.php @@ -0,0 +1,387 @@ + + * @package com.woltlab.wcf + * @subpackage util + * @category Community Framework + */ +final class PasswordUtil { + /** + * concated list of valid blowfish salt characters + * @var string + */ + private static $blowfishCharacters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789./'; + + /** + * list of supported encryption type by software identifier + * @var array + */ + private static $supportedEncryptionTypes = array( + 'ipb2', // Invision Power Board 2.x + 'ipb3', // Invision Power Board 3.x + 'smf1', // Simple Machines Forum 1.x + 'smf2', // Simple Machines Forum 2.x + 'vb3', // vBulletin 3.x + 'vb4', // vBulletin 4.x + 'vb5', // vBulletin 5.x + 'wbb2', // WoltLab Burning Board 2.x + 'wcf1', // WoltLab Community Framework 1.x + 'wcf2', // WoltLab Community Framework 2.x + 'xf1' // XenForo 1.x + ); + + /** + * blowfish cost factor + * @var string + */ + const BCRYPT_COST = '08'; + + /** + * blowfish encryption type + * @var string + */ + const BCRYPT_TYPE = '2a'; + + /** + * Returns true, if given encryption type is supported. + * + * @param string $type + * @return boolean + */ + public static function isSupported($type) { + return in_array($type, self::$supportedEncryptionTypes); + } + + /** + * Returns true, if given hash looks like a valid bcrypt hash. + * + * @param string $hash + * @return boolean + */ + public static function isBlowfish($hash) { + return (Regex::compile('^\$2(a|f|x)\$')->match($hash) ? true : false); + } + + /** + * Returns true, if given bcrypt hash uses a different cost factor and should be re-computed. + * + * @param string $hash + * @return boolean + */ + public static function isDifferentBlowfish($hash) { + $currentCost = intval(self::BCRYPT_COST); + $hashCost = intval(substr($hash, 4, 2)); + + if ($currentCost != $hashCost) { + return true; + } + + return false; + } + + /** + * Validates password against stored hash, encryption type is automatically resolved. + * + * @param string $username + * @param string $password + * @param string $dbHash + * @return boolean + */ + public static function checkPassword($username, $password, $dbHash) { + $type = self::detectEncryption($dbHash); + if ($type === 'unknown') { + throw new SystemException("Unable to determine password encryption"); + } + + // drop type from hash + $dbHash = substr($dbHash, strlen($type)); + + // check for salt + $salt = ''; + if (($pos = strrpos($dbHash, ':')) !== false) { + $salt = substr(substr($dbHash, $pos), 1); + $dbHash = substr($dbHash, 1, ($pos - 1)); + } + + // compare hash + return call_user_func('\wcf\util\PasswordUtil::'.$type, $username, $password, $salt, $dbHash); + } + + /** + * Returns encryption type if possible. + * + * @param string $hash + * @return string + */ + public static function detectEncryption($hash) { + if (($pos = strpos($hash, ':')) !== false) { + $type = substr($hash, 0, $pos); + if (in_array($type, self::$supportedEncryptionTypes)) { + return $type; + } + } + + return 'unknown'; + } + + /** + * Returns a double salted bcrypt hash. + * + * @param string $password + * @param string $salt + * @return string + */ + public static function getDoubleSaltedHash($password, $salt = null) { + if ($salt === null) { + $salt = self::getRandomSalt(); + } + + return self::getSaltedHash(self::getSaltedHash($password, $salt), $salt); + } + + /** + * Returns a simple salted bcrypt hash. + * + * @param string $password + * @param string $salt + * @return string + */ + public static function getSaltedHash($password, $salt = null) { + if ($salt === null) { + $salt = self::getRandomSalt(); + } + + return crypt($password, self::getSalt($salt)); + } + + /** + * Returns a random blowfish-compatible salt. + * + * @return string + */ + public static function getRandomSalt() { + $salt = ''; + + for ($i = 0, $maxIndex = (strlen(self::$blowfishCharacters) - 1); $i < 22; $i++) { + $salt .= self::$blowfishCharacters[mt_rand(0, $maxIndex)]; + } + + return $salt; + } + + /** + * Generates a random user password with the given character length. + * + * @param integer $length + * @return string + */ + public static function getRandomPassword($length = 8) { + $availableCharacters = array( + 'abcdefghijklmnopqrstuvwxyz', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + '0123456789', + '+#-.,;:?!' + ); + + $password = ''; + $type = 0; + for ($i = 0; $i < $length; $i++) { + $type = ($i % 4 == 0) ? 0 : ($type + 1); + $password .= substr($availableCharacters[$type], MathUtil::getRandomValue(0, strlen($availableCharacters[$type]) - 1), 1); + } + + return str_shuffle($password); + } + + /** + * Returns a blowfish salt, e.g. $2a$07$usesomesillystringforsalt$ + * + * @param string $salt + * @return string + */ + protected static function getSalt($salt) { + $salt = StringUtil::substring($salt, 0, 22); + + return '$' . self::BCRYPT_TYPE . '$' . self::BCRYPT_COST . '$' . $salt; + } + + /** + * Validates the password hash for Invision Power Board 2.x (ipb2). + * + * @param string $username + * @param string $password + * @param string $salt + * @param string $dbHash + * @return boolean + */ + protected static function ipb2($username, $password, $salt, $dbHash) { + return self::vb3($username, $password, $salt, $dbHash); + } + + /** + * Validates the password hash for Invision Power Board 3.x (ipb3). + * + * @param string $username + * @param string $password + * @param string $salt + * @param string $dbHash + * @return boolean + */ + protected static function ipb3($username, $password, $salt, $dbHash) { + return ($dbHash == md5(md5($salt) . md5($password))); + } + + /** + * Validates the password hash for MyBB 1.x (mybb1). + * + * @param string $username + * @param string $password + * @param string $salt + * @param string $dbHash + * @return boolean + */ + protected static function mybb1($username, $password, $salt, $dbHash) { + return ($dbHash == md5(md5($salt) . md5($password))); + } + + /** + * Validates the password hash for Simple Machines Forums 1.x (smf1). + * + * @param string $username + * @param string $password + * @param string $salt + * @param string $dbHash + * @return boolean + */ + protected static function smf1($username, $password, $salt, $dbHash) { + return ($dbHash == sha1(StringUtil::toLowerCase($username) . $password)); + } + + /** + * Validates the password hash for Simple Machines Forums 2.x (smf2). + * + * @param string $username + * @param string $password + * @param string $salt + * @param string $dbHash + * @return boolean + */ + protected static function smf2($username, $password, $salt, $dbHash) { + return self::smf1($username, $password, $salt, $dbHash); + } + + /** + * Validates the password hash for vBulletin 3 (vb3). + * + * @param string $username + * @param string $password + * @param string $salt + * @param string $dbHash + * @return boolean + */ + protected static function vb3($username, $password, $salt, $dbHash) { + return ($dbHash == md5(md5($password) . $salt)); + } + + /** + * Validates the password hash for vBulletin 4 (vb4). + * + * @param string $username + * @param string $password + * @param string $salt + * @param string $dbHash + * @return boolean + */ + protected static function vb4($username, $password, $salt, $dbHash) { + return self::vb3($username, $password, $salt, $dbHash); + } + + /** + * Validates the password hash for vBulletin 5 (vb5). + * + * @param string $username + * @param string $password + * @param string $salt + * @param string $dbHash + * @return boolean + */ + protected static function vb5($username, $password, $salt, $dbHash) { + return self::vb3($username, $password, $salt, $dbHash); + } + + /** + * Validates the password hash for WoltLab Burning Board 2 (wbb2). + * + * @param string $username + * @param string $password + * @param string $salt + * @param string $dbHash + * @return boolean + */ + protected static function wbb2($username, $password, $salt, $dbHash) { + if ($dbHash == md5($password)) { + return true; + } + else if ($dbHash == sha1($password)) { + return true; + } + + return false; + } + + /** + * Validates the password hash for WoltLab Community Framework 1.x (wcf1). + * + * @param string $username + * @param string $password + * @param string $salt + * @param string $dbHash + * @return boolean + */ + protected static function wcf1($username, $password, $salt, $dbHash) { + return ($dbHash == sha1($salt . sha1($salt . sha1($password)))); + } + + /** + * Validates the password hash for WoltLab Community Framework 2.x (wcf2). + * + * @param string $username + * @param string $password + * @param string $salt + * @param string $dbHash + * @return boolean + */ + protected static function wcf2($username, $password, $salt, $dbHash) { + return ($dbHash == self::getDoubleSaltedHash($password, $salt)); + } + + /** + * Validates the password hash for XenForo 1.x with (xf1). + * + * @param string $username + * @param string $password + * @param string $salt + * @param string $dbHash + * @return boolean + */ + protected static function xf1($username, $password, $salt, $dbHash) { + if ($dbHash == sha1(sha1($password) . $salt)) { + return true; + } + else if (extension_loaded('hash')) { + return ($dbHash == hash('sha256', hash('sha256', $password) . $salt)); + } + + return false; + } + + private function __construct() { } +} diff --git a/wcfsetup/install/files/lib/util/StringUtil.class.php b/wcfsetup/install/files/lib/util/StringUtil.class.php index ac6fe33f5e..f2511f764b 100644 --- a/wcfsetup/install/files/lib/util/StringUtil.class.php +++ b/wcfsetup/install/files/lib/util/StringUtil.class.php @@ -25,70 +25,6 @@ final class StringUtil { */ const HELLIP = "\xE2\x80\xA6"; - /** - * Returns a salted hash of the given value. - * - * @param string $value - * @param string $salt - * @return string - */ - public static function getSaltedHash($value, $salt) { - if (!defined('ENCRYPTION_ENABLE_SALTING') || ENCRYPTION_ENABLE_SALTING) { - $hash = ''; - // salt - if (!defined('ENCRYPTION_SALT_POSITION') || ENCRYPTION_SALT_POSITION == 'before') { - $hash .= $salt; - } - - // value - if (!defined('ENCRYPTION_ENCRYPT_BEFORE_SALTING') || ENCRYPTION_ENCRYPT_BEFORE_SALTING) { - $hash .= self::encrypt($value); - } - else { - $hash .= $value; - } - - // salt - if (defined('ENCRYPTION_SALT_POSITION') && ENCRYPTION_SALT_POSITION == 'after') { - $hash .= $salt; - } - - return self::encrypt($hash); - } - else { - return self::encrypt($value); - } - } - - /** - * Returns a double salted hash of the given value. - * - * @param string $value - * @param string $salt - * @return string - */ - public static function getDoubleSaltedHash($value, $salt) { - return self::encrypt($salt . self::getSaltedHash($value, $salt)); - } - - /** - * Encrypts the given value. - * - * @param string $value - * @return string - */ - public static function encrypt($value) { - if (defined('ENCRYPTION_METHOD')) { - switch (ENCRYPTION_METHOD) { - case 'sha1': return sha1($value); - case 'md5': return md5($value); - case 'crc32': return crc32($value); - case 'crypt': return crypt($value); - } - } - return sha1($value); - } - /** * Alias to php sha1() function. * @@ -736,29 +672,5 @@ final class StringUtil { return mb_ereg_replace('.{'.$length.'}', "\\0".$break, $string); } - /** - * Generates a random user password with the given character length. - * - * @param integer $length - * @return string new password - */ - public static function getRandomPassword($length = 8) { - $availableCharacters = array( - 0 => 'abcdefghijklmnopqrstuvwxyz', - 1 => 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', - 2 => '0123456789', - 3 => '+#-.,;:?!' - ); - - $password = ''; - $type = 0; - for ($i = 0; $i < $length; $i++) { - $type = ($i % 4 == 0) ? 0 : ($type + 1); - $password .= substr($availableCharacters[$type], MathUtil::getRandomValue(0, strlen($availableCharacters[$type]) - 1), 1); - } - - return str_shuffle($password); - } - private function __construct() { } } diff --git a/wcfsetup/setup/db/install.sql b/wcfsetup/setup/db/install.sql index 6c26a1f323..3c36af65c1 100644 --- a/wcfsetup/setup/db/install.sql +++ b/wcfsetup/setup/db/install.sql @@ -632,8 +632,7 @@ CREATE TABLE wcf1_user ( userID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY, username VARCHAR(255) NOT NULL DEFAULT '', email VARCHAR(255) NOT NULL DEFAULT '', - password VARCHAR(40) NOT NULL DEFAULT '', - salt VARCHAR(40) NOT NULL DEFAULT '', + password VARCHAR(100) NOT NULL DEFAULT '', accessToken CHAR(40) NOT NULL DEFAULT '', languageID INT(10) NOT NULL DEFAULT 0, registrationDate INT(10) NOT NULL DEFAULT 0, -- 2.20.1