<?php

  // Configuration items.
  // Change this salt! apg -m 64 is your friend.
  $salt = "salt";
  $db_host = "localhost";
  $db_user = "bna";
  $db_pass = "dbpass";
  $db_database = "bna";
  $table_prefix = "bna";
  $auth_days = 30;
  $debug = false;
  $site_password = "sitepass";

  if (file_exists("config.php"))
    require("config.php");

/*

CREATE TABLE `bna_users` (
  `userID` varchar(16) NOT NULL,
  `identifier` varchar(32) NOT NULL,
  `password` varchar(50) NOT NULL,
  `key` varchar(40) NOT NULL,
  `timeout` int(11) NOT NULL,
  PRIMARY KEY (`userID`)
);

CREATE TABLE `bna_tokens` (
  `alias` varchar(40) NOT NULL,
  `serial` varchar(40) NOT NULL,
  `secret` varchar(40) NOT NULL,
  `owner` varchar(16) NOT NULL,
  PRIMARY KEY (`serial`,`owner`)
);

*/

  // Battle.net's RSA public key - don't change it, unless you want to use a different authentication system that's working on RFC4226, and which happen to use Blizzard's protocol.
  $rsamod = "104890018807986556874007710914205443157030159668034197186125678960287470894290830530618284943118405110896322835449099433232093151168250152146023319326491587651685252774820340995950744075665455681760652136576493028733914892166700899109836291180881063097461175643998356321993663868233366705340758102567742483097";
  $rsakey = 257;
  $digits = 8;
  $timeOffset = 0;
  $enroll_host = "mobile-service.blizzard.com";
  $enroll_uri = "/enrollment/enroll2.htm";
  $restore_uri = "/enrollment/initiatePaperRestore.htm";
  $validate_uri = "/enrollment/validatePaperRestore.htm";
  $restoreChars = "0123456789ABCDEFGHJKMNPQRTUVWXYZ";

  // -------------------------------------------------------------------------------------

  $link = new mysqli($db_host, $db_user, $db_pass, $db_database);

  $logged_userID = NULL;

  function doDie($msg) {
    global $debug;

    if ($debug) {
      echo "<pre>";
      var_dump(debug_backtrace());
      echo "</pre>";
    }
    die($msg);
  }

  function doMysqlQuery($query) {
    global $link;
    $result = $link->query($query) or doDie("Error while running ".$query.": ".$link->error);
    return $result;
  }

  function safe_str($str) {
    return preg_match("/^[\w -]+$/", $str);
  }

  // -------------------------------------------------------------------------------------

  function getPSalt() {
    return substr(str_shuffle(str_repeat('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',10)),0,8);
  }

  function getNewPW($psalt, $password) {
    return sha1($psalt.sha1($password.$salt));
  }

  function createUser($userID, $password) {
    global $salt, $table_prefix, $link;

    $userID = trim($userID);
    $password = trim($password);
    if (!safe_str($userID)) doDie("Wrong login");
    $userID = $link->real_escape_string($userID);
    $psalt = getPSalt();
    $password = '$'.$psalt.'$'.getNewPW($psalt,$password);
    $id = md5($salt.md5($userID.$salt));
    doMysqlQuery("INSERT INTO ".$table_prefix."_users (userID,identifier,password) VALUES('".$userID."','".$id."','".$password."')");
  }

  function getUser($userID, $password) {
    global $table_prefix, $logged_userID, $link;

    $userID = trim($userID);
    $password = trim($password);
    if (!safe_str($userID)) doDie("Wrong login");
    $userID = $link->real_escape_string($userID);
    $result = doMysqlQuery("SELECT * FROM ".$table_prefix."_users WHERE userID='".$userID."'");
    if ($result->num_rows) {
      $ret = $result->fetch_assoc();
      if (substr($ret["password"],0,1)=='$') {
        $psalt = substr($ret["password"],1,8);
        if (substr($ret["password"],10)!=getNewPW($psalt, $password)) {
          return NULL;
        }
      } else {
        if ($ret["password"]==md5($password)) {
          $psalt = getPSalt();
          $password = '$'.$psalt.'$'.getNewPW($psalt,$password);
          doMysqlQuery("UPDATE ".$table_prefix."_users SET password='".$password."' WHERE userID='".$userID."'");
        } else {
          return NULL;
        }
      }
      $logged_userID = $ret["userID"];
      return $ret;
    }
    return NULL;
  }

  function isAuth() {
    global $salt, $table_prefix, $logged_userID, $link;

    if (!isset($_COOKIE["authentication"])) return false;
    $clean = array();
    $mysql = array();
    $now = time();

    list($identifier, $token) = explode(':', $_COOKIE["authentication"]);
    if (!safe_str($identifier)) return false;
    if (!safe_str($token)) return false;
    $clean["identifier"] = $identifier;
    $clean["key"] = $token;

    $mysql["identifier"] = $link->real_escape_string($clean["identifier"]);

    $result = doMysqlQuery("SELECT * FROM ".$table_prefix."_users WHERE identifier='".$mysql["identifier"]."'");
    if (!$result->num_rows) return false;
    $record = $result->fetch_assoc();
    if ($record["userID"]=="demo") {
      $logged_userID = $record["userID"];
      return true;
    }
    if ($clean["key"]!=$record["key"]) {
      return false;
    } elseif ($now>$record["timeout"]){
      return false;
    } elseif ($clean["identifier"]!=md5($salt.md5($record["userID"].$salt))) {
      return false;
    } else {
      $logged_userID = $record["userID"];
      return true;
    }
  }

  function doLogin($userID, $password) {
    global $table_prefix, $auth_days, $link;

    $userID = trim($userID);
    $password = trim($password);
    $user = getUser($userID, $password);
    if (is_null($user)) goLogin();
    $userID = $link->real_escape_string($userID);
    $identifier = $user["identifier"];
    $ckey = sha1(uniqid(rand(), true));
    $timeout = time()+60*60*24*$auth_days;
    $result = doMysqlQuery("UPDATE ".$table_prefix."_users SET `key`='".$ckey."',`timeout`='".$timeout."' WHERE userID='".$userID."'");
    setrawcookie('authentication', $identifier.":".$ckey, $timeout);
    $_COOKIE["authentication"] = $identifier.":".$ckey;
  }

  function doLogout() {
    global $table_prefix, $logged_userID, $link;

    setrawcookie('authentication', "", time()-3600);
    $logged_userID = trim($logged_userID);
    if (safe_str($logged_userID)) {
        $logged_userID = $link->real_escape_string($logged_userID);
        $result = doMysqlQuery("UPDATE ".$table_prefix."_users SET `key`=NULL,`timeout`='0' WHERE userID='".$logged_userID."'");
    }
    $logged_userID = NULL;
  }

  function goLogin() {
    header("Location: ?p=l");
    exit;
  }

  function showLogin() {
?>
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link href="bna.css" rel="stylesheet" type="text/css"/>
    <title>Login</title>
  </head>
  <body>
    <div id="loginframe" class="frame"><form action="?p=v" method="POST">
      <div class="title">User Login</div>
      <div class="invite">Login:</div>
      <div class="input"><input type="text" name="userID" /></div>
      <div class="invite">Password:</div>
      <div class="input"><input type="password" name="password" /></div>
      <div class="button"><input type="submit" value="Login" /></div>
    </form></div>
    <div id="newuser"><a href="?p=n">New user</a></div>
    <div id="seesrc"><a href="?p=s">See source</a></div>
    <div id="disclaimer"><a href="?p=d">Disclaimer</a></div>
  </body>
</html>
<?php
  }

  function showNewUser() {
?>
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link href="bna.css" rel="stylesheet" type="text/css"/>
    <title>New User</title>
  </head>
  <body>
    <div id="newuserframe" class="frame"><form action="?p=c" method="POST">
      <div class="title">New User</div>
      <div class="invite">Login:</div>
      <div class="input"><input type="text" name="userID" /></div>
      <div class="invite">Password:</div>
      <div class="input"><input type="password" name="password" /></div>
      <div class="invite">Site Password:</div>
      <div class="input"><input type="password" name="site_password" /></div>
      <div class="button"><input type="submit" value="Login" /></div>
    </form></div>
    <div id="seesrc"><a href="?p=s">See source</a></div>
    <div id="disclaimer"><a href="?p=d">Disclaimer</a></div>
  </body>
</html>
<?php
  }

  function showDisclaimer() {
    echo "<pre>";
    readfile("disclaimer.txt");
    echo "</pre>";
  }

  // -------------------------------------------------------------------------------------

  function createToken($serial, $secret) {
    global $table_prefix, $logged_userID, $link;

    $serial = trim($serial);
    $secret = trim($secret);
    $owner = trim($logged_userID);
    if (!safe_str($serial)) doDie("Wrong serial");
    if (!safe_str($secret)) doDie("Wrong secret");
    if (!safe_str($owner)) doDie("Wrong owner");
    $serial = $link->real_escape_string($serial);
    $secret = $link->real_escape_string($secret);
    $owner = $link->real_escape_string($owner);
    doMysqlQuery("INSERT INTO ".$table_prefix."_tokens (serial,secret,owner,alias) VALUES('".$serial."','".$secret."','".$owner."','".$serial."')");
  }

  function getToken($id) {
    global $table_prefix, $logged_userID, $link;

    $id = trim($id);
    $owner = trim($logged_userID);
    if (!isAuth()) goLogin();
    if (!isset($id)) doDie("No token id");
    if (!safe_str($id)) doDie("Wrong id");
    if (!safe_str($owner)) doDie("Wrong owner");
    $id = $link->real_escape_string($id);
    $owner = $link->real_escape_string($owner);
    $result = doMysqlQuery("SELECT * FROM ".$table_prefix."_tokens WHERE serial='".$id."' AND owner='".$owner."'");
    if (!$result->num_rows) doDie("No such token");
    return $result->fetch_assoc();
  }

  function renameToken($serial, $alias) {
    global $table_prefix, $logged_userID, $link;

    $serial = trim($serial);
    $alias = trim($alias);
    $owner = trim($logged_userID);
    if (!safe_str($serial)) doDie("Wrong serial");
    if (!safe_str($alias)) doDie("Wrong alias");
    if (!safe_str($owner)) doDie("Wrong owner");
    $serial = $link->real_escape_string($serial);
    $alias = $link->real_escape_string($alias);
    $owner = $link->real_escape_string($owner);
    doMysqlQuery("UPDATE ".$table_prefix."_tokens SET alias='".$alias."' WHERE serial='".$serial."' AND owner='".$owner."'");
  }

  // -------------------------------------------------------------------------------------

  function getEmptyEncryptMsg($otp, $region, $phone_model) {
    $r = substr(str_pad($otp, 20, "\0"), 0, 20);
    $r .= substr(str_pad($region, 2 , "\0"), 0, 2);
    $r .= substr(str_pad($phone_model, 16 , "\0"), 0, 16);
    return($r);
  }

  function BIN2BC($d) {
    $r = 0;
    for($i=0; $i<strlen($d); $i++){
      $r = bcadd(bcmul($r, 256), ord($d[$i]));
    }
    return($r);
  }

  function BC2BIN($n) {
    $r = "";
    while($n>0) {
      $r = chr(bcmod($n, 256)).$r;
      $n = bcdiv($n, 256);
    }
    return($r);
  }

  function computePassword($secret, $digits, $timeOffset) {
    $t = time()-$timeOffset;
    $r = hash_hmac('sha1', pack('xxxxN', $t/30), pack('H*', $secret), true);
    $h = array_shift(unpack('N', substr($r, ord($r[19])&0x0F, 4)))&0x7FFFFFFF;
    return array("password" => $h%pow(10, $digits), "elapsed" => $t%30);
  }

  function computeRestore($secret, $serial) {
    global $restoreChars;
    $s = unpack('C*', substr(sha1(str_replace('-', '', $serial).pack('H*', $secret), true), -10));
    $r = "";
    foreach ($s as $k)
      $r .= $restoreChars[$k&31];
    return $r;
  }

  function getNewToken($region, $phone_model) {
    global $rsamod, $rsakey, $enroll_host, $enroll_uri;

    $otp = sha1(sha1(microtime(), true).sha1(microtime(), true), true);
    if ($region=="") $region = "US";
    if ($phone_model="") $phone_model = "Motorola RAZR v3";

    $msg = getEmptyEncryptMsg($otp, $region, $phone_model);

    $e = BC2BIN(bcpowmod(BIN2BC($msg), $rsakey, $rsamod));

    $sk = fsockopen($enroll_host, 80);
    fputs($sk, "POST ".$enroll_uri." HTTP/1.1\r\n");
    fputs($sk, "Host: ".$enroll_host."\r\n");
    fputs($sk, "Content-type: application/octet-stream\r\n");
    fputs($sk, "Content-length: ".strlen($e)."\r\n");
    fputs($sk, "Connection: close\r\n\r\n");
    fputs($sk, $e);
    $rep = "";
    while(!feof($sk)) $rep .= fgets($sk, 512);
    fclose($sk);
    list($reph, $repb) = explode("\r\n\r\n", $rep, 2); 

    if(strpos($reph, "HTTP/1.1 200 OK")===false) doDie("Server error <hr><pre>".$reph."</pre>");

    for($i=0; $i<20; $i++) $repb[8+17+$i] = chr(ord($repb[8+17+$i])^ord($otp[$i]));

    $secret = array_shift(unpack('H*', substr($repb, 8+17, 20)));
    $serial = substr($repb, 8, 17);

    $rregion = substr($serial, 0, 2);
    if (($rregion!="US")&&($rregion!="EU")) doDie("Wrong serial answer; ".$serial.":".$secret);

    return array("serial" => $serial, "secret" => $secret);
  }

  function restoreToken($serial, $restore) {
    global $restoreChars, $rsakey, $rsamod, $enroll_uri, $enroll_host, $restore_uri, $validate_uri;
    $serial = str_replace('-', '', $serial);
    $sk = fsockopen($enroll_host, 80);
    fputs($sk, "POST ".$restore_uri." HTTP/1.1\r\n");
    fputs($sk, "Host: ".$enroll_host."\r\n");
    fputs($sk, "Content-type: application/octet-stream\r\n");
    fputs($sk, "Content-length: 14\r\n");
    fputs($sk, "Connection: close\r\n\r\n");
    fputs($sk, $serial);
    $rep = "";
    while(!feof($sk)) $rep .= fgets($sk, 512);
    fclose($sk);
    list($reph, $repb) = explode("\r\n\r\n", $rep, 2);

    if(strpos($reph, "HTTP/1.1 200 OK")===false) doDie("Server error <hr><pre>".$reph."</pre>.$repb");

    $rest = "";
    for($i=0; $i<10; $i++) $rest .= chr(strpos($restoreChars, $restore[$i]));
    $otp = sha1(sha1(microtime(), true).sha1(microtime(), true), true);
    $msg = hash_hmac('sha1', $serial.$repb, $rest, true).$otp;
    $e = BC2BIN(bcpowmod(BIN2BC($msg), $rsakey, $rsamod));

    $sk = fsockopen($enroll_host, 80);
    fputs($sk, "POST ".$validate_uri." HTTP/1.1\r\n");
    fputs($sk, "Host: ".$enroll_host."\r\n");
    fputs($sk, "Content-type: application/octet-stream\r\n");
    fputs($sk, "Content-length: ".strlen($e)."\r\n");
    fputs($sk, "Connection: close\r\n\r\n");
    fputs($sk, $e);
    $rep = "";
    while(!feof($sk)) $rep .= fgets($sk, 512);
    fclose($sk);
    list($reph, $repb) = explode("\r\n\r\n", $rep, 2); 

    if(strpos($reph, "HTTP/1.1 200 OK")===false) doDie("Failed challenge <hr><pre>".$reph."</pre>".$repb);

    for($i=0; $i<20; $i++) $repb[$i] = chr(ord($repb[$i])^ord($otp[$i]));

    return array_shift(unpack('H*', $repb));
  }

  // -------------------------------------------------------------------------------------

  function showTokens() {
    global $table_prefix, $logged_userID, $link;

    $owner = $logged_userID;
    if (!safe_str($owner)) doDie("Wrong userID");
    $owner = $link->real_escape_string($owner);
    $result = doMysqlQuery("SELECT * FROM ".$table_prefix."_tokens WHERE owner='".$owner."'");
?>
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link href="bna.css" rel="stylesheet" type="text/css"/>
    <title>Tokens</title>
  </head>
  <body>
    <div class="logout"><a href="?p=o">Log out <?php echo $logged_userID?></a></div>
    <div id="tokensframe" class="frame">
      <div class="title">Your tokens</div>
<?php
while (($token = $result->fetch_assoc())) {
     if ($token["alias"]=="delete") continue;
?>
      <form action="?p=r" method="POST"><input type="text" name="alias" value="<?php echo $token["alias"]?>" /><div class="input"><input type="hidden" name="serial" value="<?php echo $token["serial"]?>" /></div></form>
      <div class="serial"><a href="?p=p&i=<?php echo $token["serial"]?> "><?php echo $token["serial"]?></a></div>
      <div class="restore"><?php echo computeRestore($token["secret"], $token["serial"])?></div>
<?php
}
?>
    </div>
    <div id="enroll_US" class="enroll"><a href="?p=e&r=US">Request a new token in US</a></div>
    <div id="enroll_EU" class="enroll"><a href="?p=e&r=EU">Request a new token in EU</a></div>
    <div id="insertframe" class="frame"><form action="?p=i" method="POST">
      <div class="title">Insert a token</div>
      <div class="invite">Serial:</div>
      <div class="input"><input type="text" name="serial"></input></div>
      <div class="invite">Secret:</div>
      <div class="input"><input type="text" name="secret"></input></div>
      <div class="button"><input type="submit" value="Insert" /></div>
    </form></div>
    <div id="restoreframe" class="frame"><form action="?p=b" method="POST">
      <div class="title">Restore a token</div>
      <div class="invite">Serial:</div>
      <div class="input"><input type="text" name="serial"></input></div>
      <div class="invite">Code:</div>
      <div class="input"><input type="text" name="code"></input></div>
      <div class="button"><input type="submit" value="Restore" /></div>
    </form></div>
    <div id="seesrc"><a href="?p=s">See source</a></div>
  </body>
</html>
<?php
  }

  function showPassword($id, $ajax) {
    global $digits, $timeOffset, $logged_userID;

    $token = getToken($id);
    $secret = $token["secret"];
    $serial = $token["serial"];
    $r = computePassword($secret, $digits, $timeOffset);
    if ($ajax) {
      echo $r["password"].':'.$r["elapsed"]*1000;
      return;
    }
?>
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link href="bna.css" rel="stylesheet" type="text/css"/>
    <title>Password</title>
    <script type="text/javascript">
// <![CDATA[
var password=<?php echo $r["password"]?>;
var elapsed=<?php echo $r["elapsed"]*1000?>;
var actualElapsed=elapsed;
var serialId='<?php echo $id?>';
var d=new Date;
var origin=d.getTime();
var refreshing=false;
var xmlHttpReq=false;
function ajaxRefresh() {
  if (refreshing) return;
  refreshing=true;
  // Mozilla/Safari
  if (window.XMLHttpRequest) {
    xmlHttpReq=new XMLHttpRequest();
  }
  // IE
  else if (window.ActiveXObject) {
    xmlHttpReq=new ActiveXObject("Microsoft.XMLHTTP");
  }
  xmlHttpReq.open('GET', 'bna.php?p=p&i='+serialId+'&t=a', true);
  xmlHttpReq.onreadystatechange=function() {
    if (xmlHttpReq.readyState==4) {
      var r=xmlHttpReq.responseText.split(":");
      password=parseInt(r[0]);
      elapsed=parseInt(r[1]);
      d=new Date;
      origin=d.getTime();
      document.getElementById("password").innerHTML=password;
      refreshing=false;
    }
  }
  xmlHttpReq.send();
}
function showProgress() {
  setTimeout("showProgress()", 50);
  d=new Date;
  var current=d.getTime();
  actualElapsed=elapsed+current-origin;
  var p=Math.min(actualElapsed/300,100);
  document.getElementById("elapsed").style.width=p+"%";
  if (actualElapsed>29500) ajaxRefresh();
}
// ]]>
    </script>
  </head>
  <body onload="showProgress();">
    <div class="logout"><a href="?p=o">Log out <?php echo $logged_userID?></a></div>
    <div class="link"><a href="?p=t">Back to tokens</a></div>
    <div id="passwordframe" class="frame">
      <div class="title">Authenticator</div>
      <div id="password"><?php echo $r["password"]?></div>
    </div>
    <div class="progressbar"><div id="elapsed" class="progressbar-completed" style="width:0;"><div>&nbsp;</div></div></div>
  </body>
</html>
<?php
  }

  // -------------------------------------------------------------------------------------

  switch($_GET["p"]) {
  case "s":
    highlight_file(__FILE__);
    break;
  case "p":
    if (!isAuth()) goLogin();
    $id = $_GET["i"];
    showPassword($id, $_GET["t"]=="a");
    break;
  case "c":
    if ($_POST["site_password"] != $site_password) doDie("Wrong site password.");
    createUser($_POST["userID"], $_POST["password"]);
  case "v":
    doLogin($_POST["userID"], $_POST["password"]);
  case "t":
    if (!isAuth()) goLogin();
    showTokens();
    break;
  case "e":
    if (!isAuth()) goLogin();
    $token = getNewToken($_POST["r"],"");
    createToken($token["serial"], $token["secret"]);
    showTokens();
    break;
  case "i":
    if (!isAuth()) goLogin();
    createToken($_POST["serial"], $_POST["secret"]);
    showTokens();
    break;
  case "b":
    if (!isAuth()) goLogin();
    createToken($_POST["serial"], restoreToken($_POST["serial"], $_POST["code"]));
    showTokens();
    break;
  case "r":
    if (!isAuth()) goLogin();
    renameToken($_POST["serial"], $_POST["alias"]);
    showTokens();
    break;
  case "n":
    showNewUser();
    break;
  case "o":
    doLogout();
  case "l":
    showLogin();
    break;
  case "d":
    showDisclaimer();
    break;
  default:
    if (!isAuth()) {
      showLogin();
    } else {
      showTokens();
    }
  }
?>