diff --git a/config.php b/config.php index df3d71e..395e087 100644 --- a/config.php +++ b/config.php @@ -3,6 +3,7 @@ $isWindows = (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN'); define('ZNOTE_OS', ($isWindows) ? 'WINDOWS' : 'LINUX'); } + // Available options: TFS_02, TFS_03 // TFS 0.2 = TFS_02 // TFS 0.3 = TFS_03 (If ur using 0.3.6, set $config['salt'] to false)! @@ -19,7 +20,6 @@ // Path to server folder without / Example: C:\Users\Alvaro\Documents\GitHub\forgottenserver $config['server_path'] = ''; - // ------------------------ \\ // MYSQL CONNECTION DETAILS \\ // ------------------------ \\ @@ -36,6 +36,10 @@ // Hostname is usually localhost or 127.0.0.1. $config['sqlHost'] = '127.0.0.1'; + // QR code authenticator Only works with TFS 1.2+ + $config['twoFactorAuthenticator'] = true; + // You can use the mobile phone app "authy" with this. + /* CLOCK FUNCTION - getClock() = returns current time in numbers. - getClock(time(), true) = returns current time in formatted date @@ -57,8 +61,8 @@ // CUSTOM SERVER STUFF \\ // ------------------- \\ // Enable / disable Questlog function (true / false) - $config['EnableQuests'] = false; - + $config['EnableQuests'] = false; + // array for filling questlog (Questid, max value, name, end of the quest fill 1 for the last part 0 for all others) $config['quests'] = array( array(1501,100,"Killing in the Name of",0), @@ -324,8 +328,26 @@ // Town ids and names: (In RME map editor, open map, click CTRL + T to view towns, their names and their IDs. // townID => 'townName' etc: ['3'=>'Thais'] $config['towns'] = array( - 2 => 'Thyrfing', - 3 => 'Town 3', + 1 => 'Venore', + 2 => 'Thais', + 3 => 'Kazordoon', + 4 => 'Carlin', + 5 => "Ab'Dendriel", + 6 => 'Rookgaard', + 7 => 'Liberty Bay', + 8 => 'Port Hope', + 9 => 'Ankrahmun', + 10 => 'Darashia', + 11 => 'Edron', + 12 => 'Svargrond', + 13 => 'Yalahar', + 14 => 'Farmine', + 28 => 'Gray Beach', + 29 => 'Roshamuul', + 30 => 'Rookgaard Tutorial Island', + 31 => 'Isle of Solitude', + 32 => 'Island Of Destiny', + 33 => 'Rathleton' ); // - TFS 1.0 ONLY -- HOUSE AUCTION SYSTEM! @@ -372,7 +394,7 @@ $config['available_vocations'] = array(1, 2, 3, 4); // Available towns (specify town ids, etc: (0, 1, 2); to display 3 town options (town id 0, 1 and 2). - $config['available_towns'] = array(2); + $config['available_towns'] = array(1,2,4,5); $config['level'] = 8; $config['health'] = 185; @@ -436,8 +458,8 @@ $config['delete_character_interval'] = '3 DAY'; // Delay after user character delete request is executed eg. 1 DAY, 2 HOUR, 3 MONTH etc. - $config['validate_IP'] = true; // Only allow legal IP addresses to register and create character. - $config['salt'] = false; // Some noob 0.3.6 servers don't support salt. + $config['validate_IP'] = true; + $config['salt'] = false; // Restricted names $config['invalidNameTags'] = array("owner", "gamemaster", "hoster", "admin", "staff", "tibia", "account", "god", "anal", "ass", "fuck", "sex", "hitler", "pussy", "dick", "rape", "cm", "gm", "amazon", "valkyrie", "carrion worm", "rotworm", "rotworm queen", "cockroach", "kongra", "merlkin", "sibang", "crystal spider", "giant spider", "poison spider", "scorpion", "spider", "tarantula", "achad", "axeitus headbanger", "bloodpaw", "bovinus", "colerian the barbarian", "cursed gladiator", "frostfur", "orcus the cruel", "rocky", "the hairy one", "avalanche", "drasilla", "grimgor guteater", "kreebosh the exile", "slim", "spirit of earth", "spirit of fire", "spirit of water", "the dark dancer", "the hag", "darakan the executioner", "deathbringer", "fallen mooh'tah master ghar", "gnorre chyllson", "norgle glacierbeard", "svoren the mad", "the masked marauder", "the obliverator", "the pit lord", "webster", "barbarian bloodwalker", "barbarian brutetamer", "barbarian headsplitter", "barbarian skullhunter", "bear", "panda", "polar bear", "braindeath", "beholder", "elder beholder", "gazer", "chicken", "dire penguin", "flamingo", "parrot", "penguin", "seagull", "terror bird", "bazir", "infernatil", "thul", "munster", "son of verminor", "xenia", "zoralurk", "big boss trolliver", "foreman kneebiter", "mad technomancer", "man in the cave", "lord of the elements", "the count", "the plasmother", "dracola", "the abomination", "the handmaiden", "mr. punish", "the countess sorrow", "the imperor", "massacre", "apocalypse", "brutus bloodbeard", "deadeye devious", "demodras", "dharalion", "fernfang", "ferumbras", "general murius", "ghazbaran", "grorlam", "lethal lissy", "morgaroth", "necropharus", "orshabaal", "ron the ripper", "the evil eye", "the horned fox", "the old widow", "tiquandas revenge", "apprentice sheng", "dog", "hellhound", "war wolf", "winter wolf", "wolf", "chakoya toolshaper", "chakoya tribewarden", "chakoya windcaller", "blood crab", "crab", "frost giant", "frost giantess", "ice golem", "yeti", "acolyte of the cult", "adept of the cult", "enlightened of the cult", "novice of the cult", "ungreez", "dark torturer", "demon", "destroyer", "diabolic imp", "fire devil", "fury", "hand of cursed fate", "juggernaut", "nightmare", "plaguesmith", "blue djinn", "efreet", "admin", "green djinn", "marid", "frost dragon", "wyrm", "sea serpent", "dragon lord", "dragon", "hydra", "dragon hatchling", "dragon lord hatchling", "frost dragon hatchling", "dwarf geomancer", "dwarf guard", "dwarf soldier", "dwarf", "dworc fleshhunter", "dworc venomsniper", "dworc voodoomaster", "elephant", "mammoth", "elf arcanist", "elf scout", "elf", "charged energy elemental", "energy elemental", "massive energy elemental", "overcharged energy elemental", "energy overlord", "cat", "lion", "tiger", "azure frog", "coral frog", "crimson frog", "green frog", "orchid frog", "toad", "jagged earth elemental", "muddy earth elemental", "earth elemental", "massive earth elemental", "earth overlord", "gargoyle", "stone golem", "ghost", "phantasm", "phantasm", "pirate ghost", "spectre", "cyclops smith", "cyclops drone", "behemoth", "cyclops", "slick water elemental", "roaring water elemental", "ice overlord", "water elemental", "massive water elemental", "ancient scarab", "butterfly", "bug", "centipede", "exp bug", "larva", "scarab", "wasp", "lizard sentinel", "lizard snakecharmer", "lizard templar", "minotaur archer", "minotaur guard", "minotaur mage", "minotaur", "squirrel", "goblin demon", "badger", "bat", "deer", "the halloween hare", "hyaena", "pig", "rabbit", "silver rabbit", "skunk", "wisp", "dark monk", "monk", "tha exp carrier", "necromancer", "priestess", "orc berserker", "orc leader", "orc rider", "orc shaman", "orc spearman", "orc warlord", "orc warrior", "orc", "goblin leader", "goblin scavenger", "goblin", "goblin assassin", "assasin", "bandit", "black knight", "hero", "hunter", "nomad", "smuggler", "stalker", "poacher", "wild warrior", "ashmunrah", "dipthrah", "mahrdis", "morguthis", "omruc", "rahemos", "thalas", "vashresamun", "pirate buccaneer", "pirate corsair", "pirate cutthroat", "pirate marauder", "carniphila", "spit nettle", "fire overlord", "massive fire elemental", "blistering fire elemental", "blazing fire elemental", "fire elemental", "hellfire fighter", "quara constrictor scout", "quara hydromancer scout", "quara mantassin scout", "quara pincher scout", "quara predator scout", "quara constrictor", "quara hydromancer", "quara mantassin", "quara pincher", "quara predator", "cave rat", "rat", "cobra", "crocodile", "serpent spawn", "snake", "wyvern", "black sheep", "sheep", "mimic", "betrayed wraith", "bonebeast", "demon skeleton", "lost soul", "pirate skeleton", "skeleton", "skeleton warrior", "undead dragon", "defiler", "slime2", "slime", "bog raider", "ice witch", "warlock", "witch", "bones", "fluffy", "grynch clan goblin", "hacker", "minishabaal", "primitive", "tibia bug", "undead minion", "annihilon", "hellgorak", "latrivan", "madareth", "zugurosh", "ushuriel", "golgordan", "thornback tortoise", "tortoise", "eye of the seven", "deathslicer", "flamethrower", "magicthrower", "plaguethrower", "poisonthrower", "shredderthrower", "troll champion", "frost troll", "island troll", "swamp troll", "troll", "banshee", "blightwalker", "crypt shambler", "ghoul", "lich", "mummy", "vampire", "grim reaper", "frost dragon", "mercenary", "zathroth", "goshnar", "durin", "demora", "orc champion", "dracula", "alezzo", "prince almirith", "elf warlord", "magebomb", "nightmare scion"); @@ -480,9 +502,8 @@ // WARNING! Account names written here will have admin access to web page! $config['page_admin_access'] = array( - //'otland0', - //'otland1', - 'testing' + 'accountname', + 'secondaccountname', ); // Built-in FORUM diff --git a/engine/database/connect.php b/engine/database/connect.php index d6b997a..2aee147 100644 --- a/engine/database/connect.php +++ b/engine/database/connect.php @@ -45,6 +45,7 @@ CREATE TABLE IF NOT EXISTS `znote_accounts` ( `active` tinyint(4) NOT NULL DEFAULT '0', `activekey` int(11) NOT NULL DEFAULT '0', `flag` varchar(20) NOT NULL, + `secret` char(16) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB; diff --git a/engine/function/general.php b/engine/function/general.php index 0932f51..f342032 100644 --- a/engine/function/general.php +++ b/engine/function/general.php @@ -532,4 +532,14 @@ function logo_exists($guild) { } } +function generateRandomString($length = 16) { + $characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; + $charactersLength = strlen($characters); + $randomString = ''; + for ($i = 0; $i < $length; $i++) { + $randomString .= $characters[rand(0, $charactersLength - 1)]; + } + return $randomString; +} + ?> \ No newline at end of file diff --git a/engine/function/rfc6238.php b/engine/function/rfc6238.php new file mode 100644 index 0000000..858d679 --- /dev/null +++ b/engine/function/rfc6238.php @@ -0,0 +1,285 @@ +'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; + } +} +?> \ No newline at end of file diff --git a/layout/widgets/login.php b/layout/widgets/login.php index 561412e..eb1869c 100644 --- a/layout/widgets/login.php +++ b/layout/widgets/login.php @@ -11,6 +11,10 @@ Password:
+
  • + Token:
    + +
  • diff --git a/login.php b/login.php index cbcafea..0141e06 100644 --- a/login.php +++ b/login.php @@ -4,12 +4,14 @@ logged_in_redirect(); include 'layout/overall/header.php'; if (empty($_POST) === false) { + if ($config['log_ip']) { znote_visitor_insert_detailed_data(5); } + $username = $_POST['username']; $password = $_POST['password']; - //data_dump($_POST, false, "POST"); + if (empty($username) || empty($password)) { $errors[] = 'You need to enter a username and password.'; } else if (strlen($username) > 32 || strlen($password) > 64) { @@ -42,20 +44,62 @@ if (empty($_POST) === false) { } else $status = true; if ($status) { - setSession('user_id', $login); - - // if IP is not set (etc acc created before Znote AAC was in use) - $znote_data = user_znote_account_data($login); - if ($znote_data['ip'] == 0) { - $update_data = array( - 'ip' => getIPLong(), - ); - user_update_znote_account($update_data); - } + // Regular login success, now lets check authentication token code + if ($config['TFSVersion'] == 'TFS_10' && $config['twoFactorAuthenticator']) { + require_once("engine/function/rfc6238.php"); + + // Two factor authentication code / token + $authcode = (isset($_POST['authcode'])) ? getValue($_POST['authcode']) : false; + + // Load secret values from db + $query = mysql_select_single("SELECT `a`.`secret` AS `secret`, `za`.`secret` AS `znote_secret` FROM `accounts` AS `a` INNER JOIN `znote_accounts` AS `za` ON `a`.`id` = `za`.`account_id` WHERE `a`.`id`='".(int)$login."' LIMIT 1;"); + + // If account table HAS a secret, we need to validate it + if ($query['secret'] !== NULL) { + + // Validate the secret first to make sure all is good. + if (TokenAuth6238::verify($query['znote_secret'], $authcode) !== true) { + $errors[] = "Submitted Two-Factor Authentication token is wrong."; + $errors[] = "Make sure to type the correct token from your mobile authenticator."; + $status = false; + } + + } else { + + // secret from accounts table is null/not set. Perhaps we can activate it: + if ($query['znote_secret'] !== NULL && $authcode !== false && !empty($authcode)) { + + // Validate the secret first to make sure all is good. + if (TokenAuth6238::verify($query['znote_secret'], $authcode)) { + // Success, enable the 2FA system + mysql_update("UPDATE `accounts` SET `secret`= '$authcode' WHERE `id`='$login';"); + } else { + $errors[] = "Activating Two-Factor authentication failed."; + $errors[] = "Try to login without token and configure your app properly."; + $errors[] = "Submitted Two-Factor Authentication token is wrong."; + $errors[] = "Make sure to type the correct token from your mobile authenticator."; + $status = false; + } + } + } + } // End tfs 1.0+ with 2FA auth - // Send them to myaccount.php - header('Location: myaccount.php'); - exit(); + if ($status) { + setSession('user_id', $login); + + // if IP is not set (etc acc created before Znote AAC was in use) + $znote_data = user_znote_account_data($login); + if ($znote_data['ip'] == 0) { + $update_data = array( + 'ip' => getIPLong(), + ); + user_update_znote_account($update_data); + } + + // Send them to myaccount.php + header('Location: myaccount.php'); + exit(); + } } } } @@ -64,10 +108,10 @@ if (empty($_POST) === false) { } if (empty($errors) === false) { -?> + ?>

    We tried to log you in, but...

    - \ No newline at end of file + +include 'layout/overall/footer.php'; ?> \ No newline at end of file diff --git a/myaccount.php b/myaccount.php index eba81f2..4e7e1a4 100644 --- a/myaccount.php +++ b/myaccount.php @@ -241,6 +241,14 @@ if ($render_page) {

    My account

    Welcome to your account page,
    You have days remaining premium account.

    +

    Account security with Two-factor Authentication:

    Character List: characters.

    +

    Server compatibility error

    +

    Sorry, this server is not compatible with Two-Factor Authentication.
    + TFS 1.2 or higher is required to run two-factor authentication, grab it + here.

    + +

    Two-Factor Authentication

    +

    Account security with Two-factor Authentication: .

    + + +

    Login with a token generated from this QR code to activate:

    + + + " + alt="Two-Factor Authentication QR code image for this account." + /> + +

    How to use:

    +
      +
    1. Download an authenticator app for free on your mobile phone like Authy (Android), (iPhone) or Google Authenticator (Android), (iPhone).
    2. +
    3. Scan the QR image with the app on your phone to create a Two-Factor account for this server.
    4. +
    5. Logout, then login with username, password and token generated from your phone to enable Two-Factor Authentication.
    6. +
    + \ No newline at end of file