diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..aec5c58
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,20 @@
+/.build export-ignore
+/.github export-ignore
+/.idea export-ignore
+/.phan export-ignore
+/.phpdoc export-ignore
+/docs export-ignore
+/examples export-ignore
+/tests export-ignore
+/.editorconfig export-ignore
+/.gitattributes export-ignore
+/.gitignore export-ignore
+/.readthedocs.yml export-ignore
+/phpcs.xml.dist export-ignore
+/phpdoc.xml.dist export-ignore
+/phpmd.xml.dist export-ignore
+/phpunit.xml.dist export-ignore
+/phpstan.dist.neon export-ignore
+/phpstan-baseline.neon export-ignore
+
+*.php diff=php
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1f2499f..e72c0fa 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -4,13 +4,17 @@
on:
push:
branches:
- - main
+ - v4.x
pull_request:
branches:
- - main
+ - v4.x
name: "Continuous Integration"
+env:
+ PHP_EXTENSIONS: curl, json, sodium
+ PHP_INI_VALUES: memory_limit=-1, error_reporting=-1, display_errors=On
+
jobs:
static-code-analysis:
@@ -27,28 +31,32 @@ jobs:
- "8.1"
- "8.2"
- "8.3"
-
- env:
- PHAN_ALLOW_XDEBUG: 0
- PHAN_DISABLE_XDEBUG_WARN: 1
+ - "8.4"
+ - "8.5"
steps:
- name: "Checkout"
- uses: actions/checkout@v3
+ uses: actions/checkout@v5
- name: "Install PHP"
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
- tools: pecl
+ extensions: ${{ env.PHP_EXTENSIONS }}
+ ini-values: ${{ env.PHP_INI_VALUES }}
coverage: none
- extensions: ast, curl, gmp, json, sodium
+
+ - name: "Validate composer.json"
+ run: composer validate --ansi --strict
- name: "Install dependencies with composer"
- uses: ramsey/composer-install@v2
+ uses: ramsey/composer-install@v3
+
+ - name: "Run PHP_CodeSniffer"
+ run: php vendor/bin/phpcs -v
- - name: "Run phan"
- run: php vendor/bin/phan
+# - name: "Run PHPStan"
+# run: php vendor/bin/phpstan
tests:
@@ -68,27 +76,31 @@ jobs:
- "8.1"
- "8.2"
- "8.3"
+ - "8.4"
+ - "8.5"
steps:
- name: "Checkout"
- uses: actions/checkout@v3
+ uses: actions/checkout@v5
- name: "Install PHP with extensions"
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
+ extensions: ${{ env.PHP_EXTENSIONS }}
+ ini-values: ${{ env.PHP_INI_VALUES }}
coverage: pcov
- extensions: curl, gmp, json, sodium
- name: "Install dependencies with composer"
- uses: ramsey/composer-install@v2
+ uses: ramsey/composer-install@v3
- name: "Run tests with phpunit"
run: php vendor/bin/phpunit --colors=always --configuration=phpunit.xml.dist
- name: "Send code coverage report to Codecov.io"
- uses: codecov/codecov-action@v3
+ uses: codecov/codecov-action@v5
with:
+ token: ${{ secrets.CODECOV_TOKEN }}
files: .build/coverage/clover.xml
- name: "Send code coverage report to Codacy"
diff --git a/.gitignore b/.gitignore
index d5a788b..371c827 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,4 +6,5 @@ phpcs.xml
phpdoc.xml
phpmd.xml
phpunit.xml
+phpstan.neon
*.phpunit.result.cache
diff --git a/.phan/config.php b/.phan/config.php
deleted file mode 100644
index 2ad2500..0000000
--- a/.phan/config.php
+++ /dev/null
@@ -1,58 +0,0 @@
- null,
- 'minimum_target_php_version' => '7.4',
-
- // A list of directories that should be parsed for class and
- // method information. After excluding the directories
- // defined in exclude_analysis_directory_list, the remaining
- // files will be statically analyzed for errors.
- //
- // Thus, both first-party and third-party code being used by
- // your application should be included in this list.
- 'directory_list' => [
- 'examples',
- 'src',
- 'tests',
- 'vendor',
- ],
-
- // A regex used to match every file name that you want to
- // exclude from parsing. Actual value will exclude every
- // "test", "tests", "Test" and "Tests" folders found in
- // "vendor/" directory.
- 'exclude_file_regex' => '@^vendor/.*/(tests?|Tests?)/@',
-
- // A directory list that defines files that will be excluded
- // from static analysis, but whose class and method
- // information should be included.
- //
- // Generally, you'll want to include the directories for
- // third-party code (such as "vendor/") in this list.
- //
- // n.b.: If you'd like to parse but not analyze 3rd
- // party code, directories containing that code
- // should be added to both the `directory_list`
- // and `exclude_analysis_directory_list` arrays.
- 'exclude_analysis_directory_list' => [
- 'vendor/',
- ],
- 'suppress_issue_types' => [
- 'PhanAccessMethodInternal',
- 'PhanDeprecatedFunction',
- ],
-];
diff --git a/README.md b/README.md
index 64a661c..bc21b51 100644
--- a/README.md
+++ b/README.md
@@ -8,19 +8,18 @@ A generator for counter based ([RFC 4226](https://tools.ietf.org/html/rfc4226))
[![Codacy][codacy-badge]][codacy]
[license-badge]: https://img.shields.io/github/license/chillerlan/php-authenticator.svg
-[license]: https://github.com/chillerlan/php-authenticator/blob/main/LICENSE
-[gh-action-badge]: https://img.shields.io/github/actions/workflow/status/chillerlan/php-authenticator/ci.yml?branch=main&logo=github
-[gh-action]: https://github.com/chillerlan/php-authenticator/actions?query=branch%3Amain
-[coverage-badge]: https://img.shields.io/codecov/c/gh/chillerlan/php-authenticator/main?logo=codecov
-[coverage]: https://app.codecov.io/github/chillerlan/php-authenticator/tree/main
-[codacy-badge]: https://img.shields.io/codacy/grade/a2793225b448495c9659f27f7f52380a/main?logo=codacy
-[codacy]: https://www.codacy.com/gh/chillerlan/php-authenticator/dashboard?branch=main
+[license]: https://github.com/chillerlan/php-authenticator/blob/v4.x/LICENSE
+[gh-action-badge]: https://img.shields.io/github/actions/workflow/status/chillerlan/php-authenticator/ci.yml?branch=v4.x&logo=github&logoColor=fff
+[gh-action]: https://github.com/chillerlan/php-authenticator/actions?query=branch%3Av4.x
+[coverage-badge]: https://img.shields.io/codecov/c/gh/chillerlan/php-authenticator/v4.x?logo=codecov&logoColor=fff
+[coverage]: https://app.codecov.io/github/chillerlan/php-authenticator/tree/v4.x
+[codacy-badge]: https://img.shields.io/codacy/grade/a2793225b448495c9659f27f7f52380a/v4.x?logo=codacy&logoColor=fff
+[codacy]: https://www.codacy.com/gh/chillerlan/php-authenticator/dashboard?branch=v4.x
# Documentation
## Requirements
- PHP 7.4+
- - [`ext-curl`](https://www.php.net/manual/book.curl) for Battle.net and Steam Guard server time synchronization
- - [`ext-gmp`](https://www.php.net/manual/book.gmp) for Battle.net authenticator secret retrieval (RSA encryption)
+ - [`ext-curl`](https://www.php.net/manual/book.curl) for Steam Guard server time synchronization
- [`ext-sodium`](https://www.php.net/manual/book.sodium) for constant time implementations of base64 encode/decode and hex2bin/bin2hex
([`paragonie/constant_time_encoding`](https://github.com/paragonie/constant_time_encoding) is used as fallback)
@@ -29,15 +28,16 @@ A generator for counter based ([RFC 4226](https://tools.ietf.org/html/rfc4226))
via terminal: `composer require chillerlan/php-authenticator`
-*composer.json* (note: replace `dev-main` with a [version constraint](https://getcomposer.org/doc/articles/versions.md#writing-version-constraints), e.g. `^4.1` - see [releases](https://github.com/chillerlan/php-authenticator/releases) for valid versions)
+**composer.json**
```json
{
"require": {
"php": "^7.4 || ^8.0",
- "chillerlan/php-authenticator": "dev-main"
+ "chillerlan/php-authenticator": "dev-v4.x"
}
}
```
+Note: replace `dev-v4.x` with a [version constraint](https://getcomposer.org/doc/articles/versions.md#writing-version-constraints), e.g. `^4.1` - see [releases](https://github.com/chillerlan/php-authenticator/releases) for valid versions
Profit!
@@ -45,7 +45,7 @@ Profit!
### Create a secret
The secret is usually being created once during the activation process in a user control panel.
So all you need to do there is to display it to the user in a convenient way -
-as a text string and QR code for example - and save it somewhere with the user data.
+as a text string and [QR code](https://github.com/chillerlan/php-qrcode/blob/9964cf8ff1ad90d17c360bd320cf18e16cd59829/examples/authenticator.php) for example - and save it somewhere with the user data.
```php
use chillerlan\Authenticator\{Authenticator, AuthenticatorOptions};
@@ -168,7 +168,6 @@ $options->algorithm = AuthenticatorInterface::ALGO_SHA512;
| `TOTP` | `string` | |
| `HOTP` | `string` | |
| `STEAM_GUARD` | `string` | |
-| `BATTLE_NET` | `string` | |
| `ALGO_SHA1` | `string` | |
| `ALGO_SHA256` | `string` | |
| `ALGO_SHA512` | `string` | |
@@ -177,6 +176,6 @@ $options->algorithm = AuthenticatorInterface::ALGO_SHA512;
-
+
diff --git a/composer.json b/composer.json
index 42d31af..685f005 100644
--- a/composer.json
+++ b/composer.json
@@ -12,6 +12,16 @@
"name": "Smiley",
"email": "smiley@chillerlan.net",
"homepage": "https://github.com/codemasher"
+ },
+ {
+ "name": "Contributors",
+ "homepage":"https://github.com/chillerlan/php-authenticator/graphs/contributors"
+ }
+ ],
+ "funding": [
+ {
+ "type": "Ko-Fi",
+ "url": "https://ko-fi.com/codemasher"
}
],
"support": {
@@ -22,39 +32,45 @@
"prefer-stable": true,
"require": {
"php": "^7.4 || ^8.0",
- "chillerlan/php-settings-container": "^2.1.4 || ^3.0",
+ "chillerlan/php-settings-container": "^2.1.6 || ^3.2.1",
"paragonie/constant_time_encoding": "^2.6"
},
"require-dev": {
"ext-curl": "*",
- "ext-gmp": "*",
"ext-json": "*",
"ext-sodium": "*",
- "phan/phan": "^5.4",
- "phpmd/phpmd": "^2.13",
+ "phpmd/phpmd": "^2.15",
+ "phpstan/phpstan": "^2.1.27",
+ "phpstan/phpstan-deprecation-rules": "^2.0.3",
"phpunit/phpunit": "^9.6",
- "squizlabs/php_codesniffer": "^3.7"
+ "slevomat/coding-standard": "^8.23",
+ "squizlabs/php_codesniffer": "^4.0"
},
"suggest": {
"chillerlan/php-qrcode": "Create QR Codes for use with an authenticator app."
},
"autoload": {
"psr-4": {
- "chillerlan\\Authenticator\\": "src/"
+ "chillerlan\\Authenticator\\": "src"
}
},
"autoload-dev": {
"psr-4": {
- "chillerlan\\AuthenticatorTest\\": "tests/"
+ "chillerlan\\AuthenticatorTest\\": "tests"
}
},
"scripts": {
+ "phpcs": "@php vendor/bin/phpcs",
"phpunit": "@php vendor/bin/phpunit",
- "phan": "@php vendor/bin/phan"
+ "phpstan": "@php vendor/bin/phpstan",
+ "phpstan-baseline": "@php vendor/bin/phpstan --generate-baseline"
},
"config": {
"lock": false,
"sort-packages": true,
- "platform-check": true
+ "platform-check": true,
+ "allow-plugins": {
+ "dealerdirect/phpcodesniffer-composer-installer": true
+ }
}
}
diff --git a/examples/battlenet.php b/examples/battlenet.php
deleted file mode 100644
index 9620d57..0000000
--- a/examples/battlenet.php
+++ /dev/null
@@ -1,46 +0,0 @@
-
- * @copyright 2023 Smiley
- * @license MIT
- */
-
-use chillerlan\Authenticator\{Authenticator, AuthenticatorOptions, Authenticators\BattleNet};
-use chillerlan\Authenticator\Authenticators\AuthenticatorInterface;
-
-require_once '../vendor/autoload.php';
-
-$options = new AuthenticatorOptions([
- // switch mode to BATTLE_NET
- 'mode' => AuthenticatorInterface::BATTLE_NET,
-]);
-
-$auth = new Authenticator($options);
-
-// set a secret - Battle.net secrets come as hex strings (20 byte, 40 chars)
-$secret = $auth->setSecret('3132333435363738393031323334353637383930');
-// get a one time code
-$code = $auth->code();
-var_dump($code);
-// verify the current code
-var_dump($auth->verify($code)); // -> true
-// previous code
-var_dump($auth->verify($code, time() - $options->period)); // -> true
-// 2nd adjacent is invalid
-var_dump($auth->verify($code, time() + 2 * $options->period)); // -> false
-// allow 2 adjacent codes
-$options->adjacent = 2;
-var_dump($auth->verify($code, time() + 2 * $options->period)); // -> true
-
-// request a new authenticator from the Battle.net API
-// this requires the BattleNet class to be invoked directly as we're using non-interface methods for this
-$auth = new BattleNet;
-$data = $auth->createAuthenticator('EU');
-// the serial can be used to attach this authenticator to an existing Battle.net account
-var_dump($data);
-// it's also possible to retreive an authenticator secret from an existing serial and restore code, e.g. from WinAuth
-$data = $auth->restoreSecret($data['serial'], $data['restore_code']);
-var_dump($data);
diff --git a/examples/hotp.php b/examples/hotp.php
index 660bcc0..b445005 100644
--- a/examples/hotp.php
+++ b/examples/hotp.php
@@ -7,6 +7,7 @@
* @copyright 2017 Smiley
* @license MIT
*/
+declare(strict_types=1);
use chillerlan\Authenticator\{Authenticator, AuthenticatorOptions};
use chillerlan\Authenticator\Authenticators\AuthenticatorInterface;
diff --git a/examples/steam.php b/examples/steam.php
index 218e8f2..18e02cf 100644
--- a/examples/steam.php
+++ b/examples/steam.php
@@ -7,6 +7,7 @@
* @copyright 2017 Smiley
* @license MIT
*/
+declare(strict_types=1);
use chillerlan\Authenticator\{Authenticator, AuthenticatorOptions};
use chillerlan\Authenticator\Authenticators\AuthenticatorInterface;
@@ -29,9 +30,9 @@
// verify the current code
var_dump($auth->verify($code)); // -> true
// previous code
-var_dump($auth->verify($code, time() - $options->period)); // -> true
+var_dump($auth->verify($code, (time() - $options->period))); // -> true
// 2nd adjacent is invalid
-var_dump($auth->verify($code, time() + 2 * $options->period)); // -> false
+var_dump($auth->verify($code, (time() + 2 * $options->period))); // -> false
// allow 2 adjacent codes
$options->adjacent = 2;
-var_dump($auth->verify($code, time() + 2 * $options->period)); // -> true
+var_dump($auth->verify($code, (time() + 2 * $options->period))); // -> true
diff --git a/examples/totp.php b/examples/totp.php
index 706f368..92e16f7 100644
--- a/examples/totp.php
+++ b/examples/totp.php
@@ -7,6 +7,7 @@
* @copyright 2017 Smiley
* @license MIT
*/
+declare(strict_types=1);
use chillerlan\Authenticator\{Authenticator, AuthenticatorOptions};
use chillerlan\Authenticator\Authenticators\AuthenticatorInterface;
@@ -38,12 +39,12 @@
// verify the code
var_dump($auth->verify($code)); // -> true
// verify against the previous time slice
-var_dump($auth->verify($code, time() - $options->period)); // -> true
+var_dump($auth->verify($code, (time() - $options->period))); // -> true
// 2 steps ahead (1 is default)
-var_dump($auth->verify($code, time() + 2 * $options->period)); // -> false
+var_dump($auth->verify($code, (time() + 2 * $options->period))); // -> false
// set adjacent codes to 2 and try again
$options->adjacent = 2;
-var_dump($auth->verify($code, time() + 2 * $options->period)); // -> true
+var_dump($auth->verify($code, (time() + 2 * $options->period))); // -> true
// create an URI for use in e.g. QR codes
// -> otpauth://totp/test?secret=JQUZJ44H6M3SATXIJRKTK64VQMIU73JN&issuer=example.com&digits=8&algorithm=SHA512&period=60
diff --git a/phpcs.xml.dist b/phpcs.xml.dist
index 4e541a4..a69a504 100644
--- a/phpcs.xml.dist
+++ b/phpcs.xml.dist
@@ -1,43 +1,224 @@
-
- php-authenticator rules for phpcs
+ chillerlan rules for phpcs
examples
src
tests
+
+
+
+
+
+
+
error
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ examples
+
+
+
+
+
+
+ examples
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ examples
+
+ examples
+ benchmark
+
+
+
+
+
@@ -46,10 +227,23 @@
-
+
+ examples
+
+
+
+
+
+
+
+
+
+
+
+
@@ -65,124 +259,62 @@
-
+
-
+
-
+
-
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
- error
-
-
-
- error
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- examples
-
-
-
- examples
+
+
+
+
-
- examples
+
+
+
+
-
- examples
+
+
+
+
-
- examples
+
+
+
+
diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
new file mode 100644
index 0000000..646ebc3
--- /dev/null
+++ b/phpstan-baseline.neon
@@ -0,0 +1,67 @@
+parameters:
+ ignoreErrors:
+ -
+ rawMessage: '''
+ Call to deprecated method getUri() of class chillerlan\Authenticator\Authenticator:
+ 5.3.0 The parameter `$omitSettings` will be removed in favor of `AuthenticatorOptions::$omitUriSettings`
+ in the next major version (6.x)
+ '''
+ identifier: method.deprecated
+ count: 2
+ path: examples/hotp.php
+
+ -
+ rawMessage: '''
+ Call to deprecated method getUri() of class chillerlan\Authenticator\Authenticator:
+ 5.3.0 The parameter `$omitSettings` will be removed in favor of `AuthenticatorOptions::$omitUriSettings`
+ in the next major version (6.x)
+ '''
+ identifier: method.deprecated
+ count: 2
+ path: examples/totp.php
+
+ -
+ rawMessage: Cannot access offset 'server_time' on mixed.
+ identifier: offsetAccess.nonOffsetAccessible
+ count: 1
+ path: src/Authenticators/SteamGuard.php
+
+ -
+ rawMessage: Cannot cast mixed to int.
+ identifier: cast.int
+ count: 1
+ path: src/Authenticators/SteamGuard.php
+
+ -
+ rawMessage: 'Parameter #1 $json of function json_decode expects string, string|true given.'
+ identifier: argument.type
+ count: 1
+ path: src/Authenticators/SteamGuard.php
+
+ -
+ rawMessage: 'Parameter #1 $counter of method chillerlan\Authenticator\Authenticators\HOTP::getHMAC() expects int, float|int given.'
+ identifier: argument.type
+ count: 1
+ path: src/Authenticators/TOTP.php
+
+ -
+ rawMessage: '''
+ Call to deprecated method getUri() of class chillerlan\Authenticator\Authenticator:
+ 5.3.0 The parameter `$omitSettings` will be removed in favor of `AuthenticatorOptions::$omitUriSettings`
+ in the next major version (6.x)
+ '''
+ identifier: method.deprecated
+ count: 7
+ path: tests/AuthenticatorTest.php
+
+ -
+ rawMessage: 'Call to function is_int() with 59|1111111109|1111111111|1234567890|2000000000|20000000000 will always evaluate to true.'
+ identifier: function.alreadyNarrowedType
+ count: 1
+ path: tests/Authenticators/SteamGuardTest.php
+
+ -
+ rawMessage: 'Call to function is_int() with 59|1111111109|1111111111|1234567890|2000000000|20000000000 will always evaluate to true.'
+ identifier: function.alreadyNarrowedType
+ count: 1
+ path: tests/Authenticators/TOTPTest.php
diff --git a/phpstan.dist.neon b/phpstan.dist.neon
new file mode 100644
index 0000000..06bbc19
--- /dev/null
+++ b/phpstan.dist.neon
@@ -0,0 +1,17 @@
+# https://phpstan.org/config-reference
+
+parameters:
+ level: 9
+ tmpDir: .build/phpstan-cache
+ paths:
+ - examples
+ - src
+ - tests
+
+ treatPhpDocTypesAsCertain: false
+
+includes:
+ - phpstan-baseline.neon
+ - vendor/phpstan/phpstan/conf/bleedingEdge.neon
+ - vendor/phpstan/phpstan-deprecation-rules/rules.neon
+ - vendor/chillerlan/php-settings-container/rules-magic-access.neon
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 4038465..d53aeab 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -1,6 +1,6 @@
- ./tests
+ tests
- ./src
+ src
diff --git a/src/Authenticator.php b/src/Authenticator.php
index bacd811..7008fdf 100644
--- a/src/Authenticator.php
+++ b/src/Authenticator.php
@@ -7,17 +7,12 @@
* @copyright 2015 Smiley
* @license MIT
*/
+declare(strict_types=1);
namespace chillerlan\Authenticator;
use chillerlan\Authenticator\Authenticators\AuthenticatorInterface;
use chillerlan\Settings\SettingsContainerInterface;
-use InvalidArgumentException;
-use function http_build_query;
-use function rawurlencode;
-use function sprintf;
-use function trim;
-use const PHP_QUERY_RFC3986;
/**
* Yet another Google authenticator implementation!
@@ -33,12 +28,11 @@ class Authenticator{
/** @var \chillerlan\Settings\SettingsContainerInterface|\chillerlan\Authenticator\AuthenticatorOptions */
protected SettingsContainerInterface $options;
protected AuthenticatorInterface $authenticator;
- protected string $mode = AuthenticatorInterface::TOTP;
/**
* Authenticator constructor
*/
- public function __construct(SettingsContainerInterface $options = null, string $secret = null){
+ public function __construct(?SettingsContainerInterface $options = null, ?string $secret = null){
// phpcs:ignore
$this->setOptions($options ?? new AuthenticatorOptions);
@@ -58,10 +52,9 @@ public function setOptions(SettingsContainerInterface $options):self{
$this->options = $options;
// invoke a new authenticator interface if necessary
- if(!isset($this->authenticator) || $this->options->mode !== $this->mode){
+ if(!isset($this->authenticator) || $this->authenticator::MODE !== $this->options->mode){
$class = AuthenticatorInterface::MODES[$this->options->mode];
- $this->mode = $this->options->mode;
- $this->authenticator = new $class;
+ $this->authenticator = new $class($this->options);
}
$this->authenticator->setOptions($this->options);
@@ -94,7 +87,7 @@ public function getSecret():string{
*
* @codeCoverageIgnore
*/
- public function createSecret(int $length = null):string{
+ public function createSecret(?int $length = null):string{
return $this->authenticator->createSecret($length);
}
@@ -107,7 +100,7 @@ public function createSecret(int $length = null):string{
*
* @codeCoverageIgnore
*/
- public function code(int $data = null):string{
+ public function code(?int $data = null):string{
return $this->authenticator->code($data);
}
@@ -120,7 +113,7 @@ public function code(int $data = null):string{
*
* @codeCoverageIgnore
*/
- public function verify(string $otp, int $data = null):bool{
+ public function verify(string $otp, ?int $data = null):bool{
return $this->authenticator->verify($otp, $data);
}
@@ -129,37 +122,19 @@ public function verify(string $otp, int $data = null):bool{
*
* @link https://github.com/google/google-authenticator/wiki/Key-Uri-Format#parameters
*
- * @throws \InvalidArgumentException
+ * @deprecated 5.3.0 The parameter `$omitSettings` will be removed in favor of `AuthenticatorOptions::$omitUriSettings`
+ * in the next major version (6.x)
+ * @see \chillerlan\Authenticator\AuthenticatorOptionsTrait::$omitUriSettings
+ *
+ * @codeCoverageIgnore
*/
- public function getUri(string $label, string $issuer, int $hotpCounter = null, bool $omitSettings = null):string{
- $label = trim($label);
- $issuer = trim($issuer);
-
- if(empty($label) || empty($issuer)){
- throw new InvalidArgumentException('$label and $issuer cannot be empty');
+ public function getUri(string $label, string $issuer, ?int $hotpCounter = null, ?bool $omitSettings = null):string{
+ // a little reckless but good enough until the deprecated parameter is removed
+ if($omitSettings !== null){
+ $this->options->omitUriSettings = $omitSettings;
}
- $values = [
- 'secret' => $this->authenticator->getSecret(),
- 'issuer' => $issuer,
- ];
-
- if($omitSettings !== true){
- $values['digits'] = $this->options->digits;
- $values['algorithm'] = $this->options->algorithm;
-
- if($this->mode === AuthenticatorInterface::TOTP){
- $values['period'] = $this->options->period;
- }
-
- if($this->mode === AuthenticatorInterface::HOTP && $hotpCounter !== null){
- $values['counter'] = $hotpCounter;
- }
- }
-
- $values = http_build_query($values, '', '&', PHP_QUERY_RFC3986);
-
- return sprintf('otpauth://%s/%s?%s', $this->mode, rawurlencode($label), $values);
+ return $this->authenticator->getUri($label, $issuer, $hotpCounter);
}
}
diff --git a/src/AuthenticatorOptions.php b/src/AuthenticatorOptions.php
index 088a80a..d31e07c 100644
--- a/src/AuthenticatorOptions.php
+++ b/src/AuthenticatorOptions.php
@@ -7,14 +7,12 @@
* @copyright 2019 smiley
* @license MIT
*/
+declare(strict_types=1);
namespace chillerlan\Authenticator;
use chillerlan\Settings\SettingsContainerAbstract;
-/**
- *
- */
class AuthenticatorOptions extends SettingsContainerAbstract{
use AuthenticatorOptionsTrait;
}
diff --git a/src/AuthenticatorOptionsTrait.php b/src/AuthenticatorOptionsTrait.php
index b3e75a6..8477e11 100644
--- a/src/AuthenticatorOptionsTrait.php
+++ b/src/AuthenticatorOptionsTrait.php
@@ -7,6 +7,7 @@
* @copyright 2019 smiley
* @license MIT
*/
+declare(strict_types=1);
namespace chillerlan\Authenticator;
@@ -16,6 +17,18 @@
use function strtolower;
use function strtoupper;
+/**
+ * @property int $digits
+ * @property int $period
+ * @property int $secret_length
+ * @property string $algorithm
+ * @property string $mode
+ * @property int $adjacent
+ * @property int $time_offset
+ * @property bool $useLocalTime
+ * @property bool $forceTimeRefresh
+ * @property bool $omitUriSettings
+ */
trait AuthenticatorOptionsTrait{
/**
@@ -79,6 +92,13 @@ trait AuthenticatorOptionsTrait{
*/
protected bool $forceTimeRefresh = false;
+ /**
+ * Whether to omit the additional settings in the URI for an authenticator app (algo, digits, period)
+ *
+ * @link https://github.com/google/google-authenticator/wiki/Key-Uri-Format#parameters
+ */
+ protected bool $omitUriSettings = false;
+
/**
* Sets the code length to either 6 or 8
*
@@ -143,9 +163,9 @@ protected function set_mode(string $mode):void{
* @throws \InvalidArgumentException
*/
protected function set_adjacent(int $adjacent):void{
-
- if($adjacent < 0){
- throw new InvalidArgumentException('Invalid adjacent: '.$adjacent);
+ // limit to a sane amount
+ if($adjacent < 0 || $adjacent > 10){
+ throw new InvalidArgumentException('Invalid number of adjacent codes: '.$adjacent);
}
$this->adjacent = $adjacent;
@@ -155,7 +175,7 @@ protected function set_adjacent(int $adjacent):void{
* @throws \InvalidArgumentException
*/
protected function set_secret_length(int $secret_length):void{
- // ~ 80 to 640 bits
+
if($secret_length < 16 || $secret_length > 1024){
throw new InvalidArgumentException('Invalid secret length: '.$secret_length);
}
diff --git a/src/Authenticators/AuthenticatorAbstract.php b/src/Authenticators/AuthenticatorAbstract.php
index d311a71..12fcce9 100644
--- a/src/Authenticators/AuthenticatorAbstract.php
+++ b/src/Authenticators/AuthenticatorAbstract.php
@@ -7,6 +7,7 @@
* @copyright 2023 smiley
* @license MIT
*/
+declare(strict_types=1);
namespace chillerlan\Authenticator\Authenticators;
@@ -15,13 +16,14 @@
use chillerlan\Settings\SettingsContainerInterface;
use InvalidArgumentException;
use RuntimeException;
+use function http_build_query;
use function random_bytes;
+use function rawurlencode;
+use function sprintf;
use function time;
use function trim;
+use const PHP_QUERY_RFC3986;
-/**
- *
- */
abstract class AuthenticatorAbstract implements AuthenticatorInterface{
protected const userAgent = 'chillerlanAuthenticator/5.0 +https://github.com/chillerlan/php-authenticator';
@@ -35,32 +37,23 @@ abstract class AuthenticatorAbstract implements AuthenticatorInterface{
/**
* AuthenticatorInterface constructor
*/
- public function __construct(SettingsContainerInterface $options = null){
+ public function __construct(?SettingsContainerInterface $options = null){
// phpcs:ignore
$this->setOptions($options ?? new AuthenticatorOptions);
}
- /**
- * @inheritDoc
- */
public function setOptions(SettingsContainerInterface $options):AuthenticatorInterface{
$this->options = $options;
return $this;
}
- /**
- * @inheritDoc
- */
public function setSecret(string $encodedSecret):AuthenticatorInterface{
$this->secret = Base32::decode($this->checkEncodedSecret($encodedSecret));
return $this;
}
- /**
- * @inheritDoc
- */
public function getSecret():string{
if($this->secret === null){
@@ -70,10 +63,7 @@ public function getSecret():string{
return Base32::encode($this->secret);
}
- /**
- * @inheritDoc
- */
- public function createSecret(int $length = null):string{
+ public function createSecret(?int $length = null):string{
$length ??= $this->options->secret_length;
if($length < 16){
@@ -85,9 +75,6 @@ public function createSecret(int $length = null):string{
return $this->getSecret();
}
- /**
- * @inheritDoc
- */
public function getServertime():int{
return time();
}
@@ -116,4 +103,27 @@ protected function checkEncodedSecret(string $encodedSecret):string{
return $encodedSecret;
}
+ /**
+ * Returns an array with settings for a mobile authenticator URI for the current authenticator mode/instance
+ *
+ * @return array
+ */
+ abstract protected function getUriParams(string $issuer, ?int $counter = null):array;
+
+ public function getUri(string $label, string $issuer, ?int $counter = null):string{
+ $label = trim($label);
+ $issuer = trim($issuer);
+
+ if($label === '' || $issuer === ''){
+ throw new InvalidArgumentException('$label and $issuer cannot be empty');
+ }
+
+ return sprintf(
+ 'otpauth://%s/%s?%s',
+ $this::MODE,
+ rawurlencode($label),
+ http_build_query($this->getUriParams($issuer, $counter), '', '&', PHP_QUERY_RFC3986),
+ );
+ }
+
}
diff --git a/src/Authenticators/AuthenticatorInterface.php b/src/Authenticators/AuthenticatorInterface.php
index 4bbdd00..aa8645d 100644
--- a/src/Authenticators/AuthenticatorInterface.php
+++ b/src/Authenticators/AuthenticatorInterface.php
@@ -7,20 +7,17 @@
* @copyright 2023 smiley
* @license MIT
*/
+declare(strict_types=1);
namespace chillerlan\Authenticator\Authenticators;
use chillerlan\Settings\SettingsContainerInterface;
-/**
- *
- */
interface AuthenticatorInterface{
public const TOTP = 'totp';
public const HOTP = 'hotp';
public const STEAM_GUARD = 'steam';
- public const BATTLE_NET = 'battlenet';
public const ALGO_SHA1 = 'SHA1';
public const ALGO_SHA256 = 'SHA256';
@@ -30,7 +27,6 @@ interface AuthenticatorInterface{
self::HOTP => HOTP::class,
self::TOTP => TOTP::class,
self::STEAM_GUARD => SteamGuard::class,
- self::BATTLE_NET => BattleNet::class,
];
public const HASH_ALGOS = [
@@ -39,6 +35,13 @@ interface AuthenticatorInterface{
self::ALGO_SHA512,
];
+ /**
+ * Mode identifier. Do not call this constant from the interface. but rather from an authenticator instance.
+ *
+ * @var string
+ */
+ public const MODE = '';
+
/**
* Sets the options
*/
@@ -63,7 +66,7 @@ public function getSecret():string;
*
* @throws \InvalidArgumentException
*/
- public function createSecret(int $length = null):string;
+ public function createSecret(?int $length = null):string;
/**
* Returns the current server time as UNIX timestamp for the given application (or `time()` if not applicable)
@@ -75,7 +78,7 @@ public function getServertime():int;
*
* @internal
*/
- public function getCounter(int $data = null):int;
+ public function getCounter(?int $data = null):int;
/**
* HMAC hashes the given $data integer with the given secret
@@ -105,7 +108,7 @@ public function getOTP(int $code):string;
* - a UNIX timestamp (TOTP)
* - a counter value (HOTP)
*/
- public function code(int $data = null):string;
+ public function code(?int $data = null):string;
/**
* Checks the given $code against the secret
@@ -114,6 +117,15 @@ public function code(int $data = null):string;
* - a UNIX timestamp (TOTP)
* - a counter value (HOTP)
*/
- public function verify(string $otp, int $data = null):bool;
+ public function verify(string $otp, ?int $data = null):bool;
+
+ /**
+ * Creates a URI string for a mobile authenticator (otpauth://...)
+ *
+ * The parameter `$counter` only has effect in HOTP mode
+ *
+ * @link https://github.com/google/google-authenticator/wiki/Key-Uri-Format#parameters
+ */
+ public function getUri(string $label, string $issuer, ?int $counter = null):string;
}
diff --git a/src/Authenticators/BattleNet.php b/src/Authenticators/BattleNet.php
deleted file mode 100644
index 4afd89c..0000000
--- a/src/Authenticators/BattleNet.php
+++ /dev/null
@@ -1,425 +0,0 @@
-
- * @copyright 2023 smiley
- * @license MIT
- *
- * @noinspection PhpComposerExtensionStubsInspection
- */
-
-namespace chillerlan\Authenticator\Authenticators;
-
-use chillerlan\Authenticator\Common\Hex;
-use InvalidArgumentException;
-use RuntimeException;
-use function array_reverse;
-use function array_unshift;
-use function curl_close;
-use function curl_exec;
-use function curl_getinfo;
-use function curl_init;
-use function curl_setopt_array;
-use function floor;
-use function gmp_cmp;
-use function gmp_div;
-use function gmp_import;
-use function gmp_init;
-use function gmp_intval;
-use function gmp_mod;
-use function gmp_powm;
-use function hash_hmac;
-use function hexdec;
-use function implode;
-use function in_array;
-use function pack;
-use function preg_match;
-use function random_bytes;
-use function sha1;
-use function sprintf;
-use function str_pad;
-use function str_replace;
-use function str_split;
-use function strlen;
-use function strtoupper;
-use function substr;
-use function time;
-use function trim;
-use function unpack;
-use const CURLOPT_HTTP_VERSION;
-use const CURLOPT_HTTPHEADER;
-use const CURLOPT_POST;
-use const CURLOPT_POSTFIELDS;
-use const CURLOPT_RETURNTRANSFER;
-use const STR_PAD_LEFT;
-
-/**
- * @see https://github.com/winauth/winauth/blob/master/Authenticator/BattleNetAuthenticator.cs
- * @see https://github.com/krtek4/php-bma
- */
-final class BattleNet extends TOTP{
-
- /**
- * @var array
- */
- private const regions = ['EU', 'KR', 'US']; // 'CN',
-
- /**
- * HTTPS requests with HTTP version 1.1 only!
- *
- * @var array
- */
- private const servers = [
-# 'CN' => 'https://mobile-service.battlenet.com.cn', // ???
- 'EU' => 'https://eu.mobile-service.blizzard.com',
- 'KR' => 'https://kr.mobile-service.blizzard.com',
- 'US' => 'https://us.mobile-service.blizzard.com',
- ];
-
- /**
- * @var array
- */
- private const endpoints = [
- 'public_key' => '/enrollment/initiatePaperRestore.htm',
- 'validate' => '/enrollment/validatePaperRestore.htm',
- 'create' => '/enrollment/enroll.htm',
- 'servertime' => '/enrollment/time.htm',
- ];
-
- private const rsa_exp_base10 = '257';
- private const rsa_mod_base10 = '1048900188079865568740077109142054431570301596680341971861256789'.
- '6028747089429083053061828494311840511089632283544909943323209315'.
- '1168250152146023319326491587651685252774820340995950744075665455'.
- '6817606521365764930287339148921667008991098362911808810630974611'.
- '75643998356321993663868233366705340758102567742483097';
-
-# private const rsa_exp_base16 = '0101';
-# private const rsa_mod_base16 = '955e4bd989f3917d2f15544a7e0504eb9d7bb66b6f8a2fe470e453c779200e5e'.
-# '3ad2e43a02d06c4adbd8d328f1a426b83658e88bfd949b2af4eaf30054673a14'.
-# '19a250fa4cc1278d12855b5b25818d162c6e6ee2ab4a350d401d78f6ddb99711'.
-# 'e72626b48bd8b5b0b7f3acf9ea3c9e0005fee59e19136cdb7c83f2ab8b0a2a99';
-
- private array $curlInfo = [];
-
- /**
- * @inheritDoc
- */
- public function setSecret(string $encodedSecret):AuthenticatorInterface{
- $this->secret = Hex::decode($this->checkEncodedSecret($encodedSecret));
-
- return $this;
- }
-
- /**
- * @inheritDoc
- */
- public function getSecret():string{
-
- if($this->secret === null){
- throw new RuntimeException('No secret set');
- }
-
- return Hex::encode($this->secret);
- }
-
- /**
- * @inheritDoc
- * @codeCoverageIgnore
- */
- public function createSecret(int $length = null):string{
- throw new RuntimeException('Not implemented');
- }
-
- /**
- * @inheritDoc
- */
- public function getCounter(int $data = null):int{
- // the period is fixed to 30 seconds for Battle.net
- $this->options->period = 30;
-
- return parent::getCounter($data);
- }
-
- /**
- * @inheritDoc
- */
- public function getHMAC(int $counter):string{
- // algorithm is fixed to sha1 for Battle.net
- $this->options->algorithm = self::ALGO_SHA1;
-
- return parent::getHMAC($counter);
- }
-
- /**
- * @inheritDoc
- */
- public function getOTP(int $code):string{
- $code %= 100000000;
-
- // length is fixed to 8 for Battle.net
- return str_pad((string)$code, 8, '0', STR_PAD_LEFT);
- }
-
- /**
- * @inheritDoc
- */
- public function getServerTime():int{
-
- if($this->options->forceTimeRefresh === false && $this->serverTime !== 0){
- return $this->getAdjustedTime($this->serverTime, $this->lastRequestTime);
- }
-
- $servertime = $this->request('servertime', 'US');
-
- $this->setServertime($servertime);
-
- return $this->getAdjustedTime($this->serverTime, $this->lastRequestTime);
- }
-
- /**
- * Retrieves the secret from Battle.net using the given serial and restore code.
- * If the public key for the serial is given (from a previous retrieval), it saves a server request.
- */
- public function restoreSecret(string $serial, string $restore_code, string $public_key = null):array{
- $serial = $this->cleanSerial($serial);
- $region = $this->getRegion($serial);
-
- // fetch public key if none is given
- $pubkey = ($public_key !== null)
- ? Hex::decode($public_key)
- : $this->request('public_key', $region, $serial);
-
- // create HMAC hash from serial and restore code
- $hmac_key = $this->convertRestoreCodeToByte($restore_code);
- $hmac = hash_hmac('sha1', $serial.$pubkey, $hmac_key, true);
- // encrypt and send validation request
- $nonce = random_bytes(20);
- $encrypted_secret = $this->request('validate', $region, $serial.$this->encrypt($hmac.$nonce));
- $secret = $this->decrypt($encrypted_secret, $nonce);
-
- return [
- 'region' => $region,
- 'serial' => $this->formatSerial($serial),
- 'restore_code' => $restore_code,
- 'public_key' => Hex::encode($pubkey),
- 'secret' => Hex::encode($secret),
- ];
- }
-
- /**
- * Creates a new authenticator that can be linked to an existing Battle.net account
- */
- public function createAuthenticator(string $region, string $device = null):array{
- $region = $this->getRegion($region);
- $device = str_pad(($device ?? 'BlackBerry Pearl'), 16, "\x00");
- $nonce = random_bytes(37);
- $response = $this->request('create', $region, $this->encrypt("\x01".$nonce.$region.$device));
- // timestamp, first 8 bytes of the response
- $this->setServertime(substr($response, 0, 8));
- // decrypt rest of the response (37 bytes)
- $data = $this->decrypt(substr($response, 8), $nonce);
- // secret, first 20 bytes
- $secret = substr($data, 0, 20);
- // serial, last 17 bytes
- $serial = $this->cleanSerial(substr($data, 20));
- // the restore code is taken from the last 10 bytes of a SHA1 hashed serial and (binary) secret
- $restore_code = substr(sha1($serial.$secret, true), -10);
-
- // feed the result into the restore function to verify the restore code and fetch the public key
- return $this->restoreSecret($serial, $this->convertRestoreCodeToChar($restore_code));
- }
-
- /**
- *
- */
- private function setServertime(string $encodedTimestamp):void{
- $this->serverTime = (int)floor(hexdec(Hex::encode($encodedTimestamp)) / 1000);
- $this->lastRequestTime = (time() - (int)floor($this->curlInfo['total_time']));
- }
-
- /**
- * @throws \RuntimeException
- */
- private function getRegion(string $serial):string{
- $region = substr(strtoupper($serial), 0, 2);
-
- if(!in_array($region, self::regions)){
- throw new RuntimeException('invalid region in serial number detected');
- }
-
- return $region;
- }
-
- /**
- * cleans the given serial in (EU-1111-2222-3333) and strips hyphens (EU111122223333) for use in API requests
- *
- * @throws \InvalidArgumentException
- */
- private function cleanSerial(string $serial):string{
- $serial = str_replace('-', '', strtoupper(trim($serial)));
-
- if(!preg_match('/^[CNEUSKR]{2}\d{12}$/', $serial)){
- throw new InvalidArgumentException('invalid serial');
- }
-
- return $serial;
- }
-
- /**
- *
- */
- private function formatSerial(string $serial):string{
- $serial = $this->cleanSerial($serial);
- // split the numeric part into 3x 4 numbers
- $blocks = str_split(substr($serial, 2), 4);
- // prepend the region
- array_unshift($blocks, substr($serial, 0, 2));
-
- return implode('-', $blocks);
- }
-
- /**
- * @throws \RuntimeException
- */
- private function request(string $endpoint, string $region, string $data = null):string{
-
- $options = [
- CURLOPT_RETURNTRANSFER => true,
- CURLOPT_HTTP_VERSION => '1.1', // we need to force http 1.1, h2 will return a HTTP/600 error (???) from Battle.net
- CURLOPT_HTTPHEADER => [sprintf('User-Agent: %s', $this::userAgent)],
- ];
-
- if($data !== null){
- $options[CURLOPT_POST] = true;
- $options[CURLOPT_POSTFIELDS] = $data;
- $options[CURLOPT_HTTPHEADER][] = 'Content-Type: application/octet-stream';
- }
-
- $ch = curl_init(self::servers[$region].self::endpoints[$endpoint]);
-
- curl_setopt_array($ch, $options);
-
- $response = curl_exec($ch);
- $this->curlInfo = curl_getinfo($ch);
-
- curl_close($ch);
-
- if($this->curlInfo['http_code'] !== 200){
- // I'm not going to investigate the error further as this shouldn't happen usually
- throw new RuntimeException(sprintf('Battle.net API request error: HTTP/%s', $this->curlInfo['http_code'])); // @codeCoverageIgnore
- }
-
- return $response;
- }
-
- /**
- * Convert restore code char to byte but with appropriate mapping to exclude I,L,O and S.
- * e.g. A=10 but J=18 not 19 (as I is missing)
- */
- private function convertRestoreCodeToByte(string $restore_code):string{
- $chars = unpack('C*', $restore_code);
-
- foreach($chars as &$c){
- if($c > 47 && $c < 58){
- $c -= 48;
- }
- else{
- // S
- if($c > 82){
- $c--;
- }
- // O
- if($c > 78){
- $c--;
- }
- // L
- if($c > 75){
- $c--;
- }
- // I
- if($c > 72){
- $c--;
- }
-
- $c -= 55;
- }
-
- }
-
- return pack('C*', ...$chars);
- }
-
- /**
- * Convert restore code byte to char but with appropriate mapping to exclude I,L,O and S.
- */
- private function convertRestoreCodeToChar(string $data):string{
- $chars = unpack('C*', $data);
-
- foreach($chars as &$c){
- $c &= 0x1F;
-
- if($c < 10){
- $c += 48;
- }
- else{
- $c += 55;
- // I
- if($c > 72){
- $c++;
- }
- // L
- if($c > 75){
- $c++;
- }
- // O
- if($c > 78){
- $c++;
- }
- // S
- if($c > 82){
- $c++;
- }
- }
- }
-
- return pack('C*', ...$chars);
- }
-
- /**
- *
- */
- private function encrypt(string $data):string{
- $num = gmp_powm(gmp_import($data), self::rsa_exp_base10, self::rsa_mod_base10); // gmp_init(self::rsa_mod_base16, 16)
- $zero = gmp_init('0', 10);
- $ret = [];
-
- while(gmp_cmp($num, $zero) > 0){
- $ret[] = gmp_intval(gmp_mod($num, 256));
- $num = gmp_div($num, 256);
- }
-
- return pack('C*', ...array_reverse($ret));
- }
-
- /**
- * @throws \RuntimeException
- */
- private function decrypt(string $data, string $key):string{
-
- if(strlen($data) !== strlen($key)){
- throw new RuntimeException('The decryption key size and data size doesn\'t match');
- }
-
- $data = unpack('C*', $data);
- $key = unpack('C*', $key);
-
- foreach($data as $i => &$c){
- $c ^= $key[$i];
- }
-
- return pack('C*', ...$data);
- }
-
-}
diff --git a/src/Authenticators/HOTP.php b/src/Authenticators/HOTP.php
index 82da335..4252932 100644
--- a/src/Authenticators/HOTP.php
+++ b/src/Authenticators/HOTP.php
@@ -7,10 +7,12 @@
* @copyright 2023 smiley
* @license MIT
*/
+declare(strict_types=1);
namespace chillerlan\Authenticator\Authenticators;
use RuntimeException;
+use function array_merge;
use function hash_equals;
use function hash_hmac;
use function pack;
@@ -25,16 +27,12 @@
*/
class HOTP extends AuthenticatorAbstract{
- /**
- * @inheritDoc
- */
- public function getCounter(int $data = null):int{
+ public const MODE = self::HOTP;
+
+ public function getCounter(?int $data = null):int{
return ($data ?? 0);
}
- /**
- * @inheritDoc
- */
public function getHMAC(int $counter):string{
if($this->secret === null){
@@ -42,45 +40,58 @@ public function getHMAC(int $counter):string{
}
// @codeCoverageIgnoreStart
$data = (PHP_INT_SIZE < 8)
+ // 32-bit
? "\x00\x00\x00\x00".pack('N', $counter)
+ // 64-bit
: pack('J', $counter);
// @codeCoverageIgnoreEnd
return hash_hmac($this->options->algorithm, $data, $this->secret, true);
}
- /**
- * @inheritDoc
- */
public function getCode(string $hmac):int{
$data = unpack('C*', $hmac);
- $b = ($data[strlen($hmac)] & 0xF);
- // phpcs:ignore
- return (($data[$b + 1] & 0x7F) << 24) | ($data[$b + 2] << 16) | ($data[$b + 3] << 8) | $data[$b + 4];
+
+ if($data === false){
+ throw new RuntimeException('error while unpacking HMAC'); // @codeCoverageIgnore
+ }
+
+ $b = ($data[strlen($hmac)] & 0xF);
+
+ return (($data[$b + 1] & 0x7F) << 24) | ($data[$b + 2] << 16) | ($data[$b + 3] << 8) | $data[$b + 4]; // phpcs:ignore
}
- /**
- * @inheritDoc
- */
public function getOTP(int $code):string{
$code %= (10 ** $this->options->digits);
return str_pad((string)$code, $this->options->digits, '0', STR_PAD_LEFT);
}
- /**
- * @inheritDoc
- */
- public function code(int $data = null):string{
+ public function code(?int $data = null):string{
$hmac = $this->getHMAC($this->getCounter($data));
return $this->getOTP($this->getCode($hmac));
}
- /**
- * @inheritDoc
- */
- public function verify(string $otp, int $data = null):bool{
+ public function verify(string $otp, ?int $data = null):bool{
return hash_equals($this->code($data), $otp);
}
+ protected function getUriParams(string $issuer, ?int $counter = null):array{
+
+ $params = [
+ 'secret' => $this->getSecret(),
+ 'issuer' => $issuer,
+ 'counter' => $this->getCounter($counter),
+ ];
+
+ if(!$this->options->omitUriSettings){
+ $params = array_merge($params, [
+ 'digits' => $this->options->digits,
+ 'algorithm' => $this->options->algorithm,
+ ]);
+ }
+
+ return $params;
+ }
+
}
diff --git a/src/Authenticators/SteamGuard.php b/src/Authenticators/SteamGuard.php
index f041634..79bf2eb 100644
--- a/src/Authenticators/SteamGuard.php
+++ b/src/Authenticators/SteamGuard.php
@@ -9,18 +9,19 @@
*
* @noinspection PhpComposerExtensionStubsInspection
*/
+declare(strict_types=1);
namespace chillerlan\Authenticator\Authenticators;
use chillerlan\Authenticator\Common\Base64;
use RuntimeException;
-use function curl_close;
use function curl_exec;
use function curl_getinfo;
use function curl_init;
use function curl_setopt_array;
use function floor;
use function intdiv;
+use function is_array;
use function json_decode;
use function sprintf;
use function time;
@@ -37,21 +38,17 @@
*/
final class SteamGuard extends TOTP{
+ public const MODE = self::STEAM_GUARD;
+
private const steamCodeChars = '23456789BCDFGHJKMNPQRTVWXY';
private const steamTimeURL = 'https://api.steampowered.com/ITwoFactorService/QueryTime/v0001';
- /**
- * @inheritDoc
- */
public function setSecret(string $encodedSecret):AuthenticatorInterface{
$this->secret = Base64::decode($this->checkEncodedSecret($encodedSecret));
return $this;
}
- /**
- * @inheritDoc
- */
public function getSecret():string{
if($this->secret === null){
@@ -61,27 +58,13 @@ public function getSecret():string{
return Base64::encode($this->secret);
}
- /**
- * @inheritDoc
- * @codeCoverageIgnore
- */
- public function createSecret(int $length = null):string{
- throw new RuntimeException('Not implemented');
- }
-
- /**
- * @inheritDoc
- */
- public function getCounter(int $data = null):int{
+ public function getCounter(?int $data = null):int{
// the period is fixed to 30 seconds for Steam Guard
$this->options->period = 30;
return parent::getCounter($data);
}
- /**
- * @inheritDoc
- */
public function getHMAC(int $counter):string{
// algorithm is fixed to sha1 for Steam Guard
$this->options->algorithm = self::ALGO_SHA1;
@@ -89,9 +72,6 @@ public function getHMAC(int $counter):string{
return parent::getHMAC($counter);
}
- /**
- * @inheritDoc
- */
public function getOTP(int $code):string{
$str = '';
$len = 26; // strlen($this::steamCodeChars)
@@ -105,8 +85,11 @@ public function getOTP(int $code):string{
return $str;
}
+ public function getUri(string $label, string $issuer, ?int $counter = null):string{
+ throw new RuntimeException('Not supported');
+ }
+
/**
- * @inheritDoc
* @throws \RuntimeException
*/
public function getServerTime():int{
@@ -127,21 +110,24 @@ public function getServerTime():int{
$ch = curl_init($this::steamTimeURL);
+ // it's almost impossible to run into this, but hey, phpstan happy
+ if($ch === false){
+ throw new RuntimeException('curl_init error'); // @codeCoverageIgnore
+ }
+
curl_setopt_array($ch, $options);
$response = curl_exec($ch);
$info = curl_getinfo($ch);
- curl_close($ch);
-
- if($info['http_code'] !== 200){
+ if($info['http_code'] !== 200 || $response === false){
// I'm not going to investigate the error further as this shouldn't happen usually
throw new RuntimeException(sprintf('Steam API request error: HTTP/%s', $info['http_code'])); // @codeCoverageIgnore
}
$json = json_decode($response, true);
- if(empty($json) || !isset($json['response']['server_time'])){
+ if(!is_array($json) || !isset($json['response']['server_time'])){
throw new RuntimeException('Unable to decode Steam API response'); // @codeCoverageIgnore
}
diff --git a/src/Authenticators/TOTP.php b/src/Authenticators/TOTP.php
index 63cf322..bc2ef97 100644
--- a/src/Authenticators/TOTP.php
+++ b/src/Authenticators/TOTP.php
@@ -7,9 +7,11 @@
* @copyright 2023 smiley
* @license MIT
*/
+declare(strict_types=1);
namespace chillerlan\Authenticator\Authenticators;
+use function array_merge;
use function floor;
use function hash_equals;
use function time;
@@ -19,10 +21,9 @@
*/
class TOTP extends HOTP{
- /**
- * @inheritDoc
- */
- public function getCounter(int $data = null):int{
+ public const MODE = self::TOTP;
+
+ public function getCounter(?int $data = null):int{
$data ??= time();
if($this->options->useLocalTime === false){
@@ -32,10 +33,7 @@ public function getCounter(int $data = null):int{
return (int)floor(($data + $this->options->time_offset) / $this->options->period);
}
- /**
- * @inheritDoc
- */
- public function verify(string $otp, int $data = null):bool{
+ public function verify(string $otp, ?int $data = null):bool{
$limit = $this->options->adjacent;
if($limit === 0){
@@ -56,4 +54,22 @@ public function verify(string $otp, int $data = null):bool{
return false;
}
+ protected function getUriParams(string $issuer, ?int $counter = null):array{
+
+ $params = [
+ 'secret' => $this->getSecret(),
+ 'issuer' => $issuer,
+ ];
+
+ if(!$this->options->omitUriSettings){
+ $params = array_merge($params, [
+ 'digits' => $this->options->digits,
+ 'algorithm' => $this->options->algorithm,
+ 'period' => $this->options->period,
+ ]);
+ }
+
+ return $params;
+ }
+
}
diff --git a/src/Common/Base32.php b/src/Common/Base32.php
index 2fb6a9b..2b63799 100644
--- a/src/Common/Base32.php
+++ b/src/Common/Base32.php
@@ -7,6 +7,7 @@
* @copyright 2023 smiley
* @license MIT
*/
+declare(strict_types=1);
namespace chillerlan\Authenticator\Common;
diff --git a/src/Common/Base64.php b/src/Common/Base64.php
index f199a79..cfa3328 100644
--- a/src/Common/Base64.php
+++ b/src/Common/Base64.php
@@ -7,6 +7,7 @@
* @copyright 2023 smiley
* @license MIT
*/
+declare(strict_types=1);
namespace chillerlan\Authenticator\Common;
@@ -36,7 +37,7 @@ class Base64{
public static function encode(string $str):string{
if(function_exists('sodium_bin2base64')){
- return sodium_bin2base64($str, \SODIUM_BASE64_VARIANT_ORIGINAL);
+ return \sodium_bin2base64($str, \SODIUM_BASE64_VARIANT_ORIGINAL);
}
return ConstantTimeBase64::encode($str);
@@ -49,7 +50,7 @@ public static function decode(string $base64):string{
self::checkCharacterSet($base64);
if(function_exists('sodium_base642bin')){
- return sodium_base642bin($base64, \SODIUM_BASE64_VARIANT_ORIGINAL);
+ return \sodium_base642bin($base64, \SODIUM_BASE64_VARIANT_ORIGINAL);
}
return ConstantTimeBase64::decode($base64);
diff --git a/src/Common/Hex.php b/src/Common/Hex.php
index 8a1db69..a27ea05 100644
--- a/src/Common/Hex.php
+++ b/src/Common/Hex.php
@@ -7,6 +7,7 @@
* @copyright 2023 smiley
* @license MIT
*/
+declare(strict_types=1);
namespace chillerlan\Authenticator\Common;
@@ -15,7 +16,7 @@
use function preg_match;
/**
- *
+ * Class to provide hexadecimal encoding/decoding of strings using constant time functions
*/
class Hex{
@@ -27,21 +28,21 @@ class Hex{
public const CHARSET = '1234567890ABCDEFabcdef';
/**
- * Encode a raw-binary to hexadecimal
+ * Encode a string to hexadecimal
*
* @codeCoverageIgnore
*/
public static function encode(string $str):string{
if(function_exists('sodium_bin2hex')){
- return sodium_bin2hex($str);
+ return \sodium_bin2hex($str);
}
return ConstantTimeHex::encode($str);
}
/**
- * Decode a raw-binary string from hexadecimal
+ * Decode a string from hexadecimal
*
* @codeCoverageIgnore
*/
@@ -49,7 +50,7 @@ public static function decode(string $hex):string{
self::checkCharacterSet($hex);
if(function_exists('sodium_hex2bin')){
- return sodium_hex2bin($hex);
+ return \sodium_hex2bin($hex);
}
return ConstantTimeHex::decode($hex);
diff --git a/tests/AuthenticatorOptionsTest.php b/tests/AuthenticatorOptionsTest.php
index 782d8ac..b4088de 100644
--- a/tests/AuthenticatorOptionsTest.php
+++ b/tests/AuthenticatorOptionsTest.php
@@ -7,6 +7,7 @@
* @copyright 2019 smiley
* @license MIT
*/
+declare(strict_types=1);
namespace chillerlan\AuthenticatorTest;
@@ -88,7 +89,7 @@ public function testSetAdjacent():void{
public function testSetAdjacentException():void{
$this->expectException(InvalidArgumentException::class);
- $this->expectExceptionMessage('Invalid adjacent');
+ $this->expectExceptionMessage('Invalid number of adjacent codes');
$this->options->adjacent = -1;
}
diff --git a/tests/AuthenticatorTest.php b/tests/AuthenticatorTest.php
index a3d88f7..6206cd9 100644
--- a/tests/AuthenticatorTest.php
+++ b/tests/AuthenticatorTest.php
@@ -7,6 +7,7 @@
* @copyright 2015 Smiley
* @license MIT
*/
+declare(strict_types=1);
namespace chillerlan\AuthenticatorTest;
@@ -31,7 +32,7 @@ protected function setUp():void{
}
public function testSetSecretViaConstruct():void{
- $this->authenticator = new Authenticator(null, self::secret);
+ $this->authenticator = new Authenticator($this->options, self::secret);
$this::assertSame(self::secret, $this->authenticator->getSecret());
}
@@ -44,19 +45,19 @@ public function testGetUri():void{
$this::assertSame(
sprintf('otpauth://totp/%s?secret=%s&issuer=%s&digits=6&algorithm=SHA1&period=30', $label, self::secret, $issuer),
- $this->authenticator->getUri(self::label, self::issuer)
+ $this->authenticator->getUri(self::label, self::issuer),
);
$this->options->digits = 8;
$this::assertSame(
sprintf('otpauth://totp/%s?secret=%s&issuer=%s&digits=8&algorithm=SHA1&period=30', $label, self::secret, $issuer),
- $this->authenticator->getUri(self::label, self::issuer)
+ $this->authenticator->getUri(self::label, self::issuer),
);
$this->options->period = 45;
$this::assertSame(
sprintf('otpauth://totp/%s?secret=%s&issuer=%s&digits=8&algorithm=SHA1&period=45', $label, self::secret, $issuer),
- $this->authenticator->getUri(self::label, self::issuer)
+ $this->authenticator->getUri(self::label, self::issuer),
);
$this->options->mode = AuthenticatorInterface::HOTP;
@@ -66,24 +67,24 @@ public function testGetUri():void{
->setSecret(self::secret);
$this::assertSame(
- sprintf('otpauth://hotp/%s?secret=%s&issuer=%s&digits=8&algorithm=SHA1&counter=42', $label, self::secret, $issuer),
- $this->authenticator->getUri(self::label, self::issuer, 42)
+ sprintf('otpauth://hotp/%s?secret=%s&issuer=%s&counter=42&digits=8&algorithm=SHA1', $label, self::secret, $issuer),
+ $this->authenticator->getUri(self::label, self::issuer, 42),
);
$this->options->algorithm = AuthenticatorInterface::ALGO_SHA512;
$this::assertSame(
- sprintf('otpauth://hotp/%s?secret=%s&issuer=%s&digits=8&algorithm=SHA512', $label, self::secret, $issuer),
- $this->authenticator->getUri(self::label, self::issuer)
+ sprintf('otpauth://hotp/%s?secret=%s&issuer=%s&counter=0&digits=8&algorithm=SHA512', $label, self::secret, $issuer),
+ $this->authenticator->getUri(self::label, self::issuer, 0),
);
// test omit settings
$this::assertSame(
- sprintf('otpauth://%s/%s?secret=%s&issuer=%s', 'hotp', $label, self::secret, $issuer),
- $this->authenticator->getUri(self::label, self::issuer, 42, true)
+ sprintf('otpauth://%s/%s?secret=%s&issuer=%s&counter=42', 'hotp', $label, self::secret, $issuer),
+ $this->authenticator->getUri(self::label, self::issuer, 42, true),
);
}
- public function testGetUriEmptyLabelException(){
+ public function testGetUriEmptyLabelException():void{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('$label and $issuer cannot be empty');
diff --git a/tests/Authenticators/AuthenticatorInterfaceTestAbstract.php b/tests/Authenticators/AuthenticatorInterfaceTestAbstract.php
index e588e90..0896524 100644
--- a/tests/Authenticators/AuthenticatorInterfaceTestAbstract.php
+++ b/tests/Authenticators/AuthenticatorInterfaceTestAbstract.php
@@ -7,6 +7,7 @@
* @copyright 2023 smiley
* @license MIT
*/
+declare(strict_types=1);
namespace chillerlan\AuthenticatorTest\Authenticators;
@@ -18,9 +19,6 @@
use RuntimeException;
use function strlen;
-/**
- *
- */
abstract class AuthenticatorInterfaceTestAbstract extends TestCase{
protected AuthenticatorOptions $options;
@@ -68,7 +66,7 @@ public function testGetSecretException():void{
public function testCreateSecretDefaultLength():void{
$this::assertSame(
$this->options->secret_length,
- strlen(Base32::decode($this->authenticatorInterface->createSecret()))
+ strlen(Base32::decode($this->authenticatorInterface->createSecret())),
);
}
diff --git a/tests/Authenticators/BattleNetTest.php b/tests/Authenticators/BattleNetTest.php
deleted file mode 100644
index ec0da89..0000000
--- a/tests/Authenticators/BattleNetTest.php
+++ /dev/null
@@ -1,145 +0,0 @@
-
- * @copyright 2023 smiley
- * @license MIT
- */
-
-namespace chillerlan\AuthenticatorTest\Authenticators;
-
-use chillerlan\Authenticator\AuthenticatorOptions;
-use chillerlan\Authenticator\Authenticators\{AuthenticatorInterface, BattleNet};
-use chillerlan\Authenticator\Common\Hex;
-use Generator;
-use function date;
-use function dechex;
-use function is_int;
-use const PHP_INT_SIZE;
-
-/**
- * @property \chillerlan\Authenticator\Authenticators\BattleNet $authenticatorInterface
- */
-class BattleNetTest extends AuthenticatorInterfaceTestAbstract{
-
- protected const secret = '3132333435363738393031323334353637383930';
-
- protected const BattleNetVectors = [
- // timestamps and time slices from RFC 6238, see https://tools.ietf.org/html/rfc6238#page-14
- [ 59, '1', '94287082'],
- [ 1111111109, '23523ec', '07081804'],
- [ 1111111111, '23523ed', '14050471'],
- [ 1234567890, '273ef07', '89005924'],
- [ 2000000000, '3f940aa', '69279037'],
- // 64bit only
- [20000000000, '27bc86aa', '65353130'],
- ];
-
- protected function getInstance(AuthenticatorOptions $options):AuthenticatorInterface{
- return new BattleNet($options);
- }
-
- public function testSetGetSecret():void{
- $this->authenticatorInterface->setSecret($this::secret);
-
- $secret = $this->authenticatorInterface->getSecret();
-
- $this::assertSame($this::secret, $secret);
- $this::assertSame($this::rawsecret, Hex::decode($secret));
- }
-
- public function testCreateSecretDefaultLength():void{
- /** @noinspection PhpUnitTestFailedLineInspection */
- $this::markTestSkipped('N/A');
- }
-
- public function testCreateSecretWithLength():void{
- /** @noinspection PhpUnitTestFailedLineInspection */
- $this::markTestSkipped('N/A');
- }
-
- public function testCreateSecretCheckCharacterSet():void{
- /** @noinspection PhpUnitTestFailedLineInspection */
- $this::markTestSkipped('N/A');
- }
-
- public function testCreateSecretException():void{
- /** @noinspection PhpUnitTestFailedLineInspection */
- $this::markTestSkipped('N/A');
- }
-
- /**
- * @see https://github.com/winauth/winauth/blob/c57132f57b8a90e5219c628deb591f4603f27cb0/Authenticator/BattleNetAuthenticator.cs#L450-L451
- */
- public function testRestoreSecret():void{
- $serial = 'US-1306-2525-4376';
- $restore_code = 'CR24KPKF51';
-
- $data = $this->authenticatorInterface->restoreSecret($serial, $restore_code);
-
- $this::assertSame($serial, $data['serial']);
- $this::assertSame($restore_code, $data['restore_code']);
- $this::assertSame('US', $data['region']);
- $this::assertSame('0402761151586faf1cf74efbf3dcb831063909a127220de374d5b4876f12bfcd', $data['public_key']);
- $this::assertSame('7b0bfa8230e54424ab51777dadbfd5374143e3b0', $data['secret']);
- }
-
- /**
- * Timestamps and -slices from the RFC6238 page, codes from a verified implementation
- *
- * @see https://tools.ietf.org/html/rfc6238#page-14
- */
- public static function battleNetVectors():Generator{
- foreach(self::BattleNetVectors as [$timestamp, $timeslice, $totp]){
- // skip 64bit numbers on 32bit PHP
- if(PHP_INT_SIZE < 8 && !is_int($timestamp)){
- continue;
- }
-
- yield date('Y-m-d H:i:s', $timestamp) => [$timestamp, $timeslice, $totp];
- }
- }
-
- /**
- * @dataProvider battleNetVectors
- */
- public function testIntermediateValues(int $timestamp, string $timeslice, string $totp):void{
- $this->authenticatorInterface->setSecret($this::secret);
-
- $timeslice_intermediate = $this->authenticatorInterface->getCounter($timestamp);
-
- $this::assertSame($timeslice, dechex($timeslice_intermediate));
-
- $hmac_intermediate = $this->authenticatorInterface->getHMAC($timeslice_intermediate);
- $code_intermediate = $this->authenticatorInterface->getCode($hmac_intermediate);
- $code_formatted = $this->authenticatorInterface->getOTP($code_intermediate);
-
- $this::assertSame($totp, $code_formatted);
- }
-
- /**
- * @dataProvider battleNetVectors
- */
- public function testAdjacent(int $timestamp, string $timeslice, string $totp):void{
- $adjacent = 20;
- $limit = (2 * $adjacent);
-
- $this->authenticatorInterface->setSecret($this::secret);
-
- $this->options->adjacent = $adjacent;
- // phpcs:ignore
- for($i = -$limit; $i <= $limit; $i++){
- $this->options->time_offset = ($i * $this->options->period);
-
- $verify = $this->authenticatorInterface->verify($totp, $timestamp);
-
- ($i < -$adjacent || $i > $adjacent)
- ? $this::assertFalse($verify)
- : $this::assertTrue($verify);
- }
-
- }
-
-}
diff --git a/tests/Authenticators/HOTPTest.php b/tests/Authenticators/HOTPTest.php
index c2ac298..90549e9 100644
--- a/tests/Authenticators/HOTPTest.php
+++ b/tests/Authenticators/HOTPTest.php
@@ -7,6 +7,7 @@
* @copyright 2023 smiley
* @license MIT
*/
+declare(strict_types=1);
namespace chillerlan\AuthenticatorTest\Authenticators;
@@ -16,9 +17,6 @@
use function bin2hex;
use function sprintf;
-/**
- *
- */
class HOTPTest extends AuthenticatorInterfaceTestAbstract{
/**
diff --git a/tests/Authenticators/SteamGuardTest.php b/tests/Authenticators/SteamGuardTest.php
index a44c2e7..cbc13ec 100644
--- a/tests/Authenticators/SteamGuardTest.php
+++ b/tests/Authenticators/SteamGuardTest.php
@@ -7,6 +7,7 @@
* @copyright 2023 smiley
* @license MIT
*/
+declare(strict_types=1);
namespace chillerlan\AuthenticatorTest\Authenticators;
@@ -17,6 +18,7 @@
use function date;
use function dechex;
use function is_int;
+use function strlen;
use const PHP_INT_SIZE;
/**
@@ -51,23 +53,26 @@ public function testSetGetSecret():void{
}
public function testCreateSecretDefaultLength():void{
- /** @noinspection PhpUnitTestFailedLineInspection */
- $this::markTestSkipped('N/A');
+ $this::assertSame(
+ $this->options->secret_length,
+ strlen(Base64::decode($this->authenticatorInterface->createSecret())),
+ );
}
public function testCreateSecretWithLength():void{
- /** @noinspection PhpUnitTestFailedLineInspection */
- $this::markTestSkipped('N/A');
+
+ for($secretLength = 16; $secretLength <= 512; $secretLength += 8){
+ $secret = Base64::decode($this->authenticatorInterface->createSecret($secretLength));
+
+ $this::assertSame($secretLength, strlen($secret));
+ }
+
}
public function testCreateSecretCheckCharacterSet():void{
- /** @noinspection PhpUnitTestFailedLineInspection */
- $this::markTestSkipped('N/A');
- }
+ $secret = $this->authenticatorInterface->createSecret(32);
- public function testCreateSecretException():void{
- /** @noinspection PhpUnitTestFailedLineInspection */
- $this::markTestSkipped('N/A');
+ $this::assertMatchesRegularExpression('#^['.Base64::CHARSET.']+$#', $secret);
}
/**
@@ -107,7 +112,7 @@ public function testIntermediateValues(int $timestamp, string $timeslice, string
* @dataProvider steamGuardVectors
*/
public function testAdjacent(int $timestamp, string $timeslice, string $totp):void{
- $adjacent = 20;
+ $adjacent = 10;
$limit = (2 * $adjacent);
$this->authenticatorInterface->setSecret($this::secret);
diff --git a/tests/Authenticators/TOTPTest.php b/tests/Authenticators/TOTPTest.php
index 7e08ea2..dad4374 100644
--- a/tests/Authenticators/TOTPTest.php
+++ b/tests/Authenticators/TOTPTest.php
@@ -7,6 +7,7 @@
* @copyright 2023 smiley
* @license MIT
*/
+declare(strict_types=1);
namespace chillerlan\AuthenticatorTest\Authenticators;
@@ -19,9 +20,6 @@
use function sprintf;
use const PHP_INT_SIZE;
-/**
- *
- */
class TOTPTest extends AuthenticatorInterfaceTestAbstract{
/**
@@ -102,7 +100,7 @@ public function testIntermediateValues(string $algorithm, int $timestamp, string
* @dataProvider totpVectors
*/
public function testAdjacent(string $algorithm, int $timestamp, string $timeslice, int $code, string $totp):void{
- $adjacent = 20;
+ $adjacent = 10;
$limit = (2 * $adjacent);
$this->authenticatorInterface->setSecret(self::secrets[$algorithm]);
diff --git a/tests/Common/Base32Test.php b/tests/Common/Base32Test.php
index c03a907..4ad8f3a 100644
--- a/tests/Common/Base32Test.php
+++ b/tests/Common/Base32Test.php
@@ -7,6 +7,7 @@
* @copyright 2016 Smiley
* @license MIT
*/
+declare(strict_types=1);
namespace chillerlan\AuthenticatorTest\Common;
@@ -16,6 +17,9 @@
class Base32Test extends TestCase{
+ /**
+ * @phpstan-return array>
+ */
public static function base32DataProvider():array{
return [
['a' , 'ME' ],
diff --git a/tests/Common/Base64Test.php b/tests/Common/Base64Test.php
index 7c8d0fa..9fdbcb1 100644
--- a/tests/Common/Base64Test.php
+++ b/tests/Common/Base64Test.php
@@ -7,6 +7,7 @@
* @copyright 2023 smiley
* @license MIT
*/
+declare(strict_types=1);
namespace chillerlan\AuthenticatorTest\Common;
@@ -16,11 +17,11 @@
use function base64_decode;
use function base64_encode;
-/**
- *
- */
class Base64Test extends TestCase{
+ /**
+ * @phpstan-return array>
+ */
public static function base64DataProvider():array{
return [
['a' , 'YQ==' ],
@@ -52,7 +53,7 @@ public function testDecode(string $str, string $base64):void{
$this::assertSame($str, $decoded);
// test against native PHP
- $this::assertSame(base64_decode($base64), $decoded);
+ $this::assertSame(base64_decode($base64, true), $decoded);
}
/**
diff --git a/tests/Common/HexTest.php b/tests/Common/HexTest.php
index 298ee39..4156cf1 100644
--- a/tests/Common/HexTest.php
+++ b/tests/Common/HexTest.php
@@ -7,6 +7,7 @@
* @copyright 2023 smiley
* @license MIT
*/
+declare(strict_types=1);
namespace chillerlan\AuthenticatorTest\Common;
@@ -16,11 +17,11 @@
use function bin2hex;
use function hex2bin;
-/**
- *
- */
class HexTest extends TestCase{
+ /**
+ * @phpstan-return array>
+ */
public static function hexDataProvider():array{
return [
['a' , '61' ],