Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.
   1  <?php
   2  
   3  declare(strict_types=1);
   4  
   5  /*
   6   * The MIT License (MIT)
   7   *
   8   * Copyright (c) 2014-2018 Spomky-Labs
   9   *
  10   * This software may be modified and distributed under the terms
  11   * of the MIT license.  See the LICENSE file for details.
  12   */
  13  
  14  namespace OTPHP;
  15  
  16  use Assert\Assertion;
  17  
  18  final class TOTP extends OTP implements TOTPInterface
  19  {
  20      /**
  21       * TOTP constructor.
  22       *
  23       * @param string|null $secret
  24       * @param int         $period
  25       * @param string      $digest
  26       * @param int         $digits
  27       * @param int         $epoch
  28       */
  29      protected function __construct($secret, int $period, string $digest, int $digits, int $epoch = 0)
  30      {
  31          parent::__construct($secret, $digest, $digits);
  32          $this->setPeriod($period);
  33          $this->setEpoch($epoch);
  34      }
  35  
  36      /**
  37       * TOTP constructor.
  38       *
  39       * @param string|null $secret
  40       * @param int         $period
  41       * @param string      $digest
  42       * @param int         $digits
  43       * @param int         $epoch
  44       *
  45       * @return self
  46       */
  47      public static function create($secret = null, int $period = 30, string $digest = 'sha1', int $digits = 6, int $epoch = 0): self
  48      {
  49          return new self($secret, $period, $digest, $digits, $epoch);
  50      }
  51  
  52      /**
  53       * @param int $period
  54       */
  55      protected function setPeriod(int $period)
  56      {
  57          $this->setParameter('period', $period);
  58      }
  59  
  60      /**
  61       * {@inheritdoc}
  62       */
  63      public function getPeriod(): int
  64      {
  65          return $this->getParameter('period');
  66      }
  67  
  68      /**
  69       * @param int $epoch
  70       */
  71      private function setEpoch(int $epoch)
  72      {
  73          $this->setParameter('epoch', $epoch);
  74      }
  75  
  76      /**
  77       * {@inheritdoc}
  78       */
  79      public function getEpoch(): int
  80      {
  81          return $this->getParameter('epoch');
  82      }
  83  
  84      /**
  85       * {@inheritdoc}
  86       */
  87      public function at(int $timestamp): string
  88      {
  89          return $this->generateOTP($this->timecode($timestamp));
  90      }
  91  
  92      /**
  93       * {@inheritdoc}
  94       */
  95      public function now(): string
  96      {
  97          return $this->at(time());
  98      }
  99  
 100      /**
 101       * If no timestamp is provided, the OTP is verified at the actual timestamp
 102       * {@inheritdoc}
 103       */
 104      public function verify(string $otp, $timestamp = null, $window = null): bool
 105      {
 106          $timestamp = $this->getTimestamp($timestamp);
 107  
 108          if (null === $window) {
 109              return $this->compareOTP($this->at($timestamp), $otp);
 110          }
 111  
 112          return $this->verifyOtpWithWindow($otp, $timestamp, $window);
 113      }
 114  
 115      /**
 116       * @param string $otp
 117       * @param int    $timestamp
 118       * @param int    $window
 119       *
 120       * @return bool
 121       */
 122      private function verifyOtpWithWindow(string $otp, int $timestamp, int $window): bool
 123      {
 124          $window = abs($window);
 125  
 126          for ($i = 0; $i <= $window; $i++) {
 127              $next = (int) $i * $this->getPeriod() + $timestamp;
 128              $previous = (int) -$i * $this->getPeriod() + $timestamp;
 129              $valid = $this->compareOTP($this->at($next), $otp) ||
 130                  $this->compareOTP($this->at($previous), $otp);
 131  
 132              if ($valid) {
 133                  return true;
 134              }
 135          }
 136  
 137          return false;
 138      }
 139  
 140      /**
 141       * @param int|null $timestamp
 142       *
 143       * @return int
 144       */
 145      private function getTimestamp($timestamp): int
 146      {
 147          $timestamp = $timestamp ?? time();
 148          Assertion::greaterOrEqualThan($timestamp, 0, 'Timestamp must be at least 0.');
 149  
 150          return (int) $timestamp;
 151      }
 152  
 153      /**
 154       * {@inheritdoc}
 155       */
 156      public function getProvisioningUri(): string
 157      {
 158          $params = [];
 159          if (30 !== $this->getPeriod()) {
 160              $params['period'] = $this->getPeriod();
 161          }
 162  
 163          if (0 !== $this->getEpoch()) {
 164              $params['epoch'] = $this->getEpoch();
 165          }
 166  
 167          return $this->generateURI('totp', $params);
 168      }
 169  
 170      /**
 171       * @param int $timestamp
 172       *
 173       * @return int
 174       */
 175      private function timecode(int $timestamp): int
 176      {
 177          return (int) floor(($timestamp - $this->getEpoch()) / $this->getPeriod());
 178      }
 179  
 180      /**
 181       * {@inheritdoc}
 182       */
 183      protected function getParameterMap(): array
 184      {
 185          $v = array_merge(
 186              parent::getParameterMap(),
 187              [
 188                  'period' => function ($value) {
 189                      Assertion::greaterThan((int) $value, 0, 'Period must be at least 1.');
 190  
 191                      return (int) $value;
 192                  },
 193                  'epoch' => function ($value) {
 194                      Assertion::greaterOrEqualThan((int) $value, 0, 'Epoch must be greater than or equal to 0.');
 195  
 196                      return (int) $value;
 197                  },
 198              ]
 199          );
 200  
 201          return $v;
 202      }
 203  
 204      /**
 205       * {@inheritdoc}
 206       */
 207      protected function filterOptions(array &$options)
 208      {
 209          parent::filterOptions($options);
 210  
 211          if (isset($options['epoch']) && 0 === $options['epoch']) {
 212              unset($options['epoch']);
 213          }
 214  
 215          ksort($options);
 216      }
 217  }