Completed
Push — master ( da4c36...9fc4f7 )
by Antonio Carlos
02:03
created

Google2FA::generateHotp()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

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