573 * @param string $aes_key The MD5 hash of the encryption key, hex-encoded |
571 * @param string $aes_key The MD5 hash of the encryption key, hex-encoded |
574 * @param string $challenge The 256-bit MD5 challenge string - first 128 bits should be the hash, the last 128 should be the challenge salt |
572 * @param string $challenge The 256-bit MD5 challenge string - first 128 bits should be the hash, the last 128 should be the challenge salt |
575 * @param int $level The privilege level we're authenticating for, defaults to 0 |
573 * @param int $level The privilege level we're authenticating for, defaults to 0 |
576 * @param array $captcha_hash Optional. If we're locked out and the lockout policy is captcha, this should be the identifier for the code. |
574 * @param array $captcha_hash Optional. If we're locked out and the lockout policy is captcha, this should be the identifier for the code. |
577 * @param array $captcha_code Optional. If we're locked out and the lockout policy is captcha, this should be the code the user entered. |
575 * @param array $captcha_code Optional. If we're locked out and the lockout policy is captcha, this should be the code the user entered. |
|
576 * @param bool $lookup_key Optional. If true (default) this queries the database for the "real" encryption key. Else, uses what is given. |
578 * @return string 'success' on success, or error string on failure |
577 * @return string 'success' on success, or error string on failure |
579 */ |
578 */ |
580 |
579 |
581 function login_with_crypto($username, $aes_data, $aes_key_id, $challenge, $level = USER_LEVEL_MEMBER, $captcha_hash = false, $captcha_code = false) |
580 function login_with_crypto($username, $aes_data, $aes_key_id, $challenge, $level = USER_LEVEL_MEMBER, $captcha_hash = false, $captcha_code = false, $lookup_key = true) |
582 { |
581 { |
583 global $db, $session, $paths, $template, $plugins; // Common objects |
582 global $db, $session, $paths, $template, $plugins; // Common objects |
584 |
583 |
585 $privcache = $this->private_key; |
584 $privcache = $this->private_key; |
586 |
585 |
587 if ( !defined('IN_ENANO_INSTALL') ) |
586 if ( !defined('IN_ENANO_INSTALL') ) |
588 { |
587 { |
|
588 $timestamp_cutoff = time() - $duration; |
|
589 $q = $this->sql('SELECT timestamp FROM '.table_prefix.'lockout WHERE timestamp > ' . $timestamp_cutoff . ' AND ipaddr = \'' . $ipaddr . '\' ORDER BY timestamp DESC;'); |
|
590 $fails = $db->numrows(); |
589 // Lockout stuff |
591 // Lockout stuff |
590 $threshold = ( $_ = getConfig('lockout_threshold') ) ? intval($_) : 5; |
592 $threshold = ( $_ = getConfig('lockout_threshold') ) ? intval($_) : 5; |
591 $duration = ( $_ = getConfig('lockout_duration') ) ? intval($_) : 15; |
593 $duration = ( $_ = getConfig('lockout_duration') ) ? intval($_) : 15; |
592 // convert to minutes |
594 // convert to minutes |
593 $duration = $duration * 60; |
595 $duration = $duration * 60; |
617 'lockout_policy' => $policy, |
616 'lockout_policy' => $policy, |
618 'time_rem' => ( $duration / 60 ) - round( ( time() - $row['timestamp'] ) / 60 ), |
617 'time_rem' => ( $duration / 60 ) - round( ( time() - $row['timestamp'] ) / 60 ), |
619 'lockout_last_time' => $row['timestamp'] |
618 'lockout_last_time' => $row['timestamp'] |
620 ); |
619 ); |
621 } |
620 } |
622 $db->free_result(); |
621 } |
623 } |
622 $db->free_result(); |
624 } |
623 } |
625 |
624 |
626 // Instanciate the Rijndael encryption object |
625 // Instanciate the Rijndael encryption object |
627 $aes = AESCrypt::singleton(AES_BITS, AES_BLOCKSIZE); |
626 $aes = AESCrypt::singleton(AES_BITS, AES_BLOCKSIZE); |
628 |
627 |
629 // Fetch our decryption key |
628 // Fetch our decryption key |
630 |
629 |
631 $aes_key = $this->fetch_public_key($aes_key_id); |
630 if ( $lookup_key ) |
632 if ( !$aes_key ) |
631 { |
633 { |
632 $aes_key = $this->fetch_public_key($aes_key_id); |
634 // It could be that our key cache is full. If it seems larger than 65KB, clear it |
633 if ( !$aes_key ) |
635 if ( strlen(getConfig('login_key_cache')) > 65000 ) |
634 { |
636 { |
635 // It could be that our key cache is full. If it seems larger than 65KB, clear it |
637 setConfig('login_key_cache', ''); |
636 if ( strlen(getConfig('login_key_cache')) > 65000 ) |
|
637 { |
|
638 setConfig('login_key_cache', ''); |
|
639 return array( |
|
640 'success' => false, |
|
641 'error' => 'key_not_found_cleared', |
|
642 ); |
|
643 } |
638 return array( |
644 return array( |
639 'success' => false, |
645 'success' => false, |
640 'error' => 'key_not_found_cleared', |
646 'error' => 'key_not_found' |
641 ); |
647 ); |
642 } |
648 } |
643 return array( |
649 } |
644 'success' => false, |
650 else |
645 'error' => 'key_not_found' |
651 { |
646 ); |
652 $aes_key =& $aes_key_id; |
647 } |
653 } |
648 |
654 |
649 // Convert the key to a binary string |
655 // Convert the key to a binary string |
650 $bin_key = hexdecode($aes_key); |
656 $bin_key = hexdecode($aes_key); |
651 |
657 |
733 } |
739 } |
734 else |
740 else |
735 { |
741 { |
736 // 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 |
742 // 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 |
737 $real_pass = $aes->decrypt(hexdecode($row['password']), $this->private_key, ENC_BINARY); |
743 $real_pass = $aes->decrypt(hexdecode($row['password']), $this->private_key, ENC_BINARY); |
738 if($password == $real_pass) |
744 if($password === $real_pass && is_string($password)) |
739 { |
745 { |
740 // Yay! We passed AES authentication, now do an MD5 challenge check to make sure we weren't spoofed |
746 // Yay! We passed AES authentication. Previously an MD5 challenge was done here, this was deemed redundant in 1.1.3. |
741 $chal = substr($challenge, 0, 32); |
747 // It didn't seem to provide any additional security... |
742 $salt = substr($challenge, 32, 32); |
748 $success = true; |
743 $correct_challenge = md5( $real_pass . $salt ); |
|
744 if($chal == $correct_challenge) |
|
745 $success = true; |
|
746 } |
749 } |
747 } |
750 } |
748 if($success) |
751 if($success) |
749 { |
752 { |
750 if($level > $row['user_level']) |
753 if($level > $row['user_level']) |
751 return array( |
754 return array( |
752 'success' => false, |
755 'success' => false, |
753 'error' => 'too_big_for_britches' |
756 'error' => 'too_big_for_britches' |
754 ); |
757 ); |
|
758 |
|
759 /* |
|
760 return array( |
|
761 'success' => false, |
|
762 'error' => 'Successful authentication, but session manager is in debug mode - remove the "return array(...);" in includes/sessions.php:' . ( __LINE__ - 2 ) |
|
763 ); |
|
764 */ |
755 |
765 |
756 $sess = $this->register_session(intval($row['user_id']), $username, $password, $level); |
766 $sess = $this->register_session(intval($row['user_id']), $username, $password, $level); |
757 if($sess) |
767 if($sess) |
758 { |
768 { |
759 $this->username = $username; |
769 $this->username = $username; |
844 // Lockout stuff |
854 // Lockout stuff |
845 $threshold = ( $_ = getConfig('lockout_threshold') ) ? intval($_) : 5; |
855 $threshold = ( $_ = getConfig('lockout_threshold') ) ? intval($_) : 5; |
846 $duration = ( $_ = getConfig('lockout_duration') ) ? intval($_) : 15; |
856 $duration = ( $_ = getConfig('lockout_duration') ) ? intval($_) : 15; |
847 // convert to minutes |
857 // convert to minutes |
848 $duration = $duration * 60; |
858 $duration = $duration * 60; |
|
859 |
|
860 // get the lockout status |
|
861 $timestamp_cutoff = time() - $duration; |
|
862 $ipaddr = $db->escape($_SERVER['REMOTE_ADDR']); |
|
863 $q = $this->sql('SELECT timestamp FROM '.table_prefix.'lockout WHERE timestamp > ' . $timestamp_cutoff . ' AND ipaddr = \'' . $ipaddr . '\' ORDER BY timestamp DESC;'); |
|
864 $fails = $db->numrows(); |
|
865 |
849 $policy = ( $x = getConfig('lockout_policy') && in_array(getConfig('lockout_policy'), array('lockout', 'disable', 'captcha')) ) ? getConfig('lockout_policy') : 'lockout'; |
866 $policy = ( $x = getConfig('lockout_policy') && in_array(getConfig('lockout_policy'), array('lockout', 'disable', 'captcha')) ) ? getConfig('lockout_policy') : 'lockout'; |
|
867 $captcha_good = false; |
850 if ( $policy == 'captcha' && $captcha_hash && $captcha_code ) |
868 if ( $policy == 'captcha' && $captcha_hash && $captcha_code ) |
851 { |
869 { |
852 // policy is captcha -- check if it's correct, and if so, bypass lockout check |
870 // policy is captcha -- check if it's correct, and if so, bypass lockout check |
853 $real_code = $this->get_captcha($captcha_hash); |
871 $real_code = $this->get_captcha($captcha_hash); |
854 } |
872 $captcha_good = ( strtolower($real_code) === strtolower($captcha_code) ); |
855 if ( $policy != 'disable' && !( $policy == 'captcha' && isset($real_code) && $real_code == $captcha_code ) ) |
873 } |
856 { |
874 if ( $policy != 'disable' && !$captcha_good ) |
857 $ipaddr = $db->escape($_SERVER['REMOTE_ADDR']); |
875 { |
858 $timestamp_cutoff = time() - $duration; |
876 if ( $fails >= $threshold ) |
859 $q = $this->sql('SELECT timestamp FROM '.table_prefix.'lockout WHERE timestamp > ' . $timestamp_cutoff . ' AND ipaddr = \'' . $ipaddr . '\' ORDER BY timestamp DESC;'); |
|
860 $fails = $db->numrows(); |
|
861 if ( $fails > $threshold ) |
|
862 { |
877 { |
863 // ooh boy, somebody's in trouble ;-) |
878 // ooh boy, somebody's in trouble ;-) |
864 $row = $db->fetchrow(); |
879 $row = $db->fetchrow(); |
865 $db->free_result(); |
880 $db->free_result(); |
866 return array( |
881 return array( |
2835 { |
2850 { |
2836 global $db, $session, $paths, $template, $plugins; // Common objects |
2851 global $db, $session, $paths, $template, $plugins; // Common objects |
2837 |
2852 |
2838 if ( !preg_match('/^[a-f0-9]{32}([a-z0-9]{8})?$/', $hash) ) |
2853 if ( !preg_match('/^[a-f0-9]{32}([a-z0-9]{8})?$/', $hash) ) |
2839 { |
2854 { |
|
2855 die("session manager: bad captcha_hash $hash"); |
2840 return false; |
2856 return false; |
2841 } |
2857 } |
2842 |
2858 |
2843 // sanity check |
2859 // sanity check |
2844 if ( !is_valid_ip(@$_SERVER['REMOTE_ADDR']) || !is_int($this->user_id) ) |
2860 if ( !is_valid_ip(@$_SERVER['REMOTE_ADDR']) ) |
|
2861 { |
|
2862 die("session manager insanity: bad REMOTE_ADDR or invalid UID"); |
2845 return false; |
2863 return false; |
2846 |
2864 } |
2847 $q = $this->sql('SELECT code_id, code FROM ' . table_prefix . "captcha WHERE session_id = '$hash' AND source_ip = '{$_SERVER['REMOTE_ADDR']};"); |
2865 |
|
2866 $q = $this->sql('SELECT code_id, code FROM ' . table_prefix . "captcha WHERE session_id = '$hash' AND source_ip = '{$_SERVER['REMOTE_ADDR']}';"); |
2848 if ( $db->numrows() < 1 ) |
2867 if ( $db->numrows() < 1 ) |
|
2868 { |
|
2869 die("session manager: no rows for captcha_code $hash"); |
2849 return false; |
2870 return false; |
|
2871 } |
2850 |
2872 |
2851 list($code_id, $code) = $db->fetchrow_num(); |
2873 list($code_id, $code) = $db->fetchrow_num(); |
|
2874 |
2852 $db->free_result(); |
2875 $db->free_result(); |
2853 $this->sql('DELETE FROM ' . table_prefix . "captcha WHERE code_id = $code_id;"); |
2876 $this->sql('DELETE FROM ' . table_prefix . "captcha WHERE code_id = $code_id;"); |
|
2877 |
2854 return $code; |
2878 return $code; |
2855 } |
2879 } |
2856 |
2880 |
2857 /** |
2881 /** |
2858 * (AS OF 1.0.2: Deprecated. Captcha codes are now killed on first fetch for security.) Deletes all CAPTCHA codes cached in the DB for this user. |
2882 * (AS OF 1.0.2: Deprecated. Captcha codes are now killed on first fetch for security.) Deletes all CAPTCHA codes cached in the DB for this user. |
2951 </script> |
2975 </script> |
2952 '; |
2976 '; |
2953 return $code; |
2977 return $code; |
2954 } |
2978 } |
2955 |
2979 |
|
2980 /** |
|
2981 * Backend code for the JSON login interface. Basically a frontend to the session API that takes all parameters in one huge array. |
|
2982 * @param array LoginAPI request |
|
2983 * @return array LoginAPI response |
|
2984 */ |
|
2985 |
|
2986 function process_login_request($req) |
|
2987 { |
|
2988 global $db, $session, $paths, $template, $plugins; // Common objects |
|
2989 |
|
2990 // Setup EnanoMath and Diffie-Hellman |
|
2991 global $dh_supported; |
|
2992 $dh_supported = true; |
|
2993 try |
|
2994 { |
|
2995 require_once(ENANO_ROOT . '/includes/diffiehellman.php'); |
|
2996 } |
|
2997 catch ( Exception $e ) |
|
2998 { |
|
2999 $dh_supported = false; |
|
3000 } |
|
3001 global $_math; |
|
3002 |
|
3003 // Check for the mode |
|
3004 if ( !isset($req['mode']) ) |
|
3005 { |
|
3006 return array( |
|
3007 'mode' => 'error', |
|
3008 'error' => 'ERR_JSON_NO_MODE' |
|
3009 ); |
|
3010 } |
|
3011 |
|
3012 // Main processing switch |
|
3013 switch ( $req['mode'] ) |
|
3014 { |
|
3015 default: |
|
3016 return array( |
|
3017 'mode' => 'error', |
|
3018 'error' => 'ERR_JSON_INVALID_MODE' |
|
3019 ); |
|
3020 break; |
|
3021 case 'getkey': |
|
3022 |
|
3023 $this->start(); |
|
3024 |
|
3025 // Query database for lockout info |
|
3026 $locked_out = false; |
|
3027 // are we locked out? |
|
3028 $threshold = ( $_ = getConfig('lockout_threshold') ) ? intval($_) : 5; |
|
3029 $duration = ( $_ = getConfig('lockout_duration') ) ? intval($_) : 15; |
|
3030 // convert to minutes |
|
3031 $duration = $duration * 60; |
|
3032 $policy = ( $x = getConfig('lockout_policy') && in_array(getConfig('lockout_policy'), array('lockout', 'disable', 'captcha')) ) ? getConfig('lockout_policy') : 'lockout'; |
|
3033 if ( $policy != 'disable' ) |
|
3034 { |
|
3035 $ipaddr = $db->escape($_SERVER['REMOTE_ADDR']); |
|
3036 $timestamp_cutoff = time() - $duration; |
|
3037 $q = $this->sql('SELECT timestamp FROM '.table_prefix.'lockout WHERE timestamp > ' . $timestamp_cutoff . ' AND ipaddr = \'' . $ipaddr . '\' ORDER BY timestamp DESC;'); |
|
3038 $fails = $db->numrows(); |
|
3039 $row = $db->fetchrow(); |
|
3040 $locked_out = ( $fails >= $threshold ); |
|
3041 $lockdata = array( |
|
3042 'locked_out' => $locked_out, |
|
3043 'lockout_threshold' => $threshold, |
|
3044 'lockout_duration' => ( $duration / 60 ), |
|
3045 'lockout_fails' => $fails, |
|
3046 'lockout_policy' => $policy, |
|
3047 'lockout_last_time' => $row['timestamp'], |
|
3048 'time_rem' => ( $duration / 60 ) - round( ( time() - $row['timestamp'] ) / 60 ), |
|
3049 'captcha' => '' |
|
3050 ); |
|
3051 $db->free_result(); |
|
3052 } |
|
3053 |
|
3054 $response = array('mode' => 'build_box'); |
|
3055 $response['allow_diffiehellman'] = $dh_supported; |
|
3056 |
|
3057 $response['username'] = ( $this->user_logged_in ) ? $this->username : false; |
|
3058 $response['aes_key'] = $this->rijndael_genkey(); |
|
3059 |
|
3060 // Lockout info |
|
3061 $response['locked_out'] = $locked_out; |
|
3062 |
|
3063 $response['lockout_info'] = $lockdata; |
|
3064 if ( $policy == 'captcha' && $locked_out ) |
|
3065 { |
|
3066 $response['lockout_info']['captcha'] = $this->make_captcha(); |
|
3067 } |
|
3068 |
|
3069 // Can we do Diffie-Hellman? If so, generate and stash a public/private key pair. |
|
3070 if ( $dh_supported ) |
|
3071 { |
|
3072 $dh_key_priv = dh_gen_private(); |
|
3073 $dh_key_pub = dh_gen_public($dh_key_priv); |
|
3074 $dh_key_priv = $_math->str($dh_key_priv); |
|
3075 $dh_key_pub = $_math->str($dh_key_pub); |
|
3076 $response['dh_public_key'] = $dh_key_pub; |
|
3077 // store the keys in the DB |
|
3078 $q = $db->sql_query('INSERT INTO ' . table_prefix . "diffiehellman( public_key, private_key ) VALUES ( '$dh_key_pub', '$dh_key_priv' );"); |
|
3079 if ( !$q ) |
|
3080 $db->die_json(); |
|
3081 } |
|
3082 |
|
3083 return $response; |
|
3084 break; |
|
3085 case 'login_dh': |
|
3086 // User is requesting a login and has sent Diffie-Hellman data. |
|
3087 |
|
3088 // |
|
3089 // KEY RECONSTRUCTION |
|
3090 // |
|
3091 |
|
3092 $userinfo_crypt = $req['userinfo']; |
|
3093 $dh_public = $req['dh_public_key']; |
|
3094 $dh_hash = $req['dh_secret_hash']; |
|
3095 |
|
3096 // Check the key |
|
3097 if ( !preg_match('/^[0-9]+$/', $dh_public) || !preg_match('/^[0-9]+$/', $req['dh_client_key']) ) |
|
3098 { |
|
3099 return array( |
|
3100 'mode' => 'error', |
|
3101 'error' => 'ERR_DH_KEY_NOT_NUMERIC' |
|
3102 ); |
|
3103 } |
|
3104 |
|
3105 // Fetch private key |
|
3106 $q = $db->sql_query('SELECT private_key, key_id FROM ' . table_prefix . "diffiehellman WHERE public_key = '$dh_public';"); |
|
3107 if ( !$q ) |
|
3108 $db->die_json(); |
|
3109 |
|
3110 if ( $db->numrows() < 1 ) |
|
3111 { |
|
3112 return array( |
|
3113 'mode' => 'error', |
|
3114 'error' => 'ERR_DH_KEY_NOT_FOUND' |
|
3115 ); |
|
3116 } |
|
3117 |
|
3118 list($dh_private, $dh_key_id) = $db->fetchrow_num(); |
|
3119 $db->free_result(); |
|
3120 |
|
3121 // We have the private key, now delete the key pair, we no longer need it |
|
3122 $q = $db->sql_query('DELETE FROM ' . table_prefix . "diffiehellman WHERE key_id = $dh_key_id;"); |
|
3123 if ( !$q ) |
|
3124 $db->die_json(); |
|
3125 |
|
3126 // Generate the shared secret |
|
3127 $dh_secret = dh_gen_shared_secret($dh_private, $req['dh_client_key']); |
|
3128 $dh_secret = $_math->str($dh_secret); |
|
3129 |
|
3130 // Did we get all our math right? |
|
3131 $dh_secret_check = sha1($dh_secret); |
|
3132 if ( $dh_secret_check !== $dh_hash ) |
|
3133 { |
|
3134 return array( |
|
3135 'mode' => 'error', |
|
3136 'error' => 'ERR_DH_HASH_NO_MATCH' |
|
3137 ); |
|
3138 } |
|
3139 |
|
3140 // All good! Generate the AES key |
|
3141 $aes_key = substr(sha256($dh_secret), 0, ( AES_BITS / 4 )); |
|
3142 case 'login_aes': |
|
3143 if ( $req['mode'] == 'login_aes' ) |
|
3144 { |
|
3145 // login_aes-specific code |
|
3146 $aes_key = $this->fetch_public_key($req['key_aes']); |
|
3147 if ( !$aes_key ) |
|
3148 { |
|
3149 return array( |
|
3150 'mode' => 'error', |
|
3151 'error' => 'ERR_AES_LOOKUP_FAILED' |
|
3152 ); |
|
3153 } |
|
3154 $userinfo_crypt = $req['userinfo']; |
|
3155 } |
|
3156 // shared between the two systems from here on out |
|
3157 |
|
3158 // decrypt user info |
|
3159 $aes_key = hexdecode($aes_key); |
|
3160 $aes = AESCrypt::singleton(AES_BITS, AES_BLOCKSIZE); |
|
3161 $userinfo_json = $aes->decrypt($userinfo_crypt, $aes_key, ENC_HEX); |
|
3162 if ( !$userinfo_json ) |
|
3163 { |
|
3164 return array( |
|
3165 'mode' => 'error', |
|
3166 'error' => 'ERR_AES_DECRYPT_FAILED' |
|
3167 ); |
|
3168 } |
|
3169 // de-JSON user info |
|
3170 try |
|
3171 { |
|
3172 $userinfo = enano_json_decode($userinfo_json); |
|
3173 } |
|
3174 catch ( Exception $e ) |
|
3175 { |
|
3176 return array( |
|
3177 'mode' => 'error', |
|
3178 'error' => 'ERR_USERINFO_DECODE_FAILED' |
|
3179 ); |
|
3180 } |
|
3181 |
|
3182 if ( !isset($userinfo['username']) || !isset($userinfo['password']) ) |
|
3183 { |
|
3184 return array( |
|
3185 'mode' => 'error', |
|
3186 'error' => 'ERR_USERINFO_MISSING_VALUES' |
|
3187 ); |
|
3188 } |
|
3189 |
|
3190 $username =& $userinfo['username']; |
|
3191 $password =& $userinfo['password']; |
|
3192 |
|
3193 // attempt the login |
|
3194 // function login_without_crypto($username, $password, $already_md5ed = false, $level = USER_LEVEL_MEMBER, $captcha_hash = false, $captcha_code = false) |
|
3195 $login_result = $this->login_without_crypto($username, $password, false, intval($req['level']), @$req['captcha_hash'], @$req['captcha_code']); |
|
3196 |
|
3197 if ( $login_result['success'] ) |
|
3198 { |
|
3199 return array( |
|
3200 'mode' => 'login_success', |
|
3201 'key' => ( $this->sid_super ) ? $this->sid_super : false |
|
3202 ); |
|
3203 } |
|
3204 else |
|
3205 { |
|
3206 return array( |
|
3207 'mode' => 'login_failure', |
|
3208 'error_code' => $login_result['error'], |
|
3209 // Use this to provide a way to respawn the login box |
|
3210 'respawn_info' => $this->process_login_request(array('mode' => 'getkey')) |
|
3211 ); |
|
3212 } |
|
3213 |
|
3214 break; |
|
3215 } |
|
3216 |
|
3217 } |
|
3218 |
2956 } |
3219 } |
2957 |
3220 |
2958 /** |
3221 /** |
2959 * Class used to fetch permissions for a specific page. Used internally by SessionManager. |
3222 * Class used to fetch permissions for a specific page. Used internally by SessionManager. |
2960 * @package Enano |
3223 * @package Enano |