diff --git a/login.php b/login.php index 438754e2..3301794e 100644 --- a/login.php +++ b/login.php @@ -5,6 +5,7 @@ use MyAAC\Models\PlayerOnline; use MyAAC\Models\Account; use MyAAC\Models\Player; use MyAAC\RateLimit; +use MyAAC\TwoFactorAuth\TwoFactorAuth; require_once 'common.php'; require_once SYSTEM . 'functions.php'; @@ -12,7 +13,7 @@ require_once SYSTEM . 'init.php'; require_once SYSTEM . 'status.php'; # error function -function sendError($message, $code = 3){ +function sendError($message, $code = 3) { $ret = []; $ret['errorCode'] = $code; $ret['errorMessage'] = $message; @@ -108,17 +109,18 @@ switch ($action) { case 'login': - $port = $config['lua']['gameProtocolPort']; + $ip = configLua('ip'); + $port = configLua('gameProtocolPort'); // default world info $world = [ 'id' => 0, 'name' => $config['lua']['serverName'], - 'externaladdress' => $config['lua']['ip'], + 'externaladdress' => $ip, 'externalport' => $port, - 'externaladdressprotected' => $config['lua']['ip'], + 'externaladdressprotected' => $ip, 'externalportprotected' => $port, - 'externaladdressunprotected' => $config['lua']['ip'], + 'externaladdressunprotected' => $ip, 'externalportunprotected' => $port, 'previewstate' => 0, 'location' => 'BRA', // BRA, EUR, USA @@ -133,13 +135,12 @@ switch ($action) { $inputEmail = $request->email ?? false; $inputAccountName = $request->accountname ?? false; - $inputToken = $request->token ?? false; $account = Account::query(); - if ($inputEmail != false) { // login by email + if ($inputEmail) { // login by email $account->where('email', $inputEmail); } - else if($inputAccountName != false) { // login by account name + else if($inputAccountName) { // login by account name $account->where('name', $inputAccountName); } @@ -151,13 +152,14 @@ switch ($action) { $limiter->load(); $ban_msg = 'A wrong account, password or secret has been entered ' . setting('core.account_login_attempts_limit') . ' times in a row. You are unable to log into your account for the next ' . setting('core.account_login_ban_time') . ' minutes. Please wait.'; + if (!$account) { $limiter->increment($ip); if ($limiter->exceeded($ip)) { sendError($ban_msg); } - sendError(($inputEmail != false ? 'Email' : 'Account name') . ' or password is not correct.'); + sendError(($inputEmail ? 'Email' : 'Account name') . ' or password is not correct.'); } $current_password = encrypt((USE_ACCOUNT_SALT ? $account->salt : '') . $request->password); @@ -167,32 +169,30 @@ switch ($action) { sendError($ban_msg); } - sendError(($inputEmail != false ? 'Email' : 'Account name') . ' or password is not correct.'); + sendError(($inputEmail ? 'Email' : 'Account name') . ' or password is not correct.'); } - $accountHasSecret = false; - if (fieldExist('secret', 'accounts')) { - $accountSecret = $account->secret; - if ($accountSecret != null && $accountSecret != '') { - $accountHasSecret = true; - if ($inputToken === false) { - $limiter->increment($ip); - if ($limiter->exceeded($ip)) { - sendError($ban_msg); - } - sendError('Submit a valid two-factor authentication token.', 6); - } else { - require_once LIBS . 'rfc6238.php'; - if (TokenAuth6238::verify($accountSecret, $inputToken) !== true) { - $limiter->increment($ip); - if ($limiter->exceeded($ip)) { - sendError($ban_msg); - } + $twoFactorAuth = TwoFactorAuth::getInstance($account->id); - sendError('Two-factor authentication failed, token is wrong.', 6); - } - } + $code = ''; + if ($twoFactorAuth->isActive()) { + if ($twoFactorAuth->getAuthType() === TwoFactorAuth::TYPE_EMAIL) { + $code = $request->emailcode ?? false; } + else if ($twoFactorAuth->getAuthType() === TwoFactorAuth::TYPE_APP) { + $code = $request->token ?? false; + } + } + + $error = ''; + $errorCode = 6; + if (!$twoFactorAuth->processClientLogin($code, $error, $errorCode)) { + $limiter->increment($ip); + if ($limiter->exceeded($ip)) { + sendError($ban_msg); + } + + sendError($error, $errorCode); } $limiter->reset($ip); @@ -220,46 +220,6 @@ switch ($action) { } } - /* - * not needed anymore? - if (fieldExist('premdays', 'accounts') && fieldExist('lastday', 'accounts')) { - $save = false; - $timeNow = time(); - $premDays = $account->premdays; - $lastDay = $account->lastday; - $lastLogin = $lastDay; - - if ($premDays != 0 && $premDays != PHP_INT_MAX) { - if ($lastDay == 0) { - $lastDay = $timeNow; - $save = true; - } else { - $days = (int)(($timeNow - $lastDay) / 86400); - if ($days > 0) { - if ($days >= $premDays) { - $premDays = 0; - $lastDay = 0; - } else { - $premDays -= $days; - $reminder = ($timeNow - $lastDay) % 86400; - $lastDay = $timeNow - $reminder; - } - - $save = true; - } - } - } else if ($lastDay != 0) { - $lastDay = 0; - $save = true; - } - if ($save) { - $account->premdays = $premDays; - $account->lastday = $lastDay; - $account->save(); - } - } - */ - $worlds = [$world]; $playdata = compact('worlds', 'characters'); @@ -268,7 +228,7 @@ switch ($action) { if (!fieldExist('istutorial', 'players')) { $sessionKey .= "\n"; } - $sessionKey .= ($accountHasSecret && strlen($accountSecret) > 5) ? $inputToken : ''; + $sessionKey .= ($twoFactorAuth->isActive() && strlen($account->{'2fa_secret'}) > 5) ? $account->{'2fa_secret'} : ''; // this is workaround to distinguish between TFS 1.x and otservbr // TFS 1.x requires the number in session key diff --git a/phpstan-bootstrap.php b/phpstan-bootstrap.php index 6de2ac2b..536cee8f 100644 --- a/phpstan-bootstrap.php +++ b/phpstan-bootstrap.php @@ -4,7 +4,6 @@ require __DIR__ . '/system/libs/pot/OTS.php'; $ots = POT::getInstance(); require __DIR__ . '/system/libs/pot/InvitesDriver.php'; -require __DIR__ . '/system/libs/rfc6238.php'; require __DIR__ . '/common.php'; const ACTION = ''; diff --git a/system/libs/rfc6238.php b/system/libs/rfc6238.php deleted file mode 100644 index 5effedd3..00000000 --- a/system/libs/rfc6238.php +++ /dev/null @@ -1,284 +0,0 @@ -'0', 'B'=>'1', 'C'=>'2', 'D'=>'3', 'E'=>'4', 'F'=>'5', 'G'=>'6', 'H'=>'7', - 'I'=>'8', 'J'=>'9', 'K'=>'10', 'L'=>'11', 'M'=>'12', 'N'=>'13', 'O'=>'14', 'P'=>'15', - 'Q'=>'16', 'R'=>'17', 'S'=>'18', 'T'=>'19', 'U'=>'20', 'V'=>'21', 'W'=>'22', 'X'=>'23', - 'Y'=>'24', 'Z'=>'25', '2'=>'26', '3'=>'27', '4'=>'28', '5'=>'29', '6'=>'30', '7'=>'31' - ); - - /** - * Use padding false when encoding for urls - * - * @return base32 encoded string - * @author Bryan Ruiz - **/ - public static function encode($input, $padding = true) { - if(empty($input)) return ""; - - $input = str_split($input); - $binaryString = ""; - - for($i = 0; $i < count($input); $i++) { - $binaryString .= str_pad(base_convert(ord($input[$i]), 10, 2), 8, '0', STR_PAD_LEFT); - } - - $fiveBitBinaryArray = str_split($binaryString, 5); - $base32 = ""; - $i=0; - - while($i < count($fiveBitBinaryArray)) { - $base32 .= self::$map[base_convert(str_pad($fiveBitBinaryArray[$i], 5,'0'), 2, 10)]; - $i++; - } - - if($padding && ($x = strlen($binaryString) % 40) != 0) { - if($x == 8) $base32 .= str_repeat(self::$map[32], 6); - else if($x == 16) $base32 .= str_repeat(self::$map[32], 4); - else if($x == 24) $base32 .= str_repeat(self::$map[32], 3); - else if($x == 32) $base32 .= self::$map[32]; - } - - return $base32; - } - - public static function decode($input) { - if(empty($input)) return; - - $paddingCharCount = substr_count($input, self::$map[32]); - $allowedValues = array(6,4,3,1,0); - - if(!in_array($paddingCharCount, $allowedValues)) return false; - - for($i=0; $i<4; $i++){ - if($paddingCharCount == $allowedValues[$i] && - substr($input, -($allowedValues[$i])) != str_repeat(self::$map[32], $allowedValues[$i])) return false; - } - - $input = str_replace('=','', $input); - $input = str_split($input); - $binaryString = ""; - - for($i=0; $i < count($input); $i = $i+8) { - $x = ""; - - if(!in_array($input[$i], self::$map)) return false; - - for($j=0; $j < 8; $j++) { - $x .= str_pad(base_convert(@self::$flippedMap[@$input[$i + $j]], 10, 2), 5, '0', STR_PAD_LEFT); - } - - $eightBits = str_split($x, 8); - - for($z = 0; $z < count($eightBits); $z++) { - $binaryString .= ( ($y = chr(base_convert($eightBits[$z], 2, 10))) || ord($y) == 48 ) ? $y:""; - } - } - - return $binaryString; - } -} - -// http://www.faqs.org/rfcs/rfc6238.html -// https://github.com/Voronenko/PHPOTP/blob/08cda9cb9c30b7242cf0b3a9100a6244a2874927/code/rfc6238.php -// Local changes: http -> https, consistent indentation, 200x200 -> 300x300 QR image size, PHP end tag -class TokenAuth6238 { - - /** - * verify - * - * @param string $secretkey Secret clue (base 32). - * @return bool True if success, false if failure - */ - public static function verify($secretkey, $code, $rangein30s = 3) { - $key = base32static::decode($secretkey); - $unixtimestamp = time()/30; - - for($i=-($rangein30s); $i<=$rangein30s; $i++) { - $checktime = (int)($unixtimestamp+$i); - $thiskey = self::oath_hotp($key, $checktime); - - if ((int)$code == self::oath_truncate($thiskey,6)) { - return true; - } - - } - return false; - } - - - public static function getTokenCode($secretkey,$rangein30s = 3) { - $result = ""; - $key = base32static::decode($secretkey); - $unixtimestamp = time()/30; - - for($i=-($rangein30s); $i<=$rangein30s; $i++) { - $checktime = (int)($unixtimestamp+$i); - $thiskey = self::oath_hotp($key, $checktime); - $result = $result." # ".self::oath_truncate($thiskey,6); - } - - return $result; - } - - public static function getTokenCodeDebug($secretkey,$rangein30s = 3) { - $result = ""; - print "
SecretKey: $secretkey
"; - - $key = base32static::decode($secretkey); - print "Key(base 32 decode): $key
"; - - $unixtimestamp = time()/30; - print "UnixTimeStamp (time()/30): $unixtimestamp
"; - - for($i=-($rangein30s); $i<=$rangein30s; $i++) { - $checktime = (int)($unixtimestamp+$i); - print "Calculating oath_hotp from (int)(unixtimestamp +- 30sec offset): $checktime basing on secret key
"; - - $thiskey = self::oath_hotp($key, $checktime, true); - print "======================================================
"; - print "CheckTime: $checktime oath_hotp:".$thiskey."
"; - - $result = $result." # ".self::oath_truncate($thiskey,6,true); - } - - return $result; - } - - public static function getBarCodeUrl($username, $domain, $secretkey, $issuer) { - $url = "https://chart.apis.google.com/chart"; - $url = $url."?chs=300x300&chld=M|0&cht=qr&chl=otpauth://totp/"; - $url = $url.$username . "@" . $domain . "%3Fsecret%3D" . $secretkey . '%26issuer%3D' . rawurlencode($issuer); - return $url; - } - - public static function generateRandomClue($length = 16) { - $b32 = "234567QWERTYUIOPASDFGHJKLZXCVBNM"; - $s = ""; - - for ($i = 0; $i < $length; $i++) - $s .= $b32[rand(0,31)]; - - return $s; - } - - private static function hotp_tobytestream($key) { - $result = array(); - $last = strlen($key); - for ($i = 0; $i < $last; $i = $i + 2) { - $x = $key[$i] + $key[$i + 1]; - $x = strtoupper($x); - $x = hexdec($x); - $result = $result.chr($x); - } - - return $result; - } - - private static function oath_hotp ($key, $counter, $debug=false) { - $result = ""; - $orgcounter = $counter; - $cur_counter = array(0,0,0,0,0,0,0,0); - - if ($debug) { - print "Packing counter $counter (".dechex($counter).")into binary string - pay attention to hex representation of key and binary representation
"; - } - - for($i=7;$i>=0;$i--) { // C for unsigned char, * for repeating to the end of the input data - $cur_counter[$i] = pack ('C*', $counter); - - if ($debug) { - print $cur_counter[$i]."(".dechex(ord($cur_counter[$i])).")"." from $counter
"; - } - - $counter = $counter >> 8; - } - - if ($debug) { - foreach ($cur_counter as $char) { - print ord($char) . " "; - } - - print "
"; - } - - $binary = implode($cur_counter); - - // Pad to 8 characters - str_pad($binary, 8, chr(0), STR_PAD_LEFT); - - if ($debug) { - print "Prior to HMAC calculation pad with zero on the left until 8 characters.
"; - print "Calculate sha1 HMAC(Hash-based Message Authentication Code http://en.wikipedia.org/wiki/HMAC).
"; - print "hash_hmac ('sha1', $binary, $key)
"; - } - - $result = hash_hmac ('sha1', $binary, $key); - - if ($debug) { - print "Result: $result
"; - } - - return $result; - } - - private static function oath_truncate($hash, $length = 6, $debug=false) { - $result=""; - - // Convert to dec - if($debug) { - print "converting hex hash into characters
"; - } - - $hashcharacters = str_split($hash,2); - - if($debug) { - print_r($hashcharacters); - print "
and convert to decimals:
"; - } - - for ($j=0; $j"; - print "offset:".$offset; - } - - $result = ( - (($hmac_result[$offset+0] & 0x7f) << 24 ) | - (($hmac_result[$offset+1] & 0xff) << 16 ) | - (($hmac_result[$offset+2] & 0xff) << 8 ) | - ($hmac_result[$offset+3] & 0xff) - ) % pow(10,$length); - - return $result; - } -} diff --git a/system/src/TwoFactorAuth/TwoFactorAuth.php b/system/src/TwoFactorAuth/TwoFactorAuth.php index 43da8c00..ce61e374 100644 --- a/system/src/TwoFactorAuth/TwoFactorAuth.php +++ b/system/src/TwoFactorAuth/TwoFactorAuth.php @@ -114,6 +114,41 @@ class TwoFactorAuth return false; } + public function processClientLogin($code, string &$error, &$errorCode): bool + { + if (!$this->isActive()) { + return true; + } + + if ($this->authType == self::TYPE_EMAIL) { + $errorCode = 8; + } + + if ($code === false) { + $error = 'Submit a valid two-factor authentication token.'; + + if ($this->authType == self::TYPE_EMAIL) { + if (!$this->hasRecentEmailCode(15 * 60)) { + $this->resendEmailCode(); + } + } + + return false; + } + + if (!$this->getAuthGateway()->verifyCode($code)) { + $error = 'Two-factor authentication failed, token is wrong.'; + + return false; + } + + if ($this->authType === self::TYPE_EMAIL) { + $this->deleteOldCodes(); + } + + return true; + } + public function setAuthGateway(int $authType): void { if ($authType === self::TYPE_EMAIL) {