Skip to main content

futu_mcp/
trade_pwd.rs

1//! 交易密码来源封装
2//!
3//! 优先级(对 LLM 透明,它只会调 `futu_unlock_trade` 工具,密码永不入 prompt):
4//!   1. 环境变量 `FUTU_TRADE_PWD`(显式运行时 override)
5//!   2. 账号级 OS keychain —— `futucli set-trade-pwd --account <login-account>`
6//!   3. legacy 全局 OS keychain —— v1.4.109 前的 `trade-password` 兜底读取
7//!   4. 都没有 → 报错
8//!
9//! MD5 在 server 端计算后再发网关。LLM 从来看不到明文 / MD5。
10
11use futu_auth::{KEYRING_SERVICE, KEYRING_USERNAME_TRADE_PWD, keyring_username_for_trade_pwd};
12
13fn non_empty_trimmed(s: &str) -> Option<String> {
14    let s = s.trim();
15    (!s.is_empty()).then(|| s.to_string())
16}
17
18fn trade_pwd_account_from(
19    explicit: Option<&str>,
20    trade_pwd_account_env: Option<&str>,
21    futu_account_env: Option<&str>,
22) -> Option<String> {
23    explicit
24        .and_then(non_empty_trimmed)
25        .or_else(|| trade_pwd_account_env.and_then(non_empty_trimmed))
26        .or_else(|| futu_account_env.and_then(non_empty_trimmed))
27}
28
29fn read_keyring_password(username: &str) -> Option<String> {
30    if let Ok(entry) = keyring::Entry::new(KEYRING_SERVICE, username)
31        && let Ok(pwd) = entry.get_password()
32        && !pwd.is_empty()
33    {
34        return Some(pwd);
35    }
36    None
37}
38
39fn trade_password_from_sources(
40    account_hint: Option<&str>,
41    trade_pwd_account_env: Option<&str>,
42    futu_account_env: Option<&str>,
43    env_pwd: Option<&str>,
44    mut read_keyring: impl FnMut(&str) -> Option<String>,
45) -> Result<String, String> {
46    if let Some(pwd) = env_pwd.and_then(non_empty_trimmed) {
47        return Ok(pwd);
48    }
49
50    if let Some(account) =
51        trade_pwd_account_from(account_hint, trade_pwd_account_env, futu_account_env)
52    {
53        let scoped_username = keyring_username_for_trade_pwd(&account);
54        if let Some(pwd) = read_keyring(&scoped_username) {
55            return Ok(pwd);
56        }
57        if let Some(pwd) = read_keyring(KEYRING_USERNAME_TRADE_PWD) {
58            return Ok(pwd);
59        }
60        return Err(format!(
61            "no trade password configured for account {account}: run `futucli set-trade-pwd \
62             --account {account}`, then start futu-mcp with `--trade-pwd-account {account}` \
63             (or FUTU_TRADE_PWD_ACCOUNT={account}); alternatively set FUTU_TRADE_PWD"
64        ));
65    }
66
67    if let Some(pwd) = read_keyring(KEYRING_USERNAME_TRADE_PWD) {
68        return Ok(pwd);
69    }
70    Err(
71        "no trade password configured: run `futucli set-trade-pwd --account <login-account>` \
72         and start futu-mcp with `--trade-pwd-account <login-account>` \
73         (or set FUTU_TRADE_PWD_ACCOUNT); legacy deployments may still use FUTU_TRADE_PWD"
74            .to_string(),
75    )
76}
77
78/// 试着拿到交易密码明文。成功则返回 `Ok(pwd)`;任何来源都没找到返回 `Err`。
79pub fn get_trade_password_for_account(account_hint: Option<&str>) -> Result<String, String> {
80    let trade_pwd_account_env = std::env::var("FUTU_TRADE_PWD_ACCOUNT").ok();
81    let futu_account_env = std::env::var("FUTU_ACCOUNT").ok();
82    let env_pwd = std::env::var("FUTU_TRADE_PWD").ok();
83    trade_password_from_sources(
84        account_hint,
85        trade_pwd_account_env.as_deref(),
86        futu_account_env.as_deref(),
87        env_pwd.as_deref(),
88        read_keyring_password,
89    )
90}
91
92/// 把密码 MD5 化再返回(unlock_trade RPC 要的格式)
93pub fn get_trade_password_md5_for_account(account_hint: Option<&str>) -> Result<String, String> {
94    let pwd = get_trade_password_for_account(account_hint)?;
95    Ok(md5_hex(&pwd))
96}
97
98fn md5_hex(pwd: &str) -> String {
99    format!("{:x}", md5::compute(pwd.as_bytes()))
100}
101
102#[cfg(test)]
103mod tests;