1use hmac::{Hmac, KeyInit, Mac};
2use sha2::{Sha256, Sha512};
3use subtle::{Choice, ConditionallySelectable};
4
5use crate::hotp::{dynamic_truncate, generate_with_sha1};
6use crate::{Code, CodeError, Digits, Error, Secret};
7
8#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
10#[non_exhaustive]
11pub enum Algorithm {
12 #[default]
14 Sha1,
15 Sha256,
17 Sha512,
19}
20
21impl Algorithm {
22 #[must_use]
27 pub const fn recommended_key_len(self) -> usize {
28 match self {
29 Self::Sha1 => 20,
30 Self::Sha256 => 32,
31 Self::Sha512 => 64,
32 }
33 }
34}
35
36#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
42pub struct ValidationWindow {
43 past: u16,
44 future: u16,
45}
46
47impl ValidationWindow {
48 pub const CURRENT: Self = Self::new(0, 0);
50 pub const RFC_RECOMMENDED: Self = Self::new(1, 0);
52
53 #[must_use]
55 pub const fn new(past: u16, future: u16) -> Self {
56 Self { past, future }
57 }
58
59 #[must_use]
61 pub const fn past(self) -> u16 {
62 self.past
63 }
64
65 #[must_use]
67 pub const fn future(self) -> u16 {
68 self.future
69 }
70}
71
72impl Default for ValidationWindow {
73 fn default() -> Self {
74 Self::RFC_RECOMMENDED
75 }
76}
77
78#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
83pub struct Totp {
84 algorithm: Algorithm,
85 digits: Digits,
86 period: u64,
87 epoch: u64,
88}
89
90struct MatchAccumulator {
91 found: Choice,
92 counter: u64,
93 drift: i32,
94}
95
96impl MatchAccumulator {
97 fn new() -> Self {
98 Self {
99 found: Choice::from(0),
100 counter: 0,
101 drift: 0,
102 }
103 }
104}
105
106impl Totp {
107 pub const DEFAULT_PERIOD: u64 = 30;
109
110 pub const fn new(
118 algorithm: Algorithm,
119 digits: Digits,
120 period: u64,
121 epoch: u64,
122 ) -> Result<Self, Error> {
123 if period == 0 {
124 return Err(Error::ZeroPeriod);
125 }
126 Ok(Self {
127 algorithm,
128 digits,
129 period,
130 epoch,
131 })
132 }
133
134 #[must_use]
136 pub const fn algorithm(self) -> Algorithm {
137 self.algorithm
138 }
139
140 #[must_use]
142 pub const fn digits(self) -> Digits {
143 self.digits
144 }
145
146 #[must_use]
148 pub const fn period(self) -> u64 {
149 self.period
150 }
151
152 #[must_use]
154 pub const fn epoch(self) -> u64 {
155 self.epoch
156 }
157
158 pub const fn counter_at(self, timestamp: u64) -> Result<u64, Error> {
168 if timestamp < self.epoch {
169 return Err(Error::TimestampBeforeEpoch {
170 timestamp,
171 epoch: self.epoch,
172 });
173 }
174 Ok((timestamp - self.epoch) / self.period)
175 }
176
177 pub const fn seconds_remaining(self, timestamp: u64) -> Result<u64, Error> {
187 if timestamp < self.epoch {
188 return Err(Error::TimestampBeforeEpoch {
189 timestamp,
190 epoch: self.epoch,
191 });
192 }
193 Ok(self.period - ((timestamp - self.epoch) % self.period))
194 }
195
196 pub fn generate(&self, secret: &Secret<'_>, timestamp: u64) -> Result<Code, Error> {
203 let counter = self.counter_at(timestamp)?;
204 Ok(self.generate_for_counter(secret, counter))
205 }
206
207 pub fn verify(
216 &self,
217 secret: &Secret<'_>,
218 timestamp: u64,
219 candidate: &str,
220 ) -> Result<bool, VerifyError> {
221 let candidate = Code::parse(candidate, self.digits)?;
222 let counter = self.counter_at(timestamp)?;
223 Ok(self.generate_for_counter(secret, counter).ct_eq(candidate))
224 }
225
226 pub fn verify_window(
243 &self,
244 secret: &Secret<'_>,
245 timestamp: u64,
246 window: ValidationWindow,
247 candidate: &str,
248 ) -> Result<Option<TotpMatch>, VerifyError> {
249 let candidate = Code::parse(candidate, self.digits)?;
250 let counter = self.counter_at(timestamp)?;
251 let mut matched = MatchAccumulator::new();
252
253 self.consider_match(secret, counter, 0, candidate, &mut matched);
254
255 for distance in 1..=window.past {
256 let Some(current) = counter.checked_sub(u64::from(distance)) else {
257 continue;
258 };
259 self.consider_match(
260 secret,
261 current,
262 -i32::from(distance),
263 candidate,
264 &mut matched,
265 );
266 }
267
268 for distance in 1..=window.future {
269 let Some(current) = counter.checked_add(u64::from(distance)) else {
270 continue;
271 };
272 self.consider_match(
273 secret,
274 current,
275 i32::from(distance),
276 candidate,
277 &mut matched,
278 );
279 }
280
281 Ok(if bool::from(matched.found) {
282 Some(TotpMatch {
283 counter: matched.counter,
284 drift: matched.drift,
285 })
286 } else {
287 None
288 })
289 }
290
291 fn consider_match(
292 &self,
293 secret: &Secret<'_>,
294 counter: u64,
295 drift: i32,
296 candidate: Code,
297 matched: &mut MatchAccumulator,
298 ) {
299 let equal = self
300 .generate_for_counter(secret, counter)
301 .ct_eq_choice(candidate);
302 let select = equal & !matched.found;
303 matched.counter = u64::conditional_select(&matched.counter, &counter, select);
304 matched.drift = i32::conditional_select(&matched.drift, &drift, select);
305 matched.found |= equal;
306 }
307
308 fn generate_for_counter(&self, secret: &Secret<'_>, counter: u64) -> Code {
309 let message = counter.to_be_bytes();
310 match self.algorithm {
311 Algorithm::Sha1 => generate_with_sha1(secret, counter, self.digits),
312 Algorithm::Sha256 => {
313 let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes())
314 .expect("HMAC accepts keys of any length");
315 mac.update(&message);
316 dynamic_truncate(mac.finalize().into_bytes().as_slice(), self.digits)
317 }
318 Algorithm::Sha512 => {
319 let mut mac = Hmac::<Sha512>::new_from_slice(secret.as_bytes())
320 .expect("HMAC accepts keys of any length");
321 mac.update(&message);
322 dynamic_truncate(mac.finalize().into_bytes().as_slice(), self.digits)
323 }
324 }
325 }
326}
327
328impl Default for Totp {
329 fn default() -> Self {
330 Self {
331 algorithm: Algorithm::Sha1,
332 digits: Digits::SIX,
333 period: Self::DEFAULT_PERIOD,
334 epoch: 0,
335 }
336 }
337}
338
339#[derive(Clone, Copy, Debug, Eq, PartialEq)]
341#[non_exhaustive]
342pub enum VerifyError {
343 Code(CodeError),
345 Parameters(Error),
347}
348
349impl core::fmt::Display for VerifyError {
350 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
351 match self {
352 Self::Code(error) => write!(f, "invalid OTP code: {error}"),
353 Self::Parameters(error) => write!(f, "invalid TOTP input: {error}"),
354 }
355 }
356}
357
358#[cfg(feature = "std")]
359impl std::error::Error for VerifyError {
360 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
361 match self {
362 Self::Code(error) => Some(error),
363 Self::Parameters(error) => Some(error),
364 }
365 }
366}
367
368impl From<CodeError> for VerifyError {
369 fn from(value: CodeError) -> Self {
370 Self::Code(value)
371 }
372}
373
374impl From<Error> for VerifyError {
375 fn from(value: Error) -> Self {
376 Self::Parameters(value)
377 }
378}
379
380#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
382pub struct TotpMatch {
383 counter: u64,
384 drift: i32,
385}
386
387impl TotpMatch {
388 #[must_use]
390 pub const fn counter(self) -> u64 {
391 self.counter
392 }
393
394 #[must_use]
398 pub const fn drift(self) -> i32 {
399 self.drift
400 }
401}