mirror of
https://github.com/RobThree/TwoFactorAuth.git
synced 2026-08-20 17:32:43 +00:00
Compare commits
142 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 85408c4e77 | |||
| 3516aae5a3 | |||
| a2fa4c11a9 | |||
| 57970d3d5f | |||
| 83918a7658 | |||
| 9b2d795acc | |||
| 24f6d2fd10 | |||
| 6d70f9ca8e | |||
| 92487acba4 | |||
| ab4c33007f | |||
| 0ab012bbaf | |||
| ec82d397ee | |||
| fc3adc7245 | |||
| 337e96b7af | |||
| 91c091c316 | |||
| 9e8b31ebbc | |||
| df43660714 | |||
| 6194bb08a7 | |||
| 6f78141196 | |||
| f5eb9a7051 | |||
| 5c97ce9be8 | |||
| ab677e43ee | |||
| eb02cb7095 | |||
| c15668dc6d | |||
| f16ea16e1b | |||
| f35f2ae51a | |||
| cd27effc02 | |||
| 2ef7b8e2c6 | |||
| b964b2abbf | |||
| 8007233058 | |||
| 22d45f2ab4 | |||
| 2080319f6f | |||
| cabcf5d493 | |||
| 8477141a36 | |||
| 028867dec2 | |||
| dfc1124f96 | |||
| d4a5026d86 | |||
| bb6802c093 | |||
| b909cb38f6 | |||
| e08b0a5445 | |||
| ecef270ba7 | |||
| 194ecc28eb | |||
| 323053bd52 | |||
| 83c7449270 | |||
| 086a3758ec | |||
| bba4c207c2 | |||
| 061a2a39b4 | |||
| 76f420e6b2 | |||
| 319a58a001 | |||
| d996779182 | |||
| ab93dd41ce | |||
| ec35073c06 | |||
| 13a56018f4 | |||
| 4b6b0601b7 | |||
| 126afe4c25 | |||
| 63f6259db2 | |||
| 137df4dd3c | |||
| 9476751d6b | |||
| 0565d63e6a | |||
| d5aa22a96f | |||
| a6f7735b9f | |||
| 6416e79c3e | |||
| 83611592db | |||
| dd0ecaeed1 | |||
| 241dfec585 | |||
| 63b49ce25c | |||
| e554a9b5e4 | |||
| 9bc454f425 | |||
| c2183e16b3 | |||
| cf84371dad | |||
| 4d91d6ae27 | |||
| d60113a77a | |||
| 3f3327e2a1 | |||
| 8bf962ecee | |||
| b80431341e | |||
| f542788673 | |||
| ab51d16de7 | |||
| d4f6aa1ee6 | |||
| 04ff3e7e3f | |||
| d94ee9c769 | |||
| 955ce522f0 | |||
| e584f0c56a | |||
| 5949a29b89 | |||
| 8bf8800523 | |||
| 2fd60fad5e | |||
| 26413e69f0 | |||
| 3adb3e54e5 | |||
| c45f7a23c9 | |||
| bdfa67d1f6 | |||
| 7675282f45 | |||
| 1ca305ec4a | |||
| aadc32e85e | |||
| 9a1aeb8c1f | |||
| 4c8a88224a | |||
| 9523b35d34 | |||
| cc9400d901 | |||
| dfb5c1a113 | |||
| 098dce6735 | |||
| ae1cc5e8af | |||
| 6e71f43bb1 | |||
| 5968be2cee | |||
| 04e26c9961 | |||
| 8391c5efb0 | |||
| 27cd1e1392 | |||
| 0159e77743 | |||
| 30248a8fb5 | |||
| d6e5e2ef87 | |||
| ab76ac71a4 | |||
| 023bfc16f6 | |||
| afb5cb09be | |||
| 1d628527e5 | |||
| d4528f58fe | |||
| e6e5d59297 | |||
| a968dd392a | |||
| 86338cf3cd | |||
| dc4e99e8c3 | |||
| 656e966cdf | |||
| 921425d0ff | |||
| 4711674ec0 | |||
| e049285b46 | |||
| b52655b803 | |||
| ad89250a8f | |||
| f034bc782e | |||
| f8ba3b234b | |||
| db6256b5e8 | |||
| 041d0e38e2 | |||
| 1e75674a45 | |||
| 25f463d19b | |||
| aaf24a66bd | |||
| 75d8955967 | |||
| 7e067166aa | |||
| 58a9628a57 | |||
| 887f261c88 | |||
| 225216a616 | |||
| 79988ef58a | |||
| f1e73aab3a | |||
| db0515e826 | |||
| 4be83550c0 | |||
| ac721e57b9 | |||
| a8c297c178 | |||
| aeb4b00c60 | |||
| ba4e8c55ed |
@@ -0,0 +1,12 @@
|
||||
/.github/ export-ignore
|
||||
/demo/ export-ignore
|
||||
/docs/ export-ignore
|
||||
/tests/ export-ignore
|
||||
/testsDependency/ export-ignore
|
||||
/.gitattributes export-ignore
|
||||
/.gitignore export-ignore
|
||||
/.php-cs-fixer.dist.php export-ignore
|
||||
/logo.png export-ignore
|
||||
/multifactorauthforeveryone.png export-ignore
|
||||
/phpstan.neon export-ignore
|
||||
/phpunit.xml export-ignore
|
||||
@@ -0,0 +1 @@
|
||||
Don't be a jerk!
|
||||
@@ -10,10 +10,16 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1']
|
||||
php-version: [8.2, 8.3, 8.4, 8.5]
|
||||
bacon-version: ["^2", "^3"]
|
||||
exclude:
|
||||
- php-version: 8.4
|
||||
bacon-version: "^2"
|
||||
- php-version: 8.5
|
||||
bacon-version: "^2"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
@@ -22,9 +28,9 @@ jobs:
|
||||
coverage: xdebug
|
||||
ini-values: error_reporting=E_ALL
|
||||
|
||||
- uses: ramsey/composer-install@v1
|
||||
- uses: ramsey/composer-install@v3
|
||||
|
||||
- run: composer require bacon/bacon-qr-code
|
||||
- run: composer require bacon/bacon-qr-code:${{ matrix.bacon-version }}
|
||||
|
||||
- run: composer lint
|
||||
- run: composer lint-ci
|
||||
- run: composer test testsDependency/BaconQRCodeTest.php
|
||||
|
||||
@@ -10,27 +10,24 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.0', '8.1']
|
||||
endroid-version: ["^4"]
|
||||
include:
|
||||
- php-version: 5.6
|
||||
# earliest supported version
|
||||
endroid-version: 2.2.1
|
||||
- php-version: 7.0
|
||||
endroid-version: 2.5.1
|
||||
- php-version: 7.1
|
||||
# this version is 7.1+
|
||||
endroid-version: 3.0.0
|
||||
- php-version: 7.2
|
||||
# all later versions are 7.3+
|
||||
endroid-version: 3.5.9
|
||||
- php-version: 7.3
|
||||
endroid-version: 3.9.7
|
||||
- php-version: 7.4
|
||||
endroid-version: 4.0.0
|
||||
php-version: [8.2, 8.3, 8.4, 8.5]
|
||||
endroid-version: ["^3", "^4", "^5", "^6"]
|
||||
exclude:
|
||||
- php-version: 8.4
|
||||
endroid-version: "^3"
|
||||
- php-version: 8.4
|
||||
endroid-version: "^4"
|
||||
- php-version: 8.4
|
||||
endroid-version: "^5"
|
||||
- php-version: 8.5
|
||||
endroid-version: "^3"
|
||||
- php-version: 8.5
|
||||
endroid-version: "^4"
|
||||
- php-version: 8.5
|
||||
endroid-version: "^5"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
@@ -39,8 +36,9 @@ jobs:
|
||||
coverage: xdebug
|
||||
ini-values: error_reporting=E_ALL
|
||||
|
||||
- uses: ramsey/composer-install@v1
|
||||
- uses: ramsey/composer-install@v3
|
||||
|
||||
- run: composer require endroid/qrcode:${{ matrix.endroid-version }}
|
||||
- run: composer require endroid/qrcode:${{ matrix.endroid-version }} -W
|
||||
|
||||
- run: composer lint-ci
|
||||
- run: composer test testsDependency/EndroidQRCodeTest.php
|
||||
|
||||
@@ -10,10 +10,10 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1']
|
||||
php-version: [8.2, 8.3, 8.4, 8.5]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
@@ -22,7 +22,8 @@ jobs:
|
||||
coverage: xdebug
|
||||
ini-values: error_reporting=E_ALL
|
||||
|
||||
- uses: ramsey/composer-install@v1
|
||||
- uses: ramsey/composer-install@v3
|
||||
|
||||
- run: composer lint
|
||||
- run: composer lint-ci
|
||||
- run: composer phpstan
|
||||
- run: composer test
|
||||
|
||||
@@ -190,3 +190,4 @@ composer.lock
|
||||
.vs/
|
||||
|
||||
.phpunit.result.cache
|
||||
.php-cs-fixer.cache
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* PHP-CS-Fixer config for RobThree/TwoFactorAuth
|
||||
*/
|
||||
$finder = PhpCsFixer\Finder::create()
|
||||
->name('/\.php|\.php.dist$/')
|
||||
->exclude('build')
|
||||
->exclude('demo')
|
||||
->exclude('docs')
|
||||
->in(['lib', 'tests', 'testsDependency'])
|
||||
;
|
||||
|
||||
$config = new PhpCsFixer\Config();
|
||||
|
||||
return $config->setRules(array(
|
||||
'@PSR2' => true,
|
||||
'@PSR12' => true,
|
||||
'@PHP82Migration' => true,
|
||||
'array_syntax' => ['syntax' => 'long'],
|
||||
'class_attributes_separation' => true,
|
||||
'declare_strict_types' => true,
|
||||
'dir_constant' => true,
|
||||
'is_null' => true,
|
||||
'no_homoglyph_names' => true,
|
||||
'no_null_property_initialization' => true,
|
||||
'no_php4_constructor' => true,
|
||||
'no_unused_imports' => true,
|
||||
'no_useless_else' => true,
|
||||
'non_printable_character' => true,
|
||||
'ordered_imports' => true,
|
||||
'ordered_class_elements' => true,
|
||||
'php_unit_construct' => true,
|
||||
'pow_to_exponentiation' => true,
|
||||
'psr_autoloading' => true,
|
||||
'random_api_migration' => true,
|
||||
'return_assignment' => true,
|
||||
'self_accessor' => true,
|
||||
'semicolon_after_instruction' => true,
|
||||
'short_scalar_cast' => true,
|
||||
'simplified_null_return' => true,
|
||||
'single_class_element_per_statement' => true,
|
||||
'single_line_comment_style' => true,
|
||||
'single_quote' => true,
|
||||
'space_after_semicolon' => true,
|
||||
'standardize_not_equals' => true,
|
||||
'strict_param' => true,
|
||||
'ternary_operator_spaces' => true,
|
||||
'trailing_comma_in_multiline' => true,
|
||||
'trim_array_spaces' => true,
|
||||
'unary_operator_spaces' => true,
|
||||
'global_namespace_import' => [
|
||||
'import_classes' => true,
|
||||
'import_functions' => true,
|
||||
'import_constants' => true,
|
||||
],
|
||||
))
|
||||
->setFinder($finder)
|
||||
->setRiskyAllowed(true)
|
||||
;
|
||||
@@ -0,0 +1,71 @@
|
||||
# RobThree\TwoFactorAuth changelog
|
||||
|
||||
# Version 3.x
|
||||
|
||||
## Breaking changes
|
||||
|
||||
### PHP Version
|
||||
|
||||
Version 3.x requires at least PHP 8.2.
|
||||
|
||||
### Constructor signature change
|
||||
|
||||
In order to ensure users of this library make a conscious choice of QR Code Provider, the QR Code Provider is now a mandatory argument, in first place.
|
||||
|
||||
If you didn't provide one explicitly before, you can get the old behavior with:
|
||||
|
||||
~~~php
|
||||
use RobThree\Auth\TwoFactorAuth;
|
||||
use RobThree\Auth\Providers\Qr\QRServerProvider;
|
||||
$tfa = new TwoFactorAuth(new QRServerProvider());
|
||||
~~~
|
||||
|
||||
If you provided one before, the order of the parameters have been changed, so simply move the QRCodeProvider argument to the first place or use named arguments.
|
||||
|
||||
Documentation on selecting a QR Code Provider is available here: [QR Code Provider documentation](https://robthree.github.io/TwoFactorAuth/qr-codes.html).
|
||||
|
||||
### Default secret length
|
||||
|
||||
The default secret length has been increased from 80 bits to 160 bits (RFC4226) PR [#117](https://github.com/RobThree/TwoFactorAuth/pull/117). This might cause an issue in your application if you were previously storing secrets in a column with restricted size. This change doesn't impact existing secrets, only new ones will get longer.
|
||||
|
||||
Previously a secret was 16 characters, now it needs to be stored in a 32 characters width column.
|
||||
|
||||
You can keep the old behavior by setting `80` as argument to `createSecret()` (not recommended, see [#117](https://github.com/RobThree/TwoFactorAuth/pull/117) for further discussion).
|
||||
|
||||
## Other changes
|
||||
|
||||
* The new PHP attribute [SensitiveParameter](https://www.php.net/manual/en/class.sensitiveparameter.php) was added to the code, to prevent accidental leak of secrets in stack traces.
|
||||
* Likely not breaking anything, but now all external QR Code providers use HTTPS with a verified certificate. PR [#126](https://github.com/RobThree/TwoFactorAuth/pull/126).
|
||||
* The CSPRNG is now exclusively using `random_bytes()` PHP function. Previously a fallback to `openssl` or non cryptographically secure PRNG existed, they have been removed. PR [#122](https://github.com/RobThree/TwoFactorAuth/pull/122).
|
||||
* If an external QR code provider is used and the HTTP request results in an error, it will throw a `QRException`. Previously the error was ignored. PR [#130](https://github.com/RobThree/TwoFactorAuth/pull/130), fixes [#129](https://github.com/RobThree/TwoFactorAuth/issues/129).
|
||||
|
||||
# Version 2.x
|
||||
|
||||
## Breaking changes
|
||||
|
||||
### PHP Version
|
||||
|
||||
Version 2.x requires at least PHP 8.1.
|
||||
|
||||
### Constructor signature
|
||||
|
||||
With version 2.x, the `algorithm` parameter of `RobThree\Auth\TwoFactorAuth` constructor is now an `enum`.
|
||||
|
||||
On version 1.x:
|
||||
|
||||
~~~php
|
||||
use RobThree\Auth\TwoFactorAuth;
|
||||
|
||||
$lib = new TwoFactorAuth('issuer-name', 6, 30, 'sha1');
|
||||
~~~
|
||||
|
||||
On version 2.x, simple change the algorithm from a `string` to the correct `enum`:
|
||||
|
||||
~~~php
|
||||
use RobThree\Auth\TwoFactorAuth;
|
||||
use RobThree\Auth\Algorithm;
|
||||
|
||||
$lib = new TwoFactorAuth('issuer-name', 6, 30, Algorithm::Sha1);
|
||||
~~~
|
||||
|
||||
See the [Algorithm.php](./lib/Algorithm.php) file to see available algorithms.
|
||||
@@ -1,6 +1,6 @@
|
||||
#  PHP library for Two Factor Authentication
|
||||
|
||||
[](https://github.com/RobThree/TwoFactorAuth/actions?query=branch%3Amaster) [](https://packagist.org/packages/robthree/twofactorauth) [](LICENSE) [](https://packagist.org/packages/robthree/twofactorauth) [](https://codeclimate.com/github/RobThree/TwoFactorAuth) [](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=6MB5M2SQLP636 "Keep me off the streets")
|
||||
[](https://github.com/RobThree/TwoFactorAuth/actions?query=branch%3Amaster) [](https://packagist.org/packages/robthree/twofactorauth) [](LICENSE) [](https://packagist.org/packages/robthree/twofactorauth) [](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=6MB5M2SQLP636 "Keep me off the streets")
|
||||
|
||||
PHP library for [two-factor (or multi-factor) authentication](http://en.wikipedia.org/wiki/Multi-factor_authentication) using [TOTP](http://en.wikipedia.org/wiki/Time-based_One-time_Password_Algorithm) and [QR-codes](http://en.wikipedia.org/wiki/QR_code). Inspired by, based on but most importantly an *improvement* on '[PHPGangsta/GoogleAuthenticator](https://github.com/PHPGangsta/GoogleAuthenticator)'. There's a [.Net implementation](https://github.com/RobThree/TwoFactorAuth.Net) of this library as well.
|
||||
|
||||
@@ -10,15 +10,14 @@ PHP library for [two-factor (or multi-factor) authentication](http://en.wikipedi
|
||||
|
||||
## Requirements
|
||||
|
||||
* Tested on PHP 5.6 up to 8.0
|
||||
* [cURL](http://php.net/manual/en/book.curl.php) when using the provided `QRServerProvider` (default), `ImageChartsQRCodeProvider` or `QRicketProvider` but you can also provide your own QR-code provider.
|
||||
* [random_bytes()](http://php.net/manual/en/function.random-bytes.php), [MCrypt](http://php.net/manual/en/book.mcrypt.php), [OpenSSL](http://php.net/manual/en/book.openssl.php) or [Hash](http://php.net/manual/en/book.hash.php) depending on which built-in RNG you use (TwoFactorAuth will try to 'autodetect' and use the best available); however: feel free to provide your own (CS)RNG.
|
||||
* Requires PHP version >=8.2
|
||||
|
||||
Optionally, you may need:
|
||||
|
||||
* [sockets](https://www.php.net/manual/en/book.sockets.php) if you are using `NTPTimeProvider`
|
||||
* [endroid/qr-code](https://github.com/endroid/qr-code) if using `EndroidQrCodeProvider` or `EndroidQrCodeWithLogoProvider`.
|
||||
* [bacon/bacon-qr-code](https://github.com/Bacon/BaconQrCode) if using `BaconQrCodeProvider`.
|
||||
* [php-curl library](http://php.net/manual/en/book.curl.php) when using an external QR Code provider such as `QRServerProvider`, `ImageChartsQRCodeProvider`, `QRicketProvider` or any other custom provider connecting to an external service.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -34,10 +33,11 @@ If you need more in-depth information about the configuration available then you
|
||||
|
||||
## Integrations
|
||||
|
||||
- [CakePHP 3](https://github.com/andrej-griniuk/cakephp-two-factor-auth)
|
||||
- [CakePHP plugin](https://github.com/andrej-griniuk/cakephp-two-factor-auth)
|
||||
- [CI4-Auth: a user, group, role and permission management library for Codeigniter 4](https://github.com/glewe/ci4-auth)
|
||||
|
||||
## License
|
||||
|
||||
Licensed under MIT license. See [LICENSE](https://raw.githubusercontent.com/RobThree/TwoFactorAuth/master/LICENSE) for details.
|
||||
Licensed under MIT license. See [LICENSE](./LICENSE) for details.
|
||||
|
||||
[Logo / icon](http://www.iconmay.com/Simple/Travel_and_Tourism_Part_2/luggage_lock_safety_baggage_keys_cylinder_lock_hotel_travel_tourism_luggage_lock_icon_465) under CC0 1.0 Universal (CC0 1.0) Public Domain Dedication ([Archived page](http://riii.nl/tm7ap))
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Name>TwoFactorAuth</Name>
|
||||
<ProjectGuid>{e569f53a-a604-4579-91ce-4e35b27da47b}</ProjectGuid>
|
||||
<RootNamespace>TwoFactorAuth</RootNamespace>
|
||||
<OutputType>Library</OutputType>
|
||||
<ProjectTypeGuids>{A0786B88-2ADB-4C21-ABE8-AA2D79766269}</ProjectTypeGuids>
|
||||
<SaveServerSettingsInUserFile>False</SaveServerSettingsInUserFile>
|
||||
<Server>PHPDev</Server>
|
||||
<PublishEvent>None</PublishEvent>
|
||||
<PHPDevAutoPort>True</PHPDevAutoPort>
|
||||
<PHPDevPort>41315</PHPDevPort>
|
||||
<PHPDevHostName>localhost</PHPDevHostName>
|
||||
<IISProjectUrl>http://localhost:41315/</IISProjectUrl>
|
||||
<Runtime>PHP</Runtime>
|
||||
<RuntimeVersion>7.0</RuntimeVersion>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<IncludeDebugInformation>true</IncludeDebugInformation>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<IncludeDebugInformation>false</IncludeDebugInformation>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="demo\demo.php" />
|
||||
<Compile Include="demo\loader.php" />
|
||||
<Compile Include="lib\Providers\Qr\BaseHTTPQRCodeProvider.php" />
|
||||
<Compile Include="lib\Providers\Qr\ImageChartsQRCodeProvider.php" />
|
||||
<Compile Include="lib\Providers\Qr\IQRCodeProvider.php" />
|
||||
<Compile Include="lib\Providers\Qr\QRException.php" />
|
||||
<Compile Include="lib\Providers\Qr\QRicketProvider.php" />
|
||||
<Compile Include="lib\Providers\Qr\QRServerProvider.php" />
|
||||
<Compile Include="lib\Providers\Rng\CSRNGProvider.php" />
|
||||
<Compile Include="lib\Providers\Rng\IRNGProvider.php" />
|
||||
<Compile Include="lib\Providers\Rng\MCryptRNGProvider.php" />
|
||||
<Compile Include="lib\Providers\Rng\OpenSSLRNGProvider.php" />
|
||||
<Compile Include="lib\Providers\Rng\HashRNGProvider.php" />
|
||||
<Compile Include="lib\Providers\Rng\RNGException.php" />
|
||||
<Compile Include="lib\Providers\Time\HttpTimeProvider.php" />
|
||||
<Compile Include="lib\Providers\Time\ITimeProvider.php" />
|
||||
<Compile Include="lib\Providers\Time\LocalMachineTimeProvider.php" />
|
||||
<Compile Include="lib\Providers\Time\NTPTimeProvider.php" />
|
||||
<Compile Include="lib\Providers\Time\TimeException.php" />
|
||||
<Compile Include="lib\TwoFactorAuth.php" />
|
||||
<Compile Include=".gitignore" />
|
||||
<Compile Include="README.md" />
|
||||
<Compile Include="lib\TwoFactorAuthException.php" />
|
||||
<Compile Include="tests\TwoFactorAuthTest.php" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="lib\" />
|
||||
<Folder Include="lib\Providers\" />
|
||||
<Folder Include="lib\Providers\Time\" />
|
||||
<Folder Include="lib\Providers\Qr\" />
|
||||
<Folder Include="lib\Providers\Rng\" />
|
||||
<Folder Include="demo\" />
|
||||
<Folder Include="tests\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include=".travis.yml" />
|
||||
<Content Include="composer.json" />
|
||||
<Content Include="composer.lock" />
|
||||
<Content Include="logo.png" />
|
||||
<Content Include="multifactorauthforeveryone.png" />
|
||||
<Content Include="LICENSE" />
|
||||
<Content Include="phpunit.xml" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,22 +0,0 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 2013
|
||||
VisualStudioVersion = 12.0.30723.0
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{A0786B88-2ADB-4C21-ABE8-AA2D79766269}") = "TwoFactorAuth", "TwoFactorAuth.phpproj", "{E569F53A-A604-4579-91CE-4E35B27DA47B}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{E569F53A-A604-4579-91CE-4E35B27DA47B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E569F53A-A604-4579-91CE-4E35B27DA47B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E569F53A-A604-4579-91CE-4E35B27DA47B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E569F53A-A604-4579-91CE-4E35B27DA47B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
+21
-5
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"name": "robthree/twofactorauth",
|
||||
"description": "Two Factor Authentication",
|
||||
"version": "1.8.2",
|
||||
"type": "library",
|
||||
"keywords": [ "Authentication", "Two Factor Authentication", "Multi Factor Authentication", "TFA", "MFA", "PHP", "Authenticator", "Authy" ],
|
||||
"homepage": "https://github.com/RobThree/TwoFactorAuth",
|
||||
@@ -11,6 +10,16 @@
|
||||
"name": "Rob Janssen",
|
||||
"homepage": "http://robiii.me",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Nicolas CARPi",
|
||||
"homepage": "https://github.com/NicolasCARPi",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Will Power",
|
||||
"homepage": "https://github.com/willpower232",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"support": {
|
||||
@@ -18,11 +27,12 @@
|
||||
"source": "https://github.com/RobThree/TwoFactorAuth"
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.6.0"
|
||||
"php": ">=8.2.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "@stable",
|
||||
"php-parallel-lint/php-parallel-lint": "^1.2"
|
||||
"phpunit/phpunit": "^9",
|
||||
"friendsofphp/php-cs-fixer": "^3.13",
|
||||
"phpstan/phpstan": "^1.9"
|
||||
},
|
||||
"suggest": {
|
||||
"bacon/bacon-qr-code": "Needed for BaconQrCodeProvider provider",
|
||||
@@ -39,8 +49,14 @@
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"phpstan": [
|
||||
"phpstan analyze --xdebug lib tests testsDependency"
|
||||
],
|
||||
"lint": [
|
||||
"parallel-lint --exclude vendor ."
|
||||
"php-cs-fixer fix -v"
|
||||
],
|
||||
"lint-ci": [
|
||||
"PHP_CS_FIXER_IGNORE_ENV=1 php-cs-fixer fix -v --dry-run --stop-on-violation"
|
||||
],
|
||||
"test": [
|
||||
"XDEBUG_MODE=coverage phpunit"
|
||||
|
||||
+4
-2
@@ -11,8 +11,10 @@
|
||||
include_once str_replace(array('RobThree\\Auth', '\\'), array(__DIR__.'/../lib', '/'), $className) . '.php';
|
||||
});
|
||||
|
||||
// substitute your company or app name here
|
||||
$tfa = new RobThree\Auth\TwoFactorAuth('RobThree TwoFactorAuth');
|
||||
$tfa = new RobThree\Auth\TwoFactorAuth(
|
||||
new RobThree\Auth\Providers\Qr\QRServerProvider(),
|
||||
"Acme Corp" // substitute your company or app name here
|
||||
);
|
||||
?>
|
||||
<li>First create a secret and associate it with a user</li>
|
||||
<?php
|
||||
|
||||
+18
-10
@@ -7,28 +7,36 @@ title: Getting Started
|
||||
|
||||
The best way of making use of this project is by installing it with [composer](https://getcomposer.org/doc/01-basic-usage.md).
|
||||
|
||||
```
|
||||
php composer.phar require robthree/twofactorauth
|
||||
```
|
||||
|
||||
or if you have composer installed globally
|
||||
|
||||
```
|
||||
composer require robthree/twofactorauth
|
||||
```
|
||||
|
||||
## 2. Create an instance
|
||||
|
||||
Now you can create an instance for use with your code
|
||||
`TwoFactorAuth` constructor requires an object able to provide a QR Code image. It is the only mandatory argument. This lets you select your preferred QR Code generator/library.
|
||||
|
||||
See [QR code providers documentation](qr-codes.md) for more information about the different possibilites.
|
||||
|
||||
Example code:
|
||||
|
||||
```php
|
||||
use RobThree\Auth\TwoFactorAuth;
|
||||
use RobThree\Auth\Providers\Qr\QRServerProvider; // if using library's provider
|
||||
use RobThree\Auth\Providers\Qr\BaconQrCodeProvider; // if using Bacon
|
||||
use RobThree\Auth\Providers\Qr\EndroidQrCodeProvider; // if using Endroid
|
||||
|
||||
$tfa = new TwoFactorAuth();
|
||||
// using the default Qr Code provider from the library
|
||||
$tfa = new TwoFactorAuth(new QRServerProvider(), "Your app name");
|
||||
// using Bacon
|
||||
$tfa = new TwoFactorAuth(new BaconQrCodeProvider(), "Your app name");
|
||||
// using Endroid
|
||||
$tfa = new TwoFactorAuth(new EndroidQrCodeProvider(), "Your app name");
|
||||
// using a custom object implementing IQRCodeProvider interface
|
||||
$tfa = new TwoFactorAuth(new MyQrCodeProvider(), "Your app name");
|
||||
// using named argument and a variable
|
||||
$tfa = new TwoFactorAuth(qrcodeprovider: $qrGenerator, issuer: "Your app name");
|
||||
```
|
||||
|
||||
**Note:** if you are not using a framework that uses composer, you should [include the composer loader yourself](https://getcomposer.org/doc/01-basic-usage.md#autoloading)
|
||||
|
||||
## 3. Shared secrets
|
||||
|
||||
When your user is setting up two-factor, or multi-factor, authentication in your project, you can create a secret from the instance.
|
||||
|
||||
@@ -7,30 +7,21 @@ title: Optional Configuration
|
||||
|
||||
The instance (`new TwoFactorAuth()`) can only be configured by the constructor with the following optional arguments
|
||||
|
||||
Argument | Default value | Use
|
||||
------------------|---------------|-----
|
||||
`$issuer` | `null` | Will be displayed in the users app as the default issuer name when using QR code to import the secret
|
||||
`$digits` | `6` | The number of digits the resulting codes will be
|
||||
`$period` | `30` | The number of seconds a code will be valid
|
||||
`$algorithm` | `'sha1'` | The algorithm used (one of `sha1`, `sha256`, `sha512`, `md5`)
|
||||
`$qrcodeprovider` | `null` | QR-code provider
|
||||
`$rngprovider` | `null` | Random Number Generator provider
|
||||
`$timeprovider` | `null` | Time provider
|
||||
Argument | Default value | Use
|
||||
------------------|-------------------|-----
|
||||
`$issuer` | `null` | Will be displayed in the users app as the default issuer name when using QR code to import the secret
|
||||
`$digits` | `6` | The number of digits the resulting codes will be
|
||||
`$period` | `30` | The number of seconds a code will be valid
|
||||
`$algorithm` | `Algorithm::Sha1` | The algorithm used (one of `Algorithm::Sha1`, `Algorithm::Sha256`, `Algorithm::Sha512`, `Algorithm::Md5`)
|
||||
`$qrcodeprovider` | `null` | QR-code provider
|
||||
`$rngprovider` | `null` | Random Number Generator provider
|
||||
`$timeprovider` | `null` | Time provider
|
||||
|
||||
**Note:** the default values for `$digits`, `$period`, and `$algorithm` provide the widest variety of support amongst common authenticator apps such as Google Authenticator. If you choose to use different values for these arguments you will likely have to instruct your users to use a specific app which supports your chosen configuration.
|
||||
|
||||
### RNG providers
|
||||
|
||||
This library also comes with some [Random Number Generator (RNG)](https://en.wikipedia.org/wiki/Random_number_generation) providers. The RNG provider generates a number of random bytes and returns these bytes as a string. These values are then used to create the secret. By default (no RNG provider specified) TwoFactorAuth will try to determine the best available RNG provider to use in this order.
|
||||
|
||||
1. [CSRNGProvider](https://github.com/RobThree/TwoFactorAuth/blob/master/lib/Providers/Rng/CSRNGProvider.php) for PHP7+
|
||||
2. [MCryptRNGProvider](https://github.com/RobThree/TwoFactorAuth/blob/master/lib/Providers/Rng/MCryptRNGProvider.php) where mcrypt is available
|
||||
3. [OpenSSLRNGProvider](https://github.com/RobThree/TwoFactorAuth/blob/master/lib/Providers/Rng/OpenSSLRNGProvider.php) where openssl is available
|
||||
4. [HashRNGProvider](https://github.com/RobThree/TwoFactorAuth/blob/master/lib/Providers/Rng/HashRNGProvider.php) **non-cryptographically secure** fallback
|
||||
|
||||
Each of these RNG providers have some constructor arguments that allow you to tweak some of the settings to use when creating the random bytes.
|
||||
|
||||
You can also implement your own by implementing the [`IRNGProvider` interface](https://github.com/RobThree/TwoFactorAuth/blob/master/lib/Providers/Rng/IRNGProvider.php).
|
||||
Should you feel the need to use a CSPRNG different than `random_bytes()`, you can use the `rngprovider` argument of the constructor to provide an object implementing the [`IRNGProvider`](https://github.com/RobThree/TwoFactorAuth/blob/master/lib/Providers/Rng/IRNGProvider.php) interface.
|
||||
|
||||
### Time providers
|
||||
|
||||
|
||||
+25
-28
@@ -5,7 +5,7 @@ title: QR Codes
|
||||
|
||||
An alternative way of communicating the secret to the user is through the use of [QR Codes](http://en.wikipedia.org/wiki/QR_code) which most if not all authenticator mobile apps can scan.
|
||||
|
||||
This can avoid accidental typing errors and also pre-set some text values within the users app.
|
||||
This can avoid accidental typing errors and also pre-set some text values within the two factor authentication mobile application.
|
||||
|
||||
You can display the QR Code as a base64 encoded image using the instance as follows, supplying the users name or other public identifier as the first argument
|
||||
|
||||
@@ -16,46 +16,43 @@ You can display the QR Code as a base64 encoded image using the instance as foll
|
||||
|
||||
You can also specify a size as a third argument which is 200 by default.
|
||||
|
||||
**Note:** by default, the QR code returned by the instance is generated from a third party across the internet. If the third party is encountering problems or is not available from where you have hosted your code, your user will likely experience a delay in seeing the QR code, if it even loads at all. This can be overcome with offline providers configured when you create the instance.
|
||||
|
||||
## Online Providers
|
||||
|
||||
[QRServerProvider](qr-codes/qr-server.html) (default)
|
||||
|
||||
[ImageChartsQRCodeProvider](qr-codes/image-charts.html)
|
||||
|
||||
[QRicketProvider](qr-codes/qrickit.html)
|
||||
|
||||
## Offline Providers
|
||||
|
||||
[EndroidQrCodeProvider](qr-codes/endroid.html) and EndroidQrCodeWithLogoProvider
|
||||
[EndroidQrCodeProvider](qr-codes/endroid.md) and EndroidQrCodeWithLogoProvider
|
||||
|
||||
[BaconQRCodeProvider](qr-codes/bacon.html)
|
||||
[BaconQRCodeProvider](qr-codes/bacon.md)
|
||||
|
||||
**Note:** offline providers may have additional PHP requirements in order to function, you should study what is required before trying to make use of them.
|
||||
|
||||
## Custom Provider
|
||||
|
||||
If you wish to make your own QR Code provider to reference another service or library, it must implement the [IQRCodeProvider interface](https://github.com/RobThree/TwoFactorAuth/blob/master/lib/Providers/Qr/IQRCodeProvider.php).
|
||||
If you wish to make your own QR Code provider to reference another service or library, it must implement the [IQRCodeProvider interface](../lib/Providers/Qr/IQRCodeProvider.php).
|
||||
|
||||
It is recommended to use similar constructor arguments as the included providers to avoid big shifts when trying different providers.
|
||||
|
||||
## Using a specific provider
|
||||
|
||||
If you do not want to use the default QR code provider, you can specify the one you want to use when you create your instance.
|
||||
Example:
|
||||
|
||||
```php
|
||||
use RobThree\Auth\TwoFactorAuth;
|
||||
|
||||
$qrCodeProvider = new YourChosenProvider();
|
||||
|
||||
$tfa = new TwoFactorAuth(
|
||||
null,
|
||||
6,
|
||||
30,
|
||||
'sha1',
|
||||
$qrCodeProvider
|
||||
);
|
||||
// using a custom object implementing IQRCodeProvider
|
||||
$tfa = new TwoFactorAuth(new MyQrCodeProvider());
|
||||
// using named argument and a variable
|
||||
$tfa = new TwoFactorAuth(qrcodeprovider: $qrGenerator);
|
||||
```
|
||||
|
||||
As you create a new instance of your provider, you can supply any extra configuration there.
|
||||
## Online Providers
|
||||
|
||||
**Warning:** Using an external service for generating QR codes encoding authentication secrets is **not** recommended! You should instead make use of the included offline providers listed above.
|
||||
|
||||
* Gogr.me: [QRServerProvider](qr-codes/qr-server.md)
|
||||
* Image Charts: [ImageChartsQRCodeProvider](qr-codes/image-charts.md)
|
||||
* Qrickit: [QRicketProvider](qr-codes/qrickit.md)
|
||||
* Google Charts: [GoogleChartsQrCodeProvider](qr-codes/google-charts.md)
|
||||
|
||||
Example:
|
||||
|
||||
```php
|
||||
use RobThree\Auth\TwoFactorAuth;
|
||||
use RobThree\Auth\Providers\Qr\GoogleChartsQrCodeProvider;
|
||||
$tfa = new TwoFactorAuth(new GoogleChartsQrCodeProvider());
|
||||
```
|
||||
|
||||
@@ -27,7 +27,7 @@ Argument | Default value
|
||||
If you make use of `EndroidQrCodeWithLogoProvider` then you have access to the `setLogo` function on the provider so you may add a logo to the centre of your QR code.
|
||||
|
||||
```php
|
||||
use RobThree\Auth\TwoFactorAuth\Providers\Qr\EndroidQrCodeWithLogoProvider;
|
||||
use RobThree\Auth\Providers\Qr\EndroidQrCodeWithLogoProvider;
|
||||
|
||||
$qrCodeProvider = new EndroidQrCodeWithLogoProvider();
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
layout: post
|
||||
title: QR GoogleCharts
|
||||
---
|
||||
|
||||
See: https://developers.google.com/chart/infographics/docs/qr_codes
|
||||
|
||||
## Optional Configuration
|
||||
|
||||
Argument | Default value
|
||||
------------------------|---------------
|
||||
`$verifyssl` | `false`
|
||||
`$errorcorrectionlevel` | `'L'`
|
||||
`$margin` | `4`
|
||||
`$encoding` | `'UTF-8'`
|
||||
@@ -7,7 +7,7 @@ title: Image-Charts
|
||||
|
||||
Argument | Default value
|
||||
------------------------|---------------
|
||||
`$verifyssl` | `false`
|
||||
`$verifyssl` | `true`
|
||||
`$errorcorrectionlevel` | `'L'`
|
||||
`$margin` | `4`
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ title: QR Server
|
||||
|
||||
Argument | Default value
|
||||
------------------------|---------------
|
||||
`$verifyssl` | `false`
|
||||
`$verifyssl` | `true`
|
||||
`$errorcorrectionlevel` | `'L'`
|
||||
`$margin` | `4`
|
||||
`$qzone` | `1`
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RobThree\Auth;
|
||||
|
||||
/**
|
||||
* List of supported cryptographic algorithms
|
||||
*/
|
||||
enum Algorithm: string
|
||||
{
|
||||
case Md5 = 'md5';
|
||||
case Sha1 = 'sha1';
|
||||
case Sha256 = 'sha256';
|
||||
case Sha512 = 'sha512';
|
||||
}
|
||||
@@ -1,51 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RobThree\Auth\Providers\Qr;
|
||||
|
||||
use BaconQrCode\Writer;
|
||||
use BaconQrCode\Renderer\ImageRenderer;
|
||||
use BaconQrCode\Renderer\RendererStyle\RendererStyle;
|
||||
use BaconQrCode\Renderer\RendererStyle\Fill;
|
||||
use BaconQrCode\Renderer\Color\Rgb;
|
||||
use BaconQrCode\Renderer\RendererStyle\EyeFill;
|
||||
|
||||
use BaconQrCode\Renderer\Image\EpsImageBackEnd;
|
||||
use BaconQrCode\Renderer\Image\ImageBackEndInterface;
|
||||
use BaconQrCode\Renderer\Image\ImagickImageBackEnd;
|
||||
use BaconQrCode\Renderer\Image\SvgImageBackEnd;
|
||||
use BaconQrCode\Renderer\ImageRenderer;
|
||||
use BaconQrCode\Renderer\RendererStyle\EyeFill;
|
||||
use BaconQrCode\Renderer\RendererStyle\Fill;
|
||||
use BaconQrCode\Renderer\RendererStyle\RendererStyle;
|
||||
use BaconQrCode\Writer;
|
||||
use RuntimeException;
|
||||
|
||||
class BaconQrCodeProvider implements IQRCodeProvider
|
||||
{
|
||||
private $borderWidth = 4; // default from Bacon QR Code
|
||||
private $backgroundColour;
|
||||
private $foregroundColour;
|
||||
private $format;
|
||||
|
||||
/**
|
||||
* Ensure we using the latest Bacon QR Code and specify default options
|
||||
*
|
||||
* @param int $borderWidth space around the QR code, 4 is the default from Bacon QR Code
|
||||
* @param string $backgroundColour hex reference for the background colour
|
||||
* @param string $foregroundColour hex reference for the foreground colour
|
||||
* @param string $format the desired output, png or svg
|
||||
*/
|
||||
public function __construct($borderWidth = 4, $backgroundColour = '#ffffff', $foregroundColour = '#000000', $format = 'png')
|
||||
{
|
||||
if (! class_exists(ImagickImageBackEnd::class)) {
|
||||
throw new \RuntimeException('Make sure you are using version 2 of Bacon QR Code');
|
||||
}
|
||||
|
||||
$this->borderWidth = $borderWidth;
|
||||
$this->backgroundColour = $this->handleColour($backgroundColour);
|
||||
$this->foregroundColour = $this->handleColour($foregroundColour);
|
||||
$this->format = strtolower($format);
|
||||
public function __construct(
|
||||
private readonly int $borderWidth = 4,
|
||||
private string|array $backgroundColour = '#ffffff',
|
||||
private string|array $foregroundColour = '#000000',
|
||||
private string $format = 'png',
|
||||
) {
|
||||
$this->backgroundColour = $this->handleColour($this->backgroundColour);
|
||||
$this->foregroundColour = $this->handleColour($this->foregroundColour);
|
||||
$this->format = strtolower($this->format);
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard functions from IQRCodeProvider
|
||||
*/
|
||||
|
||||
public function getMimeType()
|
||||
public function getMimeType(): string
|
||||
{
|
||||
switch ($this->format) {
|
||||
case 'png':
|
||||
@@ -61,25 +48,20 @@ class BaconQrCodeProvider implements IQRCodeProvider
|
||||
return 'application/postscript';
|
||||
}
|
||||
|
||||
throw new \RuntimeException(sprintf('Unknown MIME-type: %s', $this->format));
|
||||
throw new RuntimeException(sprintf('Unknown MIME-type: %s', $this->format));
|
||||
}
|
||||
|
||||
public function getQRCodeImage($qrText, $size)
|
||||
public function getQRCodeImage(string $qrText, int $size): string
|
||||
{
|
||||
switch ($this->format) {
|
||||
case 'svg':
|
||||
$backend = new SvgImageBackEnd;
|
||||
break;
|
||||
case 'eps':
|
||||
$backend = new EpsImageBackEnd;
|
||||
break;
|
||||
default:
|
||||
$backend = new ImagickImageBackEnd($this->format);
|
||||
}
|
||||
$backend = match ($this->format) {
|
||||
'svg' => new SvgImageBackEnd(),
|
||||
'eps' => new EpsImageBackEnd(),
|
||||
default => new ImagickImageBackEnd($this->format),
|
||||
};
|
||||
|
||||
$output = $this->getQRCodeByBackend($qrText, $size, $backend);
|
||||
|
||||
if ($this->format == 'svg') {
|
||||
if ($this->format === 'svg') {
|
||||
$svg = explode("\n", $output);
|
||||
return $svg[1];
|
||||
}
|
||||
@@ -96,7 +78,7 @@ class BaconQrCodeProvider implements IQRCodeProvider
|
||||
$rendererStyleArgs = array($size, $this->borderWidth);
|
||||
|
||||
if (is_array($this->foregroundColour) && is_array($this->backgroundColour)) {
|
||||
$rendererStyleArgs = array_merge($rendererStyleArgs, array(
|
||||
$rendererStyleArgs = array(...$rendererStyleArgs, ...array(
|
||||
null,
|
||||
null,
|
||||
Fill::withForegroundColor(
|
||||
@@ -105,7 +87,7 @@ class BaconQrCodeProvider implements IQRCodeProvider
|
||||
new EyeFill(null, null),
|
||||
new EyeFill(null, null),
|
||||
new EyeFill(null, null)
|
||||
)
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -121,16 +103,16 @@ class BaconQrCodeProvider implements IQRCodeProvider
|
||||
* Ensure colour is an array of three values but also
|
||||
* accept a string and assume its a 3 or 6 character hex
|
||||
*/
|
||||
private function handleColour($colour)
|
||||
private function handleColour(array|string $colour): array|string
|
||||
{
|
||||
if (is_string($colour) && $colour[0] == '#') {
|
||||
$hexToRGB = function ($input) {
|
||||
$hexToRGB = static function ($input) {
|
||||
// ensure input no longer has a # for more predictable division
|
||||
// PHP 8.1 does not like implicitly casting a float to an int
|
||||
$input = trim($input, '#');
|
||||
|
||||
if (strlen($input) != 3 && strlen($input) != 6) {
|
||||
throw new \RuntimeException('Colour should be a 3 or 6 character value after the #');
|
||||
throw new RuntimeException('Colour should be a 3 or 6 character value after the #');
|
||||
}
|
||||
|
||||
// split the array into three chunks
|
||||
@@ -138,7 +120,7 @@ class BaconQrCodeProvider implements IQRCodeProvider
|
||||
|
||||
// cope with three character hex reference
|
||||
if (strlen($input) == 3) {
|
||||
array_walk($split, function (&$character) {
|
||||
array_walk($split, static function (&$character) {
|
||||
$character = str_repeat($character, 2);
|
||||
});
|
||||
}
|
||||
@@ -154,6 +136,6 @@ class BaconQrCodeProvider implements IQRCodeProvider
|
||||
return $colour;
|
||||
}
|
||||
|
||||
throw new \RuntimeException('Invalid colour value');
|
||||
throw new RuntimeException('Invalid colour value');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RobThree\Auth\Providers\Qr;
|
||||
|
||||
abstract class BaseHTTPQRCodeProvider implements IQRCodeProvider
|
||||
{
|
||||
/** @var bool */
|
||||
protected $verifyssl;
|
||||
protected bool $verifyssl = true;
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
*
|
||||
* @return string|bool
|
||||
*/
|
||||
protected function getContent($url)
|
||||
protected function getContent(string $url): string
|
||||
{
|
||||
$curlhandle = curl_init();
|
||||
|
||||
@@ -23,11 +19,13 @@ abstract class BaseHTTPQRCodeProvider implements IQRCodeProvider
|
||||
CURLOPT_DNS_CACHE_TIMEOUT => 10,
|
||||
CURLOPT_TIMEOUT => 10,
|
||||
CURLOPT_SSL_VERIFYPEER => $this->verifyssl,
|
||||
CURLOPT_USERAGENT => 'TwoFactorAuth'
|
||||
CURLOPT_USERAGENT => 'TwoFactorAuth',
|
||||
));
|
||||
$data = curl_exec($curlhandle);
|
||||
if ($data === false) {
|
||||
throw new QRException(curl_error($curlhandle));
|
||||
}
|
||||
|
||||
curl_close($curlhandle);
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RobThree\Auth\Providers\Qr;
|
||||
|
||||
use Endroid\QrCode\Color\Color;
|
||||
use Endroid\QrCode\ErrorCorrectionLevel;
|
||||
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelHigh;
|
||||
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelInterface;
|
||||
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelLow;
|
||||
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelMedium;
|
||||
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelQuartile;
|
||||
@@ -13,15 +17,24 @@ use Endroid\QrCode\Writer\PngWriter;
|
||||
class EndroidQrCodeProvider implements IQRCodeProvider
|
||||
{
|
||||
public $bgcolor;
|
||||
|
||||
public $color;
|
||||
|
||||
public $margin;
|
||||
|
||||
public $errorcorrectionlevel;
|
||||
|
||||
protected $endroid4 = false;
|
||||
|
||||
protected $endroid5 = false;
|
||||
|
||||
protected $endroid6 = false;
|
||||
|
||||
public function __construct($bgcolor = 'ffffff', $color = '000000', $margin = 0, $errorcorrectionlevel = 'H')
|
||||
{
|
||||
$this->endroid4 = method_exists(QrCode::class, 'create');
|
||||
$this->endroid5 = enum_exists(ErrorCorrectionLevel::class);
|
||||
$this->endroid6 = $this->endroid5 && !method_exists(QrCode::class, 'setSize');
|
||||
$this->endroid4 = $this->endroid6 || method_exists(QrCode::class, 'create');
|
||||
|
||||
$this->bgcolor = $this->handleColor($bgcolor);
|
||||
$this->color = $this->handleColor($color);
|
||||
@@ -29,56 +42,82 @@ class EndroidQrCodeProvider implements IQRCodeProvider
|
||||
$this->errorcorrectionlevel = $this->handleErrorCorrectionLevel($errorcorrectionlevel);
|
||||
}
|
||||
|
||||
public function getMimeType()
|
||||
public function getMimeType(): string
|
||||
{
|
||||
return 'image/png';
|
||||
}
|
||||
|
||||
public function getQRCodeImage($qrtext, $size)
|
||||
public function getQRCodeImage(string $qrText, int $size): string
|
||||
{
|
||||
if (!$this->endroid4) {
|
||||
return $this->qrCodeInstance($qrtext, $size)->writeString();
|
||||
return $this->qrCodeInstance($qrText, $size)->writeString();
|
||||
}
|
||||
|
||||
$writer = new PngWriter();
|
||||
return $writer->write($this->qrCodeInstance($qrtext, $size))->getString();
|
||||
return $writer->write($this->qrCodeInstance($qrText, $size))->getString();
|
||||
}
|
||||
|
||||
protected function qrCodeInstance($qrtext, $size)
|
||||
protected function qrCodeInstance(string $qrText, int $size): QrCode
|
||||
{
|
||||
$qrCode = new QrCode($qrtext);
|
||||
if ($this->endroid6) {
|
||||
return new QrCode(
|
||||
data: $qrText,
|
||||
errorCorrectionLevel: $this->errorcorrectionlevel,
|
||||
size: $size,
|
||||
margin: $this->margin,
|
||||
foregroundColor: $this->color,
|
||||
backgroundColor: $this->bgcolor
|
||||
);
|
||||
}
|
||||
|
||||
$qrCode = new QrCode($qrText);
|
||||
$qrCode->setSize($size);
|
||||
|
||||
$qrCode->setErrorCorrectionLevel($this->errorcorrectionlevel);
|
||||
$qrCode->setMargin($this->margin);
|
||||
$qrCode->setBackgroundColor($this->bgcolor);
|
||||
$qrCode->setForegroundColor($this->color);
|
||||
|
||||
return $qrCode;
|
||||
}
|
||||
|
||||
private function handleColor($color)
|
||||
private function handleColor(string $color): Color|array
|
||||
{
|
||||
$split = str_split($color, 2);
|
||||
$r = hexdec($split[0]);
|
||||
$g = hexdec($split[1]);
|
||||
$b = hexdec($split[2]);
|
||||
|
||||
return $this->endroid4 ? new Color($r, $g, $b, 0) : ['r' => $r, 'g' => $g, 'b' => $b, 'a' => 0];
|
||||
return $this->endroid4 ? new Color($r, $g, $b, 0) : array('r' => $r, 'g' => $g, 'b' => $b, 'a' => 0);
|
||||
}
|
||||
|
||||
private function handleErrorCorrectionLevel($level)
|
||||
private function handleErrorCorrectionLevel(string $level): ErrorCorrectionLevelInterface|ErrorCorrectionLevel
|
||||
{
|
||||
switch ($level) {
|
||||
case 'L':
|
||||
return $this->endroid4 ? new ErrorCorrectionLevelLow() : ErrorCorrectionLevel::LOW();
|
||||
case 'M':
|
||||
return $this->endroid4 ? new ErrorCorrectionLevelMedium() : ErrorCorrectionLevel::MEDIUM();
|
||||
case 'Q':
|
||||
return $this->endroid4 ? new ErrorCorrectionLevelQuartile() : ErrorCorrectionLevel::QUARTILE();
|
||||
case 'H':
|
||||
default:
|
||||
return $this->endroid4 ? new ErrorCorrectionLevelHigh() : ErrorCorrectionLevel::HIGH();
|
||||
// First check for version 5 (using enums)
|
||||
if ($this->endroid5) {
|
||||
return match ($level) {
|
||||
'L' => ErrorCorrectionLevel::Low,
|
||||
'M' => ErrorCorrectionLevel::Medium,
|
||||
'Q' => ErrorCorrectionLevel::Quartile,
|
||||
default => ErrorCorrectionLevel::High,
|
||||
};
|
||||
}
|
||||
|
||||
// If not check for version 4 (using classes)
|
||||
if ($this->endroid4) {
|
||||
return match ($level) {
|
||||
'L' => new ErrorCorrectionLevelLow(),
|
||||
'M' => new ErrorCorrectionLevelMedium(),
|
||||
'Q' => new ErrorCorrectionLevelQuartile(),
|
||||
default => new ErrorCorrectionLevelHigh(),
|
||||
};
|
||||
}
|
||||
|
||||
// Any other version will be using strings
|
||||
return match ($level) {
|
||||
'L' => ErrorCorrectionLevel::LOW(),
|
||||
'M' => ErrorCorrectionLevel::MEDIUM(),
|
||||
'Q' => ErrorCorrectionLevel::QUARTILE(),
|
||||
default => ErrorCorrectionLevel::HIGH(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RobThree\Auth\Providers\Qr;
|
||||
|
||||
use Endroid\QrCode\Logo\Logo;
|
||||
use Endroid\QrCode\QrCode;
|
||||
use Endroid\QrCode\Writer\PngWriter;
|
||||
|
||||
class EndroidQrCodeWithLogoProvider extends EndroidQrCodeProvider
|
||||
{
|
||||
protected $logoPath;
|
||||
|
||||
protected $logoSize;
|
||||
|
||||
/**
|
||||
@@ -20,33 +25,38 @@ class EndroidQrCodeWithLogoProvider extends EndroidQrCodeProvider
|
||||
$this->logoSize = (array)$size;
|
||||
}
|
||||
|
||||
public function getQRCodeImage($qrtext, $size)
|
||||
public function getQRCodeImage(string $qrText, int $size): string
|
||||
{
|
||||
if (!$this->endroid4) {
|
||||
return $this->qrCodeInstance($qrtext, $size)->writeString();
|
||||
return $this->qrCodeInstance($qrText, $size)->writeString();
|
||||
}
|
||||
|
||||
$logo = null;
|
||||
if ($this->logoPath) {
|
||||
$logo = Logo::create($this->logoPath);
|
||||
if ($this->logoSize) {
|
||||
$logo->setResizeToWidth($this->logoSize[0]);
|
||||
if (isset($this->logoSize[1])) {
|
||||
$logo->setResizeToHeight($this->logoSize[1]);
|
||||
if ($this->endroid6) {
|
||||
$logo = new Logo($this->logoPath, ...$this->logoSize);
|
||||
} else {
|
||||
$logo = Logo::create($this->logoPath);
|
||||
if ($this->logoSize) {
|
||||
$logo->setResizeToWidth($this->logoSize[0]);
|
||||
if (isset($this->logoSize[1])) {
|
||||
$logo->setResizeToHeight($this->logoSize[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$writer = new PngWriter();
|
||||
return $writer->write($this->qrCodeInstance($qrtext, $size), $logo)->getString();
|
||||
return $writer->write($this->qrCodeInstance($qrText, $size), $logo)->getString();
|
||||
}
|
||||
|
||||
protected function qrCodeInstance($qrtext, $size) {
|
||||
$qrCode = parent::qrCodeInstance($qrtext, $size);
|
||||
protected function qrCodeInstance(string $qrText, int $size): QrCode
|
||||
{
|
||||
$qrCode = parent::qrCodeInstance($qrText, $size);
|
||||
|
||||
if (!$this->endroid4 && $this->logoPath) {
|
||||
$qrCode->setLogoPath($this->logoPath);
|
||||
if ($this->logoSize) {
|
||||
$qrCode->setLogoSize($this->logoSize[0], isset($this->logoSize[1]) ? $this->logoSize[1] : null);
|
||||
$qrCode->setLogoSize($this->logoSize[0], $this->logoSize[1] ?? null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,67 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RobThree\Auth\Providers\Qr;
|
||||
|
||||
// https://developers.google.com/chart/infographics/docs/qr_codes
|
||||
class GoogleChartsQrCodeProvider extends BaseHTTPQRCodeProvider
|
||||
{
|
||||
/** @var string */
|
||||
public $errorcorrectionlevel;
|
||||
|
||||
/** @var int */
|
||||
public $margin;
|
||||
|
||||
/** @var string */
|
||||
public $encoding;
|
||||
|
||||
/**
|
||||
* @param bool $verifyssl
|
||||
* @param string $errorcorrectionlevel
|
||||
* @param int $margin
|
||||
* @param string $encoding
|
||||
*/
|
||||
public function __construct($verifyssl = false, $errorcorrectionlevel = 'L', $margin = 4, $encoding = 'UTF-8')
|
||||
public function __construct(protected bool $verifyssl = true, public string $errorcorrectionlevel = 'L', public int $margin = 4, public string $encoding = 'UTF-8')
|
||||
{
|
||||
if (!is_bool($verifyssl)) {
|
||||
throw new QRException('VerifySSL must be bool');
|
||||
}
|
||||
|
||||
$this->verifyssl = $verifyssl;
|
||||
|
||||
$this->errorcorrectionlevel = $errorcorrectionlevel;
|
||||
$this->margin = $margin;
|
||||
$this->encoding = $encoding;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getMimeType()
|
||||
public function getMimeType(): string
|
||||
{
|
||||
return 'image/png';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getQRCodeImage($qrtext, $size)
|
||||
|
||||
public function getQRCodeImage(string $qrText, int $size): string
|
||||
{
|
||||
return $this->getContent($this->getUrl($qrtext, $size));
|
||||
return $this->getContent($this->getUrl($qrText, $size));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $qrtext the value to encode in the QR code
|
||||
* @param int|string $size the desired size of the QR code
|
||||
*
|
||||
* @return string file contents of the QR code
|
||||
*/
|
||||
public function getUrl($qrtext, $size)
|
||||
public function getUrl(string $qrText, int $size): string
|
||||
{
|
||||
return 'https://chart.googleapis.com/chart'
|
||||
. '?chs=' . $size . 'x' . $size
|
||||
. '&chld=' . urlencode(strtoupper($this->errorcorrectionlevel) . '|' . $this->margin)
|
||||
. '&cht=' . 'qr'
|
||||
. '&choe=' . $this->encoding
|
||||
. '&chl=' . rawurlencode($qrtext);
|
||||
$queryParameters = array(
|
||||
'chs' => $size . 'x' . $size,
|
||||
'chld' => strtoupper($this->errorcorrectionlevel) . '|' . $this->margin,
|
||||
'cht' => 'qr',
|
||||
'choe' => $this->encoding,
|
||||
'chl' => $qrText,
|
||||
);
|
||||
|
||||
return 'https://chart.googleapis.com/chart?' . http_build_query($queryParameters);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RobThree\Auth\Providers\Qr;
|
||||
|
||||
use function base64_decode;
|
||||
use function preg_match;
|
||||
|
||||
trait HandlesDataUri
|
||||
{
|
||||
/**
|
||||
* @param string $datauri
|
||||
*
|
||||
* @return null|array
|
||||
/**
|
||||
* @return array<string, string>|null
|
||||
*/
|
||||
private function DecodeDataUri($datauri)
|
||||
private function DecodeDataUri(string $datauri): ?array
|
||||
{
|
||||
if (preg_match('/data:(?P<mimetype>[\w\.\-\+\/]+);(?P<encoding>\w+),(?P<data>.*)/', $datauri, $m) === 1) {
|
||||
return array(
|
||||
'mimetype' => $m['mimetype'],
|
||||
'encoding' => $m['encoding'],
|
||||
'data' => base64_decode($m['data'])
|
||||
'data' => base64_decode($m['data'], true),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RobThree\Auth\Providers\Qr;
|
||||
|
||||
interface IQRCodeProvider
|
||||
@@ -7,18 +9,16 @@ interface IQRCodeProvider
|
||||
/**
|
||||
* Generate and return the QR code to embed in a web page
|
||||
*
|
||||
* @param string $qrtext the value to encode in the QR code
|
||||
* @param string $qrText the value to encode in the QR code
|
||||
* @param int $size the desired size of the QR code
|
||||
*
|
||||
* @return string file contents of the QR code
|
||||
*/
|
||||
public function getQRCodeImage($qrtext, $size);
|
||||
public function getQRCodeImage(string $qrText, int $size): string;
|
||||
|
||||
/**
|
||||
* Returns the appropriate mime type for the QR code
|
||||
* that will be generated
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getMimeType();
|
||||
public function getMimeType(): string;
|
||||
}
|
||||
|
||||
@@ -1,60 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RobThree\Auth\Providers\Qr;
|
||||
|
||||
// https://image-charts.com
|
||||
/**
|
||||
* Use https://image-charts.com to provide a QR code
|
||||
*/
|
||||
class ImageChartsQRCodeProvider extends BaseHTTPQRCodeProvider
|
||||
{
|
||||
/** @var string */
|
||||
public $errorcorrectionlevel;
|
||||
|
||||
/** @var int */
|
||||
public $margin;
|
||||
|
||||
/**
|
||||
* @param bool $verifyssl
|
||||
* @param string $errorcorrectionlevel
|
||||
* @param int $margin
|
||||
*/
|
||||
public function __construct($verifyssl = false, $errorcorrectionlevel = 'L', $margin = 1)
|
||||
public function __construct(protected bool $verifyssl = true, public string $errorcorrectionlevel = 'L', public int $margin = 1)
|
||||
{
|
||||
if (!is_bool($verifyssl)) {
|
||||
throw new QRException('VerifySSL must be bool');
|
||||
}
|
||||
|
||||
$this->verifyssl = $verifyssl;
|
||||
|
||||
$this->errorcorrectionlevel = $errorcorrectionlevel;
|
||||
$this->margin = $margin;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getMimeType()
|
||||
public function getMimeType(): string
|
||||
{
|
||||
return 'image/png';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getQRCodeImage($qrtext, $size)
|
||||
public function getQRCodeImage(string $qrText, int $size): string
|
||||
{
|
||||
return $this->getContent($this->getUrl($qrtext, $size));
|
||||
return $this->getContent($this->getUrl($qrText, $size));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $qrtext the value to encode in the QR code
|
||||
* @param int $size the desired size of the QR code
|
||||
*
|
||||
* @return string file contents of the QR code
|
||||
*/
|
||||
public function getUrl($qrtext, $size)
|
||||
public function getUrl(string $qrText, int $size): string
|
||||
{
|
||||
return 'https://image-charts.com/chart?cht=qr'
|
||||
. '&chs=' . ceil($size / 2) . 'x' . ceil($size / 2)
|
||||
. '&chld=' . $this->errorcorrectionlevel . '|' . $this->margin
|
||||
. '&chl=' . rawurlencode($qrtext);
|
||||
$queryParameters = array(
|
||||
'cht' => 'qr',
|
||||
'chs' => ceil($size / 2) . 'x' . ceil($size / 2),
|
||||
'chld' => $this->errorcorrectionlevel . '|' . $this->margin,
|
||||
'chl' => $qrText,
|
||||
);
|
||||
|
||||
return 'https://image-charts.com/chart?' . http_build_query($queryParameters);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RobThree\Auth\Providers\Qr;
|
||||
|
||||
use RobThree\Auth\TwoFactorAuthException;
|
||||
|
||||
class QRException extends TwoFactorAuthException {}
|
||||
class QRException extends TwoFactorAuthException
|
||||
{
|
||||
}
|
||||
|
||||
@@ -1,57 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RobThree\Auth\Providers\Qr;
|
||||
|
||||
// http://goqr.me/api/doc/create-qr-code/
|
||||
/**
|
||||
* Use https://goqr.me/api/doc/create-qr-code/ to get QR code
|
||||
*/
|
||||
class QRServerProvider extends BaseHTTPQRCodeProvider
|
||||
{
|
||||
/** @var string */
|
||||
public $errorcorrectionlevel;
|
||||
|
||||
/** @var int */
|
||||
public $margin;
|
||||
|
||||
/** @var int */
|
||||
public $qzone;
|
||||
|
||||
/** @var string */
|
||||
public $bgcolor;
|
||||
|
||||
/** @var string */
|
||||
public $color;
|
||||
|
||||
/** @var string */
|
||||
public $format;
|
||||
|
||||
/**
|
||||
* @param bool $verifyssl
|
||||
* @param string $errorcorrectionlevel
|
||||
* @param int $margin
|
||||
* @param int $qzone
|
||||
* @param string $bgcolor
|
||||
* @param string $color
|
||||
* @param string $format
|
||||
*/
|
||||
public function __construct($verifyssl = false, $errorcorrectionlevel = 'L', $margin = 4, $qzone = 1, $bgcolor = 'ffffff', $color = '000000', $format = 'png')
|
||||
public function __construct(protected bool $verifyssl = true, public string $errorcorrectionlevel = 'L', public int $margin = 4, public int $qzone = 1, public string $bgcolor = 'ffffff', public string $color = '000000', public string $format = 'png')
|
||||
{
|
||||
if (!is_bool($verifyssl)) {
|
||||
throw new QRException('VerifySSL must be bool');
|
||||
}
|
||||
|
||||
$this->verifyssl = $verifyssl;
|
||||
|
||||
$this->errorcorrectionlevel = $errorcorrectionlevel;
|
||||
$this->margin = $margin;
|
||||
$this->qzone = $qzone;
|
||||
$this->bgcolor = $bgcolor;
|
||||
$this->color = $color;
|
||||
$this->format = $format;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getMimeType()
|
||||
public function getMimeType(): string
|
||||
{
|
||||
switch (strtolower($this->format)) {
|
||||
case 'png':
|
||||
@@ -69,40 +31,29 @@ class QRServerProvider extends BaseHTTPQRCodeProvider
|
||||
throw new QRException(sprintf('Unknown MIME-type: %s', $this->format));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getQRCodeImage($qrtext, $size)
|
||||
public function getQRCodeImage(string $qrText, int $size): string
|
||||
{
|
||||
return $this->getContent($this->getUrl($qrtext, $size));
|
||||
return $this->getContent($this->getUrl($qrText, $size));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $value
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function decodeColor($value)
|
||||
public function getUrl(string $qrText, int $size): string
|
||||
{
|
||||
return vsprintf('%d-%d-%d', sscanf($value, "%02x%02x%02x"));
|
||||
$queryParameters = array(
|
||||
'size' => $size . 'x' . $size,
|
||||
'ecc' => strtoupper($this->errorcorrectionlevel),
|
||||
'margin' => $this->margin,
|
||||
'qzone' => $this->qzone,
|
||||
'bgcolor' => $this->decodeColor($this->bgcolor),
|
||||
'color' => $this->decodeColor($this->color),
|
||||
'format' => strtolower($this->format),
|
||||
'data' => $qrText,
|
||||
);
|
||||
|
||||
return 'https://api.qrserver.com/v1/create-qr-code/?' . http_build_query($queryParameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $qrtext the value to encode in the QR code
|
||||
* @param int|string $size the desired size of the QR code
|
||||
*
|
||||
* @return string file contents of the QR code
|
||||
*/
|
||||
public function getUrl($qrtext, $size)
|
||||
private function decodeColor(string $value): string
|
||||
{
|
||||
return 'https://api.qrserver.com/v1/create-qr-code/'
|
||||
. '?size=' . $size . 'x' . $size
|
||||
. '&ecc=' . strtoupper($this->errorcorrectionlevel)
|
||||
. '&margin=' . $this->margin
|
||||
. '&qzone=' . $this->qzone
|
||||
. '&bgcolor=' . $this->decodeColor($this->bgcolor)
|
||||
. '&color=' . $this->decodeColor($this->color)
|
||||
. '&format=' . strtolower($this->format)
|
||||
. '&data=' . rawurlencode($qrtext);
|
||||
return vsprintf('%d-%d-%d', sscanf($value, '%02x%02x%02x'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,42 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RobThree\Auth\Providers\Qr;
|
||||
|
||||
// http://qrickit.com/qrickit_apps/qrickit_api.php
|
||||
/**
|
||||
* Use http://qrickit.com/qrickit_apps/qrickit_api.php to provide a QR code
|
||||
*/
|
||||
class QRicketProvider extends BaseHTTPQRCodeProvider
|
||||
{
|
||||
/** @var string */
|
||||
public $errorcorrectionlevel;
|
||||
|
||||
/** @var string */
|
||||
public $bgcolor;
|
||||
|
||||
/** @var string */
|
||||
public $color;
|
||||
|
||||
/** @var string */
|
||||
public $format;
|
||||
|
||||
/**
|
||||
* @param string $errorcorrectionlevel
|
||||
* @param string $bgcolor
|
||||
* @param string $color
|
||||
* @param string $format
|
||||
*/
|
||||
public function __construct($errorcorrectionlevel = 'L', $bgcolor = 'ffffff', $color = '000000', $format = 'p')
|
||||
public function __construct(protected bool $verifyssl = true, public string $errorcorrectionlevel = 'L', public string $bgcolor = 'ffffff', public string $color = '000000', public string $format = 'p')
|
||||
{
|
||||
$this->verifyssl = false;
|
||||
|
||||
$this->errorcorrectionlevel = $errorcorrectionlevel;
|
||||
$this->bgcolor = $bgcolor;
|
||||
$this->color = $color;
|
||||
$this->format = $format;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getMimeType()
|
||||
public function getMimeType(): string
|
||||
{
|
||||
switch (strtolower($this->format)) {
|
||||
case 'p':
|
||||
@@ -49,28 +26,22 @@ class QRicketProvider extends BaseHTTPQRCodeProvider
|
||||
throw new QRException(sprintf('Unknown MIME-type: %s', $this->format));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getQRCodeImage($qrtext, $size)
|
||||
public function getQRCodeImage(string $qrText, int $size): string
|
||||
{
|
||||
return $this->getContent($this->getUrl($qrtext, $size));
|
||||
return $this->getContent($this->getUrl($qrText, $size));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $qrtext the value to encode in the QR code
|
||||
* @param int|string $size the desired size of the QR code
|
||||
*
|
||||
* @return string file contents of the QR code
|
||||
*/
|
||||
public function getUrl($qrtext, $size)
|
||||
public function getUrl(string $qrText, int $size): string
|
||||
{
|
||||
return 'http://qrickit.com/api/qr'
|
||||
. '?qrsize=' . $size
|
||||
. '&e=' . strtolower($this->errorcorrectionlevel)
|
||||
. '&bgdcolor=' . $this->bgcolor
|
||||
. '&fgdcolor=' . $this->color
|
||||
. '&t=' . strtolower($this->format)
|
||||
. '&d=' . rawurlencode($qrtext);
|
||||
$queryParameters = array(
|
||||
'qrsize' => $size,
|
||||
'e' => strtolower($this->errorcorrectionlevel),
|
||||
'bgdcolor' => $this->bgcolor,
|
||||
'fgdcolor' => $this->color,
|
||||
't' => strtolower($this->format),
|
||||
'd' => $qrText,
|
||||
);
|
||||
|
||||
return 'https://qrickit.com/api/qr?' . http_build_query($queryParameters);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RobThree\Auth\Providers\Rng;
|
||||
|
||||
class CSRNGProvider implements IRNGProvider
|
||||
@@ -7,16 +9,8 @@ class CSRNGProvider implements IRNGProvider
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getRandomBytes($bytecount)
|
||||
public function getRandomBytes(int $bytecount): string
|
||||
{
|
||||
return random_bytes($bytecount); // PHP7+
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isCryptographicallySecure()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace RobThree\Auth\Providers\Rng;
|
||||
|
||||
class HashRNGProvider implements IRNGProvider
|
||||
{
|
||||
/** @var string */
|
||||
private $algorithm;
|
||||
|
||||
/**
|
||||
* @param string $algorithm
|
||||
*/
|
||||
public function __construct($algorithm = 'sha256')
|
||||
{
|
||||
$algos = array_values(hash_algos());
|
||||
if (!in_array($algorithm, $algos, true)) {
|
||||
throw new RNGException('Unsupported algorithm specified');
|
||||
}
|
||||
$this->algorithm = $algorithm;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getRandomBytes($bytecount)
|
||||
{
|
||||
$result = '';
|
||||
$hash = mt_rand();
|
||||
for ($i = 0; $i < $bytecount; $i++) {
|
||||
$hash = hash($this->algorithm, $hash . mt_rand(), true);
|
||||
$result .= $hash[mt_rand(0, strlen($hash) - 1)];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isCryptographicallySecure()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RobThree\Auth\Providers\Rng;
|
||||
|
||||
interface IRNGProvider
|
||||
{
|
||||
/**
|
||||
* @param int $bytecount the number of bytes of randomness to return
|
||||
*
|
||||
* @return string the random bytes
|
||||
*/
|
||||
public function getRandomBytes($bytecount);
|
||||
|
||||
/**
|
||||
* @return bool whether this provider is cryptographically secure
|
||||
*/
|
||||
public function isCryptographicallySecure();
|
||||
public function getRandomBytes(int $bytecount): string;
|
||||
}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace RobThree\Auth\Providers\Rng;
|
||||
|
||||
class MCryptRNGProvider implements IRNGProvider
|
||||
{
|
||||
/** @var int */
|
||||
private $source;
|
||||
|
||||
/**
|
||||
* @param int $source
|
||||
*/
|
||||
public function __construct($source = MCRYPT_DEV_URANDOM)
|
||||
{
|
||||
$this->source = $source;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getRandomBytes($bytecount)
|
||||
{
|
||||
$result = @mcrypt_create_iv($bytecount, $this->source);
|
||||
if ($result === false) {
|
||||
throw new RNGException('mcrypt_create_iv returned an invalid value');
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isCryptographicallySecure()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace RobThree\Auth\Providers\Rng;
|
||||
|
||||
class OpenSSLRNGProvider implements IRNGProvider
|
||||
{
|
||||
/** @var bool */
|
||||
private $requirestrong;
|
||||
|
||||
/**
|
||||
* @param bool $requirestrong
|
||||
*/
|
||||
public function __construct($requirestrong = true)
|
||||
{
|
||||
$this->requirestrong = $requirestrong;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getRandomBytes($bytecount)
|
||||
{
|
||||
$result = openssl_random_pseudo_bytes($bytecount, $crypto_strong);
|
||||
if ($this->requirestrong && ($crypto_strong === false)) {
|
||||
throw new RNGException('openssl_random_pseudo_bytes returned non-cryptographically strong value');
|
||||
}
|
||||
if ($result === false) {
|
||||
throw new RNGException('openssl_random_pseudo_bytes returned an invalid value');
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isCryptographicallySecure()
|
||||
{
|
||||
return $this->requirestrong;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RobThree\Auth\Providers\Rng;
|
||||
|
||||
use RobThree\Auth\TwoFactorAuthException;
|
||||
|
||||
class RNGException extends TwoFactorAuthException {}
|
||||
class RNGException extends TwoFactorAuthException
|
||||
{
|
||||
}
|
||||
|
||||
@@ -1,34 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RobThree\Auth\Providers\Time;
|
||||
|
||||
use DateTime;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Takes the time from any webserver by doing a HEAD request on the specified URL and extracting the 'Date:' header
|
||||
*/
|
||||
class HttpTimeProvider implements ITimeProvider
|
||||
{
|
||||
/** @var string */
|
||||
public $url;
|
||||
|
||||
/** @var string */
|
||||
public $expectedtimeformat;
|
||||
|
||||
/** @var array */
|
||||
public $options;
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @param string $expectedtimeformat
|
||||
* @param array $options
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
public function __construct($url = 'https://google.com', $expectedtimeformat = 'D, d M Y H:i:s O+', array $options = null)
|
||||
{
|
||||
$this->url = $url;
|
||||
$this->expectedtimeformat = $expectedtimeformat;
|
||||
if ($options === null) {
|
||||
$options = array(
|
||||
public function __construct(
|
||||
public string $url = 'https://google.com',
|
||||
public string $expectedtimeformat = 'D, d M Y H:i:s O+',
|
||||
public ?array $options = null,
|
||||
) {
|
||||
if ($this->options === null) {
|
||||
$this->options = array(
|
||||
'http' => array(
|
||||
'method' => 'HEAD',
|
||||
'follow_location' => false,
|
||||
@@ -38,12 +31,11 @@ class HttpTimeProvider implements ITimeProvider
|
||||
'header' => array(
|
||||
'Connection: close',
|
||||
'User-agent: TwoFactorAuth HttpTimeProvider (https://github.com/RobThree/TwoFactorAuth)',
|
||||
'Cache-Control: no-cache'
|
||||
)
|
||||
)
|
||||
'Cache-Control: no-cache',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
$this->options = $options;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,7 +44,7 @@ class HttpTimeProvider implements ITimeProvider
|
||||
public function getTime()
|
||||
{
|
||||
try {
|
||||
$context = stream_context_create($this->options);
|
||||
$context = stream_context_create($this->options);
|
||||
$fd = fopen($this->url, 'rb', false, $context);
|
||||
$headers = stream_get_meta_data($fd);
|
||||
fclose($fd);
|
||||
@@ -62,10 +54,9 @@ class HttpTimeProvider implements ITimeProvider
|
||||
return DateTime::createFromFormat($this->expectedtimeformat, trim(substr($h, 5)))->getTimestamp();
|
||||
}
|
||||
}
|
||||
throw new \Exception('Invalid or no "Date:" header found');
|
||||
} catch (\Exception $ex) {
|
||||
throw new Exception('Invalid or no "Date:" header found');
|
||||
} catch (Exception $ex) {
|
||||
throw new TimeException(sprintf('Unable to retrieve time from %s (%s)', $this->url, $ex->getMessage()));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RobThree\Auth\Providers\Time;
|
||||
|
||||
interface ITimeProvider
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RobThree\Auth\Providers\Time;
|
||||
|
||||
class LocalMachineTimeProvider implements ITimeProvider
|
||||
|
||||
@@ -1,39 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RobThree\Auth\Providers\Time;
|
||||
|
||||
use Exception;
|
||||
|
||||
use function socket_create;
|
||||
|
||||
/**
|
||||
* Takes the time from any NTP server
|
||||
*/
|
||||
class NTPTimeProvider implements ITimeProvider
|
||||
{
|
||||
/** @var string */
|
||||
public $host;
|
||||
|
||||
/** @var int */
|
||||
public $port;
|
||||
|
||||
/** @var int */
|
||||
public $timeout;
|
||||
|
||||
/**
|
||||
* @param string $host
|
||||
* @param int $port
|
||||
* @param int $timeout
|
||||
*/
|
||||
public function __construct($host = 'time.google.com', $port = 123, $timeout = 1)
|
||||
public function __construct(public string $host = 'time.google.com', public int $port = 123, public int $timeout = 1)
|
||||
{
|
||||
$this->host = $host;
|
||||
|
||||
if (!is_int($port) || $port <= 0 || $port > 65535) {
|
||||
if ($this->port <= 0 || $this->port > 65535) {
|
||||
throw new TimeException('Port must be 0 < port < 65535');
|
||||
}
|
||||
$this->port = $port;
|
||||
|
||||
if (!is_int($timeout) || $timeout < 0) {
|
||||
if ($this->timeout < 0) {
|
||||
throw new TimeException('Timeout must be >= 0');
|
||||
}
|
||||
$this->timeout = $timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,28 +30,28 @@ class NTPTimeProvider implements ITimeProvider
|
||||
public function getTime()
|
||||
{
|
||||
try {
|
||||
/* Create a socket and connect to NTP server */
|
||||
// Create a socket and connect to NTP server
|
||||
$sock = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
|
||||
socket_set_option($sock, SOL_SOCKET, SO_RCVTIMEO, ['sec' => $this->timeout, 'usec' => 0]);
|
||||
socket_set_option($sock, SOL_SOCKET, SO_RCVTIMEO, array('sec' => $this->timeout, 'usec' => 0));
|
||||
socket_connect($sock, $this->host, $this->port);
|
||||
|
||||
/* Send request */
|
||||
// Send request
|
||||
$msg = "\010" . str_repeat("\0", 47);
|
||||
socket_send($sock, $msg, strlen($msg), 0);
|
||||
|
||||
/* Receive response and close socket */
|
||||
// Receive response and close socket
|
||||
if (socket_recv($sock, $recv, 48, MSG_WAITALL) === false) {
|
||||
throw new \Exception(socket_strerror(socket_last_error($sock)));
|
||||
throw new Exception(socket_strerror(socket_last_error($sock)));
|
||||
}
|
||||
socket_close($sock);
|
||||
|
||||
/* Interpret response */
|
||||
// Interpret response
|
||||
$data = unpack('N12', $recv);
|
||||
$timestamp = (int) sprintf('%u', $data[9]);
|
||||
$timestamp = (int)sprintf('%u', $data[9]);
|
||||
|
||||
/* NTP is number of seconds since 0000 UT on 1 January 1900 Unix time is seconds since 0000 UT on 1 January 1970 */
|
||||
// NTP is number of seconds since 0000 UT on 1 January 1900 Unix time is seconds since 0000 UT on 1 January 1970
|
||||
return $timestamp - 2208988800;
|
||||
} catch (\Exception $ex) {
|
||||
} catch (Exception $ex) {
|
||||
throw new TimeException(sprintf('Unable to retrieve time from %s (%s)', $this->host, $ex->getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RobThree\Auth\Providers\Time;
|
||||
|
||||
use RobThree\Auth\TwoFactorAuthException;
|
||||
|
||||
class TimeException extends TwoFactorAuthException {}
|
||||
class TimeException extends TwoFactorAuthException
|
||||
{
|
||||
}
|
||||
|
||||
+67
-216
@@ -1,86 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RobThree\Auth;
|
||||
|
||||
use function hash_equals;
|
||||
|
||||
use RobThree\Auth\Providers\Qr\IQRCodeProvider;
|
||||
use RobThree\Auth\Providers\Qr\QRServerProvider;
|
||||
use RobThree\Auth\Providers\Rng\CSRNGProvider;
|
||||
use RobThree\Auth\Providers\Rng\HashRNGProvider;
|
||||
use RobThree\Auth\Providers\Rng\IRNGProvider;
|
||||
use RobThree\Auth\Providers\Rng\MCryptRNGProvider;
|
||||
use RobThree\Auth\Providers\Rng\OpenSSLRNGProvider;
|
||||
use RobThree\Auth\Providers\Time\HttpTimeProvider;
|
||||
use RobThree\Auth\Providers\Time\ITimeProvider;
|
||||
use RobThree\Auth\Providers\Time\LocalMachineTimeProvider;
|
||||
use RobThree\Auth\Providers\Time\NTPTimeProvider;
|
||||
use SensitiveParameter;
|
||||
|
||||
// Based on / inspired by: https://github.com/PHPGangsta/GoogleAuthenticator
|
||||
// Algorithms, digits, period etc. explained: https://github.com/google/google-authenticator/wiki/Key-Uri-Format
|
||||
class TwoFactorAuth
|
||||
{
|
||||
/** @var string */
|
||||
private $algorithm;
|
||||
private static string $_base32dict = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567=';
|
||||
|
||||
/** @var int */
|
||||
private $period;
|
||||
/** @var array<string> */
|
||||
private static array $_base32;
|
||||
|
||||
/** @var int */
|
||||
private $digits;
|
||||
/** @var array<string, int> */
|
||||
private static array $_base32lookup = array();
|
||||
|
||||
/** @var string */
|
||||
private $issuer;
|
||||
|
||||
/** @var ?IQRCodeProvider */
|
||||
private $qrcodeprovider = null;
|
||||
|
||||
/** @var ?IRNGProvider */
|
||||
private $rngprovider = null;
|
||||
|
||||
/** @var ?ITimeProvider */
|
||||
private $timeprovider = null;
|
||||
|
||||
/** @var string */
|
||||
private static $_base32dict = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567=';
|
||||
|
||||
/** @var array */
|
||||
private static $_base32;
|
||||
|
||||
/** @var array */
|
||||
private static $_base32lookup = array();
|
||||
|
||||
/** @var array */
|
||||
private static $_supportedalgos = array('sha1', 'sha256', 'sha512', 'md5');
|
||||
|
||||
/**
|
||||
* @param ?string $issuer
|
||||
* @param int $digits
|
||||
* @param int $period
|
||||
* @param string $algorithm
|
||||
* @param ?IQRCodeProvider $qrcodeprovider
|
||||
* @param ?IRNGProvider $rngprovider
|
||||
* @param ?ITimeProvider $timeprovider
|
||||
*/
|
||||
public function __construct($issuer = null, $digits = 6, $period = 30, $algorithm = 'sha1', IQRCodeProvider $qrcodeprovider = null, IRNGProvider $rngprovider = null, ITimeProvider $timeprovider = null)
|
||||
{
|
||||
$this->issuer = $issuer;
|
||||
if (!is_int($digits) || $digits <= 0) {
|
||||
throw new TwoFactorAuthException('Digits must be int > 0');
|
||||
public function __construct(
|
||||
private IQRCodeProvider $qrcodeprovider,
|
||||
private readonly ?string $issuer = null,
|
||||
private readonly int $digits = 6,
|
||||
private readonly int $period = 30,
|
||||
private readonly Algorithm $algorithm = Algorithm::Sha1,
|
||||
private ?IRNGProvider $rngprovider = null,
|
||||
private ?ITimeProvider $timeprovider = null
|
||||
) {
|
||||
if ($this->digits <= 0) {
|
||||
throw new TwoFactorAuthException('Digits must be > 0');
|
||||
}
|
||||
$this->digits = $digits;
|
||||
|
||||
if (!is_int($period) || $period <= 0) {
|
||||
if ($this->period <= 0) {
|
||||
throw new TwoFactorAuthException('Period must be int > 0');
|
||||
}
|
||||
$this->period = $period;
|
||||
|
||||
$algorithm = strtolower(trim($algorithm));
|
||||
if (!in_array($algorithm, self::$_supportedalgos)) {
|
||||
throw new TwoFactorAuthException('Unsupported algorithm: ' . $algorithm);
|
||||
}
|
||||
$this->algorithm = $algorithm;
|
||||
$this->qrcodeprovider = $qrcodeprovider;
|
||||
$this->rngprovider = $rngprovider;
|
||||
$this->timeprovider = $timeprovider;
|
||||
|
||||
self::$_base32 = str_split(self::$_base32dict);
|
||||
self::$_base32lookup = array_flip(self::$_base32);
|
||||
@@ -88,20 +50,12 @@ class TwoFactorAuth
|
||||
|
||||
/**
|
||||
* Create a new secret
|
||||
*
|
||||
* @param int $bits
|
||||
* @param bool $requirecryptosecure
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function createSecret($bits = 80, $requirecryptosecure = true)
|
||||
public function createSecret(int $bits = 160): string
|
||||
{
|
||||
$secret = '';
|
||||
$bytes = (int) ceil($bits / 5); //We use 5 bits of each byte (since we have a 32-character 'alphabet' / BASE32)
|
||||
$bytes = (int)ceil($bits / 5); // We use 5 bits of each byte (since we have a 32-character 'alphabet' / BASE32)
|
||||
$rngprovider = $this->getRngProvider();
|
||||
if ($requirecryptosecure && !$rngprovider->isCryptographicallySecure()) {
|
||||
throw new TwoFactorAuthException('RNG provider is not cryptographically secure');
|
||||
}
|
||||
$rnd = $rngprovider->getRandomBytes($bytes);
|
||||
for ($i = 0; $i < $bytes; $i++) {
|
||||
$secret .= self::$_base32[ord($rnd[$i]) & 31]; //Mask out left 3 bits for 0-31 values
|
||||
@@ -111,37 +65,24 @@ class TwoFactorAuth
|
||||
|
||||
/**
|
||||
* Calculate the code with given secret and point in time
|
||||
*
|
||||
* @param string $secret
|
||||
* @param ?int $time
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getCode($secret, $time = null)
|
||||
public function getCode(#[SensitiveParameter] string $secret, ?int $time = null): string
|
||||
{
|
||||
$secretkey = $this->base32Decode($secret);
|
||||
|
||||
$timestamp = "\0\0\0\0" . pack('N*', $this->getTimeSlice($this->getTime($time))); // Pack time into binary string
|
||||
$hashhmac = hash_hmac($this->algorithm, $timestamp, $secretkey, true); // Hash it with users secret key
|
||||
$hashhmac = hash_hmac($this->algorithm->value, $timestamp, $secretkey, true); // Hash it with users secret key
|
||||
$hashpart = substr($hashhmac, ord(substr($hashhmac, -1)) & 0x0F, 4); // Use last nibble of result as index/offset and grab 4 bytes of the result
|
||||
$value = unpack('N', $hashpart); // Unpack binary value
|
||||
$value = $value[1] & 0x7FFFFFFF; // Drop MSB, keep only 31 bits
|
||||
|
||||
return str_pad((string) ($value % pow(10, $this->digits)), $this->digits, '0', STR_PAD_LEFT);
|
||||
return str_pad((string)($value % 10 ** $this->digits), $this->digits, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the code is correct. This will accept codes starting from ($discrepancy * $period) sec ago to ($discrepancy * period) sec from now
|
||||
*
|
||||
* @param string $secret
|
||||
* @param string $code
|
||||
* @param int $discrepancy
|
||||
* @param ?int $time
|
||||
* @param int $timeslice
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function verifyCode($secret, $code, $discrepancy = 1, $time = null, &$timeslice = 0)
|
||||
public function verifyCode(string $secret, string $code, int $discrepancy = 1, ?int $time = null, ?int &$timeslice = 0): bool
|
||||
{
|
||||
$timestamp = $this->getTime($time);
|
||||
|
||||
@@ -154,72 +95,38 @@ class TwoFactorAuth
|
||||
for ($i = -$discrepancy; $i <= $discrepancy; $i++) {
|
||||
$ts = $timestamp + ($i * $this->period);
|
||||
$slice = $this->getTimeSlice($ts);
|
||||
$timeslice = $this->codeEquals($this->getCode($secret, $ts), $code) ? $slice : $timeslice;
|
||||
$timeslice = hash_equals($this->getCode($secret, $ts), $code) ? $slice : $timeslice;
|
||||
}
|
||||
|
||||
return $timeslice > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Timing-attack safe comparison of 2 codes (see http://blog.ircmaxell.com/2014/11/its-all-about-time.html)
|
||||
*
|
||||
* @param string $safe
|
||||
* @param string $user
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function codeEquals($safe, $user)
|
||||
{
|
||||
if (function_exists('hash_equals')) {
|
||||
return hash_equals($safe, $user);
|
||||
}
|
||||
// In general, it's not possible to prevent length leaks. So it's OK to leak the length. The important part is that
|
||||
// we don't leak information about the difference of the two strings.
|
||||
if (strlen($safe) === strlen($user)) {
|
||||
$result = 0;
|
||||
for ($i = 0; $i < strlen($safe); $i++) {
|
||||
$result |= (ord($safe[$i]) ^ ord($user[$i]));
|
||||
}
|
||||
return $result === 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data-uri of QRCode
|
||||
*
|
||||
* @param string $label
|
||||
* @param string $secret
|
||||
* @param mixed $size
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getQRCodeImageAsDataUri($label, $secret, $size = 200)
|
||||
public function getQRCodeImageAsDataUri(string $label, #[SensitiveParameter] string $secret, int $size = 200): string
|
||||
{
|
||||
if (!is_int($size) || $size <= 0) {
|
||||
throw new TwoFactorAuthException('Size must be int > 0');
|
||||
if ($size <= 0) {
|
||||
throw new TwoFactorAuthException('Size must be > 0');
|
||||
}
|
||||
|
||||
$qrcodeprovider = $this->getQrCodeProvider();
|
||||
return 'data:'
|
||||
. $qrcodeprovider->getMimeType()
|
||||
. $this->qrcodeprovider->getMimeType()
|
||||
. ';base64,'
|
||||
. base64_encode($qrcodeprovider->getQRCodeImage($this->getQRText($label, $secret), $size));
|
||||
. base64_encode($this->qrcodeprovider->getQRCodeImage($this->getQRText($label, $secret), $size));
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare default timeprovider with specified timeproviders and ensure the time is within the specified number of seconds (leniency)
|
||||
* @param ?array $timeproviders
|
||||
* @param int $leniency
|
||||
*
|
||||
* @return void
|
||||
* @param array<ITimeProvider> $timeproviders
|
||||
* @throws TwoFactorAuthException
|
||||
*/
|
||||
public function ensureCorrectTime(array $timeproviders = null, $leniency = 5)
|
||||
public function ensureCorrectTime(?array $timeproviders = null, int $leniency = 5): void
|
||||
{
|
||||
if ($timeproviders === null) {
|
||||
$timeproviders = array(
|
||||
new NTPTimeProvider(),
|
||||
new HttpTimeProvider()
|
||||
new HttpTimeProvider(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -239,56 +146,50 @@ class TwoFactorAuth
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ?int $time
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private function getTime($time = null)
|
||||
{
|
||||
return ($time === null) ? $this->getTimeProvider()->getTime() : $time;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $time
|
||||
* @param int $offset
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private function getTimeSlice($time = null, $offset = 0)
|
||||
{
|
||||
return (int)floor($time / $this->period) + ($offset * $this->period);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a string to be encoded in a QR code
|
||||
*
|
||||
* @param string $label
|
||||
* @param string $secret
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getQRText($label, $secret)
|
||||
public function getQRText(string $label, #[SensitiveParameter] string $secret): string
|
||||
{
|
||||
return 'otpauth://totp/' . rawurlencode($label)
|
||||
. '?secret=' . rawurlencode($secret)
|
||||
. '&issuer=' . rawurlencode((string)$this->issuer)
|
||||
. '&period=' . intval($this->period)
|
||||
. '&algorithm=' . rawurlencode(strtoupper($this->algorithm))
|
||||
. '&digits=' . intval($this->digits);
|
||||
. '&period=' . $this->period
|
||||
. '&algorithm=' . rawurlencode(strtoupper($this->algorithm->value))
|
||||
. '&digits=' . $this->digits;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $value
|
||||
* @return string
|
||||
* @throws TwoFactorAuthException
|
||||
*/
|
||||
private function base32Decode($value)
|
||||
public function getRngProvider(): IRNGProvider
|
||||
{
|
||||
if (strlen($value) == 0) {
|
||||
return $this->rngprovider ??= new CSRNGProvider();
|
||||
}
|
||||
|
||||
public function getTimeProvider(): ITimeProvider
|
||||
{
|
||||
// Set default time provider if none was specified
|
||||
return $this->timeprovider ??= new LocalMachineTimeProvider();
|
||||
}
|
||||
|
||||
private function getTime(?int $time = null): int
|
||||
{
|
||||
return $time ?? $this->getTimeProvider()->getTime();
|
||||
}
|
||||
|
||||
private function getTimeSlice(?int $time = null, int $offset = 0): int
|
||||
{
|
||||
return (int)floor($time / $this->period) + ($offset * $this->period);
|
||||
}
|
||||
|
||||
private function base32Decode(string $value): string
|
||||
{
|
||||
if ($value === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (preg_match('/[^' . preg_quote(self::$_base32dict) . ']/', $value) !== 0) {
|
||||
if (preg_match('/[^' . preg_quote(self::$_base32dict, '/') . ']/', $value) !== 0) {
|
||||
throw new TwoFactorAuthException('Invalid base32 string');
|
||||
}
|
||||
|
||||
@@ -307,54 +208,4 @@ class TwoFactorAuth
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return IQRCodeProvider
|
||||
* @throws TwoFactorAuthException
|
||||
*/
|
||||
public function getQrCodeProvider()
|
||||
{
|
||||
// Set default QR Code provider if none was specified
|
||||
if (null === $this->qrcodeprovider) {
|
||||
return $this->qrcodeprovider = new QRServerProvider();
|
||||
}
|
||||
return $this->qrcodeprovider;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return IRNGProvider
|
||||
* @throws TwoFactorAuthException
|
||||
*/
|
||||
public function getRngProvider()
|
||||
{
|
||||
if (null !== $this->rngprovider) {
|
||||
return $this->rngprovider;
|
||||
}
|
||||
if (function_exists('random_bytes')) {
|
||||
return $this->rngprovider = new CSRNGProvider();
|
||||
}
|
||||
if (function_exists('mcrypt_create_iv')) {
|
||||
return $this->rngprovider = new MCryptRNGProvider();
|
||||
}
|
||||
if (function_exists('openssl_random_pseudo_bytes')) {
|
||||
return $this->rngprovider = new OpenSSLRNGProvider();
|
||||
}
|
||||
if (function_exists('hash')) {
|
||||
return $this->rngprovider = new HashRNGProvider();
|
||||
}
|
||||
throw new TwoFactorAuthException('Unable to find a suited RNGProvider');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ITimeProvider
|
||||
* @throws TwoFactorAuthException
|
||||
*/
|
||||
public function getTimeProvider()
|
||||
{
|
||||
// Set default time provider if none was specified
|
||||
if (null === $this->timeprovider) {
|
||||
return $this->timeprovider = new LocalMachineTimeProvider();
|
||||
}
|
||||
return $this->timeprovider;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RobThree\Auth;
|
||||
|
||||
use Exception;
|
||||
|
||||
class TwoFactorAuthException extends Exception {}
|
||||
class TwoFactorAuthException extends Exception
|
||||
{
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
parameters:
|
||||
level: 6
|
||||
|
||||
excludePaths:
|
||||
- %currentWorkingDirectory%/lib/Providers/Qr/BaconQrCodeProvider.php
|
||||
- %currentWorkingDirectory%/lib/Providers/Qr/EndroidQrCodeProvider.php
|
||||
- %currentWorkingDirectory%/lib/Providers/Qr/EndroidQrCodeWithLogoProvider.php
|
||||
|
||||
paths:
|
||||
- %currentWorkingDirectory%/lib
|
||||
- %currentWorkingDirectory%/tests
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests;
|
||||
|
||||
trait MightNotMakeAssertions
|
||||
{
|
||||
/**
|
||||
* This is a shim to support PHPUnit for php 5.6 and 7.0.
|
||||
*
|
||||
* It has to be named something that doesn't collide with existing
|
||||
* TestCase methods as we can't support PHP return types right now
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function noAssertionsMade()
|
||||
{
|
||||
foreach (class_parents($this) as $parent) {
|
||||
if (method_exists($parent, 'expectNotToPerformAssertions')) {
|
||||
parent::expectNotToPerformAssertions();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
}
|
||||
@@ -1,57 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Providers\Qr;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use RobThree\Auth\Algorithm;
|
||||
use RobThree\Auth\Providers\Qr\HandlesDataUri;
|
||||
use RobThree\Auth\Providers\Qr\IQRCodeProvider;
|
||||
use RobThree\Auth\TwoFactorAuth;
|
||||
use RobThree\Auth\TwoFactorAuthException;
|
||||
use RobThree\Auth\Providers\Qr\HandlesDataUri;
|
||||
|
||||
class IQRCodeProviderTest extends TestCase
|
||||
{
|
||||
use HandlesDataUri;
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testTotpUriIsCorrect()
|
||||
{
|
||||
$qr = new TestQrProvider();
|
||||
protected IQRCodeProvider $qr;
|
||||
|
||||
$tfa = new TwoFactorAuth('Test&Issuer', 6, 30, 'sha1', $qr);
|
||||
$data = $this->DecodeDataUri($tfa->getQRCodeImageAsDataUri('Test&Label', 'VMR466AB62ZBOKHE'));
|
||||
$this->assertEquals('test/test', $data['mimetype']);
|
||||
$this->assertEquals('base64', $data['encoding']);
|
||||
$this->assertEquals('otpauth://totp/Test%26Label?secret=VMR466AB62ZBOKHE&issuer=Test%26Issuer&period=30&algorithm=SHA1&digits=6@200', $data['data']);
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->qr = new TestQrProvider();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testTotpUriIsCorrectNoIssuer()
|
||||
public function testTotpUriIsCorrect(): void
|
||||
{
|
||||
$qr = new TestQrProvider();
|
||||
$tfa = new TwoFactorAuth($this->qr, 'Test&Issuer', 6, 30, Algorithm::Sha1);
|
||||
$data = $this->DecodeDataUri($tfa->getQRCodeImageAsDataUri('Test&Label', 'VMR466AB62ZBOKHE'));
|
||||
$this->assertSame('test/test', $data['mimetype']);
|
||||
$this->assertSame('base64', $data['encoding']);
|
||||
$this->assertSame('otpauth://totp/Test%26Label?secret=VMR466AB62ZBOKHE&issuer=Test%26Issuer&period=30&algorithm=SHA1&digits=6@200', $data['data']);
|
||||
}
|
||||
|
||||
public function testTotpUriIsCorrectNoIssuer(): void
|
||||
{
|
||||
/**
|
||||
* The library specifies the issuer is null by default however in PHP 8.1
|
||||
* there is a deprecation warning for passing null as a string argument to rawurlencode
|
||||
*/
|
||||
|
||||
$tfa = new TwoFactorAuth(null, 6, 30, 'sha1', $qr);
|
||||
$tfa = new TwoFactorAuth($this->qr, null, 6, 30, Algorithm::Sha1);
|
||||
$data = $this->DecodeDataUri($tfa->getQRCodeImageAsDataUri('Test&Label', 'VMR466AB62ZBOKHE'));
|
||||
$this->assertEquals('test/test', $data['mimetype']);
|
||||
$this->assertEquals('base64', $data['encoding']);
|
||||
$this->assertEquals('otpauth://totp/Test%26Label?secret=VMR466AB62ZBOKHE&issuer=&period=30&algorithm=SHA1&digits=6@200', $data['data']);
|
||||
$this->assertSame('test/test', $data['mimetype']);
|
||||
$this->assertSame('base64', $data['encoding']);
|
||||
$this->assertSame('otpauth://totp/Test%26Label?secret=VMR466AB62ZBOKHE&issuer=&period=30&algorithm=SHA1&digits=6@200', $data['data']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testGetQRCodeImageAsDataUriThrowsOnInvalidSize()
|
||||
public function testGetQRCodeImageAsDataUriThrowsOnInvalidSize(): void
|
||||
{
|
||||
$qr = new TestQrProvider();
|
||||
|
||||
$tfa = new TwoFactorAuth('Test', 6, 30, 'sha1', $qr);
|
||||
$tfa = new TwoFactorAuth($this->qr, 'Test', 6, 30, Algorithm::Sha1);
|
||||
|
||||
$this->expectException(TwoFactorAuthException::class);
|
||||
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Providers\Qr;
|
||||
|
||||
use RobThree\Auth\Providers\Qr\IQRCodeProvider;
|
||||
|
||||
class TestQrProvider implements IQRCodeProvider
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getQRCodeImage($qrtext, $size)
|
||||
public function getQRCodeImage(string $qrText, int $size): string
|
||||
{
|
||||
return $qrtext . '@' . $size;
|
||||
return $qrText . '@' . $size;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getMimeType()
|
||||
public function getMimeType(): string
|
||||
{
|
||||
return 'test/test';
|
||||
}
|
||||
|
||||
@@ -1,30 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Providers\Rng;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Tests\MightNotMakeAssertions;
|
||||
use RobThree\Auth\Providers\Rng\CSRNGProvider;
|
||||
|
||||
class CSRNGProviderTest extends TestCase
|
||||
{
|
||||
use NeedsRngLengths, MightNotMakeAssertions;
|
||||
use NeedsRngLengths;
|
||||
|
||||
/**
|
||||
* @requires function random_bytes
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testCSRNGProvidersReturnExpectedNumberOfBytes()
|
||||
public function testCSRNGProvidersReturnExpectedNumberOfBytes(): void
|
||||
{
|
||||
if (function_exists('random_bytes')) {
|
||||
$rng = new CSRNGProvider();
|
||||
foreach ($this->rngTestLengths as $l) {
|
||||
$this->assertEquals($l, strlen($rng->getRandomBytes($l)));
|
||||
}
|
||||
$this->assertTrue($rng->isCryptographicallySecure());
|
||||
} else {
|
||||
$this->noAssertionsMade();
|
||||
$rng = new CSRNGProvider();
|
||||
foreach ($this->rngTestLengths as $l) {
|
||||
$this->assertSame($l, strlen($rng->getRandomBytes($l)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Providers\Rng;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use RobThree\Auth\Providers\Rng\HashRNGProvider;
|
||||
|
||||
class HashRNGProviderTest extends TestCase
|
||||
{
|
||||
use NeedsRngLengths;
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testHashRNGProvidersReturnExpectedNumberOfBytes()
|
||||
{
|
||||
$rng = new HashRNGProvider();
|
||||
foreach ($this->rngTestLengths as $l) {
|
||||
$this->assertEquals($l, strlen($rng->getRandomBytes($l)));
|
||||
}
|
||||
|
||||
$this->assertFalse($rng->isCryptographicallySecure());
|
||||
}
|
||||
}
|
||||
@@ -1,61 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Providers\Rng;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use RobThree\Auth\Algorithm;
|
||||
use RobThree\Auth\TwoFactorAuth;
|
||||
use RobThree\Auth\TwoFactorAuthException;
|
||||
use Tests\Providers\Qr\TestQrProvider;
|
||||
|
||||
class IRNGProviderTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testCreateSecretThrowsOnInsecureRNGProvider()
|
||||
public function testCreateSecret(): void
|
||||
{
|
||||
$rng = new TestRNGProvider();
|
||||
|
||||
$tfa = new TwoFactorAuth('Test', 6, 30, 'sha1', null, $rng);
|
||||
|
||||
$this->expectException(TwoFactorAuthException::class);
|
||||
$tfa->createSecret();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testCreateSecretOverrideSecureDoesNotThrowOnInsecureRNG()
|
||||
{
|
||||
$rng = new TestRNGProvider();
|
||||
|
||||
$tfa = new TwoFactorAuth('Test', 6, 30, 'sha1', null, $rng);
|
||||
$this->assertEquals('ABCDEFGHIJKLMNOP', $tfa->createSecret(80, false));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testCreateSecretDoesNotThrowOnSecureRNGProvider()
|
||||
{
|
||||
$rng = new TestRNGProvider(true);
|
||||
|
||||
$tfa = new TwoFactorAuth('Test', 6, 30, 'sha1', null, $rng);
|
||||
$this->assertEquals('ABCDEFGHIJKLMNOP', $tfa->createSecret());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testCreateSecretGeneratesDesiredAmountOfEntropy()
|
||||
{
|
||||
$rng = new TestRNGProvider(true);
|
||||
|
||||
$tfa = new TwoFactorAuth('Test', 6, 30, 'sha1', null, $rng);
|
||||
$this->assertEquals('A', $tfa->createSecret(5));
|
||||
$this->assertEquals('AB', $tfa->createSecret(6));
|
||||
$this->assertEquals('ABCDEFGHIJKLMNOPQRSTUVWXYZ', $tfa->createSecret(128));
|
||||
$this->assertEquals('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', $tfa->createSecret(160));
|
||||
$this->assertEquals('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', $tfa->createSecret(320));
|
||||
$this->assertEquals('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOPQRSTUVWXYZ234567A', $tfa->createSecret(321));
|
||||
$tfa = new TwoFactorAuth(new TestQrProvider(), 'Test', 6, 30, Algorithm::Sha1, null, null);
|
||||
$this->assertIsString($tfa->createSecret());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Providers\Rng;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Tests\MightNotMakeAssertions;
|
||||
use RobThree\Auth\Providers\Rng\MCryptRNGProvider;
|
||||
|
||||
class MCryptRNGProviderTest extends TestCase
|
||||
{
|
||||
use NeedsRngLengths, MightNotMakeAssertions;
|
||||
|
||||
/**
|
||||
* @requires function mcrypt_create_iv
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testMCryptRNGProvidersReturnExpectedNumberOfBytes()
|
||||
{
|
||||
if (function_exists('mcrypt_create_iv')) {
|
||||
$rng = new MCryptRNGProvider();
|
||||
|
||||
foreach ($this->rngTestLengths as $l) {
|
||||
$this->assertEquals($l, strlen($rng->getRandomBytes($l)));
|
||||
}
|
||||
|
||||
$this->assertTrue($rng->isCryptographicallySecure());
|
||||
} else {
|
||||
$this->noAssertionsMade();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Providers\Rng;
|
||||
|
||||
trait NeedsRngLengths
|
||||
{
|
||||
/** @var array */
|
||||
/** @var array<int> */
|
||||
protected $rngTestLengths = array(1, 16, 32, 256);
|
||||
}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Providers\Rng;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use RobThree\Auth\Providers\Rng\OpenSSLRNGProvider;
|
||||
|
||||
class OpenSSLRNGProviderTest extends TestCase
|
||||
{
|
||||
use NeedsRngLengths;
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testStrongOpenSSLRNGProvidersReturnExpectedNumberOfBytes()
|
||||
{
|
||||
$rng = new OpenSSLRNGProvider(true);
|
||||
foreach ($this->rngTestLengths as $l) {
|
||||
$this->assertEquals($l, strlen($rng->getRandomBytes($l)));
|
||||
}
|
||||
|
||||
$this->assertTrue($rng->isCryptographicallySecure());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testNonStrongOpenSSLRNGProvidersReturnExpectedNumberOfBytes()
|
||||
{
|
||||
$rng = new OpenSSLRNGProvider(false);
|
||||
foreach ($this->rngTestLengths as $l) {
|
||||
$this->assertEquals($l, strlen($rng->getRandomBytes($l)));
|
||||
}
|
||||
|
||||
$this->assertFalse($rng->isCryptographicallySecure());
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Providers\Rng;
|
||||
|
||||
use RobThree\Auth\Providers\Rng\IRNGProvider;
|
||||
|
||||
class TestRNGProvider implements IRNGProvider
|
||||
{
|
||||
/** @var bool */
|
||||
private $isSecure;
|
||||
|
||||
/**
|
||||
* @param bool $isSecure whether this provider is cryptographically secure
|
||||
*/
|
||||
function __construct($isSecure = false)
|
||||
{
|
||||
$this->isSecure = $isSecure;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getRandomBytes($bytecount)
|
||||
{
|
||||
$result = '';
|
||||
|
||||
for ($i = 0; $i < $bytecount; $i++) {
|
||||
$result .= chr($i);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isCryptographicallySecure()
|
||||
{
|
||||
return $this->isSecure;
|
||||
}
|
||||
}
|
||||
@@ -1,53 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Providers\Time;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Tests\MightNotMakeAssertions;
|
||||
use RobThree\Auth\TwoFactorAuthException;
|
||||
use RobThree\Auth\Algorithm;
|
||||
use RobThree\Auth\TwoFactorAuth;
|
||||
use RobThree\Auth\TwoFactorAuthException;
|
||||
use Tests\Providers\Qr\TestQrProvider;
|
||||
|
||||
class ITimeProviderTest extends TestCase
|
||||
{
|
||||
use MightNotMakeAssertions;
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testEnsureCorrectTimeDoesNotThrowForCorrectTime()
|
||||
public function testEnsureCorrectTimeDoesNotThrowForCorrectTime(): void
|
||||
{
|
||||
$this->expectNotToPerformAssertions();
|
||||
$tpr1 = new TestTimeProvider(123);
|
||||
$tpr2 = new TestTimeProvider(128);
|
||||
|
||||
$tfa = new TwoFactorAuth('Test', 6, 30, 'sha1', null, null, $tpr1);
|
||||
$tfa = new TwoFactorAuth(new TestQrProvider(), 'Test', 6, 30, Algorithm::Sha1, null, $tpr1);
|
||||
$tfa->ensureCorrectTime(array($tpr2)); // 128 - 123 = 5 => within default leniency
|
||||
|
||||
$this->noAssertionsMade();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testEnsureCorrectTimeThrowsOnIncorrectTime()
|
||||
public function testEnsureCorrectTimeThrowsOnIncorrectTime(): void
|
||||
{
|
||||
$tpr1 = new TestTimeProvider(123);
|
||||
$tpr2 = new TestTimeProvider(124);
|
||||
|
||||
$tfa = new TwoFactorAuth('Test', 6, 30, 'sha1', null, null, $tpr1);
|
||||
$tfa = new TwoFactorAuth(new TestQrProvider(), 'Test', 6, 30, Algorithm::Sha1, null, $tpr1);
|
||||
|
||||
$this->expectException(TwoFactorAuthException::class);
|
||||
|
||||
$tfa->ensureCorrectTime(array($tpr2), 0); // We force a leniency of 0, 124-123 = 1 so this should throw
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testEnsureDefaultTimeProviderReturnsCorrectTime()
|
||||
public function testEnsureDefaultTimeProviderReturnsCorrectTime(): void
|
||||
{
|
||||
$tfa = new TwoFactorAuth('Test', 6, 30, 'sha1');
|
||||
$this->expectNotToPerformAssertions();
|
||||
$tfa = new TwoFactorAuth(new TestQrProvider(), 'Test', 6, 30, Algorithm::Sha1);
|
||||
$tfa->ensureCorrectTime(array(new TestTimeProvider(time())), 1); // Use a leniency of 1, should the time change between both time() calls
|
||||
|
||||
$this->noAssertionsMade();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Providers\Time;
|
||||
|
||||
use RobThree\Auth\Providers\Time\ITimeProvider;
|
||||
@@ -12,7 +14,7 @@ class TestTimeProvider implements ITimeProvider
|
||||
/**
|
||||
* @param int $time
|
||||
*/
|
||||
function __construct($time)
|
||||
public function __construct($time)
|
||||
{
|
||||
$this->time = $time;
|
||||
}
|
||||
|
||||
+92
-138
@@ -1,150 +1,118 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use RobThree\Auth\TwoFactorAuthException;
|
||||
use ReflectionMethod;
|
||||
use RobThree\Auth\Algorithm;
|
||||
use RobThree\Auth\Providers\Time\HttpTimeProvider;
|
||||
use RobThree\Auth\Providers\Time\NTPTimeProvider;
|
||||
use RobThree\Auth\TwoFactorAuth;
|
||||
use RobThree\Auth\TwoFactorAuthException;
|
||||
use Tests\Providers\Qr\TestQrProvider;
|
||||
|
||||
class TwoFactorAuthTest extends TestCase
|
||||
{
|
||||
use MightNotMakeAssertions;
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testConstructorThrowsOnInvalidDigits()
|
||||
public function testConstructorThrowsOnInvalidDigits(): void
|
||||
{
|
||||
$this->expectException(TwoFactorAuthException::class);
|
||||
|
||||
new TwoFactorAuth('Test', 0);
|
||||
new TwoFactorAuth(new TestQrProvider(), 'Test', 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testConstructorThrowsOnInvalidPeriod()
|
||||
public function testConstructorThrowsOnInvalidPeriod(): void
|
||||
{
|
||||
$this->expectException(TwoFactorAuthException::class);
|
||||
|
||||
new TwoFactorAuth('Test', 6, 0);
|
||||
new TwoFactorAuth(new TestQrProvider(), 'Test', 6, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testConstructorThrowsOnInvalidAlgorithm()
|
||||
public function testGetCodeReturnsCorrectResults(): void
|
||||
{
|
||||
$this->expectException(TwoFactorAuthException::class);
|
||||
|
||||
new TwoFactorAuth('Test', 6, 30, 'xxx');
|
||||
$tfa = new TwoFactorAuth(new TestQrProvider(), 'Test');
|
||||
$this->assertSame('543160', $tfa->getCode('VMR466AB62ZBOKHE', 1426847216));
|
||||
$this->assertSame('538532', $tfa->getCode('VMR466AB62ZBOKHE', 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testGetCodeReturnsCorrectResults()
|
||||
public function testEnsureAllTimeProvidersReturnCorrectTime(): void
|
||||
{
|
||||
$tfa = new TwoFactorAuth('Test');
|
||||
$this->assertEquals('543160', $tfa->getCode('VMR466AB62ZBOKHE', 1426847216));
|
||||
$this->assertEquals('538532', $tfa->getCode('VMR466AB62ZBOKHE', 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testEnsureAllTimeProvidersReturnCorrectTime()
|
||||
{
|
||||
$tfa = new TwoFactorAuth('Test', 6, 30, 'sha1');
|
||||
$tfa = new TwoFactorAuth(new TestQrProvider(), 'Test', 6, 30, Algorithm::Sha1);
|
||||
$tfa->ensureCorrectTime(array(
|
||||
new \RobThree\Auth\Providers\Time\NTPTimeProvider(), // Uses pool.ntp.org by default
|
||||
new NTPTimeProvider(), // Uses pool.ntp.org by default
|
||||
//new \RobThree\Auth\Providers\Time\NTPTimeProvider('time.google.com'), // Somehow time.google.com and time.windows.com make travis timeout??
|
||||
new \RobThree\Auth\Providers\Time\HttpTimeProvider(), // Uses google.com by default
|
||||
new HttpTimeProvider(), // Uses google.com by default
|
||||
//new \RobThree\Auth\Providers\Time\HttpTimeProvider('https://github.com'), // github.com will periodically report times that are off by more than 5 sec
|
||||
new \RobThree\Auth\Providers\Time\HttpTimeProvider('https://yahoo.com'),
|
||||
new HttpTimeProvider('https://yahoo.com'),
|
||||
));
|
||||
$this->noAssertionsMade();
|
||||
$this->expectNotToPerformAssertions();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testVerifyCodeWorksCorrectly()
|
||||
public function testVerifyCodeWorksCorrectly(): void
|
||||
{
|
||||
$tfa = new TwoFactorAuth('Test', 6, 30);
|
||||
$tfa = new TwoFactorAuth(new TestQrProvider(), 'Test', 6, 30);
|
||||
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847190));
|
||||
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 0, 1426847190 + 29)); //Test discrepancy
|
||||
$this->assertFalse($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 0, 1426847190 + 30)); //Test discrepancy
|
||||
$this->assertFalse($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 0, 1426847190 - 1)); //Test discrepancy
|
||||
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 0, 1426847190 + 29)); //Test discrepancy
|
||||
$this->assertFalse($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 0, 1426847190 + 30)); //Test discrepancy
|
||||
$this->assertFalse($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 0, 1426847190 - 1)); //Test discrepancy
|
||||
|
||||
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 + 0)); //Test discrepancy
|
||||
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 + 35)); //Test discrepancy
|
||||
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 - 35)); //Test discrepancy
|
||||
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205)); //Test discrepancy
|
||||
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 + 35)); //Test discrepancy
|
||||
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 - 35)); //Test discrepancy
|
||||
|
||||
$this->assertFalse($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 + 65)); //Test discrepancy
|
||||
$this->assertFalse($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 - 65)); //Test discrepancy
|
||||
$this->assertFalse($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 + 65)); //Test discrepancy
|
||||
$this->assertFalse($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 - 65)); //Test discrepancy
|
||||
|
||||
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 2, 1426847205 + 65)); //Test discrepancy
|
||||
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 2, 1426847205 - 65)); //Test discrepancy
|
||||
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 2, 1426847205 + 65)); //Test discrepancy
|
||||
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 2, 1426847205 - 65)); //Test discrepancy
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testVerifyCorrectTimeSliceIsReturned()
|
||||
public function testVerifyCorrectTimeSliceIsReturned(): void
|
||||
{
|
||||
$tfa = new TwoFactorAuth('Test', 6, 30);
|
||||
$tfa = new TwoFactorAuth(new TestQrProvider(), 'Test', 6, 30);
|
||||
|
||||
// We test with discrepancy 3 (so total of 7 codes: c-3, c-2, c-1, c, c+1, c+2, c+3
|
||||
// Ensure each corresponding timeslice is returned correctly
|
||||
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '534113', 3, 1426847190, $timeslice1));
|
||||
$this->assertEquals(47561570, $timeslice1);
|
||||
$this->assertSame(47561570, $timeslice1);
|
||||
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '819652', 3, 1426847190, $timeslice2));
|
||||
$this->assertEquals(47561571, $timeslice2);
|
||||
$this->assertSame(47561571, $timeslice2);
|
||||
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '915954', 3, 1426847190, $timeslice3));
|
||||
$this->assertEquals(47561572, $timeslice3);
|
||||
$this->assertSame(47561572, $timeslice3);
|
||||
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 3, 1426847190, $timeslice4));
|
||||
$this->assertEquals(47561573, $timeslice4);
|
||||
$this->assertSame(47561573, $timeslice4);
|
||||
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '348401', 3, 1426847190, $timeslice5));
|
||||
$this->assertEquals(47561574, $timeslice5);
|
||||
$this->assertSame(47561574, $timeslice5);
|
||||
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '648525', 3, 1426847190, $timeslice6));
|
||||
$this->assertEquals(47561575, $timeslice6);
|
||||
$this->assertSame(47561575, $timeslice6);
|
||||
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '170645', 3, 1426847190, $timeslice7));
|
||||
$this->assertEquals(47561576, $timeslice7);
|
||||
$this->assertSame(47561576, $timeslice7);
|
||||
|
||||
// Incorrect code should return false and a 0 timeslice
|
||||
$this->assertFalse($tfa->verifyCode('VMR466AB62ZBOKHE', '111111', 3, 1426847190, $timeslice8));
|
||||
$this->assertEquals(0, $timeslice8);
|
||||
$this->assertSame(0, $timeslice8);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testGetCodeThrowsOnInvalidBase32String1()
|
||||
public function testGetCodeThrowsOnInvalidBase32String1(): void
|
||||
{
|
||||
$tfa = new TwoFactorAuth('Test');
|
||||
$tfa = new TwoFactorAuth(new TestQrProvider(), 'Test');
|
||||
|
||||
$this->expectException(TwoFactorAuthException::class);
|
||||
|
||||
$tfa->getCode('FOO1BAR8BAZ9'); //1, 8 & 9 are invalid chars
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testGetCodeThrowsOnInvalidBase32String2()
|
||||
public function testGetCodeThrowsOnInvalidBase32String2(): void
|
||||
{
|
||||
$tfa = new TwoFactorAuth('Test');
|
||||
$tfa = new TwoFactorAuth(new TestQrProvider(), 'Test');
|
||||
|
||||
$this->expectException(TwoFactorAuthException::class);
|
||||
|
||||
$tfa->getCode('mzxw6==='); //Lowercase
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testKnownBase32DecodeTestVectors()
|
||||
public function testKnownBase32DecodeTestVectors(): void
|
||||
{
|
||||
// We usually don't test internals (e.g. privates) but since we rely heavily on base32 decoding and don't want
|
||||
// to expose this method nor do we want to give people the possibility of implementing / providing their own base32
|
||||
@@ -157,90 +125,76 @@ class TwoFactorAuthTest extends TestCase
|
||||
// "In general, you don't want to break any encapsulation for the sake of testing (or as Mom used to say, "don't
|
||||
// expose your privates!"). Most of the time, you should be able to test a class by exercising its public methods."
|
||||
// Dave Thomas and Andy Hunt -- "Pragmatic Unit Testing
|
||||
$tfa = new TwoFactorAuth('Test');
|
||||
$tfa = new TwoFactorAuth(new TestQrProvider(), 'Test');
|
||||
|
||||
$method = new \ReflectionMethod(TwoFactorAuth::class, 'base32Decode');
|
||||
$method->setAccessible(true);
|
||||
$method = new ReflectionMethod(TwoFactorAuth::class, 'base32Decode');
|
||||
|
||||
// Test vectors from: https://tools.ietf.org/html/rfc4648#page-12
|
||||
$this->assertEquals('', $method->invoke($tfa, ''));
|
||||
$this->assertEquals('f', $method->invoke($tfa, 'MY======'));
|
||||
$this->assertEquals('fo', $method->invoke($tfa, 'MZXQ===='));
|
||||
$this->assertEquals('foo', $method->invoke($tfa, 'MZXW6==='));
|
||||
$this->assertEquals('foob', $method->invoke($tfa, 'MZXW6YQ='));
|
||||
$this->assertEquals('fooba', $method->invoke($tfa, 'MZXW6YTB'));
|
||||
$this->assertEquals('foobar', $method->invoke($tfa, 'MZXW6YTBOI======'));
|
||||
$this->assertSame('', $method->invoke($tfa, ''));
|
||||
$this->assertSame('f', $method->invoke($tfa, 'MY======'));
|
||||
$this->assertSame('fo', $method->invoke($tfa, 'MZXQ===='));
|
||||
$this->assertSame('foo', $method->invoke($tfa, 'MZXW6==='));
|
||||
$this->assertSame('foob', $method->invoke($tfa, 'MZXW6YQ='));
|
||||
$this->assertSame('fooba', $method->invoke($tfa, 'MZXW6YTB'));
|
||||
$this->assertSame('foobar', $method->invoke($tfa, 'MZXW6YTBOI======'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testKnownBase32DecodeUnpaddedTestVectors()
|
||||
public function testKnownBase32DecodeUnpaddedTestVectors(): void
|
||||
{
|
||||
// See testKnownBase32DecodeTestVectors() for the rationale behind testing the private base32Decode() method.
|
||||
// This test ensures that strings without the padding-char ('=') are also decoded correctly.
|
||||
// https://tools.ietf.org/html/rfc4648#page-4:
|
||||
// "In some circumstances, the use of padding ("=") in base-encoded data is not required or used."
|
||||
$tfa = new TwoFactorAuth('Test');
|
||||
$tfa = new TwoFactorAuth(new TestQrProvider(), 'Test');
|
||||
|
||||
$method = new \ReflectionMethod(TwoFactorAuth::class, 'base32Decode');
|
||||
$method->setAccessible(true);
|
||||
$method = new ReflectionMethod(TwoFactorAuth::class, 'base32Decode');
|
||||
|
||||
// Test vectors from: https://tools.ietf.org/html/rfc4648#page-12
|
||||
$this->assertEquals('', $method->invoke($tfa, ''));
|
||||
$this->assertEquals('f', $method->invoke($tfa, 'MY'));
|
||||
$this->assertEquals('fo', $method->invoke($tfa, 'MZXQ'));
|
||||
$this->assertEquals('foo', $method->invoke($tfa, 'MZXW6'));
|
||||
$this->assertEquals('foob', $method->invoke($tfa, 'MZXW6YQ'));
|
||||
$this->assertEquals('fooba', $method->invoke($tfa, 'MZXW6YTB'));
|
||||
$this->assertEquals('foobar', $method->invoke($tfa, 'MZXW6YTBOI'));
|
||||
$this->assertSame('', $method->invoke($tfa, ''));
|
||||
$this->assertSame('f', $method->invoke($tfa, 'MY'));
|
||||
$this->assertSame('fo', $method->invoke($tfa, 'MZXQ'));
|
||||
$this->assertSame('foo', $method->invoke($tfa, 'MZXW6'));
|
||||
$this->assertSame('foob', $method->invoke($tfa, 'MZXW6YQ'));
|
||||
$this->assertSame('fooba', $method->invoke($tfa, 'MZXW6YTB'));
|
||||
$this->assertSame('foobar', $method->invoke($tfa, 'MZXW6YTBOI'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testKnownTestVectors_sha1()
|
||||
public function testKnownTestVectors_sha1(): void
|
||||
{
|
||||
//Known test vectors for SHA1: https://tools.ietf.org/html/rfc6238#page-15
|
||||
$secret = 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ'; //== base32encode('12345678901234567890')
|
||||
$tfa = new TwoFactorAuth('Test', 8, 30, 'sha1');
|
||||
$this->assertEquals('94287082', $tfa->getCode($secret, 59));
|
||||
$this->assertEquals('07081804', $tfa->getCode($secret, 1111111109));
|
||||
$this->assertEquals('14050471', $tfa->getCode($secret, 1111111111));
|
||||
$this->assertEquals('89005924', $tfa->getCode($secret, 1234567890));
|
||||
$this->assertEquals('69279037', $tfa->getCode($secret, 2000000000));
|
||||
$this->assertEquals('65353130', $tfa->getCode($secret, 20000000000));
|
||||
$tfa = new TwoFactorAuth(new TestQrProvider(), 'Test', 8, 30, Algorithm::Sha1);
|
||||
$this->assertSame('94287082', $tfa->getCode($secret, 59));
|
||||
$this->assertSame('07081804', $tfa->getCode($secret, 1111111109));
|
||||
$this->assertSame('14050471', $tfa->getCode($secret, 1111111111));
|
||||
$this->assertSame('89005924', $tfa->getCode($secret, 1234567890));
|
||||
$this->assertSame('69279037', $tfa->getCode($secret, 2000000000));
|
||||
$this->assertSame('65353130', $tfa->getCode($secret, 20000000000));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testKnownTestVectors_sha256()
|
||||
public function testKnownTestVectors_sha256(): void
|
||||
{
|
||||
//Known test vectors for SHA256: https://tools.ietf.org/html/rfc6238#page-15
|
||||
$secret = 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZA'; //== base32encode('12345678901234567890123456789012')
|
||||
$tfa = new TwoFactorAuth('Test', 8, 30, 'sha256');
|
||||
$this->assertEquals('46119246', $tfa->getCode($secret, 59));
|
||||
$this->assertEquals('68084774', $tfa->getCode($secret, 1111111109));
|
||||
$this->assertEquals('67062674', $tfa->getCode($secret, 1111111111));
|
||||
$this->assertEquals('91819424', $tfa->getCode($secret, 1234567890));
|
||||
$this->assertEquals('90698825', $tfa->getCode($secret, 2000000000));
|
||||
$this->assertEquals('77737706', $tfa->getCode($secret, 20000000000));
|
||||
$tfa = new TwoFactorAuth(new TestQrProvider(), 'Test', 8, 30, Algorithm::Sha256);
|
||||
$this->assertSame('46119246', $tfa->getCode($secret, 59));
|
||||
$this->assertSame('68084774', $tfa->getCode($secret, 1111111109));
|
||||
$this->assertSame('67062674', $tfa->getCode($secret, 1111111111));
|
||||
$this->assertSame('91819424', $tfa->getCode($secret, 1234567890));
|
||||
$this->assertSame('90698825', $tfa->getCode($secret, 2000000000));
|
||||
$this->assertSame('77737706', $tfa->getCode($secret, 20000000000));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testKnownTestVectors_sha512()
|
||||
public function testKnownTestVectors_sha512(): void
|
||||
{
|
||||
//Known test vectors for SHA512: https://tools.ietf.org/html/rfc6238#page-15
|
||||
$secret = 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNA'; //== base32encode('1234567890123456789012345678901234567890123456789012345678901234')
|
||||
$tfa = new TwoFactorAuth('Test', 8, 30, 'sha512');
|
||||
$this->assertEquals('90693936', $tfa->getCode($secret, 59));
|
||||
$this->assertEquals('25091201', $tfa->getCode($secret, 1111111109));
|
||||
$this->assertEquals('99943326', $tfa->getCode($secret, 1111111111));
|
||||
$this->assertEquals('93441116', $tfa->getCode($secret, 1234567890));
|
||||
$this->assertEquals('38618901', $tfa->getCode($secret, 2000000000));
|
||||
$this->assertEquals('47863826', $tfa->getCode($secret, 20000000000));
|
||||
$tfa = new TwoFactorAuth(new TestQrProvider(), 'Test', 8, 30, Algorithm::Sha512);
|
||||
$this->assertSame('90693936', $tfa->getCode($secret, 59));
|
||||
$this->assertSame('25091201', $tfa->getCode($secret, 1111111109));
|
||||
$this->assertSame('99943326', $tfa->getCode($secret, 1111111111));
|
||||
$this->assertSame('93441116', $tfa->getCode($secret, 1234567890));
|
||||
$this->assertSame('38618901', $tfa->getCode($secret, 2000000000));
|
||||
$this->assertSame('47863826', $tfa->getCode($secret, 20000000000));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,61 +1,62 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace TestsDependency;
|
||||
|
||||
use BaconQrCode\Renderer\Image\ImagickImageBackEnd;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use RobThree\Auth\Algorithm;
|
||||
use RobThree\Auth\Providers\Qr\BaconQrCodeProvider;
|
||||
use RobThree\Auth\TwoFactorAuth;
|
||||
use RobThree\Auth\Providers\Qr\HandlesDataUri;
|
||||
use RobThree\Auth\Providers\Qr\IQRCodeProvider;
|
||||
use RobThree\Auth\TwoFactorAuth;
|
||||
use RuntimeException;
|
||||
|
||||
class BaconQRCodeTest extends TestCase
|
||||
{
|
||||
use HandlesDataUri;
|
||||
|
||||
public function testDependency()
|
||||
protected IQRCodeProvider $qr;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
// php < 7.1 will install an older Bacon QR Code
|
||||
if (! class_exists(ImagickImageBackEnd::class)) {
|
||||
$this->expectException(\RuntimeException::class);
|
||||
|
||||
$qr = new BaconQrCodeProvider(1, '#000', '#FFF', 'svg');
|
||||
} else {
|
||||
$qr = new BaconQrCodeProvider(1, '#000', '#FFF', 'svg');
|
||||
|
||||
$tfa = new TwoFactorAuth('Test&Issuer', 6, 30, 'sha1', $qr);
|
||||
|
||||
$data = $this->DecodeDataUri($tfa->getQRCodeImageAsDataUri('Test&Label', 'VMR466AB62ZBOKHE'));
|
||||
$this->assertEquals('image/svg+xml', $data['mimetype']);
|
||||
}
|
||||
$this->qr = new BaconQrCodeProvider(1, '#000', '#FFF', 'svg');
|
||||
;
|
||||
}
|
||||
|
||||
public function testBadTextColour()
|
||||
public function testDependency(): void
|
||||
{
|
||||
$this->expectException(\RuntimeException::class);
|
||||
$tfa = new TwoFactorAuth($this->qr, 'Test&Issuer', 6, 30, Algorithm::Sha1);
|
||||
|
||||
$data = $this->DecodeDataUri($tfa->getQRCodeImageAsDataUri('Test&Label', 'VMR466AB62ZBOKHE'));
|
||||
$this->assertSame('image/svg+xml', $data['mimetype']);
|
||||
}
|
||||
|
||||
public function testBadTextColour(): void
|
||||
{
|
||||
$this->expectException(RuntimeException::class);
|
||||
|
||||
new BaconQrCodeProvider(1, 'not-a-colour', '#FFF');
|
||||
}
|
||||
|
||||
public function testBadBackgroundColour()
|
||||
public function testBadBackgroundColour(): void
|
||||
{
|
||||
$this->expectException(\RuntimeException::class);
|
||||
$this->expectException(RuntimeException::class);
|
||||
|
||||
new BaconQrCodeProvider(1, '#000', 'not-a-colour');
|
||||
}
|
||||
|
||||
public function testBadTextColourHexRef()
|
||||
public function testBadTextColourHexRef(): void
|
||||
{
|
||||
$this->expectException(\RuntimeException::class);
|
||||
$this->expectException(RuntimeException::class);
|
||||
|
||||
new BaconQrCodeProvider(1, '#AAAA', '#FFF');
|
||||
}
|
||||
|
||||
public function testBadBackgroundColourHexRef()
|
||||
public function testBadBackgroundColourHexRef(): void
|
||||
{
|
||||
$this->expectException(\RuntimeException::class);
|
||||
$this->expectException(RuntimeException::class);
|
||||
|
||||
new BaconQrCodeProvider(1, '#000', '#AAAA');
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace TestsDependency;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use RobThree\Auth\TwoFactorAuth;
|
||||
use RobThree\Auth\Algorithm;
|
||||
use RobThree\Auth\Providers\Qr\EndroidQrCodeProvider;
|
||||
use RobThree\Auth\Providers\Qr\HandlesDataUri;
|
||||
use RobThree\Auth\TwoFactorAuth;
|
||||
|
||||
class EndroidQRCodeTest extends TestCase
|
||||
{
|
||||
use HandlesDataUri;
|
||||
|
||||
public function testDependency()
|
||||
public function testDependency(): void
|
||||
{
|
||||
$qr = new EndroidQrCodeProvider();
|
||||
$tfa = new TwoFactorAuth('Test&Issuer', 6, 30, 'sha1', $qr);
|
||||
$tfa = new TwoFactorAuth($qr, 'Test&Issuer', 6, 30, Algorithm::Sha1);
|
||||
$data = $this->DecodeDataUri($tfa->getQRCodeImageAsDataUri('Test&Label', 'VMR466AB62ZBOKHE'));
|
||||
$this->assertEquals('image/png', $data['mimetype']);
|
||||
$this->assertEquals('base64', $data['encoding']);
|
||||
$this->assertSame('image/png', $data['mimetype']);
|
||||
$this->assertSame('base64', $data['encoding']);
|
||||
$this->assertNotEmpty($data['data']);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user