diff -r 9cdfe82c56cd -r eb8b23f11744 includes/sessions.php --- a/includes/sessions.php Sat Jan 03 18:11:18 2009 -0500 +++ b/includes/sessions.php Sun Jan 04 00:55:40 2009 -0500 @@ -2,7 +2,7 @@ /* * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between - * Version 1.1.5 (Caoineag alpha 5) + * Version 1.1.6 (Caoineag beta 1) * Copyright (C) 2006-2008 Dan Fuhry * sessions.php - everything related to security and user management * @@ -501,7 +501,7 @@ } else { - $key = strrev($_REQUEST['auth']); + $key = $_REQUEST['auth']; if ( !empty($key) && ( strlen($key) / 2 ) % 4 == 0 ) { $super = $this->validate_session($key); @@ -645,7 +645,14 @@ { global $db, $session, $paths, $template, $plugins; // Common objects - $pass_hashed = ( $already_md5ed ) ? $password : md5($password); + if ( $already_md5ed ) + { + // No longer supported + return array( + 'mode' => 'error', + 'error' => '$already_md5ed is no longer supported (set this parameter to false and make sure the password you send to $session->login_without_crypto() is not hashed)' + ); + } // Replace underscores with spaces in username // (Added in 1.0.2) @@ -654,7 +661,7 @@ // Perhaps we're upgrading Enano? if($this->compat) { - return $this->login_compat($username, $pass_hashed, $level); + return $this->login_compat($username, md5($password), $level); } if ( !defined('IN_ENANO_INSTALL') ) @@ -710,7 +717,13 @@ $success = false; // Retrieve the real password from the database - $this->sql('SELECT password,old_encryption,user_id,user_level,temp_password,temp_password_time FROM '.table_prefix.'users WHERE ' . ENANO_SQLFUNC_LOWERCASE . '(username)=\''.$this->prepare_text(strtolower($username)).'\';'); + $username_db = $db->escape(strtolower($username)); + if ( !$db->sql_query('SELECT password,password_salt,old_encryption,user_id,user_level,temp_password,temp_password_time FROM '.table_prefix."users\n" + . " WHERE " . ENANO_SQLFUNC_LOWERCASE . "(username) = '$username_db';") ) + { + $this->sql('SELECT password,\'\' AS password_salt,old_encryption,user_id,user_level,temp_password,temp_password_time FROM '.table_prefix."users\n" + . " WHERE " . ENANO_SQLFUNC_LOWERCASE . "(username) = '$username_db';"); + } if($db->numrows() < 1) { // This wasn't logged in <1.0.2, dunno how it slipped through @@ -747,8 +760,8 @@ if((intval($row['temp_password_time']) + 3600*24) > time() ) { - $temp_pass = $aes->decrypt( $row['temp_password'], $this->private_key, ENC_HEX ); - if( md5($temp_pass) == $pass_hashed ) + $temp_pass = hmac_sha1($password, $row['password_salt']); + if( $temp_pass === $row['temp_password'] ) { $code = $plugins->setHook('login_password_reset'); foreach ( $code as $cmd ) @@ -759,31 +772,41 @@ return array( 'success' => false, 'error' => 'valid_reset', - 'redirect_url' => makeUrlComplete('Special', 'PasswordReset/stage2/' . $row['user_id'] . '/' . $row['temp_password']) + 'redirect_url' => makeUrlComplete('Special', 'PasswordReset/stage2/' . $row['user_id'] . '/' . $this->pk_encrypt($password)) ); } } - if($row['old_encryption'] == 1) + if ( $row['old_encryption'] == 1 ) { // The user's password is stored using the obsolete and insecure MD5 algorithm - we'll update the field with the new password - if($pass_hashed == $row['password'] && !$already_md5ed) + if(md5($password) === $row['password']) { - $pass_stashed = $aes->encrypt($password, $this->private_key, ENC_HEX); - $this->sql('UPDATE '.table_prefix.'users SET password=\''.$pass_stashed.'\',old_encryption=0 WHERE user_id='.$row['user_id'].';'); + $hmac_secret = AESCrypt::randkey(20); + $password_hmac = hmac_sha1($password, $hmac_secret); + $this->sql('UPDATE '.table_prefix."users SET password = '$password_hmac', password_salt = '$hmac_secret', old_encryption = 0 WHERE user_id={$row['user_id']};"); $success = true; } - elseif($pass_hashed == $row['password'] && $already_md5ed) + } + else if ( $row['old_encryption'] == 2 ) + { + // Our password field uses the 1.0RC1-1.1.5 encryption format + $real_pass = $aes->decrypt($row['password'], $this->private_key); + if($password === $real_pass) { - // We don't have the real password so don't bother with encrypting it, just call it success and get out of here + $success = true; + $hmac_secret = AESCrypt::randkey(20); + $password_hmac = hmac_sha1($password, $hmac_secret); + $this->sql('UPDATE '.table_prefix."users SET password = '$password_hmac', password_salt = '$hmac_secret', old_encryption = 0 WHERE user_id={$row['user_id']};"); $success = true; } } else { - // Our password field is up-to-date with the >=1.0RC1 encryption standards, so decrypt the password in the table and see if we have a match - $real_pass = $aes->decrypt($row['password'], $this->private_key); - if($pass_hashed == md5($real_pass)) + // Password uses HMAC-SHA1 + $user_challenge = hmac_sha1($password, $row['password_salt']); + $password_hmac =& $row['password']; + if ( $user_challenge === $password_hmac ) { $success = true; } @@ -795,7 +818,7 @@ 'success' => false, 'error' => 'too_big_for_britches' ); - $sess = $this->register_session(intval($row['user_id']), $username, $real_pass, $level, $remember); + $sess = $this->register_session($row['user_id'], $username, $password_hmac, $level, $remember); if($sess) { if($level > USER_LEVEL_MEMBER) @@ -886,37 +909,34 @@ * Basically the session key is a hex-encoded cookie (encrypted with the site's private key) that says "u=[username];p=[sha1 of password];s=[unique key id]" * @param int $user_id * @param string $username - * @param string $password + * @param string $password_hmac The HMAC of the user's password, right from the database * @param int $level The level of access to grant, defaults to USER_LEVEL_MEMBER * @param bool $remember Whether the session should be long-term (true) or not (false). Defaults to short-term. * @return bool */ - function register_session($user_id, $username, $password, $level = USER_LEVEL_MEMBER, $remember = false) + function register_session($user_id, $username, $password_hmac, $level = USER_LEVEL_MEMBER, $remember = false) { global $db, $session, $paths, $template, $plugins; // Common objects // Random key identifier - $salt = md5(microtime() . mt_rand()); + $salt = ''; + for ( $i = 0; $i < 32; $i++ ) + { + $salt .= chr(mt_rand(32, 127)); + } - // SHA1 hash of password, stored in the key - $passha1 = sha1($password); - - // Unencrypted session key - $session_key = "u=$username;p=$passha1;s=$salt"; + // Session key + $session_key = hmac_sha1($password_hmac, $salt); // Type of key $key_type = ( $level > USER_LEVEL_MEMBER ) ? SK_ELEV : ( $remember ? SK_LONG : SK_SHORT ); - // Encrypt the key - $aes = AESCrypt::singleton(AES_BITS, AES_BLOCKSIZE); - $session_key = $aes->encrypt($session_key, $this->private_key, ENC_HEX); - // If we're registering an elevated-privilege key, it needs to be on GET if($level > USER_LEVEL_MEMBER) { // Reverse it - cosmetic only ;-) - $hexkey = strrev($session_key); + $hexkey = $session_key; $this->sid_super = $hexkey; $_GET['auth'] = $hexkey; } @@ -943,10 +963,10 @@ die('Somehow an SQL injection attempt crawled into our session registrar! (2)'); // All done! - $query = $db->sql_query('INSERT INTO '.table_prefix.'session_keys(session_key, salt, user_id, auth_level, source_ip, time, key_type) VALUES(\''.$keyhash.'\', \''.$salt.'\', '.$user_id.', '.$level.', \''.$ip.'\', '.$time.', ' . $key_type . ');'); + $query = $db->sql_query('INSERT INTO '.table_prefix.'session_keys(session_key, salt, user_id, auth_level, source_ip, time, key_type) VALUES(\''.$keyhash.'\', \''.$db->escape($salt).'\', '.$user_id.', '.$level.', \''.$ip.'\', '.$time.', ' . $key_type . ');'); if ( !$query && defined('IN_ENANO_UPGRADE') ) // we're trying to upgrade so the key_type column is probably missing - try it again without specifying the key type - $this->sql('INSERT INTO '.table_prefix.'session_keys(session_key, salt, user_id, auth_level, source_ip, time) VALUES(\''.$keyhash.'\', \''.$salt.'\', '.$user_id.', '.$level.', \''.$ip.'\', '.$time.');'); + $this->sql('INSERT INTO '.table_prefix.'session_keys(session_key, salt, user_id, auth_level, source_ip, time) VALUES(\''.$keyhash.'\', \''.$db->escape($salt).'\', '.$user_id.', '.$level.', \''.$ip.'\', '.$time.');'); return true; } @@ -1025,9 +1045,33 @@ { global $db, $session, $paths, $template, $plugins; // Common objects profiler_log("SessionManager: checking session: " . sha1($key)); + + if ( strlen($key) > 48 ) + { + return $this->validate_aes_session($key); + } + + profiler_log("SessionManager: checking session: " . $key); + + return $this->validate_session_shared($key, ''); + } + + /** + * Validates an old-format AES session key. DO NOT USE THIS. Will return false if called outside of an upgrade. + * @param string Session key + * @return array + */ + + protected function validate_aes_session($key) + { + global $db, $session, $paths, $template, $plugins; // Common objects + + // No valid use except during upgrades + if ( !preg_match('/^upg-/', enano_version()) || !defined('IN_ENANO_UPGRADE') ) + return false; + $aes = AESCrypt::singleton(AES_BITS, AES_BLOCKSIZE); $decrypted_key = $aes->decrypt($key, $this->private_key, ENC_HEX); - if ( !$decrypted_key ) { // die_semicritical('AES encryption error', '
Something went wrong during the AES decryption process.
'.print_r($decrypted_key, true).''); @@ -1042,24 +1086,59 @@ } $keyhash = md5($key); $salt = $db->escape($keydata[3]); - profiler_log("SessionManager: checking session: " . sha1($key) . ": decrypted session key to $decrypted_key"); + + return $this->validate_session_shared($keyhash, $salt, true); + } + + /** + * Shared portion of session validation. Do not try to call this. + * @return array + * @access private + */ + + protected function validate_session_shared($key, $salt, $loose_call = false) + { + global $db, $session, $paths, $template, $plugins; // Common objects + // using a normal call to $db->sql_query to avoid failing on errors here - $query = $db->sql_query('SELECT u.user_id AS uid,u.username,u.password,u.email,u.real_name,u.user_level,u.theme,u.style,u.signature,' . "\n" - . ' u.reg_time,u.account_active,u.activation_key,u.user_lang,u.user_title,k.source_ip,k.time,k.auth_level,k.key_type,COUNT(p.message_id) AS num_pms,' . "\n" - . ' u.user_timezone, u.user_dst, x.* FROM '.table_prefix.'session_keys AS k' . "\n" - . ' LEFT JOIN '.table_prefix.'users AS u' . "\n" - . ' ON ( u.user_id=k.user_id )' . "\n" - . ' LEFT JOIN '.table_prefix.'users_extra AS x' . "\n" - . ' ON ( u.user_id=x.user_id OR x.user_id IS NULL )' . "\n" - . ' LEFT JOIN '.table_prefix.'privmsgs AS p' . "\n" - . ' ON ( p.message_to=u.username AND p.message_read=0 )' . "\n" - . ' WHERE k.session_key=\''.$keyhash.'\'' . "\n" - . ' AND k.salt=\''.$salt.'\'' . "\n" - . ' GROUP BY u.user_id,u.username,u.password,u.email,u.real_name,u.user_level,u.theme,u.style,u.signature,u.reg_time,u.account_active,u.activation_key,u.user_lang,u.user_timezone,u.user_title,u.user_dst,k.source_ip,k.time,k.auth_level,k.key_type,x.user_id, x.user_aim, x.user_yahoo, x.user_msn, x.user_xmpp, x.user_homepage, x.user_location, x.user_job, x.user_hobbies, x.email_public, x.disable_js_fx;'); + $columns_select = "u.user_id AS uid, u.username, u.password, u.password_salt, u.email, u.real_name, u.user_level, u.theme,\n" + . " u.style,u.signature, u.reg_time, u.account_active, u.activation_key, u.user_lang, u.user_title, k.salt, k.source_ip,\n" + . " k.time, k.auth_level, k.key_type, COUNT(p.message_id) AS num_pms, u.user_timezone, u.user_dst, x.*"; + + $columns_groupby = "u.user_id, u.username, u.password, u.email, u.real_name, u.user_level, u.theme, u.style, u.signature,\n" + . " u.reg_time, u.account_active, u.activation_key, u.user_lang, u.user_timezone, u.user_title, u.user_dst,\n" + . " k.salt, k.source_ip, k.time, k.auth_level, k.key_type, x.user_id, x.user_aim, x.user_yahoo, x.user_msn,\n" + . " x.user_xmpp, x.user_homepage, x.user_location, x.user_job, x.user_hobbies, x.email_public,\n" + . " x.disable_js_fx"; + + $joins = " LEFT JOIN " . table_prefix . "users AS u\n" + . " ON ( u.user_id=k.user_id )\n" + . " LEFT JOIN " . table_prefix . "users_extra AS x\n" + . " ON ( u.user_id=x.user_id OR x.user_id IS NULL )\n" + . " LEFT JOIN " . table_prefix . "privmsgs AS p\n" + . " ON ( p.message_to=u.username AND p.message_read=0 )\n"; + if ( !$loose_call ) + { + $key_md5 = md5($key); + $query = $db->sql_query("SELECT $columns_select\n" + . "FROM " . table_prefix . "session_keys AS k\n" + . $joins + . " WHERE k.session_key='$key_md5'\n" + . " GROUP BY $columns_groupby;"); + } + else + { + $query = $db->sql_query("SELECT $columns_select\n" + . "FROM " . table_prefix . "session_keys AS k\n" + . $joins + . " WHERE k.session_key='$key'\n" + . " AND k.salt='$salt'\n" + . " GROUP BY $columns_groupby;"); + } if ( !$query && ( defined('IN_ENANO_INSTALL') or defined('IN_ENANO_UPGRADE') ) ) { - $query = $this->sql('SELECT u.user_id AS uid,u.username,u.password,u.email,u.real_name,u.user_level,u.theme,u.style,u.signature,u.reg_time,u.account_active,u.activation_key,k.source_ip,k.time,k.auth_level,COUNT(p.message_id) AS num_pms, 1440 AS user_timezone, \'0;0;0;0;60\' AS user_dst, ' . SK_SHORT . ' AS key_type FROM '.table_prefix.'session_keys AS k + $query = $this->sql('SELECT u.user_id AS uid,u.username,u.password,\'\' AS password_salt,u.email,u.real_name,u.user_level,u.theme,u.style,u.signature,u.reg_time,u.account_active,u.activation_key,k.source_ip,k.time,k.auth_level,COUNT(p.message_id) AS num_pms, 1440 AS user_timezone, \'0;0;0;0;60\' AS user_dst, ' . SK_SHORT . ' AS key_type FROM '.table_prefix.'session_keys AS k LEFT JOIN '.table_prefix.'users AS u ON ( u.user_id=k.user_id ) LEFT JOIN '.table_prefix.'privmsgs AS p @@ -1078,7 +1157,8 @@ return false; } $row = $db->fetchrow(); - profiler_log("SessionManager: checking session: " . sha1($key) . ": selected and fetched results"); + profiler_log("SessionManager: session check: selected and fetched results"); + $row['user_id'] =& $row['uid']; $ip = $_SERVER['REMOTE_ADDR']; if($row['auth_level'] > $row['user_level']) @@ -1103,15 +1183,21 @@ return false; } - // Do the password validation - $real_pass = $aes->decrypt($row['password'], $this->private_key, ENC_HEX); - - //die('
'.print_r($keydata, true).''); - if(sha1($real_pass) != $keydata[2]) + // $loose_call is turned on only from validate_aes_session + if ( !$loose_call ) { - // Failed password check - // echo '(debug) $session->validate_session: encrypted password is wrong