diff options
Diffstat (limited to 'vendor/spomky-labs/otphp')
| -rw-r--r-- | vendor/spomky-labs/otphp/.php_cs.dist | 61 | ||||
| -rw-r--r-- | vendor/spomky-labs/otphp/CODE_OF_CONDUCT.md | 46 | ||||
| -rw-r--r-- | vendor/spomky-labs/otphp/README.md | 43 | ||||
| -rw-r--r-- | vendor/spomky-labs/otphp/SECURITY.md | 13 | ||||
| -rw-r--r-- | vendor/spomky-labs/otphp/composer.json | 49 | ||||
| -rw-r--r-- | vendor/spomky-labs/otphp/phpstan.neon | 13 | ||||
| -rw-r--r-- | vendor/spomky-labs/otphp/src/Factory.php | 97 | ||||
| -rw-r--r-- | vendor/spomky-labs/otphp/src/FactoryInterface.php | 15 | ||||
| -rw-r--r-- | vendor/spomky-labs/otphp/src/HOTP.php | 128 | ||||
| -rw-r--r-- | vendor/spomky-labs/otphp/src/HOTPInterface.php | 29 | ||||
| -rw-r--r-- | vendor/spomky-labs/otphp/src/InternalClock.php | 19 | ||||
| -rw-r--r-- | vendor/spomky-labs/otphp/src/OTP.php | 120 | ||||
| -rw-r--r-- | vendor/spomky-labs/otphp/src/OTPInterface.php | 91 | ||||
| -rw-r--r-- | vendor/spomky-labs/otphp/src/ParameterTrait.php | 132 | ||||
| -rw-r--r-- | vendor/spomky-labs/otphp/src/TOTP.php | 218 | ||||
| -rw-r--r-- | vendor/spomky-labs/otphp/src/TOTPInterface.php | 35 | ||||
| -rw-r--r-- | vendor/spomky-labs/otphp/src/Url.php | 102 |
17 files changed, 716 insertions, 495 deletions
diff --git a/vendor/spomky-labs/otphp/.php_cs.dist b/vendor/spomky-labs/otphp/.php_cs.dist deleted file mode 100644 index 7410dff65..000000000 --- a/vendor/spomky-labs/otphp/.php_cs.dist +++ /dev/null @@ -1,61 +0,0 @@ -<?php - -$header = 'The MIT License (MIT) - -Copyright (c) 2014-2019 Spomky-Labs - -This software may be modified and distributed under the terms -of the MIT license. See the LICENSE file for details.'; - -$finder = PhpCsFixer\Finder::create() - ->in(__DIR__.'/src') - ->in(__DIR__.'/tests') -; - -return PhpCsFixer\Config::create() - ->setRules([ - '@PSR1' => true, - '@PSR2' => true, - '@Symfony' => true, - '@DoctrineAnnotation' => true, - '@PHP70Migration' => true, - '@PHP71Migration' => true, - 'strict_param' => true, - 'strict_comparison' => true, - 'array_syntax' => ['syntax' => 'short'], - 'array_indentation' => true, - 'ordered_imports' => true, - 'protected_to_private' => true, - 'declare_strict_types' => true, - 'native_function_invocation' => [ - 'include' => ['@compiler_optimized'], - 'scope' => 'namespaced', - ], - 'mb_str_functions' => true, - 'method_chaining_indentation' => true, - 'linebreak_after_opening_tag' => true, - 'combine_consecutive_issets' => true, - 'combine_consecutive_unsets' => true, - 'compact_nullable_typehint' => true, - 'no_superfluous_phpdoc_tags' => true, - 'no_superfluous_elseif' => true, - 'phpdoc_trim_consecutive_blank_line_separation' => true, - 'phpdoc_order' => true, - 'pow_to_exponentiation' => true, - 'simplified_null_return' => true, - 'header_comment' => [ - 'header' => $header, - ], - 'align_multiline_comment' => [ - 'comment_type' => 'all_multiline', - ], - 'php_unit_test_annotation' => [ - 'case' => 'snake', - 'style' => 'annotation', - ], - 'php_unit_test_case_static_method_calls' => true, - ]) - ->setRiskyAllowed(true) - ->setUsingCache(true) - ->setFinder($finder) - ; diff --git a/vendor/spomky-labs/otphp/CODE_OF_CONDUCT.md b/vendor/spomky-labs/otphp/CODE_OF_CONDUCT.md deleted file mode 100644 index 4ec12c72b..000000000 --- a/vendor/spomky-labs/otphp/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,46 +0,0 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. - -## Our Standards - -Examples of behavior that contributes to creating a positive environment include: - -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery and unwelcome sexual attention or advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a professional setting - -## Our Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. - -## Scope - -This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at contact@spomky-labs.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. - -Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] - -[homepage]: http://contributor-covenant.org -[version]: http://contributor-covenant.org/version/1/4/ diff --git a/vendor/spomky-labs/otphp/README.md b/vendor/spomky-labs/otphp/README.md new file mode 100644 index 000000000..542de6fbd --- /dev/null +++ b/vendor/spomky-labs/otphp/README.md @@ -0,0 +1,43 @@ +TOTP / HOTP library in PHP +========================== + + + + +[](https://packagist.org/packages/spomky-labs/otphp) +[](https://packagist.org/packages/spomky-labs/otphp) +[](https://packagist.org/packages/spomky-labs/otphp) +[](https://packagist.org/packages/spomky-labs/otphp) + +A php library for generating one-time passwords according to [RFC 4226](https://datatracker.ietf.org/doc/html/rfc4226) (HOTP Algorithm) and [RFC 6238](https://datatracker.ietf.org/doc/html/rfc6238) (TOTP Algorithm) + +This library is compatible with Google Authenticator apps available for Android and iPhone. +It is also compatible with other applications such as [FreeOTP](https://play.google.com/store/apps/details?id=org.fedorahosted.freeotp) for example. + +# Documentation + +The documentation of this project is available in the [*doc* folder](doc/index.md). + +# Support + +I bring solutions to your problems and answer your questions. + +If you really love that project, and the work I have done or if you want I prioritize your issues, then you can help me out for a couple of :beers: or more! + +[Become a sponsor](https://github.com/sponsors/Spomky) + +Or + +[](https://www.patreon.com/FlorentMorselli) + +## Contributing + +Requests for new features, bug fixed and all other ideas to make this project useful are welcome. + +Please report all issues in [the repository bug tracker](hhttps://github.com/Spomky-Labs/otphp/issues). + +Also make sure to [follow these best practices](.github/CONTRIBUTING.md). + +## Licence + +This software is release under the [MIT licence](LICENSE). diff --git a/vendor/spomky-labs/otphp/SECURITY.md b/vendor/spomky-labs/otphp/SECURITY.md index 3c21578c6..706ad9a32 100644 --- a/vendor/spomky-labs/otphp/SECURITY.md +++ b/vendor/spomky-labs/otphp/SECURITY.md @@ -2,16 +2,15 @@ ## Supported Versions - -| Version | Supported | -| ------- | ------------------ | -| 11.0.x | :white_check_mark: | -| 10.0.x | :white_check_mark: | -| < 10.0 | :x: | +| Version | Supported | +| ------- |----------------------------------------| +| 11.0.x | :white_check_mark: | +| 10.0.x | :white_check_mark: (security fix only) | +| < 10.0 | :x: | ## Reporting a Vulnerability -Please send an email to `security@spomky-labs.com`. +Please email `security@spomky-labs.com`. If deemed necessary, you can encrypt your message using one of the following GPG key ``` diff --git a/vendor/spomky-labs/otphp/composer.json b/vendor/spomky-labs/otphp/composer.json index 583fbf9ef..bff1e48f3 100644 --- a/vendor/spomky-labs/otphp/composer.json +++ b/vendor/spomky-labs/otphp/composer.json @@ -16,23 +16,25 @@ } ], "require": { - "php": "^7.2|^8.0", + "php": ">=8.1", "ext-mbstring": "*", - "paragonie/constant_time_encoding": "^2.0", - "beberlei/assert": "^3.0", - "thecodingmachine/safe": "^0.1.14|^1.0|^2.0" + "paragonie/constant_time_encoding": "^2.0 || ^3.0", + "psr/clock": "^1.0", + "symfony/deprecation-contracts": "^3.2" }, "require-dev": { - "phpunit/phpunit": "^8.0", - "php-coveralls/php-coveralls": "^2.0", - "phpstan/phpstan": "^0.12", - "phpstan/phpstan-beberlei-assert": "^0.12", - "phpstan/phpstan-deprecation-rules": "^0.12", - "phpstan/phpstan-phpunit": "^0.12", - "phpstan/phpstan-strict-rules": "^0.12", - "thecodingmachine/phpstan-safe-rule": "^1.0 || ^2.0" - }, - "suggest": { + "ekino/phpstan-banned-code": "^1.0", + "infection/infection": "^0.26|^0.27|^0.28|^0.29", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^9.5.26|^10.0|^11.0", + "qossmic/deptrac-shim": "^1.0", + "rector/rector": "^1.0", + "symfony/phpunit-bridge": "^6.1|^7.0", + "symplify/easy-coding-standard": "^12.0" }, "autoload": { "psr-4": { "OTPHP\\": "src/" } @@ -40,11 +42,18 @@ "autoload-dev": { "psr-4": { "OTPHP\\Test\\": "tests/" } }, - "extra": { - "branch-alias": { - "v10.0": "10.0.x-dev", - "v9.0": "9.0.x-dev", - "v8.3": "8.3.x-dev" - } + "config": { + "allow-plugins": { + "phpstan/extension-installer": true, + "infection/extension-installer": true, + "composer/package-versions-deprecated": true, + "symfony/flex": true, + "symfony/runtime": true + }, + "optimize-autoloader": true, + "preferred-install": { + "*": "dist" + }, + "sort-packages": true } } diff --git a/vendor/spomky-labs/otphp/phpstan.neon b/vendor/spomky-labs/otphp/phpstan.neon deleted file mode 100644 index c9be89623..000000000 --- a/vendor/spomky-labs/otphp/phpstan.neon +++ /dev/null @@ -1,13 +0,0 @@ -parameters: - level: 7 - paths: - - src - - tests - ignoreErrors: - - '#Variable property access on \$this\(OTPHP\\OTP\)\.#' -includes: - - vendor/phpstan/phpstan-strict-rules/rules.neon - - vendor/phpstan/phpstan-phpunit/extension.neon - - vendor/phpstan/phpstan-deprecation-rules/rules.neon - - vendor/thecodingmachine/phpstan-safe-rule/phpstan-safe-rule.neon - - vendor/phpstan/phpstan-beberlei-assert/extension.neon diff --git a/vendor/spomky-labs/otphp/src/Factory.php b/vendor/spomky-labs/otphp/src/Factory.php index 70df63945..4bf41a84a 100644 --- a/vendor/spomky-labs/otphp/src/Factory.php +++ b/vendor/spomky-labs/otphp/src/Factory.php @@ -2,114 +2,103 @@ declare(strict_types=1); -/* - * The MIT License (MIT) - * - * Copyright (c) 2014-2019 Spomky-Labs - * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ - namespace OTPHP; -use Assert\Assertion; use InvalidArgumentException; -use function Safe\parse_url; -use function Safe\sprintf; +use Psr\Clock\ClockInterface; use Throwable; +use function assert; +use function count; /** * This class is used to load OTP object from a provisioning Uri. + * + * @see \OTPHP\Test\FactoryTest */ final class Factory implements FactoryInterface { - public static function loadFromProvisioningUri(string $uri): OTPInterface + public static function loadFromProvisioningUri(string $uri, ?ClockInterface $clock = null): OTPInterface { try { - $parsed_url = parse_url($uri); + $parsed_url = Url::fromString($uri); + $parsed_url->getScheme() === 'otpauth' || throw new InvalidArgumentException('Invalid scheme.'); } catch (Throwable $throwable) { throw new InvalidArgumentException('Not a valid OTP provisioning URI', $throwable->getCode(), $throwable); } - Assertion::isArray($parsed_url, 'Not a valid OTP provisioning URI'); - self::checkData($parsed_url); + if ($clock === null) { + trigger_deprecation( + 'spomky-labs/otphp', + '11.3.0', + 'The parameter "$clock" will become mandatory in 12.0.0. Please set a valid PSR Clock implementation instead of "null".' + ); + $clock = new InternalClock(); + } - $otp = self::createOTP($parsed_url); + $otp = self::createOTP($parsed_url, $clock); self::populateOTP($otp, $parsed_url); return $otp; } - /** - * @param array<string, mixed> $data - */ - private static function populateParameters(OTPInterface &$otp, array $data): void + private static function populateParameters(OTPInterface $otp, Url $data): void { - foreach ($data['query'] as $key => $value) { + foreach ($data->getQuery() as $key => $value) { $otp->setParameter($key, $value); } } - /** - * @param array<string, mixed> $data - */ - private static function populateOTP(OTPInterface &$otp, array $data): void + private static function populateOTP(OTPInterface $otp, Url $data): void { self::populateParameters($otp, $data); - $result = explode(':', rawurldecode(mb_substr($data['path'], 1))); + $result = explode(':', rawurldecode(mb_substr($data->getPath(), 1))); - if (2 > \count($result)) { + if (count($result) < 2) { $otp->setIssuerIncludedAsParameter(false); return; } - if (null !== $otp->getIssuer()) { - Assertion::eq($result[0], $otp->getIssuer(), 'Invalid OTP: invalid issuer in parameter'); + if ($otp->getIssuer() !== null) { + $result[0] === $otp->getIssuer() || throw new InvalidArgumentException( + 'Invalid OTP: invalid issuer in parameter' + ); $otp->setIssuerIncludedAsParameter(true); } - $otp->setIssuer($result[0]); - } - /** - * @param array<string, mixed> $data - */ - private static function checkData(array &$data): void - { - foreach (['scheme', 'host', 'path', 'query'] as $key) { - Assertion::keyExists($data, $key, 'Not a valid OTP provisioning URI'); - } - Assertion::eq('otpauth', $data['scheme'], 'Not a valid OTP provisioning URI'); - parse_str($data['query'], $data['query']); - Assertion::keyExists($data['query'], 'secret', 'Not a valid OTP provisioning URI'); + assert($result[0] !== ''); + + $otp->setIssuer($result[0]); } - /** - * @param array<string, mixed> $parsed_url - */ - private static function createOTP(array $parsed_url): OTPInterface + private static function createOTP(Url $parsed_url, ClockInterface $clock): OTPInterface { - switch ($parsed_url['host']) { + switch ($parsed_url->getHost()) { case 'totp': - $totp = TOTP::create($parsed_url['query']['secret']); - $totp->setLabel(self::getLabel($parsed_url['path'])); + $totp = TOTP::createFromSecret($parsed_url->getSecret(), $clock); + $totp->setLabel(self::getLabel($parsed_url->getPath())); return $totp; case 'hotp': - $hotp = HOTP::create($parsed_url['query']['secret']); - $hotp->setLabel(self::getLabel($parsed_url['path'])); + $hotp = HOTP::createFromSecret($parsed_url->getSecret()); + $hotp->setLabel(self::getLabel($parsed_url->getPath())); return $hotp; default: - throw new InvalidArgumentException(sprintf('Unsupported "%s" OTP type', $parsed_url['host'])); + throw new InvalidArgumentException(sprintf('Unsupported "%s" OTP type', $parsed_url->getHost())); } } + /** + * @param non-empty-string $data + * @return non-empty-string + */ private static function getLabel(string $data): string { $result = explode(':', rawurldecode(mb_substr($data, 1))); + $label = count($result) === 2 ? $result[1] : $result[0]; + assert($label !== ''); - return 2 === \count($result) ? $result[1] : $result[0]; + return $label; } } diff --git a/vendor/spomky-labs/otphp/src/FactoryInterface.php b/vendor/spomky-labs/otphp/src/FactoryInterface.php index 00acc2d04..dd14e45f9 100644 --- a/vendor/spomky-labs/otphp/src/FactoryInterface.php +++ b/vendor/spomky-labs/otphp/src/FactoryInterface.php @@ -2,22 +2,15 @@ declare(strict_types=1); -/* - * The MIT License (MIT) - * - * Copyright (c) 2014-2019 Spomky-Labs - * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ - namespace OTPHP; interface FactoryInterface { /** - * This method is the unique public method of the class. - * It can load a provisioning Uri and convert it into an OTP object. + * This method is the unique public method of the class. It can load a provisioning Uri and convert it into an OTP + * object. + * + * @param non-empty-string $uri */ public static function loadFromProvisioningUri(string $uri): OTPInterface; } diff --git a/vendor/spomky-labs/otphp/src/HOTP.php b/vendor/spomky-labs/otphp/src/HOTP.php index a2f4a2395..835de35f3 100644 --- a/vendor/spomky-labs/otphp/src/HOTP.php +++ b/vendor/spomky-labs/otphp/src/HOTP.php @@ -2,60 +2,78 @@ declare(strict_types=1); -/* - * The MIT License (MIT) - * - * Copyright (c) 2014-2019 Spomky-Labs - * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ - namespace OTPHP; -use Assert\Assertion; +use InvalidArgumentException; +use function is_int; +/** + * @see \OTPHP\Test\HOTPTest + */ final class HOTP extends OTP implements HOTPInterface { - protected function __construct(?string $secret, int $counter, string $digest, int $digits) - { - parent::__construct($secret, $digest, $digits); - $this->setCounter($counter); + private const DEFAULT_WINDOW = 0; + + public static function create( + null|string $secret = null, + int $counter = self::DEFAULT_COUNTER, + string $digest = self::DEFAULT_DIGEST, + int $digits = self::DEFAULT_DIGITS + ): self { + $htop = $secret !== null + ? self::createFromSecret($secret) + : self::generate() + ; + $htop->setCounter($counter); + $htop->setDigest($digest); + $htop->setDigits($digits); + + return $htop; } - public static function create(?string $secret = null, int $counter = 0, string $digest = 'sha1', int $digits = 6): HOTPInterface + public static function createFromSecret(string $secret): self { - return new self($secret, $counter, $digest, $digits); + $htop = new self($secret); + $htop->setCounter(self::DEFAULT_COUNTER); + $htop->setDigest(self::DEFAULT_DIGEST); + $htop->setDigits(self::DEFAULT_DIGITS); + + return $htop; } - protected function setCounter(int $counter): void + public static function generate(): self { - $this->setParameter('counter', $counter); + return self::createFromSecret(self::generateSecret()); } + /** + * @return 0|positive-int + */ public function getCounter(): int { - return $this->getParameter('counter'); - } + $value = $this->getParameter('counter'); + (is_int($value) && $value >= 0) || throw new InvalidArgumentException('Invalid "counter" parameter.'); - private function updateCounter(int $counter): void - { - $this->setCounter($counter); + return $value; } public function getProvisioningUri(): string { - return $this->generateURI('hotp', ['counter' => $this->getCounter()]); + return $this->generateURI('hotp', [ + 'counter' => $this->getCounter(), + ]); } /** * If the counter is not provided, the OTP is verified at the actual counter. + * + * @param null|0|positive-int $counter */ - public function verify(string $otp, ?int $counter = null, ?int $window = null): bool + public function verify(string $otp, null|int $counter = null, null|int $window = null): bool { - Assertion::greaterOrEqualThan($counter, 0, 'The counter must be at least 0.'); + $counter >= 0 || throw new InvalidArgumentException('The counter must be at least 0.'); - if (null === $counter) { + if ($counter === null) { $counter = $this->getCounter(); } elseif ($counter < $this->getCounter()) { return false; @@ -64,12 +82,45 @@ final class HOTP extends OTP implements HOTPInterface return $this->verifyOtpWithWindow($otp, $counter, $window); } - private function getWindow(?int $window): int + public function setCounter(int $counter): void + { + $this->setParameter('counter', $counter); + } + + /** + * @return array<non-empty-string, callable> + */ + protected function getParameterMap(): array + { + return [...parent::getParameterMap(), ...[ + 'counter' => static function (mixed $value): int { + $value = (int) $value; + $value >= 0 || throw new InvalidArgumentException('Counter must be at least 0.'); + + return $value; + }, + ]]; + } + + private function updateCounter(int $counter): void + { + $this->setCounter($counter); + } + + /** + * @param null|0|positive-int $window + */ + private function getWindow(null|int $window): int { - return abs($window ?? 0); + return abs($window ?? self::DEFAULT_WINDOW); } - private function verifyOtpWithWindow(string $otp, int $counter, ?int $window): bool + /** + * @param non-empty-string $otp + * @param 0|positive-int $counter + * @param null|0|positive-int $window + */ + private function verifyOtpWithWindow(string $otp, int $counter, null|int $window): bool { $window = $this->getWindow($window); @@ -83,21 +134,4 @@ final class HOTP extends OTP implements HOTPInterface return false; } - - /** - * @return array<string, mixed> - */ - protected function getParameterMap(): array - { - $v = array_merge( - parent::getParameterMap(), - ['counter' => function ($value): int { - Assertion::greaterOrEqualThan((int) $value, 0, 'Counter must be at least 0.'); - - return (int) $value; - }] - ); - - return $v; - } } diff --git a/vendor/spomky-labs/otphp/src/HOTPInterface.php b/vendor/spomky-labs/otphp/src/HOTPInterface.php index 336ce1055..915569a03 100644 --- a/vendor/spomky-labs/otphp/src/HOTPInterface.php +++ b/vendor/spomky-labs/otphp/src/HOTPInterface.php @@ -2,28 +2,35 @@ declare(strict_types=1); -/* - * The MIT License (MIT) - * - * Copyright (c) 2014-2019 Spomky-Labs - * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ - namespace OTPHP; interface HOTPInterface extends OTPInterface { + public const DEFAULT_COUNTER = 0; + /** * The initial counter (a positive integer). */ public function getCounter(): int; /** - * Create a new TOTP object. + * Create a new HOTP object. * * If the secret is null, a random 64 bytes secret will be generated. + * + * @param null|non-empty-string $secret + * @param 0|positive-int $counter + * @param non-empty-string $digest + * @param positive-int $digits + * + * @deprecated Deprecated since v11.1, use ::createFromSecret or ::generate instead */ - public static function create(?string $secret = null, int $counter = 0, string $digest = 'sha1', int $digits = 6): self; + public static function create( + null|string $secret = null, + int $counter = 0, + string $digest = 'sha1', + int $digits = 6 + ): self; + + public function setCounter(int $counter): void; } diff --git a/vendor/spomky-labs/otphp/src/InternalClock.php b/vendor/spomky-labs/otphp/src/InternalClock.php new file mode 100644 index 000000000..8be469318 --- /dev/null +++ b/vendor/spomky-labs/otphp/src/InternalClock.php @@ -0,0 +1,19 @@ +<?php + +declare(strict_types=1); + +namespace OTPHP; + +use DateTimeImmutable; +use Psr\Clock\ClockInterface; + +/** + * @internal + */ +final class InternalClock implements ClockInterface +{ + public function now(): DateTimeImmutable + { + return new DateTimeImmutable(); + } +} diff --git a/vendor/spomky-labs/otphp/src/OTP.php b/vendor/spomky-labs/otphp/src/OTP.php index 932bcf97e..f4c242c8f 100644 --- a/vendor/spomky-labs/otphp/src/OTP.php +++ b/vendor/spomky-labs/otphp/src/OTP.php @@ -2,32 +2,30 @@ declare(strict_types=1); -/* - * The MIT License (MIT) - * - * Copyright (c) 2014-2019 Spomky-Labs - * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ - namespace OTPHP; -use Assert\Assertion; +use Exception; +use InvalidArgumentException; use ParagonIE\ConstantTime\Base32; use RuntimeException; -use function Safe\ksort; -use function Safe\sprintf; +use function assert; +use function chr; +use function count; +use function is_string; +use const STR_PAD_LEFT; abstract class OTP implements OTPInterface { use ParameterTrait; - protected function __construct(?string $secret, string $digest, int $digits) + private const DEFAULT_SECRET_SIZE = 64; + + /** + * @param non-empty-string $secret + */ + protected function __construct(string $secret) { $this->setSecret($secret); - $this->setDigest($digest); - $this->setDigits($digits); } public function getQrCodeUri(string $uri, string $placeholder): string @@ -38,32 +36,52 @@ abstract class OTP implements OTPInterface } /** + * @param 0|positive-int $input + */ + public function at(int $input): string + { + return $this->generateOTP($input); + } + + /** + * @return non-empty-string + */ + final protected static function generateSecret(): string + { + return Base32::encodeUpper(random_bytes(self::DEFAULT_SECRET_SIZE)); + } + + /** * The OTP at the specified input. + * + * @param 0|positive-int $input + * + * @return non-empty-string */ protected function generateOTP(int $input): string { $hash = hash_hmac($this->getDigest(), $this->intToByteString($input), $this->getDecodedSecret(), true); + $unpacked = unpack('C*', $hash); + $unpacked !== false || throw new InvalidArgumentException('Invalid data.'); + $hmac = array_values($unpacked); - $hmac = array_values(unpack('C*', $hash)); - - $offset = ($hmac[\count($hmac) - 1] & 0xF); - $code = ($hmac[$offset + 0] & 0x7F) << 24 | ($hmac[$offset + 1] & 0xFF) << 16 | ($hmac[$offset + 2] & 0xFF) << 8 | ($hmac[$offset + 3] & 0xFF); + $offset = ($hmac[count($hmac) - 1] & 0xF); + $code = ($hmac[$offset] & 0x7F) << 24 | ($hmac[$offset + 1] & 0xFF) << 16 | ($hmac[$offset + 2] & 0xFF) << 8 | ($hmac[$offset + 3] & 0xFF); $otp = $code % (10 ** $this->getDigits()); return str_pad((string) $otp, $this->getDigits(), '0', STR_PAD_LEFT); } - public function at(int $timestamp): string - { - return $this->generateOTP($timestamp); - } - /** - * @param array<string, mixed> $options + * @param array<non-empty-string, mixed> $options */ protected function filterOptions(array &$options): void { - foreach (['algorithm' => 'sha1', 'period' => 30, 'digits' => 6] as $key => $default) { + foreach ([ + 'algorithm' => 'sha1', + 'period' => 30, + 'digits' => 6, + ] as $key => $default) { if (isset($options[$key]) && $default === $options[$key]) { unset($options[$key]); } @@ -73,42 +91,60 @@ abstract class OTP implements OTPInterface } /** - * @param array<string, mixed> $options + * @param non-empty-string $type + * @param array<non-empty-string, mixed> $options + * + * @return non-empty-string */ protected function generateURI(string $type, array $options): string { $label = $this->getLabel(); - Assertion::string($label, 'The label is not set.'); - Assertion::false($this->hasColon($label), 'Label must not contain a colon.'); - $options = array_merge($options, $this->getParameters()); + is_string($label) || throw new InvalidArgumentException('The label is not set.'); + $this->hasColon($label) === false || throw new InvalidArgumentException('Label must not contain a colon.'); + $options = [...$options, ...$this->getParameters()]; $this->filterOptions($options); - $params = str_replace(['+', '%7E'], ['%20', '~'], http_build_query($options)); + $params = str_replace(['+', '%7E'], ['%20', '~'], http_build_query($options, '', '&')); + + return sprintf( + 'otpauth://%s/%s?%s', + $type, + rawurlencode(($this->getIssuer() !== null ? $this->getIssuer() . ':' : '') . $label), + $params + ); + } - return sprintf('otpauth://%s/%s?%s', $type, rawurlencode((null !== $this->getIssuer() ? $this->getIssuer().':' : '').$label), $params); + /** + * @param non-empty-string $safe + * @param non-empty-string $user + */ + protected function compareOTP(string $safe, string $user): bool + { + return hash_equals($safe, $user); } + /** + * @return non-empty-string + */ private function getDecodedSecret(): string { try { - return Base32::decodeUpper($this->getSecret()); - } catch (\Exception $e) { + $decoded = Base32::decodeUpper($this->getSecret()); + } catch (Exception) { throw new RuntimeException('Unable to decode the secret. Is it correctly base32 encoded?'); } + assert($decoded !== ''); + + return $decoded; } private function intToByteString(int $int): string { $result = []; - while (0 !== $int) { - $result[] = \chr($int & 0xFF); + while ($int !== 0) { + $result[] = chr($int & 0xFF); $int >>= 8; } - return str_pad(implode(array_reverse($result)), 8, "\000", STR_PAD_LEFT); - } - - protected function compareOTP(string $safe, string $user): bool - { - return hash_equals($safe, $user); + return str_pad(implode('', array_reverse($result)), 8, "\000", STR_PAD_LEFT); } } diff --git a/vendor/spomky-labs/otphp/src/OTPInterface.php b/vendor/spomky-labs/otphp/src/OTPInterface.php index 66e163d5d..39ce4acd0 100644 --- a/vendor/spomky-labs/otphp/src/OTPInterface.php +++ b/vendor/spomky-labs/otphp/src/OTPInterface.php @@ -2,50 +2,80 @@ declare(strict_types=1); -/* - * The MIT License (MIT) - * - * Copyright (c) 2014-2019 Spomky-Labs - * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ - namespace OTPHP; interface OTPInterface { + public const DEFAULT_DIGITS = 6; + + public const DEFAULT_DIGEST = 'sha1'; + /** - * @return string Return the OTP at the specified timestamp + * Create a OTP object from an existing secret. + * + * @param non-empty-string $secret */ - public function at(int $timestamp): string; + public static function createFromSecret(string $secret): self; /** - * Verify that the OTP is valid with the specified input. - * If no input is provided, the input is set to a default value or false is returned. + * Create a new OTP object. A random 64 bytes secret will be generated. */ - public function verify(string $otp, ?int $input = null, ?int $window = null): bool; + public static function generate(): self; /** - * @return string The secret of the OTP + * @param non-empty-string $secret + */ + public function setSecret(string $secret): void; + + public function setDigits(int $digits): void; + + /** + * @param non-empty-string $digest + */ + public function setDigest(string $digest): void; + + /** + * Generate the OTP at the specified input. + * + * @param 0|positive-int $input + * + * @return non-empty-string Return the OTP at the specified timestamp + */ + public function at(int $input): string; + + /** + * Verify that the OTP is valid with the specified input. If no input is provided, the input is set to a default + * value or false is returned. + * + * @param non-empty-string $otp + * @param null|0|positive-int $input + * @param null|0|positive-int $window + */ + public function verify(string $otp, null|int $input = null, null|int $window = null): bool; + + /** + * @return non-empty-string The secret of the OTP */ public function getSecret(): string; /** - * @param string $label The label of the OTP + * @param non-empty-string $label The label of the OTP */ public function setLabel(string $label): void; /** - * @return string|null The label of the OTP + * @return non-empty-string|null The label of the OTP */ - public function getLabel(): ?string; + public function getLabel(): null|string; /** - * @return string|null The issuer + * @return non-empty-string|null The issuer */ public function getIssuer(): ?string; + /** + * @param non-empty-string $issuer + */ public function setIssuer(string $issuer): void; /** @@ -56,42 +86,47 @@ interface OTPInterface public function setIssuerIncludedAsParameter(bool $issuer_included_as_parameter): void; /** - * @return int Number of digits in the OTP + * @return positive-int Number of digits in the OTP */ public function getDigits(): int; /** - * @return string Digest algorithm used to calculate the OTP. Possible values are 'md5', 'sha1', 'sha256' and 'sha512' + * @return non-empty-string Digest algorithm used to calculate the OTP. Possible values are 'md5', 'sha1', 'sha256' and 'sha512' */ public function getDigest(): string; /** - * @return mixed|null + * @param non-empty-string $parameter */ - public function getParameter(string $parameter); + public function getParameter(string $parameter): mixed; + /** + * @param non-empty-string $parameter + */ public function hasParameter(string $parameter): bool; /** - * @return array<string, mixed> + * @return array<non-empty-string, mixed> */ public function getParameters(): array; /** - * @param mixed|null $value + * @param non-empty-string $parameter */ - public function setParameter(string $parameter, $value): void; + public function setParameter(string $parameter, mixed $value): void; /** * Get the provisioning URI. + * + * @return non-empty-string */ public function getProvisioningUri(): string; /** * Get the provisioning URI. * - * @param string $uri The Uri of the QRCode generator with all parameters. This Uri MUST contain a placeholder that will be replaced by the method. - * @param string $placeholder the placeholder to be replaced in the QR Code generator URI + * @param non-empty-string $uri The Uri of the QRCode generator with all parameters. This Uri MUST contain a placeholder that will be replaced by the method. + * @param non-empty-string $placeholder the placeholder to be replaced in the QR Code generator URI */ public function getQrCodeUri(string $uri, string $placeholder): string; } diff --git a/vendor/spomky-labs/otphp/src/ParameterTrait.php b/vendor/spomky-labs/otphp/src/ParameterTrait.php index 326109da3..dc92861c4 100644 --- a/vendor/spomky-labs/otphp/src/ParameterTrait.php +++ b/vendor/spomky-labs/otphp/src/ParameterTrait.php @@ -2,52 +2,42 @@ declare(strict_types=1); -/* - * The MIT License (MIT) - * - * Copyright (c) 2014-2019 Spomky-Labs - * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ - namespace OTPHP; -use Assert\Assertion; use InvalidArgumentException; -use ParagonIE\ConstantTime\Base32; -use function Safe\sprintf; +use function array_key_exists; +use function assert; +use function in_array; +use function is_int; +use function is_string; trait ParameterTrait { /** - * @var array<string, mixed> + * @var array<non-empty-string, mixed> */ - private $parameters = []; + private array $parameters = []; /** - * @var string|null + * @var non-empty-string|null */ - private $issuer; + private null|string $issuer = null; /** - * @var string|null + * @var non-empty-string|null */ - private $label; + private null|string $label = null; - /** - * @var bool - */ - private $issuer_included_as_parameter = true; + private bool $issuer_included_as_parameter = true; /** - * @return array<string, mixed> + * @return array<non-empty-string, mixed> */ public function getParameters(): array { $parameters = $this->parameters; - if (null !== $this->getIssuer() && true === $this->isIssuerIncludedAsParameter()) { + if ($this->getIssuer() !== null && $this->isIssuerIncludedAsParameter() === true) { $parameters['issuer'] = $this->getIssuer(); } @@ -56,15 +46,13 @@ trait ParameterTrait public function getSecret(): string { - return $this->getParameter('secret'); - } + $value = $this->getParameter('secret'); + (is_string($value) && $value !== '') || throw new InvalidArgumentException('Invalid "secret" parameter.'); - public function setSecret(?string $secret): void - { - $this->setParameter('secret', $secret); + return $value; } - public function getLabel(): ?string + public function getLabel(): null|string { return $this->label; } @@ -74,7 +62,7 @@ trait ParameterTrait $this->setParameter('label', $label); } - public function getIssuer(): ?string + public function getIssuer(): null|string { return $this->issuer; } @@ -96,30 +84,26 @@ trait ParameterTrait public function getDigits(): int { - return $this->getParameter('digits'); - } + $value = $this->getParameter('digits'); + (is_int($value) && $value > 0) || throw new InvalidArgumentException('Invalid "digits" parameter.'); - private function setDigits(int $digits): void - { - $this->setParameter('digits', $digits); + return $value; } public function getDigest(): string { - return $this->getParameter('algorithm'); - } + $value = $this->getParameter('algorithm'); + (is_string($value) && $value !== '') || throw new InvalidArgumentException('Invalid "algorithm" parameter.'); - private function setDigest(string $digest): void - { - $this->setParameter('algorithm', $digest); + return $value; } public function hasParameter(string $parameter): bool { - return \array_key_exists($parameter, $this->parameters); + return array_key_exists($parameter, $this->parameters); } - public function getParameter(string $parameter) + public function getParameter(string $parameter): mixed { if ($this->hasParameter($parameter)) { return $this->getParameters()[$parameter]; @@ -128,65 +112,85 @@ trait ParameterTrait throw new InvalidArgumentException(sprintf('Parameter "%s" does not exist', $parameter)); } - public function setParameter(string $parameter, $value): void + public function setParameter(string $parameter, mixed $value): void { $map = $this->getParameterMap(); - if (true === \array_key_exists($parameter, $map)) { + if (array_key_exists($parameter, $map) === true) { $callback = $map[$parameter]; $value = $callback($value); } if (property_exists($this, $parameter)) { - $this->$parameter = $value; + $this->{$parameter} = $value; } else { $this->parameters[$parameter] = $value; } } + public function setSecret(string $secret): void + { + $this->setParameter('secret', $secret); + } + + public function setDigits(int $digits): void + { + $this->setParameter('digits', $digits); + } + + public function setDigest(string $digest): void + { + $this->setParameter('algorithm', $digest); + } + /** - * @return array<string, mixed> + * @return array<non-empty-string, callable> */ protected function getParameterMap(): array { return [ - 'label' => function ($value) { - Assertion::false($this->hasColon($value), 'Label must not contain a colon.'); + 'label' => function (string $value): string { + assert($value !== ''); + $this->hasColon($value) === false || throw new InvalidArgumentException( + 'Label must not contain a colon.' + ); return $value; }, - 'secret' => function ($value): string { - if (null === $value) { - $value = Base32::encodeUpper(random_bytes(64)); - } - $value = trim(mb_strtoupper($value), '='); - - return $value; - }, - 'algorithm' => function ($value): string { + 'secret' => static fn (string $value): string => mb_strtoupper(trim($value, '=')), + 'algorithm' => static function (string $value): string { $value = mb_strtolower($value); - Assertion::inArray($value, hash_algos(), sprintf('The "%s" digest is not supported.', $value)); + in_array($value, hash_algos(), true) || throw new InvalidArgumentException(sprintf( + 'The "%s" digest is not supported.', + $value + )); return $value; }, - 'digits' => function ($value): int { - Assertion::greaterThan($value, 0, 'Digits must be at least 1.'); + 'digits' => static function ($value): int { + $value > 0 || throw new InvalidArgumentException('Digits must be at least 1.'); return (int) $value; }, - 'issuer' => function ($value) { - Assertion::false($this->hasColon($value), 'Issuer must not contain a colon.'); + 'issuer' => function (string $value): string { + assert($value !== ''); + $this->hasColon($value) === false || throw new InvalidArgumentException( + 'Issuer must not contain a colon.' + ); return $value; }, ]; } + /** + * @param non-empty-string $value + */ private function hasColon(string $value): bool { $colons = [':', '%3A', '%3a']; foreach ($colons as $colon) { - if (false !== mb_strpos($value, $colon)) { + if (str_contains($value, $colon)) { return true; } } diff --git a/vendor/spomky-labs/otphp/src/TOTP.php b/vendor/spomky-labs/otphp/src/TOTP.php index 588b37f17..035e04f95 100644 --- a/vendor/spomky-labs/otphp/src/TOTP.php +++ b/vendor/spomky-labs/otphp/src/TOTP.php @@ -2,158 +2,214 @@ declare(strict_types=1); -/* - * The MIT License (MIT) - * - * Copyright (c) 2014-2019 Spomky-Labs - * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ - namespace OTPHP; -use Assert\Assertion; -use function Safe\ksort; +use InvalidArgumentException; +use Psr\Clock\ClockInterface; +use function assert; +use function is_int; +/** + * @see \OTPHP\Test\TOTPTest + */ final class TOTP extends OTP implements TOTPInterface { - protected function __construct(?string $secret, int $period, string $digest, int $digits, int $epoch = 0) + private readonly ClockInterface $clock; + + public function __construct(string $secret, ?ClockInterface $clock = null) { - parent::__construct($secret, $digest, $digits); - $this->setPeriod($period); - $this->setEpoch($epoch); + parent::__construct($secret); + if ($clock === null) { + trigger_deprecation( + 'spomky-labs/otphp', + '11.3.0', + 'The parameter "$clock" will become mandatory in 12.0.0. Please set a valid PSR Clock implementation instead of "null".' + ); + $clock = new InternalClock(); + } + + $this->clock = $clock; } - public static function create(?string $secret = null, int $period = 30, string $digest = 'sha1', int $digits = 6, int $epoch = 0): TOTPInterface - { - return new self($secret, $period, $digest, $digits, $epoch); + public static function create( + null|string $secret = null, + int $period = self::DEFAULT_PERIOD, + string $digest = self::DEFAULT_DIGEST, + int $digits = self::DEFAULT_DIGITS, + int $epoch = self::DEFAULT_EPOCH, + ?ClockInterface $clock = null + ): self { + $totp = $secret !== null + ? self::createFromSecret($secret, $clock) + : self::generate($clock) + ; + $totp->setPeriod($period); + $totp->setDigest($digest); + $totp->setDigits($digits); + $totp->setEpoch($epoch); + + return $totp; } - protected function setPeriod(int $period): void + public static function createFromSecret(string $secret, ?ClockInterface $clock = null): self { - $this->setParameter('period', $period); + $totp = new self($secret, $clock); + $totp->setPeriod(self::DEFAULT_PERIOD); + $totp->setDigest(self::DEFAULT_DIGEST); + $totp->setDigits(self::DEFAULT_DIGITS); + $totp->setEpoch(self::DEFAULT_EPOCH); + + return $totp; } - public function getPeriod(): int + public static function generate(?ClockInterface $clock = null): self { - return $this->getParameter('period'); + return self::createFromSecret(self::generateSecret(), $clock); } - private function setEpoch(int $epoch): void + public function getPeriod(): int { - $this->setParameter('epoch', $epoch); + $value = $this->getParameter('period'); + (is_int($value) && $value > 0) || throw new InvalidArgumentException('Invalid "period" parameter.'); + + return $value; } public function getEpoch(): int { - return $this->getParameter('epoch'); - } + $value = $this->getParameter('epoch'); + (is_int($value) && $value >= 0) || throw new InvalidArgumentException('Invalid "epoch" parameter.'); - public function at(int $timestamp): string - { - return $this->generateOTP($this->timecode($timestamp)); + return $value; } - public function now(): string + public function expiresIn(): int { - return $this->at(time()); + $period = $this->getPeriod(); + + return $period - ($this->clock->now()->getTimestamp() % $this->getPeriod()); } /** - * If no timestamp is provided, the OTP is verified at the actual timestamp. + * The OTP at the specified input. + * + * @param 0|positive-int $input */ - public function verify(string $otp, ?int $timestamp = null, ?int $window = null): bool + public function at(int $input): string { - $timestamp = $this->getTimestamp($timestamp); + return $this->generateOTP($this->timecode($input)); + } - if (null === $window) { - return $this->compareOTP($this->at($timestamp), $otp); - } + public function now(): string + { + $timestamp = $this->clock->now() + ->getTimestamp(); + assert($timestamp >= 0, 'The timestamp must return a positive integer.'); - return $this->verifyOtpWithWindow($otp, $timestamp, $window); + return $this->at($timestamp); } - private function verifyOtpWithWindow(string $otp, int $timestamp, int $window): bool + /** + * If no timestamp is provided, the OTP is verified at the actual timestamp. When used, the leeway parameter will + * allow time drift. The passed value is in seconds. + * + * @param 0|positive-int $timestamp + * @param null|0|positive-int $leeway + */ + public function verify(string $otp, null|int $timestamp = null, null|int $leeway = null): bool { - $window = abs($window); - - for ($i = 0; $i <= $window; ++$i) { - $next = $i * $this->getPeriod() + $timestamp; - $previous = -$i * $this->getPeriod() + $timestamp; - $valid = $this->compareOTP($this->at($next), $otp) || - $this->compareOTP($this->at($previous), $otp); + $timestamp ??= $this->clock->now() + ->getTimestamp(); + $timestamp >= 0 || throw new InvalidArgumentException('Timestamp must be at least 0.'); - if ($valid) { - return true; - } + if ($leeway === null) { + return $this->compareOTP($this->at($timestamp), $otp); } - return false; - } - - private function getTimestamp(?int $timestamp): int - { - $timestamp = $timestamp ?? time(); - Assertion::greaterOrEqualThan($timestamp, 0, 'Timestamp must be at least 0.'); + $leeway = abs($leeway); + $leeway < $this->getPeriod() || throw new InvalidArgumentException( + 'The leeway must be lower than the TOTP period' + ); + $timestampMinusLeeway = $timestamp - $leeway; + $timestampMinusLeeway >= 0 || throw new InvalidArgumentException( + 'The timestamp must be greater than or equal to the leeway.' + ); - return $timestamp; + return $this->compareOTP($this->at($timestampMinusLeeway), $otp) + || $this->compareOTP($this->at($timestamp), $otp) + || $this->compareOTP($this->at($timestamp + $leeway), $otp); } public function getProvisioningUri(): string { $params = []; - if (30 !== $this->getPeriod()) { + if ($this->getPeriod() !== 30) { $params['period'] = $this->getPeriod(); } - if (0 !== $this->getEpoch()) { + if ($this->getEpoch() !== 0) { $params['epoch'] = $this->getEpoch(); } return $this->generateURI('totp', $params); } - private function timecode(int $timestamp): int + public function setPeriod(int $period): void { - return (int) floor(($timestamp - $this->getEpoch()) / $this->getPeriod()); + $this->setParameter('period', $period); + } + + public function setEpoch(int $epoch): void + { + $this->setParameter('epoch', $epoch); } /** - * @return array<string, mixed> + * @return array<non-empty-string, callable> */ protected function getParameterMap(): array { - $v = array_merge( - parent::getParameterMap(), - [ - 'period' => function ($value): int { - Assertion::greaterThan((int) $value, 0, 'Period must be at least 1.'); - - return (int) $value; - }, - 'epoch' => function ($value): int { - Assertion::greaterOrEqualThan((int) $value, 0, 'Epoch must be greater than or equal to 0.'); - - return (int) $value; - }, - ] - ); - - return $v; + return [ + ...parent::getParameterMap(), + 'period' => static function ($value): int { + (int) $value > 0 || throw new InvalidArgumentException('Period must be at least 1.'); + + return (int) $value; + }, + 'epoch' => static function ($value): int { + (int) $value >= 0 || throw new InvalidArgumentException( + 'Epoch must be greater than or equal to 0.' + ); + + return (int) $value; + }, + ]; } /** - * @param array<string, mixed> $options + * @param array<non-empty-string, mixed> $options */ protected function filterOptions(array &$options): void { parent::filterOptions($options); - if (isset($options['epoch']) && 0 === $options['epoch']) { + if (isset($options['epoch']) && $options['epoch'] === 0) { unset($options['epoch']); } ksort($options); } + + /** + * @param 0|positive-int $timestamp + * + * @return 0|positive-int + */ + private function timecode(int $timestamp): int + { + $timecode = (int) floor(($timestamp - $this->getEpoch()) / $this->getPeriod()); + assert($timecode >= 0); + + return $timecode; + } } diff --git a/vendor/spomky-labs/otphp/src/TOTPInterface.php b/vendor/spomky-labs/otphp/src/TOTPInterface.php index a19fe7c0b..a79fedcce 100644 --- a/vendor/spomky-labs/otphp/src/TOTPInterface.php +++ b/vendor/spomky-labs/otphp/src/TOTPInterface.php @@ -2,28 +2,41 @@ declare(strict_types=1); -/* - * The MIT License (MIT) - * - * Copyright (c) 2014-2019 Spomky-Labs - * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ - namespace OTPHP; interface TOTPInterface extends OTPInterface { + public const DEFAULT_PERIOD = 30; + + public const DEFAULT_EPOCH = 0; + /** * Create a new TOTP object. * * If the secret is null, a random 64 bytes secret will be generated. + * + * @param null|non-empty-string $secret + * @param positive-int $period + * @param non-empty-string $digest + * @param positive-int $digits + * + * @deprecated Deprecated since v11.1, use ::createFromSecret or ::generate instead */ - public static function create(?string $secret = null, int $period = 30, string $digest = 'sha1', int $digits = 6): self; + public static function create( + null|string $secret = null, + int $period = self::DEFAULT_PERIOD, + string $digest = self::DEFAULT_DIGEST, + int $digits = self::DEFAULT_DIGITS + ): self; + + public function setPeriod(int $period): void; + + public function setEpoch(int $epoch): void; /** * Return the TOTP at the current time. + * + * @return non-empty-string */ public function now(): string; @@ -32,5 +45,7 @@ interface TOTPInterface extends OTPInterface */ public function getPeriod(): int; + public function expiresIn(): int; + public function getEpoch(): int; } diff --git a/vendor/spomky-labs/otphp/src/Url.php b/vendor/spomky-labs/otphp/src/Url.php new file mode 100644 index 000000000..e88cd6d29 --- /dev/null +++ b/vendor/spomky-labs/otphp/src/Url.php @@ -0,0 +1,102 @@ +<?php + +declare(strict_types=1); + +namespace OTPHP; + +use InvalidArgumentException; +use function array_key_exists; +use function is_string; + +/** + * @internal + */ +final class Url +{ + /** + * @param non-empty-string $scheme + * @param non-empty-string $host + * @param non-empty-string $path + * @param non-empty-string $secret + * @param array<non-empty-string, mixed> $query + */ + public function __construct( + private readonly string $scheme, + private readonly string $host, + private readonly string $path, + private readonly string $secret, + private readonly array $query + ) { + } + + /** + * @return non-empty-string + */ + public function getScheme(): string + { + return $this->scheme; + } + + /** + * @return non-empty-string + */ + public function getHost(): string + { + return $this->host; + } + + /** + * @return non-empty-string + */ + public function getPath(): string + { + return $this->path; + } + + /** + * @return non-empty-string + */ + public function getSecret(): string + { + return $this->secret; + } + + /** + * @return array<non-empty-string, mixed> + */ + public function getQuery(): array + { + return $this->query; + } + + /** + * @param non-empty-string $uri + */ + public static function fromString(string $uri): self + { + $parsed_url = parse_url($uri); + $parsed_url !== false || throw new InvalidArgumentException('Invalid URI.'); + foreach (['scheme', 'host', 'path', 'query'] as $key) { + array_key_exists($key, $parsed_url) || throw new InvalidArgumentException( + 'Not a valid OTP provisioning URI' + ); + } + $scheme = $parsed_url['scheme'] ?? null; + $host = $parsed_url['host'] ?? null; + $path = $parsed_url['path'] ?? null; + $query = $parsed_url['query'] ?? null; + $scheme === 'otpauth' || throw new InvalidArgumentException('Not a valid OTP provisioning URI'); + is_string($host) || throw new InvalidArgumentException('Invalid URI.'); + is_string($path) || throw new InvalidArgumentException('Invalid URI.'); + is_string($query) || throw new InvalidArgumentException('Invalid URI.'); + $parsedQuery = []; + parse_str($query, $parsedQuery); + array_key_exists('secret', $parsedQuery) || throw new InvalidArgumentException( + 'Not a valid OTP provisioning URI' + ); + $secret = $parsedQuery['secret']; + unset($parsedQuery['secret']); + + return new self($scheme, $host, $path, $secret, $parsedQuery); + } +} |