use data_encoding::BASE32_NOPAD;
use hmac::{Hmac, KeyInit, Mac};
use sha1::Sha1;
use crate::protocol::dom::{attr_value, strip_text, tags_where};
pub fn two_factor_secret(html: &str) -> Option<String> {
let (_, _, inner) = tags_where(html, |_, attrs| {
attrs
.iter()
.any(|(name, _)| name == "data-two-factor-secret")
})
.into_iter()
.next()?;
Some(strip_text(&inner))
}
pub fn recovery_codes(html: &str) -> Vec<String> {
let Some((_, _, inner)) = tags_where(html, |_, attrs| {
attrs.iter().any(|(name, _)| name == "data-recovery-codes")
})
.into_iter()
.next() else {
return Vec::new();
};
tags_where(&inner, |tag, _| tag == "li")
.into_iter()
.map(|(_, _, code)| strip_text(&code))
.collect()
}
pub fn two_factor_state(html: &str) -> Option<String> {
tags_where(html, |tag, attrs| {
tag == "form" && attr_value(attrs, "data-two-factor").is_some()
})
.into_iter()
.next()
.and_then(|(_, attrs, _)| attr_value(&attrs, "data-two-factor"))
}
pub fn authenticator_code_at(secret: &str, unix_secs: u64) -> String {
let key = BASE32_NOPAD
.decode(secret.trim().as_bytes())
.expect("the secret is base32");
let mut mac = Hmac::<Sha1>::new_from_slice(&key).expect("HMAC takes a key of any length");
mac.update(&(unix_secs / 30).to_be_bytes());
let digest = mac.finalize().into_bytes();
let offset = usize::from(digest[19] & 0x0f);
let binary = u32::from_be_bytes([
digest[offset] & 0x7f,
digest[offset + 1],
digest[offset + 2],
digest[offset + 3],
]);
format!("{:06}", binary % 1_000_000)
}
pub fn authenticator_code(secret: &str) -> String {
authenticator_code_at(secret, now())
}
pub fn now() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("the clock is after 1970")
.as_secs()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_code_matches_rfc_6238() {
let secret = BASE32_NOPAD.encode(b"12345678901234567890");
assert_eq!(authenticator_code_at(&secret, 59), "287082");
assert_eq!(authenticator_code_at(&secret, 1_111_111_109), "081804");
assert_eq!(authenticator_code_at(&secret, 2_000_000_000), "279037");
}
}