Manager::verify()   A
last analyzed

Complexity

Conditions 4
Paths 8

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 16
rs 9.7333
c 0
b 0
f 0
cc 4
nc 8
nop 4
1
<?php
2
3
/*
4
 * This file is part of the 2amigos/2fa-library project.
5
 *
6
 * (c) 2amigOS! <http://2amigos.us/>
7
 *
8
 * For the full copyright and license information, please view
9
 * the LICENSE file that was distributed with this source code.
10
 */
11
12
namespace Da\TwoFA;
13
14
use Da\TwoFA\Contracts\TotpEncoderInterface;
15
use Da\TwoFA\Exception\InvalidSecretKeyException;
16
use Da\TwoFA\Support\Encoder;
17
use Da\TwoFA\Traits\OathTrait;
18
use Da\TwoFA\Traits\SecretValidationTrait;
19
use Da\TwoFA\Validator\OneTimePasswordValidator;
20
use Da\TwoFA\Validator\SecretKeyValidator;
21
22
class Manager
23
{
24
    use SecretValidationTrait;
25
    use OathTrait;
26
27
    /**
28
     * @var int the period parameter that defines the time (in seconds) the OTP token will be valid. Default is 30 for
29
     *          Google Authenticator.
30
     */
31
    protected $counter = 30;
32
    /**
33
     * @var int the times in 30 second cycles to avoid slight out of sync code verification. Setting this to 1 cycle
34
     *          will be valid for 60 seconds.
35
     */
36
    protected $cycles = 1;
37
    /**
38
     * @var TotpEncoderInterface
39
     */
40
    protected $encoder;
41
42
    /**
43
     * Auth constructor.
44
     *
45
     * @param SecretKeyValidator|null   $secretKeyValidator
46
     * @param TotpEncoderInterface|null $encoder
47
     */
48
    public function __construct(SecretKeyValidator $secretKeyValidator = null, TotpEncoderInterface $encoder = null)
49
    {
50
        $this->secretKeyValidator = $secretKeyValidator ?: new SecretKeyValidator();
51
        $this->encoder = $encoder ?: new Encoder();
52
    }
53
54
    /**
55
     * @return bool
56
     */
57
    public function isGoogleAuthenticatorCompatibilityEnabled(): bool
58
    {
59
        return $this->secretKeyValidator->isGoogleAuthenticatorCompatibilityEnforced();
60
    }
61
62
    /**
63
     * @return Manager
64
     */
65
    public function disableGoogleAuthenticatorCompatibility(): Manager
66
    {
67
        $cloned = clone $this;
68
        $cloned->secretKeyValidator = new SecretKeyValidator(false);
69
        $cloned->encoder = new Encoder($cloned->secretKeyValidator);
70
71
        return $cloned;
72
    }
73
74
    /**
75
     * @return $this|Manager
76
     */
77
    public function enableGoogleAuthenticatorCompatibility(): self
78
    {
79
        if (!$this->secretKeyValidator->isGoogleAuthenticatorCompatibilityEnforced()) {
80
            $cloned = clone $this;
81
            $cloned->secretKeyValidator = new SecretKeyValidator();
82
            $cloned->encoder = new Encoder($cloned->secretKeyValidator);
83
84
            return $cloned;
85
        }
86
87
        return $this;
88
    }
89
90
    /**
91
     * @return int
92
     */
93
    public function getTokenLength(): int
94
    {
95
        return $this->tokenLength;
96
    }
97
98
    /**
99
     * @param int $tokenLength
100
     *
101
     * @return Manager
102
     */
103
    public function setTokenLength($tokenLength): Manager
104
    {
105
        $this->tokenLength = $tokenLength;
106
107
        return $this;
108
    }
109
110
    /**
111
     * Wrapper function to Encoder::generateBase32RandomKey method.
112
     *
113
     * @param int    $length
114
     * @param string $prefix
115
     *
116
     * @throws InvalidSecretKeyException
117
     * @return mixed|string
118
     */
119
    public function generateSecretKey(int $length = 16, string $prefix = '')
120
    {
121
        return $this->encoder->generateBase32RandomKey($length, $prefix);
122
    }
123
124
    /**
125
     * @param int $value
126
     *
127
     * @return Manager
128
     */
129
    public function setCycles(int $value): Manager
130
    {
131
        $this->cycles = $value;
132
133
        return $this;
134
    }
135
136
    /**
137
     * @return int
138
     */
139
    public function getCycles(): int
140
    {
141
        return $this->cycles;
142
    }
143
144
    /**
145
     * @param $value
146
     *
147
     * @return Manager
148
     */
149
    public function setCounter(int $value): Manager
150
    {
151
        $this->counter = $value;
152
153
        return $this;
154
    }
155
156
    /**
157
     * @return int
158
     */
159
    public function getCounter(): int
160
    {
161
        return $this->counter;
162
    }
163
164
    /**
165
     * Returns the current Unix Timestamp divided by the $counter period.
166
     *
167
     * @return int
168
     **/
169
    public function getTimestamp(): int
170
    {
171
        return (int)floor(microtime(true) / $this->getCounter());
172
    }
173
174
    /**
175
     * Get the current one time password for a base32 encoded secret.
176
     *
177
     * @param string $secret
178
     *
179
     * @throws Exception\InvalidCharactersException
180
     * @throws InvalidSecretKeyException
181
     * @return string
182
     */
183
    public function getCurrentOneTimePassword(string $secret): string
184
    {
185
        $timestamp = $this->getTimestamp();
186
        $secret = $this->encoder->fromBase32($secret);
187
188
        return $this->oathHotp($secret, $timestamp);
189
    }
190
191
    /**
192
     * Verifies user's key vs current timestamp.
193
     *
194
     * @param string   $key          the user's input key
195
     * @param string   $secret       the secret used to
196
     * @param int|null $previousTime
197
     * @param int|null $time
198
     *
199
     * @throws InvalidSecretKeyException
200
     * @throws Exception\InvalidCharactersException
201
     * @return bool|int
202
     */
203
    public function verify(string $key, string $secret, int $previousTime = null, int $time = null)
204
    {
205
        $time = $time ? (int)$time : $this->getTimestamp();
206
        $cycles = $this->getCycles();
207
        $startTime = null === $previousTime
208
            ? $time - $cycles
209
            : max($time - $cycles, $previousTime + 1);
210
211
        $seed = $this->encoder->fromBase32($secret);
212
213
        if (strlen($seed) < 8) {
214
            throw new InvalidSecretKeyException('Secret key is too short');
215
        }
216
217
        return $this->validateOneTimePassword($key, $seed, $startTime, $time, $previousTime);
218
    }
219
220
    /**
221
     * Validates the key (OTP) and returns true if valid, false otherwise.
222
     *
223
     * @param string   $key
224
     * @param string   $seed
225
     * @param int      $startTime
226
     * @param int      $time
227
     * @param int|null $previousTime
228
     *
229
     * @return bool
230
     */
231
    protected function validateOneTimePassword(
232
        string $key,
233
        string $seed,
234
        int $startTime,
235
        int $time,
236
        $previousTime = null
237
    ): bool {
238
        return (new OneTimePasswordValidator(
239
            $seed,
240
            $this->getCycles(),
241
            $this->getTokenLength(),
242
            $startTime,
243
            $time,
244
            $previousTime
245
        ))
246
            ->validate($key);
247
    }
248
}
249