Completed
Push — master ( dab064...06100c )
by Antonio Carlos
02:08
created

Google2FA::verifyKey()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 2 Features 0
Metric Value
c 4
b 2
f 0
dl 0
loc 15
rs 9.4285
cc 2
eloc 11
nc 2
nop 5
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 PragmaRX\Google2FA\Exceptions\IncompatibleWithGoogleAuthenticatorException;
34
use PragmaRX\Google2FA\Exceptions\InvalidCharactersException;
35
use PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException;
36
use PragmaRX\Google2FA\Support\Base32;
37
use PragmaRX\Google2FA\Support\QRCode;
38
39
class Google2FA
40
{
41
    use QRCode, Base32;
42
43
    /**
44
     * Characters valid for Base 32.
45
     */
46
    const VALID_FOR_B32 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
47
48
    /**
49
     * Length of the Token generated.
50
     */
51
    protected $oneTimePasswordLength = 6;
52
53
    /**
54
     * Interval between key regeneration.
55
     */
56
    protected $keyRegeneration = 30;
57
58
    /**
59
     * Enforce Google Authenticator compatibility.
60
     */
61
    protected $enforceGoogleAuthenticatorCompatibility = true;
62
63
    /**
64
     * Secret.
65
     */
66
    protected $secret;
67
68
    /**
69
     * Window.
70
     */
71
    protected $window = 1; // Keys will be valid for 60 seconds
72
73
    /**
74
     * Check if all secret key characters are valid.
75
     *
76
     * @param $b32
77
     *
78
     * @throws InvalidCharactersException
79
     */
80
    protected function checkForValidCharacters($b32)
81
    {
82
        if (!preg_match('/^['.static::VALID_FOR_B32.']+$/', $b32, $match)) {
83
            throw new InvalidCharactersException();
84
        }
85
    }
86
87
    /**
88
     * Check if the secret key is compatible with Google Authenticator.
89
     *
90
     * @param $b32
91
     *
92
     * @throws IncompatibleWithGoogleAuthenticatorException
93
     */
94
    protected function checkGoogleAuthenticatorCompatibility($b32)
95
    {
96
        if ($this->enforceGoogleAuthenticatorCompatibility && ((strlen($b32) & (strlen($b32) - 1)) !== 0)) {
97
            throw new IncompatibleWithGoogleAuthenticatorException();
98
        }
99
    }
100
101
    /**
102
     * Generate a digit secret key in base32 format.
103
     *
104
     * @param int $length
105
     *
106
     * @return string
107
     */
108
    public function generateSecretKey($length = 16, $prefix = '')
109
    {
110
        return $this->generateBase32RandomKey($length, $prefix);
111
    }
112
113
    /**
114
     * Get key regeneration.
115
     *
116
     * @return mixed
117
     */
118
    public function getKeyRegeneration()
119
    {
120
        return $this->keyRegeneration;
121
    }
122
123
    /**
124
     * Get OTP length.
125
     *
126
     * @return mixed
127
     */
128
    public function getOneTimePasswordLength()
129
    {
130
        return $this->oneTimePasswordLength;
131
    }
132
133
    /**
134
     * Get secret.
135
     *
136
     * @return mixed
137
     */
138
    public function getSecret($secret = null)
139
    {
140
        return
141
            is_null($secret)
142
            ? $this->secret
143
            : $secret;
144
    }
145
146
    /**
147
     * Returns the current Unix Timestamp divided by the $keyRegeneration
148
     * period.
149
     *
150
     * @return int
151
     **/
152
    public function getTimestamp()
153
    {
154
        return (int) floor(microtime(true) / $this->keyRegeneration);
155
    }
156
157
    /**
158
     * Get the OTP window.
159
     *
160
     * @return mixed
161
     */
162
    public function getWindow($window = null)
163
    {
164
        return
165
            is_null($window)
166
                ? $this->window
167
                : $window;
168
    }
169
170
    /**
171
     * Get/use a starting timestamp for key verification.
172
     *
173
     * @param string|int|null $timestamp
174
     *
175
     * @return int
176
     */
177
    protected function makeTimestamp($timestamp = null)
178
    {
179
        if (is_null($timestamp)) {
180
            return $this->getTimestamp();
181
        }
182
183
        return (int) $timestamp;
184
    }
185
186
    /**
187
     * Takes the secret key and the timestamp and returns the one time
188
     * password.
189
     *
190
     * @param string $key     - Secret key in binary form.
191
     * @param int    $counter - Timestamp as returned by getTimestamp.
192
     *
193
     * @throws SecretKeyTooShortException
194
     *
195
     * @return string
196
     */
197
    public function oathHotp($key, $counter)
198
    {
199
        if (strlen($key) < 8) {
200
            throw new SecretKeyTooShortException();
201
        }
202
203
        // Counter must be 64-bit int
204
        $bin_counter = pack('N*', 0, $counter);
205
206
        $hash = hash_hmac('sha1', $bin_counter, $key, true);
207
208
        return str_pad($this->oathTruncate($hash), $this->getOneTimePasswordLength(), '0', STR_PAD_LEFT);
209
    }
210
211
    /**
212
     * Get the current one time password for a key.
213
     *
214
     * @param string $initalizationKey
215
     *
216
     * @throws InvalidCharactersException
217
     * @throws SecretKeyTooShortException
218
     *
219
     * @return string
220
     */
221
    public function getCurrentOtp($initalizationKey)
222
    {
223
        $timestamp = $this->getTimestamp();
224
225
        $secretKey = $this->base32Decode($initalizationKey);
226
227
        return $this->oathHotp($secretKey, $timestamp);
228
    }
229
230
    /**
231
     * Setter for the enforce Google Authenticator compatibility property.
232
     *
233
     * @param mixed $enforceGoogleAuthenticatorCompatibility
234
     *
235
     * @return $this
236
     */
237
    public function setEnforceGoogleAuthenticatorCompatibility($enforceGoogleAuthenticatorCompatibility)
238
    {
239
        $this->enforceGoogleAuthenticatorCompatibility = $enforceGoogleAuthenticatorCompatibility;
240
241
        return $this;
242
    }
243
244
    /**
245
     * Set key regeneration.
246
     *
247
     * @param mixed $keyRegeneration
248
     */
249
    public function setKeyRegeneration($keyRegeneration)
250
    {
251
        $this->keyRegeneration = $keyRegeneration;
252
    }
253
254
    /**
255
     * Set OTP length.
256
     *
257
     * @param mixed $oneTimePasswordLength
258
     */
259
    public function setOneTimePasswordLength($oneTimePasswordLength)
260
    {
261
        $this->oneTimePasswordLength = $oneTimePasswordLength;
262
    }
263
264
    /**
265
     * Set secret.
266
     *
267
     * @param mixed $secret
268
     */
269
    public function setSecret($secret)
270
    {
271
        $this->secret = $secret;
272
    }
273
274
    /**
275
     * Set the OTP window.
276
     *
277
     * @param mixed $window
278
     */
279
    public function setWindow($window)
280
    {
281
        $this->window = $window;
282
    }
283
284
    /**
285
     * Verifies a user inputted key against the current timestamp. Checks $window
286
     * keys either side of the timestamp.
287
     *
288
     * @param string      $key          - User specified key
289
     * @param null|string $secret
290
     * @param null|int    $window
291
     * @param null|int    $timestamp
292
     * @param null|int    $oldTimestamp
293
     *
294
     * @return bool|int
295
     */
296
    public function verify($key, $secret = null, $window = null, $timestamp = null, $oldTimestamp = null)
297
    {
298
        return $this->verifyKey(
299
            $secret,
300
            $key,
301
            $window,
302
            $timestamp,
303
            $oldTimestamp
304
        );
305
    }
306
307
    /**
308
     * Verifies a user inputted key against the current timestamp. Checks $window
309
     * keys either side of the timestamp.
310
     *
311
     * @param string   $secret
312
     * @param string   $key          - User specified key
313
     * @param null|int $window
314
     * @param null|int $timestamp
315
     * @param null|int $oldTimestamp
316
     *
317
     * @return bool|int
318
     */
319
    public function verifyKey($secret, $key, $window = null, $timestamp = null, $oldTimestamp = null)
320
    {
321
        $startingTimestamp = is_null($oldTimestamp)
322
            ? ($timestamp = $this->makeTimestamp($timestamp)) - $this->getWindow($window)
323
            : max($timestamp - $this->getWindow($window), $oldTimestamp);
324
325
       return $this->findValidOTP(
326
           $this->base32Decode($this->getSecret($secret)),
327
           $key,
328
           $window,
329
           $startingTimestamp,
330
           $timestamp,
331
           $oldTimestamp
332
       );
333
    }
334
335
    public function findValidOTP($binarySeed, $key, $window, $startingTimestamp, $timestamp, $oldTimestamp)
336
    {
337
        for (; $startingTimestamp <= $timestamp + $this->getWindow($window); $startingTimestamp++) {
338
            if (hash_equals($this->oathHotp($binarySeed, $startingTimestamp), $key)) {
339
                return
340
                    is_null($oldTimestamp)
341
                        ? true
342
                        : $startingTimestamp;
343
            }
344
        }
345
346
        return false;
347
    }
348
349
    /**
350
     * Verifies a user inputted key against the current timestamp. Checks $window
351
     * keys either side of the timestamp, but ensures that the given key is newer than
352
     * the given oldTimestamp. Useful if you need to ensure that a single key cannot
353
     * be used twice.
354
     *
355
     * @param string   $secret
356
     * @param string   $key          - User specified key
357
     * @param int      $oldTimestamp - The timestamp from the last verified key
358
     * @param int|null $window
359
     * @param int|null $timestamp
360
     *
361
     * @return bool|int - false (not verified) or the timestamp of the verified key
362
     **/
363
    public function verifyKeyNewer($secret, $key, $oldTimestamp, $window = null, $timestamp = null)
364
    {
365
        return $this->verifyKey($secret, $key, $window, $timestamp, $oldTimestamp);
366
    }
367
368
    /**
369
     * Extracts the OTP from the SHA1 hash.
370
     *
371
     * @param string $hash
372
     *
373
     * @return int
374
     **/
375
    public function oathTruncate($hash)
376
    {
377
        $offset = ord($hash[19]) & 0xf;
378
        $temp = unpack('N', substr($hash, $offset, 4));
379
380
        return substr($temp[1] & 0x7fffffff, -$this->getOneTimePasswordLength());
381
    }
382
383
    /**
384
     * Remove invalid chars from a base 32 string.
385
     *
386
     * @param $string
387
     *
388
     * @return mixed
389
     */
390
    public function removeInvalidChars($string)
391
    {
392
        return preg_replace('/[^'.static::VALID_FOR_B32.']/', '', $string);
393
    }
394
395
    /**
396
     * Get a random number.
397
     *
398
     * @param $from
399
     * @param $to
400
     *
401
     * @return int
402
     */
403
    protected function getRandomNumber($from = 0, $to = 31)
404
    {
405
        return random_int($from, $to);
406
    }
407
408
    /**
409
     * Validate the secret.
410
     *
411
     * @param $b32
412
     */
413
    protected function validateSecret($b32)
414
    {
415
        $this->checkForValidCharacters($b32);
416
417
        $this->checkGoogleAuthenticatorCompatibility($b32);
418
    }
419
420
    /**
421
     * Get the key regeneration time in seconds.
422
     *
423
     * @return int
424
     */
425
    public function getKeyRegenerationTime()
426
    {
427
        return $this->keyRegeneration;
428
    }
429
}
430