includes/sessions.php
changeset 688 f2a824ce5f18
parent 685 17ebe24cdf85
child 691 dab7850c098d
--- 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<br />';
-      $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<br />';
+          $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'] )
         {