├── .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 | Latest Stable Version 10 | License 11 | Code Quality 12 | Run tests 13 | Downloads 14 |

15 |

16 | Monthly Downloads 17 | Coverage 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 | ![playground](docs/playground.jpg) 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 | --------------------------------------------------------------------------------