Index: modules/system/system.install =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.install,v retrieving revision 1.130 diff -u -u -p -r1.130 system.install --- modules/system/system.install 5 Jul 2007 08:48:58 -0000 1.130 +++ modules/system/system.install 7 Jul 2007 23:16:03 -0000 @@ -3439,6 +3439,14 @@ function system_update_6025() { return $ret; } +function system_update_6026() { + $ret = array(); + + // Increase the users table pass field to accomodate larger secure hashes + db_update_field($ret, 'users', 'pass'); + + return $ret; +} /** * @} End of "defgroup updates-5.x-to-6.x" Index: modules/user/passhash.inc =================================================================== RCS file: modules/user/passhash.inc diff -N modules/user/passhash.inc --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/user/passhash.inc 7 Jul 2007 23:16:03 -0000 @@ -0,0 +1,290 @@ + 31) { + $iteration_count_log2 = 8; + } + + $random = ''; + + if (CRYPT_BLOWFISH == 1 && !$portable_hashes) { + $random = _passhash_get_random_bytes(16); + $hash = crypt($password, _passhash_gensalt_blowfish($random, $iteration_count_log2)); + if (strlen($hash) == 60) { + return $hash; + } + } + + if (CRYPT_EXT_DES == 1 && !$portable_hashes) { + if (strlen($random) < 3) { + $random = _passhash_get_random_bytes(3); + } + $hash = crypt($password, _passhash_gensalt_extended($random, $iteration_count_log2)); + if (strlen($hash) == 20) { + return $hash; + } + } + + if (strlen($random) < 6) { + $random = _passhash_get_random_bytes(6); + } + $hash = _passhash_crypt_private($password, _passhash_gensalt_private($random, $iteration_count_log2)); + 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 '*'; +} + +/** + * Check a password hash. + * + * @param $password + * The password to check + * @param $stored_hash + * The stored hash to check against + * + * @return + * TRUE if the password matches the hash, FALSE if it doesn't + */ +function passhash_check_password($password, $stored_hash) { + $hash = _passhash_crypt_private($password, $stored_hash); + if ($hash[0] == '*') { + $hash = crypt($password, $stored_hash); + } + + return $hash == $stored_hash; +} + +/** + * Returns the character set used in the hash + */ +function _passhash_itoa64() { + return './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; +} + +/** + * Returns a string of highly randomized characters (over the full 8-bit range) + * + * @param $count + * The number of characters to return + */ +function _passhash_get_random_bytes($count) { + $output = ''; + if (($fh = @fopen('/dev/urandom', 'rb'))) { + $output = fread($fh, $count); + fclose($fh); + } + + if (strlen($output) < $count) { + $output = ''; + $random_state = microtime() . getmypid(); + for ($i = 0; $i < $count; $i += 16) { + $random_state = md5(microtime() . $random_state); + $output .= pack('H*', md5($random_state)); + } + $output = substr($output, 0, $count); + } + + return $output; +} + +/** + * Encodes a string into a predefined character set, up to a set number of + * characters + * + * @param $input + * The string to encode + * @param $count + * The number of characters to encode + * @return + * Encoded string + */ +function _passhash_encode64($input, $count) { + $output = ''; + $i = 0; + $itoa64 = _passhash_itoa64(); + do { + $value = ord($input[$i++]); + $output .= $itoa64[$value & 0x3f]; + if ($i < $count) { + $value |= ord($input[$i]) << 8; + } + $output .= $itoa64[($value >> 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; +} + +/** + * 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. + */ + +/** + * Generates an encoded salt string for use with _passhash_crypt_private() + */ +function _passhash_gensalt_private($input, $iteration_count_log2) { + $output = '$P$'; + $itoa64 = _passhash_itoa64(); + $output .= $itoa64[min($iteration_count_log2 + ((PHP_VERSION >= '5') ? 5 : 3), 30)]; + $output .= _passhash_encode64($input, 6); + + return $output; +} + +/** + * Computes a portable password hash (fallback when CRYPT_BLOWFISH and + * CRYPT_EXT_DES are not available or portable hashes are forced) + */ +function _passhash_crypt_private($password, $setting) { + $output = '*0'; + if (substr($setting, 0, 2) == $output) { + $output = '*1'; + } + + if (substr($setting, 0, 3) != '$P$') { + return $output; + } + + $count_log2 = strpos(_passhash_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; + } + + // 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 .= _passhash_encode64($hash, 16); + + return $output; +} + +/** + * Generates an encoded salt string for use with CRYPT_EXT_DES crypt() + */ +function _passhash_gensalt_extended($input, $iteration_count_log2) { + $count_log2 = min($iteration_count_log2 + 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; + + $itoa64 = _passhash_itoa64(); + $output = '_'; + $output .= $itoa64[$count & 0x3f]; + $output .= $itoa64[($count >> 6) & 0x3f]; + $output .= $itoa64[($count >> 12) & 0x3f]; + $output .= $itoa64[($count >> 18) & 0x3f]; + + $output .= _passhash_encode64($input, 3); + + return $output; +} + +/** + * Generates an encoded salt string for use with CRYPT_BLOWFISH crypt() + */ +function _passhash_gensalt_blowfish($input, $iteration_count_log2) { + // This one needs to use a different order of characters and a + // different encoding scheme from the one in _passhash_encode64(). + // 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 that + // 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') + $iteration_count_log2 / 10); + $output .= chr(ord('0') + $iteration_count_log2 % 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; +} Index: modules/user/user.js =================================================================== RCS file: /cvs/drupal/drupal/modules/user/user.js,v retrieving revision 1.3 diff -u -u -p -r1.3 user.js --- modules/user/user.js 1 Jul 2007 15:37:10 -0000 1.3 +++ modules/user/user.js 7 Jul 2007 23:16:03 -0000 @@ -176,5 +176,9 @@ Drupal.behaviors.userSettings = function $('div.user-admin-picture-radios input[@type=radio]:not(.userSettings-processed)', context).addClass('userSettings-processed').click(function () { $('div.user-admin-picture-settings', context)[['hide', 'show'][this.value]](); }); + $('div.user-admin-hash-radios input[@type=radio]:not(.userSettings-processed)', context).addClass('userSettings-processed').click(function () { + value = this.value == 'passhash' ? 1 : 0; + $('div.user-admin-hash-settings', context)[['hide', 'show'][value]](); + }); }; Index: modules/user/user.module =================================================================== RCS file: /cvs/drupal/drupal/modules/user/user.module,v retrieving revision 1.818 diff -u -u -p -r1.818 user.module --- modules/user/user.module 5 Jul 2007 08:48:58 -0000 1.818 +++ modules/user/user.module 7 Jul 2007 23:16:03 -0000 @@ -139,10 +139,6 @@ function user_load($array = array()) { $query[] = "$key = %d"; $params[] = $value; } - else if ($key == 'pass') { - $query[] = "pass = '%s'"; - $params[] = md5($value); - } else { $query[]= "LOWER($key) = LOWER('%s')"; $params[] = $value; @@ -203,7 +199,7 @@ function user_save($account, $array = ar foreach ($array as $key => $value) { if ($key == 'pass' && !empty($value)) { $query .= "$key = '%s', "; - $v[] = md5($value); + $v[] = _user_get_hash($value); } else if ((substr($key, 0, 4) !== 'auth') && ($key != 'pass')) { if (in_array($key, $user_fields)) { @@ -266,7 +262,7 @@ function user_save($account, $array = ar switch ($key) { case 'pass': $fields[] = $key; - $values[] = md5($value); + $values[] = _user_get_hash($value); $s[] = "'%s'"; break; case 'mode': case 'sort': @@ -1124,7 +1120,13 @@ function user_login_authenticate_validat **/ function user_login_final_validate($form, &$form_state) { global $user; - if (!$user->uid) { + if ($user->uid) { + // replace wrongly hashed passwords with the selected hashing method + if ((variable_get('user_hash_method', 'passhash') == 'passhash') != ($user->pass[0] == '$' || $user->pass[0] == '_')) { + user_save($user, array('pass' => trim($form_state['values']['pass']))); + } + } + else { form_set_error('name', t('Sorry, unrecognized username or password. Have you forgotten your password?', array('@password' => url('user/password')))); watchdog('user', 'Login attempt failed for %user.', array('%user' => $form_state['values']['name'])); } @@ -1139,9 +1141,11 @@ function user_login_final_validate($form function user_authenticate($name, $pass) { global $user; - if ($account = user_load(array('name' => $name, 'pass' => $pass, 'status' => 1))) { - $user = $account; - return $user; + if ($account = user_load(array('name' => $name, 'status' => 1))) { + if (_user_valid_hash($pass, $account->pass)) { + $user = $account; + return $user; + } } } @@ -2757,6 +2761,46 @@ function user_admin_settings() { '#default_value' => variable_get('user_picture_guidelines', ''), '#description' => t("This text is displayed at the picture upload form in addition to the default guidelines. It's useful for helping or instructing your users."), ); + // let the admin select the password hash strength + $form['hash'] = array( + '#type' => 'fieldset', + '#title' => t('Password Hash'), + ); + $user_hash_method = variable_get('user_hash_method', 'passhash'); + $form['hash']['user_hash_method'] = array( + '#type' => 'radios', + '#title' => t('Hash Method'), + '#default_value' => $user_hash_method, + '#options' => array('md5' => t('original (MD5)'), 'passhash' => t('secure')), + '#description' => t('Select the method to hash the user passwords. Use "secure" unless you really have a good reason.'), + '#prefix' => '
', + '#suffix' => '
', + ); + // If JS is enabled, and the radio is defaulting to off, hide all + // the settings on page load via .css using the js-hide class so + // that there's no flicker. + $css_class = 'user-admin-hash-settings'; + if ($user_hash_method != 'passhash') { + $css_class .= ' js-hide'; + } + $form['hash']['settings'] = array( + '#prefix' => '
', + '#suffix' => '
', + ); + $form['hash']['settings']['user_hash_strength'] = array( + '#type' => 'select', + '#title' => t('Hash Strength'), + '#default_value' => variable_get('user_hash_strength', 8), + '#options' => drupal_map_assoc(range(4, 12)), + '#description' => t('Select a higher number for a more secure but slower password hash.'), + ); + $form['hash']['settings']['user_hash_portable'] = array( + '#type' => 'radios', + '#title' => t('Portable Hash'), + '#default_value' => variable_get('user_hash_portable', FALSE), + '#options' => array(FALSE => t('No'), TRUE => t('Yes')), + '#description' => t('Force the use of weaker hashes that are guaranteed to be portable across servers.'), + ); return system_settings_form($form); } @@ -3324,3 +3368,19 @@ function user_block_ip_action() { db_query("INSERT INTO {access} (mask, type, status) VALUES ('%s', '%s', %d)", $_SERVER['REMOTE_ADDR'], 'host', 0); watchdog('action', 'Banned IP address %ip', array('%ip' => $_SERVER['REMOTE_ADDR'])); } + +function _user_get_hash($pass) { + include_once './'. drupal_get_path('module', 'user') .'/passhash.inc'; + if (variable_get('user_hash_method', 'passhash') == 'passhash') { + return passhash_hash_password($pass, variable_get('user_hash_strength', 8), variable_get('user_hash_portable', FALSE)); + } + return md5($pass); +} + +function _user_valid_hash($pass, $hash) { + if ($hash[0] == '$' || $hash[0] == '_') { + include_once './'. drupal_get_path('module', 'user') .'/passhash.inc'; + return passhash_check_password($pass, $hash); + } + return md5($pass) == $hash; +} Index: modules/user/user.schema =================================================================== RCS file: /cvs/drupal/drupal/modules/user/user.schema,v retrieving revision 1.2 diff -u -u -p -r1.2 user.schema --- modules/user/user.schema 15 Jun 2007 07:15:25 -0000 1.2 +++ modules/user/user.schema 7 Jul 2007 23:16:03 -0000 @@ -47,7 +47,7 @@ function user_schema() { 'fields' => array( 'uid' => array('type' => 'serial', 'unsigned' => TRUE, 'not null' => TRUE), 'name' => array('type' => 'varchar', 'length' => 60, 'not null' => TRUE, 'default' => ''), - 'pass' => array('type' => 'varchar', 'length' => 32, 'not null' => TRUE, 'default' => ''), + 'pass' => array('type' => 'varchar', 'length' => 60, 'not null' => TRUE, 'default' => ''), 'mail' => array('type' => 'varchar', 'length' => 64, 'not null' => FALSE, 'default' => ''), 'mode' => array('type' => 'int', 'not null' => TRUE, 'default' => 0, 'size' => 'tiny'), 'sort' => array('type' => 'int', 'not null' => FALSE, 'default' => 0, 'size' => 'tiny'),