diff options
| author | wn_ <invalid@email.com> | 2024-12-09 17:58:28 +0000 |
|---|---|---|
| committer | wn_ <invalid@email.com> | 2024-12-09 17:58:28 +0000 |
| commit | f6a8facfd4bfc40025c069eebc37094d826aff58 (patch) | |
| tree | 871aab0d8adafe736d954cae1783c260699c0ec3 /vendor/spomky-labs/otphp/src | |
| parent | cd2c10f9f71409df24fc74c1bbd7d5ddbf48d991 (diff) | |
Bump 'spomky-labs/otphp' to 11.3.x.
This is mainly for PHP 8.4 compatibility.
Diffstat (limited to 'vendor/spomky-labs/otphp/src')
| -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 |
11 files changed, 638 insertions, 348 deletions
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); + } +} |