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 26 Feb 2008 13:22:36 -0000 @@ -0,0 +1,226 @@ +> 6) & 0x3f]; + if ($i++ >= $count) { + break; + } + if ($i < $count) { + $value |= ord($input[$i]) << 16; + } + $output .= $itoa64[($value >> 12) & 0x3f]; + if ($i++ >= $count) { + break; + } + $output .= $itoa64[($value >> 18) & 0x3f]; + } while ($i < $count); + + return $output; +} + +/** + * Generates a random base 64 encoded salt prefixed with settings for the hash. + * + * @param $iteration_count_log2 + * This affects the number of iterations used in the hashing process. + * A larger value is more secure, but takes more time to complete. + */ +function _passwordhash_gensalt_private($iteration_count_log2) { + $itoa64 = _passwordhash_itoa64(); + // For compatibility with the phpass framework, increase the nominal count + // by 5 for PHP 5 and portable hashes. This makes it comparable to bcrypt. + $iteration_count_log2 += 5; + // Minimum log2 iterations is 7. + $iteration_count_log2 = max($iteration_count_log2, 7); + $output = '$P$'; + // Maximum log2 iterations is 30. + $output .= $itoa64[min($iteration_count_log2, 30)]; + $output .= _passwordhash_encode64(_passwordhash_random_bytes(6), 6); + + return $output; +} + +/** + * Hash a password using a secure portable hash. + * + * @param $password + * The password to hash + * @param $setting + * An existing hash or the output of _passwordhash_gensalt_private() + * + * @return + * The hashed password or FALSE on failure. + */ +function _passwordhash_crypt_private($password, $setting) { + + if (substr($setting, 0, 3) != '$P$') { + return FALSE; + } + $itoa64 = _passwordhash_itoa64(); + $count_log2 = strpos($itoa64, $setting[3]); + // 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 .= _passwordhash_encode64($hash, 16); + + return (strlen($output) == 34) ? $output : FALSE; +} + +function _passwordhash_get_count_log2($stored_hash) { + $itoa64 = _passwordhash_itoa64(); + // Subtract 5 to get the bcrypt equivalent number. + return strpos($itoa64, $stored_hash[3]) - 5; +} + +/** + * Hash a password using a secure hash. + * + * @param $password + * A plain-text password + * @param $iteration_count_log2 + * Optional. Specifies the iteration count. Generally used only during mass + * operations where a value different than the default is neeeded for speed. + * + * @return + * A string containing the hashed password (and a salt), or FALSE on failure. + */ +function user_hash_password($password, $iteration_count_log2 = NULL) { + if (empty($iteration_count_log2)) { + $iteration_count_log2 = variable_get('passwordhash_count_log2', DRUPAL_HASH_ITERATION); + } + return _passwordhash_crypt_private($password, _passwordhash_gensalt_private($iteration_count_log2)); +} + +/** + * 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 (substr($account->pass, 0, 4) == 'U$P$') { + // This may be an updated password from system_update_7001(). Such hashes + // have 'U' added as the first character and need an extra md5(). + $stored_hash = substr($account->pass, 1); + $password = md5($password); + } + else { + $stored_hash = $account->pass; + } + $hash = _passwordhash_crypt_private($password, $stored_hash); + return ($hash && $stored_hash == $hash); +} + +/** + * 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_new_hash($account) { + // Check whether this was an updated password. + if (strlen($account->pass) != 34 || (substr($account->pass, 0, 3) != '$P$')) { + return TRUE; + } + // Check whether the iteration count used differs from the standard number. + return (_passwordhash_get_count_log2($account->pass) != variable_get('passwordhash_count_log2', DRUPAL_HASH_ITERATION)); +} + Index: modules/system/system.install =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.install,v retrieving revision 1.240 diff -u -p -r1.240 system.install --- modules/system/system.install 23 Feb 2008 08:13:09 -0000 1.240 +++ modules/system/system.install 26 Feb 2008 13:22:36 -0000 @@ -2549,6 +2549,48 @@ function system_update_7000() { } /** + * Increase the length of the password field to accomodate better hashes. + * + * Also re-hashes all current passwords to improve security. This will take a + * while to run. + */ +function system_update_7001(&$sandbox) { + $ret = array('#finished' => 0); + // A lower number than DRUPAL_HASH_ITERATION to make the update run at a reasonable speed + $drupal_new_hash_iteration = 6; + // Multi-part update + if (!isset($sandbox['user_from'])) { + db_change_field($ret, 'users', 'pass', 'pass', array('type' => 'varchar', 'length' => 128, 'not null' => TRUE, 'default' => '')); + $sandbox['user_from'] = 0; + $sandbox['user_count'] = db_result(db_query("SELECT COUNT(uid) FROM {users}")); + } + else { + require_once variable_get('passwordhash_inc', './includes/passwordhash.inc'); + // Hash again all current hashed passwords + $has_rows = FALSE; + // Update this many per page load. + $count = 1000; + $result = db_query_range("SELECT uid, pass FROM {users} WHERE uid > 0 ORDER BY uid", $sandbox['user_from'], $count); + while ($account = db_fetch_array($result)) { + $has_rows = TRUE; + $new_hash = user_hash_password($account['pass'], $drupal_new_hash_iteration); + if ($new_hash) { + // Indicate an updated password. + $new_hash = 'U'. $new_hash; + db_query("UPDATE {users} SET pass = '%s' WHERE uid = %d", $new_hash, $account['uid']); + } + } + $ret['#finished'] = $sandbox['user_from']/$sandbox['user_count']; + $sandbox['user_from'] += $count; + if (!$has_rows) { + $ret['#finished'] = 1; + $ret[] = array('success' => TRUE, 'query' => "UPDATE {users} SET pass = 'U'. user_hash_password(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 26 Feb 2008 13:22:36 -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 26 Feb 2008 13:22:37 -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_new_hash($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; + } + } } }