Completed
Push — master ( f73f7c...547dff )
by Antonio Carlos
02:07
created

Google2FA::getCurrentOtp()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
c 0
b 0
f 0
rs 9.4285
cc 1
eloc 4
nc 1
nop 1
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 Base32\Base32;
34
use BaconQrCode\Writer;
35
use PragmaRX\Google2FA\Support\Url;
36
use BaconQrCode\Renderer\Image\Png;
37
use PragmaRX\Google2FA\Contracts\Google2FA as Google2FAContract;
38
use PragmaRX\Google2FA\Exceptions\IncompatibleWithGoogleAuthenticatorException;
39
use PragmaRX\Google2FA\Exceptions\InvalidCharactersException;
40
use PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException;
41
42
class Google2FA implements Google2FAContract
43
{
44
    /**
45
     * Characters valid for Base 32.
46
     */
47
    const VALID_FOR_B32 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
48
49
    /**
50
     * Length of the Token generated.
51
     */
52
    private $oneTimePasswordLength = 6;
53
54
    /**
55
     * Interval between key regeneration.
56
     */
57
    private $keyRegeneration = 30;
58
59
    /**
60
     * Enforce Google Authenticator compatibility.
61
     */
62
    private $enforceGoogleAuthenticatorCompatibility = true;
63
64
    /**
65
     * Secret
66
     */
67
    private $secret;
68
69
    /**
70
     * Window
71
     */
72
    private $window = 1; // Keys will be valid for 60 seconds
73
74
    /**
75
     * Check if all secret key characters are valid.
76
     *
77
     * @param $b32
78
     *
79
     * @throws InvalidCharactersException
80
     */
81
    private function checkForValidCharacters($b32)
82
    {
83
        if (!preg_match('/^['.static::VALID_FOR_B32.']+$/', $b32, $match)) {
84
            throw new InvalidCharactersException();
85
        }
86
    }
87
88
    /**
89
     * Check if the secret key is compatible with Google Authenticator.
90
     *
91
     * @param $b32
92
     *
93
     * @throws IncompatibleWithGoogleAuthenticatorException
94
     */
95
    private function checkGoogleAuthenticatorCompatibility($b32)
96
    {
97
        if ($this->enforceGoogleAuthenticatorCompatibility && ((strlen($b32) & (strlen($b32) - 1)) !== 0)) {
98
            throw new IncompatibleWithGoogleAuthenticatorException();
99
        }
100
    }
101
102
    /**
103
     * Generate a digit secret key in base32 format.
104
     *
105
     * @param int $length
106
     *
107
     * @return string
108
     */
109
    public function generateSecretKey($length = 16, $prefix = '')
110
    {
111
        $b32 = '234567QWERTYUIOPASDFGHJKLZXCVBNM';
112
113
        $secret = $prefix ? $this->toBase32($prefix) : '';
114
115
        for ($i = 0; $i < $length; $i++) {
116
            $secret .= $b32[$this->getRandomNumber()];
117
        }
118
119
        $this->validateSecret($secret);
120
121
        return $secret;
122
    }
123
124
    /**
125
     * Get key regeneration.
126
     *
127
     * @return mixed
128
     */
129
    public function getKeyRegeneration()
130
    {
131
        return $this->keyRegeneration;
132
    }
133
134
    /**
135
     * Get OTP length.
136
     *
137
     * @return mixed
138
     */
139
    public function getOneTimePasswordLength()
140
    {
141
        return $this->oneTimePasswordLength;
142
    }
143
144
    /**
145
     * Get secret.
146
     *
147
     * @return mixed
148
     */
149
    public function getSecret($secret = null)
150
    {
151
        return
152
            is_null($secret)
153
            ? $this->secret
154
            : $secret
155
        ;
156
    }
157
158
    /**
159
     * Returns the current Unix Timestamp divided by the $keyRegeneration
160
     * period.
161
     *
162
     * @return int
163
     **/
164
    public function getTimestamp()
165
    {
166
        return floor(microtime(true) / $this->keyRegeneration);
167
    }
168
169
    /**
170
     * Decodes a base32 string into a binary string.
171
     *
172
     * @param string $b32
173
     *
174
     * @throws InvalidCharactersException
175
     *
176
     * @return int
177
     */
178
    public function base32Decode($b32)
179
    {
180
        $b32 = strtoupper($b32);
181
182
        $this->validateSecret($b32);
183
184
        return Base32::decode($b32);
185
    }
186
187
    /**
188
     * Get the OTP window.
189
     *
190
     * @return mixed
191
     */
192
    public function getWindow($window = null)
193
    {
194
        return
195
            is_null($window)
196
                ? $this->window
197
                : $window
198
        ;
199
    }
200
201
    /**
202
     * Get/use a starting timestamp for key verification.
203
     *
204
     * @param $useTimestamp
205
     * @return int
206
     */
207
    private function makeStartingTimestamp($useTimestamp)
208
    {
209
        if ($useTimestamp !== true) {
210
            return (int) $useTimestamp;
211
        }
212
213
        return $this->getTimestamp();
214
    }
215
216
    /**
217
     * Takes the secret key and the timestamp and returns the one time
218
     * password.
219
     *
220
     * @param string $key     - Secret key in binary form.
221
     * @param int    $counter - Timestamp as returned by getTimestamp.
222
     *
223
     * @throws SecretKeyTooShortException
224
     *
225
     * @return string
226
     */
227
    public function oathHotp($key, $counter)
228
    {
229
        if (strlen($key) < 8) {
230
            throw new SecretKeyTooShortException();
231
        }
232
233
        // Counter must be 64-bit int
234
        $bin_counter = pack('N*', 0, $counter);
235
236
        $hash = hash_hmac('sha1', $bin_counter, $key, true);
237
238
        return str_pad($this->oathTruncate($hash), $this->getOneTimePasswordLength(), '0', STR_PAD_LEFT);
239
    }
240
241
    /**
242
     * Get the current one time password for a key.
243
     *
244
     * @param string $initalizationKey
245
     *
246
     * @throws InvalidCharactersException
247
     * @throws SecretKeyTooShortException
248
     *
249
     * @return string
250
     */
251
    public function getCurrentOtp($initalizationKey)
252
    {
253
        $timestamp = $this->getTimestamp();
254
255
        $secretKey = $this->base32Decode($initalizationKey);
256
257
        return $this->oathHotp($secretKey, $timestamp);
258
    }
259
260
    /**
261
     * Setter for the enforce Google Authenticator compatibility property.
262
     *
263
     * @param mixed $enforceGoogleAuthenticatorCompatibility
264
     *
265
     * @return $this
266
     */
267
    public function setEnforceGoogleAuthenticatorCompatibility($enforceGoogleAuthenticatorCompatibility)
268
    {
269
        $this->enforceGoogleAuthenticatorCompatibility = $enforceGoogleAuthenticatorCompatibility;
270
271
        return $this;
272
    }
273
274
    /**
275
     * Set key regeneration.
276
     *
277
     * @param mixed $keyRegeneration
278
     */
279
    public function setKeyRegeneration($keyRegeneration)
280
    {
281
        $this->keyRegeneration = $keyRegeneration;
282
    }
283
284
    /**
285
     * Set OTP length.
286
     *
287
     * @param mixed $oneTimePasswordLength
288
     */
289
    public function setOneTimePasswordLength($oneTimePasswordLength)
290
    {
291
        $this->oneTimePasswordLength = $oneTimePasswordLength;
292
    }
293
294
    /**
295
     * Set secret.
296
     *
297
     * @param mixed $secret
298
     */
299
    public function setSecret($secret)
300
    {
301
        $this->secret = $secret;
302
    }
303
304
    /**
305
     * Set the OTP window.
306
     *
307
     * @param mixed $window
308
     */
309
    public function setWindow($window)
310
    {
311
        $this->window = $window;
312
    }
313
314
    /**
315
     * Verifies a user inputted key against the current timestamp. Checks $window
316
     * keys either side of the timestamp.
317
     *
318
     * @param string   $key - User specified key
319
     * @param null|string   $secret
320
     * @param null|int      $window
321
     * @param bool|int $useTimestamp
322
     * @param null|int $oldTimestamp
323
     * @return bool|int
324
     */
325
    public function verify($key, $secret = null, $window = null, $useTimestamp = true, $oldTimestamp = null)
326
    {
327
        return $this->verifyKey(
328
            $this->getSecret($secret),
329
            $key,
330
            $window,
331
            $useTimestamp,
332
            $oldTimestamp
333
        );
334
    }
335
336
    /**
337
     * Verifies a user inputted key against the current timestamp. Checks $window
338
     * keys either side of the timestamp.
339
     *
340
     * @param string   $secret
341
     * @param string   $key - User specified key
342
     * @param null|int $window
343
     * @param bool|int $useTimestamp
344
     * @param null|int $oldTimestamp
345
     * @return bool|int
346
     */
347
    public function verifyKey($secret, $key, $window = null, $useTimestamp = true, $oldTimestamp = null)
348
    {
349
        $timestamp = $this->makeStartingTimestamp($useTimestamp);
350
351
        $binarySeed = $this->base32Decode($secret);
352
353
        $ts = is_null($oldTimestamp)
354
                ? $timestamp - $this->getWindow($window)
355
                : max($timestamp - $this->getWindow($window), $oldTimestamp);
356
357
        for (; $ts <= $timestamp + $this->getWindow($window); $ts++) {
358
            if (hash_equals($this->oathHotp($binarySeed, $ts), $key)) {
359
                return
0 ignored issues
show
Bug Best Practice introduced by
The return type of return is_null($oldTimestamp) ? true : $ts; (integer|double|boolean) is incompatible with the return type declared by the interface PragmaRX\Google2FA\Contracts\Google2FA::verifyKey of type boolean.

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:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
360
                    is_null($oldTimestamp)
361
                        ? true
362
                        : $ts
363
                ;
364
            }
365
        }
366
367
        return false;
368
    }
369
370
    /**
371
     * Verifies a user inputted key against the current timestamp. Checks $window
372
     * keys either side of the timestamp, but ensures that the given key is newer than
373
     * the given oldTimestamp. Useful if you need to ensure that a single key cannot
374
     * be used twice.
375
     *
376
     * @param string $secret
377
     * @param string $key          - User specified key
378
     * @param int    $oldTimestamp - The timestamp from the last verified key
379
     * @param int    $window
380
     * @param bool   $useTimestamp
381
     *
382
     * @return bool|int - false (not verified) or the timestamp of the verified key
383
     **/
384
    public function verifyKeyNewer($secret, $key, $oldTimestamp, $window = null, $useTimestamp = true)
385
    {
386
        return $this->verifyKey($secret, $key, $window, $useTimestamp, $oldTimestamp);
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->verifyKey($secret...estamp, $oldTimestamp); of type integer|double|boolean adds the type double to the return on line 386 which is incompatible with the return type declared by the interface PragmaRX\Google2FA\Contr...ogle2FA::verifyKeyNewer of type boolean|integer.
Loading history...
387
    }
388
389
    /**
390
     * Extracts the OTP from the SHA1 hash.
391
     *
392
     * @param string $hash
393
     *
394
     * @return int
395
     **/
396
    public function oathTruncate($hash)
397
    {
398
        $offset = ord($hash[19]) & 0xf;
399
        $temp = unpack('N', substr($hash, $offset, 4));
400
401
        return substr($temp[1] & 0x7fffffff, -$this->getOneTimePasswordLength());
402
    }
403
404
    /**
405
     * Remove invalid chars from a base 32 string.
406
     *
407
     * @param $string
408
     *
409
     * @return mixed
410
     */
411
    public function removeInvalidChars($string)
412
    {
413
        return preg_replace('/[^'.static::VALID_FOR_B32.']/', '', $string);
414
    }
415
416
    /**
417
     * Creates a Google QR code url.
418
     *
419
     * @param string $company
420
     * @param string $holder
421
     * @param string $secret
422
     * @param int    $size
423
     *
424
     * @return string
425
     */
426
    public function getQRCodeGoogleUrl($company, $holder, $secret, $size = 200)
427
    {
428
        $url = $this->getQRCodeUrl($company, $holder, $secret);
429
430
        return Url::generateGoogleQRCodeUrl('https://chart.googleapis.com/', 'chart', 'chs='.$size.'x'.$size.'&chld=M|0&cht=qr&chl=', $url);
431
    }
432
433
    /**
434
     * Generates a QR code data url to display inline.
435
     *
436
     * @param string $company
437
     * @param string $holder
438
     * @param string $secret
439
     * @param int    $size
440
     * @param string $encoding Default to UTF-8
441
     *
442
     * @return string
443
     */
444
    public function getQRCodeInline($company, $holder, $secret, $size = 200, $encoding = 'utf-8')
445
    {
446
        $url = $this->getQRCodeUrl($company, $holder, $secret);
447
448
        $renderer = new Png();
449
        $renderer->setWidth($size);
450
        $renderer->setHeight($size);
451
452
        $writer = new Writer($renderer);
453
        $data = $writer->writeString($url, $encoding);
454
455
        return 'data:image/png;base64,'.base64_encode($data);
456
    }
457
458
    /**
459
     * Creates a QR code url.
460
     *
461
     * @param $company
462
     * @param $holder
463
     * @param $secret
464
     *
465
     * @return string
466
     */
467
    public function getQRCodeUrl($company, $holder, $secret)
468
    {
469
        return 'otpauth://totp/'.rawurlencode($company).':'.$holder.'?secret='.$secret.'&issuer='.rawurlencode($company).'';
470
    }
471
472
    /**
473
     * Get a random number.
474
     *
475
     * @param $from
476
     * @param $to
477
     *
478
     * @return int
479
     */
480
    private function getRandomNumber($from = 0, $to = 31)
481
    {
482
        return random_int($from, $to);
483
    }
484
485
    /**
486
     * Validate the secret.
487
     *
488
     * @param $b32
489
     */
490
    private function validateSecret($b32)
491
    {
492
        $this->checkForValidCharacters($b32);
493
494
        $this->checkGoogleAuthenticatorCompatibility($b32);
495
    }
496
497
    /**
498
     * Encode a string to Base32.
499
     *
500
     * @param $string
501
     *
502
     * @return mixed
503
     */
504
    public function toBase32($string)
505
    {
506
        $encoded = Base32::encode($string);
507
508
        return str_replace('=', '', $encoded);
509
    }
510
511
    /**
512
     * Get the key regeneration time in seconds.
513
     *
514
     * @return int
515
     */
516
    public function getKeyRegenerationTime()
517
    {
518
        return $this->keyRegeneration;
519
    }
520
}
521