1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-12-11 14:48:46 +01:00

[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
This commit is contained in:
Bernd Schoolmann 2024-12-11 03:53:00 -08:00 committed by GitHub
parent 7abdc7a423
commit e8d8a816dd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 411 additions and 106 deletions

View File

@ -338,7 +338,7 @@ checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
[[package]] [[package]]
name = "bitwarden-russh" name = "bitwarden-russh"
version = "0.1.0" 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 = [ dependencies = [
"anyhow", "anyhow",
"byteorder", "byteorder",
@ -584,6 +584,25 @@ dependencies = [
"libc", "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]] [[package]]
name = "crossbeam-utils" name = "crossbeam-utils"
version = "0.8.20" version = "0.8.20"
@ -773,6 +792,7 @@ dependencies = [
"sha2", "sha2",
"ssh-encoding", "ssh-encoding",
"ssh-key", "ssh-key",
"sysinfo",
"thiserror", "thiserror",
"tokio", "tokio",
"tokio-stream", "tokio-stream",
@ -903,6 +923,12 @@ dependencies = [
"subtle", "subtle",
] ]
[[package]]
name = "either"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
[[package]] [[package]]
name = "embed_plist" name = "embed_plist"
version = "1.2.2" version = "1.2.2"
@ -1490,6 +1516,15 @@ dependencies = [
"minimal-lexical", "minimal-lexical",
] ]
[[package]]
name = "ntapi"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4"
dependencies = [
"winapi",
]
[[package]] [[package]]
name = "num" name = "num"
version = "0.4.3" version = "0.4.3"
@ -2051,6 +2086,26 @@ dependencies = [
"getrandom", "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]] [[package]]
name = "recvmsg" name = "recvmsg"
version = "1.0.0" version = "1.0.0"
@ -2453,6 +2508,20 @@ dependencies = [
"unicode-ident", "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]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.14.0" version = "3.14.0"

View File

@ -49,7 +49,7 @@ ssh-key = { version = "=0.6.7", default-features = false, features = [
"rsa", "rsa",
"getrandom", "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 = { version = "=1.41.1", features = ["io-util", "sync", "macros", "net"] }
tokio-stream = { version = "=0.1.15", features = ["net"] } tokio-stream = { version = "=0.1.15", features = ["net"] }
tokio-util = { version = "=0.7.12", features = ["codec"] } 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"] } pkcs8 = { version = "=0.10.2", features = ["alloc", "encryption", "pem"] }
rsa = "=0.9.6" rsa = "=0.9.6"
ed25519 = { version = "=2.2.3", features = ["pkcs8"] } ed25519 = { version = "=2.2.3", features = ["pkcs8"] }
sysinfo = { version = "0.32.0", features = ["windows"] }
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
widestring = { version = "=1.1.0", optional = true } widestring = { version = "=1.1.0", optional = true }
@ -72,6 +73,7 @@ windows = { version = "=0.58.0", features = [
"Win32_System_WinRT", "Win32_System_WinRT",
"Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Input_KeyboardAndMouse",
"Win32_UI_WindowsAndMessaging", "Win32_UI_WindowsAndMessaging",
"Win32_System_Pipes",
], optional = true } ], optional = true }
[target.'cfg(windows)'.dev-dependencies] [target.'cfg(windows)'.dev-dependencies]

View File

@ -310,12 +310,16 @@ mod tests {
os_key_part_b64: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_owned(), os_key_part_b64: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_owned(),
client_key_part_b64: Some("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 = let result =
<Biometric as BiometricTrait>::get_biometric_secret(test, test, Some(key_material)) <Biometric as BiometricTrait>::get_biometric_secret(test, test, Some(key_material))
.await .await
.unwrap(); .unwrap();
crate::password::delete_password("test", "test").await.unwrap(); crate::password::delete_password("test", "test")
.await
.unwrap();
assert_eq!(result, secret); assert_eq!(result, secret);
} }
@ -328,19 +332,24 @@ mod tests {
os_key_part_b64: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_owned(), os_key_part_b64: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_owned(),
client_key_part_b64: Some("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 = let result =
<Biometric as BiometricTrait>::get_biometric_secret(test, test, Some(key_material)) <Biometric as BiometricTrait>::get_biometric_secret(test, test, Some(key_material))
.await .await
.unwrap(); .unwrap();
crate::password::delete_password("test", "test").await.unwrap(); crate::password::delete_password("test", "test")
.await
.unwrap();
assert_eq!(result, "secret"); assert_eq!(result, "secret");
} }
#[tokio::test] #[tokio::test]
async fn set_biometric_secret_requires_key() { async fn set_biometric_secret_requires_key() {
let result = <Biometric as BiometricTrait>::set_biometric_secret("", "", "", None, "").await; let result =
<Biometric as BiometricTrait>::set_biometric_secret("", "", "", None, "").await;
assert!(result.is_err()); assert!(result.is_err());
assert_eq!( assert_eq!(
result.unwrap_err().to_string(), result.unwrap_err().to_string(),

View File

@ -28,12 +28,18 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test() { async fn test() {
set_password("BitwardenTest", "BitwardenTest", "Random").await.unwrap(); set_password("BitwardenTest", "BitwardenTest", "Random")
.await
.unwrap();
assert_eq!( assert_eq!(
"Random", "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 // Ensure password is deleted
match get_password("BitwardenTest", "BitwardenTest").await { match get_password("BitwardenTest", "BitwardenTest").await {

View File

@ -5,9 +5,7 @@ use std::collections::HashMap;
pub async fn get_password(service: &str, account: &str) -> Result<String> { pub async fn get_password(service: &str, account: &str) -> Result<String> {
match get_password_new(service, account).await { match get_password_new(service, account).await {
Ok(res) => Ok(res), Ok(res) => Ok(res),
Err(_) => { Err(_) => get_password_legacy(service, account).await,
get_password_legacy(service, account).await
}
} }
} }
@ -20,8 +18,8 @@ async fn get_password_new(service: &str, account: &str) -> Result<String> {
Some(res) => { Some(res) => {
let secret = res.secret().await?; let secret = res.secret().await?;
Ok(String::from_utf8(secret.to_vec())?) 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<String> {
match res { match res {
Some(res) => { Some(res) => {
let secret = res.secret().await?; 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?; keyring.delete(&attributes).await?;
let secret_string = String::from_utf8(secret.to_vec())?; let secret_string = String::from_utf8(secret.to_vec())?;
set_password(service, account, &secret_string).await?; set_password(service, account, &secret_string).await?;
Ok(secret_string) 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<()> { pub async fn set_password(service: &str, account: &str, password: &str) -> Result<()> {
let keyring = oo7::Keyring::new().await?; let keyring = oo7::Keyring::new().await?;
let attributes = HashMap::from([("service", service), ("account", account)]); 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(()) Ok(())
} }
@ -74,22 +82,25 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test() { async fn test() {
set_password("BitwardenTest", "BitwardenTest", "Random").await.unwrap(); set_password("BitwardenTest", "BitwardenTest", "Random")
.await
.unwrap();
assert_eq!( assert_eq!(
"Random", "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 // Ensure password is deleted
match get_password("BitwardenTest", "BitwardenTest").await { match get_password("BitwardenTest", "BitwardenTest").await {
Ok(_) => { Ok(_) => {
panic!("Got a result") panic!("Got a result")
} }
Err(e) => assert_eq!( Err(e) => assert_eq!("no result", e.to_string()),
"no result",
e.to_string()
),
} }
} }
@ -97,10 +108,7 @@ mod tests {
async fn test_error_no_password() { async fn test_error_no_password() {
match get_password("Unknown", "Unknown").await { match get_password("Unknown", "Unknown").await {
Ok(_) => panic!("Got a result"), Ok(_) => panic!("Got a result"),
Err(e) => assert_eq!( Err(e) => assert_eq!("no result", e.to_string()),
"no result",
e.to_string()
),
} }
} }
} }

View File

@ -112,12 +112,18 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test() { async fn test() {
set_password("BitwardenTest", "BitwardenTest", "Random").await.unwrap(); set_password("BitwardenTest", "BitwardenTest", "Random")
.await
.unwrap();
assert_eq!( assert_eq!(
"Random", "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 // Ensure password is deleted
match get_password("BitwardenTest", "BitwardenTest").await { match get_password("BitwardenTest", "BitwardenTest").await {

View File

@ -1,4 +1,7 @@
use std::sync::Arc; use std::sync::{
atomic::{AtomicBool, AtomicU32},
Arc,
};
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
@ -10,34 +13,52 @@ use bitwarden_russh::ssh_agent::{self, Key};
#[cfg_attr(target_os = "linux", path = "unix.rs")] #[cfg_attr(target_os = "linux", path = "unix.rs")]
mod platform_ssh_agent; mod platform_ssh_agent;
#[cfg(any(target_os = "linux", target_os = "macos"))]
mod peercred_unix_listener_stream;
pub mod generator; pub mod generator;
pub mod importer; pub mod importer;
pub mod peerinfo;
#[derive(Clone)] #[derive(Clone)]
pub struct BitwardenDesktopAgent { pub struct BitwardenDesktopAgent {
keystore: ssh_agent::KeyStore, keystore: ssh_agent::KeyStore,
cancellation_token: CancellationToken, cancellation_token: CancellationToken,
show_ui_request_tx: tokio::sync::mpsc::Sender<(u32, (String, bool))>, show_ui_request_tx: tokio::sync::mpsc::Sender<SshAgentUIRequest>,
get_ui_response_rx: Arc<Mutex<tokio::sync::broadcast::Receiver<(u32, bool)>>>, get_ui_response_rx: Arc<Mutex<tokio::sync::broadcast::Receiver<(u32, bool)>>>,
request_id: Arc<Mutex<u32>>, request_id: Arc<AtomicU32>,
/// before first unlock, or after account switching, listing keys should require an unlock to get a list of public keys /// before first unlock, or after account switching, listing keys should require an unlock to get a list of public keys
needs_unlock: Arc<Mutex<bool>>, needs_unlock: Arc<AtomicBool>,
is_running: Arc<tokio::sync::Mutex<bool>>, is_running: Arc<AtomicBool>,
} }
impl ssh_agent::Agent for BitwardenDesktopAgent { pub struct SshAgentUIRequest {
async fn confirm(&self, ssh_key: Key) -> bool { pub request_id: u32,
if !*self.is_running.lock().await { pub cipher_id: Option<String>,
pub process_name: String,
pub is_list: bool,
}
impl ssh_agent::Agent<peerinfo::models::PeerInfo> 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"); println!("[BitwardenDesktopAgent] Agent is not running, but tried to call confirm");
return false; return false;
} }
let request_id = self.get_request_id().await; 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 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 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 .await
.expect("Should send request to ui"); .expect("Should send request to ui");
while let Ok((id, response)) = rx_channel.recv().await { while let Ok((id, response)) = rx_channel.recv().await {
@ -48,15 +69,20 @@ impl ssh_agent::Agent for BitwardenDesktopAgent {
false false
} }
async fn can_list(&self) -> bool { async fn can_list(&self, info: &peerinfo::models::PeerInfo) -> bool {
if !*self.needs_unlock.lock().await{ if !self.needs_unlock.load(std::sync::atomic::Ordering::Relaxed) {
return true; return true;
} }
let request_id = self.get_request_id().await; let request_id = self.get_request_id().await;
let mut rx_channel = self.get_ui_response_rx.lock().await.resubscribe(); 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 self.show_ui_request_tx
.send(message) .send(message)
.await .await
@ -72,13 +98,13 @@ impl ssh_agent::Agent for BitwardenDesktopAgent {
impl BitwardenDesktopAgent { impl BitwardenDesktopAgent {
pub fn stop(&self) { 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"); println!("[BitwardenDesktopAgent] Tried to stop agent while it is not running");
return; return;
} }
*self.is_running.blocking_lock() = false; self.is_running
self.cancellation_token.cancel(); .store(false, std::sync::atomic::Ordering::Relaxed);
self.keystore self.keystore
.0 .0
.write() .write()
@ -90,7 +116,7 @@ impl BitwardenDesktopAgent {
&mut self, &mut self,
new_keys: Vec<(String, String, String)>, new_keys: Vec<(String, String, String)>,
) -> Result<(), anyhow::Error> { ) -> Result<(), anyhow::Error> {
if !*self.is_running.blocking_lock() { if !self.is_running() {
return Err(anyhow::anyhow!( return Err(anyhow::anyhow!(
"[BitwardenDesktopAgent] Tried to set keys while agent is not running" "[BitwardenDesktopAgent] Tried to set keys while agent is not running"
)); ));
@ -99,7 +125,8 @@ impl BitwardenDesktopAgent {
let keystore = &mut self.keystore; let keystore = &mut self.keystore;
keystore.0.write().expect("RwLock is not poisoned").clear(); 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() { for (key, name, cipher_id) in new_keys.iter() {
match parse_key_safe(&key) { match parse_key_safe(&key) {
@ -127,7 +154,7 @@ impl BitwardenDesktopAgent {
} }
pub fn lock(&mut self) -> Result<(), anyhow::Error> { pub fn lock(&mut self) -> Result<(), anyhow::Error> {
if !*self.is_running.blocking_lock() { if !self.is_running() {
return Err(anyhow::anyhow!( return Err(anyhow::anyhow!(
"[BitwardenDesktopAgent] Tried to lock agent, but it is not running" "[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> { pub fn clear_keys(&mut self) -> Result<(), anyhow::Error> {
let keystore = &mut self.keystore; let keystore = &mut self.keystore;
keystore.0.write().expect("RwLock is not poisoned").clear(); 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(()) Ok(())
} }
async fn get_request_id(&self) -> u32 { 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"); println!("[BitwardenDesktopAgent] Agent is not running, but tried to get request id");
return 0; return 0;
} }
let mut request_id = self.request_id.lock().await; let request_id = self
*request_id += 1; .request_id
*request_id .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
request_id
} }
pub fn is_running(self) -> bool { pub fn is_running(&self) -> bool {
return self.is_running.blocking_lock().clone(); self.is_running.load(std::sync::atomic::Ordering::Relaxed)
} }
} }

View File

@ -1,23 +1,32 @@
use std::{
io, pin::Pin, sync::Arc, task::{Context, Poll}
};
use futures::Stream; 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::{ use tokio::{
net::windows::named_pipe::{NamedPipeServer, ServerOptions}, net::windows::named_pipe::{NamedPipeServer, ServerOptions},
select, select,
}; };
use tokio_util::sync::CancellationToken; 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"; const PIPE_NAME: &str = r"\\.\pipe\openssh-ssh-agent";
#[pin_project::pin_project] #[pin_project::pin_project]
pub struct NamedPipeServerStream { pub struct NamedPipeServerStream {
rx: tokio::sync::mpsc::Receiver<NamedPipeServer>, rx: tokio::sync::mpsc::Receiver<(NamedPipeServer, PeerInfo)>,
} }
impl NamedPipeServerStream { impl NamedPipeServerStream {
pub fn new(cancellation_token: CancellationToken, is_running: Arc<tokio::sync::Mutex<bool>>) -> Self { pub fn new(cancellation_token: CancellationToken, is_running: Arc<AtomicBool>) -> Self {
let (tx, rx) = tokio::sync::mpsc::channel(16); let (tx, rx) = tokio::sync::mpsc::channel(16);
tokio::spawn(async move { tokio::spawn(async move {
println!( 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 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); println!("[SSH Agent Natvie Module] error: {}", err);
cancellation_token.cancel(); cancellation_token.cancel();
*is_running.lock().await = false; is_running.store(false, Ordering::Relaxed);
return; return;
} }
}; };
@ -43,14 +52,32 @@ impl NamedPipeServerStream {
} }
_ = listener.connect() => { _ = listener.connect() => {
println!("[SSH Agent Native Module] Incoming connection"); 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) { listener = match ServerOptions::new().create(PIPE_NAME) {
Ok(pipe) => pipe, Ok(pipe) => pipe,
Err(err) => { Err(err) => {
println!("[SSH Agent Native Module] Encountered an error creating a new pipe {}", err); println!("[SSH Agent Native Module] Encountered an error creating a new pipe {}", err);
cancellation_token.cancel(); cancellation_token.cancel();
*is_running.lock().await = false; is_running.store(false, Ordering::Relaxed);
return; return;
} }
}; };
@ -63,12 +90,12 @@ impl NamedPipeServerStream {
} }
impl Stream for NamedPipeServerStream { impl Stream for NamedPipeServerStream {
type Item = io::Result<NamedPipeServer>; type Item = io::Result<(NamedPipeServer, PeerInfo)>;
fn poll_next( fn poll_next(
self: Pin<&mut Self>, self: Pin<&mut Self>,
cx: &mut Context<'_>, cx: &mut Context<'_>,
) -> Poll<Option<io::Result<NamedPipeServer>>> { ) -> Poll<Option<io::Result<(NamedPipeServer, PeerInfo)>>> {
let this = self.project(); let this = self.project();
this.rx.poll_recv(cx).map(|v| v.map(Ok)) this.rx.poll_recv(cx).map(|v| v.map(Ok))

View File

@ -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<Option<io::Result<(UnixStream, PeerInfo)>>> {
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<UnixListener> for PeercredUnixListenerStream {
fn as_ref(&self) -> &UnixListener {
&self.inner
}
}
impl AsMut<UnixListener> for PeercredUnixListenerStream {
fn as_mut(&mut self) -> &mut UnixListener {
&mut self.inner
}
}

View File

@ -0,0 +1,23 @@
use sysinfo::{Pid, System};
use super::models::PeerInfo;
pub fn get_peer_info(peer_pid: u32) -> Result<PeerInfo, String> {
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())
}

View File

@ -0,0 +1,2 @@
pub mod gather;
pub mod models;

View File

@ -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
}
}

View File

@ -2,7 +2,10 @@ use std::{
collections::HashMap, collections::HashMap,
fs, fs,
os::unix::fs::PermissionsExt, os::unix::fs::PermissionsExt,
sync::{Arc, RwLock}, sync::{
atomic::{AtomicBool, AtomicU32},
Arc, RwLock,
},
}; };
use bitwarden_russh::ssh_agent; use bitwarden_russh::ssh_agent;
@ -10,11 +13,13 @@ use homedir::my_home;
use tokio::{net::UnixListener, sync::Mutex}; use tokio::{net::UnixListener, sync::Mutex};
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use super::BitwardenDesktopAgent; use crate::ssh_agent::peercred_unix_listener_stream::PeercredUnixListenerStream;
use super::{BitwardenDesktopAgent, SshAgentUIRequest};
impl BitwardenDesktopAgent { impl BitwardenDesktopAgent {
pub async fn start_server( pub async fn start_server(
auth_request_tx: tokio::sync::mpsc::Sender<(u32, (String, bool))>, auth_request_tx: tokio::sync::mpsc::Sender<SshAgentUIRequest>,
auth_response_rx: Arc<Mutex<tokio::sync::broadcast::Receiver<(u32, bool)>>>, auth_response_rx: Arc<Mutex<tokio::sync::broadcast::Receiver<(u32, bool)>>>,
) -> Result<Self, anyhow::Error> { ) -> Result<Self, anyhow::Error> {
let agent = BitwardenDesktopAgent { let agent = BitwardenDesktopAgent {
@ -22,9 +27,9 @@ impl BitwardenDesktopAgent {
cancellation_token: CancellationToken::new(), cancellation_token: CancellationToken::new(),
show_ui_request_tx: auth_request_tx, show_ui_request_tx: auth_request_tx,
get_ui_response_rx: auth_response_rx, get_ui_response_rx: auth_response_rx,
request_id: Arc::new(tokio::sync::Mutex::new(0)), request_id: Arc::new(AtomicU32::new(0)),
needs_unlock: Arc::new(tokio::sync::Mutex::new(true)), needs_unlock: Arc::new(AtomicBool::new(false)),
is_running: Arc::new(tokio::sync::Mutex::new(false)), is_running: Arc::new(AtomicBool::new(false)),
}; };
let cloned_agent_state = agent.clone(); let cloned_agent_state = agent.clone();
tokio::spawn(async move { tokio::spawn(async move {
@ -75,18 +80,23 @@ impl BitwardenDesktopAgent {
return; return;
} }
let wrapper = tokio_stream::wrappers::UnixListenerStream::new(listener); let stream = PeercredUnixListenerStream::new(listener);
let cloned_keystore = cloned_agent_state.keystore.clone(); let cloned_keystore = cloned_agent_state.keystore.clone();
let cloned_cancellation_token = cloned_agent_state.cancellation_token.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( let _ = ssh_agent::serve(
wrapper, stream,
cloned_agent_state.clone(), cloned_agent_state.clone(),
cloned_keystore, cloned_keystore,
cloned_cancellation_token, cloned_cancellation_token,
) )
.await; .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"); println!("[SSH Agent Native Module] SSH Agent server exited");
} }
Err(e) => { Err(e) => {

View File

@ -3,16 +3,19 @@ pub mod named_pipe_listener_stream;
use std::{ use std::{
collections::HashMap, collections::HashMap,
sync::{Arc, RwLock}, sync::{
atomic::{AtomicBool, AtomicU32},
Arc, RwLock,
},
}; };
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use super::BitwardenDesktopAgent; use super::{BitwardenDesktopAgent, SshAgentUIRequest};
impl BitwardenDesktopAgent { impl BitwardenDesktopAgent {
pub async fn start_server( pub async fn start_server(
auth_request_tx: tokio::sync::mpsc::Sender<(u32, (String, bool))>, auth_request_tx: tokio::sync::mpsc::Sender<SshAgentUIRequest>,
auth_response_rx: Arc<Mutex<tokio::sync::broadcast::Receiver<(u32, bool)>>>, auth_response_rx: Arc<Mutex<tokio::sync::broadcast::Receiver<(u32, bool)>>>,
) -> Result<Self, anyhow::Error> { ) -> Result<Self, anyhow::Error> {
let agent_state = BitwardenDesktopAgent { let agent_state = BitwardenDesktopAgent {
@ -20,9 +23,9 @@ impl BitwardenDesktopAgent {
show_ui_request_tx: auth_request_tx, show_ui_request_tx: auth_request_tx,
get_ui_response_rx: auth_response_rx, get_ui_response_rx: auth_response_rx,
cancellation_token: CancellationToken::new(), cancellation_token: CancellationToken::new(),
request_id: Arc::new(tokio::sync::Mutex::new(0)), request_id: Arc::new(AtomicU32::new(0)),
needs_unlock: Arc::new(tokio::sync::Mutex::new(true)), needs_unlock: Arc::new(AtomicBool::new(true)),
is_running: Arc::new(tokio::sync::Mutex::new(true)), is_running: Arc::new(AtomicBool::new(true)),
}; };
let stream = named_pipe_listener_stream::NamedPipeServerStream::new( let stream = named_pipe_listener_stream::NamedPipeServerStream::new(
agent_state.cancellation_token.clone(), agent_state.cancellation_token.clone(),
@ -31,7 +34,9 @@ impl BitwardenDesktopAgent {
let cloned_agent_state = agent_state.clone(); let cloned_agent_state = agent_state.clone();
tokio::spawn(async move { 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( let _ = ssh_agent::serve(
stream, stream,
cloned_agent_state.clone(), cloned_agent_state.clone(),
@ -39,7 +44,9 @@ impl BitwardenDesktopAgent {
cloned_agent_state.cancellation_token.clone(), cloned_agent_state.cancellation_token.clone(),
) )
.await; .await;
*cloned_agent_state.is_running.lock().await = false; cloned_agent_state
.is_running
.store(false, std::sync::atomic::Ordering::Relaxed);
}); });
Ok(agent_state) Ok(agent_state)
} }

View File

@ -67,7 +67,7 @@ export declare namespace sshagent {
status: SshKeyImportStatus status: SshKeyImportStatus
sshKey?: SshKey sshKey?: SshKey
} }
export function serve(callback: (err: Error | null, arg0: string, arg1: boolean) => any): Promise<SshAgentState> export function serve(callback: (err: Error | null, arg0: string | undefined | null, arg1: boolean, arg2: string) => any): Promise<SshAgentState>
export function stop(agentState: SshAgentState): void export function stop(agentState: SshAgentState): void
export function isRunning(agentState: SshAgentState): boolean export function isRunning(agentState: SshAgentState): boolean
export function setKeys(agentState: SshAgentState, newKeys: Array<PrivateKey>): void export function setKeys(agentState: SshAgentState, newKeys: Array<PrivateKey>): void

View File

@ -247,30 +247,28 @@ pub mod sshagent {
#[napi] #[napi]
pub async fn serve( pub async fn serve(
callback: ThreadsafeFunction<(String, bool), CalleeHandled>, callback: ThreadsafeFunction<(Option<String>, bool, String), CalleeHandled>,
) -> napi::Result<SshAgentState> { ) -> napi::Result<SshAgentState> {
let (auth_request_tx, mut auth_request_rx) = let (auth_request_tx, mut auth_request_rx) =
tokio::sync::mpsc::channel::<(u32, (String, bool))>(32); tokio::sync::mpsc::channel::<desktop_core::ssh_agent::SshAgentUIRequest>(32);
let (auth_response_tx, auth_response_rx) = let (auth_response_tx, auth_response_rx) =
tokio::sync::broadcast::channel::<(u32, bool)>(32); tokio::sync::broadcast::channel::<(u32, bool)>(32);
let auth_response_tx_arc = Arc::new(Mutex::new(auth_response_tx)); let auth_response_tx_arc = Arc::new(Mutex::new(auth_response_tx));
tokio::spawn(async move { tokio::spawn(async move {
let _ = auth_response_rx; let _ = auth_response_rx;
while let Some((request_id, (cipher_uuid, is_list_request))) = while let Some(request) = auth_request_rx.recv().await {
auth_request_rx.recv().await
{
let cloned_request_id = request_id.clone();
let cloned_cipher_uuid = cipher_uuid.clone();
let cloned_response_tx_arc = auth_response_tx_arc.clone(); let cloned_response_tx_arc = auth_response_tx_arc.clone();
let cloned_callback = callback.clone(); let cloned_callback = callback.clone();
tokio::spawn(async move { 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 auth_response_tx_arc = cloned_response_tx_arc;
let callback = cloned_callback; let callback = cloned_callback;
let promise_result: Result<Promise<bool>, napi::Error> = callback let promise_result: Result<Promise<bool>, napi::Error> = callback
.call_async(Ok((cipher_uuid, is_list_request))) .call_async(Ok((
request.cipher_id,
request.is_list,
request.process_name,
)))
.await; .await;
match promise_result { match promise_result {
Ok(promise_result) => match promise_result.await { Ok(promise_result) => match promise_result.await {
@ -278,7 +276,7 @@ pub mod sshagent {
let _ = auth_response_tx_arc let _ = auth_response_tx_arc
.lock() .lock()
.await .await
.send((request_id, result)) .send((request.request_id, result))
.expect("should be able to send auth response to agent"); .expect("should be able to send auth response to agent");
} }
Err(e) => { Err(e) => {
@ -286,7 +284,7 @@ pub mod sshagent {
let _ = auth_response_tx_arc let _ = auth_response_tx_arc
.lock() .lock()
.await .await
.send((request_id, false)) .send((request.request_id, false))
.expect("should be able to send auth response to agent"); .expect("should be able to send auth response to agent");
} }
}, },
@ -295,7 +293,7 @@ pub mod sshagent {
let _ = auth_response_tx_arc let _ = auth_response_tx_arc
.lock() .lock()
.await .await
.send((request_id, false)) .send((request.request_id, false))
.expect("should be able to send auth response to agent"); .expect("should be able to send auth response to agent");
} }
} }

View File

@ -13,7 +13,7 @@ pub fn create_key(key: &str, subkey: &str, value: &str) -> Result<()> {
let key = convert_key(key)?; let key = convert_key(key)?;
let subkey = key.create(subkey)?; let subkey = key.create(subkey)?;
const DEFAULT: &str = ""; const DEFAULT: &str = "";
subkey.set_string(DEFAULT, value)?; subkey.set_string(DEFAULT, value)?;

View File

@ -29,7 +29,7 @@ export class MainSshAgentService {
init() { init() {
// handle sign request passing to UI // handle sign request passing to UI
sshagent 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 // clear all old (> SIGN_TIMEOUT) requests
this.requestResponses = this.requestResponses.filter( this.requestResponses = this.requestResponses.filter(
(response) => response.timestamp > new Date(Date.now() - this.SIGN_TIMEOUT), (response) => response.timestamp > new Date(Date.now() - this.SIGN_TIMEOUT),
@ -41,6 +41,7 @@ export class MainSshAgentService {
cipherId, cipherId,
isListRequest, isListRequest,
requestId: id_for_this_request, requestId: id_for_this_request,
processName,
}); });
const result = await firstValueFrom( const result = await firstValueFrom(

View File

@ -122,6 +122,10 @@ export class SshAgentService implements OnDestroy {
const cipherId = message.cipherId as string; const cipherId = message.cipherId as string;
const isListRequest = message.isListRequest as boolean; const isListRequest = message.isListRequest as boolean;
const requestId = message.requestId as number; const requestId = message.requestId as number;
let application = message.processName as string;
if (application == "") {
application = this.i18nService.t("unknownApplication");
}
if (isListRequest) { if (isListRequest) {
const sshCiphers = ciphers.filter( const sshCiphers = ciphers.filter(
@ -151,7 +155,7 @@ export class SshAgentService implements OnDestroy {
const dialogRef = ApproveSshRequestComponent.open( const dialogRef = ApproveSshRequestComponent.open(
this.dialogService, this.dialogService,
cipher.name, cipher.name,
this.i18nService.t("unknownApplication"), application,
); );
const result = await firstValueFrom(dialogRef.closed); const result = await firstValueFrom(dialogRef.closed);