Compare commits

...

55 Commits

Author SHA1 Message Date
Will Power 27cd1e1392 Merge pull request #97 from NicolasCARPi/php8
Move to PHP 8.1 minimum version, add typing
2023-02-25 11:33:28 +00:00
Will Power 0159e77743 Merge pull request #101 from cedric-anne/composer-archive
Ensure composer dist archive contains only usefull files
2023-02-25 11:33:05 +00:00
Nicolas CARPi 30248a8fb5 address remarks made by MasterOdin 2023-02-22 18:08:19 +01:00
Cédric Anne d6e5e2ef87 Ensure composer dist archive contains only usefull files 2023-02-22 15:42:38 +01:00
Nicolas CARPi ab76ac71a4 use phpunit 9 instead of @stable 2023-02-20 10:31:39 +01:00
Nicolas CARPi 023bfc16f6 add Will Power in authors list in composer.json 2023-02-20 10:29:18 +01:00
Nicolas CARPi afb5cb09be add Nicolas CARPi to authors in composer.json 2023-02-20 02:35:35 +01:00
Will Power 1d628527e5 Merge pull request #98 from thomascorthals/master
Namespace mistake in docs
2023-01-06 14:16:58 +00:00
thomascorthals d4528f58fe Namespace mistake in docs 2023-01-06 12:32:08 +01:00
Nicolas CARPi e6e5d59297 lint 2022-12-29 14:07:44 +01:00
Nicolas CARPi a968dd392a remove the MightNotMakeAssertions shim
use expectNotToPerformAssertions() instead
2022-12-29 14:04:37 +01:00
Nicolas CARPi 86338cf3cd split constructor in multilines 2022-12-29 13:47:06 +01:00
Nicolas CARPi dc4e99e8c3 use @PHP82Migration ruleset 2022-12-29 13:46:05 +01:00
Nicolas CARPi 656e966cdf remove useless function comment 2022-12-29 13:43:52 +01:00
Nicolas CARPi 921425d0ff use psr12 in phpcs config 2022-12-29 13:35:16 +01:00
Nicolas CARPi 4711674ec0 crank phpstan to level 6 2022-12-29 13:32:02 +01:00
Nicolas CARPi e049285b46 use lint-ci for bacon test 2022-12-09 21:46:59 +01:00
Nicolas CARPi b52655b803 add ignore env to php-cs-fixer for ci (php 8.2 issue) 2022-12-09 21:44:02 +01:00
Nicolas CARPi ad89250a8f try adding php8.2 in test matrix 2022-12-09 21:37:29 +01:00
Nicolas CARPi f034bc782e use interface 2022-12-09 19:45:53 +01:00
Nicolas CARPi f8ba3b234b fix endroid qr provider handleErrorCorrectionLevel signature 2022-12-09 19:42:16 +01:00
Nicolas CARPi db6256b5e8 fix endroid handleColor 2022-12-09 19:37:49 +01:00
Nicolas CARPi 041d0e38e2 fix bacon construct signature 2022-12-09 19:36:47 +01:00
Nicolas CARPi 1e75674a45 fix action php version and handleColour function signature for bacon qr 2022-12-09 19:32:27 +01:00
Nicolas CARPi 25f463d19b turns out I had a global gitignore with lib... -_-' 2022-12-09 19:27:50 +01:00
Nicolas CARPi aaf24a66bd why is phpstan failing in actions? 2022-12-09 19:19:24 +01:00
Nicolas CARPi 75d8955967 update the actions 2022-12-09 18:43:15 +01:00
Nicolas CARPi 7e067166aa use relative path for LICENSE link in README
(really it's to trigger an action!)
2022-12-09 18:39:28 +01:00
Nicolas CARPi 58a9628a57 add phpstan config file, ignore broken files because of optional lib
and add it to action
2022-12-09 18:32:43 +01:00
Nicolas CARPi 887f261c88 add CHANGELOG.md 2022-12-09 18:18:55 +01:00
Nicolas CARPi 225216a616 fix php version in readme 2022-12-09 18:12:38 +01:00
Nicolas CARPi 79988ef58a add phpstan 2022-12-08 00:00:18 +01:00
Nicolas CARPi f1e73aab3a fix the testsDependency folder 2022-12-07 23:29:04 +01:00
Nicolas CARPi db0515e826 fix versions in github workflow and add a lint-ci command 2022-12-07 23:14:28 +01:00
Nicolas CARPi 4be83550c0 remove php-parallel-lint: php-cs-fixer does much more 2022-12-07 23:12:17 +01:00
Nicolas CARPi ac721e57b9 add php-cs-fixer 2022-12-07 23:10:13 +01:00
Nicolas CARPi a8c297c178 second batch with QR providers 2022-12-07 23:02:41 +01:00
Nicolas CARPi aeb4b00c60 first batch 2022-12-07 22:28:52 +01:00
William Hall ba4e8c55ed 🔧 don't need version number in composer.json #90 2022-05-10 13:33:17 +01:00
William Hall 65681de5a3 🔖 actually 1.8.2 2022-03-22 16:11:07 +00:00
William Hall dc681e1ced Merge remote-tracking branch 'mpeveler/bugfix-endroid-4' 2022-02-14 17:14:18 +00:00
Matthew Peveler 5f2611cde2 Fix compatibility with endroid/qr-code 4 2022-02-14 11:09:10 -05:00
William Hall 5d36d4feb0 Merge remote-tracking branch 'origin/test-specific-provider' 2021-12-29 18:17:40 +00:00
William Hall cb45226800 🚨 test in php < 7.1 2021-12-19 19:50:25 +00:00
William Hall 2d1ec8d265 🐛 be more specific with hex colour handling 2021-12-19 19:44:10 +00:00
William Hall ae4da10ff1 🚨 test bacon qr code provider 2021-12-19 19:33:11 +00:00
William Hall 66e1e030ba ♻️ re arrange useful testing code for provider tests 2021-12-19 19:32:45 +00:00
Will Power e76f31e93b Merge pull request #83 from brainfoolong/master
convert issuer to string prevents php 8.1 errors
2021-12-19 18:21:51 +00:00
Will Power 4203749bd2 Merge pull request #85 from MasterOdin/mpeveler/chore-deprecations
Catch deprecations as part of test suite
2021-12-19 18:19:17 +00:00
William Hall 4f1543f782 🚨 add test to cover null issuer 2021-12-19 18:16:38 +00:00
Matthew Peveler 8a220fe37b Catch deprecations as part of test suite 2021-12-01 10:12:42 -05:00
William Hall 8a5cb24967 🔧 add 8.1 to testing to confirm issue 2021-12-01 09:34:38 +00:00
Roland Eigelsreiter dc9f168746 added 8.1 to builds 2021-11-29 10:25:23 +01:00
Roland Eigelsreiter 0096cce02d convert issuer to string prevents php 8.1 errors
By forcing $this->issuer to be string, even if null is set, it prevents throwing errors in PHP 8.1 is `rawurlencode` is not allowed to have null as parameter.

It would be better to force string to be already in `__construct`, but this may create a breaking change for existing users.
2021-11-29 06:50:54 +01:00
Rob Janssen 042f347666 Update LICENSE 2021-11-02 15:07:43 +01:00
54 changed files with 776 additions and 834 deletions
+30
View File
@@ -0,0 +1,30 @@
name: Test Bacon QR Code Provider
on:
push:
pull_request:
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
php-version: ['8.1', '8.2']
steps:
- uses: actions/checkout@v3
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
tools: composer
coverage: xdebug
ini-values: error_reporting=E_ALL
- uses: ramsey/composer-install@v2
- run: composer require bacon/bacon-qr-code
- run: composer lint-ci
- run: composer test testsDependency/BaconQRCodeTest.php
+31
View File
@@ -0,0 +1,31 @@
name: Test Endroid QR Code Provider
on:
push:
pull_request:
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
php-version: ['8.1', '8.2']
endroid-version: ["^4"]
steps:
- uses: actions/checkout@v3
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
tools: composer
coverage: xdebug
ini-values: error_reporting=E_ALL
- uses: ramsey/composer-install@v2
- run: composer require endroid/qrcode:${{ matrix.endroid-version }}
- run: composer lint-ci
- run: composer test testsDependency/EndroidQRCodeTest.php
+6 -4
View File
@@ -10,18 +10,20 @@ jobs:
strategy:
matrix:
php-version: ['5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0']
php-version: ['8.1', '8.2']
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
tools: composer
coverage: xdebug
ini-values: error_reporting=E_ALL
- uses: ramsey/composer-install@v1
- uses: ramsey/composer-install@v2
- run: composer lint
- run: composer lint-ci
- run: composer phpstan
- run: composer test
+1
View File
@@ -190,3 +190,4 @@ composer.lock
.vs/
.phpunit.result.cache
.php-cs-fixer.cache
+61
View File
@@ -0,0 +1,61 @@
<?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_blank_line_before_namespace' => 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)
;
+32
View File
@@ -0,0 +1,32 @@
# RobThree\TwoFactorAuth changelog
# 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 -1
View File
@@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2014-2015 Rob Janssen
Copyright (c) 2014-2021 Rob Janssen and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+3 -3
View File
@@ -10,9 +10,9 @@ PHP library for [two-factor (or multi-factor) authentication](http://en.wikipedi
## Requirements
* Tested on PHP 5.6 up to 8.0
* Requires PHP version >=8.1
* [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.
* [random_bytes()](http://php.net/manual/en/function.random-bytes.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.
Optionally, you may need:
@@ -38,6 +38,6 @@ If you need more in-depth information about the configuration available then you
## 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))
+2 -3
View File
@@ -15,7 +15,7 @@
<PHPDevHostName>localhost</PHPDevHostName>
<IISProjectUrl>http://localhost:41315/</IISProjectUrl>
<Runtime>PHP</Runtime>
<RuntimeVersion>7.0</RuntimeVersion>
<RuntimeVersion>8.1</RuntimeVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<IncludeDebugInformation>true</IncludeDebugInformation>
@@ -34,7 +34,6 @@
<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" />
@@ -67,4 +66,4 @@
<Content Include="LICENSE" />
<Content Include="phpunit.xml" />
</ItemGroup>
</Project>
</Project>
+36 -5
View File
@@ -1,7 +1,6 @@
{
"name": "robthree/twofactorauth",
"description": "Two Factor Authentication",
"version": "1.8.1",
"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.1.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,11 +49,32 @@
}
},
"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"
]
},
"archive": {
"exclude": [
"/.github/",
"/demo/",
"/docs/",
"/tests/",
"/testsDependency/",
"/.gitignore",
"/logo.png",
"/multifactorauthforeveryone.png",
"/phpunit.xml",
"/TwoFactorAuth.phpproj",
"/TwoFactorAuth.sln"
]
}
}
+2 -3
View File
@@ -24,9 +24,8 @@ Argument | Default value | Use
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
2. [OpenSSLRNGProvider](https://github.com/RobThree/TwoFactorAuth/blob/master/lib/Providers/Rng/OpenSSLRNGProvider.php) where openssl is available
3. [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.
+1 -1
View File
@@ -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();
+16
View File
@@ -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';
}
+36 -41
View File
@@ -1,51 +1,39 @@
<?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 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,17 +49,17 @@ 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;
$backend = new SvgImageBackEnd();
break;
case 'eps':
$backend = new EpsImageBackEnd;
$backend = new EpsImageBackEnd();
break;
default:
$backend = new ImagickImageBackEnd($this->format);
@@ -105,7 +93,7 @@ class BaconQrCodeProvider implements IQRCodeProvider
new EyeFill(null, null),
new EyeFill(null, null),
new EyeFill(null, null)
)
),
));
}
@@ -121,16 +109,23 @@ 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) {
// 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 #');
}
// split the array into three chunks
$split = str_split(trim($input, '#'), strlen($input) / 3);
$split = str_split($input, strlen($input) / 3);
// cope with three character hex reference
// three characters plus a # = 4
if (strlen($input) == 4) {
if (strlen($input) == 3) {
array_walk($split, function (&$character) {
$character = str_repeat($character, 2);
});
@@ -147,6 +142,6 @@ class BaconQrCodeProvider implements IQRCodeProvider
return $colour;
}
throw new \RuntimeException('Invalid colour value');
throw new RuntimeException('Invalid colour value');
}
}
+5 -9
View File
@@ -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;
/**
* @param string $url
*
* @return string|bool
*/
protected function getContent($url)
protected function getContent(string $url): string|bool
{
$curlhandle = curl_init();
@@ -23,7 +19,7 @@ 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);
+33 -12
View File
@@ -1,35 +1,57 @@
<?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;
use Endroid\QrCode\QrCode;
use Endroid\QrCode\Writer\PngWriter;
class EndroidQrCodeProvider implements IQRCodeProvider
{
public $bgcolor;
public $color;
public $margin;
public $errorcorrectionlevel;
protected $endroid4 = false;
public function __construct($bgcolor = 'ffffff', $color = '000000', $margin = 0, $errorcorrectionlevel = 'H')
{
$this->endroid4 = method_exists(QrCode::class, 'create');
$this->bgcolor = $this->handleColor($bgcolor);
$this->color = $this->handleColor($color);
$this->margin = $margin;
$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
{
return $this->qrCodeInstance($qrtext, $size)->writeString();
if (!$this->endroid4) {
return $this->qrCodeInstance($qrtext, $size)->writeString();
}
$writer = new PngWriter();
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);
$qrCode->setSize($size);
@@ -42,29 +64,28 @@ class EndroidQrCodeProvider implements IQRCodeProvider
return $qrCode;
}
private function handleColor($color)
private function handleColor(string $color): Color
{
$split = str_split($color, 2);
$r = hexdec($split[0]);
$g = hexdec($split[1]);
$b = hexdec($split[2]);
return ['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
{
switch ($level) {
case 'L':
return ErrorCorrectionLevel::LOW();
return $this->endroid4 ? new ErrorCorrectionLevelLow() : ErrorCorrectionLevel::LOW();
case 'M':
return ErrorCorrectionLevel::MEDIUM();
return $this->endroid4 ? new ErrorCorrectionLevelMedium() : ErrorCorrectionLevel::MEDIUM();
case 'Q':
return ErrorCorrectionLevel::QUARTILE();
return $this->endroid4 ? new ErrorCorrectionLevelQuartile() : ErrorCorrectionLevel::QUARTILE();
case 'H':
return ErrorCorrectionLevel::HIGH();
default:
return ErrorCorrectionLevel::HIGH();
return $this->endroid4 ? new ErrorCorrectionLevelHigh() : ErrorCorrectionLevel::HIGH();
}
}
}
@@ -1,12 +1,17 @@
<?php
declare(strict_types=1);
namespace RobThree\Auth\Providers\Qr;
use Endroid\QrCode\ErrorCorrectionLevel;
use Endroid\QrCode\Logo\Logo;
use Endroid\QrCode\QrCode;
use Endroid\QrCode\Writer\PngWriter;
class EndroidQrCodeWithLogoProvider extends EndroidQrCodeProvider
{
protected $logoPath;
protected $logoSize;
/**
@@ -20,13 +25,34 @@ class EndroidQrCodeWithLogoProvider extends EndroidQrCodeProvider
$this->logoSize = (array)$size;
}
protected function qrCodeInstance($qrtext, $size) {
public function getQRCodeImage(string $qrtext, int $size): string
{
if (!$this->endroid4) {
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]);
}
}
}
$writer = new PngWriter();
return $writer->write($this->qrCodeInstance($qrtext, $size), $logo)->getString();
}
protected function qrCodeInstance(string $qrtext, int $size): QrCode
{
$qrCode = parent::qrCodeInstance($qrtext, $size);
if ($this->logoPath) {
if (!$this->endroid4 && $this->logoPath) {
$qrCode->setLogoPath($this->logoPath);
if ($this->logoSize) {
$qrCode->setLogoSize($this->logoSize[0], $this->logoSize[1]);
$qrCode->setLogoSize($this->logoSize[0], $this->logoSize[1] ?? null);
}
}
@@ -1,61 +1,27 @@
<?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 = false, 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
+27
View File
@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace RobThree\Auth\Providers\Qr;
use function base64_decode;
use function preg_match;
trait HandlesDataUri
{
/**
* @return array<string, string>
*/
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'], true),
);
}
return null;
}
}
+4 -4
View File
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace RobThree\Auth\Providers\Qr;
interface IQRCodeProvider
@@ -12,13 +14,11 @@ interface IQRCodeProvider
*
* @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;
}
+9 -36
View File
@@ -1,56 +1,29 @@
<?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 = false, 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));
}
/**
* @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)
+5 -1
View File
@@ -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
{
}
+14 -66
View File
@@ -1,57 +1,19 @@
<?php
declare(strict_types=1);
namespace RobThree\Auth\Providers\Qr;
// http://goqr.me/api/doc/create-qr-code/
/**
* Use http://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 = false, 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,31 +31,12 @@ 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));
}
/**
* @param string $value
*
* @return string
*/
private function decodeColor($value)
{
return vsprintf('%d-%d-%d', sscanf($value, "%02x%02x%02x"));
}
/**
* @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://api.qrserver.com/v1/create-qr-code/'
. '?size=' . $size . 'x' . $size
@@ -105,4 +48,9 @@ class QRServerProvider extends BaseHTTPQRCodeProvider
. '&format=' . strtolower($this->format)
. '&data=' . rawurlencode($qrtext);
}
private function decodeColor(string $value): string
{
return vsprintf('%d-%d-%d', sscanf($value, '%02x%02x%02x'));
}
}
+10 -41
View File
@@ -1,42 +1,20 @@
<?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(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,24 +27,15 @@ 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));
}
/**
* @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
. '?qrsize=' . (string) $size
. '&e=' . strtolower($this->errorcorrectionlevel)
. '&bgdcolor=' . $this->bgcolor
. '&fgdcolor=' . $this->color
+4 -2
View File
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace RobThree\Auth\Providers\Rng;
class CSRNGProvider implements IRNGProvider
@@ -7,7 +9,7 @@ class CSRNGProvider implements IRNGProvider
/**
* {@inheritdoc}
*/
public function getRandomBytes($bytecount)
public function getRandomBytes(int $bytecount): string
{
return random_bytes($bytecount); // PHP7+
}
@@ -15,7 +17,7 @@ class CSRNGProvider implements IRNGProvider
/**
* {@inheritdoc}
*/
public function isCryptographicallySecure()
public function isCryptographicallySecure(): bool
{
return true;
}
+8 -11
View File
@@ -1,28 +1,25 @@
<?php
declare(strict_types=1);
namespace RobThree\Auth\Providers\Rng;
use function in_array;
class HashRNGProvider implements IRNGProvider
{
/** @var string */
private $algorithm;
/**
* @param string $algorithm
*/
public function __construct($algorithm = 'sha256')
public function __construct(private string $algorithm = 'sha256')
{
$algos = array_values(hash_algos());
if (!in_array($algorithm, $algos, true)) {
if (!in_array($this->algorithm, $algos, true)) {
throw new RNGException('Unsupported algorithm specified');
}
$this->algorithm = $algorithm;
}
/**
* {@inheritdoc}
*/
public function getRandomBytes($bytecount)
public function getRandomBytes(int $bytecount): string
{
$result = '';
$hash = mt_rand();
@@ -36,7 +33,7 @@ class HashRNGProvider implements IRNGProvider
/**
* {@inheritdoc}
*/
public function isCryptographicallySecure()
public function isCryptographicallySecure(): bool
{
return false;
}
+4 -10
View File
@@ -1,18 +1,12 @@
<?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);
public function getRandomBytes(int $bytecount): string;
/**
* @return bool whether this provider is cryptographically secure
*/
public function isCryptographicallySecure();
public function isCryptographicallySecure(): bool;
}
-37
View File
@@ -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;
}
}
+7 -18
View File
@@ -1,39 +1,28 @@
<?php
declare(strict_types=1);
namespace RobThree\Auth\Providers\Rng;
class OpenSSLRNGProvider implements IRNGProvider
{
/** @var bool */
private $requirestrong;
/**
* @param bool $requirestrong
*/
public function __construct($requirestrong = true)
public function __construct(private bool $requirestrong = true)
{
$this->requirestrong = $requirestrong;
}
/**
* {@inheritdoc}
*/
public function getRandomBytes($bytecount)
public function getRandomBytes(int $bytecount): string
{
$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;
// will throw an Exception on failure
return openssl_random_pseudo_bytes($bytecount, $crypto_strong);
}
/**
* {@inheritdoc}
*/
public function isCryptographicallySecure()
public function isCryptographicallySecure(): bool
{
return $this->requirestrong;
}
+5 -1
View File
@@ -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
{
}
+16 -19
View File
@@ -1,30 +1,28 @@
<?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;
/** @var array<string, mixed> */
public array $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)
{
public function __construct(
public string $url = 'https://google.com',
public string $expectedtimeformat = 'D, d M Y H:i:s O+',
array $options = null,
) {
$this->url = $url;
$this->expectedtimeformat = $expectedtimeformat;
if ($options === null) {
@@ -38,9 +36,9 @@ 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;
@@ -62,10 +60,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()));
}
}
}
+2
View File
@@ -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
+17 -29
View File
@@ -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]);
/* 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()));
}
}
+5 -1
View File
@@ -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
{
}
+107 -203
View File
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace RobThree\Auth;
use RobThree\Auth\Providers\Qr\IQRCodeProvider;
@@ -7,7 +9,6 @@ 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;
@@ -18,69 +19,30 @@ use RobThree\Auth\Providers\Time\NTPTimeProvider;
// 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 ?string $issuer = null,
private int $digits = 6,
private int $period = 30,
private Algorithm $algorithm = Algorithm::Sha1,
private ?IQRCodeProvider $qrcodeprovider = null,
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,16 +50,11 @@ 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 = 80, bool $requirecryptosecure = true): 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');
@@ -111,37 +68,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(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);
@@ -160,44 +104,13 @@ class TwoFactorAuth
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, 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();
@@ -209,17 +122,14 @@ class TwoFactorAuth
/**
* 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
*/
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,50 +149,94 @@ 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, string $secret): string
{
return 'otpauth://totp/' . rawurlencode($label)
. '?secret=' . rawurlencode($secret)
. '&issuer=' . rawurlencode($this->issuer)
. '&issuer=' . rawurlencode((string)$this->issuer)
. '&period=' . intval($this->period)
. '&algorithm=' . rawurlencode(strtoupper($this->algorithm))
. '&algorithm=' . rawurlencode(strtoupper($this->algorithm->value))
. '&digits=' . intval($this->digits);
}
/**
* @param string $value
* @return string
* @throws TwoFactorAuthException
*/
private function base32Decode($value)
public function getQrCodeProvider(): IQRCodeProvider
{
// Set default QR Code provider if none was specified
if (null === $this->qrcodeprovider) {
return $this->qrcodeprovider = new QRServerProvider();
}
return $this->qrcodeprovider;
}
/**
* @throws TwoFactorAuthException
*/
public function getRngProvider(): IRNGProvider
{
if (null !== $this->rngprovider) {
return $this->rngprovider;
}
if (function_exists('random_bytes')) {
return $this->rngprovider = new CSRNGProvider();
}
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');
}
/**
* @throws TwoFactorAuthException
*/
public function getTimeProvider(): ITimeProvider
{
// Set default time provider if none was specified
if (null === $this->timeprovider) {
return $this->timeprovider = new LocalMachineTimeProvider();
}
return $this->timeprovider;
}
/**
* Timing-attack safe comparison of 2 codes (see http://blog.ircmaxell.com/2014/11/its-all-about-time.html)
*/
private function codeEquals(string $safe, string $user): bool
{
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;
}
private function getTime(?int $time = null): int
{
return ($time === null) ? $this->getTimeProvider()->getTime() : $time;
}
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 (strlen($value) == 0) {
return '';
@@ -307,54 +261,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;
}
}
+5 -1
View File
@@ -1,7 +1,11 @@
<?php
declare(strict_types=1);
namespace RobThree\Auth;
use Exception;
class TwoFactorAuthException extends Exception {}
class TwoFactorAuthException extends Exception
{
}
+12
View File
@@ -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
View File
@@ -2,6 +2,7 @@
<phpunit backupGlobals="false"
backupStaticAttributes="false"
colors="true"
convertDeprecationsToExceptions="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
-26
View File
@@ -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);
}
}
+25 -27
View File
@@ -1,53 +1,51 @@
<?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\TwoFactorAuth;
use RobThree\Auth\TwoFactorAuthException;
class IQRCodeProviderTest extends TestCase
{
/**
* @param string $datauri
*
* @return null|array
*/
private function DecodeDataUri($datauri)
{
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'])
);
}
use HandlesDataUri;
return null;
}
/**
* @return void
*/
public function testTotpUriIsCorrect()
public function testTotpUriIsCorrect(): void
{
$qr = new TestQrProvider();
$tfa = new TwoFactorAuth('Test&Issuer', 6, 30, 'sha1', $qr);
$tfa = new TwoFactorAuth('Test&Issuer', 6, 30, Algorithm::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']);
}
/**
* @return void
*/
public function testGetQRCodeImageAsDataUriThrowsOnInvalidSize()
public function testTotpUriIsCorrectNoIssuer(): void
{
$qr = new TestQrProvider();
$tfa = new TwoFactorAuth('Test', 6, 30, 'sha1', $qr);
/**
* 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, Algorithm::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=&period=30&algorithm=SHA1&digits=6@200', $data['data']);
}
public function testGetQRCodeImageAsDataUriThrowsOnInvalidSize(): void
{
$qr = new TestQrProvider();
$tfa = new TwoFactorAuth('Test', 6, 30, Algorithm::Sha1, $qr);
$this->expectException(TwoFactorAuthException::class);
+4 -8
View File
@@ -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;
}
/**
* {@inheritdoc}
*/
public function getMimeType()
public function getMimeType(): string
{
return 'test/test';
}
+5 -6
View File
@@ -1,21 +1,20 @@
<?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();
@@ -24,7 +23,7 @@ class CSRNGProviderTest extends TestCase
}
$this->assertTrue($rng->isCryptographicallySecure());
} else {
$this->noAssertionsMade();
$this->expectNotToPerformAssertions();
}
}
}
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Tests\Providers\Rng;
use PHPUnit\Framework\TestCase;
+11 -20
View File
@@ -1,56 +1,47 @@
<?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;
class IRNGProviderTest extends TestCase
{
/**
* @return void
*/
public function testCreateSecretThrowsOnInsecureRNGProvider()
public function testCreateSecretThrowsOnInsecureRNGProvider(): void
{
$rng = new TestRNGProvider();
$tfa = new TwoFactorAuth('Test', 6, 30, 'sha1', null, $rng);
$tfa = new TwoFactorAuth('Test', 6, 30, Algorithm::Sha1, null, $rng);
$this->expectException(TwoFactorAuthException::class);
$tfa->createSecret();
}
/**
* @return void
*/
public function testCreateSecretOverrideSecureDoesNotThrowOnInsecureRNG()
public function testCreateSecretOverrideSecureDoesNotThrowOnInsecureRNG(): void
{
$rng = new TestRNGProvider();
$tfa = new TwoFactorAuth('Test', 6, 30, 'sha1', null, $rng);
$tfa = new TwoFactorAuth('Test', 6, 30, Algorithm::Sha1, null, $rng);
$this->assertEquals('ABCDEFGHIJKLMNOP', $tfa->createSecret(80, false));
}
/**
* @return void
*/
public function testCreateSecretDoesNotThrowOnSecureRNGProvider()
public function testCreateSecretDoesNotThrowOnSecureRNGProvider(): void
{
$rng = new TestRNGProvider(true);
$tfa = new TwoFactorAuth('Test', 6, 30, 'sha1', null, $rng);
$tfa = new TwoFactorAuth('Test', 6, 30, Algorithm::Sha1, null, $rng);
$this->assertEquals('ABCDEFGHIJKLMNOP', $tfa->createSecret());
}
/**
* @return void
*/
public function testCreateSecretGeneratesDesiredAmountOfEntropy()
public function testCreateSecretGeneratesDesiredAmountOfEntropy(): void
{
$rng = new TestRNGProvider(true);
$tfa = new TwoFactorAuth('Test', 6, 30, 'sha1', null, $rng);
$tfa = new TwoFactorAuth('Test', 6, 30, Algorithm::Sha1, null, $rng);
$this->assertEquals('A', $tfa->createSecret(5));
$this->assertEquals('AB', $tfa->createSecret(6));
$this->assertEquals('ABCDEFGHIJKLMNOPQRSTUVWXYZ', $tfa->createSecret(128));
@@ -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();
}
}
}
+3 -1
View File
@@ -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,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Tests\Providers\Rng;
use PHPUnit\Framework\TestCase;
+5 -10
View File
@@ -1,26 +1,21 @@
<?php
declare(strict_types=1);
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)
public function __construct(private bool $isSecure = false)
{
$this->isSecure = $isSecure;
}
/**
* {@inheritdoc}
*/
public function getRandomBytes($bytecount)
public function getRandomBytes(int $bytecount): string
{
$result = '';
@@ -34,7 +29,7 @@ class TestRNGProvider implements IRNGProvider
/**
* {@inheritdoc}
*/
public function isCryptographicallySecure()
public function isCryptographicallySecure(): bool
{
return $this->isSecure;
}
+12 -23
View File
@@ -1,53 +1,42 @@
<?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;
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('Test', 6, 30, Algorithm::Sha1, null, 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('Test', 6, 30, Algorithm::Sha1, null, 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('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();
}
}
+3 -1
View File
@@ -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;
}
+25 -72
View File
@@ -1,61 +1,41 @@
<?php
declare(strict_types=1);
namespace Tests;
use PHPUnit\Framework\TestCase;
use RobThree\Auth\TwoFactorAuthException;
use ReflectionMethod;
use RobThree\Auth\Algorithm;
use RobThree\Auth\TwoFactorAuth;
use RobThree\Auth\TwoFactorAuthException;
class TwoFactorAuthTest extends TestCase
{
use MightNotMakeAssertions;
/**
* @return void
*/
public function testConstructorThrowsOnInvalidDigits()
public function testConstructorThrowsOnInvalidDigits(): void
{
$this->expectException(TwoFactorAuthException::class);
new TwoFactorAuth('Test', 0);
}
/**
* @return void
*/
public function testConstructorThrowsOnInvalidPeriod()
public function testConstructorThrowsOnInvalidPeriod(): void
{
$this->expectException(TwoFactorAuthException::class);
new TwoFactorAuth('Test', 6, 0);
}
/**
* @return void
*/
public function testConstructorThrowsOnInvalidAlgorithm()
{
$this->expectException(TwoFactorAuthException::class);
new TwoFactorAuth('Test', 6, 30, 'xxx');
}
/**
* @return void
*/
public function testGetCodeReturnsCorrectResults()
public function testGetCodeReturnsCorrectResults(): void
{
$tfa = new TwoFactorAuth('Test');
$this->assertEquals('543160', $tfa->getCode('VMR466AB62ZBOKHE', 1426847216));
$this->assertEquals('538532', $tfa->getCode('VMR466AB62ZBOKHE', 0));
}
/**
* @return void
*/
public function testEnsureAllTimeProvidersReturnCorrectTime()
public function testEnsureAllTimeProvidersReturnCorrectTime(): void
{
$tfa = new TwoFactorAuth('Test', 6, 30, 'sha1');
$tfa = new TwoFactorAuth('Test', 6, 30, Algorithm::Sha1);
$tfa->ensureCorrectTime(array(
new \RobThree\Auth\Providers\Time\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??
@@ -63,13 +43,10 @@ class TwoFactorAuthTest extends TestCase
//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'),
));
$this->noAssertionsMade();
$this->expectNotToPerformAssertions();
}
/**
* @return void
*/
public function testVerifyCodeWorksCorrectly()
public function testVerifyCodeWorksCorrectly(): void
{
$tfa = new TwoFactorAuth('Test', 6, 30);
$this->assertTrue($tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847190));
@@ -88,10 +65,7 @@ class TwoFactorAuthTest extends TestCase
$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);
@@ -117,10 +91,7 @@ class TwoFactorAuthTest extends TestCase
$this->assertEquals(0, $timeslice8);
}
/**
* @return void
*/
public function testGetCodeThrowsOnInvalidBase32String1()
public function testGetCodeThrowsOnInvalidBase32String1(): void
{
$tfa = new TwoFactorAuth('Test');
@@ -129,10 +100,7 @@ class TwoFactorAuthTest extends TestCase
$tfa->getCode('FOO1BAR8BAZ9'); //1, 8 & 9 are invalid chars
}
/**
* @return void
*/
public function testGetCodeThrowsOnInvalidBase32String2()
public function testGetCodeThrowsOnInvalidBase32String2(): void
{
$tfa = new TwoFactorAuth('Test');
@@ -141,10 +109,7 @@ class TwoFactorAuthTest extends TestCase
$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
@@ -159,7 +124,7 @@ class TwoFactorAuthTest extends TestCase
// Dave Thomas and Andy Hunt -- "Pragmatic Unit Testing
$tfa = new TwoFactorAuth('Test');
$method = new \ReflectionMethod(TwoFactorAuth::class, 'base32Decode');
$method = new ReflectionMethod(TwoFactorAuth::class, 'base32Decode');
$method->setAccessible(true);
// Test vectors from: https://tools.ietf.org/html/rfc4648#page-12
@@ -172,10 +137,7 @@ class TwoFactorAuthTest extends TestCase
$this->assertEquals('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.
@@ -183,7 +145,7 @@ class TwoFactorAuthTest extends TestCase
// "In some circumstances, the use of padding ("=") in base-encoded data is not required or used."
$tfa = new TwoFactorAuth('Test');
$method = new \ReflectionMethod(TwoFactorAuth::class, 'base32Decode');
$method = new ReflectionMethod(TwoFactorAuth::class, 'base32Decode');
$method->setAccessible(true);
// Test vectors from: https://tools.ietf.org/html/rfc4648#page-12
@@ -196,14 +158,11 @@ class TwoFactorAuthTest extends TestCase
$this->assertEquals('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');
$tfa = new TwoFactorAuth('Test', 8, 30, Algorithm::Sha1);
$this->assertEquals('94287082', $tfa->getCode($secret, 59));
$this->assertEquals('07081804', $tfa->getCode($secret, 1111111109));
$this->assertEquals('14050471', $tfa->getCode($secret, 1111111111));
@@ -212,14 +171,11 @@ class TwoFactorAuthTest extends TestCase
$this->assertEquals('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');
$tfa = new TwoFactorAuth('Test', 8, 30, Algorithm::Sha256);
$this->assertEquals('46119246', $tfa->getCode($secret, 59));
$this->assertEquals('68084774', $tfa->getCode($secret, 1111111109));
$this->assertEquals('67062674', $tfa->getCode($secret, 1111111111));
@@ -228,14 +184,11 @@ class TwoFactorAuthTest extends TestCase
$this->assertEquals('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');
$tfa = new TwoFactorAuth('Test', 8, 30, Algorithm::Sha512);
$this->assertEquals('90693936', $tfa->getCode($secret, 59));
$this->assertEquals('25091201', $tfa->getCode($secret, 1111111109));
$this->assertEquals('99943326', $tfa->getCode($secret, 1111111111));
+55
View File
@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace TestsDependency;
use PHPUnit\Framework\TestCase;
use RobThree\Auth\Algorithm;
use RobThree\Auth\Providers\Qr\BaconQrCodeProvider;
use RobThree\Auth\Providers\Qr\HandlesDataUri;
use RobThree\Auth\TwoFactorAuth;
use RuntimeException;
class BaconQRCodeTest extends TestCase
{
use HandlesDataUri;
public function testDependency(): void
{
$qr = new BaconQrCodeProvider(1, '#000', '#FFF', 'svg');
$tfa = new TwoFactorAuth('Test&Issuer', 6, 30, Algorithm::Sha1, $qr);
$data = $this->DecodeDataUri($tfa->getQRCodeImageAsDataUri('Test&Label', 'VMR466AB62ZBOKHE'));
$this->assertEquals('image/svg+xml', $data['mimetype']);
}
public function testBadTextColour(): void
{
$this->expectException(RuntimeException::class);
new BaconQrCodeProvider(1, 'not-a-colour', '#FFF');
}
public function testBadBackgroundColour(): void
{
$this->expectException(RuntimeException::class);
new BaconQrCodeProvider(1, '#000', 'not-a-colour');
}
public function testBadTextColourHexRef(): void
{
$this->expectException(RuntimeException::class);
new BaconQrCodeProvider(1, '#AAAA', '#FFF');
}
public function testBadBackgroundColourHexRef(): void
{
$this->expectException(RuntimeException::class);
new BaconQrCodeProvider(1, '#000', '#AAAA');
}
}
+26
View File
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace TestsDependency;
use PHPUnit\Framework\TestCase;
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(): void
{
$qr = new EndroidQrCodeProvider();
$tfa = new TwoFactorAuth('Test&Issuer', 6, 30, Algorithm::Sha1, $qr);
$data = $this->DecodeDataUri($tfa->getQRCodeImageAsDataUri('Test&Label', 'VMR466AB62ZBOKHE'));
$this->assertEquals('image/png', $data['mimetype']);
$this->assertEquals('base64', $data['encoding']);
$this->assertNotEmpty($data['data']);
}
}