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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body