Index: includes/passwordhash.inc =================================================================== RCS file: includes/passwordhash.inc diff -N includes/passwordhash.inc --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ includes/passwordhash.inc 22 Feb 2008 20:57:44 -0000 @@ -0,0 +1,172 @@ +itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; + $this->iteration_count_log2 = max($iteration_count_log2, DRUPAL_HASH_ITERATION); + } + + function get_random_bytes($count) { + return substr(hash('sha256', uniqid(mt_rand(), TRUE)) . md5(uniqid(mt_rand(), TRUE), TRUE), 0, $count); + } + + function encode64($input, $count) { + $output = ''; + $i = 0; + 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; + } + + function gensalt_private() { + $output = '$P$'; + // Maximum log2 iterations is 30. + $output .= $this->itoa64[min($this->iteration_count_log2, 30)]; + $output .= $this->encode64($this->get_random_bytes(6), 6); + + return $output; + } + + function crypt_private($password, $setting) { + + if (substr($setting, 0, 3) != '$P$') { + return FALSE; + } + + $count_log2 = $this->GetHashCountLog2($setting); + // Portable hashes may come from elsewhere, so we allow < DRUPAL_HASH_ITERATION + if ($count_log2 < 7 || $count_log2 > 30) { + return FALSE; + } + + $salt = substr($setting, 4, 8); + if (strlen($salt) != 8) { + return FALSE; + } + + // 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). + + $count = 1 << $count_log2; + + $hash = md5($salt . $password, TRUE); + do { + $hash = md5($hash . $password, TRUE); + } while (--$count); + + $output = substr($setting, 0, 12); + $output .= $this->encode64($hash, 16); + + return (strlen($output) == 34) ? $output : FALSE; + } + + function HashPassword($password) { + return $this->crypt_private($password, $this->gensalt_private()); + } + + function CheckPassword($password, $stored_hash) { + $hash = $this->crypt_private($password, $stored_hash); + return ($hash && $stored_hash == $hash); + } + + function GetHashCountLog2($stored_hash) { + return strpos($this->itoa64, $stored_hash[3]); + } +} + +/** + * Hash a password using a secure hash. + * + * @param $password + * A plain-text password + * + * @return + * A string containing the hashed password (and a salt), or FALSE on failure. + */ +function user_hash_password($password) { + $ph = new PasswordHash(); + return $ph->HashPassword($password); +} + +/** + * Check whether a plain text password matches a stored hashed password. + * + * @param $password + * A plain-text password + * @param $account + * A user object with at least the fields from the {users} table. + * + * @return + * TRUE or FALSE. + */ +function user_check_password($password, $account) { + if (strlen($account->pass) <= 32 || (substr($account->pass, 0, 3) != '$P$')) { + // This may be an updated password that was re-hashed with MD5 in + // system_update_7001(). + return $account->pass == md5($account->init . $account->created . md5($password)); + } + else { + $ph = new PasswordHash(); + return $ph->CheckPassword($password, $account->pass); + } +} + +/** + * Check whether a user's hashed password needs to be replaced with a new hash. + * + * @param $account + * A user object with at least the fields from the {users} table. + * + * @return + * TRUE or FALSE. + */ +function user_needs_rehash($account) { + // Check whether this was an updated password - i.e. still a normal MD5. + if (strlen($account->pass) <= 32 || (substr($account->pass, 0, 3) != '$P$')) { + return TRUE; + } + // Check whether the iteration count used is less than the standard number. + $ph = new PasswordHash(); + return ($ph->GetHashCountLog2($account->pass) < DRUPAL_HASH_ITERATION); +} + Index: modules/system/system.install =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.install,v retrieving revision 1.239 diff -u -p -r1.239 system.install --- modules/system/system.install 20 Feb 2008 13:46:41 -0000 1.239 +++ modules/system/system.install 22 Feb 2008 20:57:44 -0000 @@ -2505,6 +2505,20 @@ function system_update_7000() { } /** + * Increase the length of the password field to accomodate better hashes. + * + * Also re-hashes all current passwords with CONCAT(init, created) as a salt. + * This provides a very modest immediate improvement in security. + */ +function system_update_7001() { + $ret = array(); + db_change_field($ret, 'users', 'pass', 'pass', array('type' => 'varchar', 'length' => 128, 'not null' => TRUE, 'default' => '')); + // Rehash with a salt all current hashed passwords. + $ret[] = update_sql("UPDATE {users} SET pass = MD5(CONCAT(CONCAT(init, created), pass)) WHERE uid > 0"); + return $ret; +} + +/** * @} End of "defgroup updates-6.x-to-7.x" * The next series of updates should start at 8000. */ Index: modules/user/user.install =================================================================== RCS file: /cvs/drupal/drupal/modules/user/user.install,v retrieving revision 1.6 diff -u -p -r1.6 user.install --- modules/user/user.install 18 Feb 2008 16:53:36 -0000 1.6 +++ modules/user/user.install 22 Feb 2008 20:57:44 -0000 @@ -144,10 +144,10 @@ function user_schema() { ), 'pass' => array( 'type' => 'varchar', - 'length' => 32, + 'length' => 128, 'not null' => TRUE, 'default' => '', - 'description' => t("User's password (md5 hash)."), + 'description' => t("User's password (hashed)."), ), 'mail' => array( 'type' => 'varchar', Index: modules/user/user.module =================================================================== RCS file: /cvs/drupal/drupal/modules/user/user.module,v retrieving revision 1.896 diff -u -p -r1.896 user.module --- modules/user/user.module 20 Feb 2008 13:46:43 -0000 1.896 +++ modules/user/user.module 22 Feb 2008 20:57:46 -0000 @@ -157,7 +157,7 @@ function user_load($array = array()) { } else if ($key == 'pass') { $query[] = "pass = '%s'"; - $params[] = md5($value); + $params[] = $value; } else { $query[]= "LOWER($key) = LOWER('%s')"; @@ -214,7 +214,9 @@ function user_save($account, $array = ar $user_fields = $table['fields']; if (!empty($array['pass'])) { - $array['pass'] = md5($array['pass']); + // Allow alternate hashing schemes. + require_once variable_get('passwordhash.inc', './includes/passwordhash.inc'); + $array['pass'] = user_hash_password(trim($array['pass'])); } else { // Avoid overwriting an existing password with a blank password. @@ -1283,12 +1285,26 @@ function user_login_final_validate($form function user_authenticate($form_values = array()) { global $user; + $password = trim($form_values['pass']); // Name and pass keys are required. - if (!empty($form_values['name']) && !empty($form_values['pass']) && - $account = user_load(array('name' => $form_values['name'], 'pass' => trim($form_values['pass']), 'status' => 1))) { - $user = $account; - user_authenticate_finalize($form_values); - return $user; + if (!empty($form_values['name']) && !empty($password)) { + $account = db_fetch_object(db_query("SELECT * FROM {users} WHERE name = '%s' AND status = 1", $form_values['name'])); + if ($account) { + // Allow alternate hashing schemes. + require_once variable_get('passwordhash.inc', './includes/passwordhash.inc'); + if (user_check_password($password, $account)) { + if (user_needs_rehash($account)) { + $new_hash = user_hash_password($password); + if ($new_hash) { + db_query("UPDATE {users} SET pass = '%s' WHERE uid = %d", $new_hash, $account->uid); + } + } + $account = user_load(array('uid' => $account->uid, 'status' => 1)); + $user = $account; + user_authenticate_finalize($form_values); + return $user; + } + } } }