diff -r ea43ac1ff2ee -r f2a824ce5f18 includes/sessions.php --- a/includes/sessions.php Tue Aug 12 00:05:09 2008 -0400 +++ b/includes/sessions.php Tue Aug 12 00:06:35 2008 -0400 @@ -563,57 +563,17 @@ * @param string $aes_key The MD5 hash of the encryption key, hex-encoded * @param string $challenge The 256-bit MD5 challenge string - first 128 bits should be the hash, the last 128 should be the challenge salt * @param int $level The privilege level we're authenticating for, defaults to 0 - * @param array $captcha_hash Optional. If we're locked out and the lockout policy is captcha, this should be the identifier for the code. - * @param array $captcha_code Optional. If we're locked out and the lockout policy is captcha, this should be the code the user entered. + * @param string $captcha_hash Optional. If we're locked out and the lockout policy is captcha, this should be the identifier for the code. + * @param string $captcha_code Optional. If we're locked out and the lockout policy is captcha, this should be the code the user entered. + * @param bool $remember Optional. If true, remembers the session for X days. Otherwise, assigns a short session. Defaults to false. * @param bool $lookup_key Optional. If true (default) this queries the database for the "real" encryption key. Else, uses what is given. * @return string 'success' on success, or error string on failure */ - function login_with_crypto($username, $aes_data, $aes_key_id, $challenge, $level = USER_LEVEL_MEMBER, $captcha_hash = false, $captcha_code = false, $lookup_key = true) + function login_with_crypto($username, $aes_data, $aes_key_id, $challenge, $level = USER_LEVEL_MEMBER, $captcha_hash = false, $captcha_code = false, $remember = false, $lookup_key = true) { global $db, $session, $paths, $template, $plugins; // Common objects - $privcache = $this->private_key; - - if ( !defined('IN_ENANO_INSTALL') ) - { - $timestamp_cutoff = time() - $duration; - $q = $this->sql('SELECT timestamp FROM '.table_prefix.'lockout WHERE timestamp > ' . $timestamp_cutoff . ' AND ipaddr = \'' . $ipaddr . '\' ORDER BY timestamp DESC;'); - $fails = $db->numrows(); - // Lockout stuff - $threshold = ( $_ = getConfig('lockout_threshold') ) ? intval($_) : 5; - $duration = ( $_ = getConfig('lockout_duration') ) ? intval($_) : 15; - // convert to minutes - $duration = $duration * 60; - $policy = ( $x = getConfig('lockout_policy') && in_array(getConfig('lockout_policy'), array('lockout', 'disable', 'captcha')) ) ? getConfig('lockout_policy') : 'lockout'; - if ( $policy == 'captcha' && $captcha_hash && $captcha_code ) - { - // policy is captcha -- check if it's correct, and if so, bypass lockout check - $real_code = $this->get_captcha($captcha_hash); - } - if ( $policy != 'disable' && !( $policy == 'captcha' && isset($real_code) && strtolower($real_code) == strtolower($captcha_code) ) ) - { - $ipaddr = $db->escape($_SERVER['REMOTE_ADDR']); - if ( $fails >= $threshold ) - { - // ooh boy, somebody's in trouble ;-) - $row = $db->fetchrow(); - $db->free_result(); - return array( - 'success' => false, - 'error' => 'locked_out', - 'lockout_threshold' => $threshold, - 'lockout_duration' => ( $duration / 60 ), - 'lockout_fails' => $fails, - 'lockout_policy' => $policy, - 'time_rem' => ( $duration / 60 ) - round( ( time() - $row['timestamp'] ) / 60 ), - 'lockout_last_time' => $row['timestamp'] - ); - } - } - $db->free_result(); - } - // Instanciate the Rijndael encryption object $aes = AESCrypt::singleton(AES_BITS, AES_BLOCKSIZE); @@ -656,163 +616,8 @@ // Decrypt our password $password = $aes->decrypt($aes_data, $bin_key, ENC_HEX); - // Initialize our success switch - $success = false; - - // Escaped username - $username = str_replace('_', ' ', $username); - $db_username_lower = $this->prepare_text(strtolower($username)); - $db_username = $this->prepare_text($username); - - // Select the user data from the table, and decrypt that so we can verify the password - $this->sql('SELECT password,old_encryption,user_id,user_level,theme,style,temp_password,temp_password_time FROM '.table_prefix.'users WHERE ' . ENANO_SQLFUNC_LOWERCASE . '(username)=\''.$db_username_lower.'\' OR username=\'' . $db_username . '\';'); - if($db->numrows() < 1) - { - // This wasn't logged in <1.0.2, dunno how it slipped through - if($level > USER_LEVEL_MEMBER) - $this->sql('INSERT INTO '.table_prefix.'logs(log_type,action,time_id,date_string,author,edit_summary,page_text) VALUES(\'security\', \'admin_auth_bad\', '.time().', \''.enano_date('d M Y h:i a').'\', \''.$db->escape($username).'\', \''.$db->escape($_SERVER['REMOTE_ADDR']).'\', ' . intval($level) . ')'); - else - $this->sql('INSERT INTO '.table_prefix.'logs(log_type,action,time_id,date_string,author,edit_summary) VALUES(\'security\', \'auth_bad\', '.time().', \''.enano_date('d M Y h:i a').'\', \''.$db->escape($username).'\', \''.$db->escape($_SERVER['REMOTE_ADDR']).'\')'); - - if ( $policy != 'disable' && !defined('IN_ENANO_INSTALL') ) - { - $ipaddr = $db->escape($_SERVER['REMOTE_ADDR']); - // increment fail count - $this->sql('INSERT INTO '.table_prefix.'lockout(ipaddr, timestamp, action) VALUES(\'' . $ipaddr . '\', ' . time() . ', \'credential\');'); - $fails++; - // ooh boy, somebody's in trouble ;-) - return array( - 'success' => false, - 'error' => ( $fails >= $threshold ) ? 'locked_out' : 'invalid_credentials', - 'lockout_threshold' => $threshold, - 'lockout_duration' => ( $duration / 60 ), - 'lockout_fails' => $fails, - 'time_rem' => ( $duration / 60 ), - 'lockout_policy' => $policy - ); - } - - return array( - 'success' => false, - 'error' => 'invalid_credentials' - ); - } - $row = $db->fetchrow(); - - // Check to see if we're logging in using a temporary password - - if((intval($row['temp_password_time']) + 3600*24) > time() ) - { - $temp_pass = $aes->decrypt( $row['temp_password'], $this->private_key, ENC_HEX ); - if( $temp_pass == $password ) - { - $url = makeUrlComplete('Special', 'PasswordReset/stage2/' . $row['user_id'] . '/' . $row['temp_password']); - - $code = $plugins->setHook('login_password_reset'); - foreach ( $code as $cmd ) - { - eval($cmd); - } - - redirect($url, '', '', 0); - exit; - } - } - - if($row['old_encryption'] == 1) - { - // The user's password is stored using the obsolete and insecure MD5 algorithm, so we'll update the field with the new password - 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'].';'); - $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; if so then do challenge authentication - $real_pass = $aes->decrypt(hexdecode($row['password']), $this->private_key, ENC_BINARY); - if($password === $real_pass && is_string($password)) - { - // Yay! We passed AES authentication. Previously an MD5 challenge was done here, this was deemed redundant in 1.1.3. - // It didn't seem to provide any additional security... - $success = true; - } - } - if($success) - { - if($level > $row['user_level']) - return array( - 'success' => false, - 'error' => 'too_big_for_britches' - ); - - /* - return array( - 'success' => false, - 'error' => 'Successful authentication, but session manager is in debug mode - remove the "return array(...);" in includes/sessions.php:' . ( __LINE__ - 2 ) - ); - */ - - $sess = $this->register_session(intval($row['user_id']), $username, $password, $level); - if($sess) - { - $this->username = $username; - $this->user_id = intval($row['user_id']); - $this->theme = $row['theme']; - $this->style = $row['style']; - - if($level > USER_LEVEL_MEMBER) - $this->sql('INSERT INTO '.table_prefix.'logs(log_type,action,time_id,date_string,author,edit_summary,page_text) VALUES(\'security\', \'admin_auth_good\', '.time().', \''.enano_date('d M Y h:i a').'\', \''.$db->escape($username).'\', \''.$db->escape($_SERVER['REMOTE_ADDR']).'\', ' . intval($level) . ')'); - else - $this->sql('INSERT INTO '.table_prefix.'logs(log_type,action,time_id,date_string,author,edit_summary) VALUES(\'security\', \'auth_good\', '.time().', \''.enano_date('d M Y h:i a').'\', \''.$db->escape($username).'\', \''.$db->escape($_SERVER['REMOTE_ADDR']).'\')'); - - $code = $plugins->setHook('login_success'); - foreach ( $code as $cmd ) - { - eval($cmd); - } - return array( - 'success' => true - ); - } - else - return array( - 'success' => false, - 'error' => 'backend_fail' - ); - } - else - { - if($level > USER_LEVEL_MEMBER) - $this->sql('INSERT INTO '.table_prefix.'logs(log_type,action,time_id,date_string,author,edit_summary,page_text) VALUES(\'security\', \'admin_auth_bad\', '.time().', \''.enano_date('d M Y h:i a').'\', \''.$db->escape($username).'\', \''.$db->escape($_SERVER['REMOTE_ADDR']).'\', ' . intval($level) . ')'); - else - $this->sql('INSERT INTO '.table_prefix.'logs(log_type,action,time_id,date_string,author,edit_summary) VALUES(\'security\', \'auth_bad\', '.time().', \''.enano_date('d M Y h:i a').'\', \''.$db->escape($username).'\', \''.$db->escape($_SERVER['REMOTE_ADDR']).'\')'); - - // Do we also need to increment the lockout countdown? - if ( $policy != 'disable' && !defined('IN_ENANO_INSTALL') ) - { - $ipaddr = $db->escape($_SERVER['REMOTE_ADDR']); - // increment fail count - $this->sql('INSERT INTO '.table_prefix.'lockout(ipaddr, timestamp, action) VALUES(\'' . $ipaddr . '\', ' . time() . ', \'credential\');'); - $fails++; - return array( - 'success' => false, - 'error' => ( $fails >= $threshold ) ? 'locked_out' : 'invalid_credentials', - 'lockout_threshold' => $threshold, - 'lockout_duration' => ( $duration / 60 ), - 'lockout_fails' => $fails, - 'time_rem' => ( $duration / 60 ), - 'lockout_policy' => $policy - ); - } - - return array( - 'success' => false, - 'error' => 'invalid_credentials' - ); - } + // Let the LoginAPI do the rest. + return $this->login_without_crypto($username, $password, false, $level, $captcha_hash, $captcha_code, $remember); } /** @@ -823,9 +628,12 @@ * @param string $password The password -OR- the MD5 hash of the password if $already_md5ed is true * @param bool $already_md5ed This should be set to true if $password is an MD5 hash, and should be false if it's plaintext. Defaults to false. * @param int $level The privilege level we're authenticating for, defaults to 0 + * @param string $captcha_hash Optional. If we're locked out and the lockout policy is captcha, this should be the identifier for the code. + * @param string $captcha_code Optional. If we're locked out and the lockout policy is captcha, this should be the code the user entered. + * @param bool $remember Optional. If true, remembers the session for X days. Otherwise, assigns a short session. Defaults to false. */ - function login_without_crypto($username, $password, $already_md5ed = false, $level = USER_LEVEL_MEMBER, $captcha_hash = false, $captcha_code = false) + function login_without_crypto($username, $password, $already_md5ed = false, $level = USER_LEVEL_MEMBER, $captcha_hash = false, $captcha_code = false, $remember = false) { global $db, $session, $paths, $template, $plugins; // Common objects @@ -979,7 +787,7 @@ 'success' => false, 'error' => 'too_big_for_britches' ); - $sess = $this->register_session(intval($row['user_id']), $username, $real_pass, $level); + $sess = $this->register_session(intval($row['user_id']), $username, $real_pass, $level, $remember); if($sess) { if($level > USER_LEVEL_MEMBER) @@ -1072,10 +880,11 @@ * @param string $username * @param string $password * @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) + function register_session($user_id, $username, $password, $level = USER_LEVEL_MEMBER, $remember = false) { // Random key identifier $salt = md5(microtime() . mt_rand()); @@ -1086,10 +895,12 @@ // Unencrypted session key $session_key = "u=$username;p=$passha1;s=$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); - $dec_DEBUG = $aes->decrypt($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) @@ -1122,7 +933,7 @@ die('Somehow an SQL injection attempt crawled into our session registrar! (2)'); // All done! - $query = $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.');'); + $query = $this->sql('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 . ');'); return true; } @@ -1220,7 +1031,7 @@ profiler_log("SessionManager: checking session: " . sha1($key) . ": decrypted session key to $decrypted_key"); // 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,COUNT(p.message_id) AS num_pms,' . "\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, 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" @@ -1234,7 +1045,7 @@ 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 FROM '.table_prefix.'session_keys AS k + $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, ' . 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 @@ -1289,18 +1100,47 @@ return false; } - $time_now = time(); - $time_key = $row['time'] + 900; - if($time_now > $time_key && $row['auth_level'] > USER_LEVEL_MEMBER) + // timestamp check + switch ( $row['key_type'] ) { - // Session timed out - // echo '(debug) $session->validate_session: super session timed out
'; - $this->sw_timed_out = true; - return false; + case SK_SHORT: + $time_now = time(); + $time_key = $row['time'] + ( 60 * intval(getConfig('session_short', '720')) ); + if ( $time_now > $time_key ) + { + // Session timed out + return false; + } + break; + case SK_LONG: + if ( intval(getConfig('session_remember_time', '0')) === 0 ) + { + // sessions last infinitely, timestamp validation is therefore successful + break; + } + $time_now = time(); + $time_key = $row['time'] + ( 86400 * intval(getConfig('session_remember_time', '30')) ); + if ( $time_now > $time_key ) + { + // Session timed out + return false; + } + break; + case SK_ELEV: + $time_now = time(); + $time_key = $row['time'] + 900; + if($time_now > $time_key && $row['auth_level'] > USER_LEVEL_MEMBER) + { + // Session timed out + // echo '(debug) $session->validate_session: super session timed out
'; + $this->sw_timed_out = true; + return false; + } + break; } - - // If this is an elevated-access session key, update the time - if( $row['auth_level'] > USER_LEVEL_MEMBER ) + + // If this is an elevated-access or short-term session key, update the time + if( $row['key_type'] == SK_ELEV || $row['key_type'] == SK_SHORT ) { $this->sql('UPDATE '.table_prefix.'session_keys SET time='.time().' WHERE session_key=\''.$keyhash.'\';'); } @@ -3617,6 +3457,8 @@ $response['username'] = ( $this->user_logged_in ) ? $this->username : false; $response['aes_key'] = $this->rijndael_genkey(); + $response['extended_time'] = intval(getConfig('session_remember_time', '30')); + // Lockout info $response['locked_out'] = $locked_out; @@ -3757,7 +3599,7 @@ // attempt the login // function login_without_crypto($username, $password, $already_md5ed = false, $level = USER_LEVEL_MEMBER, $captcha_hash = false, $captcha_code = false) - $login_result = $this->login_without_crypto($username, $password, false, intval($req['level']), @$req['captcha_hash'], @$req['captcha_code']); + $login_result = $this->login_without_crypto($username, $password, false, intval($req['level']), @$req['captcha_hash'], @$req['captcha_code'], @$req['remember']); if ( $login_result['success'] ) {