otp2/
hotp.rs

1use crate::{Otp, OtpResult, ToBytes};
2
3/// HMAC-based one-time passcode
4///
5/// Uses a counter to generate the passcode. The counter is
6/// incremented after a passcode is generated.
7pub type Hotp = Otp<Counter>;
8
9impl Hotp {
10    /// Get a HOTP generator with the given `key`, initial count, and which
11    /// generates passcodes of `length`.
12    pub fn new(key: String, initial_count: u64, length: u32) -> Self {
13        Otp {
14            key,
15            generator: Counter {
16                count: initial_count,
17            },
18            digits: length,
19        }
20    }
21}
22
23/// The backing type which implements the [`ToBytes`] interface,
24/// using a counter to generate the value bytes.
25pub struct Counter {
26    count: u64,
27}
28
29impl ToBytes for Counter {
30    fn to_bytes(&mut self) -> OtpResult<[u8; 8]> {
31        let c = self.count;
32        self.count += 1;
33        Ok(c.to_be_bytes().into())
34    }
35}
36
37#[cfg(test)]
38mod test {
39    use test_case::test_case;
40
41    use crate::hotp::Hotp;
42
43    // These test cases are copied from RFC 4226
44    // https://datatracker.ietf.org/doc/html/rfc4226#appendix-D
45    #[test_case(0, 755224)]
46    #[test_case(1, 287082)]
47    #[test_case(2, 359152)]
48    #[test_case(3, 969429)]
49    #[test_case(4, 338314)]
50    #[test_case(5, 254676)]
51    #[test_case(6, 287922)]
52    #[test_case(7, 162583)]
53    #[test_case(8, 399871)]
54    #[test_case(9, 520489)]
55    fn it_computes_correct_hotp(counter: u64, expected: u32) {
56        let key = "12345678901234567890".to_string();
57        let digits = 6;
58        let mut hotp = Hotp::new(key, counter, digits);
59        let actual = hotp.get().unwrap();
60        assert_eq!(actual, expected);
61    }
62
63    #[test]
64    fn it_increments_the_counter() {
65        let cases = vec![
66            755224, 287082, 359152, 969429, 338314, 254676, 287922, 162583, 399871, 520489,
67        ];
68        let key = "12345678901234567890".to_string();
69        let digits = 6;
70        let counter = 0;
71        let mut htop = Hotp::new(key, counter, digits);
72        for case in cases {
73            let actual = htop.get().unwrap();
74            assert_eq!(actual, case);
75        }
76    }
77}