Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

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