1<?php
2/** https://github.com/Voronenko/PHPOTP/blob/08cda9cb9c30b7242cf0b3a9100a6244a2874927/code/base32static.php
3 * Encode in Base32 based on RFC 4648.
4 * Requires 20% more space than base64
5 * Great for case-insensitive filesystems like Windows and URL's (except for = char which can be excluded using the pad option for urls)
6 *
7 * @package default
8 * @author Bryan Ruiz
9 **/
10class Base32Static {
11
12 private static $map = array(
13 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', // 7
14 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // 15
15 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', // 23
16 'Y', 'Z', '2', '3', '4', '5', '6', '7', // 31
17 '=' // padding character
18 );
19
20 private static $flippedMap = array(
21 'A'=>'0', 'B'=>'1', 'C'=>'2', 'D'=>'3', 'E'=>'4', 'F'=>'5', 'G'=>'6', 'H'=>'7',
22 'I'=>'8', 'J'=>'9', 'K'=>'10', 'L'=>'11', 'M'=>'12', 'N'=>'13', 'O'=>'14', 'P'=>'15',
23 'Q'=>'16', 'R'=>'17', 'S'=>'18', 'T'=>'19', 'U'=>'20', 'V'=>'21', 'W'=>'22', 'X'=>'23',
24 'Y'=>'24', 'Z'=>'25', '2'=>'26', '3'=>'27', '4'=>'28', '5'=>'29', '6'=>'30', '7'=>'31'
25 );
26
27 /**
28 * Use padding false when encoding for urls
29 *
30 * @return string base32 encoded string
31 * @author Bryan Ruiz
32 **/
33 public static function encode($input, $padding = true) {
34 if(empty($input)) return "";
35
36 $input = str_split($input);
37 $binaryString = "";
38
39 for($i = 0; $i < count($input); $i++) {
40 $binaryString .= str_pad(base_convert(ord($input[$i]), 10, 2), 8, '0', STR_PAD_LEFT);
41 }
42
43 $fiveBitBinaryArray = str_split($binaryString, 5);
44 $base32 = "";
45 $i=0;
46
47 while($i < count($fiveBitBinaryArray)) {
48 $base32 .= self::$map[base_convert(str_pad($fiveBitBinaryArray[$i], 5,'0'), 2, 10)];
49 $i++;
50 }
51
52 if($padding && ($x = strlen($binaryString) % 40) != 0) {
53 if($x == 8) $base32 .= str_repeat(self::$map[32], 6);
54 else if($x == 16) $base32 .= str_repeat(self::$map[32], 4);
55 else if($x == 24) $base32 .= str_repeat(self::$map[32], 3);
56 else if($x == 32) $base32 .= self::$map[32];
57 }
58
59 return $base32;
60 }
61
62 public static function decode($input) {
63 if (!is_string($input) || $input === '') return false;
64
65 $input = strtoupper($input);
66 if (!preg_match('/^[A-Z2-7]+={0,6}$/', $input)) return false;
67
68 $paddingCharCount = substr_count($input, self::$map[32]);
69 if (!in_array($paddingCharCount, array(6, 4, 3, 1, 0), true)) return false;
70 if ($paddingCharCount > 0 && strlen($input) % 8 !== 0) return false;
71
72 $input = rtrim($input, self::$map[32]);
73 if (!in_array(strlen($input) % 8, array(0, 2, 4, 5, 7), true)) return false;
74
75 $binaryString = '';
76 $buffer = 0;
77 $bufferBits = 0;
78
79 foreach (str_split($input) as $character) {
80 $buffer = ($buffer << 5) | (int)self::$flippedMap[$character];
81 $bufferBits += 5;
82
83 if ($bufferBits >= 8) {
84 $bufferBits -= 8;
85 $binaryString .= chr(($buffer >> $bufferBits) & 0xff);
86 $buffer &= $bufferBits > 0 ? (1 << $bufferBits) - 1 : 0;
87 }
88 }
89
90 return $binaryString;
91 }
92}
93
94// http://www.faqs.org/rfcs/rfc6238.html
95// https://github.com/Voronenko/PHPOTP/blob/08cda9cb9c30b7242cf0b3a9100a6244a2874927/code/rfc6238.php
96// Local changes: http -> https, consistent indentation, 200x200 -> 300x300 QR image size, PHP end tag
97class TokenAuth6238 {
98
99 /**
100 * verify
101 *
102 * @param string $secretkey Secret clue (base 32).
103 * @return bool True if success, false if failure
104 */
105 public static function verify($secretkey, $code, $rangein30s = 3) {
106 $key = Base32Static::decode($secretkey);
107 $unixtimestamp = intdiv(time(), 30);
108
109 for($i=-($rangein30s); $i<=$rangein30s; $i++) {
110 $checktime = (int)($unixtimestamp+$i);
111 $thiskey = self::oath_hotp($key, $checktime);
112
113 if (hash_equals(
114 str_pad((string)$code, 6, '0', STR_PAD_LEFT),
115 str_pad((string)self::oath_truncate($thiskey, 6), 6, '0', STR_PAD_LEFT)
116 )) {
117 return true;
118 }
119
120 }
121 return false;
122 }
123
124
125 public static function getTokenCode($secretkey,$rangein30s = 3) {
126 $result = "";
127 $key = Base32Static::decode($secretkey);
128 $unixtimestamp = intdiv(time(), 30);
129
130 for($i=-($rangein30s); $i<=$rangein30s; $i++) {
131 $checktime = (int)($unixtimestamp+$i);
132 $thiskey = self::oath_hotp($key, $checktime);
133 $result = $result." # ".self::oath_truncate($thiskey,6);
134 }
135
136 return $result;
137 }
138
139 public static function getTokenCodeDebug($secretkey,$rangein30s = 3) {
140 $result = "";
141 print "<br/>SecretKey: $secretkey <br/>";
142
143 $key = Base32Static::decode($secretkey);
144 print "Key(base 32 decode): $key <br/>";
145
146 $unixtimestamp = intdiv(time(), 30);
147 print "UnixTimeStamp (time()/30): $unixtimestamp <br/>";
148
149 for($i=-($rangein30s); $i<=$rangein30s; $i++) {
150 $checktime = (int)($unixtimestamp+$i);
151 print "Calculating oath_hotp from (int)(unixtimestamp +- 30sec offset): $checktime basing on secret key<br/>";
152
153 $thiskey = self::oath_hotp($key, $checktime, true);
154 print "======================================================<br/>";
155 print "CheckTime: $checktime oath_hotp:".$thiskey."<br/>";
156
157 $result = $result." # ".self::oath_truncate($thiskey,6,true);
158 }
159
160 return $result;
161 }
162
163 public static function getBarCodeUrl($username, $domain, $secretkey, $issuer) {
164 $label = rawurlencode($username . '@' . $domain);
165 $issuer = rawurlencode($issuer);
166
167 $otpauth = "otpauth://totp/{$label}?secret={$secretkey}&issuer={$issuer}&algorithm=SHA1&digits=6&period=30";
168
169 return 'https://api.qrserver.com/v1/create-qr-code/?' . http_build_query([
170 'size' => '300x300',
171 'data' => $otpauth
172 ]);
173 }
174
175 public static function generateRandomClue($length = 16) {
176 $b32 = "234567QWERTYUIOPASDFGHJKLZXCVBNM";
177 $s = "";
178 for ($i = 0; $i < $length; $i++) {
179 $s .= $b32[random_int(0, 31)];
180 }
181 return $s;
182 }
183
184 private static function hotp_tobytestream($key) {
185 $result = array();
186 $last = strlen($key);
187 for ($i = 0; $i < $last; $i = $i + 2) {
188 $x = $key[$i] + $key[$i + 1];
189 $x = strtoupper($x);
190 $x = hexdec($x);
191 $result = $result.chr($x);
192 }
193
194 return $result;
195 }
196
197 private static function oath_hotp ($key, $counter, $debug=false) {
198 $result = "";
199 $orgcounter = $counter;
200 $cur_counter = array(0,0,0,0,0,0,0,0);
201
202 if ($debug) {
203 print "Packing counter $counter (".dechex($counter).")into binary string - pay attention to hex representation of key and binary representation<br/>";
204 }
205
206 for($i=7;$i>=0;$i--) { // C for unsigned char, * for repeating to the end of the input data
207 $cur_counter[$i] = pack ('C*', $counter);
208
209 if ($debug) {
210 print $cur_counter[$i]."(".dechex(ord($cur_counter[$i])).")"." from $counter <br/>";
211 }
212
213 $counter = $counter >> 8;
214 }
215
216 if ($debug) {
217 foreach ($cur_counter as $char) {
218 print ord($char) . " ";
219 }
220
221 print "<br/>";
222 }
223
224 $binary = implode($cur_counter);
225
226 // Pad to 8 characters
227 str_pad($binary, 8, chr(0), STR_PAD_LEFT);
228
229 if ($debug) {
230 print "Prior to HMAC calculation pad with zero on the left until 8 characters.<br/>";
231 print "Calculate sha1 HMAC(Hash-based Message Authentication Code http://en.wikipedia.org/wiki/HMAC).<br/>";
232 print "hash_hmac ('sha1', $binary, $key)<br/>";
233 }
234
235 $result = hash_hmac ('sha1', $binary, $key);
236
237 if ($debug) {
238 print "Result: $result <br/>";
239 }
240
241 return $result;
242 }
243
244 private static function oath_truncate($hash, $length = 6, $debug=false) {
245 $result="";
246
247 // Convert to dec
248 if($debug) {
249 print "converting hex hash into characters<br/>";
250 }
251
252 $hashcharacters = str_split($hash,2);
253
254 if($debug) {
255 print_r($hashcharacters);
256 print "<br/>and convert to decimals:<br/>";
257 }
258
259 for ($j=0; $j<count($hashcharacters); $j++) {
260 $hmac_result[]=hexdec($hashcharacters[$j]);
261 }
262
263 if($debug) {
264 print_r($hmac_result);
265 }
266
267 // http://php.net/manual/ru/function.hash-hmac.php
268 // adopted from brent at thebrent dot net 21-May-2009 08:17 comment
269
270 $offset = $hmac_result[19] & 0xf;
271
272 if($debug) {
273 print "Calculating offset as 19th element of hmac:".$hmac_result[19]."<br/>";
274 print "offset:".$offset;
275 }
276
277 $result = (
278 (($hmac_result[$offset+0] & 0x7f) << 24 ) |
279 (($hmac_result[$offset+1] & 0xff) << 16 ) |
280 (($hmac_result[$offset+2] & 0xff) << 8 ) |
281 ($hmac_result[$offset+3] & 0xff)
282 ) % pow(10,$length);
283
284 return $result;
285 }
286}
287?>
288