futu_opend/
credentials.rs1use anyhow::Result;
4
5use crate::config::{RuntimeConfig, read_explicit_credential_file};
6
7pub fn resolve_login_password(
32 account: Option<&str>,
33 config: &RuntimeConfig,
34) -> Result<Option<(Option<String>, bool)>> {
35 if let Some(path) = &config.login_pwd_file {
41 let pwd = read_explicit_credential_file("--login-pwd-file", path)?;
42 tracing::info!(path = %path, "loaded login password from --login-pwd-file");
43 return Ok(Some((Some(pwd), false)));
44 }
45
46 if let Some(pwd) = &config.login_pwd
48 && !pwd.is_empty()
49 {
50 let account_fp = account.map(futu_backend::auth::redact::account_log_fingerprint);
51 tracing::warn!(
52 account_fp = ?account_fp,
53 "⚠️ --login-pwd passes plaintext password via argv; visible in `ps aux` \
54 and shell history. Recommended: `futucli set-login-pwd --account <account>` \
55 to store in OS keychain, then omit --login-pwd."
56 );
57 return Ok(Some((Some(pwd.clone()), false)));
58 }
59
60 if let Some(md5) = &config.login_pwd_md5
62 && !md5.is_empty()
63 {
64 tracing::warn!(
65 "⚠️ --login-pwd-md5 is equivalent to plaintext (can log in directly); \
66 same argv exposure as --login-pwd. Recommended: use `futucli set-login-pwd` \
67 instead."
68 );
69 return Ok(Some((Some(md5.clone()), true)));
70 }
71
72 if let Ok(pwd) = std::env::var("FUTU_PWD")
74 && !pwd.is_empty()
75 {
76 tracing::info!("loaded login password from FUTU_PWD env var");
77 return Ok(Some((Some(pwd), false)));
78 }
79
80 if let Some(acc) = account {
82 let account_fp = futu_backend::auth::redact::account_log_fingerprint(acc);
83 tracing::info!(
86 account_fp = %account_fp,
87 "loading login password from OS keychain (may take ~10s on first unlock)"
88 );
89 let username = futu_auth::keyring_username_for_login_pwd(acc);
90 match keyring::Entry::new(futu_auth::KEYRING_SERVICE, &username) {
91 Ok(entry) => match entry.get_password() {
92 Ok(pwd) if !pwd.is_empty() => {
93 tracing::info!(account_fp = %account_fp, "loaded login password from OS keychain");
94 return Ok(Some((Some(pwd), false)));
95 }
96 Ok(_) => {
97 tracing::debug!(account_fp = %account_fp, "keychain entry exists but is empty");
98 }
99 Err(keyring::Error::NoEntry) => {
100 tracing::debug!(account_fp = %account_fp, "no keychain entry for this account");
101 }
102 Err(e) => {
103 tracing::warn!(account_fp = %account_fp, error = %e, "keychain read failed");
104 }
105 },
106 Err(e) => {
107 tracing::warn!(error = %e, "keychain backend unavailable");
108 }
109 }
110 }
111
112 if std::io::IsTerminal::is_terminal(&std::io::stdin())
114 && let Some(acc) = account
115 {
116 match rpassword::prompt_password(format!("Login password for account {acc}: ")) {
117 Ok(pwd) if !pwd.is_empty() => {
118 tracing::info!("loaded login password from interactive prompt");
119 eprintln!(
121 " tip: run `futucli set-login-pwd --account <account>` once to \
122 skip this prompt next time."
123 );
124 return Ok(Some((Some(pwd), false)));
125 }
126 Ok(_) => {
127 tracing::warn!("empty password from prompt");
128 }
129 Err(e) => {
130 tracing::warn!(error = %e, "prompt_password failed");
131 }
132 }
133 }
134
135 Ok(None)
137}
138
139#[cfg(test)]
140mod tests;