otp_rs/
hotp.rs

1use anyhow::Result;
2use data_encoding::BASE32_NOPAD;
3use hmac::Hmac;
4use hmac::Mac;
5use hmac::NewMac;
6use sha1::Sha1;
7
8#[derive(Debug, Eq, PartialEq, Clone)]
9pub struct HOTP {
10  secret: Vec<u8>,
11}
12
13impl HOTP {
14  pub fn new(secret: &str) -> HOTP {
15    HOTP {
16      secret: secret.as_bytes().to_vec(),
17    }
18  }
19
20  pub fn from_base32(secret: &str) -> Result<HOTP> {
21    let secret = BASE32_NOPAD
22      .decode(secret.as_bytes())
23      .expect("Invalid base32 value");
24    Ok(HOTP { secret })
25  }
26
27  pub fn from_bytes(secret: &[u8]) -> HOTP {
28    HOTP {
29      secret: secret.to_vec(),
30    }
31  }
32
33  pub fn generate(&self, counter: u64) -> Result<u32> {
34    let mut hmac = Hmac::<Sha1>::new_from_slice(self.secret.as_slice()).expect("Invalid secret");
35
36    hmac.update(&counter.to_be_bytes());
37    let result = hmac.finalize();
38    let digest = result.into_bytes();
39    let offset = (digest.last().expect("Invalid Digest") & 0xf) as usize;
40    let code: [u8; 4] = digest[offset..offset + 4]
41      .try_into()
42      .expect("Invalid digest");
43    let code = u32::from_be_bytes(code);
44    Ok((code & 0x7fffffff) % 1000000)
45  }
46
47  pub fn verify(&self, code: u32, last: u64, trials: u64) -> bool {
48    let code_str = code.to_string();
49    let code_bytes = code_str.as_bytes();
50    if code_bytes.len() > 6 {
51      return false;
52    }
53    for i in last + 1..last + trials + 1 {
54      println!("{}", i);
55      let valid_code = self.generate(i).expect("Fail to generate code").to_string();
56      let valid_bytes = valid_code.as_bytes();
57      if code_bytes.len() != valid_code.len() {
58        continue;
59      }
60      let mut rv = 0;
61      for (a, b) in code_bytes.iter().zip(valid_bytes.iter()) {
62        rv |= a ^ b;
63      }
64      if rv == 0 {
65        return true;
66      }
67    }
68    false
69  }
70
71  pub fn base32_secret(&self) -> String {
72    BASE32_NOPAD.encode(&self.secret)
73  }
74  pub fn to_uri(&self, label: &str, issuer: &str, counter: u64) -> String {
75    format!(
76      "otpauth://hotp/{}?secret={}&issuer={}&counter={}",
77      label,
78      self.base32_secret(),
79      issuer,
80      counter
81    )
82  }
83}