561 * @param string $username The username |
561 * @param string $username The username |
562 * @param string $aes_data The encrypted password, hex-encoded |
562 * @param string $aes_data The encrypted password, hex-encoded |
563 * @param string $aes_key The MD5 hash of the encryption key, hex-encoded |
563 * @param string $aes_key The MD5 hash of the encryption key, hex-encoded |
564 * @param string $challenge The 256-bit MD5 challenge string - first 128 bits should be the hash, the last 128 should be the challenge salt |
564 * @param string $challenge The 256-bit MD5 challenge string - first 128 bits should be the hash, the last 128 should be the challenge salt |
565 * @param int $level The privilege level we're authenticating for, defaults to 0 |
565 * @param int $level The privilege level we're authenticating for, defaults to 0 |
566 * @param array $captcha_hash Optional. If we're locked out and the lockout policy is captcha, this should be the identifier for the code. |
566 * @param string $captcha_hash Optional. If we're locked out and the lockout policy is captcha, this should be the identifier for the code. |
567 * @param array $captcha_code Optional. If we're locked out and the lockout policy is captcha, this should be the code the user entered. |
567 * @param string $captcha_code Optional. If we're locked out and the lockout policy is captcha, this should be the code the user entered. |
|
568 * @param bool $remember Optional. If true, remembers the session for X days. Otherwise, assigns a short session. Defaults to false. |
568 * @param bool $lookup_key Optional. If true (default) this queries the database for the "real" encryption key. Else, uses what is given. |
569 * @param bool $lookup_key Optional. If true (default) this queries the database for the "real" encryption key. Else, uses what is given. |
569 * @return string 'success' on success, or error string on failure |
570 * @return string 'success' on success, or error string on failure |
570 */ |
571 */ |
571 |
572 |
572 function login_with_crypto($username, $aes_data, $aes_key_id, $challenge, $level = USER_LEVEL_MEMBER, $captcha_hash = false, $captcha_code = false, $lookup_key = true) |
573 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) |
573 { |
574 { |
574 global $db, $session, $paths, $template, $plugins; // Common objects |
575 global $db, $session, $paths, $template, $plugins; // Common objects |
575 |
|
576 $privcache = $this->private_key; |
|
577 |
|
578 if ( !defined('IN_ENANO_INSTALL') ) |
|
579 { |
|
580 $timestamp_cutoff = time() - $duration; |
|
581 $q = $this->sql('SELECT timestamp FROM '.table_prefix.'lockout WHERE timestamp > ' . $timestamp_cutoff . ' AND ipaddr = \'' . $ipaddr . '\' ORDER BY timestamp DESC;'); |
|
582 $fails = $db->numrows(); |
|
583 // Lockout stuff |
|
584 $threshold = ( $_ = getConfig('lockout_threshold') ) ? intval($_) : 5; |
|
585 $duration = ( $_ = getConfig('lockout_duration') ) ? intval($_) : 15; |
|
586 // convert to minutes |
|
587 $duration = $duration * 60; |
|
588 $policy = ( $x = getConfig('lockout_policy') && in_array(getConfig('lockout_policy'), array('lockout', 'disable', 'captcha')) ) ? getConfig('lockout_policy') : 'lockout'; |
|
589 if ( $policy == 'captcha' && $captcha_hash && $captcha_code ) |
|
590 { |
|
591 // policy is captcha -- check if it's correct, and if so, bypass lockout check |
|
592 $real_code = $this->get_captcha($captcha_hash); |
|
593 } |
|
594 if ( $policy != 'disable' && !( $policy == 'captcha' && isset($real_code) && strtolower($real_code) == strtolower($captcha_code) ) ) |
|
595 { |
|
596 $ipaddr = $db->escape($_SERVER['REMOTE_ADDR']); |
|
597 if ( $fails >= $threshold ) |
|
598 { |
|
599 // ooh boy, somebody's in trouble ;-) |
|
600 $row = $db->fetchrow(); |
|
601 $db->free_result(); |
|
602 return array( |
|
603 'success' => false, |
|
604 'error' => 'locked_out', |
|
605 'lockout_threshold' => $threshold, |
|
606 'lockout_duration' => ( $duration / 60 ), |
|
607 'lockout_fails' => $fails, |
|
608 'lockout_policy' => $policy, |
|
609 'time_rem' => ( $duration / 60 ) - round( ( time() - $row['timestamp'] ) / 60 ), |
|
610 'lockout_last_time' => $row['timestamp'] |
|
611 ); |
|
612 } |
|
613 } |
|
614 $db->free_result(); |
|
615 } |
|
616 |
576 |
617 // Instanciate the Rijndael encryption object |
577 // Instanciate the Rijndael encryption object |
618 $aes = AESCrypt::singleton(AES_BITS, AES_BLOCKSIZE); |
578 $aes = AESCrypt::singleton(AES_BITS, AES_BLOCKSIZE); |
619 |
579 |
620 // Fetch our decryption key |
580 // Fetch our decryption key |
654 ); |
614 ); |
655 |
615 |
656 // Decrypt our password |
616 // Decrypt our password |
657 $password = $aes->decrypt($aes_data, $bin_key, ENC_HEX); |
617 $password = $aes->decrypt($aes_data, $bin_key, ENC_HEX); |
658 |
618 |
659 // Initialize our success switch |
619 // Let the LoginAPI do the rest. |
660 $success = false; |
620 return $this->login_without_crypto($username, $password, false, $level, $captcha_hash, $captcha_code, $remember); |
661 |
|
662 // Escaped username |
|
663 $username = str_replace('_', ' ', $username); |
|
664 $db_username_lower = $this->prepare_text(strtolower($username)); |
|
665 $db_username = $this->prepare_text($username); |
|
666 |
|
667 // Select the user data from the table, and decrypt that so we can verify the password |
|
668 $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 . '\';'); |
|
669 if($db->numrows() < 1) |
|
670 { |
|
671 // This wasn't logged in <1.0.2, dunno how it slipped through |
|
672 if($level > USER_LEVEL_MEMBER) |
|
673 $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) . ')'); |
|
674 else |
|
675 $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']).'\')'); |
|
676 |
|
677 if ( $policy != 'disable' && !defined('IN_ENANO_INSTALL') ) |
|
678 { |
|
679 $ipaddr = $db->escape($_SERVER['REMOTE_ADDR']); |
|
680 // increment fail count |
|
681 $this->sql('INSERT INTO '.table_prefix.'lockout(ipaddr, timestamp, action) VALUES(\'' . $ipaddr . '\', ' . time() . ', \'credential\');'); |
|
682 $fails++; |
|
683 // ooh boy, somebody's in trouble ;-) |
|
684 return array( |
|
685 'success' => false, |
|
686 'error' => ( $fails >= $threshold ) ? 'locked_out' : 'invalid_credentials', |
|
687 'lockout_threshold' => $threshold, |
|
688 'lockout_duration' => ( $duration / 60 ), |
|
689 'lockout_fails' => $fails, |
|
690 'time_rem' => ( $duration / 60 ), |
|
691 'lockout_policy' => $policy |
|
692 ); |
|
693 } |
|
694 |
|
695 return array( |
|
696 'success' => false, |
|
697 'error' => 'invalid_credentials' |
|
698 ); |
|
699 } |
|
700 $row = $db->fetchrow(); |
|
701 |
|
702 // Check to see if we're logging in using a temporary password |
|
703 |
|
704 if((intval($row['temp_password_time']) + 3600*24) > time() ) |
|
705 { |
|
706 $temp_pass = $aes->decrypt( $row['temp_password'], $this->private_key, ENC_HEX ); |
|
707 if( $temp_pass == $password ) |
|
708 { |
|
709 $url = makeUrlComplete('Special', 'PasswordReset/stage2/' . $row['user_id'] . '/' . $row['temp_password']); |
|
710 |
|
711 $code = $plugins->setHook('login_password_reset'); |
|
712 foreach ( $code as $cmd ) |
|
713 { |
|
714 eval($cmd); |
|
715 } |
|
716 |
|
717 redirect($url, '', '', 0); |
|
718 exit; |
|
719 } |
|
720 } |
|
721 |
|
722 if($row['old_encryption'] == 1) |
|
723 { |
|
724 // The user's password is stored using the obsolete and insecure MD5 algorithm, so we'll update the field with the new password |
|
725 if(md5($password) == $row['password']) |
|
726 { |
|
727 $pass_stashed = $aes->encrypt($password, $this->private_key, ENC_HEX); |
|
728 $this->sql('UPDATE '.table_prefix.'users SET password=\''.$pass_stashed.'\',old_encryption=0 WHERE user_id='.$row['user_id'].';'); |
|
729 $success = true; |
|
730 } |
|
731 } |
|
732 else |
|
733 { |
|
734 // 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 |
|
735 $real_pass = $aes->decrypt(hexdecode($row['password']), $this->private_key, ENC_BINARY); |
|
736 if($password === $real_pass && is_string($password)) |
|
737 { |
|
738 // Yay! We passed AES authentication. Previously an MD5 challenge was done here, this was deemed redundant in 1.1.3. |
|
739 // It didn't seem to provide any additional security... |
|
740 $success = true; |
|
741 } |
|
742 } |
|
743 if($success) |
|
744 { |
|
745 if($level > $row['user_level']) |
|
746 return array( |
|
747 'success' => false, |
|
748 'error' => 'too_big_for_britches' |
|
749 ); |
|
750 |
|
751 /* |
|
752 return array( |
|
753 'success' => false, |
|
754 'error' => 'Successful authentication, but session manager is in debug mode - remove the "return array(...);" in includes/sessions.php:' . ( __LINE__ - 2 ) |
|
755 ); |
|
756 */ |
|
757 |
|
758 $sess = $this->register_session(intval($row['user_id']), $username, $password, $level); |
|
759 if($sess) |
|
760 { |
|
761 $this->username = $username; |
|
762 $this->user_id = intval($row['user_id']); |
|
763 $this->theme = $row['theme']; |
|
764 $this->style = $row['style']; |
|
765 |
|
766 if($level > USER_LEVEL_MEMBER) |
|
767 $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) . ')'); |
|
768 else |
|
769 $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']).'\')'); |
|
770 |
|
771 $code = $plugins->setHook('login_success'); |
|
772 foreach ( $code as $cmd ) |
|
773 { |
|
774 eval($cmd); |
|
775 } |
|
776 return array( |
|
777 'success' => true |
|
778 ); |
|
779 } |
|
780 else |
|
781 return array( |
|
782 'success' => false, |
|
783 'error' => 'backend_fail' |
|
784 ); |
|
785 } |
|
786 else |
|
787 { |
|
788 if($level > USER_LEVEL_MEMBER) |
|
789 $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) . ')'); |
|
790 else |
|
791 $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']).'\')'); |
|
792 |
|
793 // Do we also need to increment the lockout countdown? |
|
794 if ( $policy != 'disable' && !defined('IN_ENANO_INSTALL') ) |
|
795 { |
|
796 $ipaddr = $db->escape($_SERVER['REMOTE_ADDR']); |
|
797 // increment fail count |
|
798 $this->sql('INSERT INTO '.table_prefix.'lockout(ipaddr, timestamp, action) VALUES(\'' . $ipaddr . '\', ' . time() . ', \'credential\');'); |
|
799 $fails++; |
|
800 return array( |
|
801 'success' => false, |
|
802 'error' => ( $fails >= $threshold ) ? 'locked_out' : 'invalid_credentials', |
|
803 'lockout_threshold' => $threshold, |
|
804 'lockout_duration' => ( $duration / 60 ), |
|
805 'lockout_fails' => $fails, |
|
806 'time_rem' => ( $duration / 60 ), |
|
807 'lockout_policy' => $policy |
|
808 ); |
|
809 } |
|
810 |
|
811 return array( |
|
812 'success' => false, |
|
813 'error' => 'invalid_credentials' |
|
814 ); |
|
815 } |
|
816 } |
621 } |
817 |
622 |
818 /** |
623 /** |
819 * Attempts to login without using crypto stuff, mainly for use when the other side doesn't like Javascript |
624 * Attempts to login without using crypto stuff, mainly for use when the other side doesn't like Javascript |
820 * This method of authentication is inherently insecure, there's really nothing we can do about it except hope and pray that everyone moves to Firefox |
625 * This method of authentication is inherently insecure, there's really nothing we can do about it except hope and pray that everyone moves to Firefox |
821 * Technically it still uses crypto, but it only decrypts the password already stored, which is (obviously) required for authentication |
626 * Technically it still uses crypto, but it only decrypts the password already stored, which is (obviously) required for authentication |
822 * @param string $username The username |
627 * @param string $username The username |
823 * @param string $password The password -OR- the MD5 hash of the password if $already_md5ed is true |
628 * @param string $password The password -OR- the MD5 hash of the password if $already_md5ed is true |
824 * @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. |
629 * @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. |
825 * @param int $level The privilege level we're authenticating for, defaults to 0 |
630 * @param int $level The privilege level we're authenticating for, defaults to 0 |
826 */ |
631 * @param string $captcha_hash Optional. If we're locked out and the lockout policy is captcha, this should be the identifier for the code. |
827 |
632 * @param string $captcha_code Optional. If we're locked out and the lockout policy is captcha, this should be the code the user entered. |
828 function login_without_crypto($username, $password, $already_md5ed = false, $level = USER_LEVEL_MEMBER, $captcha_hash = false, $captcha_code = false) |
633 * @param bool $remember Optional. If true, remembers the session for X days. Otherwise, assigns a short session. Defaults to false. |
|
634 */ |
|
635 |
|
636 function login_without_crypto($username, $password, $already_md5ed = false, $level = USER_LEVEL_MEMBER, $captcha_hash = false, $captcha_code = false, $remember = false) |
829 { |
637 { |
830 global $db, $session, $paths, $template, $plugins; // Common objects |
638 global $db, $session, $paths, $template, $plugins; // Common objects |
831 |
639 |
832 $pass_hashed = ( $already_md5ed ) ? $password : md5($password); |
640 $pass_hashed = ( $already_md5ed ) ? $password : md5($password); |
833 |
641 |
1070 * 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]" |
878 * 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]" |
1071 * @param int $user_id |
879 * @param int $user_id |
1072 * @param string $username |
880 * @param string $username |
1073 * @param string $password |
881 * @param string $password |
1074 * @param int $level The level of access to grant, defaults to USER_LEVEL_MEMBER |
882 * @param int $level The level of access to grant, defaults to USER_LEVEL_MEMBER |
|
883 * @param bool $remember Whether the session should be long-term (true) or not (false). Defaults to short-term. |
1075 * @return bool |
884 * @return bool |
1076 */ |
885 */ |
1077 |
886 |
1078 function register_session($user_id, $username, $password, $level = USER_LEVEL_MEMBER) |
887 function register_session($user_id, $username, $password, $level = USER_LEVEL_MEMBER, $remember = false) |
1079 { |
888 { |
1080 // Random key identifier |
889 // Random key identifier |
1081 $salt = md5(microtime() . mt_rand()); |
890 $salt = md5(microtime() . mt_rand()); |
1082 |
891 |
1083 // SHA1 hash of password, stored in the key |
892 // SHA1 hash of password, stored in the key |
1084 $passha1 = sha1($password); |
893 $passha1 = sha1($password); |
1085 |
894 |
1086 // Unencrypted session key |
895 // Unencrypted session key |
1087 $session_key = "u=$username;p=$passha1;s=$salt"; |
896 $session_key = "u=$username;p=$passha1;s=$salt"; |
|
897 |
|
898 // Type of key |
|
899 $key_type = ( $level > USER_LEVEL_MEMBER ) ? SK_ELEV : ( $remember ? SK_LONG : SK_SHORT ); |
1088 |
900 |
1089 // Encrypt the key |
901 // Encrypt the key |
1090 $aes = AESCrypt::singleton(AES_BITS, AES_BLOCKSIZE); |
902 $aes = AESCrypt::singleton(AES_BITS, AES_BLOCKSIZE); |
1091 $session_key = $aes->encrypt($session_key, $this->private_key, ENC_HEX); |
903 $session_key = $aes->encrypt($session_key, $this->private_key, ENC_HEX); |
1092 $dec_DEBUG = $aes->decrypt($session_key, $this->private_key, ENC_HEX); |
|
1093 |
904 |
1094 // If we're registering an elevated-privilege key, it needs to be on GET |
905 // If we're registering an elevated-privilege key, it needs to be on GET |
1095 if($level > USER_LEVEL_MEMBER) |
906 if($level > USER_LEVEL_MEMBER) |
1096 { |
907 { |
1097 // Reverse it - cosmetic only ;-) |
908 // Reverse it - cosmetic only ;-) |
1218 $keyhash = md5($key); |
1029 $keyhash = md5($key); |
1219 $salt = $db->escape($keydata[3]); |
1030 $salt = $db->escape($keydata[3]); |
1220 profiler_log("SessionManager: checking session: " . sha1($key) . ": decrypted session key to $decrypted_key"); |
1031 profiler_log("SessionManager: checking session: " . sha1($key) . ": decrypted session key to $decrypted_key"); |
1221 // using a normal call to $db->sql_query to avoid failing on errors here |
1032 // using a normal call to $db->sql_query to avoid failing on errors here |
1222 $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" |
1033 $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" |
1223 . ' 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" |
1034 . ' 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" |
1224 . ' u.user_timezone, x.* FROM '.table_prefix.'session_keys AS k' . "\n" |
1035 . ' u.user_timezone, x.* FROM '.table_prefix.'session_keys AS k' . "\n" |
1225 . ' LEFT JOIN '.table_prefix.'users AS u' . "\n" |
1036 . ' LEFT JOIN '.table_prefix.'users AS u' . "\n" |
1226 . ' ON ( u.user_id=k.user_id )' . "\n" |
1037 . ' ON ( u.user_id=k.user_id )' . "\n" |
1227 . ' LEFT JOIN '.table_prefix.'users_extra AS x' . "\n" |
1038 . ' LEFT JOIN '.table_prefix.'users_extra AS x' . "\n" |
1228 . ' ON ( u.user_id=x.user_id OR x.user_id IS NULL )' . "\n" |
1039 . ' ON ( u.user_id=x.user_id OR x.user_id IS NULL )' . "\n" |
1232 . ' AND k.salt=\''.$salt.'\'' . "\n" |
1043 . ' AND k.salt=\''.$salt.'\'' . "\n" |
1233 . ' 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,k.source_ip,k.time,k.auth_level,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;'); |
1044 . ' 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,k.source_ip,k.time,k.auth_level,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;'); |
1234 |
1045 |
1235 if ( !$query && ( defined('IN_ENANO_INSTALL') or defined('IN_ENANO_UPGRADE') ) ) |
1046 if ( !$query && ( defined('IN_ENANO_INSTALL') or defined('IN_ENANO_UPGRADE') ) ) |
1236 { |
1047 { |
1237 $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 |
1048 $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 |
1238 LEFT JOIN '.table_prefix.'users AS u |
1049 LEFT JOIN '.table_prefix.'users AS u |
1239 ON ( u.user_id=k.user_id ) |
1050 ON ( u.user_id=k.user_id ) |
1240 LEFT JOIN '.table_prefix.'privmsgs AS p |
1051 LEFT JOIN '.table_prefix.'privmsgs AS p |
1241 ON ( p.message_to=u.username AND p.message_read=0 ) |
1052 ON ( p.message_to=u.username AND p.message_read=0 ) |
1242 WHERE k.session_key=\''.$keyhash.'\' |
1053 WHERE k.session_key=\''.$keyhash.'\' |