Index: branches/5.2.x/core/kernel/utility/formatters/password_formatter.php =================================================================== diff -u -N -r14758 -r15590 --- branches/5.2.x/core/kernel/utility/formatters/password_formatter.php (.../password_formatter.php) (revision 14758) +++ branches/5.2.x/core/kernel/utility/formatters/password_formatter.php (.../password_formatter.php) (revision 15590) @@ -1,6 +1,6 @@ _phpPass = $this->Application->makeClass('PasswordHash', Array (8, false)); + } + + /** + * The method is supposed to alter config options or configure object in some way based on its usage of formatters * The methods is called for every field with formatter defined when configuring item. * Could be used for adding additional VirtualFields to an object required by some special Formatter * @@ -27,26 +47,33 @@ */ function PrepareOptions($field_name, &$field_options, &$object) { - if ( isset( $field_options['verify_field'] ) ) { - $add_fields = Array (); - $options = Array ('master_field' => $field_name, /*'error_field' => $field_name,*/ 'formatter' => 'kPasswordFormatter'); + if ( !isset($field_options['verify_field']) ) { + return; + } - $copy_options = Array ('encryption_method', 'salt', 'required', 'skip_empty'); - foreach ($copy_options as $copy_option) { - if (array_key_exists($copy_option, $field_options)) { - $options[$copy_option] = $field_options[$copy_option]; - } + $add_fields = Array (); + $options = Array ( + 'master_field' => $field_name, +// 'error_field' => $field_name, + 'formatter' => 'kPasswordFormatter' + ); + + $copy_options = Array ('hashing_method', 'hashing_method_field', 'salt', 'required', 'skip_empty'); + + foreach ($copy_options as $copy_option) { + if ( array_key_exists($copy_option, $field_options) ) { + $options[$copy_option] = $field_options[$copy_option]; } + } - $add_fields[ $field_options['verify_field'] ] = $options; + $add_fields[$field_options['verify_field']] = $options; - $add_fields[$field_name.'_plain'] = Array('type'=>'string', 'error_field'=>$field_name); - $add_fields[ $field_options['verify_field'].'_plain' ] = Array('type'=>'string', 'error_field'=>$field_options['verify_field'] ); + $add_fields[$field_name . '_plain'] = Array ('type' => 'string', 'error_field' => $field_name); + $add_fields[$field_options['verify_field'] . '_plain'] = Array ('type' => 'string', 'error_field' => $field_options['verify_field']); - $virtual_fields = $object->getVirtualFields(); - $add_fields = kUtil::array_merge_recursive($add_fields, $virtual_fields); - $object->setVirtualFields($add_fields); - } + $virtual_fields = $object->getVirtualFields(); + $add_fields = kUtil::array_merge_recursive($add_fields, $virtual_fields); + $object->setVirtualFields($add_fields); } /** @@ -58,7 +85,7 @@ * @param string $format * @return string */ - function Format($value, $field_name, &$object, $format=null) + function Format($value, $field_name, &$object, $format = null) { return $value; } @@ -74,85 +101,237 @@ */ public function Parse($value, $field_name, &$object) { + list ($password_field, $verify_field) = $this->_getPasswordFields($value, $field_name, $object); + $options = $object->GetFieldOptions($field_name); + $salt = $object->GetFieldOption($password_field, 'salt', false, ''); + $hashing_method = isset($options['hashing_method']) ? $options['hashing_method'] : $object->GetDBField($options['hashing_method_field']); + if ( $object->GetFieldOption($password_field, 'verify_field_set') && $object->GetFieldOption($verify_field, 'master_field_set') ) { + $new_password = $object->GetDBField($password_field . '_plain'); + $verify_password = $object->GetDBField($verify_field . '_plain'); + + if ( $new_password == '' && $verify_password == '' ) { + $stored_hash = $object->GetDBField($password_field); + + if ( !$this->checkPassword('', $stored_hash, $hashing_method) ) { + // return empty string causing password from database to stay + return $value; + } + else { + return $this->hashPassword($value, $salt, $hashing_method); + } + } + + // determine admin or front + $phrase_error_prefix = $this->Application->isAdmin ? 'la' : 'lu'; + + if ( $new_password != $verify_password ) { + // passwords don't match (no matter what is their length) + $object->SetError($verify_field, 'passwords_do_not_match', $phrase_error_prefix . '_passwords_do_not_match'); + } + + $min_length = $this->Application->ConfigValue('Min_Password'); // for error message too + $min_length = $object->GetFieldOption($password_field, 'min_length', false, $min_length); + + if ( mb_strlen($new_password) < $min_length ) { + $error_msg = '+' . sprintf($this->Application->Phrase($phrase_error_prefix . '_passwords_too_short'), $min_length); // + -> not phrase + $object->SetError($password_field, 'passwords_min_length', $error_msg); + } + } + + if ( $value == '' ) { + // new value is empty - return hash from database + return $object->GetDBField($field_name); + } + + return $this->hashPassword($value, $salt, $hashing_method); + } + + /** + * Finds out names of password and verify password fields and updates "_plain" virtual field + * + * @param string $value + * @param string $field_name + * @param kDBItem $object + * @return Array + * @access protected + */ + protected function _getPasswordFields($value, $field_name, &$object) + { + $options = $object->GetFieldOptions($field_name); + $flip_count = 0; - $fields_set = true; $password_field = $verify_field = ''; $fields = Array ('master_field', 'verify_field'); // 1. collect values from both Password and VerifyPassword fields while ($flip_count < 2) { if ( getArrayValue($options, $fields[0]) ) { - $tmp_field = $options[ $fields[0] ]; - $object->SetDBField($field_name.'_plain', $value); + $tmp_field = $options[$fields[0]]; + $object->SetDBField($field_name . '_plain', $value); - if ( !$object->GetFieldOption($tmp_field, $fields[1].'_set') ) { - $object->SetFieldOption($tmp_field, $fields[1].'_set', true); + if ( !$object->GetFieldOption($tmp_field, $fields[1] . '_set') ) { + $object->SetFieldOption($tmp_field, $fields[1] . '_set', true); } - $password_field = $options[ $fields[0] ]; + $password_field = $options[$fields[0]]; $verify_field = $field_name; } $fields = array_reverse($fields); $flip_count++; } - $salt = $object->GetFieldOption($password_field, 'salt', false, ''); + return Array ($password_field, $verify_field); + } - if ($object->GetFieldOption($password_field, 'verify_field_set') && $object->GetFieldOption($verify_field, 'master_field_set')) { - $new_password = $object->GetDBField($password_field . '_plain'); - $verify_password = $object->GetDBField($verify_field . '_plain'); + /** + * Creates hash from given password and salt + * + * @param string $password + * @param string $salt + * @param int $hashing_method + * @return string + * @throws InvalidArgumentException + * @access public + */ + public function hashPassword($password, $salt = null, $hashing_method = PasswordHashingMethod::PHPPASS) + { + switch ( $hashing_method ) { + case PasswordHashingMethod::NONE: + return $password; + break; - if ($new_password == '' && $verify_password == '') { - // both passwords are empty -> keep old password - if ($object->GetDBField($password_field) != $this->EncryptPassword('', $salt)) { - if ($options['encryption_method'] == 'plain') { - return $value; - } + case PasswordHashingMethod::MD5: + return $this->_md5hash($password, $salt, false); + break; - return $this->EncryptPassword($value); - } - else { - return $value; - } - } + case PasswordHashingMethod::MD5_AND_PHPPASS: + return $this->_phpPass->hashPassword($this->_md5hash($password, $salt, true)); + break; - // determine admin or front - $phrase_error_prefix = $this->Application->isAdmin ? 'la' : 'lu'; + case PasswordHashingMethod::PHPPASS: + return $this->_phpPass->hashPassword($password); + break; - if ($new_password != $verify_password) { - // passwords don't match (no matter what is their length) - $object->SetError($verify_field, 'passwords_do_not_match', $phrase_error_prefix.'_passwords_do_not_match'); - } + default: + throw new InvalidArgumentException('Unknown password hashing method "' . $hashing_method . '"'); + break; + } + } - $min_length = $this->Application->ConfigValue('Min_Password'); // for error message too - $min_length = $object->GetFieldOption($password_field, 'min_length', false, $min_length); + /** + * Checks, that user password is valid + * + * @param string $password Non-hashed password provided by user + * @param string $stored_hash Hash, calculated before from correct user password (must have salt inside) + * @param int $hashing_method Hash generation method + * @return bool + * @access public + * @throws InvalidArgumentException + */ + public function checkPassword($password, $stored_hash = null, $hashing_method = PasswordHashingMethod::PHPPASS) + { + $salt = ''; - if (mb_strlen($new_password) < $min_length) { - $error_msg = '+' . sprintf($this->Application->Phrase($phrase_error_prefix.'_passwords_too_short'), $min_length); // + -> not phrase - $object->SetError($password_field, 'passwords_min_length', $error_msg); - } + if ( $hashing_method != PasswordHashingMethod::PHPPASS && strpos($stored_hash, ':') !== false ) { + list ($salt, $stored_hash) = explode(':', $stored_hash, 2); } - if ($value == '') { - return $object->GetDBField($field_name); + switch ( $hashing_method ) { + case PasswordHashingMethod::NONE: + return $password == $stored_hash; + break; + + case PasswordHashingMethod::MD5: + return $this->_md5hash($password, $salt, false) == $stored_hash; + break; + + case PasswordHashingMethod::MD5_AND_PHPPASS: + $password_hashed = preg_match('/^[a-f0-9]{32}$/', $password); + return $this->_phpPass->checkPassword($this->_md5hash($password, $salt, $password_hashed), $stored_hash); + break; + + case PasswordHashingMethod::PHPPASS: + return $this->_phpPass->checkPassword($password, $stored_hash); + break; + + default: + throw new InvalidArgumentException('Unknown password hashing method "' . $hashing_method . '"'); + break; } + } - if ($options['encryption_method'] == 'plain') { - return $value; + /** + * Checks a password stored as system setting using phppass with fallback to salted md5 + * + * @param string $setting_name + * @param string $password + * @param int $hashing_method + * @return bool + * @access public + */ + public function checkPasswordFromSetting($setting_name, $password, $hashing_method = PasswordHashingMethod::PHPPASS) + { + $stored_hash = $this->Application->ConfigValue($setting_name); + $stored_hash = $this->prepareHash($stored_hash, 'b38', $hashing_method); + + if ( $this->checkPassword($password, $stored_hash, $hashing_method) ) { + return true; } - return $this->EncryptPassword($value, $salt); + if ( $hashing_method != PasswordHashingMethod::MD5 ) { + if ( $this->checkPasswordFromSetting($setting_name, $password, PasswordHashingMethod::MD5) ) { + // rehash password on the go using more secure algorithm + $this->Application->SetConfigValue($setting_name, $this->hashPassword($password)); + + return true; + } + } + + return false; } - function EncryptPassword($value, $salt=null) + /** + * Ensures, that salt is always present in the hash + * + * @param string $stored_hash + * @param string $salt + * @param int $hashing_method + * @return string + * @access public + */ + public function prepareHash($stored_hash, $salt = '', $hashing_method = PasswordHashingMethod::PHPPASS) { - if (!isset($salt) || !$salt) { - // if empty salt, assume, that it's not passed at all - return md5($value); + if ( $hashing_method == PasswordHashingMethod::PHPPASS ) { + return $stored_hash; } - return md5(md5($value).$salt); + + // embed salt into hash generated not by phppass + return $salt . ':' . $stored_hash; } + + /** + * Hashes password using MD5 algorithm + * + * @param string $password + * @param string $salt + * @param bool $password_hashed + * @return string + * @access protected + */ + protected function _md5hash($password, $salt = null, $password_hashed = false) + { + if ( !$password_hashed ) { + $password = md5($password); + } + + if ( isset($salt) && $salt ) { + return md5($password . $salt); + } + + // if empty salt, assume, that it's not passed at all + return $password; + } } \ No newline at end of file Index: branches/5.2.x/core/admin_templates/users/root_edit_password.tpl =================================================================== diff -u -N -r14244 -r15590 --- branches/5.2.x/core/admin_templates/users/root_edit_password.tpl (.../root_edit_password.tpl) (revision 14244) +++ branches/5.2.x/core/admin_templates/users/root_edit_password.tpl (.../root_edit_password.tpl) (revision 15590) @@ -11,7 +11,7 @@