summaryrefslogtreecommitdiff
path: root/vendor/spomky-labs/otphp/src/TOTP.php
diff options
context:
space:
mode:
authorwn_ <invalid@email.com>2024-12-09 17:58:28 +0000
committerwn_ <invalid@email.com>2024-12-09 17:58:28 +0000
commitf6a8facfd4bfc40025c069eebc37094d826aff58 (patch)
tree871aab0d8adafe736d954cae1783c260699c0ec3 /vendor/spomky-labs/otphp/src/TOTP.php
parentcd2c10f9f71409df24fc74c1bbd7d5ddbf48d991 (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.php218
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;
+ }
}