otp2/
totp.rs

1use crate::{Otp, OtpResult, ToBytes};
2
3use unix_time::Instant;
4
5/// Time-based one-time passcode.
6///
7/// Provides one-time passcodes that are valid within a window
8/// of time after the passcode is generated.
9pub type Totp = Otp<Time>;
10
11impl Totp {
12    /// Get a TOTP generator.
13    ///
14    /// Repeated calls to [`Self::get`] will return the same
15    /// passcode when in the same `window`.
16    pub fn new(key: String, t0: Instant, window: u64, length: u32) -> Self {
17        Totp::new_with_now(key, t0, window, length, Box::new(|| Instant::now()))
18    }
19
20    /// Get a TOTP generator with a custom function to provide the
21    /// "now" value.
22    ///
23    /// See [`Self::new`].
24    pub fn new_with_now(
25        key: String,
26        t0: Instant,
27        step: u64,
28        digits: u32,
29        now: Box<dyn Fn() -> Instant>,
30    ) -> Self {
31        Otp {
32            key,
33            generator: Time { t0, step, now },
34            digits,
35        }
36    }
37}
38
39/// The backing type which implements the [`ToBytes`] interface,
40/// using the current time to generate the value bytes.
41pub struct Time {
42    t0: Instant,
43    step: u64,
44    now: Box<dyn Fn() -> Instant>,
45}
46
47impl ToBytes for Time {
48    fn to_bytes(&mut self) -> OtpResult<[u8; 8]> {
49        let t0 = self.t0;
50        let now = (self.now)();
51        let elapsed = now - t0;
52        let steps = elapsed.as_secs() / self.step;
53        Ok(steps.to_be_bytes().into())
54    }
55}
56
57#[cfg(test)]
58mod test {
59    use test_case::test_case;
60    use unix_time::Instant;
61
62    use crate::Totp;
63
64    // These test cases are copied from RFC 6238
65    // https://datatracker.ietf.org/doc/html/rfc6238#appendix-B
66    #[test_case(59, 94287082)]
67    #[test_case(1111111109, 07081804)]
68    #[test_case(1111111111, 14050471)]
69    #[test_case(1234567890, 89005924)]
70    #[test_case(2000000000, 69279037)]
71    #[test_case(20000000000, 65353130)]
72    fn it_computes_correct_totp(count: u64, expected_code: u32) {
73        let digits = 8;
74        let key = "12345678901234567890".to_string();
75        let step = 30;
76        let t0 = Instant::at(0, 0);
77        let mut otp = Totp::new_with_now(
78            key,
79            t0,
80            step,
81            digits,
82            Box::new(move || Instant::at(count, 0)),
83        );
84        let actual_code = otp.get().unwrap();
85        assert_eq!(actual_code, expected_code);
86    }
87}