use derive_builder::Builder;
use ring::{
constant_time::verify_slices_are_equal,
hmac::{sign, Key, Tag, HMAC_SHA1_FOR_LEGACY_USE_ONLY},
};
use crate::otp::secret_encoding;
const OTP_DIGITS: usize = 6;
fn truncated_hash(hmac: &Tag) -> u32 {
let hmac_result = hmac.as_ref();
let offset = (hmac_result[hmac_result.len() - 1 ] as usize) & 0xf;
let bin_code: u32 = (((hmac_result[offset] & 0x7f) as u32) << 24)
| (((hmac_result[offset + 1] & 0xff) as u32) << 16)
| (((hmac_result[offset + 2] & 0xff) as u32) << 8)
| (hmac_result[offset + 3] & 0xff) as u32;
bin_code % 10u32.pow(OTP_DIGITS as u32)
}
#[derive(Default, Debug, Builder)]
pub struct Hotp {
#[builder(default)]
counter: u64,
#[builder(default)]
key: Vec<u8>,
}
impl HotpBuilder {
pub fn new() -> Self {
Self::default()
}
secret_encoding!(Self);
}
impl Hotp {
pub fn increment_counter(&mut self) -> &mut Self {
let Hotp { counter, .. } = self;
*counter += 1;
self
}
pub fn generate(&self) -> String {
let hash_key = Key::new(HMAC_SHA1_FOR_LEGACY_USE_ONLY, &self.key);
let Hotp { counter, .. } = self;
let counter_bytes = counter.to_be_bytes();
let hashed_tag = sign(&hash_key, &counter_bytes);
let code: u32 = truncated_hash(&hashed_tag);
format!("{:0>width$}", code, width = OTP_DIGITS)
}
pub fn validate(&self, code: &str) -> bool {
if code.len() != OTP_DIGITS {
return false;
}
let hashed_tag = sign(
&Key::new(HMAC_SHA1_FOR_LEGACY_USE_ONLY, &self.key),
code.as_bytes(),
);
let ref_code = self.generate().into_bytes();
let hashed_ref_tag = sign(
&Key::new(HMAC_SHA1_FOR_LEGACY_USE_ONLY, &self.key),
&ref_code,
);
verify_slices_are_equal(hashed_tag.as_ref(), hashed_ref_tag.as_ref())
.map(|_| true)
.unwrap_or(false)
}
}
#[test]
fn test_generate() {
let mut hotp = HotpBuilder::new()
.base32_secret("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
.build()
.unwrap();
for _ in 0..2 {
assert_eq!(hotp.generate(), "679988")
}
assert!(!hotp.validate("123456"));
assert!(hotp.validate("679988"));
hotp.increment_counter();
for _ in 0..2 {
assert_ne!(hotp.generate(), "679988");
assert_eq!(hotp.generate(), "983918");
}
for mut hotp in [
HotpBuilder::new()
.key("12345678901234567890".as_bytes().to_owned())
.build()
.expect("failed to initialize HOTP client"),
HotpBuilder::new()
.base32_secret("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ")
.build()
.expect("failed to initialize HOTP client"),
] {
for _ in 0..2 {
assert_eq!(hotp.generate(), "755224");
}
hotp.increment_counter();
for _ in 0..2 {
assert_eq!(hotp.generate(), "287082");
}
}
}