Files
openidea/crates/ide-ssh/tests/os_credential_store.rs
Fabio Nunes ad27a7064a fix(security): keep saved passwords in the OS credential store (#910)
SSH passwords and key passphrases, git HTTPS credentials, the HTTP proxy
password and data source passwords were kept in plaintext JSON files in
the app data directory. They now live in the OS credential store via
keyring 4 (macOS Keychain, Windows Credential Manager, Secret Service on
Linux); each file keeps only an index of keys under a per-directory id.

Files from older versions are moved key by key on first use, with a
.migrating sidecar so a failed or interrupted move retries without
duplicating credentials. Without a credential store (headless Linux)
the old file is still read but nothing new is written to disk; the
secret lasts for the session. New data sources no longer save their
password or SSH tunnel secret unless asked.

Also keeps the index path in SecretStore for global data sources and
runs their test against the in-memory store.

Fixes #740 (credentials part)

Co-authored-by: LittleGuava <52166187+LittleGuava@users.noreply.github.com>
2026-11-05 09:47:19 +08:00

45 lines
1.8 KiB
Rust

//! Saved passwords against the real OS credential store (Windows Credential
//! Manager, macOS Keychain, Secret Service).
use ide_ssh::secrets::SecretFile;
/// Forgets every key of the file, also when the test panics.
struct Forget<'a>(&'a SecretFile);
impl Drop for Forget<'_> {
fn drop(&mut self) {
for key in self.0.keys() {
self.0.delete(&key);
}
}
}
#[test]
#[ignore = "uses the OS credential store"]
fn saves_moves_and_forgets_in_the_os_store() {
assert!(keyring::Entry::store_status().is_ok(), "no OS credential store");
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("git-credentials.json");
std::fs::write(&path, r#"{"https://old.example": "me\nold-os-pw"}"#).unwrap();
let file = SecretFile::new(path.clone());
let forget = Forget(&file);
file.set("https://new.example", "me\nnew-os-pw").unwrap();
let on_disk = std::fs::read_to_string(&path).unwrap();
assert!(!on_disk.contains("old-os-pw") && !on_disk.contains("new-os-pw"), "{on_disk}");
let reopened = SecretFile::new(path);
assert_eq!(reopened.get("https://old.example").as_deref(), Some("me\nold-os-pw"));
assert_eq!(reopened.get("https://new.example").as_deref(), Some("me\nnew-os-pw"));
#[cfg(windows)]
{
let index: serde_json::Value = serde_json::from_str(&on_disk).unwrap();
let service = format!("ai.l0ng.openidea/{}", index["id"].as_str().unwrap());
let listed = std::process::Command::new("cmdkey").arg("/list").output().unwrap();
let listed = String::from_utf8_lossy(&listed.stdout);
assert_eq!(listed.matches(service.as_str()).count(), 2, "{listed}");
}
drop(forget);
assert_eq!(reopened.keys(), Vec::<String>::new());
assert_eq!(reopened.get("https://old.example"), None);
}