Skip to content

Commit 58b838d

Browse files
committed
✨ v3
1 parent ad5e67b commit 58b838d

32 files changed

+1860
-432
lines changed

.github/workflows/ci.yml

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,49 @@ name: "Continuous Integration"
1313

1414
jobs:
1515

16+
static-code-analysis:
17+
name: "Static Code Analysis"
18+
19+
runs-on: ubuntu-latest
20+
21+
strategy:
22+
fail-fast: true
23+
matrix:
24+
php-version:
25+
- "7.2"
26+
- "7.3"
27+
- "7.4"
28+
- "8.0"
29+
- "8.1"
30+
- "8.2"
31+
- "8.3"
32+
33+
env:
34+
PHAN_ALLOW_XDEBUG: 0
35+
PHAN_DISABLE_XDEBUG_WARN: 1
36+
37+
steps:
38+
- name: "Checkout"
39+
uses: actions/checkout@v3
40+
41+
- name: "Install PHP"
42+
uses: shivammathur/setup-php@v2
43+
with:
44+
php-version: ${{ matrix.php-version }}
45+
tools: pecl
46+
coverage: none
47+
extensions: ast, curl, gmp, json, sodium
48+
49+
- name: "Install dependencies with composer"
50+
uses: ramsey/composer-install@v2
51+
52+
- name: "Run phan"
53+
run: php vendor/bin/phan
54+
55+
1656
tests:
1757
name: "Unit Tests"
58+
needs: static-code-analysis
1859
runs-on: ${{ matrix.os }}
1960

2061
strategy:
@@ -24,11 +65,13 @@ jobs:
2465
- ubuntu-latest
2566
- windows-latest
2667
php-version:
27-
- "7.0"
28-
- "7.1"
2968
- "7.2"
3069
- "7.3"
3170
- "7.4"
71+
- "8.0"
72+
- "8.1"
73+
- "8.2"
74+
- "8.3"
3275

3376
steps:
3477
- name: "Checkout"
@@ -39,7 +82,7 @@ jobs:
3982
with:
4083
php-version: ${{ matrix.php-version }}
4184
coverage: xdebug
42-
extensions: ast
85+
extensions: curl, gmp, json, sodium
4386

4487
- name: "Install dependencies with composer"
4588
uses: ramsey/composer-install@v2

.phan/config.php

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
/**
3+
* This configuration will be read and overlaid on top of the
4+
* default configuration. Command-line arguments will be applied
5+
* after this file is read.
6+
*/
7+
return [
8+
// Supported values: `'5.6'`, `'7.0'`, `'7.1'`, `'7.2'`, `'7.3'`,
9+
// `'7.4'`, `null`.
10+
// If this is set to `null`,
11+
// then Phan assumes the PHP version which is closest to the minor version
12+
// of the php executable used to execute Phan.
13+
//
14+
// Note that the **only** effect of choosing `'5.6'` is to infer
15+
// that functions removed in php 7.0 exist.
16+
// (See `backward_compatibility_checks` for additional options)
17+
'target_php_version' => null,
18+
'minimum_target_php_version' => '7.2',
19+
20+
// A list of directories that should be parsed for class and
21+
// method information. After excluding the directories
22+
// defined in exclude_analysis_directory_list, the remaining
23+
// files will be statically analyzed for errors.
24+
//
25+
// Thus, both first-party and third-party code being used by
26+
// your application should be included in this list.
27+
'directory_list' => [
28+
'examples',
29+
'src',
30+
'tests',
31+
'vendor',
32+
],
33+
34+
// A regex used to match every file name that you want to
35+
// exclude from parsing. Actual value will exclude every
36+
// "test", "tests", "Test" and "Tests" folders found in
37+
// "vendor/" directory.
38+
'exclude_file_regex' => '@^vendor/.*/(tests?|Tests?)/@',
39+
40+
// A directory list that defines files that will be excluded
41+
// from static analysis, but whose class and method
42+
// information should be included.
43+
//
44+
// Generally, you'll want to include the directories for
45+
// third-party code (such as "vendor/") in this list.
46+
//
47+
// n.b.: If you'd like to parse but not analyze 3rd
48+
// party code, directories containing that code
49+
// should be added to both the `directory_list`
50+
// and `exclude_analysis_directory_list` arrays.
51+
'exclude_analysis_directory_list' => [
52+
'vendor/',
53+
],
54+
'suppress_issue_types' => [
55+
'PhanAccessMethodInternal',
56+
'PhanDeprecatedFunction',
57+
],
58+
];

README.md

Lines changed: 49 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,22 @@ A generator for counter based ([RFC 4226](https://tools.ietf.org/html/rfc4226))
1818

1919
# Documentation
2020
## Requirements
21-
- PHP 7.0+
21+
- PHP 7.2+
22+
- [`ext-curl`](https://www.php.net/manual/book.curl) for Battle.net and Steam Guard server time synchronization
23+
- [`ext-gmp`](https://www.php.net/manual/book.gmp) for Battle.net authenticator secret retrieval (RSA encryption)
24+
- [`ext-sodium`](https://www.php.net/manual/book.sodium) for constant time implementations of base64 encode/decode and hex2bin/bin2hex
25+
([`paragonie/constant_time_encoding`](https://github.com/paragonie/constant_time_encoding) is used as fallback)
2226

2327
## Installation
2428
**requires [composer](https://getcomposer.org)**
2529

2630
via terminal: `composer require chillerlan/php-authenticator`
2731

28-
*composer.json* (note: replace `dev-main` with a [version constraint](https://getcomposer.org/doc/articles/versions.md#writing-version-constraints), e.g. `^2.1` - see [releases](https://github.com/chillerlan/php-authenticator/releases) for valid versions)
32+
*composer.json* (note: replace `dev-main` with a [version constraint](https://getcomposer.org/doc/articles/versions.md#writing-version-constraints), e.g. `^3.1` - see [releases](https://github.com/chillerlan/php-authenticator/releases) for valid versions)
2933
```json
3034
{
3135
"require": {
32-
"php": "^7.0",
36+
"php": "^7.2 || ^8.0",
3337
"chillerlan/php-authenticator": "dev-main"
3438
}
3539
}
@@ -43,12 +47,15 @@ The secret is usually being created once during the activation process in a user
4347
So all you need to do there is to display it to the user in a convenient way -
4448
as a text string and QR code for example - and save it somewhere with the user data.
4549
```php
46-
use chillerlan\Authenticator\Authenticator;
50+
use chillerlan\Authenticator\{Authenticator, AuthenticatorOptions};
4751

48-
$authenticator = new Authenticator;
52+
$options = new AuthenticatorOptions;
53+
$options->secret_length = 32;
54+
55+
$authenticator = new Authenticator($options);
4956
// create a secret (stored somewhere in a *safe* place on the server. safe... hahaha jk)
5057
$secret = $authenticator->createSecret();
51-
// you can also specify the length of the secret key
58+
// you can also specify the length of the secret key, which overrides the options setting
5259
$secret = $authenticator->createSecret(20);
5360
// set an existing secret
5461
$authenticator->setSecret($secret);
@@ -71,25 +78,23 @@ if($authenticator->verify($otp)){
7178
#### time based (TOTP)
7279
Verify adjacent codes
7380
```php
74-
// $period value from options
75-
$period = 30;
7681
// try the first adjacent
77-
$authenticator->verify($otp, time() - $period); // -> true
82+
$authenticator->verify($otp, time() - $options->period); // -> true
7883
// try the second adjacent, default is 1
79-
$authenticator->verify($otp, time() + 2 * $period); // -> false
84+
$authenticator->verify($otp, time() + 2 * $options->period); // -> false
8085
// allow 2 adjacent codes
81-
$authenticator->setOptions(['adjacent' => 2]);
82-
$authenticator->verify($otp, time() + 2 * $period); // -> true
86+
$options->adjacent = 2;
87+
$authenticator->verify($otp, time() + 2 * $options->period); // -> true
8388
```
8489

8590
#### counter based (HOTP)
8691
```php
8792
// switch mode to HOTP
88-
$authenticator->setOptions(['mode' => AuthenticatorInterface::HOTP]);
93+
$options->mode = AuthenticatorInterface::HOTP;
8994
// user sends the OTP for code #42, which is equivalent to
9095
$otp = $authenticator->code(42); // -> 123456
9196
// verify [123456, 42]
92-
$authenticator->verify($otp, $counterValueFromUserDatabase); // -> true
97+
$authenticator->verify($otp, $counterValueFromUserDatabase) // -> true
9398
```
9499

95100
### URI creation
@@ -107,25 +112,39 @@ Keep in mind that several URI settings are not (yet) recognized by all authentic
107112

108113
```php
109114
// code length, currently 6 or 8
110-
$authenticator->setOptions(['digits' => 8]);
115+
$options->digits = 8;
111116
// valid period between 15 and 60 seconds
112-
$authenticator->setOptions(['period' => 45]);
117+
$options->period = 45;
113118
// set the HMAC hash algorithm
114-
$authenticator->setOptions(['algorithm' => AuthenticatorInterface::ALGO_SHA512]);
119+
$options->algorithm = AuthenticatorInterface::ALGO_SHA512;
115120
```
116121

117122
## API
118123
### `Authenticator`
119-
| method | return | description |
120-
|---------------------------------------------------------------------------------------------|------------------|------------------------------------------------------------------|
121-
| `__construct(array $options = null, string $secret = null)` | - | |
122-
| `setOptions(array $options)` | `Authenticator` | called internally by `__construct()` |
123-
| `setSecret(string $secret)` | `Authenticator` | called internally by `__construct()` |
124-
| `getSecret()` | `string` | |
125-
| `createSecret(int $length = null)` | `string` | `$length` overrides `$options` setting |
126-
| `code(int $data = null)` | `string` | `$data` may be a UNIX timestamp (TOTP) or a counter value (HOTP) |
127-
| `verify(string $otp, int $data = null)` | `bool` | for `$data` see `Authenticator::code()` |
128-
| `getUri(string $label, string $issuer, int $hotpCounter = null, bool $omitSettings = null)` | `string` | |
124+
| method | return | description |
125+
|---------------------------------------------------------------------------------------------|-----------------|------------------------------------------------------------------|
126+
| `__construct(SettingsContainerInterface $options = null, string $secret = null)` | - | |
127+
| `setOptions(SettingsContainerInterface $options)` | `Authenticator` | called internally by `__construct()` |
128+
| `setSecret(string $secret)` | `Authenticator` | called internally by `__construct()` |
129+
| `getSecret()` | `string` | |
130+
| `createSecret(int $length = null)` | `string` | `$length` overrides `AuthenticatorOptions` setting |
131+
| `code(int $data = null)` | `string` | `$data` may be a UNIX timestamp (TOTP) or a counter value (HOTP) |
132+
| `verify(string $otp, int $data = null)` | `bool` | for `$data` see `Authenticator::code()` |
133+
| `getUri(string $label, string $issuer, int $hotpCounter = null, bool $omitSettings = null)` | `string` | |
134+
135+
### `AuthenticatorOptions`
136+
#### Properties
137+
| property | type | default | allowed | description |
138+
|---------------------|----------|---------|----------------------------------------|---------------------------------------------------------------------------------|
139+
| `$digits` | `int` | 6 | 6 or 8 | auth code length |
140+
| `$period` | `int` | 30 | 15 - 60 | validation period (seconds) |
141+
| `$secret_length` | `int` | 20 | &gt;= 16 | length of the secret phrase (bytes, unencoded binary) |
142+
| `$algorithm` | `string` | `SHA1` | `SHA1`, `SHA256` or `SHA512` | HMAC hash algorithm, see `AuthenticatorInterface::HASH_ALGOS` |
143+
| `$mode` | `string` | `totp` | `totp`, `hotp`, `battlenet` or `steam` | authenticator mode: time- or counter based, see `AuthenticatorInterface::MODES` |
144+
| `$adjacent` | `int` | 1 | &gt;= 0 | number of allowed adjacent codes |
145+
| `$time_offset` | `int` | 0 | * | fixed time offset that will be added to the current time value |
146+
| `$useLocalTime` | `bool` | true | * | whether to use local time or request server time |
147+
| `$forceTimeRefresh` | `bool` | false | * | whether to force refreshing server time on each call |
129148

130149
### `AuthenticatorInterface`
131150
#### Methods
@@ -135,6 +154,7 @@ $authenticator->setOptions(['algorithm' => AuthenticatorInterface::ALGO_SHA512])
135154
| `setSecret(string $encodedSecret)` | `AuthenticatorInterface` | |
136155
| `getSecret()` | `string` | |
137156
| `createSecret(int $length = null)` | `string` | |
157+
| `getServertime()` | `int` | |
138158
| `getCounter(int $data = null)` | `int` | internal |
139159
| `getHMAC(int $counter)` | `string` | internal |
140160
| `getCode(string $hmac)` | `int` | internal |
@@ -147,6 +167,8 @@ $authenticator->setOptions(['algorithm' => AuthenticatorInterface::ALGO_SHA512])
147167
|---------------|----------|-----------------------------------|
148168
| `TOTP` | `string` | |
149169
| `HOTP` | `string` | |
170+
| `STEAM_GUARD` | `string` | |
171+
| `BATTLE_NET` | `string` | |
150172
| `ALGO_SHA1` | `string` | |
151173
| `ALGO_SHA256` | `string` | |
152174
| `ALGO_SHA512` | `string` | |

composer.json

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "chillerlan/php-authenticator",
3-
"description": "A generator for counter- and time based 2-factor authentication codes (Google Authenticator). PHP 7.0+",
3+
"description": "A generator for counter- and time based 2-factor authentication codes (Google Authenticator). PHP 7.2+",
44
"homepage": "https://github.com/chillerlan/php-authenticator",
55
"license": "MIT",
66
"type": "library",
@@ -21,12 +21,18 @@
2121
"minimum-stability": "stable",
2222
"prefer-stable": true,
2323
"require": {
24-
"php": "^7.0",
24+
"php": "^7.2 || ^8.0",
25+
"chillerlan/php-settings-container": "^1.2.2 || ^2.1.4 || ^3.0",
2526
"paragonie/constant_time_encoding": "^2.6"
2627
},
2728
"require-dev": {
29+
"ext-curl": "*",
30+
"ext-gmp": "*",
31+
"ext-json": "*",
32+
"ext-sodium": "*",
33+
"phan/phan": "^5.4",
2834
"phpmd/phpmd": "^2.13",
29-
"phpunit/phpunit": "^6.5 || ^7.5",
35+
"phpunit/phpunit": "^8.5 || ^9.6",
3036
"squizlabs/php_codesniffer": "^3.7"
3137
},
3238
"suggest": {
@@ -43,7 +49,8 @@
4349
}
4450
},
4551
"scripts": {
46-
"phpunit": "@php vendor/bin/phpunit"
52+
"phpunit": "@php vendor/bin/phpunit",
53+
"phan": "@php vendor/bin/phan --allow-polyfill-parser"
4754
},
4855
"config": {
4956
"lock": false,

examples/battlenet.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
/**
3+
* Battle.net example
4+
*
5+
* @created 28.06.2023
6+
* @author Smiley <smiley@chillerlan.net>
7+
* @copyright 2023 Smiley
8+
* @license MIT
9+
*/
10+
11+
use chillerlan\Authenticator\{Authenticator, AuthenticatorOptions, Authenticators\BattleNet};
12+
use chillerlan\Authenticator\Authenticators\AuthenticatorInterface;
13+
14+
require_once '../vendor/autoload.php';
15+
16+
$options = new AuthenticatorOptions([
17+
// switch mode to BATTLE_NET
18+
'mode' => AuthenticatorInterface::BATTLE_NET,
19+
]);
20+
21+
$auth = new Authenticator($options);
22+
23+
// set a secret - Battle.net secrets come as hex strings (20 byte, 40 chars)
24+
$secret = $auth->setSecret('3132333435363738393031323334353637383930');
25+
// get a one time code
26+
$code = $auth->code();
27+
var_dump($code);
28+
// verify the current code
29+
var_dump($auth->verify($code)); // -> true
30+
// previous code
31+
var_dump($auth->verify($code, time() - $options->period)); // -> true
32+
// 2nd adjacent is invalid
33+
var_dump($auth->verify($code, time() + 2 * $options->period)); // -> false
34+
// allow 2 adjacent codes
35+
$options->adjacent = 2;
36+
var_dump($auth->verify($code, time() + 2 * $options->period)); // -> true
37+
38+
// request a new authenticator from the Battle.net API
39+
// this requires the BattleNet class to be invoked directly as we're using non-interface methods for this
40+
$auth = new BattleNet;
41+
$data = $auth->createAuthenticator('EU');
42+
// the serial can be used to attach this authenticator to an existing Battle.net account
43+
var_dump($data);
44+
// it's also possible to retreive an authenticator secret from an existing serial and restore code, e.g. from WinAuth
45+
$data = $auth->restoreSecret($data['serial'], $data['restore_code']);
46+
var_dump($data);

examples/hotp.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,19 @@
88
* @license MIT
99
*/
1010

11-
use chillerlan\Authenticator\Authenticator;
11+
use chillerlan\Authenticator\{Authenticator, AuthenticatorOptions};
1212
use chillerlan\Authenticator\Authenticators\AuthenticatorInterface;
1313

1414
require_once '../vendor/autoload.php';
1515

16-
$options = [
16+
$options = new AuthenticatorOptions([
1717
// switch mode to HOTP
1818
'mode' => AuthenticatorInterface::HOTP,
1919
// change the code length
2020
'digits' => 8,
2121
// set the HMAC hash algo
2222
'algorithm' => AuthenticatorInterface::ALGO_SHA256,
23-
];
23+
]);
2424

2525
$auth = new Authenticator($options);
2626

0 commit comments

Comments
 (0)