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/TOTP.php | |
| 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/TOTP.php')
| -rw-r--r-- | vendor/spomky-labs/otphp/src/TOTP.php | 218 |
1 files changed, 137 insertions, 81 deletions
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; + } } |