Offer to save a password when a login is submitted
Submitting a form containing a password now offers to remember it. It goes into the macOS Keychain, through the Security framework rather than the `security` binary - a password passed as a command-line argument is visible in `ps` to anyone on the machine, however briefly. Never apps.json, never a log. The value travels as little as it can: the injected script hands it straight to Rust, which holds it in memory and tells the shell only which host and which username, since that is all the shell needs to ask the question. It is written on Save and dropped on anything else. Autofill is deliberately not built. Reading a password back out and injecting it into a page is a materially larger surface than offering to store one, and deserves its own decision.
This commit is contained in:
@@ -52,6 +52,16 @@ pub struct NotificationClick {
|
||||
pub notification_id: String,
|
||||
}
|
||||
|
||||
/// A login worth offering to remember. Carries no password: the value stays in
|
||||
/// Rust until it is either written to the Keychain or dropped.
|
||||
#[derive(Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PasswordOffer {
|
||||
pub app_id: String,
|
||||
pub host: String,
|
||||
pub account: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct IconEvent {
|
||||
@@ -247,6 +257,27 @@ fn handle_sentinel(
|
||||
}
|
||||
}
|
||||
|
||||
// A login was just submitted. The password is held here and offered;
|
||||
// the shell is told only the host and the username, because that is
|
||||
// all it needs to ask the question and the less that value travels
|
||||
// the better.
|
||||
"savepw" => {
|
||||
let (Some(host), Some(pass)) = (params.get("h"), params.get("p")) else {
|
||||
return;
|
||||
};
|
||||
if pass.is_empty() {
|
||||
return;
|
||||
}
|
||||
let account = params.get("u").cloned().unwrap_or_default();
|
||||
let state = handle.state::<crate::commands::AppState>();
|
||||
*state.pending_password.lock().unwrap() =
|
||||
Some((host.clone(), account.clone(), pass.clone()));
|
||||
let _ = handle.emit(
|
||||
"password-offer",
|
||||
PasswordOffer { app_id: from.clone(), host: host.clone(), account },
|
||||
);
|
||||
}
|
||||
|
||||
"emptycache" => {
|
||||
empty_cache(&handle, &from);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user