1use core::{
10 fmt,
11 hint::{
12 assert_unchecked,
13 unreachable_unchecked,
14 },
15 num::NonZeroU64,
16};
17
18use hmac::{
19 Hmac,
20 KeyInit as _,
21 Mac as _,
22};
23use sha1::Sha1;
24use sha2::{
25 Sha256,
26 Sha512,
27};
28use subtle::ConstantTimeEq as _;
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum Algorithm {
33 Sha1,
35 Sha256,
37 Sha512,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum Error {
44 ZeroStep,
46 InvalidDigits,
48 InvalidSecret,
50}
51
52impl fmt::Display for Error {
53 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54 match self {
55 | Error::ZeroStep => write!(f, "Time step cannot be zero"),
56 | Error::InvalidDigits => write!(f, "Digits must be between 1 and 10"),
57 | Error::InvalidSecret => write!(f, "Invalid secret key length"),
58 }
59 }
60}
61
62impl core::error::Error for Error {}
63
64#[derive(Debug, Clone)]
66pub struct Hotp<'a> {
67 secret: &'a [u8],
68 algorithm: Algorithm,
69 digits: u8,
70}
71
72impl<'a> Hotp<'a> {
73 pub fn new<'s: 'a>(secret: &'s impl AsRef<[u8]>, algorithm: Algorithm, digits: u8) -> Result<Self, Error> {
83 let secret = secret.as_ref();
84
85 if digits == 0 || digits > 10 {
86 return Err(Error::InvalidDigits);
87 }
88
89 Ok(Self {
90 secret,
91 algorithm,
92 digits,
93 })
94 }
95
96 #[must_use]
98 pub fn secret(&self) -> &'a [u8] {
99 self.secret
100 }
101
102 #[must_use]
104 pub fn algorithm(&self) -> Algorithm {
105 self.algorithm
106 }
107
108 #[must_use]
110 pub fn digits(&self) -> u8 {
111 self.digits
112 }
113
114 #[must_use]
120 pub fn generate(&self, counter: u64) -> Result<String, Error> {
121 unsafe {
123 assert_unchecked(self.digits > 0 && self.digits <= 10);
124 }
125
126 let counter_bytes = counter.to_be_bytes();
127 let mut p = [0u8; 4];
128
129 match self.algorithm {
132 | Algorithm::Sha1 => {
133 let mut mac = Hmac::<Sha1>::new_from_slice(self.secret).map_err(|_| Error::InvalidSecret)?;
134 mac.update(&counter_bytes);
135 let result = mac.finalize().into_bytes();
136
137 let offset = usize::from(result[19] & 0x0F);
139
140 let Some(slice) = result.get(offset .. offset + 4) else {
141 unsafe { unreachable_unchecked() }
145 };
146 p.copy_from_slice(slice);
147 },
148 | Algorithm::Sha256 => {
149 let mut mac = Hmac::<Sha256>::new_from_slice(self.secret).map_err(|_| Error::InvalidSecret)?;
150 mac.update(&counter_bytes);
151 let result = mac.finalize().into_bytes();
152
153 let offset = usize::from(result[31] & 0x0F);
155
156 let Some(slice) = result.get(offset .. offset + 4) else {
157 unsafe { unreachable_unchecked() }
161 };
162 p.copy_from_slice(slice);
163 },
164 | Algorithm::Sha512 => {
165 let mut mac = Hmac::<Sha512>::new_from_slice(self.secret).map_err(|_| Error::InvalidSecret)?;
166 mac.update(&counter_bytes);
167 let result = mac.finalize().into_bytes();
168
169 let offset = usize::from(result[63] & 0x0F);
171
172 let Some(slice) = result.get(offset .. offset + 4) else {
173 unsafe { unreachable_unchecked() }
177 };
178 p.copy_from_slice(slice);
179 },
180 };
181
182 let binary_code =
185 (u32::from(p[0] & 0x7F)) << 24 | u32::from(p[1]) << 16 | u32::from(p[2]) << 8 | u32::from(p[3]);
186
187 let modulo = 10_u64.pow(u32::from(self.digits));
188 let final_code = u64::from(binary_code).rem_euclid(modulo);
189
190 Ok(format!("{:0width$}", final_code, width = self.digits as usize))
191 }
192
193 #[must_use]
199 pub fn verify(&self, code: &str, counter: u64) -> Result<bool, Error> {
200 unsafe {
202 assert_unchecked(self.digits > 0 && self.digits <= 10);
203 }
204
205 if code.len() != self.digits as usize {
206 return Ok(false);
207 }
208
209 let expected_code = self.generate(counter)?;
210 Ok(bool::from(expected_code.as_bytes().ct_eq(code.as_bytes())))
211 }
212}
213
214#[derive(Debug, Clone)]
216pub struct Totp<'a> {
217 hotp: Hotp<'a>,
218 step_seconds: u64,
219 t0: u64,
220}
221
222impl<'a> Totp<'a> {
223 #[must_use]
235 pub fn new<'s: 'a>(
236 secret: &'s impl AsRef<[u8]>,
237 algorithm: Algorithm,
238 digits: u8,
239 step_seconds: NonZeroU64,
240 t0: u64,
241 ) -> Result<Self, Error> {
242 Ok(Self {
243 hotp: Hotp::new(secret, algorithm, digits)?,
244 step_seconds: step_seconds.get(),
245 t0,
246 })
247 }
248
249 #[must_use]
251 pub fn secret(&self) -> &'a [u8] {
252 self.hotp.secret()
253 }
254
255 #[must_use]
257 pub fn algorithm(&self) -> Algorithm {
258 self.hotp.algorithm()
259 }
260
261 #[must_use]
263 pub fn digits(&self) -> u8 {
264 self.hotp.digits()
265 }
266
267 #[must_use]
269 pub fn step_seconds(&self) -> u64 {
270 self.step_seconds
271 }
272
273 #[must_use]
275 pub fn t0(&self) -> u64 {
276 self.t0
277 }
278
279 #[must_use]
285 pub fn generate(&self, unix_time_sec: u64) -> Result<String, Error> {
286 let step = self.calculate_step(unix_time_sec);
287 self.hotp.generate(step)
288 }
289
290 #[must_use]
303 pub fn verify(&self, code: &str, unix_time_sec: u64, skew_tolerance: u64) -> Result<bool, Error> {
304 unsafe {
306 assert_unchecked(self.hotp.digits > 0 && self.hotp.digits <= 10);
307 }
308
309 if code.len() != self.hotp.digits as usize {
310 return Ok(false);
311 }
312
313 let current_step = self.calculate_step(unix_time_sec);
314 let mut is_valid = false;
315
316 let start_step = current_step.saturating_sub(skew_tolerance);
317 let end_step = current_step.saturating_add(skew_tolerance);
318
319 for step in start_step ..= end_step {
320 let expected_code = self.hotp.generate(step)?;
321 let eq_result = expected_code.as_bytes().ct_eq(code.as_bytes());
322 is_valid |= bool::from(eq_result);
323 }
324
325 Ok(is_valid)
326 }
327
328 #[must_use]
330 fn calculate_step(&self, unix_time: u64) -> u64 {
331 unsafe {
334 assert_unchecked(self.step_seconds > 0);
335 }
336
337 if unix_time < self.t0 {
339 0
340 } else {
341 unix_time.saturating_sub(self.t0) / self.step_seconds
342 }
343 }
344}
345
346#[cfg(test)]
347mod tests {
348 use core::num::NonZeroU64;
349
350 use super::*;
351
352 #[test]
353 fn rfc4226_hotp_tests() -> Result<(), Error> {
354 let secret = b"12345678901234567890";
355 let hotp = Hotp::new(secret, Algorithm::Sha1, 6)?;
356
357 assert_eq!(hotp.secret(), secret);
358 assert_eq!(hotp.algorithm(), Algorithm::Sha1);
359 assert_eq!(hotp.digits(), 6);
360
361 let expected_results = [
362 (0, "755224"),
363 (1, "287082"),
364 (2, "359152"),
365 (3, "969429"),
366 (4, "338314"),
367 (5, "254676"),
368 (6, "287922"),
369 (7, "162583"),
370 (8, "399871"),
371 (9, "520489"),
372 ];
373
374 for (count, expected) in expected_results {
375 assert_eq!(hotp.generate(count)?, expected, "HOTP mismatch at count {}", count);
376 assert!(hotp.verify(expected, count)?, "Verification failed at count {}", count);
377 }
378
379 Ok(())
380 }
381
382 #[test]
383 fn rfc6238_totp_tests_sha1() -> Result<(), Error> {
384 let secret: &[u8; 20] = b"12345678901234567890";
385 let step = NonZeroU64::new(30).ok_or(Error::ZeroStep)?;
386 let totp = Totp::new(secret, Algorithm::Sha1, 8, step, 0)?;
387
388 assert_eq!(totp.secret(), secret);
389 assert_eq!(totp.algorithm(), Algorithm::Sha1);
390 assert_eq!(totp.digits(), 8);
391 assert_eq!(totp.step_seconds(), 30);
392 assert_eq!(totp.t0(), 0);
393
394 let expected_results = [
395 (59, "94287082"),
396 (1111111109, "07081804"),
397 (1111111111, "14050471"),
398 (1234567890, "89005924"),
399 (2000000000, "69279037"),
400 (20000000000, "65353130"),
401 ];
402
403 for (time, expected) in expected_results {
404 assert_eq!(
405 totp.generate(time)?,
406 expected,
407 "TOTP SHA1 generation failed at time {}",
408 time
409 );
410 assert!(
411 totp.verify(expected, time, 0)?,
412 "TOTP SHA1 verification failed at time {}",
413 time
414 );
415 }
416
417 Ok(())
418 }
419
420 #[test]
421 fn rfc6238_totp_tests_sha256() -> Result<(), Error> {
422 let secret: &[u8; 32] = b"12345678901234567890123456789012";
423 let step = NonZeroU64::new(30).ok_or(Error::ZeroStep)?;
424 let totp = Totp::new(secret, Algorithm::Sha256, 8, step, 0)?;
425
426 let expected_results = [
427 (59, "46119246"),
428 (1111111109, "68084774"),
429 (1111111111, "67062674"),
430 (1234567890, "91819424"),
431 (2000000000, "90698825"),
432 (20000000000, "77737706"),
433 ];
434
435 for (time, expected) in expected_results {
436 assert_eq!(
437 totp.generate(time)?,
438 expected,
439 "TOTP SHA256 generation failed at time {}",
440 time
441 );
442 assert!(
443 totp.verify(expected, time, 0)?,
444 "TOTP SHA256 verification failed at time {}",
445 time
446 );
447 }
448
449 Ok(())
450 }
451
452 #[test]
453 fn rfc6238_totp_tests_sha512() -> Result<(), Error> {
454 let secret: &[u8; 64] = b"1234567890123456789012345678901234567890123456789012345678901234";
455 let step = NonZeroU64::new(30).ok_or(Error::ZeroStep)?;
456 let totp = Totp::new(secret, Algorithm::Sha512, 8, step, 0)?;
457
458 let expected_results = [
459 (59, "90693936"),
460 (1111111109, "25091201"),
461 (1111111111, "99943326"),
462 (1234567890, "93441116"),
463 (2000000000, "38618901"),
464 (20000000000, "47863826"),
465 ];
466
467 for (time, expected) in expected_results {
468 assert_eq!(
469 totp.generate(time)?,
470 expected,
471 "TOTP SHA512 generation failed at time {}",
472 time
473 );
474 assert!(
475 totp.verify(expected, time, 0)?,
476 "TOTP SHA512 verification failed at time {}",
477 time
478 );
479 }
480
481 Ok(())
482 }
483}