From e73efa225e073f9bb1292bce939451fa4e81831b Mon Sep 17 00:00:00 2001 From: lodar Date: Thu, 20 Aug 2026 02:27:54 +0000 Subject: [PATCH] feat(pairing-cli): `source --nsec -` reads the key from stdin, never argv MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `buzz-pair source` transfers a real secret key. Today its only form is `--nsec `: an argv element, and argv is world-readable in `/proc//cmdline` for the entire life of the process — which for a `source` session is up to the 120s it spends waiting for the target to show up. Any local user, and anything that snapshots the process table, gets the key. `--nsec -` takes the key from the first line of stdin instead. It is read before any session I/O and through the same buffered `io::stdin()` handle the SAS prompt already uses, a line at a time, so the interactive y/n answer is simply the next line and the existing flow is unchanged: { printf '%s\n' "$nsec"; cat; } | buzz-pair source --nsec - --relay wss://... `--nsec ` keeps working exactly as before; this only adds the `-` form. The clap help text names both `-` and the word stdin so a caller can probe `source --help` and refuse to hand a key to a build that does not support it. Signed-off-by: lodar --- crates/buzz-pairing-cli/src/main.rs | 34 +++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/crates/buzz-pairing-cli/src/main.rs b/crates/buzz-pairing-cli/src/main.rs index 1eb9d215f92..7cd94316ab0 100644 --- a/crates/buzz-pairing-cli/src/main.rs +++ b/crates/buzz-pairing-cli/src/main.rs @@ -3,11 +3,19 @@ //! # Usage //! //! ```text -//! buzz-pair source --relay wss://relay.example.com [--nsec nsec1...] +//! buzz-pair source --relay wss://relay.example.com [--nsec nsec1...|--nsec -] //! buzz-pair target [--relay wss://relay.example.com] //! buzz-pair test-vectors //! ``` //! +//! `--nsec -` reads the key from the first line of stdin instead of argv, so it +//! never lands in the world-readable `/proc//cmdline`. Everything after +//! that first line is still the session's stdin (the y/n SAS answer): +//! +//! ```text +//! { printf '%s\n' "$nsec"; cat; } | buzz-pair source --nsec - --relay wss://... +//! ``` +//! //! The `source` subcommand acts as the secret-holding device; `target` acts //! as the receiving device. Together they exercise the full NIP-AB protocol //! over a live Nostr relay. @@ -50,7 +58,9 @@ enum Cmd { #[arg(long, default_value = "wss://relay.damus.io")] relay: String, - /// nsec (bech32) of the key to transfer. If omitted, generates a test key. + /// nsec (bech32) of the key to transfer, or '-' to read the nsec from + /// the first line of stdin (keeps the key out of argv, which is + /// world-readable in /proc/PID/cmdline). If omitted, generates a test key. #[arg(long)] nsec: Option, }, @@ -578,12 +588,28 @@ fn parse_relay_event(text: &str, sub_id: &str) -> Option { /// /// If `nsec` is provided, parse it as bech32 and return the raw nsec string. /// Otherwise generate a fresh test key and return its nsec. +/// +/// The literal `-` means "read the nsec from the first line of stdin", so the +/// key never appears in argv. fn resolve_payload(nsec: Option) -> Result<(Zeroizing, PayloadType), CliError> { match nsec { Some(s) => { + // `--nsec -` takes the key from the first line of stdin. An argv + // element is world-readable in /proc//cmdline for the whole + // life of the process, and a `source` session waits up to 120s for + // the target, so passing the key literally exposes it for that + // whole window. The read goes through the same buffered + // `io::stdin()` handle the SAS prompt uses, a line at a time, so + // the y/n answer is simply the next line. + let s = if s == "-" { + Zeroizing::new(read_line()?) + } else { + Zeroizing::new(s) + }; // Validate it parses as a secret key. - let _sk = SecretKey::parse(&s).map_err(|e| CliError::InvalidNsec(e.to_string()))?; - Ok((Zeroizing::new(s), PayloadType::Nsec)) + let _sk = + SecretKey::parse(s.as_str()).map_err(|e| CliError::InvalidNsec(e.to_string()))?; + Ok((s, PayloadType::Nsec)) } None => { let keys = Keys::generate();