Completed
Push — master ( 635b37...9d1f6a )
by Antonio Carlos
02:14
created

Google2FA::getSecret()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 7
rs 9.4285
cc 2
eloc 5
nc 2
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 PragmaRX\Google2FA\Support\Base32;
34
use PragmaRX\Google2FA\Support\QRCode;
35
use PragmaRX\Google2FA\Exceptions\InvalidCharactersException;
36
use PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException;
37
use PragmaRX\Google2FA\Exceptions\IncompatibleWithGoogleAuthenticatorException;
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 makeStartingTimestamp($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
        $timestamp = $this->makeStartingTimestamp($timestamp);
322
323
        $binarySeed = $this->base32Decode($this->getSecret($secret));
324
325
        $ts = is_null($oldTimestamp)
326
                ? $timestamp - $this->getWindow($window)
327
                : max($timestamp - $this->getWindow($window), $oldTimestamp);
328
329
        for (; $ts <= $timestamp + $this->getWindow($window); $ts++) {
330
            if (hash_equals($this->oathHotp($binarySeed, $ts), $key)) {
331
                return
332
                    is_null($oldTimestamp)
333
                        ? true
334
                        : $ts;
335
            }
336
        }
337
338
        return false;
339
    }
340
341
    /**
342
     * Verifies a user inputted key against the current timestamp. Checks $window
343
     * keys either side of the timestamp, but ensures that the given key is newer than
344
     * the given oldTimestamp. Useful if you need to ensure that a single key cannot
345
     * be used twice.
346
     *
347
     * @param string   $secret
348
     * @param string   $key          - User specified key
349
     * @param int      $oldTimestamp - The timestamp from the last verified key
350
     * @param int|null $window
351
     * @param int|null $timestamp
352
     *
353
     * @return bool|int - false (not verified) or the timestamp of the verified key
354
     **/
355
    public function verifyKeyNewer($secret, $key, $oldTimestamp, $window = null, $timestamp = null)
356
    {
357
        return $this->verifyKey($secret, $key, $window, $timestamp, $oldTimestamp);
358
    }
359
360
    /**
361
     * Extracts the OTP from the SHA1 hash.
362
     *
363
     * @param string $hash
364
     *
365
     * @return int
366
     **/
367
    public function oathTruncate($hash)
368
    {
369
        $offset = ord($hash[19]) & 0xf;
370
        $temp = unpack('N', substr($hash, $offset, 4));
371
372
        return substr($temp[1] & 0x7fffffff, -$this->getOneTimePasswordLength());
373
    }
374
375
    /**
376
     * Remove invalid chars from a base 32 string.
377
     *
378
     * @param $string
379
     *
380
     * @return mixed
381
     */
382
    public function removeInvalidChars($string)
383
    {
384
        return preg_replace('/[^'.static::VALID_FOR_B32.']/', '', $string);
385
    }
386
387
    /**
388
     * Get a random number.
389
     *
390
     * @param $from
391
     * @param $to
392
     *
393
     * @return int
394
     */
395
    protected function getRandomNumber($from = 0, $to = 31)
396
    {
397
        return random_int($from, $to);
398
    }
399
400
    /**
401
     * Validate the secret.
402
     *
403
     * @param $b32
404
     */
405
    protected function validateSecret($b32)
406
    {
407
        $this->checkForValidCharacters($b32);
408
409
        $this->checkGoogleAuthenticatorCompatibility($b32);
410
    }
411
412
    /**
413
     * Get the key regeneration time in seconds.
414
     *
415
     * @return int
416
     */
417
    public function getKeyRegenerationTime()
418
    {
419
        return $this->keyRegeneration;
420
    }
421
}
422