1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
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;
}
}
|