|
1
|
|
|
<?php |
|
2
|
|
|
|
|
3
|
|
|
namespace PragmaRX\Google2FA; |
|
4
|
|
|
|
|
5
|
|
|
/* |
|
6
|
|
|
* This program is free software: you can redistribute it and/or modify |
|
7
|
|
|
* it under the terms of the GNU General Public License as published by |
|
8
|
|
|
* the Free Software Foundation, either version 3 of the License, or |
|
9
|
|
|
* (at your option) any later version. |
|
10
|
|
|
* |
|
11
|
|
|
* This program is distributed in the hope that it will be useful, |
|
12
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
13
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
14
|
|
|
* GNU General Public License for more details. |
|
15
|
|
|
* |
|
16
|
|
|
* You should have received a copy of the GNU General Public License |
|
17
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
18
|
|
|
* |
|
19
|
|
|
* PHP Google two-factor authentication module. |
|
20
|
|
|
* |
|
21
|
|
|
* See http://www.idontplaydarts.com/2011/07/google-totp-two-factor-authentication-for-php/ |
|
22
|
|
|
* for more details |
|
23
|
|
|
* |
|
24
|
|
|
* @author Phil (Orginal author of this class) |
|
25
|
|
|
* |
|
26
|
|
|
* Changes have been made in the original class to remove all static methods and, also, |
|
27
|
|
|
* provide some other methods. |
|
28
|
|
|
* |
|
29
|
|
|
* @package Google2FA |
|
30
|
|
|
* @author Antonio Carlos Ribeiro @ PragmaRX |
|
31
|
|
|
**/ |
|
32
|
|
|
|
|
33
|
|
|
use BaconQrCode\Renderer\Image\Png; |
|
34
|
|
|
use BaconQrCode\Writer; |
|
35
|
|
|
use Base32\Base32; |
|
36
|
|
|
use PragmaRX\Google2FA\Contracts\Google2FA as Google2FAContract; |
|
37
|
|
|
use PragmaRX\Google2FA\Exceptions\IncompatibleWithGoogleAuthenticatorException; |
|
38
|
|
|
use PragmaRX\Google2FA\Exceptions\InvalidCharactersException; |
|
39
|
|
|
use PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException; |
|
40
|
|
|
use PragmaRX\Google2FA\Support\Url; |
|
41
|
|
|
|
|
42
|
|
|
class Google2FA implements Google2FAContract |
|
43
|
|
|
{ |
|
44
|
|
|
/** |
|
45
|
|
|
* Interval between key regeneration. |
|
46
|
|
|
*/ |
|
47
|
|
|
const KEY_REGENERATION = 30; |
|
48
|
|
|
|
|
49
|
|
|
/** |
|
50
|
|
|
* Length of the Token generated. |
|
51
|
|
|
*/ |
|
52
|
|
|
const OPT_LENGTH = 6; |
|
53
|
|
|
|
|
54
|
|
|
/** |
|
55
|
|
|
* Characters valid for Base 32. |
|
56
|
|
|
*/ |
|
57
|
|
|
const VALID_FOR_B32 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; |
|
58
|
|
|
|
|
59
|
|
|
/** |
|
60
|
|
|
* Enforce Google Authenticator compatibility. |
|
61
|
|
|
*/ |
|
62
|
|
|
private $enforceGoogleAuthenticatorCompatibility = true; |
|
63
|
|
|
|
|
64
|
|
|
/** |
|
65
|
|
|
* Check if all secret key characters are valid. |
|
66
|
|
|
* |
|
67
|
|
|
* @param $b32 |
|
68
|
|
|
* |
|
69
|
|
|
* @throws InvalidCharactersException |
|
70
|
|
|
*/ |
|
71
|
|
|
private function checkForValidCharacters($b32) |
|
72
|
|
|
{ |
|
73
|
|
|
if (!preg_match('/^['.static::VALID_FOR_B32.']+$/', $b32, $match)) { |
|
74
|
|
|
throw new InvalidCharactersException(); |
|
75
|
|
|
} |
|
76
|
|
|
} |
|
77
|
|
|
|
|
78
|
|
|
/** |
|
79
|
|
|
* Check if the secret key is compatible with Google Authenticator. |
|
80
|
|
|
* |
|
81
|
|
|
* @param $b32 |
|
82
|
|
|
* |
|
83
|
|
|
* @throws IncompatibleWithGoogleAuthenticatorException |
|
84
|
|
|
*/ |
|
85
|
|
|
private function checkGoogleAuthenticatorCompatibility($b32) |
|
86
|
|
|
{ |
|
87
|
|
|
if ($this->enforceGoogleAuthenticatorCompatibility && ((strlen($b32) & (strlen($b32) - 1)) !== 0)) { |
|
88
|
|
|
throw new IncompatibleWithGoogleAuthenticatorException(); |
|
89
|
|
|
} |
|
90
|
|
|
} |
|
91
|
|
|
|
|
92
|
|
|
/** |
|
93
|
|
|
* Generate a digit secret key in base32 format. |
|
94
|
|
|
* |
|
95
|
|
|
* @param int $length |
|
96
|
|
|
* |
|
97
|
|
|
* @return string |
|
98
|
|
|
*/ |
|
99
|
|
|
public function generateSecretKey($length = 16, $prefix = '') |
|
100
|
|
|
{ |
|
101
|
|
|
$b32 = '234567QWERTYUIOPASDFGHJKLZXCVBNM'; |
|
102
|
|
|
|
|
103
|
|
|
$secret = $prefix ? $this->toBase32($prefix) : ''; |
|
104
|
|
|
|
|
105
|
|
|
for ($i = 0; $i < $length; $i++) { |
|
106
|
|
|
$secret .= $b32[$this->getRandomNumber()]; |
|
107
|
|
|
} |
|
108
|
|
|
|
|
109
|
|
|
$this->validateSecret($secret); |
|
110
|
|
|
|
|
111
|
|
|
return $secret; |
|
112
|
|
|
} |
|
113
|
|
|
|
|
114
|
|
|
/** |
|
115
|
|
|
* Returns the current Unix Timestamp divided by the KEY_REGENERATION |
|
116
|
|
|
* period. |
|
117
|
|
|
* |
|
118
|
|
|
* @return int |
|
119
|
|
|
**/ |
|
120
|
|
|
public function getTimestamp() |
|
121
|
|
|
{ |
|
122
|
|
|
return floor(microtime(true) / static::KEY_REGENERATION); |
|
123
|
|
|
} |
|
124
|
|
|
|
|
125
|
|
|
/** |
|
126
|
|
|
* Decodes a base32 string into a binary string. |
|
127
|
|
|
* |
|
128
|
|
|
* @param string $b32 |
|
129
|
|
|
* |
|
130
|
|
|
* @throws InvalidCharactersException |
|
131
|
|
|
* |
|
132
|
|
|
* @return int |
|
133
|
|
|
*/ |
|
134
|
|
|
public function base32Decode($b32) |
|
135
|
|
|
{ |
|
136
|
|
|
$b32 = strtoupper($b32); |
|
137
|
|
|
|
|
138
|
|
|
$this->validateSecret($b32); |
|
139
|
|
|
|
|
140
|
|
|
return Base32::decode($b32); |
|
141
|
|
|
} |
|
142
|
|
|
|
|
143
|
|
|
/** |
|
144
|
|
|
* Get/use a starting timestamp for key verification. |
|
145
|
|
|
* |
|
146
|
|
|
* @param $useTimestamp |
|
147
|
|
|
* @return int |
|
148
|
|
|
*/ |
|
149
|
|
|
private function makeStartingTimestamp($useTimestamp) |
|
150
|
|
|
{ |
|
151
|
|
|
if ($useTimestamp !== true) { |
|
152
|
|
|
return (int) $useTimestamp; |
|
153
|
|
|
} |
|
154
|
|
|
|
|
155
|
|
|
return $this->getTimestamp(); |
|
156
|
|
|
} |
|
157
|
|
|
|
|
158
|
|
|
/** |
|
159
|
|
|
* Takes the secret key and the timestamp and returns the one time |
|
160
|
|
|
* password. |
|
161
|
|
|
* |
|
162
|
|
|
* @param string $key - Secret key in binary form. |
|
163
|
|
|
* @param int $counter - Timestamp as returned by getTimestamp. |
|
164
|
|
|
* |
|
165
|
|
|
* @throws SecretKeyTooShortException |
|
166
|
|
|
* |
|
167
|
|
|
* @return string |
|
168
|
|
|
*/ |
|
169
|
|
|
public function oathHotp($key, $counter) |
|
170
|
|
|
{ |
|
171
|
|
|
if (strlen($key) < 8) { |
|
172
|
|
|
throw new SecretKeyTooShortException(); |
|
173
|
|
|
} |
|
174
|
|
|
|
|
175
|
|
|
// Counter must be 64-bit int |
|
176
|
|
|
$bin_counter = pack('N*', 0, $counter); |
|
177
|
|
|
|
|
178
|
|
|
$hash = hash_hmac('sha1', $bin_counter, $key, true); |
|
179
|
|
|
|
|
180
|
|
|
return str_pad($this->oathTruncate($hash), static::OPT_LENGTH, '0', STR_PAD_LEFT); |
|
181
|
|
|
} |
|
182
|
|
|
|
|
183
|
|
|
/** |
|
184
|
|
|
* Get the current one time password for a key. |
|
185
|
|
|
* |
|
186
|
|
|
* @param string $initalizationKey |
|
187
|
|
|
* |
|
188
|
|
|
* @throws InvalidCharactersException |
|
189
|
|
|
* @throws SecretKeyTooShortException |
|
190
|
|
|
* |
|
191
|
|
|
* @return string |
|
192
|
|
|
*/ |
|
193
|
|
|
public function getCurrentOtp($initalizationKey) |
|
194
|
|
|
{ |
|
195
|
|
|
$timestamp = $this->getTimestamp(); |
|
196
|
|
|
|
|
197
|
|
|
$secretKey = $this->base32Decode($initalizationKey); |
|
198
|
|
|
|
|
199
|
|
|
return $this->oathHotp($secretKey, $timestamp); |
|
200
|
|
|
} |
|
201
|
|
|
|
|
202
|
|
|
/** |
|
203
|
|
|
* Setter for the enforce Google Authenticator compatibility property. |
|
204
|
|
|
* |
|
205
|
|
|
* @param mixed $enforceGoogleAuthenticatorCompatibility |
|
206
|
|
|
* |
|
207
|
|
|
* @return $this |
|
208
|
|
|
*/ |
|
209
|
|
|
public function setEnforceGoogleAuthenticatorCompatibility($enforceGoogleAuthenticatorCompatibility) |
|
210
|
|
|
{ |
|
211
|
|
|
$this->enforceGoogleAuthenticatorCompatibility = $enforceGoogleAuthenticatorCompatibility; |
|
212
|
|
|
|
|
213
|
|
|
return $this; |
|
214
|
|
|
} |
|
215
|
|
|
|
|
216
|
|
|
/** |
|
217
|
|
|
* Verifies a user inputted key against the current timestamp. Checks $window |
|
218
|
|
|
* keys either side of the timestamp. |
|
219
|
|
|
* |
|
220
|
|
|
* @param string $b32seed |
|
221
|
|
|
* @param string $key - User specified key |
|
222
|
|
|
* @param int $window |
|
223
|
|
|
* @param bool|int $useTimestamp |
|
224
|
|
|
* @param null|int $oldTimestamp |
|
225
|
|
|
* @return bool|int |
|
226
|
|
|
*/ |
|
227
|
|
|
public function verifyKey($b32seed, $key, $window = 4, $useTimestamp = true, $oldTimestamp = null) |
|
228
|
|
|
{ |
|
229
|
|
|
$timestamp = $this->makeStartingTimestamp($useTimestamp); |
|
230
|
|
|
|
|
231
|
|
|
$binarySeed = $this->base32Decode($b32seed); |
|
232
|
|
|
|
|
233
|
|
|
$ts = is_null($oldTimestamp) |
|
234
|
|
|
? $timestamp - $window |
|
235
|
|
|
: max($timestamp - $window, $oldTimestamp); |
|
236
|
|
|
|
|
237
|
|
|
for (; $ts <= $timestamp + $window; $ts++) { |
|
238
|
|
|
if (hash_equals($this->oathHotp($binarySeed, $ts), $key)) { |
|
239
|
|
|
return |
|
|
|
|
|
|
240
|
|
|
is_null($oldTimestamp) |
|
241
|
|
|
? true |
|
242
|
|
|
: $ts |
|
243
|
|
|
; |
|
244
|
|
|
} |
|
245
|
|
|
} |
|
246
|
|
|
|
|
247
|
|
|
return false; |
|
248
|
|
|
} |
|
249
|
|
|
|
|
250
|
|
|
/** |
|
251
|
|
|
* Verifies a user inputted key against the current timestamp. Checks $window |
|
252
|
|
|
* keys either side of the timestamp, but ensures that the given key is newer than |
|
253
|
|
|
* the given oldTimestamp. Useful if you need to ensure that a single key cannot |
|
254
|
|
|
* be used twice. |
|
255
|
|
|
* |
|
256
|
|
|
* @param string $b32seed |
|
257
|
|
|
* @param string $key - User specified key |
|
258
|
|
|
* @param int $oldTimestamp - The timestamp from the last verified key |
|
259
|
|
|
* @param int $window |
|
260
|
|
|
* @param bool $useTimestamp |
|
261
|
|
|
* |
|
262
|
|
|
* @return bool|int - false (not verified) or the timestamp of the verified key |
|
263
|
|
|
**/ |
|
264
|
|
|
public function verifyKeyNewer($b32seed, $key, $oldTimestamp, $window = 4, $useTimestamp = true) |
|
265
|
|
|
{ |
|
266
|
|
|
return $this->verifyKey($b32seed, $key, $window, $useTimestamp, $oldTimestamp); |
|
|
|
|
|
|
267
|
|
|
} |
|
268
|
|
|
|
|
269
|
|
|
/** |
|
270
|
|
|
* Extracts the OTP from the SHA1 hash. |
|
271
|
|
|
* |
|
272
|
|
|
* @param string $hash |
|
273
|
|
|
* |
|
274
|
|
|
* @return int |
|
275
|
|
|
**/ |
|
276
|
|
|
public function oathTruncate($hash) |
|
277
|
|
|
{ |
|
278
|
|
|
$offset = ord($hash[19]) & 0xf; |
|
279
|
|
|
$temp = unpack('N', substr($hash, $offset, 4)); |
|
280
|
|
|
|
|
281
|
|
|
return substr($temp[1] & 0x7fffffff, -static::OPT_LENGTH); |
|
282
|
|
|
} |
|
283
|
|
|
|
|
284
|
|
|
/** |
|
285
|
|
|
* Remove invalid chars from a base 32 string. |
|
286
|
|
|
* |
|
287
|
|
|
* @param $string |
|
288
|
|
|
* |
|
289
|
|
|
* @return mixed |
|
290
|
|
|
*/ |
|
291
|
|
|
public function removeInvalidChars($string) |
|
292
|
|
|
{ |
|
293
|
|
|
return preg_replace('/[^'.static::VALID_FOR_B32.']/', '', $string); |
|
294
|
|
|
} |
|
295
|
|
|
|
|
296
|
|
|
/** |
|
297
|
|
|
* Creates a Google QR code url. |
|
298
|
|
|
* |
|
299
|
|
|
* @param string $company |
|
300
|
|
|
* @param string $holder |
|
301
|
|
|
* @param string $secret |
|
302
|
|
|
* @param int $size |
|
303
|
|
|
* |
|
304
|
|
|
* @return string |
|
305
|
|
|
*/ |
|
306
|
|
|
public function getQRCodeGoogleUrl($company, $holder, $secret, $size = 200) |
|
307
|
|
|
{ |
|
308
|
|
|
$url = $this->getQRCodeUrl($company, $holder, $secret); |
|
309
|
|
|
|
|
310
|
|
|
return Url::generateGoogleQRCodeUrl('https://chart.googleapis.com/', 'chart', 'chs='.$size.'x'.$size.'&chld=M|0&cht=qr&chl=', $url); |
|
311
|
|
|
} |
|
312
|
|
|
|
|
313
|
|
|
/** |
|
314
|
|
|
* Generates a QR code data url to display inline. |
|
315
|
|
|
* |
|
316
|
|
|
* @param string $company |
|
317
|
|
|
* @param string $holder |
|
318
|
|
|
* @param string $secret |
|
319
|
|
|
* @param int $size |
|
320
|
|
|
* @param string $encoding Default to UTF-8 |
|
321
|
|
|
* |
|
322
|
|
|
* @return string |
|
323
|
|
|
*/ |
|
324
|
|
|
public function getQRCodeInline($company, $holder, $secret, $size = 200, $encoding = 'utf-8') |
|
325
|
|
|
{ |
|
326
|
|
|
$url = $this->getQRCodeUrl($company, $holder, $secret); |
|
327
|
|
|
|
|
328
|
|
|
$renderer = new Png(); |
|
329
|
|
|
$renderer->setWidth($size); |
|
330
|
|
|
$renderer->setHeight($size); |
|
331
|
|
|
|
|
332
|
|
|
$writer = new Writer($renderer); |
|
333
|
|
|
$data = $writer->writeString($url, $encoding); |
|
334
|
|
|
|
|
335
|
|
|
return 'data:image/png;base64,'.base64_encode($data); |
|
336
|
|
|
} |
|
337
|
|
|
|
|
338
|
|
|
/** |
|
339
|
|
|
* Creates a QR code url. |
|
340
|
|
|
* |
|
341
|
|
|
* @param $company |
|
342
|
|
|
* @param $holder |
|
343
|
|
|
* @param $secret |
|
344
|
|
|
* |
|
345
|
|
|
* @return string |
|
346
|
|
|
*/ |
|
347
|
|
|
public function getQRCodeUrl($company, $holder, $secret) |
|
348
|
|
|
{ |
|
349
|
|
|
return 'otpauth://totp/'.rawurlencode($company).':'.$holder.'?secret='.$secret.'&issuer='.rawurlencode($company).''; |
|
350
|
|
|
} |
|
351
|
|
|
|
|
352
|
|
|
/** |
|
353
|
|
|
* Get a random number. |
|
354
|
|
|
* |
|
355
|
|
|
* @param $from |
|
356
|
|
|
* @param $to |
|
357
|
|
|
* |
|
358
|
|
|
* @return int |
|
359
|
|
|
*/ |
|
360
|
|
|
private function getRandomNumber($from = 0, $to = 31) |
|
361
|
|
|
{ |
|
362
|
|
|
return random_int($from, $to); |
|
363
|
|
|
} |
|
364
|
|
|
|
|
365
|
|
|
/** |
|
366
|
|
|
* Validate the secret. |
|
367
|
|
|
* |
|
368
|
|
|
* @param $b32 |
|
369
|
|
|
*/ |
|
370
|
|
|
private function validateSecret($b32) |
|
371
|
|
|
{ |
|
372
|
|
|
$this->checkForValidCharacters($b32); |
|
373
|
|
|
|
|
374
|
|
|
$this->checkGoogleAuthenticatorCompatibility($b32); |
|
375
|
|
|
} |
|
376
|
|
|
|
|
377
|
|
|
/** |
|
378
|
|
|
* Encode a string to Base32. |
|
379
|
|
|
* |
|
380
|
|
|
* @param $string |
|
381
|
|
|
* |
|
382
|
|
|
* @return mixed |
|
383
|
|
|
*/ |
|
384
|
|
|
public function toBase32($string) |
|
385
|
|
|
{ |
|
386
|
|
|
$encoded = Base32::encode($string); |
|
387
|
|
|
|
|
388
|
|
|
return str_replace('=', '', $encoded); |
|
389
|
|
|
} |
|
390
|
|
|
|
|
391
|
|
|
/** |
|
392
|
|
|
* Get the key regeneration time in seconds. |
|
393
|
|
|
* |
|
394
|
|
|
* @return int |
|
395
|
|
|
*/ |
|
396
|
|
|
public function getKeyRegenerationTime() |
|
397
|
|
|
{ |
|
398
|
|
|
return static::KEY_REGENERATION; |
|
399
|
|
|
} |
|
400
|
|
|
} |
|
401
|
|
|
|
If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.
Let’s take a look at an example:
Our function
my_functionexpects aPostobject, and outputs the author of the post. The base classPostreturns a simple string and outputting a simple string will work just fine. However, the child classBlogPostwhich is a sub-type ofPostinstead decided to return anobject, and is therefore violating the SOLID principles. If aBlogPostwere passed tomy_function, PHP would not complain, but ultimately fail when executing thestrtouppercall in its body.