Completed
Push — master ( 523e95...9392c6 )
by Antonio Carlos
05:07
created

Google2FA::getTimestamp()   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
c 0
b 0
f 0
rs 10
cc 1
eloc 2
nc 1
nop 0
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
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...
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);
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 266 which is incompatible with the return type declared by the interface PragmaRX\Google2FA\Contr...ogle2FA::verifyKeyNewer of type boolean|integer.
Loading history...
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