From 9b2752526f9f4bfac4831eb61fa63edd60117f77 Mon Sep 17 00:00:00 2001 From: paulr Date: Mon, 15 Nov 2010 14:07:44 +1100 Subject: [PATCH] An initially working library. --- doco/readme.txt | 11 ++- lib/lib.php | 223 ++++++++++++++++++++++++++++++++++++++++++++-- unittests/authtest.php | 43 +++++++++ unittests/createurl.php | 5 +- unittests/createuser.php | 17 ++++ 5 files changed, 288 insertions(+), 11 deletions(-) create mode 100644 unittests/authtest.php create mode 100644 unittests/createuser.php diff --git a/doco/readme.txt b/doco/readme.txt index 1ab3a1e..75bd4ac 100644 --- a/doco/readme.txt +++ b/doco/readme.txt @@ -42,4 +42,13 @@ hex, this I do by converting to binary first... Lets how thats going to work in the long run. The sad thing is is that if I were writing it in C/C++/etc, bit math is easy and while im sure its not too different in PHP, I've never done it, so this seeemed -easier. \ No newline at end of file +easier. + + +Acknowledgements +================ + +Google, for producing google authenticator +The guys at mOTP who got me all excited about token authenticators +The guys on this page who spell out how to do hotp + http://php.net/manual/en/function.hash-hmac.php \ No newline at end of file diff --git a/lib/lib.php b/lib/lib.php index 883ccb9..262149f 100644 --- a/lib/lib.php +++ b/lib/lib.php @@ -13,54 +13,216 @@ class GoogleAuthenticator { $this->dbConnector = false; } } else { - setupDB(); + $this->setupDB($file); } $this->dbFile = $file; } // creates the database (tables); - function setupDB() { + function setupDB($file) { + try { - $this->$dbConnector = new PDO("sqlite:$file"); + $this->dbConnector = new PDO("sqlite:$file"); } catch(PDOException $exep) { $this->errorText = $exep->getMessage(); $this->dbConnector = false; } // here we create some tables and stuff + $this->dbConnector->query('CREATE TABLE "users" ("user_id" INTEGER PRIMARY KEY AUTOINCREMENT,"user_name" TEXT NOT NULL,"user_tokenid" INTEGER)'); + $this->dbConnector->query('CREATE TABLE "tokens" ("token_id" INTEGER PRIMARY KEY AUTOINCREMENT,"token_key" TEXT NOT NULL, "token_type" TEXT NOT NULL, "token_lastid" INTEGER NOT NULL)'); } // creates "user" in the database and returns a url for // the phone. If user already exists, this returns false // if any error occurs, this returns false function setupUser($username) { - $key = _createBase32Key(); + $key = $this->createBase32Key(); + + // sql for inserting into db + $sql = "select * from users where user_name='$username'"; + $res = $this->dbConnector->query($sql); + + if($res->fetchCount()>0) { + $this->errorText = "User Already Exists, $username"; + return false; + } + + // and finally create 'em + $this->dbConnector->query("insert into tokens values (NULL, '$key', 'HOTP','0')"); + $id = $this->dbConnector->lastInsertID(); + $this->dbConnector->query("insert into users values (NULL, '$username', '$id')"); + + $url = $this->createURL($username, $key); + + return $url; + } + + // set the token type the user it going to use. + // this defaults to HOTP - we only do 30s token + // so lets not be able to set that yet + function setupTokenType($username, $tokentype) { + if($tokentype!="HOTP" and $tokentype!="TOTP") { + $errorText = "Invalid Token Type"; + return false; + } + + $sql = "select * from users where user_name='$username'"; + $res = $this->dbConnector->query($sql); + + foreach($res as $row) { + $tid = $row["user_tokenid"]; + } + + + // TODO MUST DO ERROR CHECK HERE, this line could be lethal + $sql = "update tokens set token_type='$tokentype' where token_id='$tid'"; + + return true; + } + + + // create "user" with insert + function createUser($username, $key) { + // sql for inserting into db + $sql = "select * from users where user_name='$username'"; + $res = $this->dbConnector->query($sql); + + //if($res) if($res->fetchCount()>0) { + //$this->errorText = "User Already Exists, $username"; + //return false; + //} + + // and finally create 'em + $this->dbConnector->query("insert into tokens values (NULL, '$key', 'HOTP', '0')"); + $id = $this->dbConnector->lastInsertID(); + $this->dbConnector->query("insert into users values (NULL, '$username', '$id')"); + + $url = $this->createURL($username, $key); + + return $url; } // Replcate "user" in the database... All this really // does is to replace the key for the user. Returns false // if the user doesnt exist of the key is poop function replaceUser($username) { + $key = $this->createBase32Key(); + + // delete the user - TODO, clean up auth tokens + $sql = "delete from users where user_name='$username'"; + $res = $this->dbConnector->query($sql); + + // sql for inserting into db - just making sure. + $sql = "select * from users where user_name='$username'"; + $res = $this->dbConnector->query($sql); + + if($res->fetchCount()>0) { + $this->errorText = "User Already Exists, $username"; + return false; + } + // and finally create 'em + $this->dbConnector->query("insert into tokens values (NULL, '$key', '0')"); + $id = $this->dbConnector->lastInsertID(); + $this->dbConnector->query("insert into users values (NULL, '$username', '$id')"); + + $url = $this->createURL($username, $key); + + return $url; } // sets the key for a user - this is assuming you dont want // to use one created by the application. returns false // if the key is invalid or the user doesn't exist. function setUserKey($username, $key) { + $sql = "select * from users where user_name='$username'"; + $res = $this->dbConnector->query($sql); + + foreach($res as $row) { + $tid = $row["user_tokenid"]; + } + + // TODO MUST DO ERROR CHECK HERE, this line could be lethal + $sql = "update tokens set token_key='$key' where token_id='$tid'"; + + return true; } // self explanitory? function deleteUser($username) { + $sql = "select * from users where user_name='$username'"; + $res = $this->dbConnector->query($sql); + foreach($res as $row) { + $tid = $row["user_tokenid"]; + } + + + // TODO MUST DO ERROR CHECK HERE, this line could be lethal + $sql = "delete from tokens where token_id='$tid'"; + $this->dbConnector->query($sql); + + $sql = "delete from users where user_name='$username'"; + $this->dbConnector->query($sql); } // user has input their user name and some code, authenticate // it function authenticateUser($username, $code) { + $sql = "select * from users where user_name='$username'"; + $res = $this->dbConnector->query($sql); + + $tid = -1; + foreach($res as $row) { + $tid = $row["user_tokenid"]; + } + + // for HOTP tokens we start at x and go to x+20 + + // for TOTP we go +/-1min TODO = remember that +/- 1min should + // be changed based on stepping if we change the expiration time + // for keys + // $this->dbConnector->query('CREATE TABLE "tokens" ("token_id" INTEGER PRIMARY KEY AUTOINCREMENT,"token_key" TEXT NOT NULL, "token_type" TEXT NOT NULL, "token_lastid" INTEGER NOT NULL)'); + + $sql = "select * from tokens where token_id='$tid'"; + $res = $this->dbConnector->query($sql); + + $tkey = ""; + $ttype = ""; + $tlid = ""; + foreach($res as $row) { + $tkey = $row["token_key"]; + $ttype = $row["token_type"]; + $tlid = $row["token_lastid"]; + } + + switch($ttype) { + case "HOTP": + $st = $tlid; + $en = $tlid+20; + for($i=$st; $i<$en; $i++) { + $stest = $this->oath_hotp($tkey, $i); + //echo "code: $code, $stest, $tkey\n"; + if($code == $stest) { + $sql = "update tokens set token_lastid='$i' where token_id='$tid'"; + $this->dbConnector->query($sql); + return true; + } + } + return false; + break; + case "TOTP": + break; + default: + echo "how the frig did i end up here?"; + } + + return false; + } // this function allows a user to resync their key. If too @@ -77,7 +239,8 @@ class GoogleAuthenticator { // create a url compatibile with google authenticator. function createURL($user, $key) { - $url = "otpauth://hotp/$user?secret=$key"; + $url = "otpauth://totp/$user?secret=$key"; + echo "url: $url\n"; return $url; } @@ -102,10 +265,10 @@ class GoogleAuthenticator { $dous = ""; for($i = 0; $i < strlen($b32); $i++) { - $in = strrpos($alphabet, $b32[$i]); - $b = str_pad(base_convert($in, 10, 2), 5, "0", STR_PAD_LEFT); - $out .= $b; - $dous .= $b."."; + $in = strrpos($alphabet, $b32[$i]); + $b = str_pad(base_convert($in, 10, 2), 5, "0", STR_PAD_LEFT); + $out .= $b; + $dous .= $b."."; } $ar = str_split($out,20); @@ -148,6 +311,48 @@ class GoogleAuthenticator { return $out2; } + function oath_hotp($key, $counter) + { + $key = pack("H*", $key); + $cur_counter = array(0,0,0,0,0,0,0,0); + for($i=7;$i>=0;$i--) + { + $cur_counter[$i] = pack ('C*', $counter); + $counter = $counter >> 8; + } + $bin_counter = implode($cur_counter); + // Pad to 8 chars + if (strlen ($bin_counter) < 8) + { + $bin_counter = str_repeat (chr(0), 8 - strlen ($bin_counter)) . $bin_counter; + } + + // HMAC + $hash = hash_hmac ('sha1', $bin_counter, $key); + return $this->oath_truncate($hash); + } + + function oath_truncate($hash, $length = 6) + { + // Convert to dec + foreach(str_split($hash,2) as $hex) + { + $hmac_result[]=hexdec($hex); + } + + // Find offset + $offset = $hmac_result[19] & 0xf; + + // Algorithm from RFC + return + ( + (($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); + } + // some private data bits. private $errorText; diff --git a/unittests/authtest.php b/unittests/authtest.php new file mode 100644 index 0000000..649cc64 --- /dev/null +++ b/unittests/authtest.php @@ -0,0 +1,43 @@ +createUser("User1", "9732e257c94c9930818d"); + +if($ga->authenticateUser("User1", "393101")) { + echo "Passed: correct\n"; +} else { + echo "Failed: INCORRECT\n"; +} + +if($ga->authenticateUser("User1", "805347")) { + echo "Passed: correct\n"; +} else { + echo "Failed: INCORRECT\n"; +} + +if($ga->authenticateUser("User1", "428248")) { + echo "Passed: correct\n"; +} else { + echo "Failed: INCORRECT\n"; +} + +if($ga->authenticateUser("User1", "234523")) { + echo "Passed: INCORRECT\n"; +} else { + echo "Failed: correct\n"; +} + +if($ga->authenticateUser("User1", "598723")) { + echo "Passed: correct\n"; +} else { + echo "Failed: INCORRECT\n"; +} + +?> \ No newline at end of file diff --git a/unittests/createurl.php b/unittests/createurl.php index 7d3d4ca..f1240f7 100644 --- a/unittests/createurl.php +++ b/unittests/createurl.php @@ -8,9 +8,12 @@ echo "creating 10000 keys\n"; $oldkey = ""; $key = $ga->createBase32Key(); +$hex = $ga->helperb322hex($key); + $url = $ga->createURL("someuser", $key); system("qrencode -s 6 -o /tmp/file.unittest $url"); system("eog /tmp/file.unittest"); -unlink("/tmp/file.unittest"); +echo "key in hex: $hex\n"; +//unlink("/tmp/file.unittest"); ?> diff --git a/unittests/createuser.php b/unittests/createuser.php new file mode 100644 index 0000000..7b037a4 --- /dev/null +++ b/unittests/createuser.php @@ -0,0 +1,17 @@ +createBase32Key(); + +$url = $ga->setupUser("someuser", $key); + +system("qrencode -s 6 -o /tmp/file.unittest $url"); +system("eog /tmp/file.unittest"); +unlink("/tmp/file.unittest"); + +?> \ No newline at end of file -- 1.7.0.4