includes/sessions.php
changeset 801 eb8b23f11744
parent 782 96848f04bbba
child 802 59cce2313f9d
equal deleted inserted replaced
800:9cdfe82c56cd 801:eb8b23f11744
     1 <?php
     1 <?php
     2 
     2 
     3 /*
     3 /*
     4  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
     4  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
     5  * Version 1.1.5 (Caoineag alpha 5)
     5  * Version 1.1.6 (Caoineag beta 1)
     6  * Copyright (C) 2006-2008 Dan Fuhry
     6  * Copyright (C) 2006-2008 Dan Fuhry
     7  * sessions.php - everything related to security and user management
     7  * sessions.php - everything related to security and user management
     8  *
     8  *
     9  * This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License
     9  * This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License
    10  * as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
    10  * as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
   499             $key = $_REQUEST['auth'];
   499             $key = $_REQUEST['auth'];
   500             $super = $this->compat_validate_session($key);
   500             $super = $this->compat_validate_session($key);
   501           }
   501           }
   502           else
   502           else
   503           {
   503           {
   504             $key = strrev($_REQUEST['auth']);
   504             $key = $_REQUEST['auth'];
   505             if ( !empty($key) && ( strlen($key) / 2 ) % 4 == 0 )
   505             if ( !empty($key) && ( strlen($key) / 2 ) % 4 == 0 )
   506             {
   506             {
   507               $super = $this->validate_session($key);
   507               $super = $this->validate_session($key);
   508             }
   508             }
   509           }
   509           }
   643   
   643   
   644   function login_without_crypto($username, $password, $already_md5ed = false, $level = USER_LEVEL_MEMBER, $captcha_hash = false, $captcha_code = false, $remember = false)
   644   function login_without_crypto($username, $password, $already_md5ed = false, $level = USER_LEVEL_MEMBER, $captcha_hash = false, $captcha_code = false, $remember = false)
   645   {
   645   {
   646     global $db, $session, $paths, $template, $plugins; // Common objects
   646     global $db, $session, $paths, $template, $plugins; // Common objects
   647     
   647     
   648     $pass_hashed = ( $already_md5ed ) ? $password : md5($password);
   648     if ( $already_md5ed )
       
   649     {
       
   650       // No longer supported
       
   651       return array(
       
   652           'mode' => 'error',
       
   653           '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)'
       
   654         );
       
   655     }
   649     
   656     
   650     // Replace underscores with spaces in username
   657     // Replace underscores with spaces in username
   651     // (Added in 1.0.2)
   658     // (Added in 1.0.2)
   652     $username = str_replace('_', ' ', $username);
   659     $username = str_replace('_', ' ', $username);
   653     
   660     
   654     // Perhaps we're upgrading Enano?
   661     // Perhaps we're upgrading Enano?
   655     if($this->compat)
   662     if($this->compat)
   656     {
   663     {
   657       return $this->login_compat($username, $pass_hashed, $level);
   664       return $this->login_compat($username, md5($password), $level);
   658     }
   665     }
   659     
   666     
   660     if ( !defined('IN_ENANO_INSTALL') )
   667     if ( !defined('IN_ENANO_INSTALL') )
   661     {
   668     {
   662       // Lockout stuff
   669       // Lockout stuff
   708     
   715     
   709     // Initialize our success switch
   716     // Initialize our success switch
   710     $success = false;
   717     $success = false;
   711     
   718     
   712     // Retrieve the real password from the database
   719     // Retrieve the real password from the database
   713     $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)).'\';');
   720     $username_db = $db->escape(strtolower($username));
       
   721     if ( !$db->sql_query('SELECT password,password_salt,old_encryption,user_id,user_level,temp_password,temp_password_time FROM '.table_prefix."users\n"
       
   722                        . "  WHERE " . ENANO_SQLFUNC_LOWERCASE . "(username) = '$username_db';") )
       
   723     {
       
   724       $this->sql('SELECT password,\'\' AS password_salt,old_encryption,user_id,user_level,temp_password,temp_password_time FROM '.table_prefix."users\n"
       
   725                . "  WHERE " . ENANO_SQLFUNC_LOWERCASE . "(username) = '$username_db';");
       
   726     }
   714     if($db->numrows() < 1)
   727     if($db->numrows() < 1)
   715     {
   728     {
   716       // This wasn't logged in <1.0.2, dunno how it slipped through
   729       // This wasn't logged in <1.0.2, dunno how it slipped through
   717       if($level > USER_LEVEL_MEMBER)
   730       if($level > USER_LEVEL_MEMBER)
   718         $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) . ')');
   731         $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) . ')');
   745     
   758     
   746     // Check to see if we're logging in using a temporary password
   759     // Check to see if we're logging in using a temporary password
   747     
   760     
   748     if((intval($row['temp_password_time']) + 3600*24) > time() )
   761     if((intval($row['temp_password_time']) + 3600*24) > time() )
   749     {
   762     {
   750       $temp_pass = $aes->decrypt( $row['temp_password'], $this->private_key, ENC_HEX );
   763       $temp_pass = hmac_sha1($password, $row['password_salt']);
   751       if( md5($temp_pass) == $pass_hashed )
   764       if( $temp_pass === $row['temp_password'] )
   752       {
   765       {
   753         $code = $plugins->setHook('login_password_reset');
   766         $code = $plugins->setHook('login_password_reset');
   754         foreach ( $code as $cmd )
   767         foreach ( $code as $cmd )
   755         {
   768         {
   756           eval($cmd);
   769           eval($cmd);
   757         }
   770         }
   758         
   771         
   759         return array(
   772         return array(
   760             'success' => false,
   773             'success' => false,
   761             'error' => 'valid_reset',
   774             'error' => 'valid_reset',
   762             'redirect_url' => makeUrlComplete('Special', 'PasswordReset/stage2/' . $row['user_id'] . '/' . $row['temp_password'])
   775             'redirect_url' => makeUrlComplete('Special', 'PasswordReset/stage2/' . $row['user_id'] . '/' . $this->pk_encrypt($password))
   763           );
   776           );
   764       }
   777       }
   765     }
   778     }
   766     
   779     
   767     if($row['old_encryption'] == 1)
   780     if ( $row['old_encryption'] == 1 )
   768     {
   781     {
   769       // The user's password is stored using the obsolete and insecure MD5 algorithm - we'll update the field with the new password
   782       // The user's password is stored using the obsolete and insecure MD5 algorithm - we'll update the field with the new password
   770       if($pass_hashed == $row['password'] && !$already_md5ed)
   783       if(md5($password) === $row['password'])
   771       {
   784       {
   772         $pass_stashed = $aes->encrypt($password, $this->private_key, ENC_HEX);
   785         $hmac_secret = AESCrypt::randkey(20);
   773         $this->sql('UPDATE '.table_prefix.'users SET password=\''.$pass_stashed.'\',old_encryption=0 WHERE user_id='.$row['user_id'].';');
   786         $password_hmac = hmac_sha1($password, $hmac_secret);
       
   787         $this->sql('UPDATE '.table_prefix."users SET password = '$password_hmac', password_salt = '$hmac_secret', old_encryption = 0 WHERE user_id={$row['user_id']};");
   774         $success = true;
   788         $success = true;
   775       }
   789       }
   776       elseif($pass_hashed == $row['password'] && $already_md5ed)
   790     }
   777       {
   791     else if ( $row['old_encryption'] == 2 )
   778         // We don't have the real password so don't bother with encrypting it, just call it success and get out of here
   792     {
       
   793       // Our password field uses the 1.0RC1-1.1.5 encryption format
       
   794       $real_pass = $aes->decrypt($row['password'], $this->private_key);
       
   795       if($password === $real_pass)
       
   796       {
   779         $success = true;
   797         $success = true;
       
   798         $hmac_secret = AESCrypt::randkey(20);
       
   799         $password_hmac = hmac_sha1($password, $hmac_secret);
       
   800         $this->sql('UPDATE '.table_prefix."users SET password = '$password_hmac', password_salt = '$hmac_secret', old_encryption = 0 WHERE user_id={$row['user_id']};");
       
   801         $success = true;
   780       }
   802       }
   781     }
   803     }
   782     else
   804     else
   783     {
   805     {
   784       // 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
   806       // Password uses HMAC-SHA1
   785       $real_pass = $aes->decrypt($row['password'], $this->private_key);
   807       $user_challenge = hmac_sha1($password, $row['password_salt']);
   786       if($pass_hashed == md5($real_pass))
   808       $password_hmac =& $row['password'];
       
   809       if ( $user_challenge === $password_hmac )
   787       {
   810       {
   788         $success = true;
   811         $success = true;
   789       }
   812       }
   790     }
   813     }
   791     if($success)
   814     if($success)
   793       if((int)$level > (int)$row['user_level'])
   816       if((int)$level > (int)$row['user_level'])
   794         return array(
   817         return array(
   795           'success' => false,
   818           'success' => false,
   796           'error' => 'too_big_for_britches'
   819           'error' => 'too_big_for_britches'
   797         );
   820         );
   798       $sess = $this->register_session(intval($row['user_id']), $username, $real_pass, $level, $remember);
   821       $sess = $this->register_session($row['user_id'], $username, $password_hmac, $level, $remember);
   799       if($sess)
   822       if($sess)
   800       {
   823       {
   801         if($level > USER_LEVEL_MEMBER)
   824         if($level > USER_LEVEL_MEMBER)
   802           $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) . ')');
   825           $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) . ')');
   803         else
   826         else
   884   /**
   907   /**
   885    * Registers a session key in the database. This function *ASSUMES* that the username and password have already been validated!
   908    * Registers a session key in the database. This function *ASSUMES* that the username and password have already been validated!
   886    * 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]"
   909    * 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]"
   887    * @param int $user_id
   910    * @param int $user_id
   888    * @param string $username
   911    * @param string $username
   889    * @param string $password
   912    * @param string $password_hmac The HMAC of the user's password, right from the database
   890    * @param int $level The level of access to grant, defaults to USER_LEVEL_MEMBER
   913    * @param int $level The level of access to grant, defaults to USER_LEVEL_MEMBER
   891    * @param bool $remember Whether the session should be long-term (true) or not (false). Defaults to short-term.
   914    * @param bool $remember Whether the session should be long-term (true) or not (false). Defaults to short-term.
   892    * @return bool
   915    * @return bool
   893    */
   916    */
   894    
   917    
   895   function register_session($user_id, $username, $password, $level = USER_LEVEL_MEMBER, $remember = false)
   918   function register_session($user_id, $username, $password_hmac, $level = USER_LEVEL_MEMBER, $remember = false)
   896   {
   919   {
   897     global $db, $session, $paths, $template, $plugins; // Common objects
   920     global $db, $session, $paths, $template, $plugins; // Common objects
   898     
   921     
   899     // Random key identifier
   922     // Random key identifier
   900     $salt = md5(microtime() . mt_rand());
   923     $salt = '';
   901     
   924     for ( $i = 0; $i < 32; $i++ )
   902     // SHA1 hash of password, stored in the key
   925     {
   903     $passha1 = sha1($password);
   926       $salt .= chr(mt_rand(32, 127));
   904     
   927     }
   905     // Unencrypted session key
   928     
   906     $session_key = "u=$username;p=$passha1;s=$salt";
   929     // Session key
       
   930     $session_key = hmac_sha1($password_hmac, $salt);
   907     
   931     
   908     // Type of key
   932     // Type of key
   909     $key_type = ( $level > USER_LEVEL_MEMBER ) ? SK_ELEV : ( $remember ? SK_LONG : SK_SHORT );
   933     $key_type = ( $level > USER_LEVEL_MEMBER ) ? SK_ELEV : ( $remember ? SK_LONG : SK_SHORT );
   910     
   934     
   911     // Encrypt the key
       
   912     $aes = AESCrypt::singleton(AES_BITS, AES_BLOCKSIZE);
       
   913     $session_key = $aes->encrypt($session_key, $this->private_key, ENC_HEX);
       
   914     
       
   915     // If we're registering an elevated-privilege key, it needs to be on GET
   935     // If we're registering an elevated-privilege key, it needs to be on GET
   916     if($level > USER_LEVEL_MEMBER)
   936     if($level > USER_LEVEL_MEMBER)
   917     {
   937     {
   918       // Reverse it - cosmetic only ;-)
   938       // Reverse it - cosmetic only ;-)
   919       $hexkey = strrev($session_key);
   939       $hexkey = $session_key;
   920       $this->sid_super = $hexkey;
   940       $this->sid_super = $hexkey;
   921       $_GET['auth'] = $hexkey;
   941       $_GET['auth'] = $hexkey;
   922     }
   942     }
   923     else
   943     else
   924     {
   944     {
   941       die('Somehow an SQL injection attempt crawled into our session registrar! (1)');
   961       die('Somehow an SQL injection attempt crawled into our session registrar! (1)');
   942     if(!is_int($level))
   962     if(!is_int($level))
   943       die('Somehow an SQL injection attempt crawled into our session registrar! (2)');
   963       die('Somehow an SQL injection attempt crawled into our session registrar! (2)');
   944     
   964     
   945     // All done!
   965     // All done!
   946     $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 . ');');
   966     $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 . ');');
   947     if ( !$query && defined('IN_ENANO_UPGRADE') )
   967     if ( !$query && defined('IN_ENANO_UPGRADE') )
   948       // we're trying to upgrade so the key_type column is probably missing - try it again without specifying the key type
   968       // we're trying to upgrade so the key_type column is probably missing - try it again without specifying the key type
   949       $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.');');
   969       $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.');');
   950       
   970       
   951     return true;
   971     return true;
   952   }
   972   }
   953   
   973   
   954   /**
   974   /**
  1023    
  1043    
  1024   function validate_session($key)
  1044   function validate_session($key)
  1025   {
  1045   {
  1026     global $db, $session, $paths, $template, $plugins; // Common objects
  1046     global $db, $session, $paths, $template, $plugins; // Common objects
  1027     profiler_log("SessionManager: checking session: " . sha1($key));
  1047     profiler_log("SessionManager: checking session: " . sha1($key));
       
  1048     
       
  1049     if ( strlen($key) > 48 )
       
  1050     {
       
  1051       return $this->validate_aes_session($key);
       
  1052     }
       
  1053     
       
  1054     profiler_log("SessionManager: checking session: " . $key);
       
  1055     
       
  1056     return $this->validate_session_shared($key, '');
       
  1057   }
       
  1058   
       
  1059   /**
       
  1060    * Validates an old-format AES session key. DO NOT USE THIS. Will return false if called outside of an upgrade.
       
  1061    * @param string Session key
       
  1062    * @return array
       
  1063    */
       
  1064   
       
  1065   protected function validate_aes_session($key)
       
  1066   {
       
  1067     global $db, $session, $paths, $template, $plugins; // Common objects
       
  1068     
       
  1069     // No valid use except during upgrades
       
  1070     if ( !preg_match('/^upg-/', enano_version()) || !defined('IN_ENANO_UPGRADE') )
       
  1071       return false;
       
  1072     
  1028     $aes = AESCrypt::singleton(AES_BITS, AES_BLOCKSIZE);
  1073     $aes = AESCrypt::singleton(AES_BITS, AES_BLOCKSIZE);
  1029     $decrypted_key = $aes->decrypt($key, $this->private_key, ENC_HEX);
  1074     $decrypted_key = $aes->decrypt($key, $this->private_key, ENC_HEX);
  1030     
       
  1031     if ( !$decrypted_key )
  1075     if ( !$decrypted_key )
  1032     {
  1076     {
  1033       // die_semicritical('AES encryption error', '<p>Something went wrong during the AES decryption process.</p><pre>'.print_r($decrypted_key, true).'</pre>');
  1077       // die_semicritical('AES encryption error', '<p>Something went wrong during the AES decryption process.</p><pre>'.print_r($decrypted_key, true).'</pre>');
  1034       return false;
  1078       return false;
  1035     }
  1079     }
  1040       // echo '(debug) $session->validate_session: Key does not match regex<br />Decrypted key: '.$decrypted_key;
  1084       // echo '(debug) $session->validate_session: Key does not match regex<br />Decrypted key: '.$decrypted_key;
  1041       return false;
  1085       return false;
  1042     }
  1086     }
  1043     $keyhash = md5($key);
  1087     $keyhash = md5($key);
  1044     $salt = $db->escape($keydata[3]);
  1088     $salt = $db->escape($keydata[3]);
  1045     profiler_log("SessionManager: checking session: " . sha1($key) . ": decrypted session key to $decrypted_key");
  1089     
       
  1090     return $this->validate_session_shared($keyhash, $salt, true);
       
  1091   }
       
  1092   
       
  1093   /**
       
  1094    * Shared portion of session validation. Do not try to call this.
       
  1095    * @return array
       
  1096    * @access private
       
  1097    */
       
  1098   
       
  1099   protected function validate_session_shared($key, $salt, $loose_call = false)
       
  1100   {
       
  1101     global $db, $session, $paths, $template, $plugins; // Common objects
       
  1102     
  1046     // using a normal call to $db->sql_query to avoid failing on errors here
  1103     // using a normal call to $db->sql_query to avoid failing on errors here
  1047     $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"
  1104     $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"
  1048                              . '    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"
  1105                       . "  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"
  1049                              . '    u.user_timezone, u.user_dst, x.* FROM '.table_prefix.'session_keys AS k' . "\n"
  1106                       . "  k.time, k.auth_level, k.key_type, COUNT(p.message_id) AS num_pms, u.user_timezone, u.user_dst, x.*";
  1050                              . '  LEFT JOIN '.table_prefix.'users AS u' . "\n"
  1107     
  1051                              . '    ON ( u.user_id=k.user_id )' . "\n"
  1108     $columns_groupby = "u.user_id, u.username, u.password, u.email, u.real_name, u.user_level, u.theme, u.style, u.signature,\n"
  1052                              . '  LEFT JOIN '.table_prefix.'users_extra AS x' . "\n"
  1109                       . "           u.reg_time, u.account_active, u.activation_key, u.user_lang, u.user_timezone, u.user_title, u.user_dst,\n"
  1053                              . '    ON ( u.user_id=x.user_id OR x.user_id IS NULL )' . "\n"
  1110                       . "           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"
  1054                              . '  LEFT JOIN '.table_prefix.'privmsgs AS p' . "\n"
  1111                       . "           x.user_xmpp, x.user_homepage, x.user_location, x.user_job, x.user_hobbies, x.email_public,\n"
  1055                              . '    ON ( p.message_to=u.username AND p.message_read=0 )' . "\n"
  1112                       . "           x.disable_js_fx";
  1056                              . '  WHERE k.session_key=\''.$keyhash.'\'' . "\n"
  1113     
  1057                              . '    AND k.salt=\''.$salt.'\'' . "\n"
  1114     $joins = "  LEFT JOIN " . table_prefix . "users AS u\n"
  1058                              . '  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;');
  1115             . "    ON ( u.user_id=k.user_id )\n"
       
  1116             . "  LEFT JOIN " . table_prefix . "users_extra AS x\n"
       
  1117             . "    ON ( u.user_id=x.user_id OR x.user_id IS NULL )\n"
       
  1118             . "  LEFT JOIN " . table_prefix . "privmsgs AS p\n"
       
  1119             . "    ON ( p.message_to=u.username AND p.message_read=0 )\n";
       
  1120     if ( !$loose_call )
       
  1121     {
       
  1122       $key_md5 = md5($key);
       
  1123       $query = $db->sql_query("SELECT $columns_select\n"
       
  1124                             . "FROM " . table_prefix . "session_keys AS k\n"
       
  1125                             . $joins
       
  1126                             . "  WHERE k.session_key='$key_md5'\n"
       
  1127                             . "  GROUP BY $columns_groupby;");
       
  1128     }
       
  1129     else
       
  1130     {
       
  1131       $query = $db->sql_query("SELECT $columns_select\n"
       
  1132                             . "FROM " . table_prefix . "session_keys AS k\n"
       
  1133                             . $joins
       
  1134                             . "  WHERE k.session_key='$key'\n"
       
  1135                             . "    AND k.salt='$salt'\n"
       
  1136                             . "  GROUP BY $columns_groupby;");
       
  1137     }
  1059     
  1138     
  1060     if ( !$query && ( defined('IN_ENANO_INSTALL') or defined('IN_ENANO_UPGRADE') ) )
  1139     if ( !$query && ( defined('IN_ENANO_INSTALL') or defined('IN_ENANO_UPGRADE') ) )
  1061     {
  1140     {
  1062       $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
  1141       $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
  1063                              LEFT JOIN '.table_prefix.'users AS u
  1142                              LEFT JOIN '.table_prefix.'users AS u
  1064                                ON ( u.user_id=k.user_id )
  1143                                ON ( u.user_id=k.user_id )
  1065                              LEFT JOIN '.table_prefix.'privmsgs AS p
  1144                              LEFT JOIN '.table_prefix.'privmsgs AS p
  1066                                ON ( p.message_to=u.username AND p.message_read=0 )
  1145                                ON ( p.message_to=u.username AND p.message_read=0 )
  1067                              WHERE k.session_key=\''.$keyhash.'\'
  1146                              WHERE k.session_key=\''.$keyhash.'\'
  1076     {
  1155     {
  1077       // echo '(debug) $session->validate_session: Key was not found in database<br />';
  1156       // echo '(debug) $session->validate_session: Key was not found in database<br />';
  1078       return false;
  1157       return false;
  1079     }
  1158     }
  1080     $row = $db->fetchrow();
  1159     $row = $db->fetchrow();
  1081     profiler_log("SessionManager: checking session: " . sha1($key) . ": selected and fetched results");
  1160     profiler_log("SessionManager: session check: selected and fetched results");
       
  1161     
  1082     $row['user_id'] =& $row['uid'];
  1162     $row['user_id'] =& $row['uid'];
  1083     $ip = $_SERVER['REMOTE_ADDR'];
  1163     $ip = $_SERVER['REMOTE_ADDR'];
  1084     if($row['auth_level'] > $row['user_level'])
  1164     if($row['auth_level'] > $row['user_level'])
  1085     {
  1165     {
  1086       // Failed authorization check
  1166       // Failed authorization check
  1101       // echo '(debug) $session->validate_session: IP address mismatch<br />';
  1181       // echo '(debug) $session->validate_session: IP address mismatch<br />';
  1102       if ( $fail )
  1182       if ( $fail )
  1103         return false;
  1183         return false;
  1104     }
  1184     }
  1105     
  1185     
  1106     // Do the password validation
  1186     // $loose_call is turned on only from validate_aes_session
  1107     $real_pass = $aes->decrypt($row['password'], $this->private_key, ENC_HEX);
  1187     if ( !$loose_call )
  1108     
  1188     {
  1109     //die('<pre>'.print_r($keydata, true).'</pre>');
  1189       $correct_key = hexdecode(hmac_sha1($row['password'], $row['salt']));
  1110     if(sha1($real_pass) != $keydata[2])
  1190       $user_key = hexdecode($key);
  1111     {
  1191       if ( $correct_key !== $user_key || !is_string($user_key) )
  1112       // Failed password check
  1192       {
  1113       // echo '(debug) $session->validate_session: encrypted password is wrong<br />Real password: '.$real_pass.'<br />Real hash: '.sha1($real_pass).'<br />User hash: '.$keydata[2];
  1193         return false;
  1114       return false;
  1194       }
       
  1195     }
       
  1196     else
       
  1197     {
       
  1198       // if this is a "loose call", this only works once (during the final upgrade stage). Destroy the contents of session_keys.
       
  1199       if ( $row['auth_level'] == USER_LEVEL_ADMIN )
       
  1200         $this->sql('DELETE FROM ' . table_prefix . "session_keys;");
  1115     }
  1201     }
  1116     
  1202     
  1117     // timestamp check
  1203     // timestamp check
  1118     switch ( $row['key_type'] )
  1204     switch ( $row['key_type'] )
  1119     {
  1205     {
  1154     }
  1240     }
  1155         
  1241         
  1156     // If this is an elevated-access or short-term session key, update the time
  1242     // If this is an elevated-access or short-term session key, update the time
  1157     if( $row['key_type'] == SK_ELEV || $row['key_type'] == SK_SHORT )
  1243     if( $row['key_type'] == SK_ELEV || $row['key_type'] == SK_SHORT )
  1158     {
  1244     {
  1159       $this->sql('UPDATE '.table_prefix.'session_keys SET time='.time().' WHERE session_key=\''.$keyhash.'\';');
  1245       $this->sql('UPDATE '.table_prefix.'session_keys SET time='.time().' WHERE session_key=\''.md5($key).'\';');
  1160     }
  1246     }
  1161     
  1247     
  1162     $user_extra = array();
  1248     $user_extra = array();
  1163     foreach ( array('user_aim', 'user_yahoo', 'user_msn', 'user_xmpp', 'user_homepage', 'user_location', 'user_job', 'user_hobbies', 'email_public', 'disable_js_fx') as $column )
  1249     foreach ( array('user_aim', 'user_yahoo', 'user_msn', 'user_xmpp', 'user_homepage', 'user_location', 'user_job', 'user_hobbies', 'email_public', 'disable_js_fx') as $column )
  1164     {
  1250     {
  1167     }
  1253     }
  1168     
  1254     
  1169     $this->user_extra = $user_extra;
  1255     $this->user_extra = $user_extra;
  1170     // Leave the rest to PHP's automatic garbage collector ;-)
  1256     // Leave the rest to PHP's automatic garbage collector ;-)
  1171     
  1257     
  1172     $row['password'] = md5($real_pass);
  1258     $row['password'] = '';
  1173     $row['user_timezone'] = intval($row['user_timezone']) - 1440;
  1259     $row['user_timezone'] = intval($row['user_timezone']) - 1440;
  1174     
  1260     
  1175     profiler_log("SessionManager: finished session check");
  1261     profiler_log("SessionManager: finished session check");
  1176     
  1262     
  1177     return $row;
  1263     return $row;
  1254       $aes = AESCrypt::singleton(AES_BITS, AES_BLOCKSIZE);
  1340       $aes = AESCrypt::singleton(AES_BITS, AES_BLOCKSIZE);
  1255       if(!$this->user_logged_in || $this->auth_level < ( USER_LEVEL_MEMBER + 1))
  1341       if(!$this->user_logged_in || $this->auth_level < ( USER_LEVEL_MEMBER + 1))
  1256       {
  1342       {
  1257         return 'success';
  1343         return 'success';
  1258       }
  1344       }
  1259       // See if we can get rid of the cached decrypted session key
       
  1260       $key_bin = hex2bin(strrev($this->sid_super));
       
  1261       $key_hash = sha1($key_bin . '::' . $this->private_key);
       
  1262       aes_decrypt_cache_destroy($key_hash);
       
  1263       // Destroy elevated privileges
  1345       // Destroy elevated privileges
  1264       $keyhash = md5(strrev($this->sid_super));
  1346       $keyhash = md5($this->sid_super);
  1265       $this->sql('DELETE FROM '.table_prefix.'session_keys WHERE session_key=\''.$keyhash.'\' AND user_id=\'' . $this->user_id . '\';');
  1347       $this->sql('DELETE FROM '.table_prefix.'session_keys WHERE session_key=\''.$keyhash.'\' AND user_id=\'' . $this->user_id . '\';');
  1266       $this->sid_super = false;
  1348       $this->sid_super = false;
  1267       $this->auth_level = USER_LEVEL_MEMBER;
  1349       $this->auth_level = USER_LEVEL_MEMBER;
  1268     }
  1350     }
  1269     else
  1351     else
  1994    * @return bool
  2076    * @return bool
  1995    */
  2077    */
  1996    
  2078    
  1997   function register_temp_password($user_id, $password)
  2079   function register_temp_password($user_id, $password)
  1998   {
  2080   {
  1999     $aes = AESCrypt::singleton(AES_BITS, AES_BLOCKSIZE);
  2081     global $db;
  2000     $temp_pass = $aes->encrypt($password, $this->private_key, ENC_HEX);
  2082     if ( !is_int($user_id) )
       
  2083       return false;
       
  2084     
       
  2085     $this->sql('SELECT password_salt FROM ' . table_prefix . "users WHERE user_id = $user_id;");
       
  2086     if ( $db->numrows() < 1 )
       
  2087       return false;
       
  2088     
       
  2089     list($salt) = $db->fetchrow_num();
       
  2090     $db->free_result();
       
  2091     
       
  2092     $temp_pass = hmac_sha1($password, $salt);
  2001     $this->sql('UPDATE '.table_prefix.'users SET temp_password=\'' . $temp_pass . '\',temp_password_time='.time().' WHERE user_id='.intval($user_id).';');
  2093     $this->sql('UPDATE '.table_prefix.'users SET temp_password=\'' . $temp_pass . '\',temp_password_time='.time().' WHERE user_id='.intval($user_id).';');
  2002   }
  2094   }
  2003   
  2095   
  2004   /**
  2096   /**
  2005    * Sends a request to the admin panel to have the username $u activated.
  2097    * Sends a request to the admin panel to have the username $u activated.
  2280       }
  2372       }
  2281     }
  2373     }
  2282     
  2374     
  2283     // Yay! We're done
  2375     // Yay! We're done
  2284     return 'success';
  2376     return 'success';
       
  2377   }
       
  2378   
       
  2379   /**
       
  2380    * Sets a user's password.
       
  2381    * @param int|string User ID or username
       
  2382    * @param string New password
       
  2383    */
       
  2384   
       
  2385   function set_password($user, $password)
       
  2386   {
       
  2387     // Generate new password and salt
       
  2388     $hmac_secret = hexencode(AESCrypt::randkey(20), '', '');
       
  2389     $password_hmac = hmac_sha1($password, $hmac_secret);
       
  2390     
       
  2391     // Figure out how we want to specify the user
       
  2392     $uidcol = is_int($user) ? "user_id = $user" : ENANO_SQLFUNC_LOWERCASE . "(username) = '" . strtolower($this->prepare_text($user)) . "'";
       
  2393     
       
  2394     // Perform update
       
  2395     $this->sql('UPDATE ' . table_prefix . "users SET old_encryption = 0, password = '$password_hmac', password_salt = '$hmac_secret' WHERE $uidcol;");
       
  2396     
       
  2397     return true;
  2285   }
  2398   }
  2286   
  2399   
  2287   /**
  2400   /**
  2288    * Encrypts a string using the site's private key.
  2401    * Encrypts a string using the site's private key.
  2289    * @param string
  2402    * @param string
  3387     }
  3500     }
  3388     return $ret;
  3501     return $ret;
  3389   }
  3502   }
  3390   
  3503   
  3391   /**
  3504   /**
  3392    * Generates some Javascript that calls the AES encryption library.
  3505    * Generates some Javascript that calls the AES encryption library. Put this after your </form>.
  3393    * @param string The name of the form
  3506    * @param string The name of the form
  3394    * @param string The name of the password field
  3507    * @param string The name of the password field
  3395    * @param string The name of the field that switches encryption on or off
  3508    * @param string The name of the field that switches encryption on or off
  3396    * @param string The name of the field that contains the encryption key
  3509    * @param string The name of the field that contains the encryption key
  3397    * @param string The name of the field that will contain the encrypted password
  3510    * @param string The name of the field that will contain the encrypted password
  3400    * @param string The name of the field with the DiffieHellman public key
  3513    * @param string The name of the field with the DiffieHellman public key
  3401    * @param string The name of the field that the client should populate with its public key
  3514    * @param string The name of the field that the client should populate with its public key
  3402    * @return string
  3515    * @return string
  3403    */
  3516    */
  3404    
  3517    
  3405   function aes_javascript($form_name, $pw_field, $use_crypt, $crypt_key, $crypt_data, $challenge, $dh_supported = false, $dh_pubkey = false, $dh_client_pubkey = false)
  3518   function aes_javascript($form_name, $pw_field, $use_crypt = 'use_crypt', $crypt_key = 'crypt_key', $crypt_data = 'crypt_data', $challenge = 'challenge_data', $dh_supported = 'dh_supported', $dh_pubkey = 'dh_public_key', $dh_client_pubkey = 'dh_client_public_key')
  3406   {
  3519   {
  3407     $code = '
  3520     $code = '
  3408       <script type="text/javascript">
  3521       <script type="text/javascript">
  3409           
  3522           
  3410           function runEncryption()
  3523           function runEncryption(nowhiteout)
  3411           {
  3524           {
  3412             var frm = document.forms.'.$form_name.';
  3525             var frm = document.forms.'.$form_name.';
  3413             whiteOutForm(frm);
  3526             if ( !nowhiteout )
       
  3527               whiteOutForm(frm);
  3414             
  3528             
  3415             load_component(\'crypto\');
  3529             load_component(\'crypto\');
  3416             var testpassed = ' . ( ( isset($_GET['use_crypt']) && $_GET['use_crypt']=='0') ? 'false; // CRYPTO-AUTH DISABLED ON USER REQUEST // ' : '' ) . '( aes_self_test() && md5_vm_test() );
  3530             var testpassed = ' . ( ( isset($_GET['use_crypt']) && $_GET['use_crypt']=='0') ? 'false; // CRYPTO-AUTH DISABLED ON USER REQUEST // ' : '' ) . '( aes_self_test() && md5_vm_test() );
  3417             var use_diffiehellman = false;' . "\n";
  3531             var use_diffiehellman = false;' . "\n";
  3418     if ( $dh_supported && $dh_pubkey )
  3532     if ( $dh_supported && $dh_pubkey )
  3425     $code .= '
  3539     $code .= '
  3426     
  3540     
  3427             if ( frm[\'' . $dh_supported . '\'] )
  3541             if ( frm[\'' . $dh_supported . '\'] )
  3428             {
  3542             {
  3429               frm[\'' . $dh_supported . '\'].value = ( use_diffiehellman ) ? "true" : "false";
  3543               frm[\'' . $dh_supported . '\'].value = ( use_diffiehellman ) ? "true" : "false";
       
  3544             }
       
  3545             
       
  3546             if ( frm["' . $pw_field . '_confirm"] )
       
  3547             {
       
  3548               pass1 = frm.' . $pw_field . '.value;
       
  3549               pass2 = frm.' . $pw_field . '_confirm.value;
       
  3550               if ( pass1 != pass2 )
       
  3551               {
       
  3552                 load_component("l10n");
       
  3553                 alert($lang.get("userfuncs_passreset_err_no_match"));
       
  3554                 return false;
       
  3555               }
       
  3556               if ( pass1.length < 6 )
       
  3557               {
       
  3558                 load_component("l10n");
       
  3559                 alert($lang.get("userfuncs_passreset_err_too_short"));
       
  3560                 return false;
       
  3561               }
       
  3562               frm.' . $pw_field . '_confirm.value = "";
  3430             }
  3563             }
  3431             
  3564             
  3432             if ( testpassed && use_diffiehellman )
  3565             if ( testpassed && use_diffiehellman )
  3433             {
  3566             {
  3434               // try to blank out the table to prevent double submits and what have you
  3567               // try to blank out the table to prevent double submits and what have you
  3504               }
  3637               }
  3505               cryptstring = byteArrayToHex(cryptstring);
  3638               cryptstring = byteArrayToHex(cryptstring);
  3506               frm.'.$crypt_data.'.value = cryptstring;
  3639               frm.'.$crypt_data.'.value = cryptstring;
  3507               frm.'.$pw_field.'.value = \'\';
  3640               frm.'.$pw_field.'.value = \'\';
  3508             }
  3641             }
  3509             return false;
       
  3510           }
  3642           }
  3511         </script>
  3643         </script>
  3512         ';
  3644         ';
  3513     return $code;
  3645     return $code;
       
  3646   }
       
  3647   
       
  3648   /**
       
  3649    * Generates the HTML form elements required for an encrypted logon experience.
       
  3650    * @return string
       
  3651    */
       
  3652   
       
  3653   function generate_aes_form()
       
  3654   {
       
  3655     $return = '<input type="hidden" name="use_crypt" value="no" />';
       
  3656     $return .= '<input type="hidden" name="crypt_key" value="' . $this->rijndael_genkey() . '" />';
       
  3657     $return .= '<input type="hidden" name="crypt_data" value="" />';
       
  3658     $return .= '<input type="hidden" name="challenge_data" value="' . $this->dss_rand() . '" />';
       
  3659     
       
  3660     require_once(ENANO_ROOT . '/includes/math.php');
       
  3661     require_once(ENANO_ROOT . '/includes/diffiehellman.php');
       
  3662     
       
  3663     global $dh_supported, $_math;
       
  3664     if ( $dh_supported )
       
  3665     {
       
  3666       $dh_key_priv = dh_gen_private();
       
  3667       $dh_key_pub = dh_gen_public($dh_key_priv);
       
  3668       $dh_key_priv = $_math->str($dh_key_priv);
       
  3669       $dh_key_pub = $_math->str($dh_key_pub);
       
  3670       // store the keys in the DB
       
  3671       $this->sql('INSERT INTO ' . table_prefix . "diffiehellman( public_key, private_key ) VALUES ( '$dh_key_pub', '$dh_key_priv' );");
       
  3672       
       
  3673       $return .=  "<input type=\"hidden\" name=\"dh_supported\" value=\"true\" />
       
  3674             <input type=\"hidden\" name=\"dh_public_key\" value=\"$dh_key_pub\" />
       
  3675             <input type=\"hidden\" name=\"dh_client_public_key\" value=\"\" />";
       
  3676     }
       
  3677     else
       
  3678     {
       
  3679       $return .=  "<input type=\"hidden\" name=\"dh_supported\" value=\"false\" />";
       
  3680     }
       
  3681     return $return;
       
  3682   }
       
  3683   
       
  3684   /**
       
  3685    * If you used all the same form fields as the normal login interface, this will take care of DiffieHellman for you and return the key.
       
  3686    * @param string Password field name (defaults to "password")
       
  3687    * @return string
       
  3688    */
       
  3689   
       
  3690   function get_aes_post($fieldname = 'password')
       
  3691   {
       
  3692     global $db, $session, $paths, $template, $plugins; // Common objects
       
  3693     
       
  3694     $aes = AESCrypt::singleton(AES_BITS, AES_BLOCKSIZE);
       
  3695     if ( $_POST['use_crypt'] == 'yes' )
       
  3696     {
       
  3697       $crypt_key = $this->fetch_public_key($_POST['crypt_key']);
       
  3698       if ( !$crypt_key )
       
  3699       {
       
  3700         throw new Exception($lang->get('user_err_key_not_found'));
       
  3701       }
       
  3702       $crypt_key = hexdecode($crypt_key);
       
  3703       $data = $aes->decrypt($_POST['crypt_data'], $crypt_key, ENC_HEX);
       
  3704     }
       
  3705     else if ( $_POST['use_crypt'] == 'yes_dh' )
       
  3706     {
       
  3707       require_once(ENANO_ROOT . '/includes/math.php');
       
  3708       require_once(ENANO_ROOT . '/includes/diffiehellman.php');
       
  3709       
       
  3710       global $dh_supported, $_math;
       
  3711         
       
  3712       if ( !$dh_supported )
       
  3713       {
       
  3714         throw new Exception('Server does not support DiffieHellman, denying request');
       
  3715       }
       
  3716       
       
  3717       // Fetch private key
       
  3718       $dh_public = $_POST['dh_public_key'];
       
  3719       if ( !preg_match('/^[0-9]+$/', $dh_public) )
       
  3720       {
       
  3721         throw new Exception('ERR_DH_KEY_NOT_INTEGER');
       
  3722       }
       
  3723       $q = $db->sql_query('SELECT private_key, key_id FROM ' . table_prefix . "diffiehellman WHERE public_key = '$dh_public';");
       
  3724       if ( !$q )
       
  3725         $db->die_json();
       
  3726       
       
  3727       if ( $db->numrows() < 1 )
       
  3728       {
       
  3729         throw new Exception('ERR_DH_KEY_NOT_FOUND');
       
  3730       }
       
  3731       
       
  3732       list($dh_private, $dh_key_id) = $db->fetchrow_num();
       
  3733       $db->free_result();
       
  3734       
       
  3735       // We have the private key, now delete the key pair, we no longer need it
       
  3736       $q = $db->sql_query('DELETE FROM ' . table_prefix . "diffiehellman WHERE key_id = $dh_key_id;");
       
  3737       if ( !$q )
       
  3738         $db->die_json();
       
  3739       
       
  3740       // Generate the shared secret
       
  3741       $dh_secret = dh_gen_shared_secret($dh_private, $_POST['dh_client_public_key']);
       
  3742       $dh_secret = $_math->str($dh_secret);
       
  3743       
       
  3744       // Did we get all our math right?
       
  3745       $dh_secret_check = sha1($dh_secret);
       
  3746       $dh_hash = $_POST['crypt_key'];
       
  3747       if ( $dh_secret_check !== $dh_hash )
       
  3748       {
       
  3749         throw new Exception('ERR_DH_HASH_NO_MATCH');
       
  3750       }
       
  3751       
       
  3752       // All good! Generate the AES key
       
  3753       $aes_key = substr(sha256($dh_secret), 0, ( AES_BITS / 4 ));
       
  3754       
       
  3755       // decrypt user info
       
  3756       $aes_key = hexdecode($aes_key);
       
  3757       $aes = AESCrypt::singleton(AES_BITS, AES_BLOCKSIZE);
       
  3758       $data = $aes->decrypt($_POST['crypt_data'], $aes_key, ENC_HEX);
       
  3759     }
       
  3760     else
       
  3761     {
       
  3762       $data = $_POST[$fieldname];
       
  3763     }
       
  3764     return $data;
  3514   }
  3765   }
  3515   
  3766   
  3516   /**
  3767   /**
  3517    * Backend code for the JSON login interface. Basically a frontend to the session API that takes all parameters in one huge array.
  3768    * Backend code for the JSON login interface. Basically a frontend to the session API that takes all parameters in one huge array.
  3518    * @param array LoginAPI request
  3769    * @param array LoginAPI request
  3734         $username =& $userinfo['username'];
  3985         $username =& $userinfo['username'];
  3735         $password =& $userinfo['password'];
  3986         $password =& $userinfo['password'];
  3736         
  3987         
  3737         // If we're logging in with a temp password, attach to the login_password_reset hook to send our JSON response
  3988         // If we're logging in with a temp password, attach to the login_password_reset hook to send our JSON response
  3738         // A bit hackish since it just dies with the response :-(
  3989         // A bit hackish since it just dies with the response :-(
  3739         $plugins->attachHook('login_password_reset', '$this->process_login_request(array(\'mode\' => \'respond_password_reset\', \'user_id\' => $row[\'user_id\'], \'temp_password\' => $row[\'temp_password\']));');
  3990         $plugins->attachHook('login_password_reset', '$this->process_login_request(array(\'mode\' => \'respond_password_reset\', \'user_id\' => $row[\'user_id\'], \'temp_password\' => $this->pk_encrypt($password)));');
  3740         
  3991         
  3741         // attempt the login
  3992         // attempt the login
  3742         // function login_without_crypto($username, $password, $already_md5ed = false, $level = USER_LEVEL_MEMBER, $captcha_hash = false, $captcha_code = false)
  3993         // function login_without_crypto($username, $password, $already_md5ed = false, $level = USER_LEVEL_MEMBER, $captcha_hash = false, $captcha_code = false)
  3743         $login_result = $this->login_without_crypto($username, $password, false, intval($req['level']), @$req['captcha_hash'], @$req['captcha_code'], @$req['remember']);
  3994         $login_result = $this->login_without_crypto($username, $password, false, intval($req['level']), @$req['captcha_hash'], @$req['captcha_code'], @$req['remember']);
  3744         
  3995