1use data_encoding::BASE32_NOPAD;
8use hmac::{Hmac, KeyInit, Mac};
9use sha1::Sha1;
10
11use crate::protocol::dom::{attr_value, strip_text, tags_where};
12
13pub fn two_factor_secret(html: &str) -> Option<String> {
15 let (_, _, inner) = tags_where(html, |_, attrs| {
16 attrs
17 .iter()
18 .any(|(name, _)| name == "data-two-factor-secret")
19 })
20 .into_iter()
21 .next()?;
22 Some(strip_text(&inner))
23}
24
25pub fn recovery_codes(html: &str) -> Vec<String> {
28 let Some((_, _, inner)) = tags_where(html, |_, attrs| {
29 attrs.iter().any(|(name, _)| name == "data-recovery-codes")
30 })
31 .into_iter()
32 .next() else {
33 return Vec::new();
34 };
35 tags_where(&inner, |tag, _| tag == "li")
36 .into_iter()
37 .map(|(_, _, code)| strip_text(&code))
38 .collect()
39}
40
41pub fn two_factor_state(html: &str) -> Option<String> {
44 tags_where(html, |tag, attrs| {
45 tag == "form" && attr_value(attrs, "data-two-factor").is_some()
46 })
47 .into_iter()
48 .next()
49 .and_then(|(_, attrs, _)| attr_value(&attrs, "data-two-factor"))
50}
51
52pub fn authenticator_code_at(secret: &str, unix_secs: u64) -> String {
58 let key = BASE32_NOPAD
59 .decode(secret.trim().as_bytes())
60 .expect("the secret is base32");
61 let mut mac = Hmac::<Sha1>::new_from_slice(&key).expect("HMAC takes a key of any length");
62 mac.update(&(unix_secs / 30).to_be_bytes());
63 let digest = mac.finalize().into_bytes();
64 let offset = usize::from(digest[19] & 0x0f);
65 let binary = u32::from_be_bytes([
66 digest[offset] & 0x7f,
67 digest[offset + 1],
68 digest[offset + 2],
69 digest[offset + 3],
70 ]);
71 format!("{:06}", binary % 1_000_000)
72}
73
74pub fn authenticator_code(secret: &str) -> String {
76 authenticator_code_at(secret, now())
77}
78
79pub fn now() -> u64 {
81 std::time::SystemTime::now()
82 .duration_since(std::time::UNIX_EPOCH)
83 .expect("the clock is after 1970")
84 .as_secs()
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90
91 #[test]
93 fn the_code_matches_rfc_6238() {
94 let secret = BASE32_NOPAD.encode(b"12345678901234567890");
95 assert_eq!(authenticator_code_at(&secret, 59), "287082");
96 assert_eq!(authenticator_code_at(&secret, 1_111_111_109), "081804");
97 assert_eq!(authenticator_code_at(&secret, 2_000_000_000), "279037");
98 }
99}