6 * Error checking, lots of error checking
7 * have a way of encapsulating token data stright into a single field so it could be added
8 * in some way to a preexisting app without modifying the DB as such... or by just adding
9 * a single field to a user table...
12 class GoogleAuthenticator {
14 // first we init google authenticator by passing it a filename
15 // for its sqlite database.
16 function __construct($file) {
17 if(file_exists($file)) {
19 $this->dbConnector = new PDO("sqlite:$file");
20 } catch(PDOException $exep) {
21 $this->errorText = $exep->getMessage();
22 $this->dbConnector = false;
25 $this->setupDB($file);
28 $this->dbFile = $file;
31 // creates the database (tables);
32 function setupDB($file) {
35 $this->dbConnector = new PDO("sqlite:$file");
36 } catch(PDOException $exep) {
37 $this->errorText = $exep->getMessage();
38 $this->dbConnector = false;
41 // here we create some tables and stuff
42 $this->dbConnector->query('CREATE TABLE "users" ("user_id" INTEGER PRIMARY KEY AUTOINCREMENT,"user_name" TEXT NOT NULL,"user_tokenid" INTEGER)');
43 $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)');
46 // creates "user" in the database and returns a url for
47 // the phone. If user already exists, this returns false
48 // if any error occurs, this returns false
49 function setupUser($username, $tokentype="HOTP") {
50 $key = $this->createBase32Key();
52 // sql for inserting into db
53 $key = $this->createUser($username, $key, $tokentype);
58 // this could get ugly for large databases.. we'll worry about that if it ever happens.
59 function getUserList() {
60 $res = $this->dbConnector->query("select user_name from users");
63 foreach($res as $row) {
64 //error_log("user: ".$row["user_name"]);
65 $ar[$i] = $row["user_name"];
72 // set the token type the user it going to use.
73 // this defaults to HOTP - we only do 30s token
74 // so lets not be able to set that yet
75 function setupTokenType($username, $tokentype) {
76 if($tokentype!="HOTP" and $tokentype!="TOTP") {
77 $errorText = "Invalid Token Type";
81 $sql = "select * from users where user_name='$username'";
82 $res = $this->dbConnector->query($sql);
84 foreach($res as $row) {
85 $tid = $row["user_tokenid"];
89 // TODO MUST DO ERROR CHECK HERE, this line could be lethal
90 $sql = "update tokens set token_type='$tokentype' where token_id='$tid'";
96 // create "user" with insert
97 function createUser($username, $key, $ttype="HOTP") {
98 // sql for inserting into db
99 $sql = "select * from users where user_name='$username'";
100 $res = $this->dbConnector->query($sql);
102 //if($res) if($res->fetchCount()>0) {
103 //$this->errorText = "User Already Exists, $username";
107 // and finally create 'em
108 $hkey = $this->helperb322hex($key);
109 $this->dbConnector->query("insert into tokens values (NULL, '$hkey', '$ttype', '0')");
110 $id = $this->dbConnector->lastInsertID();
111 $this->dbConnector->query("insert into users values (NULL, '$username', '$id')");
116 // Replcate "user" in the database... All this really
117 // does is to replace the key for the user. Returns false
118 // if the user doesnt exist of the key is poop
119 function replaceUser($username) {
120 $key = $this->createBase32Key();
122 // delete the user - TODO, clean up auth tokens
123 $sql = "delete from users where user_name='$username'";
124 $res = $this->dbConnector->query($sql);
126 // sql for inserting into db - just making sure.
127 $sql = "select * from users where user_name='$username'";
128 $res = $this->dbConnector->query($sql);
130 if($res->fetchCount()>0) {
131 $this->errorText = "User Already Exists, $username";
135 // and finally create 'em
136 $this->dbConnector->query("insert into tokens values (NULL, '$key', '0')");
137 $id = $this->dbConnector->lastInsertID();
138 $this->dbConnector->query("insert into users values (NULL, '$username', '$id')");
140 $url = $this->createURL($username, $key);
145 // sets the key for a user - this is assuming you dont want
146 // to use one created by the application. returns false
147 // if the key is invalid or the user doesn't exist.
148 function setUserKey($username, $key) {
149 $sql = "select * from users where user_name='$username'";
150 $res = $this->dbConnector->query($sql);
152 foreach($res as $row) {
153 $tid = $row["user_tokenid"];
157 // TODO MUST DO ERROR CHECK HERE, this line could be lethal
158 $sql = "update tokens set token_key='$key' where token_id='$tid'";
165 function userExists($username) {
166 $sql = "select * from users where user_name='$username'";
167 $res = $this->dbConnector->query($sql);
170 foreach($res as $row) {
171 $tid = $row["user_tokenid"];
174 if($tid == -1) return false;
180 function deleteUser($username) {
181 $sql = "select * from users where user_name='$username'";
182 $res = $this->dbConnector->query($sql);
184 foreach($res as $row) {
185 $tid = $row["user_tokenid"];
189 // TODO MUST DO ERROR CHECK HERE, this line could be lethal
190 $sql = "delete from tokens where token_id='$tid'";
191 $this->dbConnector->query($sql);
193 $sql = "delete from users where user_name='$username'";
194 $this->dbConnector->query($sql);
197 // user has input their user name and some code, authenticate
199 function authenticateUser($username, $code) {
200 $sql = "select * from users where user_name='$username'";
201 $res = $this->dbConnector->query($sql);
204 foreach($res as $row) {
205 $tid = $row["user_tokenid"];
208 // for HOTP tokens we start at x and go to x+20
210 // for TOTP we go +/-1min TODO = remember that +/- 1min should
211 // be changed based on stepping if we change the expiration time
214 // $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)');
216 $sql = "select * from tokens where token_id='$tid'";
217 $res = $this->dbConnector->query($sql);
222 foreach($res as $row) {
223 $tkey = $row["token_key"];
224 $ttype = $row["token_type"];
225 $tlid = $row["token_lastid"];
232 for($i=$st; $i<$en; $i++) {
233 $stest = $this->oath_hotp($tkey, $i);
234 //error_log("code: $code, $stest, $tkey, $tid");
235 if($code == $stest) {
236 $sql = "update tokens set token_lastid='$i' where token_id='$tid'";
237 $this->dbConnector->query($sql);
245 $t_ear = $t_now - 45;
246 $t_lat = $t_now + 60;
247 $t_st = ((int)($t_ear/30));
248 $t_en = ((int)($t_lat/30));
249 //error_log("kmac: $t_now, $t_ear, $t_lat, $t_st, $t_en");
250 for($i=$t_st; $i<=$t_en; $i++) {
251 $stest = $this->oath_hotp($tkey, $i);
252 //error_log("code: $code, $stest, $tkey\n");
253 if($code == $stest) {
259 echo "how the frig did i end up here?";
266 // this function allows a user to resync their key. If too
267 // many codes are called, we only check up to 20 codes in the future
268 // so if the user is at 21, they'll always fail.
269 function resyncCode($username, $code1, $code2) {
270 // here we'll go from 0 all the way thru to 200k.. if we cant find the code, so be it, they'll need a new one
271 $sql = "select * from users where user_name='$username'";
272 $res = $this->dbConnector->query($sql);
275 foreach($res as $row) {
276 $tid = $row["user_tokenid"];
279 // for HOTP tokens we start at x and go to x+20
281 // for TOTP we go +/-1min TODO = remember that +/- 1min should
282 // be changed based on stepping if we change the expiration time
285 // $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)');
287 $sql = "select * from tokens where token_id='$tid'";
288 $res = $this->dbConnector->query($sql);
293 foreach($res as $row) {
294 $tkey = $row["token_key"];
295 $ttype = $row["token_type"];
296 $tlid = $row["token_lastid"];
303 for($i=$st; $i<$en; $i++) {
304 $stest = $this->oath_hotp($tkey, $i);
305 //echo "code: $code, $stest, $tkey\n";
306 if($code1 == $stest) {
307 $stest2 = $this->oath_hotp($tkey, $i+1);
308 if($code2 == $stest2) {
309 $sql = "update tokens set token_lastid='$i' where token_id='$tid'";
310 $this->dbConnector->query($sql);
320 echo "how the frig did i end up here?";
327 // gets the error text associated with the last error
328 function getErrorText() {
329 return $this->errorText;
332 // create a url compatibile with google authenticator.
333 function createURL($user, $key,$toktype = "HOTP") {
334 // oddity in the google authenticator... hotp needs to be lowercase.
335 $toktype = strtolower($toktype);
336 if($toktype == "hotp") {
337 $url = "otpauth://$toktype/$user?secret=$key&counter=1";
339 $url = "otpauth://$toktype/$user?secret=$key";
341 //echo "url: $url\n";
345 // creeates a base 32 key (random)
346 function createBase32Key() {
347 $alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
349 for($i=0; $i<16; $i++) {
350 $offset = rand(0,strlen($alphabet)-1);
351 //echo "$i off is $offset\n";
352 $key .= $alphabet[$offset];
359 function helperb322hex($b32) {
360 $alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
365 for($i = 0; $i < strlen($b32); $i++) {
366 $in = strrpos($alphabet, $b32[$i]);
367 $b = str_pad(base_convert($in, 10, 2), 5, "0", STR_PAD_LEFT);
372 $ar = str_split($out,20);
374 //echo "$dous, $b\n";
378 foreach($ar as $val) {
379 $rv = str_pad(base_convert($val, 2, 16), 5, "0", STR_PAD_LEFT);
380 //echo "rv: $rv from $val\n";
389 function helperhex2b32($hex) {
390 $alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
392 $ar = str_split($hex, 5);
395 foreach($ar as $var) {
396 $bc = base_convert($var, 16, 2);
397 $bin = str_pad($bc, 20, "0", STR_PAD_LEFT);
399 //echo "$bc was, $var is, $bin are\n";
403 $ar2 = str_split($out, 5);
404 foreach($ar2 as $var2) {
405 $bc = base_convert($var2, 2, 10);
406 $out2 .= $alphabet[$bc];
412 function oath_hotp($key, $counter)
414 $key = pack("H*", $key);
415 $cur_counter = array(0,0,0,0,0,0,0,0);
418 $cur_counter[$i] = pack ('C*', $counter);
419 $counter = $counter >> 8;
421 $bin_counter = implode($cur_counter);
423 if (strlen ($bin_counter) < 8)
425 $bin_counter = str_repeat (chr(0), 8 - strlen ($bin_counter)) . $bin_counter;
429 $hash = hash_hmac ('sha1', $bin_counter, $key);
430 return str_pad($this->oath_truncate($hash), 6, "0", STR_PAD_LEFT);
433 function oath_truncate($hash, $length = 6)
436 foreach(str_split($hash,2) as $hex)
438 $hmac_result[]=hexdec($hex);
442 $offset = $hmac_result[19] & 0xf;
444 // Algorithm from RFC
447 (($hmac_result[$offset+0] & 0x7f) << 24 ) |
448 (($hmac_result[$offset+1] & 0xff) << 16 ) |
449 (($hmac_result[$offset+2] & 0xff) << 8 ) |
450 ($hmac_result[$offset+3] & 0xff)
455 // some private data bits.
458 private $dbConnector;