otps/
hotp.rs

1use derive_builder::Builder;
2use ring::{
3    constant_time::verify_slices_are_equal,
4    hmac::{sign, Key, Tag, HMAC_SHA1_FOR_LEGACY_USE_ONLY},
5};
6
7use crate::otp::secret_encoding;
8
9/// Number of digits in an HOTP value; system parameter
10const OTP_DIGITS: usize = 6;
11
12/// It converts an HMAC-SHA-1 value into an HOTP value as define in [RFC 4226 - Section 5.3](https://datatracker.ietf.org/doc/html/rfc4226#section-5.3)
13fn truncated_hash(hmac: &Tag) -> u32 {
14    let hmac_result = hmac.as_ref();
15    let offset = (hmac_result[hmac_result.len() - 1 /* 19 */] as usize) & 0xf;
16    let bin_code: u32 = (((hmac_result[offset] & 0x7f) as u32) << 24)
17        | (((hmac_result[offset + 1] & 0xff) as u32) << 16)
18        | (((hmac_result[offset + 2] & 0xff) as u32) << 8)
19        | (hmac_result[offset + 3] & 0xff) as u32;
20    bin_code % 10u32.pow(OTP_DIGITS as u32)
21}
22
23/// HMAC-based one-time password
24///
25/// - RFC 4226: <https://datatracker.ietf.org/doc/html/rfc4226>
26/// - Generating an HOTP value: <https://datatracker.ietf.org/doc/html/rfc4226#section-5.3>
27#[derive(Default, Debug, Builder)]
28pub struct Hotp {
29    #[builder(default)]
30    counter: u64,
31    #[builder(default)]
32    key: Vec<u8>,
33}
34
35impl HotpBuilder {
36    pub fn new() -> Self {
37        Self::default()
38    }
39
40    secret_encoding!(Self);
41}
42
43impl Hotp {
44    pub fn increment_counter(&mut self) -> &mut Self {
45        let Hotp { counter, .. } = self;
46        *counter += 1;
47        self
48    }
49
50    pub fn generate(&self) -> String {
51        let hash_key = Key::new(HMAC_SHA1_FOR_LEGACY_USE_ONLY, &self.key);
52        let Hotp { counter, .. } = self;
53        let counter_bytes = counter.to_be_bytes();
54        let hashed_tag = sign(&hash_key, &counter_bytes);
55        let code: u32 = truncated_hash(&hashed_tag);
56        format!("{:0>width$}", code, width = OTP_DIGITS)
57    }
58
59    pub fn validate(&self, code: &str) -> bool {
60        if code.len() != OTP_DIGITS {
61            return false;
62        }
63
64        let hashed_tag = sign(
65            &Key::new(HMAC_SHA1_FOR_LEGACY_USE_ONLY, &self.key),
66            code.as_bytes(),
67        );
68
69        let ref_code = self.generate().into_bytes();
70        let hashed_ref_tag = sign(
71            &Key::new(HMAC_SHA1_FOR_LEGACY_USE_ONLY, &self.key),
72            &ref_code,
73        );
74
75        verify_slices_are_equal(hashed_tag.as_ref(), hashed_ref_tag.as_ref())
76            .map(|_| true)
77            .unwrap_or(false)
78    }
79}
80
81#[test]
82fn test_generate() {
83    let mut hotp = HotpBuilder::new()
84        .base32_secret("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
85        .build()
86        .unwrap();
87
88    for _ in 0..2 {
89        assert_eq!(hotp.generate(), "679988")
90    }
91
92    assert!(!hotp.validate("123456"));
93    assert!(hotp.validate("679988"));
94
95    hotp.increment_counter();
96
97    for _ in 0..2 {
98        assert_ne!(hotp.generate(), "679988");
99        assert_eq!(hotp.generate(), "983918");
100    }
101
102    for mut hotp in [
103        HotpBuilder::new()
104            .key("12345678901234567890".as_bytes().to_owned())
105            .build()
106            .expect("failed to initialize HOTP client"),
107        HotpBuilder::new()
108            .base32_secret("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ")
109            .build()
110            .expect("failed to initialize HOTP client"),
111    ] {
112        for _ in 0..2 {
113            assert_eq!(hotp.generate(), "755224");
114        }
115
116        hotp.increment_counter();
117
118        for _ in 0..2 {
119            assert_eq!(hotp.generate(), "287082");
120        }
121    }
122}