├── .github
└── workflows
│ └── run-tests.yml
├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── composer.json
└── src
├── Exceptions
├── Contracts
│ ├── Google2FA.php
│ ├── IncompatibleWithGoogleAuthenticator.php
│ ├── InvalidAlgorithm.php
│ ├── InvalidCharacters.php
│ └── SecretKeyTooShort.php
├── Google2FAException.php
├── IncompatibleWithGoogleAuthenticatorException.php
├── InvalidAlgorithmException.php
├── InvalidCharactersException.php
└── SecretKeyTooShortException.php
├── Google2FA.php
└── Support
├── Base32.php
├── Constants.php
└── QRCode.php
/.github/workflows/run-tests.yml:
--------------------------------------------------------------------------------
1 | name: Run tests
2 |
3 | on:
4 | push:
5 | pull_request:
6 | schedule:
7 | - cron: '0 0 * * *'
8 |
9 | jobs:
10 | php-tests:
11 | runs-on: ubuntu-latest
12 |
13 | strategy:
14 | matrix:
15 | php: [8.4, 8.3, 8.2, 8.1, 8.0, 7.4]
16 |
17 | name: P${{ matrix.php }}
18 |
19 | steps:
20 | - name: Setup PHP ${{ matrix.php }}
21 | uses: shivammathur/setup-php@v2
22 | with:
23 | php-version: ${{ matrix.php }}
24 | extensions: mbstring
25 | ini-values: post_max_size=256M, max_execution_time=180
26 | coverage: xdebug
27 | tools: php-cs-fixer, phpunit
28 |
29 | - name: Checkout code
30 | uses: actions/checkout@v4
31 |
32 | - name: Configure Git user
33 | run: |
34 | git config --global user.email "acr@antoniocarlosribeiro.com"; git config --global user.name "Antonio Ribeiro"
35 |
36 | - name: Install dependencies
37 | run: |
38 | php --version
39 | composer require --prefer-dist --no-interaction --no-suggest
40 |
41 | - name: Execute tests
42 | run: vendor/bin/phpunit
43 |
44 | - name: Execute PHPStan
45 | run: vendor/bin/phpstan analyse -c phpstan.neon
46 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## Change Log
2 |
3 | ## [8.0.1] - 2020-05-05
4 | ### Added
5 | - Test using GitHub Actions
6 | ### Fixed
7 | - Improve PHP 8.1 compatibility
8 |
9 | ## [8.0.0] - 2020-05-05
10 | ### Added
11 | - PHP 8 Support
12 | - Tests
13 | - Extract som test helpers
14 | - PHPStan checks
15 | ### Changed
16 | - PHP required version bumped to >= 7.1
17 | - Exception interfaces extending Throwable
18 |
19 | ## [7.0.0] - 2019-09-21
20 | ### Added
21 | - PHPStan checks
22 | ### Removed
23 | - Constants::ARGUMENT_NOT_SET - This is a BC break
24 |
25 | ## [6.1.3] - 2019-09-21
26 | ### Drafted
27 | - To fix inserted BC break
28 |
29 | ## [6.1.2] - 2019-09-21
30 | ### DELETED
31 | - To fix inserted BC break
32 |
33 | ## [6.1.1] - 2019-09-21
34 | ### DELETED
35 | - To fix inserted BC break
36 |
37 | ## [6.0.0] - 2019-09-11
38 | ### Added
39 | - Base exception class and interfaces
40 | ### Removed
41 | - Support for PHP 5.4 to 7.0, will keep supporting PHP 7.1, 7.2, 7.3 & 7.4
42 |
43 | ## [5.0.0] - 2019-05-19
44 | ### Changed
45 | - Remove dead Google Charts API
46 |
47 | ## [4.0.0] - 2018-10-06
48 | ### Changed
49 | - Bacon QRCode package removed
50 |
51 | ## [3.0.1] - 2018-03-15
52 | ### Changed
53 | - Relicensed to MIT
54 |
55 | ## [3.0.0] - 2018-03-07
56 | ### Changed
57 | - It's now mandatory to enable Google Api secret key access by executing `setAllowInsecureCallToGoogleApis(true);`
58 |
59 | ## [2.0.4] - 2017-06-22
60 | ### Fixed
61 | - Fix Base32 to keep supporting PHP 5.4 && 5.5.
62 |
63 | ## [2.0.3] - 2017-06-22
64 | ## [2.0.2] - 2017-06-21
65 | ## [2.0.1] - 2017-06-20
66 | ### Fixed
67 | - Minor bugs
68 |
69 | ## [2.0.0] - 2017-06-20
70 | ### Changed
71 | - Drop the Laravel support in favor of a bridge package (https://github.com/antonioribeiro/google2fa-laravel).
72 | - Using a more secure Base 32 algorithm, to prevent cache-timing attacks.
73 | - Added verifyKeyNewer() method to prevent reuse of keys.
74 | - Refactored to remove complexity, by extracting support methods.
75 | - Created a package playground page (https://pragmarx.com/google2fa)
76 |
77 | ## [2.0.0] - 2017-06-20
78 | ### Changed
79 | - Drop the Laravel support in favor of a bridge package (https://github.com/antonioribeiro/google2fa-laravel).
80 | - Using a more secure Base 32 algorithm, to prevent cache-timing attacks.
81 | - Added verifyKeyNewer() method to prevent reuse of keys.
82 | - Refactored to remove complexity, by extracting support methods.
83 | - Created a package playground page (https://pragmarx.com/google2fa)
84 |
85 | ## [1.0.1] - 2016-07-18
86 | ### Changed
87 | - Drop support for PHP 5.3.7, require PHP 5.4+.
88 | - Coding style is now PSR-2 automatically enforced by StyleCI.
89 |
90 | ## [1.0.0] - 2016-07-17
91 | ### Changed
92 | - Package bacon/bacon-qr-code was moved to "suggest".
93 |
94 | ## [0.8.1] - 2016-07-17
95 | ### Fixed
96 | - Allow paragonie/random_compat ~1.4|~2.0.
97 |
98 | ## [0.8.0] - 2016-07-17
99 | ### Changed
100 | - Bumped christian-riesen/base32 to ~1.3
101 | - Use paragonie/random_compat to generate cryptographically secure random secret keys
102 | - Readme improvements
103 | - Drop simple-qrcode in favor of bacon/bacon-qr-code
104 | - Fix tavis setup for phpspec, PHP 7, hhvm and improve cache
105 |
106 | ## [0.7.0] - 2015-11-07
107 | ### Changed
108 | - Fixed URL generation for QRCodes
109 | - Avoid time attacks
110 |
111 | ## [0.2.0] - 2015-02-19
112 | ### Changed
113 | - Laravel 5 compatibility.
114 |
115 | ## [0.1.0] - 2014-07-06
116 | ### Added
117 | - First version.
118 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright 2014-2018 Phil, Antonio Carlos Ribeiro and All Contributors
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Google2FA
2 | ## Google Two-Factor Authentication for PHP
3 |
4 | Google2FA is a PHP implementation of the Google Two-Factor Authentication Module, supporting the HMAC-Based One-time Password (HOTP) algorithm specified in [RFC 4226](https://tools.ietf.org/html/rfc4226) and the Time-based One-time Password (TOTP) algorithm specified in [RFC 6238](https://tools.ietf.org/html/rfc6238).
5 |
6 | ---
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | ---
21 |
22 | ## Menu
23 |
24 | - [Version Compatibility](#version-compatibility)
25 | - [Google Two-Factor Authentication for PHP](#google-two-factor-authentication-for-php)
26 | - [Laravel bridge](#laravel-bridge)
27 | - [Demos, Example & Playground](#demos-example--playground)
28 | - [Requirements](#requirements)
29 | - [Installing](#installing)
30 | - [Usage](#usage)
31 | - [How To Generate And Use Two Factor Authentication](#how-to-generate-and-use-two-factor-authentication)
32 | - [Generating QRCodes](#generating-qrcodes)
33 | - [QR Code Packages](#qr-code-packages)
34 | - [Examples of Usage](#examples-of-usage)
35 | - [HMAC Algorithms](#hmac-algorithms)
36 | - [Server Time](#server-time)
37 | - [Validation Window](#validation-window)
38 | - [Using a Bigger and Prefixing the Secret Key](#using-a-bigger-and-prefixing-the-secret-key)
39 | - [Google Authenticator secret key compatibility](#google-authenticator-secret-key-compatibility)
40 | - [Google Authenticator Apps](#google-authenticator-apps)
41 | - [Deprecation Warning](#deprecation-warning)
42 | - [Testing](#testing)
43 | - [Authors](#authors)
44 | - [License](#license)
45 | - [Contributing](#contributing)
46 |
47 | ## Version Compatibility
48 |
49 | PHP | Google2FA
50 | :--------|:----------
51 | 5.4 | 7.x LTS
52 | 5.5 | 7.x LTS
53 | 5.6 | 7.x LTS
54 | 7.1 | 8.x
55 | 7.2 | 8.x
56 | 7.3 | 8.x
57 | 7.4 | 8.x
58 | 8.0 (β) | 8.x
59 |
60 | ## Laravel bridge
61 |
62 | This package is agnostic, but there's a [Laravel bridge](https://github.com/antonioribeiro/google2fa-laravel).
63 |
64 | ## About QRCode generation
65 |
66 | This package does not generate QRCodes for 2FA.
67 |
68 | If you are looking for Google Two-Factor Authentication, but also need to generate QRCode for it, you can use the [Google2FA QRCode package](https://github.com/antonioribeiro/google2fa-qrcode), which integrates this package and also generates QRCodes using the BaconQRCode library, or check options on how to do it yourself [here in the docs](#qr-code-packages).
69 |
70 | ## Demos, Example & Playground
71 |
72 | Please check the [Google2FA Package Playground](http://pragmarx.com/playground/google2fa).
73 |
74 | 
75 |
76 | Here's a demo app showing how to use Google2FA: [google2fa-example](https://github.com/antonioribeiro/google2fa-example).
77 |
78 | You can scan the QR code on [this (old) demo page](https://antoniocarlosribeiro.com/technology/google2fa) with a Google Authenticator app and view the code changing (almost) in real time.
79 |
80 | ## Requirements
81 |
82 | - PHP 7.1 or greater
83 |
84 | ## Installing
85 |
86 | Use Composer to install it:
87 |
88 | composer require pragmarx/google2fa
89 |
90 | To generate inline QRCodes, you'll need to install a QR code generator, e.g. [BaconQrCode](https://github.com/Bacon/BaconQrCode):
91 |
92 | composer require bacon/bacon-qr-code
93 |
94 | ## Usage
95 |
96 | ### Instantiate it directly
97 |
98 | ```php
99 | use PragmaRX\Google2FA\Google2FA;
100 |
101 | $google2fa = new Google2FA();
102 |
103 | return $google2fa->generateSecretKey();
104 | ```
105 |
106 | ## How To Generate And Use Two Factor Authentication
107 |
108 | Generate a secret key for your user and save it:
109 |
110 | ```php
111 | $user->google2fa_secret = $google2fa->generateSecretKey();
112 | ```
113 |
114 | ## Generating QRCodes
115 |
116 | The more secure way of creating QRCode is to do it yourself or using a library. First you have to install a QR code generator e.g. BaconQrCode, as stated above, then you just have to generate the QR code url using:
117 |
118 | ```php
119 | $qrCodeUrl = $google2fa->getQRCodeUrl(
120 | $companyName,
121 | $companyEmail,
122 | $secretKey
123 | );
124 | ```
125 |
126 | Once you have the QR code url, you can feed it to your preferred QR code generator.
127 |
128 | ```php
129 | // Use your own QR Code generator to generate a data URL:
130 | $google2fa_url = custom_generate_qrcode_url($qrCodeUrl);
131 |
132 | /// and in your view:
133 |
134 |
135 | ```
136 |
137 | And to verify, you just have to:
138 |
139 | ```php
140 | $secret = $request->input('secret');
141 |
142 | $valid = $google2fa->verifyKey($user->google2fa_secret, $secret);
143 | ```
144 |
145 | ## QR Code Packages
146 |
147 | This package suggests the use of [Bacon/QRCode](https://github.com/Bacon/BaconQrCode) because
148 | it is known as a good QR Code package, but you can use it with any other package, for
149 | instance [Google2FA QRCode](https://github.com/antonioribeiro/google2fa-qrcode),
150 | [Simple QrCode](https://www.simplesoftware.io/docs/simple-qrcode)
151 | or [Endroid QR Code](https://github.com/endroid/qr-code), all of them use
152 | [Bacon/QRCode](https://github.com/Bacon/BaconQrCode) to produce QR Codes.
153 |
154 | Usually you'll need a 2FA URL, so you just have to use the URL generator:
155 |
156 | ```php
157 | $google2fa->getQRCodeUrl($companyName, $companyEmail, $secretKey)
158 | ```
159 |
160 | ## Examples of Usage
161 |
162 | ### [Google2FA QRCode](https://github.com/antonioribeiro/google2fa-qrcode)
163 |
164 | Get a QRCode to be used inline:
165 |
166 | ```php
167 | $google2fa = (new \PragmaRX\Google2FAQRCode\Google2FA());
168 |
169 | $inlineUrl = $google2fa->getQRCodeInline(
170 | 'Company Name',
171 | 'company@email.com',
172 | $google2fa->generateSecretKey()
173 | );
174 | ```
175 |
176 | And use in your template:
177 |
178 | ```php
179 |
180 | ```
181 |
182 | ### [Simple QrCode](https://www.simplesoftware.io/docs/simple-qrcode)
183 |
184 | ```php
185 |
186 | {!! QrCode::size(100)->generate($google2fa->getQRCodeUrl($companyName, $companyEmail, $secretKey)); !!}
187 |
Scan me to return to the original page.
188 |
189 | ```
190 |
191 | ### [Endroid QR Code Generator](https://github.com/endroid/qr-code)
192 |
193 | Generate the data URL
194 |
195 | ```php
196 |
197 | $qrCode = new \Endroid\QrCode\QrCode($value);
198 | $qrCode->setSize(100);
199 | $google2fa_url = $qrCode->writeDataUri();
200 | ```
201 |
202 | And in your view
203 |
204 | ```php
205 |
206 | {!! $google2fa_url !!}
207 |
Scan me to return to the original page.
208 |
209 | ```
210 |
211 | ### [Bacon/QRCode](https://github.com/Bacon/BaconQrCode)
212 |
213 | ```php
214 | getQRCodeUrl(
225 | 'pragmarx',
226 | 'google2fa@pragmarx.com',
227 | $google2fa->generateSecretKey()
228 | );
229 |
230 | $writer = new Writer(
231 | new ImageRenderer(
232 | new RendererStyle(400),
233 | new ImagickImageBackEnd()
234 | )
235 | );
236 |
237 | $qrcode_image = base64_encode($writer->writeString($g2faUrl));
238 | ```
239 |
240 | And show it as an image:
241 |
242 | ```php
243 |
244 | ```
245 |
246 | ## HMAC Algorithms
247 |
248 | To comply with [RFC6238](https://tools.ietf.org/html/rfc6238), this package supports SHA1, SHA256 and SHA512. It defaults to SHA1, so to use a different algorithm you just have to use the method `setAlgorithm()`:
249 |
250 | ``` php
251 |
252 | use PragmaRX\Google2FA\Support\Constants;
253 |
254 | $google2fa->setAlgorithm(Constants::SHA512);
255 | ```
256 |
257 | ## Server Time
258 |
259 | It's really important that you keep your server time in sync with some NTP server, on Ubuntu you can add this to the crontab:
260 |
261 | ```bash
262 | sudo service ntp stop
263 | sudo ntpd -gq
264 | sudo service ntp start
265 | ```
266 |
267 | ## Validation Window
268 |
269 | To avoid problems with clocks that are slightly out of sync, we do not check against the current key only but also consider `$window` keys each from the past and future. You can pass `$window` as optional third parameter to `verifyKey`, it defaults to `1`. When a new key is generated every 30 seconds, then with the default setting, keys from one previous, the current, and one next 30-seconds intervals will be considered. To the user with properly synchronized clock, it will look like the key is valid for 60 seconds instead of 30, as the system will accept it even when it is already expired for let's say 29 seconds.
270 |
271 | ```php
272 | $secret = $request->input('secret');
273 |
274 | $window = 8; // 8 keys (respectively 4 minutes) past and future
275 |
276 | $valid = $google2fa->verifyKey($user->google2fa_secret, $secret, $window);
277 | ```
278 |
279 | Setting the `$window` parameter to `0` may also mean that the system will not accept a key that was valid when the user has seen it in their generator as it usually takes some time for the user to input the key to the particular form field.
280 |
281 | An attacker might be able to watch the user entering his credentials and one time key.
282 | Without further precautions, the key remains valid until it is no longer within the window of the server time. In order to prevent usage of a one time key that has already been used, you can utilize the `verifyKeyNewer` function.
283 |
284 | ```php
285 | $secret = $request->input('secret');
286 |
287 | $timestamp = $google2fa->verifyKeyNewer($user->google2fa_secret, $secret, $user->google2fa_ts);
288 |
289 | if ($timestamp !== false) {
290 | $user->update(['google2fa_ts' => $timestamp]);
291 | // successful
292 | } else {
293 | // failed
294 | }
295 | ```
296 |
297 | Note that `$timestamp` is either `false` (if the key is invalid or has been used before) or the provided key's unix timestamp divided by the key regeneration period of 30 seconds.
298 |
299 | ## Using a Bigger and Prefixing the Secret Key
300 |
301 | Although the probability of collision of a 16 bytes (128 bits) random string is very low, you can harden it by:
302 |
303 | #### Use a bigger key
304 |
305 | ```php
306 | $secretKey = $google2fa->generateSecretKey(32); // defaults to 16 bytes
307 | ```
308 |
309 | #### You can prefix your secret keys
310 |
311 | You may prefix your secret keys, but you have to understand that, as your secret key must have length in power of 2, your prefix will have to have a complementary size. So if your key is 16 bytes long, if you add a prefix it must also be 16 bytes long, but as your prefixes will be converted to base 32, the max length of your prefix is 10 bytes. So, those are the sizes you can use in your prefixes:
312 |
313 | ```
314 | 1, 2, 5, 10, 20, 40, 80...
315 | ```
316 |
317 | And it can be used like so:
318 |
319 | ```php
320 | $prefix = strpad($userId, 10, 'X');
321 |
322 | $secretKey = $google2fa->generateSecretKey(16, $prefix);
323 | ```
324 |
325 | #### Window
326 |
327 | The Window property defines how long a OTP will work, or how many cycles it will last. A key has a 30 seconds cycle, setting the window to 0 will make the key last for those 30 seconds, setting it to 2 will make it last for 120 seconds. This is how you set the window:
328 |
329 | ```php
330 | $secretKey = $google2fa->setWindow(4);
331 | ```
332 |
333 | But you can also set the window while checking the key. If you need to set a window of 4 during key verification, this is how you do:
334 |
335 | ```php
336 | $isValid = $google2fa->verifyKey($seed, $key, 4);
337 | ```
338 |
339 | #### Key Regeneration Interval
340 |
341 | You can change key regeneration interval, which defaults to 30 seconds, but remember that this is a default value on most authentication apps, like Google Authenticator, which will, basically, make your app out of sync with them.
342 |
343 | ```php
344 | $google2fa->setKeyRegeneration(40);
345 | ```
346 |
347 | ## Google Authenticator secret key compatibility
348 |
349 | To be compatible with Google Authenticator, your (converted to base 32) secret key length must be at least 8 chars and be a power of 2: 8, 16, 32, 64...
350 |
351 | So, to prevent errors, you can do something like this while generating it:
352 |
353 | ```php
354 | $secretKey = '123456789';
355 |
356 | $secretKey = str_pad($secretKey, pow(2,ceil(log(strlen($secretKey),2))), 'X');
357 | ```
358 |
359 | And it will generate
360 |
361 | ```
362 | 123456789XXXXXXX
363 | ```
364 |
365 | By default, this package will enforce compatibility, but, if Google Authenticator is not a target, you can disable it by doing
366 |
367 | ```php
368 | $google2fa->setEnforceGoogleAuthenticatorCompatibility(false);
369 | ```
370 |
371 | ## Google Authenticator Apps
372 |
373 | To use the two factor authentication, your user will have to install a Google Authenticator compatible app, those are some of the currently available:
374 |
375 | * [Authy for iOS, Android, Chrome, OS X](https://www.authy.com/)
376 | * [FreeOTP for iOS, Android and Pebble](https://apps.getpebble.com/en_US/application/52f1a4c3c4117252f9000bb8)
377 | * [Google Authenticator for iOS](https://itunes.apple.com/us/app/google-authenticator/id388497605?mt=8)
378 | * [Google Authenticator for Android](https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2)
379 | * [Google Authenticator (port) on Windows Store](https://www.microsoft.com/en-us/store/p/google-authenticator/9wzdncrdnkrf)
380 | * [Microsoft Authenticator for Windows Phone](https://www.microsoft.com/en-us/store/apps/authenticator/9wzdncrfj3rj)
381 | * [LastPass Authenticator for iOS, Android, OS X, Windows](https://lastpass.com/auth/)
382 | * [1Password for iOS, Android, OS X, Windows](https://1password.com)
383 |
384 | ## Deprecation Warning
385 |
386 | Google API for QR generator is turned off. All versions of that package prior to 5.0.0 are deprecated. Please upgrade and check documentation regarding [QRCode generation](https://github.com/antonioribeiro/google2fa#generating-qrcodes).
387 |
388 | ## Testing
389 |
390 | The package tests were written with [PHPUnit](https://phpunit.de/). There are some Composer scripts to help you run tests and analysis:
391 |
392 | PHPUnit:
393 |
394 | ````
395 | composer test
396 | ````
397 |
398 | PHPStan analysis:
399 |
400 | ````
401 | composer analyse
402 | ````
403 |
404 | ## Authors
405 |
406 | - [Antonio Carlos Ribeiro](http://twitter.com/iantonioribeiro)
407 | - [Phil (Orginal author of this class)](https://www.idontplaydarts.com/static/ga.php_.txt)
408 | - [All Contributors](https://github.com/antonioribeiro/google2fa/graphs/contributors)
409 |
410 | ## License
411 |
412 | Google2FA is licensed under the MIT License - see the [LICENSE](LICENSE.md) file for details.
413 |
414 | ## Contributing
415 |
416 | Pull requests and issues are more than welcome.
417 |
418 | ## Sponsorships
419 |
420 | ### Direct
421 |
422 | None.
423 |
424 | ### Indirect
425 |
426 | - JetBrains - [Open Source License](https://www.jetbrains.com/community/opensource/#support) (since 2020)
427 | - Blackfire - [Open Source License](https://www.blackfire.io/open-source/) (since 2022)
428 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pragmarx/google2fa",
3 | "description": "A One Time Password Authentication package, compatible with Google Authenticator.",
4 | "keywords": [
5 | "authentication",
6 | "two factor authentication",
7 | "google2fa",
8 | "2fa"
9 | ],
10 | "license": "MIT",
11 | "authors": [
12 | {
13 | "name": "Antonio Carlos Ribeiro",
14 | "email": "acr@antoniocarlosribeiro.com",
15 | "role": "Creator & Designer"
16 | }
17 | ],
18 | "require": {
19 | "php": "^7.1|^8.0",
20 | "paragonie/constant_time_encoding": "^1.0|^2.0|^3.0"
21 | },
22 | "require-dev": {
23 | "phpunit/phpunit": "^7.5.15|^8.5|^9.0",
24 | "phpstan/phpstan": "^1.9"
25 | },
26 | "autoload": {
27 | "psr-4": {
28 | "PragmaRX\\Google2FA\\": "src/"
29 | }
30 | },
31 | "autoload-dev": {
32 | "psr-4": {
33 | "PragmaRX\\Google2FA\\Tests\\": "tests/"
34 | },
35 | "files": ["tests/helpers.php"]
36 | },
37 | "scripts": {
38 | "test": "bash ./tests/tools/test.sh",
39 | "analyse": "bash ./tests/tools/analyse.sh"
40 | },
41 | "minimum-stability": "dev",
42 | "prefer-stable": true
43 | }
44 |
--------------------------------------------------------------------------------
/src/Exceptions/Contracts/Google2FA.php:
--------------------------------------------------------------------------------
1 | getWindow($window);
79 | $startingTimestamp++
80 | ) {
81 | if (
82 | hash_equals($this->oathTotp($secret, $startingTimestamp), (string) $key)
83 | ) {
84 | return is_null($oldTimestamp)
85 | ? true
86 | : $startingTimestamp;
87 | }
88 | }
89 |
90 | return false;
91 | }
92 |
93 | /**
94 | * Generate the HMAC OTP.
95 | *
96 | * @param string $secret
97 | * @param int $counter
98 | *
99 | * @return string
100 | */
101 | protected function generateHotp(
102 | #[\SensitiveParameter]
103 | $secret,
104 | $counter
105 | ) {
106 | return hash_hmac(
107 | $this->getAlgorithm(),
108 | pack('N*', 0, $counter), // Counter must be 64-bit int
109 | $secret,
110 | true
111 | );
112 | }
113 |
114 | /**
115 | * Generate a digit secret key in base32 format.
116 | *
117 | * @param int $length
118 | * @param string $prefix
119 | *
120 | * @throws \PragmaRX\Google2FA\Exceptions\IncompatibleWithGoogleAuthenticatorException
121 | * @throws \PragmaRX\Google2FA\Exceptions\InvalidCharactersException
122 | * @throws \PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException
123 | *
124 | * @return string
125 | */
126 | public function generateSecretKey($length = 16, $prefix = '')
127 | {
128 | return $this->generateBase32RandomKey($length, $prefix);
129 | }
130 |
131 | /**
132 | * Get the current one time password for a key.
133 | *
134 | * @param string $secret
135 | *
136 | * @throws \PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException
137 | * @throws \PragmaRX\Google2FA\Exceptions\InvalidCharactersException
138 | * @throws \PragmaRX\Google2FA\Exceptions\IncompatibleWithGoogleAuthenticatorException
139 | *
140 | * @return string
141 | */
142 | public function getCurrentOtp(
143 | #[\SensitiveParameter]
144 | $secret
145 | ) {
146 | return $this->oathTotp($secret, $this->getTimestamp());
147 | }
148 |
149 | /**
150 | * Get the HMAC algorithm.
151 | *
152 | * @return string
153 | */
154 | public function getAlgorithm()
155 | {
156 | return $this->algorithm;
157 | }
158 |
159 | /**
160 | * Get key regeneration.
161 | *
162 | * @return int
163 | */
164 | public function getKeyRegeneration()
165 | {
166 | return $this->keyRegeneration;
167 | }
168 |
169 | /**
170 | * Get OTP length.
171 | *
172 | * @return int
173 | */
174 | public function getOneTimePasswordLength()
175 | {
176 | return $this->oneTimePasswordLength;
177 | }
178 |
179 | /**
180 | * Get secret.
181 | *
182 | * @param string|null $secret
183 | *
184 | * @return string
185 | */
186 | public function getSecret(
187 | #[\SensitiveParameter]
188 | $secret = null
189 | ) {
190 | return is_null($secret) ? $this->secret : $secret;
191 | }
192 |
193 | /**
194 | * Returns the current Unix Timestamp divided by the $keyRegeneration
195 | * period.
196 | *
197 | * @return int
198 | **/
199 | public function getTimestamp()
200 | {
201 | return (int) floor(microtime(true) / $this->keyRegeneration);
202 | }
203 |
204 | /**
205 | * Get a list of valid HMAC algorithms.
206 | *
207 | * @return array
208 | */
209 | protected function getValidAlgorithms()
210 | {
211 | return [
212 | Constants::SHA1,
213 | Constants::SHA256,
214 | Constants::SHA512,
215 | ];
216 | }
217 |
218 | /**
219 | * Get the OTP window.
220 | *
221 | * @param null|int $window
222 | *
223 | * @return int
224 | */
225 | public function getWindow($window = null)
226 | {
227 | return is_null($window) ? $this->window : $window;
228 | }
229 |
230 | /**
231 | * Make a window based starting timestamp.
232 | *
233 | * @param int|null $window
234 | * @param int $timestamp
235 | * @param int|null $oldTimestamp
236 | *
237 | * @return mixed
238 | */
239 | private function makeStartingTimestamp($window, $timestamp, $oldTimestamp = null)
240 | {
241 | return is_null($oldTimestamp)
242 | ? $timestamp - $this->getWindow($window)
243 | : max($timestamp - $this->getWindow($window), $oldTimestamp + 1);
244 | }
245 |
246 | /**
247 | * Get/use a starting timestamp for key verification.
248 | *
249 | * @param string|int|null $timestamp
250 | *
251 | * @return int
252 | */
253 | protected function makeTimestamp($timestamp = null)
254 | {
255 | if (is_null($timestamp)) {
256 | return $this->getTimestamp();
257 | }
258 |
259 | return (int) $timestamp;
260 | }
261 |
262 | /**
263 | * Takes the secret key and the timestamp and returns the one time
264 | * password.
265 | *
266 | * @param string $secret Secret key in binary form.
267 | * @param int $counter Timestamp as returned by getTimestamp.
268 | *
269 | * @throws \PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException
270 | * @throws \PragmaRX\Google2FA\Exceptions\InvalidCharactersException
271 | * @throws Exceptions\IncompatibleWithGoogleAuthenticatorException
272 | *
273 | * @return string
274 | */
275 | public function oathTotp(
276 | #[\SensitiveParameter]
277 | $secret,
278 | $counter
279 | ) {
280 | if (strlen($secret) < 8) {
281 | throw new SecretKeyTooShortException();
282 | }
283 |
284 | $secret = $this->base32Decode($this->getSecret($secret));
285 |
286 | return str_pad(
287 | $this->oathTruncate($this->generateHotp($secret, $counter)),
288 | $this->getOneTimePasswordLength(),
289 | '0',
290 | STR_PAD_LEFT
291 | );
292 | }
293 |
294 | /**
295 | * Extracts the OTP from the SHA1 hash.
296 | *
297 | * @param string $hash
298 | *
299 | * @return string
300 | **/
301 | public function oathTruncate(
302 | #[\SensitiveParameter]
303 | $hash
304 | ) {
305 | $offset = ord($hash[strlen($hash) - 1]) & 0xF;
306 |
307 | $temp = unpack('N', substr($hash, $offset, 4));
308 |
309 | $temp = $temp[1] & 0x7FFFFFFF;
310 |
311 | return substr(
312 | (string) $temp,
313 | -$this->getOneTimePasswordLength()
314 | );
315 | }
316 |
317 | /**
318 | * Remove invalid chars from a base 32 string.
319 | *
320 | * @param string $string
321 | *
322 | * @return string|null
323 | */
324 | public function removeInvalidChars($string)
325 | {
326 | return preg_replace(
327 | '/[^'.Constants::VALID_FOR_B32.']/',
328 | '',
329 | $string
330 | );
331 | }
332 |
333 | /**
334 | * Setter for the enforce Google Authenticator compatibility property.
335 | *
336 | * @param mixed $enforceGoogleAuthenticatorCompatibility
337 | *
338 | * @return $this
339 | */
340 | public function setEnforceGoogleAuthenticatorCompatibility(
341 | $enforceGoogleAuthenticatorCompatibility
342 | ) {
343 | $this->enforceGoogleAuthenticatorCompatibility = $enforceGoogleAuthenticatorCompatibility;
344 |
345 | return $this;
346 | }
347 |
348 | /**
349 | * Set the HMAC hashing algorithm.
350 | *
351 | * @param mixed $algorithm
352 | *
353 | * @throws \PragmaRX\Google2FA\Exceptions\InvalidAlgorithmException
354 | *
355 | * @return \PragmaRX\Google2FA\Google2FA
356 | */
357 | public function setAlgorithm($algorithm)
358 | {
359 | // Default to SHA1 HMAC algorithm
360 | if (!in_array($algorithm, $this->getValidAlgorithms())) {
361 | throw new InvalidAlgorithmException();
362 | }
363 |
364 | $this->algorithm = $algorithm;
365 |
366 | return $this;
367 | }
368 |
369 | /**
370 | * Set key regeneration.
371 | *
372 | * @param mixed $keyRegeneration
373 | */
374 | public function setKeyRegeneration($keyRegeneration)
375 | {
376 | $this->keyRegeneration = $keyRegeneration;
377 | }
378 |
379 | /**
380 | * Set OTP length.
381 | *
382 | * @param mixed $oneTimePasswordLength
383 | */
384 | public function setOneTimePasswordLength($oneTimePasswordLength)
385 | {
386 | $this->oneTimePasswordLength = $oneTimePasswordLength;
387 | }
388 |
389 | /**
390 | * Set secret.
391 | *
392 | * @param mixed $secret
393 | */
394 | public function setSecret(
395 | #[\SensitiveParameter]
396 | $secret
397 | ) {
398 | $this->secret = $secret;
399 | }
400 |
401 | /**
402 | * Set the OTP window.
403 | *
404 | * @param mixed $window
405 | */
406 | public function setWindow($window)
407 | {
408 | $this->window = $window;
409 | }
410 |
411 | /**
412 | * Verifies a user inputted key against the current timestamp. Checks $window
413 | * keys either side of the timestamp.
414 | *
415 | * @param string $key User specified key
416 | * @param string $secret
417 | * @param null|int $window
418 | * @param null|int $timestamp
419 | * @param null|int $oldTimestamp
420 | *
421 | * @throws \PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException
422 | * @throws \PragmaRX\Google2FA\Exceptions\InvalidCharactersException
423 | * @throws \PragmaRX\Google2FA\Exceptions\IncompatibleWithGoogleAuthenticatorException
424 | *
425 | * @return bool|int
426 | */
427 | public function verify(
428 | #[\SensitiveParameter]
429 | $key,
430 | #[\SensitiveParameter]
431 | $secret,
432 | $window = null,
433 | $timestamp = null,
434 | $oldTimestamp = null
435 | ) {
436 | return $this->verifyKey(
437 | $secret,
438 | $key,
439 | $window,
440 | $timestamp,
441 | $oldTimestamp
442 | );
443 | }
444 |
445 | /**
446 | * Verifies a user inputted key against the current timestamp. Checks $window
447 | * keys either side of the timestamp.
448 | *
449 | * @param string $secret
450 | * @param string $key User specified key
451 | * @param int|null $window
452 | * @param null|int $timestamp
453 | * @param null|int $oldTimestamp
454 | *
455 | * @throws \PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException
456 | * @throws \PragmaRX\Google2FA\Exceptions\InvalidCharactersException
457 | * @throws \PragmaRX\Google2FA\Exceptions\IncompatibleWithGoogleAuthenticatorException
458 | *
459 | * @return bool|int
460 | */
461 | public function verifyKey(
462 | #[\SensitiveParameter]
463 | $secret,
464 | #[\SensitiveParameter]
465 | $key,
466 | $window = null,
467 | $timestamp = null,
468 | $oldTimestamp = null
469 | ) {
470 | $timestamp = $this->makeTimestamp($timestamp);
471 |
472 | return $this->findValidOTP(
473 | $secret,
474 | $key,
475 | $window,
476 | $this->makeStartingTimestamp($window, $timestamp, $oldTimestamp),
477 | $timestamp,
478 | $oldTimestamp
479 | );
480 | }
481 |
482 | /**
483 | * Verifies a user inputted key against the current timestamp. Checks $window
484 | * keys either side of the timestamp, but ensures that the given key is newer than
485 | * the given oldTimestamp. Useful if you need to ensure that a single key cannot
486 | * be used twice.
487 | *
488 | * @param string $secret
489 | * @param string $key User specified key
490 | * @param int|null $oldTimestamp The timestamp from the last verified key
491 | * @param int|null $window
492 | * @param int|null $timestamp
493 | *
494 | * @throws \PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException
495 | * @throws \PragmaRX\Google2FA\Exceptions\InvalidCharactersException
496 | * @throws \PragmaRX\Google2FA\Exceptions\IncompatibleWithGoogleAuthenticatorException
497 | *
498 | * @return bool|int
499 | */
500 | public function verifyKeyNewer(
501 | #[\SensitiveParameter]
502 | $secret,
503 | #[\SensitiveParameter]
504 | $key,
505 | $oldTimestamp,
506 | $window = null,
507 | $timestamp = null
508 | ) {
509 | return $this->verifyKey(
510 | $secret,
511 | $key,
512 | $window,
513 | $timestamp,
514 | $oldTimestamp
515 | );
516 | }
517 | }
518 |
--------------------------------------------------------------------------------
/src/Support/Base32.php:
--------------------------------------------------------------------------------
1 | toBase32($prefix) : '';
50 |
51 | $secret = $this->strPadBase32($secret, $length);
52 |
53 | $this->validateSecret($secret);
54 |
55 | return $secret;
56 | }
57 |
58 | /**
59 | * Decodes a base32 string into a binary string.
60 | *
61 | * @param string $b32
62 | *
63 | * @throws \PragmaRX\Google2FA\Exceptions\InvalidCharactersException
64 | * @throws \PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException
65 | * @throws \PragmaRX\Google2FA\Exceptions\IncompatibleWithGoogleAuthenticatorException
66 | *
67 | * @return string
68 | */
69 | public function base32Decode(
70 | #[\SensitiveParameter]
71 | $b32
72 | ) {
73 | $b32 = strtoupper($b32);
74 |
75 | $this->validateSecret($b32);
76 |
77 | return ParagonieBase32::decodeUpper($b32);
78 | }
79 |
80 | /**
81 | * Check if the string length is power of two.
82 | *
83 | * @param string $b32
84 | *
85 | * @return bool
86 | */
87 | protected function isCharCountNotAPowerOfTwo(
88 | #[\SensitiveParameter]
89 | $b32
90 | ) {
91 | return (strlen($b32) & (strlen($b32) - 1)) !== 0;
92 | }
93 |
94 | /**
95 | * Pad string with random base 32 chars.
96 | *
97 | * @param string $string
98 | * @param int $length
99 | *
100 | * @throws \Exception
101 | *
102 | * @return string
103 | */
104 | private function strPadBase32(
105 | #[\SensitiveParameter]
106 | $string,
107 | $length
108 | ) {
109 | for ($i = 0; $i < $length; $i++) {
110 | $string .= substr(
111 | Constants::VALID_FOR_B32_SCRAMBLED,
112 | $this->getRandomNumber(),
113 | 1
114 | );
115 | }
116 |
117 | return $string;
118 | }
119 |
120 | /**
121 | * Encode a string to Base32.
122 | *
123 | * @param string $string
124 | *
125 | * @return string
126 | */
127 | public function toBase32(
128 | #[\SensitiveParameter]
129 | $string
130 | ) {
131 | $encoded = ParagonieBase32::encodeUpper($string);
132 |
133 | return str_replace('=', '', $encoded);
134 | }
135 |
136 | /**
137 | * Get a random number.
138 | *
139 | * @param int $from
140 | * @param int $to
141 | *
142 | * @throws \Exception
143 | *
144 | * @return int
145 | */
146 | protected function getRandomNumber($from = 0, $to = 31)
147 | {
148 | return random_int($from, $to);
149 | }
150 |
151 | /**
152 | * Validate the secret.
153 | *
154 | * @param string $b32
155 | *
156 | * @throws \PragmaRX\Google2FA\Exceptions\InvalidCharactersException
157 | * @throws \PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException
158 | * @throws \PragmaRX\Google2FA\Exceptions\IncompatibleWithGoogleAuthenticatorException
159 | */
160 | protected function validateSecret(
161 | #[\SensitiveParameter]
162 | $b32
163 | ) {
164 | $this->checkForValidCharacters($b32);
165 |
166 | $this->checkGoogleAuthenticatorCompatibility($b32);
167 |
168 | $this->checkIsBigEnough($b32);
169 | }
170 |
171 | /**
172 | * Check if the secret key is compatible with Google Authenticator.
173 | *
174 | * @param string $b32
175 | *
176 | * @throws IncompatibleWithGoogleAuthenticatorException
177 | */
178 | protected function checkGoogleAuthenticatorCompatibility(
179 | #[\SensitiveParameter]
180 | $b32
181 | ) {
182 | if (
183 | $this->enforceGoogleAuthenticatorCompatibility &&
184 | $this->isCharCountNotAPowerOfTwo($b32) // Google Authenticator requires it to be a power of 2 base32 length string
185 | ) {
186 | throw new IncompatibleWithGoogleAuthenticatorException();
187 | }
188 | }
189 |
190 | /**
191 | * Check if all secret key characters are valid.
192 | *
193 | * @param string $b32
194 | *
195 | * @throws \PragmaRX\Google2FA\Exceptions\InvalidCharactersException
196 | */
197 | protected function checkForValidCharacters(
198 | #[\SensitiveParameter]
199 | $b32
200 | ) {
201 | if (
202 | preg_replace('/[^'.Constants::VALID_FOR_B32.']/', '', $b32) !==
203 | $b32
204 | ) {
205 | throw new InvalidCharactersException();
206 | }
207 | }
208 |
209 | /**
210 | * Check if secret key length is big enough.
211 | *
212 | * @param string $b32
213 | *
214 | * @throws \PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException
215 | */
216 | protected function checkIsBigEnough(
217 | #[\SensitiveParameter]
218 | $b32
219 | ) {
220 | // Minimum = 128 bits
221 | // Recommended = 160 bits
222 | // Compatible with Google Authenticator = 256 bits
223 |
224 | if (
225 | $this->charCountBits($b32) < 128
226 | ) {
227 | throw new SecretKeyTooShortException();
228 | }
229 | }
230 | }
231 |
--------------------------------------------------------------------------------
/src/Support/Constants.php:
--------------------------------------------------------------------------------
1 | getAlgorithm())).
32 | '&digits='.
33 | rawurlencode(strtoupper((string) $this->getOneTimePasswordLength())).
34 | '&period='.
35 | rawurlencode(strtoupper((string) $this->getKeyRegeneration())).
36 | '';
37 | }
38 | }
39 |
--------------------------------------------------------------------------------