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]]
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"

View File

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

View File

@ -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(),

View File

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

View File

@ -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()),
}
}
}

View File

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

View File

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

View File

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

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,
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) => {

View File

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

View File

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

View File

@ -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");
}
}

View File

@ -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)?;

View File

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

View File

@ -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);