Completed
Push — master ( 5090c5...f73f7c )
by Antonio Carlos
02:02
created

Google2FA::getQRCodeGoogleUrl()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
c 0
b 0
f 0
rs 9.4285
cc 1
eloc 3
nc 1
nop 4
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
     * 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
     * Secret
66
     */
67
    private $secret;
68
69
    /**
70
     * Window
71
     */
72
    private $window = 1;
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 secret.
126
     *
127
     * @return mixed
128
     */
129
    public function getSecret($secret = null)
130
    {
131
        return
132
            is_null($secret)
133
            ? $this->secret
134
            : $secret
135
        ;
136
    }
137
138
    /**
139
     * Returns the current Unix Timestamp divided by the KEY_REGENERATION
140
     * period.
141
     *
142
     * @return int
143
     **/
144
    public function getTimestamp()
145
    {
146
        return floor(microtime(true) / static::KEY_REGENERATION);
147
    }
148
149
    /**
150
     * Decodes a base32 string into a binary string.
151
     *
152
     * @param string $b32
153
     *
154
     * @throws InvalidCharactersException
155
     *
156
     * @return int
157
     */
158
    public function base32Decode($b32)
159
    {
160
        $b32 = strtoupper($b32);
161
162
        $this->validateSecret($b32);
163
164
        return Base32::decode($b32);
165
    }
166
167
    /**
168
     * Get the OTP window.
169
     *
170
     * @return mixed
171
     */
172
    public function getWindow($window = null)
173
    {
174
        return
175
            is_null($window)
176
                ? $this->window
177
                : $window
178
        ;
179
    }
180
181
    /**
182
     * Get/use a starting timestamp for key verification.
183
     *
184
     * @param $useTimestamp
185
     * @return int
186
     */
187
    private function makeStartingTimestamp($useTimestamp)
188
    {
189
        if ($useTimestamp !== true) {
190
            return (int) $useTimestamp;
191
        }
192
193
        return $this->getTimestamp();
194
    }
195
196
    /**
197
     * Takes the secret key and the timestamp and returns the one time
198
     * password.
199
     *
200
     * @param string $key     - Secret key in binary form.
201
     * @param int    $counter - Timestamp as returned by getTimestamp.
202
     *
203
     * @throws SecretKeyTooShortException
204
     *
205
     * @return string
206
     */
207
    public function oathHotp($key, $counter)
208
    {
209
        if (strlen($key) < 8) {
210
            throw new SecretKeyTooShortException();
211
        }
212
213
        // Counter must be 64-bit int
214
        $bin_counter = pack('N*', 0, $counter);
215
216
        $hash = hash_hmac('sha1', $bin_counter, $key, true);
217
218
        return str_pad($this->oathTruncate($hash), static::OPT_LENGTH, '0', STR_PAD_LEFT);
219
    }
220
221
    /**
222
     * Get the current one time password for a key.
223
     *
224
     * @param string $initalizationKey
225
     *
226
     * @throws InvalidCharactersException
227
     * @throws SecretKeyTooShortException
228
     *
229
     * @return string
230
     */
231
    public function getCurrentOtp($initalizationKey)
232
    {
233
        $timestamp = $this->getTimestamp();
234
235
        $secretKey = $this->base32Decode($initalizationKey);
236
237
        return $this->oathHotp($secretKey, $timestamp);
238
    }
239
240
    /**
241
     * Setter for the enforce Google Authenticator compatibility property.
242
     *
243
     * @param mixed $enforceGoogleAuthenticatorCompatibility
244
     *
245
     * @return $this
246
     */
247
    public function setEnforceGoogleAuthenticatorCompatibility($enforceGoogleAuthenticatorCompatibility)
248
    {
249
        $this->enforceGoogleAuthenticatorCompatibility = $enforceGoogleAuthenticatorCompatibility;
250
251
        return $this;
252
    }
253
254
    /**
255
     * Set secret.
256
     *
257
     * @param mixed $secret
258
     */
259
    public function setSecret($secret)
260
    {
261
        $this->secret = $secret;
262
    }
263
264
    /**
265
     * Set the OTP window.
266
     *
267
     * @param mixed $window
268
     */
269
    public function setWindow($window)
270
    {
271
        $this->window = $window;
272
    }
273
274
    /**
275
     * Verifies a user inputted key against the current timestamp. Checks $window
276
     * keys either side of the timestamp.
277
     *
278
     * @param string   $key - User specified key
279
     * @param null|string   $secret
280
     * @param null|int      $window
281
     * @param bool|int $useTimestamp
282
     * @param null|int $oldTimestamp
283
     * @return bool|int
284
     */
285
    public function verify($key, $secret = null, $window = null, $useTimestamp = true, $oldTimestamp = null)
286
    {
287
        return $this->verifyKey(
288
            $this->getSecret($secret),
289
            $key,
290
            $window,
291
            $useTimestamp,
292
            $oldTimestamp
293
        );
294
    }
295
296
    /**
297
     * Verifies a user inputted key against the current timestamp. Checks $window
298
     * keys either side of the timestamp.
299
     *
300
     * @param string   $secret
301
     * @param string   $key - User specified key
302
     * @param null|int $window
303
     * @param bool|int $useTimestamp
304
     * @param null|int $oldTimestamp
305
     * @return bool|int
306
     */
307
    public function verifyKey($secret, $key, $window = null, $useTimestamp = true, $oldTimestamp = null)
308
    {
309
        $timestamp = $this->makeStartingTimestamp($useTimestamp);
310
311
        $binarySeed = $this->base32Decode($secret);
312
313
        $ts = is_null($oldTimestamp)
314
                ? $timestamp - $this->getWindow($window)
315
                : max($timestamp - $this->getWindow($window), $oldTimestamp);
316
317
        for (; $ts <= $timestamp + $this->getWindow($window); $ts++) {
318
            if (hash_equals($this->oathHotp($binarySeed, $ts), $key)) {
319
                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...
320
                    is_null($oldTimestamp)
321
                        ? true
322
                        : $ts
323
                ;
324
            }
325
        }
326
327
        return false;
328
    }
329
330
    /**
331
     * Verifies a user inputted key against the current timestamp. Checks $window
332
     * keys either side of the timestamp, but ensures that the given key is newer than
333
     * the given oldTimestamp. Useful if you need to ensure that a single key cannot
334
     * be used twice.
335
     *
336
     * @param string $secret
337
     * @param string $key          - User specified key
338
     * @param int    $oldTimestamp - The timestamp from the last verified key
339
     * @param int    $window
340
     * @param bool   $useTimestamp
341
     *
342
     * @return bool|int - false (not verified) or the timestamp of the verified key
343
     **/
344
    public function verifyKeyNewer($secret, $key, $oldTimestamp, $window = null, $useTimestamp = true)
345
    {
346
        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 346 which is incompatible with the return type declared by the interface PragmaRX\Google2FA\Contr...ogle2FA::verifyKeyNewer of type boolean|integer.
Loading history...
347
    }
348
349
    /**
350
     * Extracts the OTP from the SHA1 hash.
351
     *
352
     * @param string $hash
353
     *
354
     * @return int
355
     **/
356
    public function oathTruncate($hash)
357
    {
358
        $offset = ord($hash[19]) & 0xf;
359
        $temp = unpack('N', substr($hash, $offset, 4));
360
361
        return substr($temp[1] & 0x7fffffff, -static::OPT_LENGTH);
362
    }
363
364
    /**
365
     * Remove invalid chars from a base 32 string.
366
     *
367
     * @param $string
368
     *
369
     * @return mixed
370
     */
371
    public function removeInvalidChars($string)
372
    {
373
        return preg_replace('/[^'.static::VALID_FOR_B32.']/', '', $string);
374
    }
375
376
    /**
377
     * Creates a Google QR code url.
378
     *
379
     * @param string $company
380
     * @param string $holder
381
     * @param string $secret
382
     * @param int    $size
383
     *
384
     * @return string
385
     */
386
    public function getQRCodeGoogleUrl($company, $holder, $secret, $size = 200)
387
    {
388
        $url = $this->getQRCodeUrl($company, $holder, $secret);
389
390
        return Url::generateGoogleQRCodeUrl('https://chart.googleapis.com/', 'chart', 'chs='.$size.'x'.$size.'&chld=M|0&cht=qr&chl=', $url);
391
    }
392
393
    /**
394
     * Generates a QR code data url to display inline.
395
     *
396
     * @param string $company
397
     * @param string $holder
398
     * @param string $secret
399
     * @param int    $size
400
     * @param string $encoding Default to UTF-8
401
     *
402
     * @return string
403
     */
404
    public function getQRCodeInline($company, $holder, $secret, $size = 200, $encoding = 'utf-8')
405
    {
406
        $url = $this->getQRCodeUrl($company, $holder, $secret);
407
408
        $renderer = new Png();
409
        $renderer->setWidth($size);
410
        $renderer->setHeight($size);
411
412
        $writer = new Writer($renderer);
413
        $data = $writer->writeString($url, $encoding);
414
415
        return 'data:image/png;base64,'.base64_encode($data);
416
    }
417
418
    /**
419
     * Creates a QR code url.
420
     *
421
     * @param $company
422
     * @param $holder
423
     * @param $secret
424
     *
425
     * @return string
426
     */
427
    public function getQRCodeUrl($company, $holder, $secret)
428
    {
429
        return 'otpauth://totp/'.rawurlencode($company).':'.$holder.'?secret='.$secret.'&issuer='.rawurlencode($company).'';
430
    }
431
432
    /**
433
     * Get a random number.
434
     *
435
     * @param $from
436
     * @param $to
437
     *
438
     * @return int
439
     */
440
    private function getRandomNumber($from = 0, $to = 31)
441
    {
442
        return random_int($from, $to);
443
    }
444
445
    /**
446
     * Validate the secret.
447
     *
448
     * @param $b32
449
     */
450
    private function validateSecret($b32)
451
    {
452
        $this->checkForValidCharacters($b32);
453
454
        $this->checkGoogleAuthenticatorCompatibility($b32);
455
    }
456
457
    /**
458
     * Encode a string to Base32.
459
     *
460
     * @param $string
461
     *
462
     * @return mixed
463
     */
464
    public function toBase32($string)
465
    {
466
        $encoded = Base32::encode($string);
467
468
        return str_replace('=', '', $encoded);
469
    }
470
471
    /**
472
     * Get the key regeneration time in seconds.
473
     *
474
     * @return int
475
     */
476
    public function getKeyRegenerationTime()
477
    {
478
        return static::KEY_REGENERATION;
479
    }
480
}
481