futu_backend/auth/commconfig/
totp.rs1use hmac::{Hmac, Mac};
8use sha1::Sha1;
9
10pub fn base32_decode(s: &str) -> Option<Vec<u8>> {
11 fn ch(c: u8) -> Option<u8> {
12 match c {
13 b'A'..=b'Z' => Some(c - b'A'),
14 b'a'..=b'z' => Some(c - b'a'),
15 b'2'..=b'7' => Some(c - b'2' + 26),
16 _ => None,
17 }
18 }
19 let s = s.trim_end_matches('=');
20 let mut out = Vec::with_capacity(s.len() * 5 / 8);
21 let mut buf: u32 = 0;
22 let mut bits: u32 = 0;
23 for c in s.bytes() {
24 let v = ch(c)? as u32;
25 buf = (buf << 5) | v;
26 bits += 5;
27 if bits >= 8 {
28 bits -= 8;
29 out.push((buf >> bits) as u8);
30 buf &= (1u32 << bits) - 1;
31 }
32 }
33 Some(out)
34}
35
36pub fn gen_totp_sha1(base32_key: &str, unix_ts: i64, period_sec: u32) -> Option<String> {
45 if base32_key.is_empty() || unix_ts <= 0 || period_sec == 0 {
46 return None;
47 }
48 let key = base32_decode(base32_key)?;
49 let counter = (unix_ts as u64) / (period_sec as u64);
50 let counter_be = counter.to_be_bytes();
51
52 let mut mac = Hmac::<Sha1>::new_from_slice(&key).ok()?;
53 mac.update(&counter_be);
54 let hmac_bytes = mac.finalize().into_bytes();
55 debug_assert_eq!(hmac_bytes.len(), 20);
56
57 let offset = (hmac_bytes[19] & 0x0f) as usize;
58 let truncated = u32::from_be_bytes([
59 hmac_bytes[offset],
60 hmac_bytes[offset + 1],
61 hmac_bytes[offset + 2],
62 hmac_bytes[offset + 3],
63 ]) & 0x7fff_ffff;
64 let code = truncated % 1_000_000;
65 Some(format!("{code:06}"))
66}