From 0b69969261d3aab51a66bbc2a49734e981f227fe Mon Sep 17 00:00:00 2001 From: smiley Date: Sat, 8 Jul 2023 23:23:44 +0200 Subject: [PATCH 1/5] :octocat: v3.x --- .github/workflows/ci.yml | 4 ++-- README.md | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 35151c7..73c1ba7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,10 +4,10 @@ on: push: branches: - - main + - v3.x pull_request: branches: - - main + - v3.x name: "Continuous Integration" diff --git a/README.md b/README.md index 79893db..7167a91 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,13 @@ 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/v3.x/LICENSE +[gh-action-badge]: https://img.shields.io/github/actions/workflow/status/chillerlan/php-authenticator/ci.yml?branch=v3.x&logo=github +[gh-action]: https://github.com/chillerlan/php-authenticator/actions?query=branch%3Av3.x +[coverage-badge]: https://img.shields.io/codecov/c/gh/chillerlan/php-authenticator/v3.x?logo=codecov +[coverage]: https://app.codecov.io/github/chillerlan/php-authenticator/tree/v3.x +[codacy-badge]: https://img.shields.io/codacy/grade/a2793225b448495c9659f27f7f52380a/v3.x?logo=codacy +[codacy]: https://www.codacy.com/gh/chillerlan/php-authenticator/dashboard?branch=v3.x # Documentation ## Requirements @@ -29,12 +29,12 @@ 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. `^3.1` - see [releases](https://github.com/chillerlan/php-authenticator/releases) for valid versions) +*composer.json* (note: replace `dev-v3.x` 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) ```json { "require": { "php": "^7.2 || ^8.0", - "chillerlan/php-authenticator": "dev-main" + "chillerlan/php-authenticator": "dev-v3.x" } } ``` @@ -177,6 +177,6 @@ $options->algorithm = AuthenticatorInterface::ALGO_SHA512;

- 2FA ALL THE THINGS! + 2FA ALL THE THINGS!

From 14dce061e61907d730177a9c5666b5797f6e6927 Mon Sep 17 00:00:00 2001 From: smiley Date: Sun, 9 Jul 2023 00:32:47 +0200 Subject: [PATCH 2/5] :lipstick: --- .phan/config.php | 10 +++++----- examples/battlenet.php | 10 +++++----- examples/steam.php | 6 +++--- examples/totp.php | 6 +++--- phpcs.xml.dist | 1 + 5 files changed, 17 insertions(+), 16 deletions(-) diff --git a/.phan/config.php b/.phan/config.php index 7f83555..426b4a8 100644 --- a/.phan/config.php +++ b/.phan/config.php @@ -14,8 +14,8 @@ // Note that the **only** effect of choosing `'5.6'` is to infer // that functions removed in php 7.0 exist. // (See `backward_compatibility_checks` for additional options) - 'target_php_version' => null, - 'minimum_target_php_version' => '7.2', + 'target_php_version' => null, + 'minimum_target_php_version' => '7.2', // A list of directories that should be parsed for class and // method information. After excluding the directories @@ -24,7 +24,7 @@ // // Thus, both first-party and third-party code being used by // your application should be included in this list. - 'directory_list' => [ + 'directory_list' => [ 'examples', 'src', 'tests', @@ -35,7 +35,7 @@ // exclude from parsing. Actual value will exclude every // "test", "tests", "Test" and "Tests" folders found in // "vendor/" directory. - 'exclude_file_regex' => '@^vendor/.*/(tests?|Tests?)/@', + 'exclude_file_regex' => '@^vendor/.*/(tests?|Tests?)/@', // A directory list that defines files that will be excluded // from static analysis, but whose class and method @@ -51,7 +51,7 @@ 'exclude_analysis_directory_list' => [ 'vendor/', ], - 'suppress_issue_types' => [ + 'suppress_issue_types' => [ 'PhanAccessMethodInternal', 'PhanDeprecatedFunction', ], diff --git a/examples/battlenet.php b/examples/battlenet.php index 9620d57..9efb4d7 100644 --- a/examples/battlenet.php +++ b/examples/battlenet.php @@ -8,8 +8,8 @@ * @license MIT */ -use chillerlan\Authenticator\{Authenticator, AuthenticatorOptions, Authenticators\BattleNet}; -use chillerlan\Authenticator\Authenticators\AuthenticatorInterface; +use chillerlan\Authenticator\{Authenticator, AuthenticatorOptions}; +use chillerlan\Authenticator\Authenticators\{AuthenticatorInterface, BattleNet}; require_once '../vendor/autoload.php'; @@ -28,12 +28,12 @@ // 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 // 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 diff --git a/examples/steam.php b/examples/steam.php index 218e8f2..e87151a 100644 --- a/examples/steam.php +++ b/examples/steam.php @@ -29,9 +29,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..1b77f92 100644 --- a/examples/totp.php +++ b/examples/totp.php @@ -38,12 +38,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..242d90e 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -138,6 +138,7 @@ + From 5da490d41501c549807889ed4655c42474bb63b9 Mon Sep 17 00:00:00 2001 From: smiley Date: Sat, 6 Jan 2024 14:30:13 +0100 Subject: [PATCH 3/5] :octocat: +gitattributes (cherry picked from commit 0ba98a21e61a9bca38227a9ca68a35a9dd092527) --- .gitattributes | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8add556 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,17 @@ +/.build export-ignore +/.github 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 + +*.php diff=php From 23f85fe4976aa6795c4b170cf8a3375e1c53b862 Mon Sep 17 00:00:00 2001 From: smiley Date: Sat, 6 Jan 2024 14:50:58 +0100 Subject: [PATCH 4/5] :octocat: remove BattleNet secret creation/restore (#6) (cherry picked from commit 0d0f61fbb90de50814d494f565c1107479e21304) --- examples/battlenet.php | 12 +- src/Authenticators/BattleNet.php | 342 +------------------------ tests/Authenticators/BattleNetTest.php | 16 -- 3 files changed, 2 insertions(+), 368 deletions(-) diff --git a/examples/battlenet.php b/examples/battlenet.php index 9efb4d7..7b2a646 100644 --- a/examples/battlenet.php +++ b/examples/battlenet.php @@ -9,7 +9,7 @@ */ use chillerlan\Authenticator\{Authenticator, AuthenticatorOptions}; -use chillerlan\Authenticator\Authenticators\{AuthenticatorInterface, BattleNet}; +use chillerlan\Authenticator\Authenticators\AuthenticatorInterface; require_once '../vendor/autoload.php'; @@ -34,13 +34,3 @@ // 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/src/Authenticators/BattleNet.php b/src/Authenticators/BattleNet.php index cb784fa..07e8b68 100644 --- a/src/Authenticators/BattleNet.php +++ b/src/Authenticators/BattleNet.php @@ -13,97 +13,17 @@ 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 + * @see https://github.com/jleclanche/python-bna/issues/38 */ 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'; - - /** @var array */ - private $curlInfo = []; - /** * @inheritDoc */ @@ -163,264 +83,4 @@ public function getOTP(int $code):string{ 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/tests/Authenticators/BattleNetTest.php b/tests/Authenticators/BattleNetTest.php index ec0da89..a9b2acd 100644 --- a/tests/Authenticators/BattleNetTest.php +++ b/tests/Authenticators/BattleNetTest.php @@ -70,22 +70,6 @@ public function testCreateSecretException():void{ $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 * From 4fb2e236d55378428774731cba7f7d9a6ad093cf Mon Sep 17 00:00:00 2001 From: smiley Date: Sat, 6 Jan 2024 17:01:17 +0100 Subject: [PATCH 5/5] :octocat: dependency updates --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 1292adf..4b60b94 100644 --- a/composer.json +++ b/composer.json @@ -31,9 +31,9 @@ "ext-json": "*", "ext-sodium": "*", "phan/phan": "^5.4", - "phpmd/phpmd": "^2.13", + "phpmd/phpmd": "^2.15", "phpunit/phpunit": "^8.5 || ^9.6", - "squizlabs/php_codesniffer": "^3.7" + "squizlabs/php_codesniffer": "^3.8" }, "suggest": { "chillerlan/php-qrcode": "Create QR Codes for use with an authenticator app."