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
9const OTP_DIGITS: usize = 6;
11
12fn truncated_hash(hmac: &Tag) -> u32 {
14 let hmac_result = hmac.as_ref();
15 let offset = (hmac_result[hmac_result.len() - 1 ] 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#[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}