use hmac::{Hmac, KeyInit, Mac};
use sha1::Sha1;
use crate::{Code, CodeError, Digits, Secret};
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct Hotp {
digits: Digits,
}
impl Hotp {
#[must_use]
pub const fn new(digits: Digits) -> Self {
Self { digits }
}
#[must_use]
pub const fn digits(self) -> Digits {
self.digits
}
#[must_use]
pub fn generate(&self, secret: &Secret<'_>, counter: u64) -> Code {
generate_with_sha1(secret, counter, self.digits)
}
pub fn verify(
&self,
secret: &Secret<'_>,
counter: u64,
candidate: &str,
) -> Result<bool, CodeError> {
let candidate = Code::parse(candidate, self.digits)?;
Ok(self.generate(secret, counter).ct_eq(candidate))
}
pub fn verify_window(
&self,
secret: &Secret<'_>,
counter: u64,
look_ahead: u16,
candidate: &str,
) -> Result<Option<HotpMatch>, CodeError> {
let candidate = Code::parse(candidate, self.digits)?;
let mut matched = None;
for offset in 0..=u64::from(look_ahead) {
let Some(current) = counter.checked_add(offset) else {
continue;
};
let equal = self.generate(secret, current).ct_eq(candidate);
if equal && matched.is_none() {
matched = Some(HotpMatch {
counter: current,
next_counter: current.checked_add(1),
});
}
}
Ok(matched)
}
}
impl Default for Hotp {
fn default() -> Self {
Self::new(Digits::SIX)
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct HotpMatch {
counter: u64,
next_counter: Option<u64>,
}
impl HotpMatch {
#[must_use]
pub const fn counter(self) -> u64 {
self.counter
}
#[must_use]
pub const fn next_counter(self) -> Option<u64> {
self.next_counter
}
}
pub(crate) fn generate_with_sha1(secret: &Secret<'_>, counter: u64, digits: Digits) -> Code {
let mut mac =
Hmac::<Sha1>::new_from_slice(secret.as_bytes()).expect("HMAC accepts keys of any length");
mac.update(&counter.to_be_bytes());
let output = mac.finalize().into_bytes();
dynamic_truncate(output.as_slice(), digits)
}
pub(crate) fn dynamic_truncate(output: &[u8], digits: Digits) -> Code {
let offset = usize::from(output[output.len() - 1] & 0x0f);
let binary = u32::from_be_bytes([
output[offset],
output[offset + 1],
output[offset + 2],
output[offset + 3],
]) & 0x7fff_ffff;
Code::generated(binary % digits.modulus(), digits)
}