From 0f4a90691446893797da7c1f79feaeea6dc97634 Mon Sep 17 00:00:00 2001 From: Andy Dixon Date: Sun, 11 Oct 2026 17:04:24 +0100 Subject: [PATCH] rust: keychain calls off the tokio runtime; plan reflects as-built crate choices --- docs/rust-port-plan.md | 10 ++++++---- rust/core/src/secrets.rs | 24 ++++++++++++++++-------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/docs/rust-port-plan.md b/docs/rust-port-plan.md index 9965ce0..321bc17 100644 --- a/docs/rust-port-plan.md +++ b/docs/rust-port-plan.md @@ -25,13 +25,13 @@ style of Leviathan Desktop (`../leviathan/rust`). | Terminal emulation | `alacritty_terminal` | VT parser, grid, scrollback, selection and regex search (the same engine Zed uses). We draw it with egui's painter (§6) | | Local shell | `portable-pty` | `forkpty` on Unix, ConPTY on Windows | | Serial | `serialport` | Baud/bits/parity/stop, RTS/CTS and XON/XOFF, DTR/RTS, break, port enumeration | -| IRC | `irc` (tokio, rustls) | Registration, PING, CTCP, NickServ via PRIVMSG; reconnect and channel-state persistence are ours | -| VNC | `vnc-rs` | RFB handshake, VNC auth, Raw/CopyRect/Tight/ZRLE → egui texture | +| IRC | none (our own client over std TCP, `rustls` for TLS) | Registration, PING, CTCP, NickServ via PRIVMSG; reconnect and channel-state persistence are ours | +| VNC | none (our own RFB client; `des`, `flate2`, `image` for Tight JPEG) | RFB 3.3–3.8, VNC auth, Raw/CopyRect/Hextile/ZRLE/Tight → egui texture | | HTTP (GitHub, updater) | `ureq` 3 | Blocking calls on `spawn_blocking` | | SQLite | `rusqlite` (`bundled`) | SQLite compiled in, so no system library is needed | | Vault, export | `argon2`, `aes-gcm`, `rand` | Native Argon2id at 128 MiB takes about 0.3 s | -| Keychain | `keyring` 3 | Must read entries written by go-keyring (§3) | -| TOTP | `totp-rs` | | +| Keychain | `keyring` 3 (pure-Rust D-Bus via zbus on Linux) | Must read entries written by go-keyring (§3) | +| TOTP | none (`hmac` + `sha1`, ~30 lines) | RFC 6238, normalised like pquerna/otp | | JSON | `serde`, `serde_json` | Protocol configs stay JSON columns | | Telnet, RAW, recordings | none (tokio `TcpStream`, serde) | Small ports of the Go code | @@ -235,6 +235,8 @@ Terminal views are repainted only when their session receives data, and for inpu ## 6a. VNC +**As built:** `vnc-rs` was not used; the fallback below (our own RFB client) is what shipped. + - `vnc-rs` on a tokio task, with an optional SSH tunnel through §2's `direct-tcpip`. - Decoded rectangles are applied to an RGBA buffer, and dirty frames update a `TextureHandle`, scaled to fit. - Input: pointer → `PointerEvent`; keys → X11 keysyms (port the keysym map from `vnc.js`). diff --git a/rust/core/src/secrets.rs b/rust/core/src/secrets.rs index 9dc55f3..fea0c80 100644 --- a/rust/core/src/secrets.rs +++ b/rust/core/src/secrets.rs @@ -177,6 +177,12 @@ 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). +fn off_runtime(f: impl FnOnce() -> T + Send + 'static) -> T { + std::thread::spawn(f).join().expect("keychain thread panicked") +} + fn entry(service: &str, key: &str) -> keyring::Result { // go-keyring's Windows target is "service:user"; keyring's default differs. if cfg!(windows) { @@ -199,30 +205,32 @@ impl Keychain { } pub fn available(&self) -> bool { - let probe = "__nexus_keychain_probe__"; - entry(&self.service, probe).and_then(|e| e.set_password("probe").and_then(|_| e.delete_credential())).is_ok() + let service = self.service.clone(); + off_runtime(move || entry(&service, "__nexus_keychain_probe__").and_then(|e| e.set_password("probe").and_then(|_| e.delete_credential())).is_ok()) } /// 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 { - if let Ok(v) = entry(&self.service, key).and_then(|e| e.get_password()) { + 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 = self.legacy.as_deref()?; - let v = entry(legacy, key).and_then(|e| e.get_password()).ok()?; + 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) } pub fn set(&self, key: &str, value: &str) -> Result<()> { - entry(&self.service, key) - .and_then(|e| e.set_password(value)) + let (service, key2, value) = (self.service.clone(), key.to_string(), value.to_string()); + off_runtime(move || entry(&service, &key2).and_then(|e| e.set_password(&value))) .map_err(|e| format!("keychain: failed to store secret {key:?}: {e}").into()) } pub fn delete(&self, key: &str) { - let _ = entry(&self.service, key).and_then(|e| e.delete_credential()); + let (service, key) = (self.service.clone(), key.to_string()); + let _ = off_runtime(move || entry(&service, &key).and_then(|e| e.delete_credential())); } }