Passed
Push — master ( f4fe32...17c969 )
by Antonio Carlos
03:03
created

Google2FA::findValidOTP()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 23
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4.0312

Importance

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