_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 * * @param string $field_name * @param array $field_options * @param kDBBase $object */ function PrepareOptions($field_name, &$field_options, &$object) { if ( !isset($field_options['verify_field']) ) { return; } $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_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); } /** * Formats value of a given field * * @param string $value * @param string $field_name * @param kDBItem|kDBList $object * @param string $format * @return string */ function Format($value, $field_name, &$object, $format = null) { return $value; } /** * Performs password & verify password field validation * * @param mixed $value * @param string $field_name * @param kDBItem $object * @return mixed * @access public */ 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', false), $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; $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); if ( !$object->GetFieldOption($tmp_field, $fields[1] . '_set') ) { $object->SetFieldOption($tmp_field, $fields[1] . '_set', true); } $password_field = $options[$fields[0]]; $verify_field = $field_name; } $fields = array_reverse($fields); $flip_count++; } return Array ($password_field, $verify_field); } /** * 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; case PasswordHashingMethod::MD5: return $this->_md5hash($password, $salt, false); break; case PasswordHashingMethod::MD5_AND_PHPPASS: return $this->_phpPass->hashPassword($this->_md5hash($password, $salt, true)); break; case PasswordHashingMethod::PHPPASS: return $this->_phpPass->hashPassword($password); break; default: throw new InvalidArgumentException('Unknown password hashing method "' . $hashing_method . '"'); break; } } /** * 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 ( $hashing_method != PasswordHashingMethod::PHPPASS && strpos($stored_hash, ':') !== false ) { list ($salt, $stored_hash) = explode(':', $stored_hash, 2); } 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; } } /** * 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; } 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; } /** * 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 ( $hashing_method == PasswordHashingMethod::PHPPASS ) { return $stored_hash; } // 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; } }