Attached Files |
improved_password_hashing_core.patch [^] (42,404 bytes) 2012-10-18 10:33
[Show Content]
Index: admin_templates/users/root_edit_password.tpl
===================================================================
--- admin_templates/users/root_edit_password.tpl (revision 15437)
+++ admin_templates/users/root_edit_password.tpl (working copy)
@@ -11,7 +11,7 @@
<script type="text/javascript">
a_toolbar = new ToolBar();
a_toolbar.AddButton( new ToolBarButton('select', '<inp2:m_phrase label="la_ToolTip_Save" escape="1"/>', function() {
- submit_event('u','OnUpdateRootPassword');
+ submit_event('u','OnUpdatePassword');
}
) );
a_toolbar.AddButton( new ToolBarButton('cancel', '<inp2:m_phrase label="la_ToolTip_Cancel" escape="1"/>', function() {
Index: install.php
===================================================================
--- install.php (revision 15552)
+++ install.php (working copy)
@@ -851,10 +851,11 @@
case 'root_password':
// update root password in database
- $password = md5( md5($this->Application->GetVar('root_password')) . 'b38');
+ $password_formatter = $this->Application->recallObject('kPasswordFormatter');
+ /* @var $password_formatter kPasswordFormatter */
$config_values = Array (
- 'RootPass' => $password,
+ 'RootPass' => $password_formatter->hashPassword($this->Application->GetVar('root_password')),
'Backup_Path' => FULL_PATH . $this->toolkit->getSystemConfig('Misc', 'WriteablePath') . DIRECTORY_SEPARATOR . 'backupdata',
'DefaultEmailSender' => 'portal@' . $this->toolkit->getSystemConfig('Misc', 'Domain')
);
Index: install/install_schema.sql
===================================================================
--- install/install_schema.sql (revision 15552)
+++ install/install_schema.sql (working copy)
@@ -282,6 +282,7 @@
PortalUserId int(11) NOT NULL AUTO_INCREMENT,
Username varchar(255) NOT NULL DEFAULT '',
`Password` varchar(255) DEFAULT 'd41d8cd98f00b204e9800998ecf8427e',
+ PasswordHashingMethod tinyint(4) NOT NULL DEFAULT '3',
FirstName varchar(255) NOT NULL DEFAULT '',
LastName varchar(255) NOT NULL DEFAULT '',
Company varchar(255) NOT NULL DEFAULT '',
Index: install/upgrades.php
===================================================================
--- install/upgrades.php (revision 15450)
+++ install/upgrades.php (working copy)
@@ -2295,4 +2295,47 @@
// drop RunInterval column
$this->Conn->Query('ALTER TABLE ' . $table_name . ' DROP RunInterval');
}
+
+ /**
+ * Update to 5.2.1-B1
+ *
+ * @param string $mode when called mode {before, after)
+ */
+ public function Upgrade_5_2_1_B1($mode)
+ {
+ if ( $mode != 'after' ) {
+ return;
+ }
+
+ $this->_updateUserPasswords();
+ }
+
+ protected function _updateUserPasswords()
+ {
+ $user_table = $this->Application->getUnitOption('u', 'TableName');
+
+ $sql = 'SELECT Password, PortalUserId
+ FROM ' . $user_table . '
+ WHERE PasswordHashingMethod = ' . PasswordHashingMethod::MD5;
+ $user_passwords = $this->Conn->GetColIterator($sql, 'PortalUserId');
+
+ if ( !count($user_passwords) ) {
+ // no users at all or existing users have converted passwords already
+ return;
+ }
+
+ kUtil::setResourceLimit();
+
+ $password_formatter = $this->Application->recallObject('kPasswordFormatter');
+ /* @var $password_formatter kPasswordFormatter */
+
+ foreach ($user_passwords as $user_id => $user_password) {
+ $fields_hash = Array (
+ 'Password' => $password_formatter->hashPassword($user_password, '', PasswordHashingMethod::MD5_AND_PHPPASS),
+ 'PasswordHashingMethod' => PasswordHashingMethod::MD5_AND_PHPPASS,
+ );
+
+ $this->Conn->doUpdate($fields_hash, TABLE_PREFIX . 'Users', 'PortalUserId = ' . $user_id);
+ }
+ }
}
\ No newline at end of file
Index: install/upgrades.sql
===================================================================
--- install/upgrades.sql (revision 15552)
+++ install/upgrades.sql (working copy)
@@ -2856,3 +2856,6 @@
INSERT INTO SystemSettings VALUES(DEFAULT, 'SystemLogRotationInterval', '2419200', 'In-Portal', 'in-portal:configure_advanced', 'la_section_SettingsLogs', 'la_config_SystemLogRotationInterval', 'select', NULL, '86400=la_opt_OneDay||604800=la_opt_OneWeek||1209600=la_opt_TwoWeeks||2419200=la_opt_OneMonth||7257600=la_opt_ThreeMonths||29030400=la_opt_OneYear||-1=la_opt_SystemLogKeepForever', 65.03, 0, 1, 'hint:la_config_SystemLogRotationInterval');
INSERT INTO SystemSettings VALUES(DEFAULT, 'SystemLogNotificationEmail', '', 'In-Portal', 'in-portal:configure_advanced', 'la_section_SettingsLogs', 'la_config_SystemLogNotificationEmail', 'text', 'a:5:{s:4:"type";s:6:"string";s:9:"formatter";s:10:"kFormatter";s:6:"regexp";s:85:"/^([-a-zA-Z0-9!\\#$%&*+\\/=?^_`{|}~.]+@[a-zA-Z0-9]{1}[-.a-zA-Z0-9_]*\\.[a-zA-Z]{2,6})$/i";s:10:"error_msgs";a:1:{s:14:"invalid_format";s:18:"!la_invalid_email!";}s:7:"default";s:0:"";}', NULL, 65.04, 0, 1, 'hint:la_config_SystemLogNotificationEmail');
INSERT INTO EmailEvents (EventId, Event, ReplacementTags, Enabled, FrontEndOnly, Module, Description, Type, AllowChangingSender, AllowChangingRecipient) VALUES(DEFAULT, 'SYSTEM.LOG.NOTIFY', NULL, 1, 0, 'Core', 'Notification about message added to System Log', 1, 1, 1);
+
+ALTER TABLE Users ADD PasswordHashingMethod TINYINT NOT NULL DEFAULT '3' AFTER Password;
+UPDATE Users SET PasswordHashingMethod = 1;
Index: kernel/application.php
===================================================================
--- kernel/application.php (revision 15569)
+++ kernel/application.php (working copy)
@@ -729,6 +729,7 @@
$this->registerClass('kLogger', KERNEL_PATH . '/utility/logger.php');
$this->registerClass('kUnitConfigReader', KERNEL_PATH . '/utility/unit_config_reader.php');
+ $this->registerClass('PasswordHash', KERNEL_PATH . '/utility/php_pass.php');
// Params class descendants
$this->registerClass('kArray', KERNEL_PATH . '/utility/params.php');
Index: kernel/constants.php
===================================================================
--- kernel/constants.php (revision 15563)
+++ kernel/constants.php (working copy)
@@ -164,6 +164,13 @@
const ADMIN = 1;
}
+ class PasswordHashingMethod {
+ const NONE = 0;
+ const MD5 = 1;
+ const MD5_AND_PHPPASS = 2;
+ const PHPPASS = 3;
+ }
+
// selectors
define('STYLE_BASE', 1);
define('STYLE_BLOCK', 2);
Index: kernel/utility/formatters/password_formatter.php
===================================================================
--- kernel/utility/formatters/password_formatter.php (revision 15437)
+++ kernel/utility/formatters/password_formatter.php (working copy)
@@ -17,7 +17,27 @@
class kPasswordFormatter extends kFormatter
{
/**
- * The method is supposed to alter config options or cofigure object in some way based on its usage of formatters
+ * Instance of PHPPass
+ *
+ * @var PasswordHash
+ * @access protected
+ */
+ protected $_phpPass;
+
+ /**
+ * Creates formatter instance
+ *
+ * @access public
+ */
+ public function __construct()
+ {
+ parent::__construct();
+
+ $this->_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,24 +101,81 @@
*/
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;
}
@@ -99,60 +183,155 @@
$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: kernel/utility/php_pass.php
===================================================================
--- kernel/utility/php_pass.php (revision 0)
+++ kernel/utility/php_pass.php (revision 0)
@@ -0,0 +1,291 @@
+<?php
+#
+# Portable PHP password hashing framework.
+#
+# Version 0.3 / genuine.
+#
+# Written by Solar Designer <solar at openwall.com> in 2004-2006 and placed in
+# the public domain. Revised in subsequent years, still public domain.
+#
+# There's absolutely no warranty.
+#
+# The homepage URL for this framework is:
+#
+# http://www.openwall.com/phpass/
+#
+# Please be sure to update the Version line if you edit this file in any way.
+# It is suggested that you leave the main version number intact, but indicate
+# your project name (after the slash) and add your own revision information.
+#
+# Please do not change the "private" password hashing method implemented in
+# here, thereby making your hashes incompatible. However, if you must, please
+# change the hash type identifier (the "$P$") to something different.
+#
+# Obviously, since this code is in the public domain, the above are not
+# requirements (there can be none), but merely suggestions.
+#
+class PasswordHash {
+
+ protected $_itoa64;
+
+ protected $_iterationCountLog2;
+
+ protected $_portableHashes;
+
+ protected $_randomState;
+
+ public function __construct($iteration_count_log2, $portable_hashes)
+ {
+ $this->_itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
+
+ if ( $iteration_count_log2 < 4 || $iteration_count_log2 > 31 ) {
+ $iteration_count_log2 = 8;
+ }
+
+ $this->_iterationCountLog2 = $iteration_count_log2;
+ $this->_portableHashes = $portable_hashes;
+
+ $this->_randomState = microtime();
+
+ if ( function_exists('getmypid') ) {
+ $this->_randomState .= getmypid();
+ }
+ }
+
+ protected function _getRandomBytes($count)
+ {
+ $output = '';
+
+ if ( is_readable('/dev/urandom') && ($fh = @fopen('/dev/urandom', 'rb')) ) {
+ $output = fread($fh, $count);
+ fclose($fh);
+ }
+
+ if ( strlen($output) < $count ) {
+ $output = '';
+
+ for ($i = 0; $i < $count; $i += 16) {
+ $this->_randomState = md5(microtime() . $this->_randomState);
+ $output .= pack('H*', md5($this->_randomState));
+ }
+
+ $output = substr($output, 0, $count);
+ }
+
+ return $output;
+ }
+
+ protected function _encode64($input, $count)
+ {
+ $i = 0;
+ $output = '';
+
+ do {
+ $value = ord($input[$i++]);
+ $output .= $this->_itoa64[$value & 0x3f];
+
+ if ( $i < $count ) {
+ $value |= ord($input[$i]) << 8;
+ }
+
+ $output .= $this->_itoa64[($value >> 6) & 0x3f];
+
+ if ( $i++ >= $count ) {
+ break;
+ }
+
+ if ( $i < $count ) {
+ $value |= ord($input[$i]) << 16;
+ }
+
+ $output .= $this->_itoa64[($value >> 12) & 0x3f];
+
+ if ( $i++ >= $count ) {
+ break;
+ }
+
+ $output .= $this->_itoa64[($value >> 18) & 0x3f];
+ } while ($i < $count);
+
+ return $output;
+ }
+
+ protected function _genSaltPrivate($input)
+ {
+ $output = '$P$';
+ $output .= $this->_itoa64[min($this->_iterationCountLog2 + ((PHP_VERSION >= '5') ? 5 : 3), 30)];
+ $output .= $this->_encode64($input, 6);
+
+ return $output;
+ }
+
+ protected function _cryptPrivate($password, $setting)
+ {
+ $output = '*0';
+
+ if ( substr($setting, 0, 2) == $output ) {
+ $output = '*1';
+ }
+
+ $id = substr($setting, 0, 3);
+
+ # We use "$P$", phpBB3 uses "$H$" for the same thing
+ if ( $id != '$P$' && $id != '$H$' ) {
+ return $output;
+ }
+
+ $count_log2 = strpos($this->_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;
+ }
+
+ # We're kind of forced to use MD5 here since it's the only
+ # cryptographic primitive available in all versions of PHP
+ # currently in use. To implement our own low-level crypto
+ # in PHP would result in much worse performance and
+ # consequently in lower iteration counts and hashes that are
+ # quicker to crack (by non-PHP code).
+ if ( PHP_VERSION >= '5' ) {
+ $hash = md5($salt . $password, true);
+
+ do {
+ $hash = md5($hash . $password, true);
+ } while (--$count);
+ }
+ else {
+ $hash = pack('H*', md5($salt . $password));
+
+ do {
+ $hash = pack('H*', md5($hash . $password));
+ } while (--$count);
+ }
+
+ $output = substr($setting, 0, 12);
+ $output .= $this->_encode64($hash, 16);
+
+ return $output;
+ }
+
+ protected function _genSaltExtended($input)
+ {
+ $count_log2 = min($this->_iterationCountLog2 + 8, 24);
+
+ # This should be odd to not reveal weak DES keys, and the
+ # maximum valid value is (2**24 - 1) which is odd anyway.
+ $count = (1 << $count_log2) - 1;
+
+ $output = '_';
+ $output .= $this->_itoa64[$count & 0x3f];
+ $output .= $this->_itoa64[($count >> 6) & 0x3f];
+ $output .= $this->_itoa64[($count >> 12) & 0x3f];
+ $output .= $this->_itoa64[($count >> 18) & 0x3f];
+
+ $output .= $this->_encode64($input, 3);
+
+ return $output;
+ }
+
+ protected function _genSaltBlowfish($input)
+ {
+ # This one needs to use a different order of characters and a
+ # different encoding scheme from the one in encode64() above.
+ # We care because the last character in our encoded string will
+ # only represent 2 bits. While two known implementations of
+ # bcrypt will happily accept and correct a salt string which
+ # has the 4 unused bits set to non-zero, we do not want to take
+ # chances and we also do not want to waste an additional byte
+ # of entropy.
+ $itoa64 = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+
+ $output = '$2a$';
+ $output .= chr(ord('0') + $this->_iterationCountLog2 / 10);
+ $output .= chr(ord('0') + $this->_iterationCountLog2 % 10);
+ $output .= '$';
+
+ $i = 0;
+
+ do {
+ $c1 = ord($input[$i++]);
+ $output .= $itoa64[$c1 >> 2];
+ $c1 = ($c1 & 0x03) << 4;
+
+ if ( $i >= 16 ) {
+ $output .= $itoa64[$c1];
+ break;
+ }
+
+ $c2 = ord($input[$i++]);
+ $c1 |= $c2 >> 4;
+ $output .= $itoa64[$c1];
+ $c1 = ($c2 & 0x0f) << 2;
+
+ $c2 = ord($input[$i++]);
+ $c1 |= $c2 >> 6;
+ $output .= $itoa64[$c1];
+ $output .= $itoa64[$c2 & 0x3f];
+ } while (1);
+
+ return $output;
+ }
+
+ public function hashPassword($password)
+ {
+ $random = '';
+
+ if ( CRYPT_BLOWFISH == 1 && !$this->_portableHashes ) {
+ $random = $this->_getRandomBytes(16);
+ $hash = crypt($password, $this->_genSaltBlowfish($random));
+
+ if ( strlen($hash) == 60 ) {
+ return $hash;
+ }
+ }
+
+ if ( CRYPT_EXT_DES == 1 && !$this->_portableHashes ) {
+ if ( strlen($random) < 3 ) {
+ $random = $this->_getRandomBytes(3);
+ }
+
+ $hash = crypt($password, $this->_genSaltExtended($random));
+
+ if ( strlen($hash) == 20 ) {
+ return $hash;
+ }
+ }
+
+ if ( strlen($random) < 6 ) {
+ $random = $this->_getRandomBytes(6);
+ }
+
+ $hash = $this->_cryptPrivate($password, $this->_genSaltPrivate($random));
+
+ if ( strlen($hash) == 34 ) {
+ return $hash;
+ }
+
+ # Returning '*' on error is safe here, but would _not_ be safe
+ # in a crypt(3)-like function used _both_ for generating new
+ # hashes and for validating passwords against existing hashes.
+ return '*';
+ }
+
+ public function checkPassword($password, $stored_hash)
+ {
+ $hash = $this->_cryptPrivate($password, $stored_hash);
+
+ if ( $hash[0] == '*' ) {
+ $hash = crypt($password, $stored_hash);
+ }
+
+ return $hash == $stored_hash;
+ }
+}
\ No newline at end of file
Property changes on: kernel\utility\php_pass.php
___________________________________________________________________
Added: svn:keywords
+ Id
Added: svn:eol-style
+ LF
Index: units/admin/admin_tag_processor.php
===================================================================
--- units/admin/admin_tag_processor.php (revision 15541)
+++ units/admin/admin_tag_processor.php (working copy)
@@ -1055,10 +1055,11 @@
* Performs HTTP Authentification for administrative console
*
* @param Array $params
+ * @return bool
*/
function HTTPAuth($params)
{
- if (!$this->Application->ConfigValue('UseHTTPAuth')) {
+ if ( !$this->Application->ConfigValue('UseHTTPAuth') ) {
// http authentification not required
return true;
}
@@ -1066,12 +1067,12 @@
$super_admin_ips = defined('SA_IP') ? SA_IP : false;
$auth_bypass_ips = $this->Application->ConfigValue('HTTPAuthBypassIPs');
- if (($auth_bypass_ips && kUtil::ipMatch($auth_bypass_ips)) || ($super_admin_ips && kUtil::ipMatch($super_admin_ips))) {
+ if ( ($auth_bypass_ips && kUtil::ipMatch($auth_bypass_ips)) || ($super_admin_ips && kUtil::ipMatch($super_admin_ips)) ) {
// user ip is in ip bypass list
return true;
}
- if (!array_key_exists('PHP_AUTH_USER', $_SERVER)) {
+ if ( !array_key_exists('PHP_AUTH_USER', $_SERVER) ) {
// ask user to authentificate, when not authentificated before
return $this->_httpAuthentificate();
}
@@ -1079,7 +1080,7 @@
// validate user credentials (browsers remembers user/password
// and sends them each time page is visited, so no need to save
// authentification result in session)
- if ($this->Application->ConfigValue('HTTPAuthUsername') != $_SERVER['PHP_AUTH_USER']) {
+ if ( $this->Application->ConfigValue('HTTPAuthUsername') != $_SERVER['PHP_AUTH_USER'] ) {
// incorrect username
return $this->_httpAuthentificate();
}
@@ -1087,9 +1088,7 @@
$password_formatter = $this->Application->recallObject('kPasswordFormatter');
/* @var $password_formatter kPasswordFormatter */
- $password = $password_formatter->EncryptPassword($_SERVER['PHP_AUTH_PW'], 'b38');
-
- if ($this->Application->ConfigValue('HTTPAuthPassword') != $password) {
+ if ( !$password_formatter->checkPasswordFromSetting('HTTPAuthPassword', $_SERVER['PHP_AUTH_PW']) ) {
// incorrect password
return $this->_httpAuthentificate();
}
@@ -1101,7 +1100,7 @@
/**
* Ask user to authentificate
*
- * @return false
+ * @return bool
*/
function _httpAuthentificate()
{
Index: units/configuration/configuration_event_handler.php
===================================================================
--- units/configuration/configuration_event_handler.php (revision 15525)
+++ units/configuration/configuration_event_handler.php (working copy)
@@ -150,7 +150,7 @@
$password_formatter = $this->Application->recallObject('kPasswordFormatter');
/* @var $password_formatter kPasswordFormatter */
- $object->SetDBField('VariableValue', $password_formatter->EncryptPassword($object->GetDBField('VariableValue'), 'b38'));
+ $object->SetDBField('VariableValue', $password_formatter->hashPassword($object->GetDBField('VariableValue')));
}
}
Index: units/forms/form_submissions/form_submissions_eh.php
===================================================================
--- units/forms/form_submissions/form_submissions_eh.php (revision 15569)
+++ units/forms/form_submissions/form_submissions_eh.php (working copy)
@@ -149,7 +149,7 @@
if ($options['ElementType'] == 'password') {
$field_options['formatter'] = 'kPasswordFormatter';
- $field_options['encryption_method'] = 'plain';
+ $field_options['hashing_method'] = PasswordHashingMethod::NONE;
$field_options['verify_field'] = 'fld_' . $field_id . '_verify';
}
Index: units/helpers/user_helper.php
===================================================================
--- units/helpers/user_helper.php (revision 15545)
+++ units/helpers/user_helper.php (working copy)
@@ -35,18 +35,18 @@
*/
function loginUser($username, $password, $dry_run = false, $remember_login = false, $remember_login_cookie = '')
{
- if (!isset($this->event)) {
+ if ( !isset($this->event) ) {
$this->event = new kEvent('u:OnLogin');
}
- if (!$password && !$remember_login_cookie) {
+ if ( !$password && !$remember_login_cookie ) {
return LoginResult::INVALID_PASSWORD;
}
$object =& $this->getUserObject();
// process "Save Username" checkbox
- if ($this->Application->isAdmin) {
+ if ( $this->Application->isAdmin ) {
$save_username = $this->Application->GetVar('cb_save_username') ? $username : '';
$this->Application->Session->SetCookie('save_username', $save_username, strtotime('+1 year'));
@@ -57,13 +57,12 @@
// logging in "root" (admin only)
$super_admin = ($username == 'super-root') && $this->verifySuperAdmin();
- if ($this->Application->isAdmin && ($username == 'root') || ($super_admin && $username == 'super-root')) {
- $root_password = $this->Application->ConfigValue('RootPass');
+ if ( $this->Application->isAdmin && ($username == 'root') || ($super_admin && $username == 'super-root') ) {
$password_formatter = $this->Application->recallObject('kPasswordFormatter');
/* @var $password_formatter kPasswordFormatter */
- if ($root_password != $password_formatter->EncryptPassword($password, 'b38')) {
+ if ( !$password_formatter->checkPasswordFromSetting('RootPass', $password) ) {
return LoginResult::INVALID_PASSWORD;
}
@@ -71,10 +70,10 @@
$object->Clear($user_id);
$object->SetDBField('Username', 'root');
- if (!$dry_run) {
+ if ( !$dry_run ) {
$this->loginUserById($user_id, $remember_login_cookie);
- if ($super_admin) {
+ if ( $super_admin ) {
$this->Application->StoreVar('super_admin', 1);
}
@@ -91,27 +90,32 @@
$user_id = $this->getUserId($username, $password, $remember_login_cookie);
- if ($user_id) {
+ if ( $user_id ) {
$object->Load($user_id);
- if (!$this->checkBanRules($object)) {
+ if ( !$this->checkBanRules($object) ) {
return LoginResult::BANNED;
}
- if ($object->GetDBField('Status') == STATUS_ACTIVE) {
+ if ( $object->GetDBField('Status') == STATUS_ACTIVE ) {
if ( !$this->checkLoginPermission() ) {
return LoginResult::NO_PERMISSION;
}
- if (!$dry_run) {
+ if ( !$dry_run ) {
$this->loginUserById($user_id, $remember_login_cookie);
- if ($remember_login) {
+ if ( $remember_login ) {
// remember username & password when "Remember Login" checkbox us checked (when user is using login form on Front-End)
- $this->Application->Session->SetCookie('remember_login', $username . '|' . md5($password), strtotime('+1 month'));
+ $sql = 'SELECT MD5(Password)
+ FROM ' . TABLE_PREFIX . 'Users
+ WHERE PortalUserId = ' . $user_id;
+ $remember_login_hash = $this->Conn->GetOne($sql);
+
+ $this->Application->Session->SetCookie('remember_login', $username . '|' . $remember_login_hash, strtotime('+1 month'));
}
- if (!$remember_login_cookie) {
+ if ( !$remember_login_cookie ) {
// reset counters
$this->Application->resetCounters('UserSessions');
@@ -126,7 +130,7 @@
else {
$pending_template = $this->Application->GetVar('pending_disabled_template');
- if ($pending_template !== false && !$dry_run) {
+ if ( $pending_template !== false && !$dry_run ) {
// when user found, but it's not yet approved redirect hit to notification template
$this->event->redirect = $pending_template;
@@ -139,7 +143,7 @@
}
}
- if (!$dry_run) {
+ if ( !$dry_run ) {
$this->event->SetRedirectParam('pass', 'all');
// $this->event->SetRedirectParam('pass_category', 1); // to test
}
@@ -323,19 +327,60 @@
*/
function getUserId($username, $password, $remember_login_cookie)
{
- $password = md5($password);
+ if ( $remember_login_cookie ) {
+ list ($username, $password) = explode('|', $remember_login_cookie); // 0 - username, 1 - md5(password_hash)
+ }
- if ($remember_login_cookie) {
- list ($username, $password) = explode('|', $remember_login_cookie); // 0 - username, 1 - md5(password)
+ $sql = 'SELECT PortalUserId, Password, PasswordHashingMethod
+ FROM ' . TABLE_PREFIX . 'Users
+ WHERE Email = %1$s OR Username = %1$s';
+ $user_info = $this->Conn->GetRow(sprintf($sql, $this->Conn->qstr($username)));
+
+ if ( $user_info ) {
+ if ( $remember_login_cookie ) {
+ return md5($user_info['Password']) == $password;
+ }
+ else {
+ $password_formatter = $this->Application->recallObject('kPasswordFormatter');
+ /* @var $password_formatter kPasswordFormatter */
+
+ $hashing_method = $user_info['PasswordHashingMethod'];
+
+ if ( $password_formatter->checkPassword($password, $user_info['Password'], $hashing_method) ) {
+ if ( $hashing_method != PasswordHashingMethod::PHPPASS ) {
+ $this->_fixUserPassword($user_info['PortalUserId'], $password);
+ }
+
+ return $user_info['PortalUserId'];
+ }
+ }
}
- $sql = 'SELECT PortalUserId
- FROM ' . TABLE_PREFIX . 'Users
- WHERE (Email = %1$s OR Username = %1$s) AND (Password = %2$s)';
- return $this->Conn->GetOne( sprintf($sql, $this->Conn->qstr($username), $this->Conn->qstr($password) ) );
+ return false;
}
/**
+ * Apply new password hashing to given user's password
+ *
+ * @param int $user_id
+ * @param string $password
+ * @return void
+ * @access protected
+ */
+ protected function _fixUserPassword($user_id, $password)
+ {
+ $password_formatter = $this->Application->recallObject('kPasswordFormatter');
+ /* @var $password_formatter kPasswordFormatter */
+
+ $fields_hash = Array (
+ 'Password' => $password_formatter->hashPassword($password),
+ 'PasswordHashingMethod' => PasswordHashingMethod::PHPPASS,
+ );
+
+ $this->Conn->doUpdate($fields_hash, TABLE_PREFIX . 'Users', 'PortalUserId = ' . $user_id);
+ }
+
+ /**
* Process all required data and redirect logged-in user
*
* @param string $username
Index: units/users/users_config.php
===================================================================
--- units/users/users_config.php (revision 15586)
+++ units/users/users_config.php (working copy)
@@ -345,9 +345,17 @@
'unique' => Array (), 'default' => '',
),
'Password' => Array (
- 'formatter' => 'kPasswordFormatter', 'encryption_method' => 'md5', 'verify_field' => 'VerifyPassword',
- 'default' => 'd41d8cd98f00b204e9800998ecf8427e'
+ 'formatter' => 'kPasswordFormatter', 'hashing_method_field' => 'PasswordHashingMethod', 'verify_field' => 'VerifyPassword',
+ 'default' => ''
),
+ 'PasswordHashingMethod' => Array (
+ 'formatter' => 'kOptionsFormatter', 'options' => Array (
+ PasswordHashingMethod::MD5 => 'md5',
+ PasswordHashingMethod::MD5_AND_PHPPASS => 'md5+phppass',
+ PasswordHashingMethod::PHPPASS => 'phppass'
+ ),
+ 'default' => PasswordHashingMethod::PHPPASS
+ ),
'FirstName' => Array ('default' => ''),
'LastName' => Array ('default' => ''),
'Company' => Array ('default' => ''),
@@ -430,8 +438,8 @@
'VirtualFields' => Array (
'PrimaryGroup' => Array ('default' => ''),
'RootPassword' => Array (
- 'formatter' => 'kPasswordFormatter', 'encryption_method' => 'md5',
- 'verify_field' => 'VerifyRootPassword', 'default' => 'd41d8cd98f00b204e9800998ecf8427e'
+ 'formatter' => 'kPasswordFormatter', 'hashing_method' => PasswordHashingMethod::PHPPASS, 'salt' => 'b38',
+ 'verify_field' => 'VerifyRootPassword', 'default' => 'b38:d41d8cd98f00b204e9800998ecf8427e'
),
'EmailPassword' => Array ('default' => ''),
'FullName' => Array ('default' => ''),
@@ -520,6 +528,7 @@
'PortalUserId' => Array ('type' => 'int', 'not_null' => 1),
'Username' => Array ('type' => 'string', 'not_null' => 1),
'Password' => Array ('type' => 'string', 'skip_empty' => 1),
+ 'PasswordHashingMethod' => Array ('type' => 'int'),
'FirstName' => Array ('type' => 'string', 'not_null' => 1),
'LastName' => Array ('type' => 'string', 'not_null' => 1),
'Company' => Array ('type' => 'string','not_null' => 1),
Index: units/users/users_event_handler.php
===================================================================
--- units/users/users_event_handler.php (revision 15586)
+++ units/users/users_event_handler.php (working copy)
@@ -30,7 +30,6 @@
$permissions = Array (
// admin
'OnSetPersistantVariable' => Array('self' => 'view'), // because setting to logged in user only
- 'OnUpdateRootPassword' => Array('self' => true),
'OnUpdatePassword' => Array('self' => true),
'OnSaveSelected' => Array ('self' => 'view'),
'OnGeneratePassword' => Array ('self' => 'view'),
@@ -1157,16 +1156,6 @@
* Allows to change root password
*
* @param kEvent $event
- */
- function OnUpdateRootPassword($event)
- {
- $this->OnUpdatePassword($event);
- }
-
- /**
- * Allows to change root password
- *
- * @param kEvent $event
* @return void
* @access protected
*/
@@ -1197,21 +1186,11 @@
$object = $event->getObject(Array ('skip_autoload' => true));
/* @var $object UsersItem */
- // put salt to user's config
- $field_options = $object->GetFieldOptions('RootPassword');
- $field_options['salt'] = 'b38';
-
// this is internal hack to allow root/root passwords for dev
if ( $this->Application->isDebugMode() && $field_values['RootPassword'] == 'root' ) {
- $field_options['min_length'] = 4;
+ $object->SetFieldOption('RootPassword', 'min_length', 4);
}
- $object->SetFieldOptions('RootPassword', $field_options);
-
- $verify_options = $object->GetFieldOptions('VerifyRootPassword');
- $verify_options['salt'] = 'b38';
- $object->SetFieldOptions('VerifyRootPassword', $verify_options);
-
$this->RemoveRequiredFields($object);
$object->SetDBField('RootPassword', $this->Application->ConfigValue('RootPass'));
$object->SetFieldsFromHash($field_values, $this->getRequestProtectedFields($field_values));
@@ -1922,9 +1901,8 @@
/* @var $password_formatter kPasswordFormatter */
$new_root_password = kUtil::generatePassword();
- $new_root_password_encrypted = $password_formatter->EncryptPassword($new_root_password, 'b38');
- $this->Application->SetConfigValue('RootPass', $new_root_password_encrypted);
+ $this->Application->SetConfigValue('RootPass', $password_formatter->hashPassword($new_root_password));
$this->Application->EmailEventAdmin('ROOT.RESET.PASSWORD', null, Array ('password' => $new_root_password));
$event->SetRedirectParam('reset', 1);
improved_password_hashing_modules.patch [^] (1,456 bytes) 2012-10-18 10:33
[Show Content]
Index: in-commerce/units/orders/orders_config.php
===================================================================
--- in-commerce/units/orders/orders_config.php (revision 15560)
+++ in-commerce/units/orders/orders_config.php (working copy)
@@ -541,7 +541,7 @@
// for "one step checkout"
'UserPassword' => Array (
'type' => 'string',
- 'formatter' => 'kPasswordFormatter', 'encryption_method' => 'md5', 'verify_field' => 'VerifyUserPassword',
+ 'formatter' => 'kPasswordFormatter', 'hashing_method' => PasswordHashingMethod::MD5, 'verify_field' => 'VerifyUserPassword',
'skip_empty' => 1, 'default' => 'd41d8cd98f00b204e9800998ecf8427e'
),
Index: in-commerce/units/orders/orders_event_handler.php
===================================================================
--- in-commerce/units/orders/orders_event_handler.php (revision 15568)
+++ in-commerce/units/orders/orders_event_handler.php (working copy)
@@ -3691,6 +3691,12 @@
}
$this->Application->setUnitOption($event->Prefix, 'Fields', $fields);
+
+ $user_forms = $this->Application->getUnitOption('u', 'Forms');
+
+ $virtual_fields = $this->Application->getUnitOption($event->Prefix, 'VirtualFields');
+ $virtual_fields['UserPassword']['hashing_method'] = $user_forms['default']['Fields']['PasswordHashingMethod']['default'];
+ $this->Application->setUnitOption($event->Prefix, 'VirtualFields', $virtual_fields);
}
/**
|