diff -r a434d60e525d -r 242353360e37 includes/sessions.php --- a/includes/sessions.php Tue Feb 19 08:32:57 2008 -0500 +++ b/includes/sessions.php Wed Feb 20 14:38:39 2008 -0500 @@ -47,11 +47,11 @@ var $username; /** - * User ID of currently logged-in user, or -1 if not logged in + * User ID of currently logged-in user, or 1 if not logged in * @var int */ - var $user_id; + var $user_id = 1; /** * Real name of currently logged-in user, or blank if not logged in @@ -154,7 +154,7 @@ * @var string */ - var $auth_level = -1; + var $auth_level = 1; /** * State variable to track if a session timed out @@ -475,9 +475,7 @@ $this->signature = $userdata['signature']; $this->reg_time = $userdata['reg_time']; } - // Small security risk here - it allows someone who has already authenticated as an administrator to store the "super" key in - // the cookie. Change this to USER_LEVEL_MEMBER to override that. The same 15-minute restriction applies to this "exploit". - $this->auth_level = $userdata['auth_level']; + $this->auth_level = USER_LEVEL_MEMBER; if(!isset($template->named_theme_list[$this->theme])) { if($this->compat || !is_object($template)) @@ -575,10 +573,11 @@ * @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 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) + function login_with_crypto($username, $aes_data, $aes_key_id, $challenge, $level = USER_LEVEL_MEMBER, $captcha_hash = false, $captcha_code = false, $lookup_key = true) { global $db, $session, $paths, $template, $plugins; // Common objects @@ -586,6 +585,9 @@ 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; @@ -600,9 +602,6 @@ if ( $policy != 'disable' && !( $policy == 'captcha' && isset($real_code) && strtolower($real_code) == strtolower($captcha_code) ) ) { $ipaddr = $db->escape($_SERVER['REMOTE_ADDR']); - $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(); if ( $fails >= $threshold ) { // ooh boy, somebody's in trouble ;-) @@ -619,8 +618,8 @@ 'lockout_last_time' => $row['timestamp'] ); } - $db->free_result(); } + $db->free_result(); } // Instanciate the Rijndael encryption object @@ -628,22 +627,29 @@ // Fetch our decryption key - $aes_key = $this->fetch_public_key($aes_key_id); - if ( !$aes_key ) + if ( $lookup_key ) { - // It could be that our key cache is full. If it seems larger than 65KB, clear it - if ( strlen(getConfig('login_key_cache')) > 65000 ) + $aes_key = $this->fetch_public_key($aes_key_id); + if ( !$aes_key ) { - setConfig('login_key_cache', ''); + // It could be that our key cache is full. If it seems larger than 65KB, clear it + if ( strlen(getConfig('login_key_cache')) > 65000 ) + { + setConfig('login_key_cache', ''); + return array( + 'success' => false, + 'error' => 'key_not_found_cleared', + ); + } return array( 'success' => false, - 'error' => 'key_not_found_cleared', + 'error' => 'key_not_found' ); } - return array( - 'success' => false, - 'error' => 'key_not_found' - ); + } + else + { + $aes_key =& $aes_key_id; } // Convert the key to a binary string @@ -735,14 +741,11 @@ { // 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) + if($password === $real_pass && is_string($password)) { - // Yay! We passed AES authentication, now do an MD5 challenge check to make sure we weren't spoofed - $chal = substr($challenge, 0, 32); - $salt = substr($challenge, 32, 32); - $correct_challenge = md5( $real_pass . $salt ); - if($chal == $correct_challenge) - $success = true; + // 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) @@ -752,6 +755,13 @@ '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) @@ -823,7 +833,7 @@ * @param int $level The privilege level we're authenticating for, defaults to 0 */ - function login_without_crypto($username, $password, $already_md5ed = false, $level = USER_LEVEL_MEMBER) + function login_without_crypto($username, $password, $already_md5ed = false, $level = USER_LEVEL_MEMBER, $captcha_hash = false, $captcha_code = false) { global $db, $session, $paths, $template, $plugins; // Common objects @@ -846,19 +856,24 @@ $duration = ( $_ = getConfig('lockout_duration') ) ? intval($_) : 15; // convert to minutes $duration = $duration * 60; + + // get the lockout status + $timestamp_cutoff = time() - $duration; + $ipaddr = $db->escape($_SERVER['REMOTE_ADDR']); + $q = $this->sql('SELECT timestamp FROM '.table_prefix.'lockout WHERE timestamp > ' . $timestamp_cutoff . ' AND ipaddr = \'' . $ipaddr . '\' ORDER BY timestamp DESC;'); + $fails = $db->numrows(); + $policy = ( $x = getConfig('lockout_policy') && in_array(getConfig('lockout_policy'), array('lockout', 'disable', 'captcha')) ) ? getConfig('lockout_policy') : 'lockout'; + $captcha_good = false; 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); + $captcha_good = ( strtolower($real_code) === strtolower($captcha_code) ); } - if ( $policy != 'disable' && !( $policy == 'captcha' && isset($real_code) && $real_code == $captcha_code ) ) + if ( $policy != 'disable' && !$captcha_good ) { - $ipaddr = $db->escape($_SERVER['REMOTE_ADDR']); - $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(); - if ( $fails > $threshold ) + if ( $fails >= $threshold ) { // ooh boy, somebody's in trouble ;-) $row = $db->fetchrow(); @@ -870,12 +885,12 @@ 'lockout_duration' => ( $duration / 60 ), 'lockout_fails' => $fails, 'lockout_policy' => $policy, - 'time_rem' => $duration - round( ( time() - $row['timestamp'] ) / 60 ), + 'time_rem' => ( $duration / 60 ) - round( ( time() - $row['timestamp'] ) / 60 ), 'lockout_last_time' => $row['timestamp'] ); } - $db->free_result(); } + $db->free_result(); } // Instanciate the Rijndael encryption object @@ -2837,20 +2852,29 @@ if ( !preg_match('/^[a-f0-9]{32}([a-z0-9]{8})?$/', $hash) ) { + die("session manager: bad captcha_hash $hash"); return false; } // sanity check - if ( !is_valid_ip(@$_SERVER['REMOTE_ADDR']) || !is_int($this->user_id) ) + if ( !is_valid_ip(@$_SERVER['REMOTE_ADDR']) ) + { + die("session manager insanity: bad REMOTE_ADDR or invalid UID"); return false; + } - $q = $this->sql('SELECT code_id, code FROM ' . table_prefix . "captcha WHERE session_id = '$hash' AND source_ip = '{$_SERVER['REMOTE_ADDR']};"); + $q = $this->sql('SELECT code_id, code FROM ' . table_prefix . "captcha WHERE session_id = '$hash' AND source_ip = '{$_SERVER['REMOTE_ADDR']}';"); if ( $db->numrows() < 1 ) + { + die("session manager: no rows for captcha_code $hash"); return false; + } list($code_id, $code) = $db->fetchrow_num(); + $db->free_result(); $this->sql('DELETE FROM ' . table_prefix . "captcha WHERE code_id = $code_id;"); + return $code; } @@ -2953,6 +2977,245 @@ return $code; } + /** + * Backend code for the JSON login interface. Basically a frontend to the session API that takes all parameters in one huge array. + * @param array LoginAPI request + * @return array LoginAPI response + */ + + function process_login_request($req) + { + global $db, $session, $paths, $template, $plugins; // Common objects + + // Setup EnanoMath and Diffie-Hellman + global $dh_supported; + $dh_supported = true; + try + { + require_once(ENANO_ROOT . '/includes/diffiehellman.php'); + } + catch ( Exception $e ) + { + $dh_supported = false; + } + global $_math; + + // Check for the mode + if ( !isset($req['mode']) ) + { + return array( + 'mode' => 'error', + 'error' => 'ERR_JSON_NO_MODE' + ); + } + + // Main processing switch + switch ( $req['mode'] ) + { + default: + return array( + 'mode' => 'error', + 'error' => 'ERR_JSON_INVALID_MODE' + ); + break; + case 'getkey': + + $this->start(); + + // Query database for lockout info + $locked_out = false; + // are we locked out? + $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 != 'disable' ) + { + $ipaddr = $db->escape($_SERVER['REMOTE_ADDR']); + $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(); + $row = $db->fetchrow(); + $locked_out = ( $fails >= $threshold ); + $lockdata = array( + 'locked_out' => $locked_out, + 'lockout_threshold' => $threshold, + 'lockout_duration' => ( $duration / 60 ), + 'lockout_fails' => $fails, + 'lockout_policy' => $policy, + 'lockout_last_time' => $row['timestamp'], + 'time_rem' => ( $duration / 60 ) - round( ( time() - $row['timestamp'] ) / 60 ), + 'captcha' => '' + ); + $db->free_result(); + } + + $response = array('mode' => 'build_box'); + $response['allow_diffiehellman'] = $dh_supported; + + $response['username'] = ( $this->user_logged_in ) ? $this->username : false; + $response['aes_key'] = $this->rijndael_genkey(); + + // Lockout info + $response['locked_out'] = $locked_out; + + $response['lockout_info'] = $lockdata; + if ( $policy == 'captcha' && $locked_out ) + { + $response['lockout_info']['captcha'] = $this->make_captcha(); + } + + // Can we do Diffie-Hellman? If so, generate and stash a public/private key pair. + if ( $dh_supported ) + { + $dh_key_priv = dh_gen_private(); + $dh_key_pub = dh_gen_public($dh_key_priv); + $dh_key_priv = $_math->str($dh_key_priv); + $dh_key_pub = $_math->str($dh_key_pub); + $response['dh_public_key'] = $dh_key_pub; + // store the keys in the DB + $q = $db->sql_query('INSERT INTO ' . table_prefix . "diffiehellman( public_key, private_key ) VALUES ( '$dh_key_pub', '$dh_key_priv' );"); + if ( !$q ) + $db->die_json(); + } + + return $response; + break; + case 'login_dh': + // User is requesting a login and has sent Diffie-Hellman data. + + // + // KEY RECONSTRUCTION + // + + $userinfo_crypt = $req['userinfo']; + $dh_public = $req['dh_public_key']; + $dh_hash = $req['dh_secret_hash']; + + // Check the key + if ( !preg_match('/^[0-9]+$/', $dh_public) || !preg_match('/^[0-9]+$/', $req['dh_client_key']) ) + { + return array( + 'mode' => 'error', + 'error' => 'ERR_DH_KEY_NOT_NUMERIC' + ); + } + + // Fetch private key + $q = $db->sql_query('SELECT private_key, key_id FROM ' . table_prefix . "diffiehellman WHERE public_key = '$dh_public';"); + if ( !$q ) + $db->die_json(); + + if ( $db->numrows() < 1 ) + { + return array( + 'mode' => 'error', + 'error' => 'ERR_DH_KEY_NOT_FOUND' + ); + } + + list($dh_private, $dh_key_id) = $db->fetchrow_num(); + $db->free_result(); + + // We have the private key, now delete the key pair, we no longer need it + $q = $db->sql_query('DELETE FROM ' . table_prefix . "diffiehellman WHERE key_id = $dh_key_id;"); + if ( !$q ) + $db->die_json(); + + // Generate the shared secret + $dh_secret = dh_gen_shared_secret($dh_private, $req['dh_client_key']); + $dh_secret = $_math->str($dh_secret); + + // Did we get all our math right? + $dh_secret_check = sha1($dh_secret); + if ( $dh_secret_check !== $dh_hash ) + { + return array( + 'mode' => 'error', + 'error' => 'ERR_DH_HASH_NO_MATCH' + ); + } + + // All good! Generate the AES key + $aes_key = substr(sha256($dh_secret), 0, ( AES_BITS / 4 )); + case 'login_aes': + if ( $req['mode'] == 'login_aes' ) + { + // login_aes-specific code + $aes_key = $this->fetch_public_key($req['key_aes']); + if ( !$aes_key ) + { + return array( + 'mode' => 'error', + 'error' => 'ERR_AES_LOOKUP_FAILED' + ); + } + $userinfo_crypt = $req['userinfo']; + } + // shared between the two systems from here on out + + // decrypt user info + $aes_key = hexdecode($aes_key); + $aes = AESCrypt::singleton(AES_BITS, AES_BLOCKSIZE); + $userinfo_json = $aes->decrypt($userinfo_crypt, $aes_key, ENC_HEX); + if ( !$userinfo_json ) + { + return array( + 'mode' => 'error', + 'error' => 'ERR_AES_DECRYPT_FAILED' + ); + } + // de-JSON user info + try + { + $userinfo = enano_json_decode($userinfo_json); + } + catch ( Exception $e ) + { + return array( + 'mode' => 'error', + 'error' => 'ERR_USERINFO_DECODE_FAILED' + ); + } + + if ( !isset($userinfo['username']) || !isset($userinfo['password']) ) + { + return array( + 'mode' => 'error', + 'error' => 'ERR_USERINFO_MISSING_VALUES' + ); + } + + $username =& $userinfo['username']; + $password =& $userinfo['password']; + + // 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']); + + if ( $login_result['success'] ) + { + return array( + 'mode' => 'login_success', + 'key' => ( $this->sid_super ) ? $this->sid_super : false + ); + } + else + { + return array( + 'mode' => 'login_failure', + 'error_code' => $login_result['error'], + // Use this to provide a way to respawn the login box + 'respawn_info' => $this->process_login_request(array('mode' => 'getkey')) + ); + } + + break; + } + + } + } /**