refactor: drop the AlchementaryLink keychain fallback and Go's Windows target format

Keychain reads no longer fall back to the AlchementaryLink service and
copy entries forward, and Windows Credential Manager entries use
keyring's own target naming instead of go-keyring's 'service:user'.
Credentials saved by Alchementary Link, or by earlier builds on Windows,
are no longer found and need re-entering.
This commit is contained in:
Andy Dixon
2026-10-12 10:06:38 +01:00
parent d68343365b
commit 88c5cca021
4 changed files with 10 additions and 22 deletions
+6 -18
View File
@@ -249,8 +249,6 @@ pub fn write_private(path: &std::path::Path, data: &[u8]) -> Result<()> {
}
pub const KEYCHAIN_SERVICE: &str = "NexusTerminal";
/// The service name Alchementary Link wrote under; read as a fallback (§7 of the port plan).
pub const LEGACY_KEYCHAIN_SERVICE: &str = "AlchementaryLink";
/// Runs a keychain call on a plain thread: the D-Bus backend may start its own async runtime,
/// which panics if the caller is already inside tokio (as SSH authentication is).
@@ -259,20 +257,18 @@ fn off_runtime<T: Send + 'static>(f: impl FnOnce() -> T + Send + 'static) -> T {
}
fn entry(service: &str, key: &str) -> keyring::Result<keyring::Entry> {
// go-keyring's Windows target is "service:user"; keyring's default differs.
if cfg!(windows) { keyring::Entry::new_with_target(&format!("{service}:{key}"), service, key) } else { keyring::Entry::new(service, key) }
keyring::Entry::new(service, key)
}
/// The OS keychain. Every call can fail (no secret service, locked keyring); callers fall back
/// to the vault.
pub struct Keychain {
service: String,
legacy: Option<String>,
}
impl Keychain {
pub fn new(service: &str, legacy: Option<&str>) -> Keychain {
Keychain { service: service.into(), legacy: legacy.map(Into::into) }
pub fn new(service: &str) -> Keychain {
Keychain { service: service.into() }
}
/// Probes with a read of an entry that doesn't exist: "no entry" proves the keychain answers,
@@ -283,17 +279,9 @@ impl Keychain {
off_runtime(move || matches!(entry(&service, "__nexus_keychain_probe__").and_then(|e| e.get_password()), Ok(_) | Err(keyring::Error::NoEntry)))
}
/// Reads a secret; a miss under the current service falls back to the legacy one and copies
/// the value forward. Legacy items are never deleted, so the old app keeps working.
pub fn get(&self, key: &str) -> Option<String> {
let (service, key2) = (self.service.clone(), key.to_string());
if let Ok(v) = off_runtime(move || entry(&service, &key2).and_then(|e| e.get_password())) {
return Some(v);
}
let (legacy, key2) = (self.legacy.clone()?, key.to_string());
let v = off_runtime(move || entry(&legacy, &key2).and_then(|e| e.get_password())).ok()?;
let _ = self.set(key, &v);
Some(v)
let (service, key) = (self.service.clone(), key.to_string());
off_runtime(move || entry(&service, &key).and_then(|e| e.get_password())).ok()
}
pub fn set(&self, key: &str, value: &str) -> Result<()> {
@@ -439,7 +427,7 @@ mod tests {
fn deleting_a_profile_clears_every_vault_kind() {
let dir = std::env::temp_dir().join(format!("nexus-delprof-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let s = Secrets { vault: Vault::new(dir.clone()), keychain: Keychain::new("NexusTerminalTest", None), keychain_ok: false };
let s = Secrets { vault: Vault::new(dir.clone()), keychain: Keychain::new("NexusTerminalTest"), keychain_ok: false };
s.vault.initialise("pw").unwrap();
for kind in SecretKind::ALL {
s.vault.set(&kind.key("p1"), "v").unwrap();
+2 -2
View File
@@ -6,7 +6,7 @@ use crate::clients::sftp::{self, FileEntry};
use crate::config::AppConfig;
use crate::models::*;
use crate::secrets::SecretKind;
use crate::secrets::{KEYCHAIN_SERVICE, Keychain, LEGACY_KEYCHAIN_SERVICE, Secrets, Vault};
use crate::secrets::{KEYCHAIN_SERVICE, Keychain, Secrets, Vault};
use crate::sessions::Manager;
use crate::storage::Store;
use crate::{Result, exportimport, keyfiles, sshconfig, updater, validation};
@@ -39,7 +39,7 @@ pub struct SshKeyInfo {
impl Service {
pub fn open(config: AppConfig) -> Result<Service> {
let store = Arc::new(Store::open(&config.db_path)?);
let secrets = Arc::new(Secrets::new(Vault::new(config.vault_dir.clone()), Keychain::new(KEYCHAIN_SERVICE, Some(LEGACY_KEYCHAIN_SERVICE))));
let secrets = Arc::new(Secrets::new(Vault::new(config.vault_dir.clone()), Keychain::new(KEYCHAIN_SERVICE)));
let sessions = Manager::new(store.clone(), secrets.clone(), config.recordings_dir.clone());
Ok(Service { config, store, secrets, sessions })
}
+1 -1
View File
@@ -1452,7 +1452,7 @@ mod tests {
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let store = Arc::new(Store::open(&dir.join("nexus.db")).unwrap());
let secrets = Arc::new(Secrets { vault: Vault::new(dir.join("vault")), keychain: Keychain::new("NexusTerminalTest", None), keychain_ok: false });
let secrets = Arc::new(Secrets { vault: Vault::new(dir.join("vault")), keychain: Keychain::new("NexusTerminalTest"), keychain_ok: false });
(Manager::new(store, secrets, dir.join("recordings")), dir)
}
+1 -1
View File
@@ -16,7 +16,7 @@ fn manager(name: &str) -> (Arc<Manager>, mpsc::Receiver<Event>, std::path::PathB
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let store = Arc::new(Store::open(&dir.join("nexus.db")).unwrap());
let secrets = Arc::new(Secrets { vault: Vault::new(dir.join("vault")), keychain: Keychain::new("NexusTerminalTest", None), keychain_ok: false });
let secrets = Arc::new(Secrets { vault: Vault::new(dir.join("vault")), keychain: Keychain::new("NexusTerminalTest"), keychain_ok: false });
let m = Manager::new(store, secrets, dir.join("recordings"));
let (tx, rx) = mpsc::channel();
let tx = Mutex::new(tx);