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