Compare commits

..

108 Commits

Author SHA1 Message Date
Will Power ab93dd41ce Merge pull request #115 from Mattie112/endroidqr-v5
Fix issue #114 (Support for EndroidQR v5)
2023-11-14 12:50:27 +00:00
William Hall ec35073c06 🔧 test all endroid versions we can care about 2023-11-14 12:49:15 +00:00
Matthijs van Schooten 13a56018f4 Fixed typo 2023-11-10 13:43:28 +01:00
Matthijs van Schooten 4b6b0601b7 Fixed check ordering 2023-11-10 13:41:25 +01:00
Matthijs van Schooten 126afe4c25 Removed dependency again from composer 2023-11-10 13:32:44 +01:00
Matthijs van Schooten 63f6259db2 Added support for EndroidQR v5 2023-11-10 13:27:45 +01:00
William Hall 137df4dd3c 🔧 remove deprecated rule https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/pull/7066 2023-07-29 15:46:20 +01:00
Will Power 9476751d6b Merge pull request #110 from modelrailroader/patch-1
Fix docs: Construct new TwoFactorAuth-class
2023-07-29 15:36:31 +01:00
Jan 0565d63e6a fix: better understanding for QrCodeProvider class 2023-06-23 21:21:37 +02:00
Jan d5aa22a96f fix: hyperlinks for new file extensions 2023-06-23 17:56:25 +02:00
Jan a6f7735b9f fix: docs for new PHP Version 2023-06-23 17:54:58 +02:00
Jan 6416e79c3e fix: fixed typing 2023-06-11 12:22:26 +02:00
Jan 83611592db fix: fixed documentation 2023-06-11 12:20:59 +02:00
Jan dd0ecaeed1 Fix: Changes for > PHP 8 2023-06-08 16:56:08 +02:00
Jan 241dfec585 fix: fixed docs 2023-06-05 16:09:42 +02:00
Rob Janssen 63b49ce25c Merge pull request #108 from xHeaven/master
code improvements, readability enhancements, type safety, modern syntax adaptation
2023-06-04 01:03:17 +02:00
Mark Magyar e554a9b5e4 change all $qrtext to camelCase version 2023-05-28 19:47:10 +02:00
Mark Magyar 9bc454f425 use null coalescing assignment instead of just null coalescing 2023-05-28 18:58:39 +02:00
Mark Magyar c2183e16b3 use property promotion for options 2023-05-28 18:46:07 +02:00
Mark Magyar cf84371dad use http_build_query instead of sprintf to improve readability 2023-05-28 01:58:44 +02:00
Mark Magyar 4d91d6ae27 use sprintf instead of concatenating 2023-05-28 01:13:12 +02:00
Mark Magyar d60113a77a drop setAccessible calls as they its NOOP since PHP 8.1 2023-05-27 22:02:24 +02:00
Mark Magyar 3f3327e2a1 use type safe assertions 2023-05-27 22:01:34 +02:00
Mark Magyar 8bf962ecee add readonly identifier 2023-05-27 22:00:48 +02:00
Mark Magyar b80431341e remove pointless +0 2023-05-27 22:00:11 +02:00
Mark Magyar f542788673 fix formatting 2023-05-27 21:59:55 +02:00
Mark Magyar ab51d16de7 remove redundant assignment, use shorthard for nullable type 2023-05-27 21:43:04 +02:00
Mark Magyar d4f6aa1ee6 add missing @throws tag 2023-05-27 21:41:10 +02:00
Mark Magyar 04ff3e7e3f fix nullable return type hint 2023-05-27 21:37:44 +02:00
Mark Magyar d94ee9c769 remove useless @throws because it never throws 2023-05-27 21:36:04 +02:00
Mark Magyar 955ce522f0 add readonly modifiers wherever possible 2023-05-27 21:33:54 +02:00
Mark Magyar e584f0c56a remove unnecessary type casts 2023-05-27 21:30:29 +02:00
Mark Magyar 5949a29b89 use null coalescing operator instead of if checks 2023-05-27 21:29:57 +02:00
Mark Magyar 8bf8800523 use empty string comparison instead of strlen call 2023-05-27 21:29:57 +02:00
Mark Magyar 2fd60fad5e use type safe asserts 2023-05-27 21:29:57 +02:00
Mark Magyar 26413e69f0 extract strlen check from loop 2023-05-27 21:29:57 +02:00
Mark Magyar 3adb3e54e5 add missing delimiter for proper escaping 2023-05-27 21:29:57 +02:00
Mark Magyar c45f7a23c9 fix return types for v3, replace switch with match 2023-05-27 21:29:57 +02:00
Mark Magyar bdfa67d1f6 use static keyword for anonymous function definition 2023-05-27 21:29:57 +02:00
Mark Magyar 7675282f45 put scalar types to the right 2023-05-27 21:29:57 +02:00
Mark Magyar 1ca305ec4a use spread syntax instead of array_merge 2023-05-27 21:29:57 +02:00
Mark Magyar aadc32e85e make parameter explicitly nullable 2023-05-27 21:29:57 +02:00
Mark Magyar 9a1aeb8c1f fix formatting 2023-05-27 20:19:48 +02:00
Mark Magyar 4c8a88224a replace switch with match 2023-05-27 20:14:33 +02:00
Mark Magyar 9523b35d34 use strict comparison 2023-05-27 20:06:37 +02:00
Mark Magyar cc9400d901 fix possible typo in parameter name
Interface's method signature:
```php
public function getQRCodeImage(string $qrtext, int $size): string;
```
2023-05-27 20:05:58 +02:00
Mark Magyar dfb5c1a113 make $borderWidth readonly 2023-05-27 20:04:08 +02:00
William Hall 098dce6735 📚 duplicate default provider warning into docs 2023-04-30 14:36:00 +01:00
Rob Janssen ae1cc5e8af Merge pull request #105 from RMEngels/master
Add warning of default QR Code provider to README.md
2023-04-30 01:27:44 +02:00
RMEngels 6e71f43bb1 Add warning of default QR Code provider to README.md 2023-04-25 11:30:44 +02:00
Will Power 5968be2cee Merge pull request #102 from NicolasCARPi/badges
Fix badges in README
2023-02-27 10:01:43 +00:00
Nicolas CARPi 04e26c9961 remove codeclimate badge because coverage is not uploaded there anyway 2023-02-25 13:07:20 +01:00
Nicolas CARPi 8391c5efb0 fix build badge and codeclimate badge
see: https://github.com/badges/shields/issues/8671
2023-02-25 13:05:01 +01:00
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
55 changed files with 940 additions and 983 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: ["^3","^4","^5"]
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 }} -W
- 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
+60
View File
@@ -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)
;
+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
+11 -4
View File
@@ -1,6 +1,6 @@
# ![Logo](https://raw.githubusercontent.com/RobThree/TwoFactorAuth/master/logo.png) PHP library for Two Factor Authentication
[![Build status](https://img.shields.io/github/workflow/status/RobThree/TwoFactorAuth/Test/master?style=flat-square)](https://github.com/RobThree/TwoFactorAuth/actions?query=branch%3Amaster) [![Latest Stable Version](https://img.shields.io/packagist/v/robthree/twofactorauth.svg?style=flat-square)](https://packagist.org/packages/robthree/twofactorauth) [![License](https://img.shields.io/packagist/l/robthree/twofactorauth.svg?style=flat-square)](LICENSE) [![Downloads](https://img.shields.io/packagist/dt/robthree/twofactorauth.svg?style=flat-square)](https://packagist.org/packages/robthree/twofactorauth) [![Code Climate](https://img.shields.io/codeclimate/github/RobThree/TwoFactorAuth.svg?style=flat-square)](https://codeclimate.com/github/RobThree/TwoFactorAuth) [![PayPal donate button](http://img.shields.io/badge/paypal-donate-orange.svg?style=flat-square)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=6MB5M2SQLP636 "Keep me off the streets")
[![Build status](https://img.shields.io/github/actions/workflow/status/robthree/twofactorauth/test.yml?branch=master)](https://github.com/RobThree/TwoFactorAuth/actions?query=branch%3Amaster) [![Latest Stable Version](https://img.shields.io/packagist/v/robthree/twofactorauth.svg?style=flat-square)](https://packagist.org/packages/robthree/twofactorauth) [![License](https://img.shields.io/packagist/l/robthree/twofactorauth.svg?style=flat-square)](LICENSE) [![Downloads](https://img.shields.io/packagist/dt/robthree/twofactorauth.svg?style=flat-square)](https://packagist.org/packages/robthree/twofactorauth) [![PayPal donate button](http://img.shields.io/badge/paypal-donate-orange.svg?style=flat-square)](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.
@@ -8,11 +8,18 @@ PHP library for [two-factor (or multi-factor) authentication](http://en.wikipedi
<img src="https://raw.githubusercontent.com/RobThree/TwoFactorAuth/master/multifactorauthforeveryone.png">
</p>
## Warning:
By default, this package uses the `lib/Providers/QRServerProvider.php` as QR code generator. This provider is __not__ suggested for applications where absolute security is needed, because it uses an external service for the QR code generation.
You can make use of the included [Endroid](https://robthree.github.io/TwoFactorAuth/qr-codes/endroid.html) or [Bacon](https://robthree.github.io/TwoFactorAuth/qr-codes/bacon.html) providers which generate locally.
## 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 +45,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"
]
}
}
+11 -12
View File
@@ -7,15 +7,15 @@ 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.
@@ -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.
+9 -10
View File
@@ -20,17 +20,19 @@ You can also specify a size as a third argument which is 200 by default.
## Online Providers
[QRServerProvider](qr-codes/qr-server.html) (default)
[QRServerProvider](qr-codes/qr-server.md) (default)
[ImageChartsQRCodeProvider](qr-codes/image-charts.html)
**Warning:** Whilst it is the default, this provider is not suggested for applications where absolute security is needed, because it uses an external service for the QR code generation. You can make use of the included offline providers listed below which generate locally.
[QRicketProvider](qr-codes/qrickit.html)
[ImageChartsQRCodeProvider](qr-codes/image-charts.md)
[QRicketProvider](qr-codes/qrickit.md)
## 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.
@@ -50,11 +52,8 @@ use RobThree\Auth\TwoFactorAuth;
$qrCodeProvider = new YourChosenProvider();
$tfa = new TwoFactorAuth(
null,
6,
30,
'sha1',
$qrCodeProvider
issuer: "Your Company Or App Name",
qrcodeprovider: $qrCodeProvider
);
```
+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';
}
+42 -53
View File
@@ -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,17 +103,24 @@ 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 #');
}
// 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) {
array_walk($split, function (&$character) {
if (strlen($input) == 3) {
array_walk($split, static function (&$character) {
$character = str_repeat($character, 2);
});
}
@@ -147,6 +136,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);
+59 -19
View File
@@ -1,37 +1,62 @@
<?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;
protected $endroid5 = 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->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 = new QrCode($qrText);
$qrCode->setSize($size);
$qrCode->setErrorCorrectionLevel($this->errorcorrectionlevel);
@@ -42,29 +67,44 @@ class EndroidQrCodeProvider implements IQRCodeProvider
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 ['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 ErrorCorrectionLevel::LOW();
case 'M':
return ErrorCorrectionLevel::MEDIUM();
case 'Q':
return ErrorCorrectionLevel::QUARTILE();
case 'H':
return ErrorCorrectionLevel::HIGH();
default:
return 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\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) {
$qrCode = parent::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->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);
}
}
+17 -48
View File
@@ -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 = 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
. '&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);
}
}
+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>|null
*/
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;
}
}
+5 -5
View File
@@ -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;
}
+18 -41
View File
@@ -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 = 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));
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);
}
}
+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
{
}
+24 -73
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,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'));
}
}
+20 -48
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,28 +27,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 'http://qrickit.com/api/qr?' . http_build_query($queryParameters);
}
}
+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 readonly 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 readonly 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
{
}
+17 -26
View File
@@ -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()));
}
}
}
+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
+18 -30
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]);
$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
{
}
+100 -206
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 readonly ?string $issuer = null,
private readonly int $digits = 6,
private readonly int $period = 30,
private readonly 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,15 @@ 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
* @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(),
);
}
@@ -240,55 +151,88 @@ class TwoFactorAuth
}
/**
* @param ?int $time
*
* @return int
* Builds a string to be encoded in a QR code
*/
private function getTime($time = null)
public function getQRText(string $label, string $secret): string
{
return ($time === null) ? $this->getTimeProvider()->getTime() : $time;
return 'otpauth://totp/' . rawurlencode($label)
. '?secret=' . rawurlencode($secret)
. '&issuer=' . rawurlencode((string)$this->issuer)
. '&period=' . $this->period
. '&algorithm=' . rawurlencode(strtoupper($this->algorithm->value))
. '&digits=' . $this->digits;
}
public function getQrCodeProvider(): IQRCodeProvider
{
// Set default QR Code provider if none was specified
return $this->qrcodeprovider ??= new QRServerProvider();
}
/**
* @param int $time
* @param int $offset
*
* @return int
* @throws TwoFactorAuthException
*/
private function getTimeSlice($time = null, $offset = 0)
public function getRngProvider(): IRNGProvider
{
if ($this->rngprovider !== null) {
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');
}
public function getTimeProvider(): ITimeProvider
{
// Set default time provider if none was specified
return $this->timeprovider ??= new LocalMachineTimeProvider();
}
/**
* 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;
$strlen = strlen($safe);
for ($i = 0; $i < $strlen; $i++) {
$result |= (ord($safe[$i]) ^ ord($user[$i]));
}
return $result === 0;
}
return false;
}
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);
}
/**
* Builds a string to be encoded in a QR code
*
* @param string $label
* @param string $secret
*
* @return string
*/
public function getQRText($label, $secret)
private function base32Decode(string $value): string
{
return 'otpauth://totp/' . rawurlencode($label)
. '?secret=' . rawurlencode($secret)
. '&issuer=' . rawurlencode($this->issuer)
. '&period=' . intval($this->period)
. '&algorithm=' . rawurlencode(strtoupper($this->algorithm))
. '&digits=' . intval($this->digits);
}
/**
* @param string $value
* @return string
*/
private function base32Decode($value)
{
if (strlen($value) == 0) {
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 +251,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);
}
}
+28 -30
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']);
$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']);
}
/**
* @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->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']);
}
public function testGetQRCodeImageAsDataUriThrowsOnInvalidSize(): void
{
$qr = new TestQrProvider();
$tfa = new TwoFactorAuth('Test', 6, 30, Algorithm::Sha1, $qr);
$this->expectException(TwoFactorAuthException::class);
+5 -9
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;
return $qrText . '@' . $size;
}
/**
* {@inheritdoc}
*/
public function getMimeType()
public function getMimeType(): string
{
return 'test/test';
}
+6 -7
View File
@@ -1,30 +1,29 @@
<?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->assertSame($l, strlen($rng->getRandomBytes($l)));
}
$this->assertTrue($rng->isCryptographicallySecure());
} else {
$this->noAssertionsMade();
$this->expectNotToPerformAssertions();
}
}
}
+3 -1
View File
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Tests\Providers\Rng;
use PHPUnit\Framework\TestCase;
@@ -16,7 +18,7 @@ class HashRNGProviderTest extends TestCase
{
$rng = new HashRNGProvider();
foreach ($this->rngTestLengths as $l) {
$this->assertEquals($l, strlen($rng->getRandomBytes($l)));
$this->assertSame($l, strlen($rng->getRandomBytes($l)));
}
$this->assertFalse($rng->isCryptographicallySecure());
+19 -28
View File
@@ -1,61 +1,52 @@
<?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);
$this->assertEquals('ABCDEFGHIJKLMNOP', $tfa->createSecret(80, false));
$tfa = new TwoFactorAuth('Test', 6, 30, Algorithm::Sha1, null, $rng);
$this->assertSame('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);
$this->assertEquals('ABCDEFGHIJKLMNOP', $tfa->createSecret());
$tfa = new TwoFactorAuth('Test', 6, 30, Algorithm::Sha1, null, $rng);
$this->assertSame('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);
$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('Test', 6, 30, Algorithm::Sha1, null, $rng);
$this->assertSame('A', $tfa->createSecret(5));
$this->assertSame('AB', $tfa->createSecret(6));
$this->assertSame('ABCDEFGHIJKLMNOPQRSTUVWXYZ', $tfa->createSecret(128));
$this->assertSame('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', $tfa->createSecret(160));
$this->assertSame('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', $tfa->createSecret(320));
$this->assertSame('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOPQRSTUVWXYZ234567A', $tfa->createSecret(321));
}
}
@@ -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;
@@ -16,7 +18,7 @@ class OpenSSLRNGProviderTest extends TestCase
{
$rng = new OpenSSLRNGProvider(true);
foreach ($this->rngTestLengths as $l) {
$this->assertEquals($l, strlen($rng->getRandomBytes($l)));
$this->assertSame($l, strlen($rng->getRandomBytes($l)));
}
$this->assertTrue($rng->isCryptographicallySecure());
@@ -29,7 +31,7 @@ class OpenSSLRNGProviderTest extends TestCase
{
$rng = new OpenSSLRNGProvider(false);
foreach ($this->rngTestLengths as $l) {
$this->assertEquals($l, strlen($rng->getRandomBytes($l)));
$this->assertSame($l, strlen($rng->getRandomBytes($l)));
}
$this->assertFalse($rng->isCryptographicallySecure());
+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 readonly 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;
}
+82 -129
View File
@@ -1,126 +1,99 @@
<?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;
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));
$this->assertSame('543160', $tfa->getCode('VMR466AB62ZBOKHE', 1426847216));
$this->assertSame('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 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);
$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);
// 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');
@@ -129,10 +102,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 +111,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,23 +126,19 @@ class TwoFactorAuthTest extends TestCase
// Dave Thomas and Andy Hunt -- "Pragmatic Unit Testing
$tfa = new TwoFactorAuth('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.
@@ -183,64 +146,54 @@ 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->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('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('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('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));
}
}
+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->assertSame('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->assertSame('image/png', $data['mimetype']);
$this->assertSame('base64', $data['encoding']);
$this->assertNotEmpty($data['data']);
}
}