57a4cbf79b7765f15ca6dd6f3a74aa35cec85ce8
[ga4php.git] / lib / lib.php
1 <?php
2
3 /*
4  * TODO's:
5  * Implement TOTP fully
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...
10  */
11
12 class GoogleAuthenticator {
13         
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)) {
18                         try {
19                                 $this->dbConnector = new PDO("sqlite:$file");
20                         } catch(PDOException $exep) {
21                                 $this->errorText = $exep->getMessage();
22                                 $this->dbConnector = false;
23                         }                       
24                 } else {
25                         $this->setupDB($file);
26                 }
27                 
28                 $this->dbFile = $file;
29         }
30         
31         // creates the database (tables);
32         function setupDB($file) {
33                 
34                 try {
35                         $this->dbConnector = new PDO("sqlite:$file");
36                 } catch(PDOException $exep) {
37                         $this->errorText = $exep->getMessage();
38                         $this->dbConnector = false;
39                 }                       
40         
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)');
44         }
45         
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();
51                 
52                 // sql for inserting into db
53                 $key = $this->createUser($username, $key, $tokentype);
54                 return $key;
55         }
56         
57         
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");
61                 $i = 0;
62                 $ar = array();
63                 foreach($res as $row) {
64                         //error_log("user: ".$row["user_name"]);
65                         $ar[$i] = $row["user_name"];
66                         $i++;
67                 }
68                 
69                 return $ar;
70         }
71         
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";
78                         return false;
79                 }
80                 
81                 $sql = "select * from users where user_name='$username'";
82                 $res = $this->dbConnector->query($sql);
83                 
84                 foreach($res as $row) {
85                         $tid = $row["user_tokenid"];    
86                 }
87                 
88                 
89                 // TODO MUST DO ERROR CHECK HERE, this line could be lethal
90                 $sql = "update tokens set token_type='$tokentype' where token_id='$tid'";
91                 
92                 return true;    
93         }
94         
95         
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);
101
102                 //if($res) if($res->fetchCount()>0) {
103                         //$this->errorText = "User Already Exists, $username";
104                         //return false;
105                 //}
106                 
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')");
112
113                 return $key;
114         }
115         
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();
121                 
122                 // delete the user - TODO, clean up auth tokens
123                 $sql = "delete from users where user_name='$username'";
124                 $res = $this->dbConnector->query($sql);
125                 
126                 // sql for inserting into db - just making sure.
127                 $sql = "select * from users where user_name='$username'";
128                 $res = $this->dbConnector->query($sql);
129
130                 if($res->fetchCount()>0) {
131                         $this->errorText = "User Already Exists, $username";
132                         return false;
133                 }
134                 
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')");
139
140                 $url = $this->createURL($username, $key);
141                 
142                 return $url;
143         }
144         
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);
151                 
152                 foreach($res as $row) {
153                         $tid = $row["user_tokenid"];    
154                 }
155                 
156                 
157                 // TODO MUST DO ERROR CHECK HERE, this line could be lethal
158                 $sql = "update tokens set token_key='$key' where token_id='$tid'";
159                 
160                 return true;
161         }
162         
163         
164         // have user?
165         function userExists($username) {
166                 $sql = "select * from users where user_name='$username'";
167                 $res = $this->dbConnector->query($sql);
168                 
169                 $tid = -1;
170                 foreach($res as $row) {
171                         $tid = $row["user_tokenid"];    
172                 }
173                 
174                 if($tid == -1) return false;
175                 else return $tid;
176         }
177         
178         
179         // self explanitory?
180         function deleteUser($username) {
181                 $sql = "select * from users where user_name='$username'";
182                 $res = $this->dbConnector->query($sql);
183                 
184                 foreach($res as $row) {
185                         $tid = $row["user_tokenid"];    
186                 }
187                 
188                 
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);
192                 
193                 $sql = "delete from users where user_name='$username'";
194                 $this->dbConnector->query($sql);
195         }
196         
197         // user has input their user name and some code, authenticate
198         // it
199         function authenticateUser($username, $code) {
200                 $sql = "select * from users where user_name='$username'";
201                 $res = $this->dbConnector->query($sql);
202                 
203                 $tid = -1;
204                 foreach($res as $row) {
205                         $tid = $row["user_tokenid"];    
206                 }
207                 
208                 // for HOTP tokens we start at x and go to x+20
209                 
210                 // for TOTP we go +/-1min TODO = remember that +/- 1min should
211                 // be changed based on stepping if we change the expiration time
212                 // for keys
213                 
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)');
215                 
216                 $sql = "select * from tokens where token_id='$tid'";
217                 $res = $this->dbConnector->query($sql);
218                 
219                 $tkey = "";
220                 $ttype = "";
221                 $tlid = "";
222                 foreach($res as $row) {
223                         $tkey = $row["token_key"];
224                         $ttype = $row["token_type"];
225                         $tlid = $row["token_lastid"];   
226                 }
227                 
228                 switch($ttype) {
229                         case "HOTP":
230                                 $st = $tlid;
231                                 $en = $tlid+20;
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);
238                                                 return true;
239                                         }
240                                 }
241                                 return false;
242                                 break;
243                         case "TOTP":
244                                 $t_now = time();
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) {
254                                                 return true;
255                                         }
256                                 }
257                                 break;
258                         default:
259                                 echo "how the frig did i end up here?";
260                 }
261                 
262                 return false;
263
264         }
265         
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);
273                 
274                 $tid = -1;
275                 foreach($res as $row) {
276                         $tid = $row["user_tokenid"];    
277                 }
278                 
279                 // for HOTP tokens we start at x and go to x+20
280                 
281                 // for TOTP we go +/-1min TODO = remember that +/- 1min should
282                 // be changed based on stepping if we change the expiration time
283                 // for keys
284                 
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)');
286                 
287                 $sql = "select * from tokens where token_id='$tid'";
288                 $res = $this->dbConnector->query($sql);
289                 
290                 $tkey = "";
291                 $ttype = "";
292                 $tlid = "";
293                 foreach($res as $row) {
294                         $tkey = $row["token_key"];
295                         $ttype = $row["token_type"];
296                         $tlid = $row["token_lastid"];   
297                 }
298                 
299                 switch($ttype) {
300                         case "HOTP":
301                                 $st = 0;
302                                 $en = 200000;
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);
311                                                         return true;
312                                                 }
313                                         }
314                                 }
315                                 return false;
316                                 break;
317                         case "TOTP":
318                                 break;
319                         default:
320                                 echo "how the frig did i end up here?";
321                 }
322                 
323                 return false;
324                 
325         }
326         
327         // gets the error text associated with the last error
328         function getErrorText() {
329                 return $this->errorText;
330         }
331         
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";
338                 } else {
339                         $url = "otpauth://$toktype/$user?secret=$key";
340                 }
341                 //echo "url: $url\n";
342                 return $url;
343         }
344         
345         // creeates a base 32 key (random)
346         function createBase32Key() {
347                 $alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
348                 $key = "";
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];
353                 }
354                 
355                 return $key;
356         }
357                 
358         
359         function helperb322hex($b32) {
360         $alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
361
362         $out = "";
363         $dous = "";
364
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);
368             $out .= $b;
369             $dous .= $b.".";
370         }
371
372         $ar = str_split($out,20);
373
374         //echo "$dous, $b\n";
375
376         //print_r($ar);
377         $out2 = "";
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";
381                 $out2 .= $rv;
382
383         }
384         //echo "$out2\n";
385
386         return $out2;
387         }
388         
389         function helperhex2b32($hex) {
390         $alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
391
392         $ar = str_split($hex, 5);
393
394         $out = "";
395         foreach($ar as $var) {
396                 $bc = base_convert($var, 16, 2);
397                 $bin = str_pad($bc, 20, "0", STR_PAD_LEFT);
398                 $out .= $bin;
399                 //echo "$bc was, $var is, $bin are\n";
400         }
401
402         $out2 = "";
403         $ar2 = str_split($out, 5);
404         foreach($ar2 as $var2) {
405                 $bc = base_convert($var2, 2, 10);
406                 $out2 .= $alphabet[$bc];
407         }
408
409         return $out2;
410         }
411         
412         function oath_hotp($key, $counter)
413         {
414                 $key = pack("H*", $key);
415             $cur_counter = array(0,0,0,0,0,0,0,0);
416             for($i=7;$i>=0;$i--)
417             {
418                 $cur_counter[$i] = pack ('C*', $counter);
419                 $counter = $counter >> 8;
420             }
421             $bin_counter = implode($cur_counter);
422             // Pad to 8 chars
423             if (strlen ($bin_counter) < 8)
424             {
425                 $bin_counter = str_repeat (chr(0), 8 - strlen ($bin_counter)) . $bin_counter;
426             }
427         
428             // HMAC
429             $hash = hash_hmac ('sha1', $bin_counter, $key);
430             return str_pad($this->oath_truncate($hash), 6, "0", STR_PAD_LEFT);
431         }
432         
433         function oath_truncate($hash, $length = 6)
434         {
435             // Convert to dec
436             foreach(str_split($hash,2) as $hex)
437             {
438                 $hmac_result[]=hexdec($hex);
439             }
440         
441             // Find offset
442             $offset = $hmac_result[19] & 0xf;
443         
444             // Algorithm from RFC
445             return
446             (
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)
451             ) % pow(10,$length);
452         }
453         
454         
455         // some private data bits.
456         private $errorText;
457         private $dbFile;
458         private $dbConnector;
459 }
460 ?>