Skip to main content

futu_backend/auth/commconfig/
totp.rs

1//! auth/commconfig/totp — base32 decode + TOTP SHA1 generator
2//! (v1.4.110 CC Batch M: 拆自 commconfig.rs L183-243)
3//!
4//! 用于 select_all API 的 auth_token 参数, 对齐 C++ 富途内部 TOTP 算法
5//! (key=PEHMABDNLXIOG65U, period=30s, sha1, 6 digit).
6
7use 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
36/// 生成 6 位 TOTP auth_token。对齐 C++ `GenGoogleOTPCode_SHA1`
37/// (`OMBase_API_OTP.cpp:12`):
38///
39/// - 计数器 = (unix_time / period) 作大端 u64 `[u8; 8]`
40/// - mac = `HMAC-SHA1(base32_decode(key), counter_bytes)`
41/// - offset = `mac[19] & 0x0f`
42/// - truncated = `u32::from_be_bytes(mac[offset..offset+4]) & 0x7fff_ffff`
43/// - code = truncated % 1_000_000(6 位补零)
44pub 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}