1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-11-22 11:45:59 +01:00

[PM-14993] Add ssh-agent error handling and security fixes (#12048)

* Add error handling and security fixes

* Add is running status, and add more errors on windows
This commit is contained in:
Bernd Schoolmann 2024-11-20 01:38:21 -08:00 committed by GitHub
parent eda38855f0
commit 5e6c5c8779
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 104 additions and 21 deletions

View File

@ -20,18 +20,16 @@ pub struct BitwardenDesktopAgent {
show_ui_request_tx: tokio::sync::mpsc::Sender<(u32, String)>, show_ui_request_tx: tokio::sync::mpsc::Sender<(u32, String)>,
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<Mutex<u32>>,
} is_running: Arc<tokio::sync::Mutex<bool>>,
impl BitwardenDesktopAgent {
async fn get_request_id(&self) -> u32 {
let mut request_id = self.request_id.lock().await;
*request_id += 1;
*request_id
}
} }
impl ssh_agent::Agent for BitwardenDesktopAgent { impl ssh_agent::Agent for BitwardenDesktopAgent {
async fn confirm(&self, ssh_key: Key) -> bool { async fn confirm(&self, ssh_key: Key) -> bool {
if !*self.is_running.lock().await {
println!("[BitwardenDesktopAgent] Agent is not running, but tried to call confirm");
return false;
}
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();
@ -50,6 +48,12 @@ impl ssh_agent::Agent for BitwardenDesktopAgent {
impl BitwardenDesktopAgent { impl BitwardenDesktopAgent {
pub fn stop(&self) { pub fn stop(&self) {
if !*self.is_running.blocking_lock() {
println!("[BitwardenDesktopAgent] Tried to stop agent while it is not running");
return;
}
*self.is_running.blocking_lock() = false;
self.cancellation_token.cancel(); self.cancellation_token.cancel();
self.keystore self.keystore
.0 .0
@ -62,6 +66,12 @@ 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() {
return Err(anyhow::anyhow!(
"[BitwardenDesktopAgent] Tried to set keys while agent is not running"
));
}
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();
@ -91,6 +101,12 @@ impl BitwardenDesktopAgent {
} }
pub fn lock(&mut self) -> Result<(), anyhow::Error> { pub fn lock(&mut self) -> Result<(), anyhow::Error> {
if !*self.is_running.blocking_lock() {
return Err(anyhow::anyhow!(
"[BitwardenDesktopAgent] Tried to lock agent, but it is not running"
));
}
let keystore = &mut self.keystore; let keystore = &mut self.keystore;
keystore keystore
.0 .0
@ -102,6 +118,21 @@ impl BitwardenDesktopAgent {
}); });
Ok(()) Ok(())
} }
async fn get_request_id(&self) -> u32 {
if !*self.is_running.lock().await {
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
}
pub fn is_running(self) -> bool {
return self.is_running.blocking_lock().clone();
}
} }
fn parse_key_safe(pem: &str) -> Result<ssh_key::private::PrivateKey, anyhow::Error> { fn parse_key_safe(pem: &str) -> Result<ssh_key::private::PrivateKey, anyhow::Error> {

View File

@ -1,7 +1,5 @@
use std::{ use std::{
io, io, pin::Pin, sync::Arc, task::{Context, Poll}
pin::Pin,
task::{Context, Poll},
}; };
use futures::Stream; use futures::Stream;
@ -19,14 +17,23 @@ pub struct NamedPipeServerStream {
} }
impl NamedPipeServerStream { impl NamedPipeServerStream {
pub fn new(cancellation_token: CancellationToken) -> Self { pub fn new(cancellation_token: CancellationToken, is_running: Arc<tokio::sync::Mutex<bool>>) -> 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!(
"[SSH Agent Native Module] Creating named pipe server on {}", "[SSH Agent Native Module] Creating named pipe server on {}",
PIPE_NAME PIPE_NAME
); );
let mut listener = ServerOptions::new().create(PIPE_NAME).unwrap(); let mut listener = match ServerOptions::new().create(PIPE_NAME) {
Ok(pipe) => pipe,
Err(err) => {
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;
return;
}
};
loop { loop {
println!("[SSH Agent Native Module] Waiting for connection"); println!("[SSH Agent Native Module] Waiting for connection");
select! { select! {
@ -37,7 +44,16 @@ 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(); tx.send(listener).await.unwrap();
listener = ServerOptions::new().create(PIPE_NAME).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;
return;
}
};
} }
} }
} }

View File

@ -1,5 +1,7 @@
use std::{ use std::{
collections::HashMap, collections::HashMap,
fs,
os::unix::fs::PermissionsExt,
sync::{Arc, RwLock}, sync::{Arc, RwLock},
}; };
@ -15,14 +17,13 @@ impl BitwardenDesktopAgent {
auth_request_tx: tokio::sync::mpsc::Sender<(u32, String)>, auth_request_tx: tokio::sync::mpsc::Sender<(u32, String)>,
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> {
use std::path::PathBuf;
let agent = BitwardenDesktopAgent { let agent = BitwardenDesktopAgent {
keystore: ssh_agent::KeyStore(Arc::new(RwLock::new(HashMap::new()))), keystore: ssh_agent::KeyStore(Arc::new(RwLock::new(HashMap::new()))),
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(tokio::sync::Mutex::new(0)),
is_running: Arc::new(tokio::sync::Mutex::new(false)),
}; };
let cloned_agent_state = agent.clone(); let cloned_agent_state = agent.clone();
tokio::spawn(async move { tokio::spawn(async move {
@ -33,7 +34,12 @@ impl BitwardenDesktopAgent {
let ssh_agent_directory = match my_home() { let ssh_agent_directory = match my_home() {
Ok(Some(home)) => home, Ok(Some(home)) => home,
_ => PathBuf::from("/tmp/"), _ => {
println!(
"[SSH Agent Native Module] Could not determine home directory"
);
return;
}
}; };
ssh_agent_directory ssh_agent_directory
.join(".bitwarden-ssh-agent.sock") .join(".bitwarden-ssh-agent.sock")
@ -48,19 +54,38 @@ impl BitwardenDesktopAgent {
ssh_path ssh_path
); );
let sockname = std::path::Path::new(&ssh_path); let sockname = std::path::Path::new(&ssh_path);
let _ = std::fs::remove_file(sockname); if let Err(e) = std::fs::remove_file(sockname) {
println!(
"[SSH Agent Native Module] Could not remove existing socket file: {}",
e
);
return;
}
match UnixListener::bind(sockname) { match UnixListener::bind(sockname) {
Ok(listener) => { Ok(listener) => {
// Only the current user should be able to access the socket
if let Err(e) = fs::set_permissions(sockname, fs::Permissions::from_mode(0o600))
{
println!(
"[SSH Agent Native Module] Could not set socket permissions: {}",
e
);
return;
}
let wrapper = tokio_stream::wrappers::UnixListenerStream::new(listener); let wrapper = tokio_stream::wrappers::UnixListenerStream::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;
let _ = ssh_agent::serve( let _ = ssh_agent::serve(
wrapper, wrapper,
cloned_agent_state, cloned_agent_state.clone(),
cloned_keystore, cloned_keystore,
cloned_cancellation_token, cloned_cancellation_token,
) )
.await; .await;
*cloned_agent_state.is_running.lock().await = false;
println!("[SSH Agent Native Module] SSH Agent server exited"); println!("[SSH Agent Native Module] SSH Agent server exited");
} }
Err(e) => { Err(e) => {

View File

@ -21,13 +21,16 @@ impl BitwardenDesktopAgent {
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(tokio::sync::Mutex::new(0)),
is_running: Arc::new(tokio::sync::Mutex::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(),
agent_state.is_running.clone(),
); );
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;
let _ = ssh_agent::serve( let _ = ssh_agent::serve(
stream, stream,
cloned_agent_state.clone(), cloned_agent_state.clone(),
@ -35,6 +38,7 @@ impl BitwardenDesktopAgent {
cloned_agent_state.cancellation_token.clone(), cloned_agent_state.cancellation_token.clone(),
) )
.await; .await;
*cloned_agent_state.is_running.lock().await = false;
}); });
Ok(agent_state) Ok(agent_state)
} }

View File

@ -71,6 +71,7 @@ export declare namespace sshagent {
} }
export function serve(callback: (err: Error | null, arg: string) => any): Promise<SshAgentState> export function serve(callback: (err: Error | null, arg: string) => any): Promise<SshAgentState>
export function stop(agentState: SshAgentState): void export function stop(agentState: SshAgentState): void
export function isRunning(agentState: SshAgentState): boolean
export function setKeys(agentState: SshAgentState, newKeys: Array<PrivateKey>): void export function setKeys(agentState: SshAgentState, newKeys: Array<PrivateKey>): void
export function lock(agentState: SshAgentState): void export function lock(agentState: SshAgentState): void
export function importKey(encodedKey: string, password: string): SshKeyImportResult export function importKey(encodedKey: string, password: string): SshKeyImportResult

View File

@ -307,6 +307,12 @@ pub mod sshagent {
Ok(()) Ok(())
} }
#[napi]
pub fn is_running(agent_state: &mut SshAgentState) -> bool {
let bitwarden_agent_state = agent_state.state.clone();
bitwarden_agent_state.is_running()
}
#[napi] #[napi]
pub fn set_keys( pub fn set_keys(
agent_state: &mut SshAgentState, agent_state: &mut SshAgentState,

View File

@ -79,7 +79,7 @@ export class MainSshAgentService {
ipcMain.handle( ipcMain.handle(
"sshagent.setkeys", "sshagent.setkeys",
async (event: any, keys: { name: string; privateKey: string; cipherId: string }[]) => { async (event: any, keys: { name: string; privateKey: string; cipherId: string }[]) => {
if (this.agentState != null) { if (this.agentState != null && (await sshagent.isRunning(this.agentState))) {
sshagent.setKeys(this.agentState, keys); sshagent.setKeys(this.agentState, keys);
} }
}, },
@ -107,7 +107,7 @@ export class MainSshAgentService {
); );
ipcMain.handle("sshagent.lock", async (event: any) => { ipcMain.handle("sshagent.lock", async (event: any) => {
if (this.agentState != null) { if (this.agentState != null && (await sshagent.isRunning(this.agentState))) {
sshagent.lock(this.agentState); sshagent.lock(this.agentState);
} }
}); });