Commit ed59911f authored by Dries's avatar Dries

- Patch #29706 by pwolanin, solardiz, et al: more secure password hashing.

  This is a big and important patch for Drupal's security.  We are switching
  to much stronger password hashes that are also compatible with the Portable
  PHP password hashing framework.

  The new password hashes defeat a number of attacks, including:

  - The ability to try candidate passwords against multiple hashes at once.
  - The ability to use pre-hashed lists of candidate passwords.
  - The ability to determine whether two users have the same (or different)
    password without actually having to guess one of the passwords.

  Also implemented a pluggable password hashing API (similar to how an alternate
  cache mechanism can be used) to allow developers to readily substitute an
  alternative hashing and authentication scheme.

  Thanks all!
parent 76329845
......@@ -4,6 +4,11 @@ Drupal 7.0, xxxx-xx-xx (development version)
----------------------
- Security:
* Protected cron.php -- cron will only run if the proper key is provided.
* Changed to much stronger password hashes that are also compatible with the
Portable PHP password hashing framework.
* Implemented a pluggable password hashing API (similar to how an alternate
cache mechanism can be used) to allow developers to readily substitute
an alternative hashing and authentication scheme.
- Usability:
* Implemented drag-and-drop positioning for input format listings.
* Provide descriptions for permissions on the administration page.
......
......@@ -2307,6 +2307,44 @@ function drupal_urlencode($text) {
}
}
/**
* Returns a string of highly randomized bytes (over the full 8-bit range).
*
* This function is better than simply calling mt_rand() or any other built-in
* PHP function because it can return a long string of bytes (compared to < 4
* bytes normally from mt_rand()) and uses the best available pseudo-random source.
*
* @param $count
* The number of characters (bytes) to return in the string.
*/
function drupal_random_bytes($count) {
static $random_state;
// We initialize with the somewhat random PHP process ID on the first call.
if (empty($random_state)) {
$random_state = getmypid();
}
$output = '';
// /dev/urandom is available on many *nix systems and is considered the best
// commonly available pseudo-random source.
if ($fh = @fopen('/dev/urandom', 'rb')) {
$output = fread($fh, $count);
fclose($fh);
}
// If /dev/urandom is not available or returns no bytes, this loop will
// generate a good set of pseudo-random bytes on any system.
// Note that it may be important that our $random_state is passed
// through md5() prior to being rolled into $output, that the two md5()
// invocations are different, and that the extra input into the first one -
// the microtime() - is prepended rather than appended. This is to avoid
// directly leaking $random_state via the $output stream, which could
// allow for trivial prediction of further "random" numbers.
while (strlen($output) < $count) {
$random_state = md5(microtime() . mt_rand() . $random_state);
$output .= md5(mt_rand() . $random_state, TRUE);
}
return substr($output, 0, $count);
}
/**
* Ensure the private key variable used to generate tokens is set.
*
......@@ -2315,7 +2353,7 @@ function drupal_urlencode($text) {
*/
function drupal_get_private_key() {
if (!($key = variable_get('drupal_private_key', 0))) {
$key = md5(uniqid(mt_rand(), true)) . md5(uniqid(mt_rand(), true));
$key = md5(drupal_random_bytes(64));
variable_set('drupal_private_key', $key);
}
return $key;
......
......@@ -150,10 +150,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',
......@@ -295,3 +295,55 @@ function user_schema() {
return $schema;
}
/**
* @defgroup user-updates-6.x-to-7.x User updates from 6.x to 7.x
* @{
*/
/**
* Increase the length of the password field to accommodate better hashes.
*
* Also re-hashes all current passwords to improve security. This may be a
* lengthy process, and is performed batch-wise.
*/
function user_update_7000(&$sandbox) {
$ret = array('#finished' => 0);
// Lower than DRUPAL_HASH_COUNT to make the update run at a reasonable speed.
$hash_count_log2 = 11;
// 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('password_inc', './includes/password.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'], $hash_count_log2);
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 user-updates-6.x-to-7.x"
* The next series of updates should start at 8000.
*/
......@@ -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,13 @@ function user_save($account, $array = array(), $category = 'account') {
$user_fields = $table['fields'];
if (!empty($array['pass'])) {
$array['pass'] = md5($array['pass']);
// Allow alternate password hashing schemes.
require_once variable_get('password_inc', './includes/password.inc');
$array['pass'] = user_hash_password(trim($array['pass']));
// Abort if the hashing failed and returned FALSE.
if (!$array['pass']) {
return FALSE;
}
}
else {
// Avoid overwriting an existing password with a blank password.
......@@ -1283,12 +1289,26 @@ function user_login_final_validate($form, &$form_state) {
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 password hashing schemes.
require_once variable_get('password_inc', './includes/password.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;
}
}
}
}
......
#!/usr/bin/php
<?php
// $Id$
/**
* Drupal hash script - to generate a hash from a plaintext password
*
* Check for your PHP interpreter - on Windows you'll probably have to
* replace line 1 with
* #!c:/program files/php/php.exe
*
* @param password1 [password2 [password3 ...]]
* Plain-text passwords in quotes (or with spaces backslah escaped).
*/
function variable_get($x, $default) {
return $default;
}
if (version_compare(PHP_VERSION, "5.2.0", "<")) {
$version = PHP_VERSION;
echo <<<EOF
ERROR: This script requires at least PHP version 5.2.0. You invoked it with
PHP version {$version}.
\n
EOF;
exit;
}
$script = basename(array_shift($_SERVER['argv']));
if (in_array('--help', $_SERVER['argv']) || empty($_SERVER['argv'])) {
echo <<<EOF
Generate Drupal password hashes from the shell.
Usage: {$script} [OPTIONS] "<plan-text password>"
Example: {$script} "mynewpassword"
All arguments are long options.
--help Print this page.
--root <path>
Set the working directory for the script to the specified path.
To execute this script this has to be the root directory of your
Drupal installation, e.g. /home/www/foo/drupal (assuming Drupal
running on Unix). Use surrounding quotation marks on Windows.
"<password1>" ["<password2>" ["<password3>" ...]]
One or more plan-text passwords enclosed by double quotes. The
output hash may be manually entered into the {users}.pass field to
change a password via SQL to a known value.
To run this script without the --root argument invoke it from the root directory
of your Drupal installation as
./scripts/{$script}
\n
EOF;
exit;
}
$passwords = array();
// Parse invocation arguments.
while ($param = array_shift($_SERVER['argv'])) {
switch ($param) {
case '--root':
// Change the working directory.
$path = array_shift($_SERVER['argv']);
if (is_dir($path)) {
chdir($path);
}
break;
default:
// Add a password to the list to be processed.
$passwords[] = $param;
break;
}
}
include_once('includes/password.inc');
include_once('includes/common.inc');
foreach ($passwords as $password) {
print("\npassword: $password \t\thash: ". user_hash_password($password) ."\n");
}
print("\n");
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment