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