diff --git a/classes/AdminTab.php b/classes/AdminTab.php index 6e277f73f..2f06fe13e 100644 --- a/classes/AdminTab.php +++ b/classes/AdminTab.php @@ -1179,9 +1179,10 @@ abstract class AdminTabCore if ($key == 'passwd' && Tools::getValue('id_'.$table) && empty($value)) { continue; } - /* Automatically encrypt password in MD5 */ + /* Automatically hash password */ if ($key == 'passwd' && !empty($value)) { - $value = Tools::encrypt($value); + $crypto = new Hashing(); + $value = $crypto->hash($value); } $object->{$key} = $value; } diff --git a/classes/Customer.php b/classes/Customer.php index 0c2ac0023..6ce60debc 100644 --- a/classes/Customer.php +++ b/classes/Customer.php @@ -167,7 +167,7 @@ class CustomerCore extends ObjectModel 'lastname' => array('type' => self::TYPE_STRING, 'validate' => 'isName', 'required' => true, 'size' => 32), 'firstname' => array('type' => self::TYPE_STRING, 'validate' => 'isName', 'required' => true, 'size' => 32), 'email' => array('type' => self::TYPE_STRING, 'validate' => 'isEmail', 'required' => true, 'size' => 128), - 'passwd' => array('type' => self::TYPE_STRING, 'validate' => 'isPasswd', 'required' => true, 'size' => 32), + 'passwd' => array('type' => self::TYPE_STRING, 'validate' => 'isHashedPassword', 'required' => true, 'size' => 60), 'last_passwd_gen' => array('type' => self::TYPE_STRING, 'copy_post' => false), 'id_gender' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedId'), 'birthday' => array('type' => self::TYPE_DATE, 'validate' => 'isBirthDate'), @@ -374,7 +374,7 @@ class CustomerCore extends ObjectModel */ public function getByEmail($email, $passwd = null, $ignore_guest = true) { - if (!Validate::isEmail($email) || ($passwd && !Validate::isPasswd($passwd))) { + if (!Validate::isEmail($email) || ($passwd && !Validate::isHashedPassword($passwd))) { die(Tools::displayError()); } @@ -383,19 +383,28 @@ class CustomerCore extends ObjectModel FROM `'._DB_PREFIX_.'customer` WHERE `email` = \''.pSQL($email).'\' '.Shop::addSqlRestriction(Shop::SHARE_CUSTOMER).' - '.(isset($passwd) ? 'AND `passwd` = \''.pSQL(Tools::encrypt($passwd)).'\'' : '').' AND `deleted` = 0 '.($ignore_guest ? ' AND `is_guest` = 0' : '')); if (!$result) { return false; } + if (isset($passwd)) { + $crypto = new Hashing(); + if (!$crypto->checkHash($passwd, $result['passwd'])) { + return false; + } + } $this->id = $result['id_customer']; foreach ($result as $key => $value) { if (property_exists($this, $key)) { $this->{$key} = $value; } } + if (isset($passwd) && !$crypto->isFirstHash($passwd, $result['passwd'])) { + $this->passwd = $crypto->hash($passwd); + $this->update(); + } return $this; } @@ -560,7 +569,7 @@ class CustomerCore extends ObjectModel */ public static function checkPassword($id_customer, $passwd) { - if (!Validate::isUnsignedId($id_customer) || !Validate::isMd5($passwd)) { + if (!Validate::isUnsignedId($id_customer) || !Validate::isHashedPassword($passwd)) { die(Tools::displayError()); } $cache_id = 'Customer::checkPassword'.(int)$id_customer.'-'.$passwd; @@ -847,12 +856,13 @@ class CustomerCore extends ObjectModel if (empty($password)) { $password = Tools::passwdGen(8, 'RANDOM'); } - if (!Validate::isPasswd($password)) { + if (!Validate::isHashedPassword($password)) { return false; } $this->is_guest = 0; - $this->passwd = Tools::encrypt($password); + $crypto = new Hashing(); + $this->passwd = $crypto->hash($password); $this->cleanGroups(); $this->addGroups(array(Configuration::get('PS_CUSTOMER_GROUP'))); // add default customer group $this->id_default_group = (int) Configuration::get('PS_CUSTOMER_GROUP'); @@ -887,7 +897,8 @@ class CustomerCore extends ObjectModel public function setWsPasswd($passwd) { if ($this->id == 0 || $this->passwd != $passwd) { - $this->passwd = Tools::encrypt($passwd); + $crypto = new Hashing(); + $this->passwd = $crypto->hash($passwd); } return true; } diff --git a/classes/Employee.php b/classes/Employee.php index 778aea505..84e95fa7f 100644 --- a/classes/Employee.php +++ b/classes/Employee.php @@ -102,7 +102,7 @@ class EmployeeCore extends ObjectModel 'firstname' => array('type' => self::TYPE_STRING, 'validate' => 'isName', 'required' => true, 'size' => 32), 'email' => array('type' => self::TYPE_STRING, 'validate' => 'isEmail', 'required' => true, 'size' => 128), 'id_lang' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedInt', 'required' => true), - 'passwd' => array('type' => self::TYPE_STRING, 'validate' => 'isPasswdAdmin', 'required' => true, 'size' => 32), + 'passwd' => array('type' => self::TYPE_STRING, 'validate' => 'isPasswdAdmin', 'required' => true, 'size' => 60), 'last_passwd_gen' => array('type' => self::TYPE_STRING), 'active' => array('type' => self::TYPE_BOOL, 'validate' => 'isBool'), 'optin' => array('type' => self::TYPE_BOOL, 'validate' => 'isBool'), @@ -273,7 +273,7 @@ class EmployeeCore extends ObjectModel */ public function getByEmail($email, $passwd = null, $active_only = true) { - if (!Validate::isEmail($email) || ($passwd != null && !Validate::isPasswd($passwd))) { + if (!Validate::isEmail($email) || ($passwd != null && !Validate::isHashedPassword($passwd))) { die(Tools::displayError()); } @@ -281,11 +281,17 @@ class EmployeeCore extends ObjectModel SELECT * FROM `'._DB_PREFIX_.'employee` WHERE `email` = \''.pSQL($email).'\' - '.($active_only ? ' AND `active` = 1' : '') - .($passwd !== null ? ' AND `passwd` = \''.Tools::encrypt($passwd).'\'' : '')); + '.($active_only ? ' AND `active` = 1' : '')); if (!$result) { return false; } + + if ($passwd !== null) { + $crypto = new Hashing(); + if (!$crypto->checkHash($passwd, $result['passwd'])) { + return false; + } + } $this->id = $result['id_employee']; $this->id_profile = $result['id_profile']; foreach ($result as $key => $value) { @@ -293,6 +299,11 @@ class EmployeeCore extends ObjectModel $this->{$key} = $value; } } + + if ($passwd !== null && !$crypto->isFirstHash($passwd, $result['passwd'])) { + $this->passwd = $crypto->hash($passwd); + $this->update(); + } return $this; } @@ -316,7 +327,7 @@ class EmployeeCore extends ObjectModel */ public static function checkPassword($id_employee, $passwd) { - if (!Validate::isUnsignedId($id_employee) || !Validate::isPasswd($passwd, 8)) { + if (!Validate::isUnsignedId($id_employee) || !Validate::isHashedPassword($passwd, 8)) { die(Tools::displayError()); } @@ -364,19 +375,20 @@ class EmployeeCore extends ObjectModel // validate and set password for the employee public function setWsPasswd($passwd) { + $crypto = new Hashing(); if ($this->id != 0) { if ($this->passwd != $passwd) { - if (!Validate::isPasswd($passwd, Validate::ADMIN_PASSWORD_LENGTH)) { + if (!Validate::isHashedPassword($passwd, Validate::ADMIN_PASSWORD_LENGTH)) { WebserviceRequest::getInstance()->setError(400, 'The password must be at least '.Validate::ADMIN_PASSWORD_LENGTH.' characters long.', 134); } else { - $this->passwd = Tools::encrypt($passwd); + $this->passwd = $crypto->hash($passwd); } } } else { - if (!Validate::isPasswd($passwd, Validate::ADMIN_PASSWORD_LENGTH)) { + if (!Validate::isHashedPassword($passwd, Validate::ADMIN_PASSWORD_LENGTH)) { WebserviceRequest::getInstance()->setError(400, 'The password must be at least '.Validate::ADMIN_PASSWORD_LENGTH.' characters long.', 134); } else { - $this->passwd = Tools::encrypt($passwd); + $this->passwd = $crypto->hash($passwd); } } diff --git a/classes/Hashing.php b/classes/Hashing.php new file mode 100644 index 000000000..ca8a245b5 --- /dev/null +++ b/classes/Hashing.php @@ -0,0 +1,125 @@ + + * @copyright 2007-2017 PrestaShop SA + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + + +class HashingCore +{ + /** @var array should contain hashing methods */ + private $hashMethods = array(); + + /** + * Check if it's the first function of the array that was used for hashing + * + * @param string $passwd The password you want to check + * @param string $hash The hash you want to check + * @param string $staticSalt A static salt + * + * @return bool Result of the verify function + */ + public function isFirstHash($passwd, $hash, $staticSalt = _COOKIE_KEY_) + { + if (!count($this->hashMethods)) { + $this->initHashMethods(); + } + + $closure = reset($this->hashMethods); + + return $closure['verify']($passwd, $hash, $staticSalt); + } + + /** + * Iterate on hash_methods array and return true if it matches + * + * @param string $passwd The password you want to check + * @param string $hash The hash you want to check + * @param string $staticSalt A static salt + * + * @return bool `true` is returned if the function find a match else false + */ + public function checkHash($passwd, $hash, $staticSalt = _COOKIE_KEY_) + { + if (!count($this->hashMethods)) { + $this->initHashMethods(); + } + + foreach ($this->hashMethods as $closure) { + if ($closure['verify']($passwd, $hash, $staticSalt)) { + return true; + } + } + + return false; + } + + /** + * Hash the `$plaintextPassword` string and return the result of the 1st hashing method + * contained in Hashing::hashMethods + * + * @param string $plaintextPassword The password you want to hash + * @param string $staticSalt The static salt + * + * @return string + */ + public function hash($plaintextPassword, $staticSalt = _COOKIE_KEY_) + { + if (!count($this->hashMethods)) { + $this->initHashMethods(); + } + + $closure = reset($this->hashMethods); + + return $closure['hash']($plaintextPassword, $staticSalt, $closure['option']); + } + + /** + * Init hash methods + * + * @return void + */ + private function initHashMethods() + { + $this->hashMethods = array( + 'bcrypt' => array( + 'option' => array(), + 'hash' => function ($passwd, $staticSalt, $option) { + return password_hash($passwd, PASSWORD_BCRYPT); + }, + 'verify' => function ($passwd, $hash, $staticSalt) { + return password_verify($passwd, $hash); + }, + ), + 'md5' => array( + 'option' => array(), + 'hash' => function ($passwd, $staticSalt, $option) { + return md5($staticSalt.$passwd); + }, + 'verify' => function ($passwd, $hash, $staticSalt) { + return md5($staticSalt.$passwd) === $hash; + }, + ), + ); + } +} diff --git a/classes/ObjectModel.php b/classes/ObjectModel.php index e49faffa3..8c316c571 100644 --- a/classes/ObjectModel.php +++ b/classes/ObjectModel.php @@ -1188,7 +1188,8 @@ abstract class ObjectModelCore implements Core_Foundation_Database_EntityInterfa } if ($field == 'passwd') { if ($value = Tools::getValue($field)) { - $this->{$field} = Tools::encrypt($value); + $crypto = new Hashing(); + $this->{$field} = $crypto->hash($value); } } else { $this->{$field} = $value; diff --git a/classes/Referrer.php b/classes/Referrer.php index d6b347669..93641ae5f 100644 --- a/classes/Referrer.php +++ b/classes/Referrer.php @@ -53,7 +53,7 @@ class ReferrerCore extends ObjectModel 'primary' => 'id_referrer', 'fields' => array( 'name' => array('type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'required' => true, 'size' => 64), - 'passwd' => array('type' => self::TYPE_STRING, 'validate' => 'isPasswd', 'size' => 32), + 'passwd' => array('type' => self::TYPE_STRING, 'validate' => 'isPasswd', 'size' => 60), 'http_referer_regexp' => array('type' => self::TYPE_STRING, 'validate' => 'isCleanHtml', 'size' => 64), 'request_uri_regexp' => array('type' => self::TYPE_STRING, 'validate' => 'isCleanHtml', 'size' => 64), 'http_referer_like' => array('type' => self::TYPE_STRING, 'validate' => 'isCleanHtml', 'size' => 64), diff --git a/classes/Validate.php b/classes/Validate.php index b7178a8b5..d47f64b66 100644 --- a/classes/Validate.php +++ b/classes/Validate.php @@ -88,6 +88,19 @@ class ValidateCore return preg_match('/^[a-f0-9A-F]{32}$/', $md5); } + /** + * Check for password hash validity (legacy MD5 or bcrypt), or plaintext password. + * + * @param string $hash + * @return bool Validity is ok or not + */ + public static function isHashedPassword($hashedPasswd) + { + return self::isPasswd($hashedPasswd) + || Tools::strlen($hashedPasswd) == 32 + || Tools::strlen($hashedPasswd) == 60; + } + /** * Check for SHA1 string validity * @@ -1140,4 +1153,4 @@ class ValidateCore { return (preg_match('/^(?:'.Configuration::get('PS_INVOICE_PREFIX', Context::getContext()->language->id).')\s*([0-9]+)$/i', $id)); } -} \ No newline at end of file +} diff --git a/classes/controller/AdminController.php b/classes/controller/AdminController.php index 7e979e4d3..9388bf96e 100644 --- a/classes/controller/AdminController.php +++ b/classes/controller/AdminController.php @@ -3838,9 +3838,10 @@ class AdminControllerCore extends Controller if ($key == 'passwd' && Tools::getValue('id_'.$table) && empty($value)) { continue; } - /* Automatically encrypt password in MD5 */ + /* Automatically hash password */ if ($key == 'passwd' && !empty($value)) { - $value = Tools::encrypt($value); + $crypto = new Hashing(); + $value = $crypto->hash($value); } $object->{$key} = $value; } @@ -4817,4 +4818,4 @@ class AdminControllerCore extends Controller return 0; } } -} \ No newline at end of file +} diff --git a/classes/webservice/WebserviceSpecificManagementBookings.php b/classes/webservice/WebserviceSpecificManagementBookings.php index 10e52dab0..4e542bdcf 100644 --- a/classes/webservice/WebserviceSpecificManagementBookings.php +++ b/classes/webservice/WebserviceSpecificManagementBookings.php @@ -1378,7 +1378,8 @@ class WebserviceSpecificManagementBookingsCore Extends ObjectModel implements We $objCustomer->firstname = $customerDetails['firstname']; $objCustomer->lastname = $customerDetails['lastname']; $objCustomer->email = $customerDetails['email']; - $objCustomer->passwd = md5(time()._COOKIE_KEY_); + $crypto = new Hashing(); + $objCustomer->passwd = $crypto->hash(Tools::passwdGen(32, 'RANDOM')); $objCustomer->phone = (isset($customerDetails['phone']) ? $customerDetails['phone'] : ''); $objCustomer->cleanGroups(); $objCustomer->add(); diff --git a/controllers/admin/AdminImportController.php b/controllers/admin/AdminImportController.php index d2819efca..f7ba432e1 100644 --- a/controllers/admin/AdminImportController.php +++ b/controllers/admin/AdminImportController.php @@ -2835,7 +2835,8 @@ class AdminImportControllerCore extends AdminController AdminImportController::arrayWalk($info, array('AdminImportController', 'fillInfo'), $customer); if ($customer->passwd) { - $customer->passwd = Tools::encrypt($customer->passwd); + $crypto = new Hashing(); + $customer->passwd = $crypto->hash($customer->passwd); } $customers_shop = array(); diff --git a/controllers/admin/AdminLoginController.php b/controllers/admin/AdminLoginController.php index f48096f05..e438f022e 100644 --- a/controllers/admin/AdminLoginController.php +++ b/controllers/admin/AdminLoginController.php @@ -263,7 +263,8 @@ class AdminLoginControllerCore extends AdminController if (!count($this->errors)) { $pwd = Tools::passwdGen(10, 'RANDOM'); - $employee->passwd = Tools::encrypt($pwd); + $crypto = new Hashing(); + $employee->passwd = $crypto->hash($pwd); $employee->last_passwd_gen = date('Y-m-d H:i:s', time()); $params = array( diff --git a/controllers/front/IdentityController.php b/controllers/front/IdentityController.php index 917e301f4..9f8a1c904 100644 --- a/controllers/front/IdentityController.php +++ b/controllers/front/IdentityController.php @@ -71,7 +71,7 @@ class IdentityControllerCore extends FrontController $this->errors[] = Tools::displayError('This email address is not valid'); } elseif ($this->customer->email != $email && Customer::customerExists($email, true)) { $this->errors[] = Tools::displayError('An account using this email address has already been registered.'); - } elseif (!Tools::getIsset('old_passwd') || (Tools::encrypt($old_passwd) != $this->context->cookie->passwd)) { + } elseif (!Tools::getIsset('old_passwd') || !(new Hashing())->checkHash($old_passwd, $this->context->cookie->passwd)) { $this->errors[] = Tools::displayError('The password you entered is incorrect.'); } elseif (Tools::getValue('passwd') != Tools::getValue('confirmation')) { $this->errors[] = Tools::displayError('The password and confirmation do not match.'); diff --git a/controllers/front/PasswordController.php b/controllers/front/PasswordController.php index 317818c87..5bfd526ee 100644 --- a/controllers/front/PasswordController.php +++ b/controllers/front/PasswordController.php @@ -73,7 +73,8 @@ class PasswordControllerCore extends FrontController } elseif ((strtotime($customer->last_passwd_gen.'+'.(int)Configuration::get('PS_PASSWD_TIME_FRONT').' minutes') - time()) > 0) { Tools::redirect('index.php?controller=authentication&error_regen_pwd'); } else { - $customer->passwd = Tools::encrypt($password = Tools::passwdGen(MIN_PASSWD_LENGTH, 'RANDOM')); + $crypto = new Hashing(); + $customer->passwd = $crypto->hash($password = Tools::passwdGen(MIN_PASSWD_LENGTH, 'RANDOM')); $customer->last_passwd_gen = date('Y-m-d H:i:s', time()); if ($customer->update()) { Hook::exec('actionPasswordRenew', array('customer' => $customer, 'password' => $password)); diff --git a/install/data/db_structure.sql b/install/data/db_structure.sql index 7550fbcad..37b5d93da 100644 --- a/install/data/db_structure.sql +++ b/install/data/db_structure.sql @@ -593,7 +593,7 @@ CREATE TABLE `PREFIX_customer` ( `firstname` varchar(32) NOT NULL, `lastname` varchar(32) NOT NULL, `email` varchar(128) NOT NULL, - `passwd` varchar(32) NOT NULL, + `passwd` varchar(60) NOT NULL, `last_passwd_gen` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `birthday` date DEFAULT NULL, `newsletter` tinyint(1) unsigned NOT NULL DEFAULT '0', @@ -779,7 +779,7 @@ CREATE TABLE `PREFIX_employee` ( `lastname` varchar(32) NOT NULL, `firstname` varchar(32) NOT NULL, `email` varchar(128) NOT NULL, - `passwd` varchar(32) NOT NULL, + `passwd` varchar(60) NOT NULL, `last_passwd_gen` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `stats_date_from` date DEFAULT NULL, `stats_date_to` date DEFAULT NULL, @@ -1781,7 +1781,7 @@ CREATE TABLE `PREFIX_range_weight` ( CREATE TABLE `PREFIX_referrer` ( `id_referrer` int(10) unsigned NOT NULL auto_increment, `name` varchar(64) NOT NULL, - `passwd` varchar(32) DEFAULT NULL, + `passwd` varchar(60) DEFAULT NULL, `http_referer_regexp` varchar(64) DEFAULT NULL, `http_referer_like` varchar(64) DEFAULT NULL, `request_uri_regexp` varchar(64) DEFAULT NULL, diff --git a/install/fixtures/fashion/install.php b/install/fixtures/fashion/install.php index f7ef6fcca..e14ad92bf 100644 --- a/install/fixtures/fashion/install.php +++ b/install/fixtures/fashion/install.php @@ -35,7 +35,8 @@ class InstallFixturesFashion extends InstallXmlLoader public function createEntityCustomer($identifier, array $data, array $data_lang) { if ($identifier == 'John') { - $data['passwd'] = Tools::encrypt('123456789'); + $crypto = new Hashing(); + $data['passwd'] = $crypto->hash('123456789'); $data['last_passwd_gen'] = date('Y-m-d H:i:s'); $data['birthday'] = date('Y-m-d', strtotime('-30 years')); $data['newsletter_date_add'] = date('Y-m-d H:i:s'); diff --git a/install/models/install.php b/install/models/install.php index 4f48210c1..2a36638d8 100644 --- a/install/models/install.php +++ b/install/models/install.php @@ -566,7 +566,8 @@ class InstallModelInstall extends InstallAbstractModel $employee->firstname = Tools::ucfirst($data['admin_firstname']); $employee->lastname = Tools::ucfirst($data['admin_lastname']); $employee->email = $data['admin_email']; - $employee->passwd = md5(_COOKIE_KEY_.$data['admin_password']); + $crypto = new Hashing(); + $employee->passwd = $crypto->hash($data['admin_password']); $employee->last_passwd_gen = date('Y-m-d h:i:s', strtotime('-360 minutes')); $employee->bo_theme = 'default'; $employee->default_tab = 1;