Completed
Push — master ( 9392c6...d181b5 )
by Antonio Carlos
02:25
created

Google2FA::setWindow()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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