1use futu_auth::{
13 KEYRING_USERNAME_TRADE_PWD, keyring_username_for_trade_pwd,
14 secret_store::{PasswordLookup, SecretStoreError},
15};
16
17fn non_empty_trimmed(s: &str) -> Option<String> {
18 let s = s.trim();
19 (!s.is_empty()).then(|| s.to_string())
20}
21
22fn trade_pwd_account_from(
23 explicit: Option<&str>,
24 trade_pwd_account_env: Option<&str>,
25 futu_account_env: Option<&str>,
26) -> Option<String> {
27 explicit
28 .and_then(non_empty_trimmed)
29 .or_else(|| trade_pwd_account_env.and_then(non_empty_trimmed))
30 .or_else(|| futu_account_env.and_then(non_empty_trimmed))
31}
32
33fn read_keyring_password(
34 username: &str,
35) -> Result<Option<String>, futu_auth::secret_store::SecretStoreError> {
36 match futu_auth::secret_store::read_password(username) {
37 Ok(PasswordLookup::Found(password)) => Ok(Some(password)),
38 Ok(PasswordLookup::Empty | PasswordLookup::NoEntry) => Ok(None),
39 Err(SecretStoreError::Backend { .. }) => Ok(None),
42 Err(error) => Err(error),
43 }
44}
45
46fn trade_password_from_sources(
47 account_hint: Option<&str>,
48 trade_pwd_account_env: Option<&str>,
49 futu_account_env: Option<&str>,
50 env_pwd: Option<&str>,
51 mut read_keyring: impl FnMut(&str) -> Result<Option<String>, SecretStoreError>,
52) -> Result<String, String> {
53 if let Some(pwd) = env_pwd.and_then(non_empty_trimmed) {
54 return Ok(pwd);
55 }
56
57 if let Some(account) =
58 trade_pwd_account_from(account_hint, trade_pwd_account_env, futu_account_env)
59 {
60 let scoped_username = keyring_username_for_trade_pwd(&account);
61 if let Some(pwd) = read_keyring(&scoped_username).map_err(keyring_read_error)? {
62 return Ok(pwd);
63 }
64 if let Some(pwd) = read_keyring(KEYRING_USERNAME_TRADE_PWD).map_err(keyring_read_error)? {
65 return Ok(pwd);
66 }
67 return Err(missing_trade_password_message(Some(&account)));
68 }
69
70 if let Some(pwd) = read_keyring(KEYRING_USERNAME_TRADE_PWD).map_err(keyring_read_error)? {
71 return Ok(pwd);
72 }
73 Err(missing_trade_password_message(None))
74}
75
76fn keyring_read_error(error: SecretStoreError) -> String {
77 format!(
78 "OS keychain trade-password lookup failed within the {}-second safety boundary: \
79 {error}; set FUTU_TRADE_PWD instead. {}",
80 futu_auth::secret_store::KEYRING_OPERATION_TIMEOUT.as_secs(),
81 keyring_platform_detail()
82 )
83}
84
85#[cfg(target_os = "macos")]
86fn keyring_platform_detail() -> &'static str {
87 "Packaged cross-binary Keychain sharing is not supported."
88}
89
90#[cfg(not(target_os = "macos"))]
91fn keyring_platform_detail() -> &'static str {
92 "The platform credential-store operation did not finish safely."
93}
94
95#[cfg(target_os = "macos")]
96fn missing_trade_password_message(account: Option<&str>) -> String {
97 let account_hint = account
98 .map(|value| format!(" for account {value}"))
99 .unwrap_or_default();
100 format!(
101 "no trade password configured{account_hint}: set FUTU_TRADE_PWD; packaged macOS \
102 futucli/futu-mcp binaries have distinct ad-hoc signing identities, so \
103 set-trade-pwd Keychain sharing is not supported"
104 )
105}
106
107#[cfg(not(target_os = "macos"))]
108fn missing_trade_password_message(account: Option<&str>) -> String {
109 match account {
110 Some(account) => format!(
111 "no trade password configured for account {account}: run `futucli set-trade-pwd \
112 --account {account}`, then start futu-mcp with `--trade-pwd-account {account}` \
113 (or FUTU_TRADE_PWD_ACCOUNT={account}); alternatively set FUTU_TRADE_PWD"
114 ),
115 None => "no trade password configured: run `futucli set-trade-pwd --account \
116 <login-account>` and start futu-mcp with `--trade-pwd-account \
117 <login-account>` (or set FUTU_TRADE_PWD_ACCOUNT); legacy deployments may \
118 still use FUTU_TRADE_PWD"
119 .to_string(),
120 }
121}
122
123pub fn get_trade_password_for_account(account_hint: Option<&str>) -> Result<String, String> {
125 let trade_pwd_account_env = std::env::var("FUTU_TRADE_PWD_ACCOUNT").ok();
126 let futu_account_env = std::env::var("FUTU_ACCOUNT").ok();
127 let env_pwd = std::env::var("FUTU_TRADE_PWD").ok();
128 trade_password_from_sources(
129 account_hint,
130 trade_pwd_account_env.as_deref(),
131 futu_account_env.as_deref(),
132 env_pwd.as_deref(),
133 read_keyring_password,
134 )
135}
136
137pub fn get_trade_password_md5_for_account(account_hint: Option<&str>) -> Result<String, String> {
139 let pwd = get_trade_password_for_account(account_hint)?;
140 Ok(md5_hex(&pwd))
141}
142
143fn md5_hex(pwd: &str) -> String {
144 format!("{:x}", md5::compute(pwd.as_bytes()))
145}
146
147#[cfg(test)]
148mod tests;