|
1
|
|
|
<?php |
|
2
|
|
|
|
|
3
|
|
|
namespace TOTP; |
|
4
|
|
|
|
|
5
|
|
|
/** |
|
6
|
|
|
* A class for generating the codes compatible with the Google Authenticator and similar TOTP |
|
7
|
|
|
* clients. |
|
8
|
|
|
* |
|
9
|
|
|
* NOTE: A lot of the logic from this class has been borrowed from this class: |
|
10
|
|
|
* https://www.idontplaydarts.com/wp-content/uploads/2011/07/ga.php_.txt |
|
11
|
|
|
* |
|
12
|
|
|
* @author Chris Cornutt <[email protected]> |
|
13
|
|
|
* @package GAuth |
|
14
|
|
|
* @license MIT |
|
15
|
|
|
* |
|
16
|
|
|
* Simple Machines Forum (SMF) |
|
17
|
|
|
* |
|
18
|
|
|
* @package SMF |
|
19
|
|
|
* @author Simple Machines http://www.simplemachines.org |
|
20
|
|
|
* @copyright 2017 Simple Machines and individual contributors |
|
21
|
|
|
* @license http://www.simplemachines.org/about/smf/license.php BSD |
|
22
|
|
|
* |
|
23
|
|
|
* @version 2.1 Beta 4 |
|
24
|
|
|
*/ |
|
25
|
|
|
|
|
26
|
|
|
/** |
|
27
|
|
|
* Class Auth |
|
28
|
|
|
* @package TOTP |
|
29
|
|
|
*/ |
|
30
|
|
|
class Auth |
|
31
|
|
|
{ |
|
32
|
|
|
/** |
|
33
|
|
|
* @var array Internal lookup table |
|
34
|
|
|
*/ |
|
35
|
|
|
private $lookup = array(); |
|
36
|
|
|
|
|
37
|
|
|
/** |
|
38
|
|
|
* @var string Initialization key |
|
39
|
|
|
*/ |
|
40
|
|
|
private $initKey = null; |
|
41
|
|
|
|
|
42
|
|
|
/** |
|
43
|
|
|
* @var integer Seconds between key refreshes |
|
44
|
|
|
*/ |
|
45
|
|
|
private $refreshSeconds = 30; |
|
46
|
|
|
|
|
47
|
|
|
/** |
|
48
|
|
|
* @var integer The length of codes to generate |
|
49
|
|
|
*/ |
|
50
|
|
|
private $codeLength = 6; |
|
51
|
|
|
|
|
52
|
|
|
/** |
|
53
|
|
|
* @var integer Range plus/minus for "window of opportunity" on allowed codes |
|
54
|
|
|
*/ |
|
55
|
|
|
private $range = 2; |
|
56
|
|
|
|
|
57
|
|
|
/** |
|
58
|
|
|
* Initialize the object and set up the lookup table |
|
59
|
|
|
* Optionally the Initialization key |
|
60
|
|
|
* |
|
61
|
|
|
* @param string $initKey Initialization key |
|
62
|
|
|
*/ |
|
63
|
|
|
public function __construct($initKey = null) |
|
64
|
|
|
{ |
|
65
|
|
|
$this->buildLookup(); |
|
66
|
|
|
|
|
67
|
|
|
if ($initKey !== null) { |
|
68
|
|
|
$this->setInitKey($initKey); |
|
69
|
|
|
} |
|
70
|
|
|
} |
|
71
|
|
|
|
|
72
|
|
|
/** |
|
73
|
|
|
* Build the base32 lookup table |
|
74
|
|
|
*/ |
|
75
|
|
|
public function buildLookup() |
|
76
|
|
|
{ |
|
77
|
|
|
$lookup = array_combine( |
|
78
|
|
|
array_merge(range('A', 'Z'), range(2, 7)), |
|
79
|
|
|
range(0, 31) |
|
80
|
|
|
); |
|
81
|
|
|
$this->setLookup($lookup); |
|
82
|
|
|
} |
|
83
|
|
|
|
|
84
|
|
|
/** |
|
85
|
|
|
* Get the current "range" value |
|
86
|
|
|
* @return integer Range value |
|
87
|
|
|
*/ |
|
88
|
|
|
public function getRange() |
|
89
|
|
|
{ |
|
90
|
|
|
return $this->range; |
|
91
|
|
|
} |
|
92
|
|
|
|
|
93
|
|
|
/** |
|
94
|
|
|
* Set the "range" value |
|
95
|
|
|
* |
|
96
|
|
|
* @param integer $range Range value |
|
97
|
|
|
* @return \TOTP\Auth instance |
|
98
|
|
|
*/ |
|
99
|
|
|
public function setRange($range) |
|
100
|
|
|
{ |
|
101
|
|
|
if (!is_numeric($range)) { |
|
102
|
|
|
throw new \InvalidArgumentException('Invalid window range'); |
|
103
|
|
|
} |
|
104
|
|
|
$this->range = $range; |
|
|
|
|
|
|
105
|
|
|
return $this; |
|
106
|
|
|
} |
|
107
|
|
|
|
|
108
|
|
|
/** |
|
109
|
|
|
* Set the initialization key for the object |
|
110
|
|
|
* |
|
111
|
|
|
* @param string $key Initialization key |
|
112
|
|
|
* @throws \InvalidArgumentException If hash is not valid base32 |
|
113
|
|
|
* @return \TOTP\Auth instance |
|
114
|
|
|
*/ |
|
115
|
|
|
public function setInitKey($key) |
|
116
|
|
|
{ |
|
117
|
|
View Code Duplication |
if (preg_match('/^[' . implode('', array_keys($this->getLookup())) . ']+$/', $key) == false) { |
|
|
|
|
|
|
118
|
|
|
throw new \InvalidArgumentException('Invalid base32 hash!'); |
|
119
|
|
|
} |
|
120
|
|
|
$this->initKey = $key; |
|
121
|
|
|
return $this; |
|
122
|
|
|
} |
|
123
|
|
|
|
|
124
|
|
|
/** |
|
125
|
|
|
* Get the current Initialization key |
|
126
|
|
|
* |
|
127
|
|
|
* @return string Initialization key |
|
128
|
|
|
*/ |
|
129
|
|
|
public function getInitKey() |
|
130
|
|
|
{ |
|
131
|
|
|
return $this->initKey; |
|
132
|
|
|
} |
|
133
|
|
|
|
|
134
|
|
|
/** |
|
135
|
|
|
* Set the contents of the internal lookup table |
|
136
|
|
|
* |
|
137
|
|
|
* @param array $lookup Lookup data set |
|
138
|
|
|
* @throws \InvalidArgumentException If lookup given is not an array |
|
139
|
|
|
* @return \TOTP\Auth instance |
|
140
|
|
|
*/ |
|
141
|
|
|
public function setLookup($lookup) |
|
142
|
|
|
{ |
|
143
|
|
|
if (!is_array($lookup)) { |
|
144
|
|
|
throw new \InvalidArgumentException('Lookup value must be an array'); |
|
145
|
|
|
} |
|
146
|
|
|
$this->lookup = $lookup; |
|
147
|
|
|
return $this; |
|
148
|
|
|
} |
|
149
|
|
|
|
|
150
|
|
|
/** |
|
151
|
|
|
* Get the current lookup data set |
|
152
|
|
|
* |
|
153
|
|
|
* @return array Lookup data |
|
154
|
|
|
*/ |
|
155
|
|
|
public function getLookup() |
|
156
|
|
|
{ |
|
157
|
|
|
return $this->lookup; |
|
158
|
|
|
} |
|
159
|
|
|
|
|
160
|
|
|
/** |
|
161
|
|
|
* Get the number of seconds for code refresh currently set |
|
162
|
|
|
* |
|
163
|
|
|
* @return integer Refresh in seconds |
|
164
|
|
|
*/ |
|
165
|
|
|
public function getRefresh() |
|
166
|
|
|
{ |
|
167
|
|
|
return $this->refreshSeconds; |
|
168
|
|
|
} |
|
169
|
|
|
|
|
170
|
|
|
/** |
|
171
|
|
|
* Set the number of seconds to refresh codes |
|
172
|
|
|
* |
|
173
|
|
|
* @param integer $seconds Seconds to refresh |
|
174
|
|
|
* @throws \InvalidArgumentException If seconds value is not numeric |
|
175
|
|
|
* @return \TOTP\Auth instance |
|
176
|
|
|
*/ |
|
177
|
|
|
public function setRefresh($seconds) |
|
178
|
|
|
{ |
|
179
|
|
|
if (!is_numeric($seconds)) { |
|
180
|
|
|
throw new \InvalidArgumentException('Seconds must be numeric'); |
|
181
|
|
|
} |
|
182
|
|
|
$this->refreshSeconds = $seconds; |
|
|
|
|
|
|
183
|
|
|
return $this; |
|
184
|
|
|
} |
|
185
|
|
|
|
|
186
|
|
|
/** |
|
187
|
|
|
* Get the current length for generated codes |
|
188
|
|
|
* |
|
189
|
|
|
* @return integer Code length |
|
190
|
|
|
*/ |
|
191
|
|
|
public function getCodeLength() |
|
192
|
|
|
{ |
|
193
|
|
|
return $this->codeLength; |
|
194
|
|
|
} |
|
195
|
|
|
|
|
196
|
|
|
/** |
|
197
|
|
|
* Set the length of the generated codes |
|
198
|
|
|
* |
|
199
|
|
|
* @param integer $length Code length |
|
200
|
|
|
* @return \TOTP\Auth instance |
|
201
|
|
|
*/ |
|
202
|
|
|
public function setCodeLength($length) |
|
203
|
|
|
{ |
|
204
|
|
|
$this->codeLength = $length; |
|
205
|
|
|
return $this; |
|
206
|
|
|
} |
|
207
|
|
|
|
|
208
|
|
|
/** |
|
209
|
|
|
* Validate the given code |
|
210
|
|
|
* |
|
211
|
|
|
* @param string $code Code entered by user |
|
212
|
|
|
* @param string $initKey Initialization key |
|
213
|
|
|
* @param string $timestamp Timestamp for calculation |
|
214
|
|
|
* @param integer $range Seconds before/after to validate hash against |
|
215
|
|
|
* @throws \InvalidArgumentException If incorrect code length |
|
216
|
|
|
* @return boolean Pass/fail of validation |
|
217
|
|
|
*/ |
|
218
|
|
|
public function validateCode($code, $initKey = null, $timestamp = null, $range = null) |
|
219
|
|
|
{ |
|
220
|
|
|
if (strlen($code) !== $this->getCodeLength()) { |
|
221
|
|
|
throw new \InvalidArgumentException('Incorrect code length'); |
|
222
|
|
|
} |
|
223
|
|
|
|
|
224
|
|
|
$range = ($range == null) ? $this->getRange() : $range; |
|
|
|
|
|
|
225
|
|
|
$timestamp = ($timestamp == null) ? $this->generateTimestamp() : $timestamp; |
|
|
|
|
|
|
226
|
|
|
$initKey = ($initKey == null) ? $this->getInitKey() : $initKey; |
|
|
|
|
|
|
227
|
|
|
|
|
228
|
|
|
$binary = $this->base32_decode($initKey); |
|
229
|
|
|
|
|
230
|
|
|
for ($time = ($timestamp - $range); $time <= ($timestamp + $range); $time++) { |
|
231
|
|
|
if ($this->generateOneTime($binary, $time) == $code) { |
|
232
|
|
|
return true; |
|
233
|
|
|
} |
|
234
|
|
|
} |
|
235
|
|
|
return false; |
|
236
|
|
|
} |
|
237
|
|
|
|
|
238
|
|
|
/** |
|
239
|
|
|
* Generate a one-time code |
|
240
|
|
|
* |
|
241
|
|
|
* @param string $initKey Initialization key [optional] |
|
242
|
|
|
* @param string $timestamp Timestamp for calculation [optional] |
|
243
|
|
|
* @return string Generated code/hash |
|
244
|
|
|
*/ |
|
245
|
|
|
public function generateOneTime($initKey = null, $timestamp = null) |
|
246
|
|
|
{ |
|
247
|
|
|
$initKey = ($initKey == null) ? $this->getInitKey() : $initKey; |
|
|
|
|
|
|
248
|
|
|
$timestamp = ($timestamp == null) ? $this->generateTimestamp() : $timestamp; |
|
|
|
|
|
|
249
|
|
|
|
|
250
|
|
|
$hash = hash_hmac( |
|
251
|
|
|
'sha1', |
|
252
|
|
|
pack('N*', 0) . pack('N*', $timestamp), |
|
253
|
|
|
$initKey, |
|
254
|
|
|
true |
|
255
|
|
|
); |
|
256
|
|
|
|
|
257
|
|
|
return str_pad($this->truncateHash($hash), $this->getCodeLength(), '0', STR_PAD_LEFT); |
|
258
|
|
|
} |
|
259
|
|
|
|
|
260
|
|
|
/** |
|
261
|
|
|
* Generate a code/hash |
|
262
|
|
|
* Useful for making Initialization codes |
|
263
|
|
|
* |
|
264
|
|
|
* @param integer $length Length for the generated code |
|
265
|
|
|
* @return string Generated code |
|
266
|
|
|
*/ |
|
267
|
|
|
public function generateCode($length = 16) |
|
268
|
|
|
{ |
|
269
|
|
|
$lookup = implode('', array_keys($this->getLookup())); |
|
270
|
|
|
$code = ''; |
|
271
|
|
|
|
|
272
|
|
|
for ($i = 0; $i < $length; $i++) { |
|
273
|
|
|
$code .= $lookup[mt_rand(0, strlen($lookup) - 1)]; |
|
274
|
|
|
} |
|
275
|
|
|
|
|
276
|
|
|
return $code; |
|
277
|
|
|
} |
|
278
|
|
|
|
|
279
|
|
|
/** |
|
280
|
|
|
* Generate the timestamp for the calculation |
|
281
|
|
|
* |
|
282
|
|
|
* @return integer Timestamp |
|
283
|
|
|
*/ |
|
284
|
|
|
public function generateTimestamp() |
|
285
|
|
|
{ |
|
286
|
|
|
return floor(microtime(true) / $this->getRefresh()); |
|
287
|
|
|
} |
|
288
|
|
|
|
|
289
|
|
|
/** |
|
290
|
|
|
* Truncate the given hash down to just what we need |
|
291
|
|
|
* |
|
292
|
|
|
* @param string $hash Hash to truncate |
|
293
|
|
|
* @return string Truncated hash value |
|
294
|
|
|
*/ |
|
295
|
|
|
public function truncateHash($hash) |
|
296
|
|
|
{ |
|
297
|
|
|
$offset = ord($hash[19]) & 0xf; |
|
298
|
|
|
|
|
299
|
|
|
return ( |
|
300
|
|
|
((ord($hash[$offset + 0]) & 0x7f) << 24) | |
|
301
|
|
|
((ord($hash[$offset + 1]) & 0xff) << 16) | |
|
302
|
|
|
((ord($hash[$offset + 2]) & 0xff) << 8) | |
|
303
|
|
|
(ord($hash[$offset + 3]) & 0xff) |
|
304
|
|
|
) % pow(10, $this->getCodeLength()); |
|
305
|
|
|
} |
|
306
|
|
|
|
|
307
|
|
|
/** |
|
308
|
|
|
* Base32 decoding function |
|
309
|
|
|
* |
|
310
|
|
|
* @param string $hash The base32-encoded hash |
|
311
|
|
|
* @throws \InvalidArgumentException When hash is not valid |
|
312
|
|
|
* @return string Binary value of hash |
|
313
|
|
|
*/ |
|
314
|
|
|
public function base32_decode($hash) |
|
315
|
|
|
{ |
|
316
|
|
|
$lookup = $this->getLookup(); |
|
317
|
|
|
|
|
318
|
|
View Code Duplication |
if (preg_match('/^[' . implode('', array_keys($lookup)) . ']+$/', $hash) == false) { |
|
|
|
|
|
|
319
|
|
|
throw new \InvalidArgumentException('Invalid base32 hash!'); |
|
320
|
|
|
} |
|
321
|
|
|
|
|
322
|
|
|
$hash = strtoupper($hash); |
|
323
|
|
|
$buffer = 0; |
|
324
|
|
|
$length = 0; |
|
325
|
|
|
$binary = ''; |
|
326
|
|
|
|
|
327
|
|
|
for ($i = 0; $i < strlen($hash); $i++) { |
|
328
|
|
|
$buffer = $buffer << 5; |
|
329
|
|
|
$buffer += $lookup[$hash[$i]]; |
|
330
|
|
|
$length += 5; |
|
331
|
|
|
|
|
332
|
|
|
if ($length >= 8) { |
|
333
|
|
|
$length -= 8; |
|
334
|
|
|
$binary .= chr(($buffer & (0xFF << $length)) >> $length); |
|
335
|
|
|
} |
|
336
|
|
|
} |
|
337
|
|
|
|
|
338
|
|
|
return $binary; |
|
339
|
|
|
} |
|
340
|
|
|
|
|
341
|
|
|
/** |
|
342
|
|
|
* Returns a URL to QR code for embedding the QR code |
|
343
|
|
|
* |
|
344
|
|
|
* @param string $name The name |
|
345
|
|
|
* @param string $code The generated code |
|
346
|
|
|
* @return string The URL to the QR code |
|
347
|
|
|
*/ |
|
348
|
|
|
public function getQrCodeUrl($name, $code) |
|
349
|
|
|
{ |
|
350
|
|
|
$urlencoded = urlencode('otpauth://totp/' . urlencode($name) . '?secret=' . $code); |
|
351
|
|
|
return 'https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl=' . $urlencoded; |
|
352
|
|
|
} |
|
353
|
|
|
} |
|
354
|
|
|
|
|
355
|
|
|
?> |
|
|
|
|
|
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountIdthat can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theidproperty of an instance of theAccountclass. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.