diff options
Diffstat (limited to 'vendor/spomky-labs/otphp/src/HOTP.php')
-rw-r--r-- | vendor/spomky-labs/otphp/src/HOTP.php | 124 |
1 files changed, 124 insertions, 0 deletions
diff --git a/vendor/spomky-labs/otphp/src/HOTP.php b/vendor/spomky-labs/otphp/src/HOTP.php new file mode 100644 index 000000000..aa5a22754 --- /dev/null +++ b/vendor/spomky-labs/otphp/src/HOTP.php @@ -0,0 +1,124 @@ +<?php + +declare(strict_types=1); + +namespace OTPHP; + +use InvalidArgumentException; +use function is_int; + +/** + * @see \OTPHP\Test\HOTPTest + */ +final class HOTP extends OTP implements HOTPInterface +{ + 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 createFromSecret(string $secret): self + { + $htop = new self($secret); + $htop->setCounter(self::DEFAULT_COUNTER); + $htop->setDigest(self::DEFAULT_DIGEST); + $htop->setDigits(self::DEFAULT_DIGITS); + + return $htop; + } + + public static function generate(): self + { + return self::createFromSecret(self::generateSecret()); + } + + public function getCounter(): int + { + $value = $this->getParameter('counter'); + is_int($value) || throw new InvalidArgumentException('Invalid "counter" parameter.'); + + return $value; + } + + public function getProvisioningUri(): string + { + return $this->generateURI('hotp', [ + 'counter' => $this->getCounter(), + ]); + } + + /** + * If the counter is not provided, the OTP is verified at the actual counter. + */ + public function verify(string $otp, null|int $counter = null, null|int $window = null): bool + { + $counter >= 0 || throw new InvalidArgumentException('The counter must be at least 0.'); + + if ($counter === null) { + $counter = $this->getCounter(); + } elseif ($counter < $this->getCounter()) { + return false; + } + + return $this->verifyOtpWithWindow($otp, $counter, $window); + } + + public function setCounter(int $counter): void + { + $this->setParameter('counter', $counter); + } + + /** + * @return array<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); + } + + private function getWindow(null|int $window): int + { + return abs($window ?? self::DEFAULT_WINDOW); + } + + private function verifyOtpWithWindow(string $otp, int $counter, null|int $window): bool + { + $window = $this->getWindow($window); + + for ($i = $counter; $i <= $counter + $window; ++$i) { + if ($this->compareOTP($this->at($i), $otp)) { + $this->updateCounter($i + 1); + + return true; + } + } + + return false; + } +} |