a bit more error checking, moved the skew's and hunt values into global variables...
[ga4php.git] / lib / ga4php.php
1 <?php
2
3 abstract class GoogleAuthenticator {
4         
5         function __construct($totpskew=1, $hotpskew=10, $hotphuntvalue=200000) {
6                 // the hotpskew is how many tokens forward we look to find the input
7                 // code the user used
8                 $this->hotpSkew = $hotpskew;
9                 
10                 // the totpskew value is how many tokens either side of the current
11                 // token we should check, based on a time skew.
12                 $this->totpSkew = $totpskew;
13                 
14                 // the hotphuntvalue is what we use to resync tokens.
15                 // when a user resyncs, we search from 0 to $hutphutvalue to find
16                 // the two token codes the user has entered - 200000 seems like overkill
17                 // really as i cant imagine any token out there would ever make it
18                 // past 200000 token code requests.
19                 $this->hotpHuntValue = $hotphuntvalue;
20         }
21         
22         abstract function getData($username);
23         abstract function putData($username, $data);
24         abstract function getUsers();
25         
26         // a function to create an empty data structure, filled with some defaults
27         function createEmptyData() {
28                 $data["tokenkey"] = ""; // the token key
29                 $data["tokentype"] = "HOTP"; // the token type
30                 $data["tokentimer"] = 30; // the token timer (For totp) and not supported by ga yet             
31                 $data["tokencounter"] = 1; // the token counter for hotp
32                 $data["tokenalgorithm"] = "SHA1"; // the token algorithm (not supported by ga yet)
33                 $data["user1"] = ""; // a place for implementors to store their own data
34                 
35                 return $data;
36         }
37         
38         // an internal funciton to get 
39         function internalGetData($username) {
40                 $data = $this->getData($username);
41                 $deco = unserialize(base64_decode($data));
42                 
43                 if(!$deco) {
44                         $deco = $this->createEmptyData();
45                 }
46                 
47                 return $deco;
48         }
49         
50
51         function internalPutData($username, $data) {
52                 $enco = base64_encode(serialize($data));
53                 
54                 return $this->putData($username, $enco);
55         }
56         
57
58         // set the token type the user it going to use.
59         // this defaults to HOTP - we only do 30s token
60         // so lets not be able to set that yet
61         function setTokenType($username, $tokentype) {
62                 $tokentype = strtoupper($tokentype);
63                 if($tokentype!="HOTP" and $tokentype!="TOTP") {
64                         $errorText = "Invalid Token Type";
65                         return false;
66                 }
67                 
68                 $data = $this->internalGetData($username);
69                 $data["tokentype"] = $tokentype;
70                 return $this->internalPutData($username, $data);
71                 
72         }
73         
74         
75         // create "user" with insert
76         function setUser($username, $ttype="HOTP", $key = "", $hexkey="") {
77                 if($key == "") $key = $this->createBase32Key();
78                 $hkey = $this->helperb322hex($key);
79                 if($hexkey != "") $hkey = $hexkey;
80                 
81                 $token = $this->internalGetData($username);
82                 $token["tokenkey"] = $hkey;
83                 $token["tokentype"] = $ttype;
84                 
85                 if(!$this->internalPutData($username, $token)) {
86                         return false;
87                 }               
88                 return $key;
89         }
90         
91         
92         function hasToken($username) {
93                 $token = $this->internalGetData($username);
94                 // TODO: change this to a pattern match for an actual key
95                 if(!isset($token["tokenkey"])) return false;
96                 if($token["tokenkey"] == "") return false;
97                 return true;
98         }
99         
100         
101         // sets the key for a user - this is assuming you dont want
102         // to use one created by the application. returns false
103         // if the key is invalid or the user doesn't exist.
104         function setUserKey($username, $key) {
105                 // consider scrapping this
106                 $token = $this->internalGetData($username);
107                 $token["tokenkey"] = $key;
108                 $this->internalPutData($username, $token);              
109         }
110         
111         
112         // self explanitory?
113         function deleteUser($username) {
114                 // oh, we need to figure out how to do thi?
115                 $data = $this->internalGetData($username);
116                 $data["tokenkey"] = "";
117                 $this->internalPutData($username);              
118         }
119         
120         // user has input their user name and some code, authenticate
121         // it
122         function authenticateUser($username, $code) {
123
124                 if(preg_match("/[0-9][0-9][0-9][0-9][0-9][0-9]/",$code)<1) return false;
125                 //error_log("begin auth user");
126                 $tokendata = $this->internalGetData($username);
127                 //$asdf = print_r($tokendata, true);
128                 //error_log("dat is $asdf");
129                 
130                 if($tokendata["tokenkey"] == "") {
131                         $errorText = "No Assigned Token";
132                         return false;
133                 }
134                 
135                 // TODO: check return value
136                 $ttype = $tokendata["tokentype"];
137                 $tlid = $tokendata["tokencounter"];
138                 $tkey = $tokendata["tokenkey"];
139                 
140                 //$asdf = print_r($tokendata, true);
141                 //error_log("dat is $asdf");
142                 switch($ttype) {
143                         case "HOTP":
144                                 error_log("in hotp");
145                                 $st = $tlid;
146                                 $en = $tlid+$this->hotpSkew;
147                                 for($i=$st; $i<$en; $i++) {
148                                         $stest = $this->oath_hotp($tkey, $i);
149                                         error_log("testing code: $code, $stest, $tkey, $tid");
150                                         if($code == $stest) {
151                                                 $tokendata["tokencounter"] = $i;
152                                                 $this->internalPutData($username, $tokendata);
153                                                 return true;
154                                         }
155                                 }
156                                 return false;
157                                 break;
158                         case "TOTP":
159                                 error_log("in totp");
160                                 $t_now = time();
161                                 $t_ear = $t_now - ($this->totpSkew*$tokendata["tokentimer"]);
162                                 $t_lat = $t_now + ($this->totpSkew*$tokendata["tokentimer"]);
163                                 $t_st = ((int)($t_ear/$tokendata["tokentimer"]));
164                                 $t_en = ((int)($t_lat/$tokendata["tokentimer"]));
165                                 //error_log("kmac: $t_now, $t_ear, $t_lat, $t_st, $t_en");
166                                 for($i=$t_st; $i<=$t_en; $i++) {
167                                         $stest = $this->oath_hotp($tkey, $i);
168                                         error_log("testing code: $code, $stest, $tkey\n");
169                                         if($code == $stest) {
170                                                 return true;
171                                         }
172                                 }
173                                 break;
174                         default:
175                                 return false;
176                 }
177                 
178                 return false;
179
180         }
181         
182         // this function allows a user to resync their key. If too
183         // many codes are called, we only check up to 20 codes in the future
184         // so if the user is at 21, they'll always fail. 
185         function resyncCode($username, $code1, $code2) {
186                 // 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
187                 // for HOTP tokens we start at x and go to x+20
188                 
189                 // for TOTP we go +/-1min TODO = remember that +/- 1min should
190                 // be changed based on stepping if we change the expiration time
191                 // for keys
192                 
193                 //              $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)');
194                 $tokendata = internalGetData($username);
195                 
196                 // TODO: check return value
197                 $ttype = $tokendata["tokentype"];
198                 $tlid = $tokendata["tokencounter"];
199                 $tkey = $tokendata["tokenkey"];
200                 
201                 if($tkey == "") {
202                         $this->errorText = "No Assigned Token";
203                         return false;
204                 }
205                 
206                 switch($ttype) {
207                         case "HOTP":
208                                 $st = 0;
209                                 $en = $this->hotpHuntValue;
210                                 for($i=$st; $i<$en; $i++) {
211                                         $stest = $this->oath_hotp($tkey, $i);
212                                         //echo "code: $code, $stest, $tkey\n";
213                                         if($code1 == $stest) {
214                                                 $stest2 = $this->oath_hotp($tkey, $i+1);
215                                                 if($code2 == $stest2) {
216                                                         $tokendata["tokencounter"] = $i+1;
217                                                         internalPutData($username, $tokendata);                                         
218                                                         return true;
219                                                 }
220                                         }
221                                 }
222                                 return false;
223                                 break;
224                         case "TOTP":
225                                 // ignore it?
226                                 break;
227                         default:
228                                 echo "how the frig did i end up here?";
229                 }
230                 
231                 return false;
232         }
233         
234         // gets the error text associated with the last error
235         function getErrorText() {
236                 return $this->errorText;
237         }
238         
239         // create a url compatibile with google authenticator.
240         function createURL($user) {
241                 // oddity in the google authenticator... hotp needs to be lowercase.
242                 $data = $this->internalGetData($user);
243                 $toktype = $data["tokentype"];
244                 $key = $this->helperhex2b32($data["tokenkey"]);
245                 $counter = $data["tokencounter"];
246                 $toktype = strtolower($toktype);
247                 if($toktype == "hotp") {
248                         $url = "otpauth://$toktype/$user?secret=$key&counter=$counter";
249                 } else {
250                         $url = "otpauth://$toktype/$user?secret=$key";
251                 }
252                 //echo "url: $url\n";
253                 return $url;
254         }
255         
256         // creeates a base 32 key (random)
257         function createBase32Key() {
258                 $alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
259                 $key = "";
260                 for($i=0; $i<16; $i++) {
261                         $offset = rand(0,strlen($alphabet)-1);
262                         //echo "$i off is $offset\n";
263                         $key .= $alphabet[$offset];
264                 }
265                 
266                 return $key;
267         }
268         
269         // returns a hex key
270         function getKey($username) {
271                 $data = $this->internalGetData($username);
272                 $key = $data["tokenkey"];
273                 
274                 return $key;
275         }
276                 
277         // get key type
278         function getTokenType($username) {
279                 $data = $this->internalGetData($username);
280                 $toktype = $data["tokentype"];
281                 
282                 return $toktype;
283         }
284         
285         
286         // TODO: lots of error checking goes in here
287         function helperb322hex($b32) {
288         $alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
289
290         $out = "";
291         $dous = "";
292
293         for($i = 0; $i < strlen($b32); $i++) {
294                 $in = strrpos($alphabet, $b32[$i]);
295                 $b = str_pad(base_convert($in, 10, 2), 5, "0", STR_PAD_LEFT);
296             $out .= $b;
297             $dous .= $b.".";
298         }
299
300         $ar = str_split($out,20);
301
302         //echo "$dous, $b\n";
303
304         //print_r($ar);
305         $out2 = "";
306         foreach($ar as $val) {
307                 $rv = str_pad(base_convert($val, 2, 16), 5, "0", STR_PAD_LEFT);
308                 //echo "rv: $rv from $val\n";
309                 $out2 .= $rv;
310
311         }
312         //echo "$out2\n";
313
314         return $out2;
315         }
316         
317         // TODO: lots of error checking goes in here
318         function helperhex2b32($hex) {
319         $alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
320
321         $ar = str_split($hex, 5);
322
323         $out = "";
324         foreach($ar as $var) {
325                 $bc = base_convert($var, 16, 2);
326                 $bin = str_pad($bc, 20, "0", STR_PAD_LEFT);
327                 $out .= $bin;
328                 //echo "$bc was, $var is, $bin are\n";
329         }
330
331         $out2 = "";
332         $ar2 = str_split($out, 5);
333         foreach($ar2 as $var2) {
334                 $bc = base_convert($var2, 2, 10);
335                 $out2 .= $alphabet[$bc];
336         }
337
338         return $out2;
339         }
340         
341         // i've put alot of faith in the code from the
342         // php site's examples for the hash_hmac algorithm
343         // i assume its mostly correct but i should do
344         // some testing to verify this is actually the case
345         function oath_hotp($key, $counter)
346         {
347                 $key = pack("H*", $key);
348             $cur_counter = array(0,0,0,0,0,0,0,0);
349             for($i=7;$i>=0;$i--)
350             {
351                 $cur_counter[$i] = pack ('C*', $counter);
352                 $counter = $counter >> 8;
353             }
354             $bin_counter = implode($cur_counter);
355             // Pad to 8 chars
356             if (strlen ($bin_counter) < 8)
357             {
358                 $bin_counter = str_repeat (chr(0), 8 - strlen ($bin_counter)) . $bin_counter;
359             }
360         
361             // HMAC
362             $hash = hash_hmac ('sha1', $bin_counter, $key);
363             return str_pad($this->oath_truncate($hash), 6, "0", STR_PAD_LEFT);
364         }
365         
366         
367         function oath_truncate($hash, $length = 6)
368         {
369             // Convert to dec
370             foreach(str_split($hash,2) as $hex)
371             {
372                 $hmac_result[]=hexdec($hex);
373             }
374         
375             // Find offset
376             $offset = $hmac_result[19] & 0xf;
377         
378             // Algorithm from RFC
379             return
380             (
381                 (($hmac_result[$offset+0] & 0x7f) << 24 ) |
382                 (($hmac_result[$offset+1] & 0xff) << 16 ) |
383                 (($hmac_result[$offset+2] & 0xff) << 8 ) |
384                 ($hmac_result[$offset+3] & 0xff)
385             ) % pow(10,$length);
386         }
387         
388         
389         // some private data bits.
390         private $getDatafunction;
391         private $putDatafunction;
392         private $errorText;
393         private $errorCode;
394         
395         private $hotpSkew;
396         private $totpSkew;
397         
398         private $hotpHuntValue;
399         
400         
401         /*
402          * error codes
403          * 1: Auth Failed
404          * 2: No Key
405          * 3: input code was invalid (user input an invalid code - must be 6 numerical digits)
406          * 4: user doesnt exist?
407          * 5: key invalid
408          */
409 }
410 ?>