vendor/spomky-labs/otphp/src/TOTP.php line 19

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace OTPHP;
  4. use InvalidArgumentException;
  5. use Psr\Clock\ClockInterface;
  6. use function assert;
  7. use function is_int;
  8. /**
  9.  * @see \OTPHP\Test\TOTPTest
  10.  */
  11. final class TOTP extends OTP implements TOTPInterface
  12. {
  13.     private readonly ClockInterface $clock;
  14.     public function __construct(string $secret, ?ClockInterface $clock null)
  15.     {
  16.         parent::__construct($secret);
  17.         if ($clock === null) {
  18.             trigger_deprecation(
  19.                 'spomky-labs/otphp',
  20.                 '11.3.0',
  21.                 'The parameter "$clock" will become mandatory in 12.0.0. Please set a valid PSR Clock implementation instead of "null".'
  22.             );
  23.             $clock = new InternalClock();
  24.         }
  25.         $this->clock $clock;
  26.     }
  27.     public static function create(
  28.         null|string $secret null,
  29.         int $period self::DEFAULT_PERIOD,
  30.         string $digest self::DEFAULT_DIGEST,
  31.         int $digits self::DEFAULT_DIGITS,
  32.         int $epoch self::DEFAULT_EPOCH,
  33.         ?ClockInterface $clock null
  34.     ): self {
  35.         $totp $secret !== null
  36.             self::createFromSecret($secret$clock)
  37.             : self::generate($clock)
  38.         ;
  39.         $totp->setPeriod($period);
  40.         $totp->setDigest($digest);
  41.         $totp->setDigits($digits);
  42.         $totp->setEpoch($epoch);
  43.         return $totp;
  44.     }
  45.     public static function createFromSecret(string $secret, ?ClockInterface $clock null): self
  46.     {
  47.         $totp = new self($secret$clock);
  48.         $totp->setPeriod(self::DEFAULT_PERIOD);
  49.         $totp->setDigest(self::DEFAULT_DIGEST);
  50.         $totp->setDigits(self::DEFAULT_DIGITS);
  51.         $totp->setEpoch(self::DEFAULT_EPOCH);
  52.         return $totp;
  53.     }
  54.     public static function generate(?ClockInterface $clock null): self
  55.     {
  56.         return self::createFromSecret(self::generateSecret(), $clock);
  57.     }
  58.     public function getPeriod(): int
  59.     {
  60.         $value $this->getParameter('period');
  61.         (is_int($value) && $value 0) || throw new InvalidArgumentException('Invalid "period" parameter.');
  62.         return $value;
  63.     }
  64.     public function getEpoch(): int
  65.     {
  66.         $value $this->getParameter('epoch');
  67.         (is_int($value) && $value >= 0) || throw new InvalidArgumentException('Invalid "epoch" parameter.');
  68.         return $value;
  69.     }
  70.     public function expiresIn(): int
  71.     {
  72.         $period $this->getPeriod();
  73.         return $period - ($this->clock->now()->getTimestamp() % $this->getPeriod());
  74.     }
  75.     /**
  76.      * The OTP at the specified input.
  77.      *
  78.      * @param 0|positive-int $input
  79.      */
  80.     public function at(int $input): string
  81.     {
  82.         return $this->generateOTP($this->timecode($input));
  83.     }
  84.     public function now(): string
  85.     {
  86.         $timestamp $this->clock->now()
  87.             ->getTimestamp();
  88.         assert($timestamp >= 0'The timestamp must return a positive integer.');
  89.         return $this->at($timestamp);
  90.     }
  91.     /**
  92.      * If no timestamp is provided, the OTP is verified at the actual timestamp. When used, the leeway parameter will
  93.      * allow time drift. The passed value is in seconds.
  94.      *
  95.      * @param 0|positive-int $timestamp
  96.      * @param null|0|positive-int $leeway
  97.      */
  98.     public function verify(string $otpnull|int $timestamp nullnull|int $leeway null): bool
  99.     {
  100.         $timestamp ??= $this->clock->now()
  101.             ->getTimestamp();
  102.         $timestamp >= || throw new InvalidArgumentException('Timestamp must be at least 0.');
  103.         if ($leeway === null) {
  104.             return $this->compareOTP($this->at($timestamp), $otp);
  105.         }
  106.         $leeway abs($leeway);
  107.         $leeway $this->getPeriod() || throw new InvalidArgumentException(
  108.             'The leeway must be lower than the TOTP period'
  109.         );
  110.         $timestampMinusLeeway $timestamp $leeway;
  111.         $timestampMinusLeeway >= || throw new InvalidArgumentException(
  112.             'The timestamp must be greater than or equal to the leeway.'
  113.         );
  114.         return $this->compareOTP($this->at($timestampMinusLeeway), $otp)
  115.             || $this->compareOTP($this->at($timestamp), $otp)
  116.             || $this->compareOTP($this->at($timestamp $leeway), $otp);
  117.     }
  118.     public function getProvisioningUri(): string
  119.     {
  120.         $params = [];
  121.         if ($this->getPeriod() !== 30) {
  122.             $params['period'] = $this->getPeriod();
  123.         }
  124.         if ($this->getEpoch() !== 0) {
  125.             $params['epoch'] = $this->getEpoch();
  126.         }
  127.         return $this->generateURI('totp'$params);
  128.     }
  129.     public function setPeriod(int $period): void
  130.     {
  131.         $this->setParameter('period'$period);
  132.     }
  133.     public function setEpoch(int $epoch): void
  134.     {
  135.         $this->setParameter('epoch'$epoch);
  136.     }
  137.     /**
  138.      * @return array<non-empty-string, callable>
  139.      */
  140.     protected function getParameterMap(): array
  141.     {
  142.         return [
  143.             ...parent::getParameterMap(),
  144.             'period' => static function ($value): int {
  145.                 (int) $value || throw new InvalidArgumentException('Period must be at least 1.');
  146.                 return (int) $value;
  147.             },
  148.             'epoch' => static function ($value): int {
  149.                 (int) $value >= || throw new InvalidArgumentException(
  150.                     'Epoch must be greater than or equal to 0.'
  151.                 );
  152.                 return (int) $value;
  153.             },
  154.         ];
  155.     }
  156.     /**
  157.      * @param array<non-empty-string, mixed> $options
  158.      */
  159.     protected function filterOptions(array &$options): void
  160.     {
  161.         parent::filterOptions($options);
  162.         if (isset($options['epoch']) && $options['epoch'] === 0) {
  163.             unset($options['epoch']);
  164.         }
  165.         ksort($options);
  166.     }
  167.     /**
  168.      * @param 0|positive-int $timestamp
  169.      *
  170.      * @return 0|positive-int
  171.      */
  172.     private function timecode(int $timestamp): int
  173.     {
  174.         $timecode = (int) floor(($timestamp $this->getEpoch()) / $this->getPeriod());
  175.         assert($timecode >= 0);
  176.         return $timecode;
  177.     }
  178. }