various improvemens
[ga4php.git] / lib / lib.php
1 <?php
2
3 /*
4  * TODO's:
5  * Implement TOTP fully
6  * Error checking, lots of error checking
7  */
8
9 class GoogleAuthenticator {
10         
11         // first we init google authenticator by passing it a filename
12         // for its sqlite database.
13         function __construct($file) {
14                 if(file_exists($file)) {
15                         try {
16                                 $this->dbConnector = new PDO("sqlite:$file");
17                         } catch(PDOException $exep) {
18                                 $this->errorText = $exep->getMessage();
19                                 $this->dbConnector = false;
20                         }                       
21                 } else {
22                         $this->setupDB($file);
23                 }
24                 
25                 $this->dbFile = $file;
26         }
27         
28         // creates the database (tables);
29         function setupDB($file) {
30                 
31                 try {
32                         $this->dbConnector = new PDO("sqlite:$file");
33                 } catch(PDOException $exep) {
34                         $this->errorText = $exep->getMessage();
35                         $this->dbConnector = false;
36                 }                       
37         
38                 // here we create some tables and stuff
39                 $this->dbConnector->query('CREATE TABLE "users" ("user_id" INTEGER PRIMARY KEY AUTOINCREMENT,"user_name" TEXT NOT NULL,"user_tokenid" INTEGER)');
40                 $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)');
41         }
42         
43         // creates "user" in the database and returns a url for
44         // the phone. If user already exists, this returns false
45         // if any error occurs, this returns false
46         function setupUser($username) {
47                 $key = $this->createBase32Key();
48                 
49                 // sql for inserting into db
50                 $sql = "select * from users where user_name='$username'";
51                 $res = $this->dbConnector->query($sql);
52
53                 //if($res->fetchCount()>0) {
54                         //$this->errorText = "User Already Exists, $username";
55                         //return false;
56                 //}
57                 
58                 // and finally create 'em
59                 $hkey = $this->helperb322hex($key);
60                 error_log("key for user $username is $hkey, $key");
61                 $this->dbConnector->query("insert into tokens values (NULL, '$hkey', 'HOTP','0')");
62                 $id = $this->dbConnector->lastInsertID();
63                 $this->dbConnector->query("insert into users values (NULL, '$username', '$id')");
64
65                 $url = $this->createURL($username, $key);
66                 
67                 return $url;
68         }
69         
70         
71         // this could get ugly for large databases.. we'll worry about that if it ever happens.
72         function getUserList() {
73                 $res = $this->dbConnector->query("select user_name from users");
74                 $i = 0;
75                 $ar = array();
76                 foreach($res as $row) {
77                         error_log("user: ".$row["user_name"]);
78                         $ar[$i] = $row["user_name"];
79                         $i++;
80                 }
81                 
82                 return $ar;
83         }
84         
85         // set the token type the user it going to use.
86         // this defaults to HOTP - we only do 30s token
87         // so lets not be able to set that yet
88         function setupTokenType($username, $tokentype) {
89                 if($tokentype!="HOTP" and $tokentype!="TOTP") {
90                         $errorText = "Invalid Token Type";
91                         return false;
92                 }
93                 
94                 $sql = "select * from users where user_name='$username'";
95                 $res = $this->dbConnector->query($sql);
96                 
97                 foreach($res as $row) {
98                         $tid = $row["user_tokenid"];    
99                 }
100                 
101                 
102                 // TODO MUST DO ERROR CHECK HERE, this line could be lethal
103                 $sql = "update tokens set token_type='$tokentype' where token_id='$tid'";
104                 
105                 return true;    
106         }
107         
108         
109         // create "user" with insert
110         function createUser($username, $key) {
111                 // sql for inserting into db
112                 $sql = "select * from users where user_name='$username'";
113                 $res = $this->dbConnector->query($sql);
114
115                 //if($res) if($res->fetchCount()>0) {
116                         //$this->errorText = "User Already Exists, $username";
117                         //return false;
118                 //}
119                 
120                 // and finally create 'em
121                 $hkey = $this->helperb322hex($key);
122                 $this->dbConnector->query("insert into tokens values (NULL, '$hkey', 'HOTP', '0')");
123                 $id = $this->dbConnector->lastInsertID();
124                 $this->dbConnector->query("insert into users values (NULL, '$username', '$id')");
125
126                 $url = $this->createURL($username, $key);
127                 
128                 return $url;
129         }
130         
131         // Replcate "user" in the database... All this really
132         // does is to replace the key for the user. Returns false
133         // if the user doesnt exist of the key is poop
134         function replaceUser($username) {
135                 $key = $this->createBase32Key();
136                 
137                 // delete the user - TODO, clean up auth tokens
138                 $sql = "delete from users where user_name='$username'";
139                 $res = $this->dbConnector->query($sql);
140                 
141                 // sql for inserting into db - just making sure.
142                 $sql = "select * from users where user_name='$username'";
143                 $res = $this->dbConnector->query($sql);
144
145                 if($res->fetchCount()>0) {
146                         $this->errorText = "User Already Exists, $username";
147                         return false;
148                 }
149                 
150                 // and finally create 'em
151                 $this->dbConnector->query("insert into tokens values (NULL, '$key', '0')");
152                 $id = $this->dbConnector->lastInsertID();
153                 $this->dbConnector->query("insert into users values (NULL, '$username', '$id')");
154
155                 $url = $this->createURL($username, $key);
156                 
157                 return $url;
158         }
159         
160         // sets the key for a user - this is assuming you dont want
161         // to use one created by the application. returns false
162         // if the key is invalid or the user doesn't exist.
163         function setUserKey($username, $key) {
164                 $sql = "select * from users where user_name='$username'";
165                 $res = $this->dbConnector->query($sql);
166                 
167                 foreach($res as $row) {
168                         $tid = $row["user_tokenid"];    
169                 }
170                 
171                 
172                 // TODO MUST DO ERROR CHECK HERE, this line could be lethal
173                 $sql = "update tokens set token_key='$key' where token_id='$tid'";
174                 
175                 return true;
176         }
177         
178         
179         // have user?
180         function userExists($username) {
181                 $sql = "select * from users where user_name='$username'";
182                 $res = $this->dbConnector->query($sql);
183                 
184                 $tid = -1;
185                 foreach($res as $row) {
186                         $tid = $row["user_tokenid"];    
187                 }
188                 
189                 if($tid == -1) return false;
190                 else return $tid;
191         }
192         
193         
194         // self explanitory?
195         function deleteUser($username) {
196                 $sql = "select * from users where user_name='$username'";
197                 $res = $this->dbConnector->query($sql);
198                 
199                 foreach($res as $row) {
200                         $tid = $row["user_tokenid"];    
201                 }
202                 
203                 
204                 // TODO MUST DO ERROR CHECK HERE, this line could be lethal
205                 $sql = "delete from tokens where token_id='$tid'";
206                 $this->dbConnector->query($sql);
207                 
208                 $sql = "delete from users where user_name='$username'";
209                 $this->dbConnector->query($sql);
210         }
211         
212         // user has input their user name and some code, authenticate
213         // it
214         function authenticateUser($username, $code) {
215                 $sql = "select * from users where user_name='$username'";
216                 $res = $this->dbConnector->query($sql);
217                 
218                 $tid = -1;
219                 foreach($res as $row) {
220                         $tid = $row["user_tokenid"];    
221                 }
222                 
223                 // for HOTP tokens we start at x and go to x+20
224                 
225                 // for TOTP we go +/-1min TODO = remember that +/- 1min should
226                 // be changed based on stepping if we change the expiration time
227                 // for keys
228                 
229                 //              $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)');
230                 
231                 $sql = "select * from tokens where token_id='$tid'";
232                 $res = $this->dbConnector->query($sql);
233                 
234                 $tkey = "";
235                 $ttype = "";
236                 $tlid = "";
237                 foreach($res as $row) {
238                         $tkey = $row["token_key"];
239                         $ttype = $row["token_type"];
240                         $tlid = $row["token_lastid"];   
241                 }
242                 
243                 switch($ttype) {
244                         case "HOTP":
245                                 $st = $tlid;
246                                 $en = $tlid+20;
247                                 for($i=$st; $i<$en; $i++) {
248                                         $stest = $this->oath_hotp($tkey, $i);
249                                         //echo "code: $code, $stest, $tkey\n";
250                                         if($code == $stest) {
251                                                 $sql = "update tokens set token_lastid='$i' where token_id='$tid'";
252                                                 $this->dbConnector->query($sql);
253                                                 return true;
254                                         }
255                                 }
256                                 return false;
257                                 break;
258                         case "TOTP":
259                                 break;
260                         default:
261                                 echo "how the frig did i end up here?";
262                 }
263                 
264                 return false;
265
266         }
267         
268         // this function allows a user to resync their key. If too
269         // many codes are called, we only check up to 20 codes in the future
270         // so if the user is at 21, they'll always fail. 
271         function resyncCode($username, $code1, $code2) {
272                 // 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
273                 $sql = "select * from users where user_name='$username'";
274                 $res = $this->dbConnector->query($sql);
275                 
276                 $tid = -1;
277                 foreach($res as $row) {
278                         $tid = $row["user_tokenid"];    
279                 }
280                 
281                 // for HOTP tokens we start at x and go to x+20
282                 
283                 // for TOTP we go +/-1min TODO = remember that +/- 1min should
284                 // be changed based on stepping if we change the expiration time
285                 // for keys
286                 
287                 //              $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)');
288                 
289                 $sql = "select * from tokens where token_id='$tid'";
290                 $res = $this->dbConnector->query($sql);
291                 
292                 $tkey = "";
293                 $ttype = "";
294                 $tlid = "";
295                 foreach($res as $row) {
296                         $tkey = $row["token_key"];
297                         $ttype = $row["token_type"];
298                         $tlid = $row["token_lastid"];   
299                 }
300                 
301                 switch($ttype) {
302                         case "HOTP":
303                                 $st = 0;
304                                 $en = 200000;
305                                 for($i=$st; $i<$en; $i++) {
306                                         $stest = $this->oath_hotp($tkey, $i);
307                                         //echo "code: $code, $stest, $tkey\n";
308                                         if($code1 == $stest) {
309                                                 $stest2 = $this->oath_hotp($tkey, $i+1);
310                                                 if($code2 == $stest2) {
311                                                         $sql = "update tokens set token_lastid='$i' where token_id='$tid'";
312                                                         $this->dbConnector->query($sql);
313                                                         return true;
314                                                 }
315                                         }
316                                 }
317                                 return false;
318                                 break;
319                         case "TOTP":
320                                 break;
321                         default:
322                                 echo "how the frig did i end up here?";
323                 }
324                 
325                 return false;
326                 
327         }
328         
329         // gets the error text associated with the last error
330         function getErrorText() {
331                 return $this->errorText;
332         }
333         
334         // create a url compatibile with google authenticator.
335         function createURL($user, $key) {
336                 $url = "otpauth://hotp/$user?secret=$key";
337                 //echo "url: $url\n";
338                 return $url;
339         }
340         
341         // creeates a base 32 key (random)
342         function createBase32Key() {
343                 $alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
344                 $key = "";
345                 for($i=0; $i<16; $i++) {
346                         $offset = rand(0,strlen($alphabet)-1);
347                         //echo "$i off is $offset\n";
348                         $key .= $alphabet[$offset];
349                 }
350                 
351                 return $key;
352         }
353                 
354         
355         function helperb322hex($b32) {
356         $alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
357
358         $out = "";
359         $dous = "";
360
361         for($i = 0; $i < strlen($b32); $i++) {
362                 $in = strrpos($alphabet, $b32[$i]);
363                 $b = str_pad(base_convert($in, 10, 2), 5, "0", STR_PAD_LEFT);
364             $out .= $b;
365             $dous .= $b.".";
366         }
367
368         $ar = str_split($out,20);
369
370         //echo "$dous, $b\n";
371
372         //print_r($ar);
373         $out2 = "";
374         foreach($ar as $val) {
375                 $rv = str_pad(base_convert($val, 2, 16), 5, "0", STR_PAD_LEFT);
376                 //echo "rv: $rv from $val\n";
377                 $out2 .= $rv;
378
379         }
380         //echo "$out2\n";
381
382         return $out2;
383         }
384         
385         function helperhex2b32($hex) {
386         $alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
387
388         $ar = str_split($hex, 5);
389
390         $out = "";
391         foreach($ar as $var) {
392                 $bc = base_convert($var, 16, 2);
393                 $bin = str_pad($bc, 20, "0", STR_PAD_LEFT);
394                 $out .= $bin;
395                 //echo "$bc was, $var is, $bin are\n";
396         }
397
398         $out2 = "";
399         $ar2 = str_split($out, 5);
400         foreach($ar2 as $var2) {
401                 $bc = base_convert($var2, 2, 10);
402                 $out2 .= $alphabet[$bc];
403         }
404
405         return $out2;
406         }
407         
408         function oath_hotp($key, $counter)
409         {
410                 $key = pack("H*", $key);
411             $cur_counter = array(0,0,0,0,0,0,0,0);
412             for($i=7;$i>=0;$i--)
413             {
414                 $cur_counter[$i] = pack ('C*', $counter);
415                 $counter = $counter >> 8;
416             }
417             $bin_counter = implode($cur_counter);
418             // Pad to 8 chars
419             if (strlen ($bin_counter) < 8)
420             {
421                 $bin_counter = str_repeat (chr(0), 8 - strlen ($bin_counter)) . $bin_counter;
422             }
423         
424             // HMAC
425             $hash = hash_hmac ('sha1', $bin_counter, $key);
426             return str_pad($this->oath_truncate($hash), 6, "0", STR_PAD_LEFT);
427         }
428         
429         function oath_truncate($hash, $length = 6)
430         {
431             // Convert to dec
432             foreach(str_split($hash,2) as $hex)
433             {
434                 $hmac_result[]=hexdec($hex);
435             }
436         
437             // Find offset
438             $offset = $hmac_result[19] & 0xf;
439         
440             // Algorithm from RFC
441             return
442             (
443                 (($hmac_result[$offset+0] & 0x7f) << 24 ) |
444                 (($hmac_result[$offset+1] & 0xff) << 16 ) |
445                 (($hmac_result[$offset+2] & 0xff) << 8 ) |
446                 ($hmac_result[$offset+3] & 0xff)
447             ) % pow(10,$length);
448         }
449         
450         
451         // some private data bits.
452         private $errorText;
453         private $dbFile;
454         private $dbConnector;
455 }
456 ?>