From e8d8a816dd79923c309d64e42d6476faeb76df7c Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 11 Dec 2024 03:53:00 -0800 Subject: [PATCH] [PM-14988] Use peercred / GetNamedPipeClientProcessId to gather info about process connecting to ssh agent (#12065) * Fix double prompt when unlocking by ssh request * Add peercred for unix * Enable apple-app-store feature * Add generic parameter * Update * Add procinfo for windows * Show connecting app in ui * Use struct instead of tuple * Use atomics instead of mutex * Fix windows build * Use is_running function * Cleanup named pipe listener * Cleanups * Cargo fmt * Replace "" with none * Rebuild index.d.ts * Fix is running check --- apps/desktop/desktop_native/Cargo.lock | 71 +++++++++++++++- apps/desktop/desktop_native/core/Cargo.toml | 4 +- .../core/src/biometric/windows.rs | 19 +++-- .../desktop_native/core/src/password/macos.rs | 12 ++- .../desktop_native/core/src/password/unix.rs | 48 ++++++----- .../core/src/password/windows.rs | 12 ++- .../desktop_native/core/src/ssh_agent/mod.rs | 83 +++++++++++++------ .../ssh_agent/named_pipe_listener_stream.rs | 49 ++++++++--- .../peercred_unix_listener_stream.rs | 72 ++++++++++++++++ .../core/src/ssh_agent/peerinfo/gather.rs | 23 +++++ .../core/src/ssh_agent/peerinfo/mod.rs | 2 + .../core/src/ssh_agent/peerinfo/models.rs | 32 +++++++ .../desktop_native/core/src/ssh_agent/unix.rs | 30 ++++--- .../core/src/ssh_agent/windows.rs | 23 +++-- apps/desktop/desktop_native/napi/index.d.ts | 2 +- apps/desktop/desktop_native/napi/src/lib.rs | 24 +++--- .../napi/src/registry/windows.rs | 2 +- .../platform/main/main-ssh-agent.service.ts | 3 +- .../platform/services/ssh-agent.service.ts | 6 +- 19 files changed, 411 insertions(+), 106 deletions(-) create mode 100644 apps/desktop/desktop_native/core/src/ssh_agent/peercred_unix_listener_stream.rs create mode 100644 apps/desktop/desktop_native/core/src/ssh_agent/peerinfo/gather.rs create mode 100644 apps/desktop/desktop_native/core/src/ssh_agent/peerinfo/mod.rs create mode 100644 apps/desktop/desktop_native/core/src/ssh_agent/peerinfo/models.rs diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 1cf8b24c26..09d3d15e89 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -338,7 +338,7 @@ checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "bitwarden-russh" version = "0.1.0" -source = "git+https://github.com/bitwarden/bitwarden-russh.git?rev=b4e7f2fedbe3df8c35545feb000176d3e7b2bc32#b4e7f2fedbe3df8c35545feb000176d3e7b2bc32" +source = "git+https://github.com/bitwarden/bitwarden-russh.git?rev=23b50e3bbe6d56ef19ab0e98e8bb1462cb6d77ae#23b50e3bbe6d56ef19ab0e98e8bb1462cb6d77ae" dependencies = [ "anyhow", "byteorder", @@ -584,6 +584,25 @@ dependencies = [ "libc", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.20" @@ -773,6 +792,7 @@ dependencies = [ "sha2", "ssh-encoding", "ssh-key", + "sysinfo", "thiserror", "tokio", "tokio-stream", @@ -903,6 +923,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + [[package]] name = "embed_plist" version = "1.2.2" @@ -1490,6 +1516,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "ntapi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +dependencies = [ + "winapi", +] + [[package]] name = "num" version = "0.4.3" @@ -2051,6 +2086,26 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "recvmsg" version = "1.0.0" @@ -2453,6 +2508,20 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sysinfo" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b5ae3f4f7d64646c46c4cae4e3f01d1c5d255c7406fdd7c7f999a94e488791" +dependencies = [ + "core-foundation-sys", + "libc", + "memchr", + "ntapi", + "rayon", + "windows 0.57.0", +] + [[package]] name = "tempfile" version = "3.14.0" diff --git a/apps/desktop/desktop_native/core/Cargo.toml b/apps/desktop/desktop_native/core/Cargo.toml index adfdd818a1..597a082b23 100644 --- a/apps/desktop/desktop_native/core/Cargo.toml +++ b/apps/desktop/desktop_native/core/Cargo.toml @@ -49,7 +49,7 @@ ssh-key = { version = "=0.6.7", default-features = false, features = [ "rsa", "getrandom", ] } -bitwarden-russh = { git = "https://github.com/bitwarden/bitwarden-russh.git", rev = "b4e7f2fedbe3df8c35545feb000176d3e7b2bc32" } +bitwarden-russh = { git = "https://github.com/bitwarden/bitwarden-russh.git", rev = "23b50e3bbe6d56ef19ab0e98e8bb1462cb6d77ae" } tokio = { version = "=1.41.1", features = ["io-util", "sync", "macros", "net"] } tokio-stream = { version = "=0.1.15", features = ["net"] } tokio-util = { version = "=0.7.12", features = ["codec"] } @@ -59,6 +59,7 @@ rand_chacha = "=0.3.1" pkcs8 = { version = "=0.10.2", features = ["alloc", "encryption", "pem"] } rsa = "=0.9.6" ed25519 = { version = "=2.2.3", features = ["pkcs8"] } +sysinfo = { version = "0.32.0", features = ["windows"] } [target.'cfg(windows)'.dependencies] widestring = { version = "=1.1.0", optional = true } @@ -72,6 +73,7 @@ windows = { version = "=0.58.0", features = [ "Win32_System_WinRT", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_WindowsAndMessaging", + "Win32_System_Pipes", ], optional = true } [target.'cfg(windows)'.dev-dependencies] diff --git a/apps/desktop/desktop_native/core/src/biometric/windows.rs b/apps/desktop/desktop_native/core/src/biometric/windows.rs index fcc5b95cc4..d17ea75287 100644 --- a/apps/desktop/desktop_native/core/src/biometric/windows.rs +++ b/apps/desktop/desktop_native/core/src/biometric/windows.rs @@ -310,12 +310,16 @@ mod tests { os_key_part_b64: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_owned(), client_key_part_b64: Some("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_owned()), }; - crate::password::set_password(test, test, secret).await.unwrap(); + crate::password::set_password(test, test, secret) + .await + .unwrap(); let result = ::get_biometric_secret(test, test, Some(key_material)) .await .unwrap(); - crate::password::delete_password("test", "test").await.unwrap(); + crate::password::delete_password("test", "test") + .await + .unwrap(); assert_eq!(result, secret); } @@ -328,19 +332,24 @@ mod tests { os_key_part_b64: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_owned(), client_key_part_b64: Some("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_owned()), }; - crate::password::set_password(test, test, &secret.to_string()).await.unwrap(); + crate::password::set_password(test, test, &secret.to_string()) + .await + .unwrap(); let result = ::get_biometric_secret(test, test, Some(key_material)) .await .unwrap(); - crate::password::delete_password("test", "test").await.unwrap(); + crate::password::delete_password("test", "test") + .await + .unwrap(); assert_eq!(result, "secret"); } #[tokio::test] async fn set_biometric_secret_requires_key() { - let result = ::set_biometric_secret("", "", "", None, "").await; + let result = + ::set_biometric_secret("", "", "", None, "").await; assert!(result.is_err()); assert_eq!( result.unwrap_err().to_string(), diff --git a/apps/desktop/desktop_native/core/src/password/macos.rs b/apps/desktop/desktop_native/core/src/password/macos.rs index c911a0d243..b69854905d 100644 --- a/apps/desktop/desktop_native/core/src/password/macos.rs +++ b/apps/desktop/desktop_native/core/src/password/macos.rs @@ -28,12 +28,18 @@ mod tests { #[tokio::test] async fn test() { - set_password("BitwardenTest", "BitwardenTest", "Random").await.unwrap(); + set_password("BitwardenTest", "BitwardenTest", "Random") + .await + .unwrap(); assert_eq!( "Random", - get_password("BitwardenTest", "BitwardenTest").await.unwrap() + get_password("BitwardenTest", "BitwardenTest") + .await + .unwrap() ); - delete_password("BitwardenTest", "BitwardenTest").await.unwrap(); + delete_password("BitwardenTest", "BitwardenTest") + .await + .unwrap(); // Ensure password is deleted match get_password("BitwardenTest", "BitwardenTest").await { diff --git a/apps/desktop/desktop_native/core/src/password/unix.rs b/apps/desktop/desktop_native/core/src/password/unix.rs index 20a79625ef..f73b41de8c 100644 --- a/apps/desktop/desktop_native/core/src/password/unix.rs +++ b/apps/desktop/desktop_native/core/src/password/unix.rs @@ -5,9 +5,7 @@ use std::collections::HashMap; pub async fn get_password(service: &str, account: &str) -> Result { match get_password_new(service, account).await { Ok(res) => Ok(res), - Err(_) => { - get_password_legacy(service, account).await - } + Err(_) => get_password_legacy(service, account).await, } } @@ -20,8 +18,8 @@ async fn get_password_new(service: &str, account: &str) -> Result { Some(res) => { let secret = res.secret().await?; Ok(String::from_utf8(secret.to_vec())?) - }, - None => Err(anyhow!("no result")) + } + None => Err(anyhow!("no result")), } } @@ -37,20 +35,30 @@ async fn get_password_legacy(service: &str, account: &str) -> Result { match res { Some(res) => { let secret = res.secret().await?; - println!("deleting legacy secret service entry {} {}", service, account); + println!( + "deleting legacy secret service entry {} {}", + service, account + ); keyring.delete(&attributes).await?; let secret_string = String::from_utf8(secret.to_vec())?; set_password(service, account, &secret_string).await?; Ok(secret_string) - }, - None => Err(anyhow!("no result")) + } + None => Err(anyhow!("no result")), } } pub async fn set_password(service: &str, account: &str, password: &str) -> Result<()> { let keyring = oo7::Keyring::new().await?; let attributes = HashMap::from([("service", service), ("account", account)]); - keyring.create_item("org.freedesktop.Secret.Generic", &attributes, password, true).await?; + keyring + .create_item( + "org.freedesktop.Secret.Generic", + &attributes, + password, + true, + ) + .await?; Ok(()) } @@ -74,22 +82,25 @@ mod tests { #[tokio::test] async fn test() { - set_password("BitwardenTest", "BitwardenTest", "Random").await.unwrap(); + set_password("BitwardenTest", "BitwardenTest", "Random") + .await + .unwrap(); assert_eq!( "Random", - get_password("BitwardenTest", "BitwardenTest").await.unwrap() + get_password("BitwardenTest", "BitwardenTest") + .await + .unwrap() ); - delete_password("BitwardenTest", "BitwardenTest").await.unwrap(); + delete_password("BitwardenTest", "BitwardenTest") + .await + .unwrap(); // Ensure password is deleted match get_password("BitwardenTest", "BitwardenTest").await { Ok(_) => { panic!("Got a result") } - Err(e) => assert_eq!( - "no result", - e.to_string() - ), + Err(e) => assert_eq!("no result", e.to_string()), } } @@ -97,10 +108,7 @@ mod tests { async fn test_error_no_password() { match get_password("Unknown", "Unknown").await { Ok(_) => panic!("Got a result"), - Err(e) => assert_eq!( - "no result", - e.to_string() - ), + Err(e) => assert_eq!("no result", e.to_string()), } } } diff --git a/apps/desktop/desktop_native/core/src/password/windows.rs b/apps/desktop/desktop_native/core/src/password/windows.rs index 873e717ac8..2a66640286 100644 --- a/apps/desktop/desktop_native/core/src/password/windows.rs +++ b/apps/desktop/desktop_native/core/src/password/windows.rs @@ -112,12 +112,18 @@ mod tests { #[tokio::test] async fn test() { - set_password("BitwardenTest", "BitwardenTest", "Random").await.unwrap(); + set_password("BitwardenTest", "BitwardenTest", "Random") + .await + .unwrap(); assert_eq!( "Random", - get_password("BitwardenTest", "BitwardenTest").await.unwrap() + get_password("BitwardenTest", "BitwardenTest") + .await + .unwrap() ); - delete_password("BitwardenTest", "BitwardenTest").await.unwrap(); + delete_password("BitwardenTest", "BitwardenTest") + .await + .unwrap(); // Ensure password is deleted match get_password("BitwardenTest", "BitwardenTest").await { diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs b/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs index 9d04ea87cc..82b90c7bff 100644 --- a/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs +++ b/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs @@ -1,4 +1,7 @@ -use std::sync::Arc; +use std::sync::{ + atomic::{AtomicBool, AtomicU32}, + Arc, +}; use tokio::sync::Mutex; use tokio_util::sync::CancellationToken; @@ -10,34 +13,52 @@ use bitwarden_russh::ssh_agent::{self, Key}; #[cfg_attr(target_os = "linux", path = "unix.rs")] mod platform_ssh_agent; +#[cfg(any(target_os = "linux", target_os = "macos"))] +mod peercred_unix_listener_stream; + pub mod generator; pub mod importer; - +pub mod peerinfo; #[derive(Clone)] pub struct BitwardenDesktopAgent { keystore: ssh_agent::KeyStore, cancellation_token: CancellationToken, - show_ui_request_tx: tokio::sync::mpsc::Sender<(u32, (String, bool))>, + show_ui_request_tx: tokio::sync::mpsc::Sender, get_ui_response_rx: Arc>>, - request_id: Arc>, + request_id: Arc, /// before first unlock, or after account switching, listing keys should require an unlock to get a list of public keys - needs_unlock: Arc>, - is_running: Arc>, + needs_unlock: Arc, + is_running: Arc, } -impl ssh_agent::Agent for BitwardenDesktopAgent { - async fn confirm(&self, ssh_key: Key) -> bool { - if !*self.is_running.lock().await { +pub struct SshAgentUIRequest { + pub request_id: u32, + pub cipher_id: Option, + pub process_name: String, + pub is_list: bool, +} + +impl ssh_agent::Agent for BitwardenDesktopAgent { + async fn confirm(&self, ssh_key: Key, info: &peerinfo::models::PeerInfo) -> bool { + if !self.is_running() { println!("[BitwardenDesktopAgent] Agent is not running, but tried to call confirm"); return false; } let request_id = self.get_request_id().await; + println!( + "[SSH Agent] Confirming request from application: {}", + info.process_name() + ); let mut rx_channel = self.get_ui_response_rx.lock().await.resubscribe(); - let message = (request_id, (ssh_key.cipher_uuid.clone(), false)); self.show_ui_request_tx - .send(message) + .send(SshAgentUIRequest { + request_id, + cipher_id: Some(ssh_key.cipher_uuid.clone()), + process_name: info.process_name().to_string(), + is_list: false, + }) .await .expect("Should send request to ui"); while let Ok((id, response)) = rx_channel.recv().await { @@ -48,15 +69,20 @@ impl ssh_agent::Agent for BitwardenDesktopAgent { false } - async fn can_list(&self) -> bool { - if !*self.needs_unlock.lock().await{ + async fn can_list(&self, info: &peerinfo::models::PeerInfo) -> bool { + if !self.needs_unlock.load(std::sync::atomic::Ordering::Relaxed) { return true; } let request_id = self.get_request_id().await; let mut rx_channel = self.get_ui_response_rx.lock().await.resubscribe(); - let message = (request_id, ("".to_string(), true)); + let message = SshAgentUIRequest { + request_id, + cipher_id: None, + process_name: info.process_name().to_string(), + is_list: true, + }; self.show_ui_request_tx .send(message) .await @@ -72,13 +98,13 @@ impl ssh_agent::Agent for BitwardenDesktopAgent { impl BitwardenDesktopAgent { pub fn stop(&self) { - if !*self.is_running.blocking_lock() { + if !self.is_running() { println!("[BitwardenDesktopAgent] Tried to stop agent while it is not running"); return; } - *self.is_running.blocking_lock() = false; - self.cancellation_token.cancel(); + self.is_running + .store(false, std::sync::atomic::Ordering::Relaxed); self.keystore .0 .write() @@ -90,7 +116,7 @@ impl BitwardenDesktopAgent { &mut self, new_keys: Vec<(String, String, String)>, ) -> Result<(), anyhow::Error> { - if !*self.is_running.blocking_lock() { + if !self.is_running() { return Err(anyhow::anyhow!( "[BitwardenDesktopAgent] Tried to set keys while agent is not running" )); @@ -99,7 +125,8 @@ impl BitwardenDesktopAgent { let keystore = &mut self.keystore; keystore.0.write().expect("RwLock is not poisoned").clear(); - *self.needs_unlock.blocking_lock() = false; + self.needs_unlock + .store(true, std::sync::atomic::Ordering::Relaxed); for (key, name, cipher_id) in new_keys.iter() { match parse_key_safe(&key) { @@ -127,7 +154,7 @@ impl BitwardenDesktopAgent { } pub fn lock(&mut self) -> Result<(), anyhow::Error> { - if !*self.is_running.blocking_lock() { + if !self.is_running() { return Err(anyhow::anyhow!( "[BitwardenDesktopAgent] Tried to lock agent, but it is not running" )); @@ -148,24 +175,26 @@ impl BitwardenDesktopAgent { pub fn clear_keys(&mut self) -> Result<(), anyhow::Error> { let keystore = &mut self.keystore; keystore.0.write().expect("RwLock is not poisoned").clear(); - *self.needs_unlock.blocking_lock() = true; + self.needs_unlock + .store(true, std::sync::atomic::Ordering::Relaxed); Ok(()) } async fn get_request_id(&self) -> u32 { - if !*self.is_running.lock().await { + if !self.is_running() { println!("[BitwardenDesktopAgent] Agent is not running, but tried to get request id"); return 0; } - let mut request_id = self.request_id.lock().await; - *request_id += 1; - *request_id + let request_id = self + .request_id + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + request_id } - pub fn is_running(self) -> bool { - return self.is_running.blocking_lock().clone(); + pub fn is_running(&self) -> bool { + self.is_running.load(std::sync::atomic::Ordering::Relaxed) } } diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/named_pipe_listener_stream.rs b/apps/desktop/desktop_native/core/src/ssh_agent/named_pipe_listener_stream.rs index 49c3aa8061..1358abe32e 100644 --- a/apps/desktop/desktop_native/core/src/ssh_agent/named_pipe_listener_stream.rs +++ b/apps/desktop/desktop_native/core/src/ssh_agent/named_pipe_listener_stream.rs @@ -1,23 +1,32 @@ -use std::{ - io, pin::Pin, sync::Arc, task::{Context, Poll} -}; - use futures::Stream; +use std::os::windows::prelude::AsRawHandle as _; +use std::{ + io, + pin::Pin, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, + task::{Context, Poll}, +}; use tokio::{ net::windows::named_pipe::{NamedPipeServer, ServerOptions}, select, }; use tokio_util::sync::CancellationToken; +use windows::Win32::{Foundation::HANDLE, System::Pipes::GetNamedPipeClientProcessId}; + +use crate::ssh_agent::peerinfo::{self, models::PeerInfo}; const PIPE_NAME: &str = r"\\.\pipe\openssh-ssh-agent"; #[pin_project::pin_project] pub struct NamedPipeServerStream { - rx: tokio::sync::mpsc::Receiver, + rx: tokio::sync::mpsc::Receiver<(NamedPipeServer, PeerInfo)>, } impl NamedPipeServerStream { - pub fn new(cancellation_token: CancellationToken, is_running: Arc>) -> Self { + pub fn new(cancellation_token: CancellationToken, is_running: Arc) -> Self { let (tx, rx) = tokio::sync::mpsc::channel(16); tokio::spawn(async move { println!( @@ -30,7 +39,7 @@ impl NamedPipeServerStream { println!("[SSH Agent Native Module] Encountered an error creating the first pipe. The system's openssh service must likely be disabled"); println!("[SSH Agent Natvie Module] error: {}", err); cancellation_token.cancel(); - *is_running.lock().await = false; + is_running.store(false, Ordering::Relaxed); return; } }; @@ -43,14 +52,32 @@ impl NamedPipeServerStream { } _ = listener.connect() => { println!("[SSH Agent Native Module] Incoming connection"); - tx.send(listener).await.unwrap(); + let handle = HANDLE(listener.as_raw_handle()); + let mut pid = 0; + unsafe { + if let Err(e) = GetNamedPipeClientProcessId(handle, &mut pid) { + println!("Error getting named pipe client process id {}", e); + continue + } + }; + + let peer_info = peerinfo::gather::get_peer_info(pid as u32); + let peer_info = match peer_info { + Err(err) => { + println!("Failed getting process info for pid {} {}", pid, err); + continue + }, + Ok(info) => info, + }; + + tx.send((listener, peer_info)).await.unwrap(); listener = match ServerOptions::new().create(PIPE_NAME) { Ok(pipe) => pipe, Err(err) => { println!("[SSH Agent Native Module] Encountered an error creating a new pipe {}", err); cancellation_token.cancel(); - *is_running.lock().await = false; + is_running.store(false, Ordering::Relaxed); return; } }; @@ -63,12 +90,12 @@ impl NamedPipeServerStream { } impl Stream for NamedPipeServerStream { - type Item = io::Result; + type Item = io::Result<(NamedPipeServer, PeerInfo)>; fn poll_next( self: Pin<&mut Self>, cx: &mut Context<'_>, - ) -> Poll>> { + ) -> Poll>> { let this = self.project(); this.rx.poll_recv(cx).map(|v| v.map(Ok)) diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/peercred_unix_listener_stream.rs b/apps/desktop/desktop_native/core/src/ssh_agent/peercred_unix_listener_stream.rs new file mode 100644 index 0000000000..f0114fc08d --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/peercred_unix_listener_stream.rs @@ -0,0 +1,72 @@ +use futures::Stream; +use std::io; +use std::pin::Pin; +use std::task::{Context, Poll}; +use tokio::net::{UnixListener, UnixStream}; + +use super::peerinfo; +use super::peerinfo::models::PeerInfo; + +#[derive(Debug)] +pub struct PeercredUnixListenerStream { + inner: UnixListener, +} + +impl PeercredUnixListenerStream { + pub fn new(listener: UnixListener) -> Self { + Self { inner: listener } + } +} + +impl Stream for PeercredUnixListenerStream { + type Item = io::Result<(UnixStream, PeerInfo)>; + + fn poll_next( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + match self.inner.poll_accept(cx) { + Poll::Ready(Ok((stream, _))) => { + let pid = match stream.peer_cred() { + Ok(peer) => match peer.pid() { + Some(pid) => pid, + None => { + return Poll::Ready(Some(Err(io::Error::new( + io::ErrorKind::Other, + "Failed to get peer PID", + )))); + } + }, + Err(err) => { + return Poll::Ready(Some(Err(io::Error::new( + io::ErrorKind::Other, + format!("Failed to get peer credentials: {}", err), + )))); + } + }; + let peer_info = peerinfo::gather::get_peer_info(pid as u32); + match peer_info { + Ok(info) => Poll::Ready(Some(Ok((stream, info)))), + Err(err) => Poll::Ready(Some(Err(io::Error::new( + io::ErrorKind::Other, + format!("Failed to get peer info: {}", err), + )))), + } + } + Poll::Ready(Err(err)) => Poll::Ready(Some(Err(err))), + Poll::Pending => Poll::Pending, + } + } +} + +impl AsRef for PeercredUnixListenerStream { + fn as_ref(&self) -> &UnixListener { + &self.inner + } +} + +impl AsMut for PeercredUnixListenerStream { + fn as_mut(&mut self) -> &mut UnixListener { + &mut self.inner + } +} diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/peerinfo/gather.rs b/apps/desktop/desktop_native/core/src/ssh_agent/peerinfo/gather.rs new file mode 100644 index 0000000000..699203d613 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/peerinfo/gather.rs @@ -0,0 +1,23 @@ +use sysinfo::{Pid, System}; + +use super::models::PeerInfo; + +pub fn get_peer_info(peer_pid: u32) -> Result { + let s = System::new_all(); + if let Some(process) = s.process(Pid::from_u32(peer_pid)) { + let peer_process_name = match process.name().to_str() { + Some(name) => name.to_string(), + None => { + return Err("Failed to get process name".to_string()); + } + }; + + return Ok(PeerInfo::new( + peer_pid, + process.pid().as_u32(), + peer_process_name, + )); + } + + Err("Failed to get process".to_string()) +} diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/peerinfo/mod.rs b/apps/desktop/desktop_native/core/src/ssh_agent/peerinfo/mod.rs new file mode 100644 index 0000000000..fb12aa66e0 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/peerinfo/mod.rs @@ -0,0 +1,2 @@ +pub mod gather; +pub mod models; diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/peerinfo/models.rs b/apps/desktop/desktop_native/core/src/ssh_agent/peerinfo/models.rs new file mode 100644 index 0000000000..823d912883 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ssh_agent/peerinfo/models.rs @@ -0,0 +1,32 @@ +/** +* Peerinfo represents the information of a peer process connecting over a socket. +* This can be later extended to include more information (icon, app name) for the corresponding application. +*/ +#[derive(Debug)] +pub struct PeerInfo { + uid: u32, + pid: u32, + process_name: String, +} + +impl PeerInfo { + pub fn new(uid: u32, pid: u32, process_name: String) -> Self { + Self { + uid, + pid, + process_name, + } + } + + pub fn uid(&self) -> u32 { + self.uid + } + + pub fn pid(&self) -> u32 { + self.pid + } + + pub fn process_name(&self) -> &str { + &self.process_name + } +} diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/unix.rs b/apps/desktop/desktop_native/core/src/ssh_agent/unix.rs index ed2fe9ffab..a74c1205b5 100644 --- a/apps/desktop/desktop_native/core/src/ssh_agent/unix.rs +++ b/apps/desktop/desktop_native/core/src/ssh_agent/unix.rs @@ -2,7 +2,10 @@ use std::{ collections::HashMap, fs, os::unix::fs::PermissionsExt, - sync::{Arc, RwLock}, + sync::{ + atomic::{AtomicBool, AtomicU32}, + Arc, RwLock, + }, }; use bitwarden_russh::ssh_agent; @@ -10,11 +13,13 @@ use homedir::my_home; use tokio::{net::UnixListener, sync::Mutex}; use tokio_util::sync::CancellationToken; -use super::BitwardenDesktopAgent; +use crate::ssh_agent::peercred_unix_listener_stream::PeercredUnixListenerStream; + +use super::{BitwardenDesktopAgent, SshAgentUIRequest}; impl BitwardenDesktopAgent { pub async fn start_server( - auth_request_tx: tokio::sync::mpsc::Sender<(u32, (String, bool))>, + auth_request_tx: tokio::sync::mpsc::Sender, auth_response_rx: Arc>>, ) -> Result { let agent = BitwardenDesktopAgent { @@ -22,9 +27,9 @@ impl BitwardenDesktopAgent { cancellation_token: CancellationToken::new(), show_ui_request_tx: auth_request_tx, get_ui_response_rx: auth_response_rx, - request_id: Arc::new(tokio::sync::Mutex::new(0)), - needs_unlock: Arc::new(tokio::sync::Mutex::new(true)), - is_running: Arc::new(tokio::sync::Mutex::new(false)), + request_id: Arc::new(AtomicU32::new(0)), + needs_unlock: Arc::new(AtomicBool::new(false)), + is_running: Arc::new(AtomicBool::new(false)), }; let cloned_agent_state = agent.clone(); tokio::spawn(async move { @@ -75,18 +80,23 @@ impl BitwardenDesktopAgent { return; } - let wrapper = tokio_stream::wrappers::UnixListenerStream::new(listener); + let stream = PeercredUnixListenerStream::new(listener); + let cloned_keystore = cloned_agent_state.keystore.clone(); let cloned_cancellation_token = cloned_agent_state.cancellation_token.clone(); - *cloned_agent_state.is_running.lock().await = true; + cloned_agent_state + .is_running + .store(true, std::sync::atomic::Ordering::Relaxed); let _ = ssh_agent::serve( - wrapper, + stream, cloned_agent_state.clone(), cloned_keystore, cloned_cancellation_token, ) .await; - *cloned_agent_state.is_running.lock().await = false; + cloned_agent_state + .is_running + .store(false, std::sync::atomic::Ordering::Relaxed); println!("[SSH Agent Native Module] SSH Agent server exited"); } Err(e) => { diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/windows.rs b/apps/desktop/desktop_native/core/src/ssh_agent/windows.rs index 6a99b7cfb0..bc63ef552b 100644 --- a/apps/desktop/desktop_native/core/src/ssh_agent/windows.rs +++ b/apps/desktop/desktop_native/core/src/ssh_agent/windows.rs @@ -3,16 +3,19 @@ pub mod named_pipe_listener_stream; use std::{ collections::HashMap, - sync::{Arc, RwLock}, + sync::{ + atomic::{AtomicBool, AtomicU32}, + Arc, RwLock, + }, }; use tokio::sync::Mutex; use tokio_util::sync::CancellationToken; -use super::BitwardenDesktopAgent; +use super::{BitwardenDesktopAgent, SshAgentUIRequest}; impl BitwardenDesktopAgent { pub async fn start_server( - auth_request_tx: tokio::sync::mpsc::Sender<(u32, (String, bool))>, + auth_request_tx: tokio::sync::mpsc::Sender, auth_response_rx: Arc>>, ) -> Result { let agent_state = BitwardenDesktopAgent { @@ -20,9 +23,9 @@ impl BitwardenDesktopAgent { show_ui_request_tx: auth_request_tx, get_ui_response_rx: auth_response_rx, cancellation_token: CancellationToken::new(), - request_id: Arc::new(tokio::sync::Mutex::new(0)), - needs_unlock: Arc::new(tokio::sync::Mutex::new(true)), - is_running: Arc::new(tokio::sync::Mutex::new(true)), + request_id: Arc::new(AtomicU32::new(0)), + needs_unlock: Arc::new(AtomicBool::new(true)), + is_running: Arc::new(AtomicBool::new(true)), }; let stream = named_pipe_listener_stream::NamedPipeServerStream::new( agent_state.cancellation_token.clone(), @@ -31,7 +34,9 @@ impl BitwardenDesktopAgent { let cloned_agent_state = agent_state.clone(); tokio::spawn(async move { - *cloned_agent_state.is_running.lock().await = true; + cloned_agent_state + .is_running + .store(true, std::sync::atomic::Ordering::Relaxed); let _ = ssh_agent::serve( stream, cloned_agent_state.clone(), @@ -39,7 +44,9 @@ impl BitwardenDesktopAgent { cloned_agent_state.cancellation_token.clone(), ) .await; - *cloned_agent_state.is_running.lock().await = false; + cloned_agent_state + .is_running + .store(false, std::sync::atomic::Ordering::Relaxed); }); Ok(agent_state) } diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index 0eaba19791..b884829e77 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -67,7 +67,7 @@ export declare namespace sshagent { status: SshKeyImportStatus sshKey?: SshKey } - export function serve(callback: (err: Error | null, arg0: string, arg1: boolean) => any): Promise + export function serve(callback: (err: Error | null, arg0: string | undefined | null, arg1: boolean, arg2: string) => any): Promise export function stop(agentState: SshAgentState): void export function isRunning(agentState: SshAgentState): boolean export function setKeys(agentState: SshAgentState, newKeys: Array): void diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index 5037108afd..a7e2144b1d 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -247,30 +247,28 @@ pub mod sshagent { #[napi] pub async fn serve( - callback: ThreadsafeFunction<(String, bool), CalleeHandled>, + callback: ThreadsafeFunction<(Option, bool, String), CalleeHandled>, ) -> napi::Result { let (auth_request_tx, mut auth_request_rx) = - tokio::sync::mpsc::channel::<(u32, (String, bool))>(32); + tokio::sync::mpsc::channel::(32); let (auth_response_tx, auth_response_rx) = tokio::sync::broadcast::channel::<(u32, bool)>(32); let auth_response_tx_arc = Arc::new(Mutex::new(auth_response_tx)); tokio::spawn(async move { let _ = auth_response_rx; - while let Some((request_id, (cipher_uuid, is_list_request))) = - auth_request_rx.recv().await - { - let cloned_request_id = request_id.clone(); - let cloned_cipher_uuid = cipher_uuid.clone(); + while let Some(request) = auth_request_rx.recv().await { let cloned_response_tx_arc = auth_response_tx_arc.clone(); let cloned_callback = callback.clone(); tokio::spawn(async move { - let request_id = cloned_request_id; - let cipher_uuid = cloned_cipher_uuid; let auth_response_tx_arc = cloned_response_tx_arc; let callback = cloned_callback; let promise_result: Result, napi::Error> = callback - .call_async(Ok((cipher_uuid, is_list_request))) + .call_async(Ok(( + request.cipher_id, + request.is_list, + request.process_name, + ))) .await; match promise_result { Ok(promise_result) => match promise_result.await { @@ -278,7 +276,7 @@ pub mod sshagent { let _ = auth_response_tx_arc .lock() .await - .send((request_id, result)) + .send((request.request_id, result)) .expect("should be able to send auth response to agent"); } Err(e) => { @@ -286,7 +284,7 @@ pub mod sshagent { let _ = auth_response_tx_arc .lock() .await - .send((request_id, false)) + .send((request.request_id, false)) .expect("should be able to send auth response to agent"); } }, @@ -295,7 +293,7 @@ pub mod sshagent { let _ = auth_response_tx_arc .lock() .await - .send((request_id, false)) + .send((request.request_id, false)) .expect("should be able to send auth response to agent"); } } diff --git a/apps/desktop/desktop_native/napi/src/registry/windows.rs b/apps/desktop/desktop_native/napi/src/registry/windows.rs index 481dfb5dc4..aeb381dafd 100644 --- a/apps/desktop/desktop_native/napi/src/registry/windows.rs +++ b/apps/desktop/desktop_native/napi/src/registry/windows.rs @@ -13,7 +13,7 @@ pub fn create_key(key: &str, subkey: &str, value: &str) -> Result<()> { let key = convert_key(key)?; let subkey = key.create(subkey)?; - + const DEFAULT: &str = ""; subkey.set_string(DEFAULT, value)?; diff --git a/apps/desktop/src/platform/main/main-ssh-agent.service.ts b/apps/desktop/src/platform/main/main-ssh-agent.service.ts index 9141e30d82..8858134a6b 100644 --- a/apps/desktop/src/platform/main/main-ssh-agent.service.ts +++ b/apps/desktop/src/platform/main/main-ssh-agent.service.ts @@ -29,7 +29,7 @@ export class MainSshAgentService { init() { // handle sign request passing to UI sshagent - .serve(async (err: Error, cipherId: string, isListRequest: boolean) => { + .serve(async (err: Error, cipherId: string, isListRequest: boolean, processName: string) => { // clear all old (> SIGN_TIMEOUT) requests this.requestResponses = this.requestResponses.filter( (response) => response.timestamp > new Date(Date.now() - this.SIGN_TIMEOUT), @@ -41,6 +41,7 @@ export class MainSshAgentService { cipherId, isListRequest, requestId: id_for_this_request, + processName, }); const result = await firstValueFrom( diff --git a/apps/desktop/src/platform/services/ssh-agent.service.ts b/apps/desktop/src/platform/services/ssh-agent.service.ts index 9dc7abeca0..651e67e946 100644 --- a/apps/desktop/src/platform/services/ssh-agent.service.ts +++ b/apps/desktop/src/platform/services/ssh-agent.service.ts @@ -122,6 +122,10 @@ export class SshAgentService implements OnDestroy { const cipherId = message.cipherId as string; const isListRequest = message.isListRequest as boolean; const requestId = message.requestId as number; + let application = message.processName as string; + if (application == "") { + application = this.i18nService.t("unknownApplication"); + } if (isListRequest) { const sshCiphers = ciphers.filter( @@ -151,7 +155,7 @@ export class SshAgentService implements OnDestroy { const dialogRef = ApproveSshRequestComponent.open( this.dialogService, cipher.name, - this.i18nService.t("unknownApplication"), + application, ); const result = await firstValueFrom(dialogRef.closed);