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:
parent
7abdc7a423
commit
e8d8a816dd
71
apps/desktop/desktop_native/Cargo.lock
generated
71
apps/desktop/desktop_native/Cargo.lock
generated
@ -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"
|
||||
|
@ -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]
|
||||
|
@ -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 =
|
||||
<Biometric as BiometricTrait>::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 =
|
||||
<Biometric as BiometricTrait>::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 = <Biometric as BiometricTrait>::set_biometric_secret("", "", "", None, "").await;
|
||||
let result =
|
||||
<Biometric as BiometricTrait>::set_biometric_secret("", "", "", None, "").await;
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().to_string(),
|
||||
|
@ -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 {
|
||||
|
@ -5,9 +5,7 @@ use std::collections::HashMap;
|
||||
pub async fn get_password(service: &str, account: &str) -> Result<String> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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<SshAgentUIRequest>,
|
||||
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
|
||||
needs_unlock: Arc<Mutex<bool>>,
|
||||
is_running: Arc<tokio::sync::Mutex<bool>>,
|
||||
needs_unlock: Arc<AtomicBool>,
|
||||
is_running: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
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<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");
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<NamedPipeServer>,
|
||||
rx: tokio::sync::mpsc::Receiver<(NamedPipeServer, PeerInfo)>,
|
||||
}
|
||||
|
||||
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);
|
||||
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<NamedPipeServer>;
|
||||
type Item = io::Result<(NamedPipeServer, PeerInfo)>;
|
||||
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<io::Result<NamedPipeServer>>> {
|
||||
) -> Poll<Option<io::Result<(NamedPipeServer, PeerInfo)>>> {
|
||||
let this = self.project();
|
||||
|
||||
this.rx.poll_recv(cx).map(|v| v.map(Ok))
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
pub mod gather;
|
||||
pub mod models;
|
@ -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
|
||||
}
|
||||
}
|
@ -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<SshAgentUIRequest>,
|
||||
auth_response_rx: Arc<Mutex<tokio::sync::broadcast::Receiver<(u32, bool)>>>,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
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) => {
|
||||
|
@ -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<SshAgentUIRequest>,
|
||||
auth_response_rx: Arc<Mutex<tokio::sync::broadcast::Receiver<(u32, bool)>>>,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
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)
|
||||
}
|
||||
|
2
apps/desktop/desktop_native/napi/index.d.ts
vendored
2
apps/desktop/desktop_native/napi/index.d.ts
vendored
@ -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<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 isRunning(agentState: SshAgentState): boolean
|
||||
export function setKeys(agentState: SshAgentState, newKeys: Array<PrivateKey>): void
|
||||
|
@ -247,30 +247,28 @@ pub mod sshagent {
|
||||
|
||||
#[napi]
|
||||
pub async fn serve(
|
||||
callback: ThreadsafeFunction<(String, bool), CalleeHandled>,
|
||||
callback: ThreadsafeFunction<(Option<String>, bool, String), CalleeHandled>,
|
||||
) -> napi::Result<SshAgentState> {
|
||||
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) =
|
||||
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<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;
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user