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) {