futu_backend/auth/types.rs
1//! v1.4.110+ Tier 1 split (from `auth/mod.rs`): 顶层类型 + 2 const.
2//!
3//! - `UserAttribution` enum + impl (region / auth_domain / conn_identity 映射)
4//! - `AuthConfig` (输入: account / pwd / device_id / client_type)
5//! - `AuthResult` (输出: client_sig / client_key / auth_code_list / svr_time_offset / web_sig / ...)
6//! - `BrokerAuthCode` (auth_code_list 单元)
7//! - `AUTH_SERVER_PROD` / `TGTGT_VALIDITY_SECS` 顶层 const
8//!
9//! 不含业务逻辑 — 业务 fn 仍在 mod.rs.
10
11/// 默认认证服务器——CN / HK 归属地账号用这个。
12/// 海外账号(US/SG/AU/JP)实际请求时会按 `UserAttribution::auth_domain()` 切换。
13pub const AUTH_SERVER_PROD: &str = "https://auth.futunn.com";
14
15/// v1.4.71: TGTGT 票据有效期(30 天),对齐 C++ `auth_cryptor.cpp:135`
16/// `CreateNewTgtgt` 里 `InvalidTime = RefreshTime + 30 * 24 * 3600`。
17///
18/// **绝对不 hardcode**:之前 `30 * 24 * 3600` 魔法数散落在 2 处(`handle_device_verify`
19/// + `tgtgt_payload_structure_aligns_cpp_spec` test),改动需同步。提为 const 保一致。
20pub const TGTGT_VALIDITY_SECS: u32 = 30 * 24 * 3600;
21
22/// 用户归属地(对齐 C++ `FTLogin/Src/ftlogin/ftlogin_def.h:261-270`
23/// 和 `config/impl/user_attr_config.cpp:8-15`)
24///
25/// salt 响应里 `user_attribution` 字段决定认证域名:
26/// - CN / HK → `auth.futunn.com`
27/// - US / SG / AU / JP → `auth.moomoo.com`
28///
29/// 海外账号(moomoo)如果发到 futunn 域名会返回 `error_code=11`(误导为"验证码",
30/// 实际是服务端校验失败),必须按 attribution 切域名。
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
32#[repr(u8)]
33#[non_exhaustive]
34pub enum UserAttribution {
35 /// 中国大陆
36 Cn = 1,
37 /// 美国(moomoo)
38 Us = 2,
39 /// 新加坡(moomoo)
40 Sg = 3,
41 /// 澳大利亚(moomoo)
42 Au = 4,
43 /// 日本(moomoo)
44 Jp = 5,
45 /// 香港
46 Hk = 6,
47}
48
49impl UserAttribution {
50 /// 从 salt 响应的 `user_attribution` 整数字段解析。
51 /// 未知值(0 或 7+)返回 `None`,让调用方走 fallback(默认 futunn)。
52 pub fn from_u32(v: u32) -> Option<Self> {
53 match v {
54 1 => Some(Self::Cn),
55 2 => Some(Self::Us),
56 3 => Some(Self::Sg),
57 4 => Some(Self::Au),
58 5 => Some(Self::Jp),
59 6 => Some(Self::Hk),
60 _ => None,
61 }
62 }
63
64 /// 对齐 C++ `user_attr_config.cpp:8-15` 的映射表。
65 pub fn auth_domain(self) -> &'static str {
66 match self {
67 Self::Cn | Self::Hk => "https://auth.futunn.com",
68 Self::Us | Self::Sg | Self::Au | Self::Jp => "https://auth.moomoo.com",
69 }
70 }
71
72 /// 人读地区代码(日志 / 凭据文件用)。
73 pub fn region(self) -> &'static str {
74 match self {
75 Self::Cn => "CN",
76 Self::Us => "US",
77 Self::Sg => "SG",
78 Self::Au => "AU",
79 Self::Jp => "JP",
80 Self::Hk => "HK",
81 }
82 }
83
84 /// TCP 登录 `ReqEncryptData.conn_identity` 字段:
85 /// 对齐 C++ `FTConnCmn.proto` ConnIdentity enum,UserAttribution 数值直接对应
86 /// (CN=1, US=2, SG=3, AU=4, JP=5, HK=6,见 `user_attr_config.cpp:8-15`)
87 pub fn to_conn_identity(self) -> u32 {
88 self as u32
89 }
90}
91
92/// v1.4.19:`ZeroizeOnDrop` 让 `AuthConfig` drop 时自动把 `password` 字段的
93/// 堆内存清零,减少进程 core dump / `/proc/<pid>/mem` 读到明文的窗口。
94/// 其他字段(auth_server / account / device_id)不是秘密,`#[zeroize(skip)]`
95/// 跳过。注意 `Clone` 每次复制都会产生新堆分配,drop 时各自 zeroize。
96#[derive(Debug, Clone, zeroize::ZeroizeOnDrop)]
97pub struct AuthConfig {
98 #[zeroize(skip)]
99 pub auth_server: String,
100 #[zeroize(skip)]
101 pub account: String,
102 /// 密码:`password_is_md5 = false` 时是明文(内部做 MD5);
103 /// `password_is_md5 = true` 时是 32 位小写 hex 的 MD5(直接使用)。
104 /// drop 时自动 zeroize。
105 pub password: String,
106 /// 是否为预哈希 MD5;`false` 时按明文处理
107 #[zeroize(skip)]
108 pub password_is_md5: bool,
109 #[zeroize(skip)]
110 pub device_id: String,
111 /// HTTP `X-Futu-Client-Type` header 值。
112 ///
113 /// 对齐 C++ `NNBase_Define_Enum.h:1113-1114`:
114 /// - `40` = `NN_ClientType_FutuOpenD`(牛牛 FTNN)
115 /// - `60` = `NN_ClientType_FutuOpenDMooMoo`(moomoo FTMM)
116 ///
117 /// v1.4.15:moomoo 的 `auth.moomoo.com` 对 client-type=40 直接拒绝
118 /// (返回 `error_code=2`),必须用 60。由 `--platform` flag 决定。
119 #[zeroize(skip)]
120 pub client_type: u8,
121}
122
123/// `auth_code_list` 响应条目——每个 broker 一项。
124/// 对齐 C++ `FTLogin/Src/ftlogin/auth/impl/auth_impl.cpp:3504`(`ParseAuthCodeList`)
125#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
126pub struct BrokerAuthCode {
127 pub broker_id: u32,
128 pub auth_code: String,
129 pub invalid_time: u64,
130}
131
132#[derive(Debug, Clone)]
133pub struct AuthResult {
134 pub user_id: u64,
135 pub client_sig: Vec<u8>,
136 pub client_key: Vec<u8>,
137 /// 从 salt 响应拿到的归属地,TCP 登录时会用来派生 conn_identity
138 pub user_attribution: UserAttribution,
139 /// HTTP auth 响应的 `auth_code_list`——每个已授权 broker 的票据,
140 /// 用来后续向 `/broker_auth/client_auth` 换取 broker_client_sig / broker_client_key
141 pub auth_code_list: Vec<BrokerAuthCode>,
142 /// 原始 rand_key(解密用)——broker_auth 响应里的 `broker_client_key` 也是
143 /// 用这个 rand_key 加密过的,需要它做 `aes_cbc_md5_decrypt_var` 解开
144 pub rand_key: Vec<u8>,
145 /// v1.4.22:**服务端时间 - 本机时间**(秒)—— salt 响应时捕获。
146 ///
147 /// 用于让后续 TOTP / time-based token 用"服务端时间"而不是本机时间。
148 /// 机器时钟飘了 > 30s 的场景下 TOTP 会被服务端拒,offset 校正后可免。
149 /// 对齐 C++ `INNBiz_SvrTime::GetSvrTimeStamp()` 机制。
150 ///
151 /// 使用方式:`let server_now = local_now + svr_time_offset;`
152 /// 首次 salt 时 offset < 0 说明本机时钟比服务端快,反之则慢。0 = 没取到。
153 pub svr_time_offset: i64,
154 /// v1.4.93 G3 (CLAUDE.md C4 audit): `web_sig` from `/authority/` 响应里的
155 /// `web_sig_new` 字段(对齐 C++ `auth_impl.cpp:3193,3260`
156 /// `account.web_sig_`)。
157 ///
158 /// **用途**:G2 [`crate::auth::repull::repull_auth_code`] 把它作 POST
159 /// `/authority/repull_auth_code` body 字段(C++ `auth_impl.cpp:738-748`),
160 /// broker auth_code 过期时拉新 auth_code,让 broker channel self-heal
161 /// 不必重启 daemon。
162 ///
163 /// 缺失 → 空字符串(旧 v1.4.92 及之前的凭据 / device-verify shell 路径
164 /// 没此字段)。空时调用方应跳过 repull, fallback 走 platform refresh。
165 pub web_sig: String,
166 /// v1.4.93 G1 (CLAUDE.md C4 audit P1): client_sig 失效的本地时戳 (秒, UTC epoch).
167 /// 对齐 C++ `auth_impl.cpp:3245-3247` 解 `cltsig_invalidtime` (相对服务端时间秒)
168 /// + `svr_time` 得本地时间, 写到 `account.client_sig_invalid_local_time_s_`.
169 ///
170 /// **用途**: 记录服务端下发的 client_sig 失效时间。当前 auth 模块只负责
171 /// 解析/持久化该字段,不启动 proactive timer;长跑 daemon 的 client_sig
172 /// 更新走 reconnect 失败后的 reactive remember-login refresh 路径。
173 ///
174 /// **缺失 (老 backend / 旧 credentials shell)**: 0 (不触发 proactive refresh).
175 pub client_sig_invalid_local_time_s: u64,
176 /// v1.4.94 G6 (P2 protocol gap): `moomoo_client_sig` from `/authority/`
177 /// 响应里的 `moomoo_client_sig` 字段, **base64 已解码**.
178 ///
179 /// 对齐 C++ `auth_impl.cpp:3195` `ParseJsonString(jval_result, "moomoo_client_sig", mm_sig);`
180 /// 映射到 `account.us_client_sig_`. 用于 moomoo / US 路径独立于
181 /// `client_sig` 的 broker channel 鉴权 — 当账号 attribution = US/SG/AU/JP/CA
182 /// 时, broker_auth_code 换 client_sig 走的是 `moomoo_client_sig` 而不是
183 /// 主 `client_sig`.
184 ///
185 /// 缺失 (futunn HK 账号 / 老 backend) → 空 Vec (handler 检查 `is_empty()`
186 /// 决定 fallback 主 `client_sig`).
187 ///
188 /// ## ⚠️ v1.4.96 BUG #010 doctrine fix (eli double-tester 2026-04-26):
189 ///
190 /// 真机 verify 发现 backend 对 **futunn 账号也下发 moomoo_client_sig**
191 /// (mm_sig_len=128). 字段名 `moomoo_*` **不**意味着账号是 moomoo 系.
192 ///
193 /// **不要**基于 `moomoo_client_sig.is_empty()` 判账号 broker path. 真正
194 /// 的 broker path 判断用 `broker_id` (1001/1007=Futu HK/US, 6xxx=moomoo).
195 pub moomoo_client_sig: Vec<u8>,
196 /// v1.4.94 G6 (P2 protocol gap): `moomoo_client_key` 解密后的 client key
197 /// (对应 moomoo path), 与 `client_key` 平级. 缺失 → 空 Vec.
198 ///
199 /// 对齐 C++ `auth_impl.cpp:3196,3260` `account.us_client_key_` (经
200 /// `UpdateRandKey` 解密).
201 pub moomoo_client_key: Vec<u8>,
202 /// v1.4.94 G6 (P2 protocol gap): `moomoo_web_sig_new` from `/authority/`
203 /// 响应. 对齐 C++ `auth_impl.cpp:3197,3260` `account.us_web_sig_`. 用于
204 /// moomoo path repull_auth_code (类似 `web_sig` 之于主 path). 缺失 →
205 /// 空字符串.
206 pub moomoo_web_sig: String,
207}