futu_opend/
credentials.rs1#![allow(unused_imports)]
4
5use anyhow::Result;
6
7use crate::config::{RuntimeConfig, read_explicit_credential_file};
8
9pub fn resolve_login_password(
34 account: Option<&str>,
35 config: &RuntimeConfig,
36) -> Result<Option<(Option<String>, bool)>> {
37 if let Some(path) = &config.login_pwd_file {
43 let pwd = read_explicit_credential_file("--login-pwd-file", path)?;
44 tracing::info!(path = %path, "loaded login password from --login-pwd-file");
45 return Ok(Some((Some(pwd), false)));
46 }
47
48 if let Some(pwd) = &config.login_pwd
50 && !pwd.is_empty()
51 {
52 tracing::warn!(
53 "⚠️ --login-pwd passes plaintext password via argv; visible in `ps aux` \
54 and shell history. Recommended: `futucli set-login-pwd --account {}` \
55 to store in OS keychain, then omit --login-pwd.",
56 account.unwrap_or("<account>")
57 );
58 return Ok(Some((Some(pwd.clone()), false)));
59 }
60
61 if let Some(md5) = &config.login_pwd_md5
63 && !md5.is_empty()
64 {
65 tracing::warn!(
66 "⚠️ --login-pwd-md5 is equivalent to plaintext (can log in directly); \
67 same argv exposure as --login-pwd. Recommended: use `futucli set-login-pwd` \
68 instead."
69 );
70 return Ok(Some((Some(md5.clone()), true)));
71 }
72
73 if let Ok(pwd) = std::env::var("FUTU_PWD")
75 && !pwd.is_empty()
76 {
77 tracing::info!("loaded login password from FUTU_PWD env var");
78 return Ok(Some((Some(pwd), false)));
79 }
80
81 if let Some(acc) = account {
83 tracing::info!(
86 account = acc,
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 = acc, "loaded login password from OS keychain");
94 return Ok(Some((Some(pwd), false)));
95 }
96 Ok(_) => {
97 tracing::debug!(account = acc, "keychain entry exists but is empty");
98 }
99 Err(keyring::Error::NoEntry) => {
100 tracing::debug!(account = acc, "no keychain entry for this account");
101 }
102 Err(e) => {
103 tracing::warn!(account = acc, 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 {acc}` 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}