Index: modules/system/system.install =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.install,v retrieving revision 1.119 diff -u -u -p -r1.119 system.install --- modules/system/system.install 30 May 2007 08:08:58 -0000 1.119 +++ modules/system/system.install 4 Jun 2007 22:13:15 -0000 @@ -3339,6 +3339,16 @@ function system_update_6022() { return $ret; } + +function system_update_6023() { + $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" * The next series of updates should start at 7000. Index: modules/user/user.module =================================================================== RCS file: /cvs/drupal/drupal/modules/user/user.module,v retrieving revision 1.788 diff -u -u -p -r1.788 user.module --- modules/user/user.module 30 May 2007 08:08:59 -0000 1.788 +++ modules/user/user.module 4 Jun 2007 22:13:15 -0000 @@ -94,10 +94,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; @@ -158,7 +154,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)) { @@ -223,7 +219,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 'uid': case 'mode': case 'sort': @@ -1069,7 +1065,13 @@ function user_login_validate($form, &$fo else if ($form_values['pass']) { $user = user_authenticate($form_values['name'], trim($form_values['pass'])); - 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_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_values['name'])); } @@ -1097,9 +1099,11 @@ function user_authenticate($name, $pass) global $user; // Try to log in the user locally. Don't set $user unless successful. - 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; + } } // Strip name and server from ID: @@ -2688,6 +2692,34 @@ 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'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + ); + $form['hash']['user_hash_method'] = array( + '#type' => 'radios', + '#title' => t('Hash Method'), + '#default_value' => variable_get('user_hash_method', 'passhash'), + '#options' => array('passhash' => t('secure'), 'md5' => t('original')), + '#description' => t('Select the method to hash the user passwords. Use "secure" unless you really have a good reason.'), + ); + $form['hash']['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']['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); } @@ -3151,3 +3183,19 @@ function _user_mail_notify($op, $account } return $result; } + +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.1 diff -u -u -p -r1.1 user.schema --- modules/user/user.schema 25 May 2007 12:46:46 -0000 1.1 +++ modules/user/user.schema 4 Jun 2007 22:13:15 -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'), =================================================================== 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 28 May 2007 14:28:27 -0000 @@ -0,0 +1,247 @@ + 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; +}