mirror of
https://github.com/bitwarden/browser
synced 2026-02-07 20:24:01 +00:00
MVP rewrite
This commit is contained in:
774
apps/desktop/desktop_native/Cargo.lock
generated
774
apps/desktop/desktop_native/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,7 @@ members = [
|
||||
"macos_provider",
|
||||
"napi",
|
||||
"proxy",
|
||||
"ssh_agent",
|
||||
"windows_plugin_authenticator"
|
||||
]
|
||||
|
||||
@@ -53,8 +54,8 @@ security-framework = "=3.5.0"
|
||||
security-framework-sys = "=2.15.0"
|
||||
serde = "=1.0.209"
|
||||
serde_json = "=1.0.127"
|
||||
sha2 = "=0.10.8"
|
||||
simplelog = "=0.12.2"
|
||||
sha2 = "=0.10.9"
|
||||
ssh-encoding = "=0.2.0"
|
||||
ssh-key = { version = "=0.6.7", default-features = false }
|
||||
sysinfo = "=0.35.0"
|
||||
|
||||
@@ -19,6 +19,7 @@ autotype = { path = "../autotype" }
|
||||
base64 = { workspace = true }
|
||||
bitwarden_chromium_importer = { path = "../bitwarden_chromium_importer" }
|
||||
desktop_core = { path = "../core" }
|
||||
ssh_agent = { path = "../ssh_agent" }
|
||||
hex = { workspace = true }
|
||||
napi = { workspace = true, features = ["async"] }
|
||||
napi-derive = { workspace = true }
|
||||
|
||||
26
apps/desktop/desktop_native/napi/index.d.ts
vendored
26
apps/desktop/desktop_native/napi/index.d.ts
vendored
@@ -236,3 +236,29 @@ export declare namespace autotype {
|
||||
export function getForegroundWindowTitle(): string
|
||||
export function typeInput(input: Array<number>, keyboardShortcut: Array<string>): void
|
||||
}
|
||||
export declare namespace sshagent_v2 {
|
||||
export interface PrivateKey {
|
||||
privateKey: string
|
||||
name: string
|
||||
cipherId: string
|
||||
}
|
||||
export interface SshKey {
|
||||
privateKey: string
|
||||
publicKey: string
|
||||
keyFingerprint: string
|
||||
}
|
||||
export interface SshUiRequest {
|
||||
cipherId?: string
|
||||
isList: boolean
|
||||
processName: string
|
||||
isForwarding: boolean
|
||||
namespace?: string
|
||||
}
|
||||
export function serve(callback: (err: Error | null, arg: SshUiRequest) => any): Promise<SshAgentState>
|
||||
export function stop(agentState: SshAgentState): void
|
||||
export function isRunning(agentState: SshAgentState): boolean
|
||||
export function setKeys(agentState: SshAgentState, newKeys: Array<PrivateKey>): void
|
||||
export function lock(agentState: SshAgentState): void
|
||||
export function clearKeys(agentState: SshAgentState): void
|
||||
export class SshAgentState { }
|
||||
}
|
||||
|
||||
@@ -1053,3 +1053,207 @@ pub mod autotype {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub mod sshagent_v2 {
|
||||
use std::sync::Arc;
|
||||
|
||||
use napi::{
|
||||
bindgen_prelude::Promise,
|
||||
threadsafe_function::{ErrorStrategy::CalleeHandled, ThreadsafeFunction},
|
||||
};
|
||||
use ssh_agent::agent::ui_requester;
|
||||
use ssh_agent::{
|
||||
self,
|
||||
agent::{ui_requester::UiRequestMessage, BitwardenDesktopAgent},
|
||||
memory::UnlockedSshItem,
|
||||
protocol::types::KeyPair,
|
||||
transport::unix_listener_stream::UnixListenerStream,
|
||||
};
|
||||
use tokio::{self, sync::Mutex};
|
||||
use tracing::{error, info};
|
||||
|
||||
#[napi]
|
||||
pub struct SshAgentState {
|
||||
agent: BitwardenDesktopAgent,
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct PrivateKey {
|
||||
pub private_key: String,
|
||||
pub name: String,
|
||||
pub cipher_id: String,
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct SshKey {
|
||||
pub private_key: String,
|
||||
pub public_key: String,
|
||||
pub key_fingerprint: String,
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct SshUIRequest {
|
||||
pub cipher_id: Option<String>,
|
||||
pub is_list: bool,
|
||||
pub process_name: String,
|
||||
pub is_forwarding: bool,
|
||||
pub namespace: Option<String>,
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_async)] // FIXME: Remove unused async!
|
||||
#[napi]
|
||||
pub async fn serve(
|
||||
callback: ThreadsafeFunction<SshUIRequest, CalleeHandled>,
|
||||
) -> napi::Result<SshAgentState> {
|
||||
let (auth_request_tx, mut auth_request_rx) =
|
||||
tokio::sync::mpsc::channel::<ssh_agent::agent::ui_requester::UiRequestMessage>(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));
|
||||
let ui_requester =
|
||||
ui_requester::UiRequester::new(auth_request_tx, Arc::new(Mutex::new(auth_response_rx)));
|
||||
|
||||
tokio::spawn(async move {
|
||||
let _ = ui_requester;
|
||||
|
||||
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 auth_response_tx_arc = cloned_response_tx_arc;
|
||||
let callback = cloned_callback;
|
||||
|
||||
let js_request = match request.clone() {
|
||||
UiRequestMessage::ListRequest {
|
||||
request_id: _,
|
||||
connection_info,
|
||||
} => SshUIRequest {
|
||||
cipher_id: None,
|
||||
is_list: true,
|
||||
process_name: "".to_string(),
|
||||
is_forwarding: connection_info.is_forwarding(),
|
||||
namespace: None,
|
||||
},
|
||||
UiRequestMessage::AuthRequest {
|
||||
request_id,
|
||||
connection_info,
|
||||
cipher_id,
|
||||
} => SshUIRequest {
|
||||
cipher_id: Some(cipher_id),
|
||||
is_list: false,
|
||||
process_name: "".to_string(),
|
||||
is_forwarding: connection_info.is_forwarding(),
|
||||
namespace: None,
|
||||
},
|
||||
UiRequestMessage::SignRequest {
|
||||
request_id,
|
||||
connection_info,
|
||||
cipher_id,
|
||||
namespace,
|
||||
} => SshUIRequest {
|
||||
cipher_id: Some(cipher_id),
|
||||
is_list: false,
|
||||
process_name: "".to_string(),
|
||||
is_forwarding: connection_info.is_forwarding(),
|
||||
namespace: Some(namespace),
|
||||
},
|
||||
};
|
||||
|
||||
let promise_result: Result<Promise<bool>, napi::Error> =
|
||||
callback.call_async(Ok(js_request)).await;
|
||||
match promise_result {
|
||||
Ok(promise_result) => match promise_result.await {
|
||||
Ok(result) => {
|
||||
let _ = auth_response_tx_arc
|
||||
.lock()
|
||||
.await
|
||||
.send((request.id(), result))
|
||||
.expect("should be able to send auth response to agent");
|
||||
}
|
||||
Err(e) => {
|
||||
error!(error = %e, "Calling UI callback promise was rejected");
|
||||
let _ = auth_response_tx_arc
|
||||
.lock()
|
||||
.await
|
||||
.send((request.id(), false))
|
||||
.expect("should be able to send auth response to agent");
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
error!(error = %e, "Calling UI callback could not create promise");
|
||||
let _ = auth_response_tx_arc
|
||||
.lock()
|
||||
.await
|
||||
.send((request.id(), false))
|
||||
.expect("should be able to send auth response to agent");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let agent = BitwardenDesktopAgent::new(ui_requester);
|
||||
let agent_copy = agent.clone();
|
||||
tokio::spawn(async move {
|
||||
UnixListenerStream::listen("/home/quexten/.ssh-sock".to_string(), agent_copy)
|
||||
.await
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
Ok(SshAgentState { agent: agent })
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn stop(agent_state: &mut SshAgentState) -> napi::Result<()> {
|
||||
agent_state.agent.stop();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn is_running(agent_state: &mut SshAgentState) -> bool {
|
||||
// let bitwarden_agent_state = agent_state.state.clone();
|
||||
// bitwarden_agent_state.is_running()
|
||||
true
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn set_keys(
|
||||
agent_state: &mut SshAgentState,
|
||||
new_keys: Vec<PrivateKey>,
|
||||
) -> napi::Result<()> {
|
||||
agent_state.agent.set_keys(
|
||||
new_keys
|
||||
.iter()
|
||||
.filter_map(|k| {
|
||||
let private_key =
|
||||
ssh_agent::protocol::types::PrivateKey::try_from(k.private_key.clone())
|
||||
.ok()?;
|
||||
Some(UnlockedSshItem::new(
|
||||
KeyPair::new(private_key, k.name.clone()),
|
||||
k.cipher_id.clone(),
|
||||
))
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn lock(agent_state: &mut SshAgentState) -> napi::Result<()> {
|
||||
// let bitwarden_agent_state = &mut agent_state.state;
|
||||
// bitwarden_agent_state
|
||||
// .lock()
|
||||
// .map_err(|e| napi::Error::from_reason(e.to_string()))
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn clear_keys(agent_state: &mut SshAgentState) -> napi::Result<()> {
|
||||
// let bitwarden_agent_state = &mut agent_state.state;
|
||||
// bitwarden_agent_state
|
||||
// .clear_keys()
|
||||
// .map_err(|e| napi::Error::from_reason(e.to_string()))
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
54
apps/desktop/desktop_native/ssh_agent/Cargo.toml
Normal file
54
apps/desktop/desktop_native/ssh_agent/Cargo.toml
Normal file
@@ -0,0 +1,54 @@
|
||||
[package]
|
||||
name = "ssh_agent"
|
||||
edition = { workspace = true }
|
||||
license = { workspace = true }
|
||||
version = { workspace = true }
|
||||
publish = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = [
|
||||
"dep:windows",
|
||||
]
|
||||
manual_test = []
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
byteorder = { workspace = true }
|
||||
block-padding = { version = "=0.4.0-rc.4" }
|
||||
ed25519 = { workspace = true, features = ["pkcs8"] }
|
||||
ed25519-dalek = { version = "2.2.0", features = ["rand_core"] }
|
||||
futures = { workspace = true }
|
||||
inout = { version = "=0.2.0-rc.6" }
|
||||
homedir = { workspace = true }
|
||||
log = { workspace = true }
|
||||
rsa = { version = "=0.10.0-rc.8", features = ["sha2"] }
|
||||
sha2 = "0.10.9"
|
||||
ssh-encoding = "=0.3.0-rc.2"
|
||||
ssh-key = { version = "=0.7.0-rc.3", features = [
|
||||
"encryption",
|
||||
"ed25519",
|
||||
"rsa",
|
||||
"ecdsa",
|
||||
"getrandom",
|
||||
] }
|
||||
num_enum = "0.7.4"
|
||||
signature = "3.0.0-rc.4"
|
||||
sysinfo = { workspace = true, features = ["windows"] }
|
||||
tracing-subscriber.workspace = true
|
||||
tracing = { workspace = true }
|
||||
p256 = "0.13.2"
|
||||
p384 = "0.13.1"
|
||||
p521 = "0.13.3"
|
||||
tokio = { workspace = true, features = ["io-util", "sync", "macros", "net"] }
|
||||
tokio-util = { workspace = true, features = ["codec"] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows = { workspace = true, features = [
|
||||
"Win32_System_Pipes",
|
||||
], optional = true }
|
||||
windows-future = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
102
apps/desktop/desktop_native/ssh_agent/src/agent/agent.rs
Normal file
102
apps/desktop/desktop_native/ssh_agent/src/agent/agent.rs
Normal file
@@ -0,0 +1,102 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use futures::Stream;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::{
|
||||
agent::ui_requester::{self, UiRequester},
|
||||
memory::UnlockedSshItem,
|
||||
protocol::{
|
||||
self,
|
||||
key_store::Agent,
|
||||
protocol::serve_listener,
|
||||
types::{KeyPair, PublicKeyWithName},
|
||||
},
|
||||
transport::peer_info::PeerInfo,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct BitwardenDesktopAgent {
|
||||
cancellation_token: CancellationToken,
|
||||
key_store: Arc<Mutex<crate::memory::KeyStore>>,
|
||||
ui_requester: UiRequester,
|
||||
}
|
||||
|
||||
impl BitwardenDesktopAgent {
|
||||
pub fn new(ui_requester: UiRequester) -> Self {
|
||||
Self {
|
||||
cancellation_token: CancellationToken::new(),
|
||||
key_store: Arc::new(Mutex::new(crate::memory::KeyStore::new())),
|
||||
ui_requester,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn serve<S, L>(&self, listener: L)
|
||||
where
|
||||
S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Send + Sync + Unpin + 'static,
|
||||
L: Stream<Item = tokio::io::Result<(S, PeerInfo)>> + Unpin,
|
||||
{
|
||||
serve_listener(listener, self.cancellation_token.clone(), self)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn stop(&self) {
|
||||
self.cancellation_token.cancel();
|
||||
}
|
||||
|
||||
pub fn set_keys(&self, keys: Vec<UnlockedSshItem>) {
|
||||
self.key_store
|
||||
.lock()
|
||||
.expect("Failed to lock key store")
|
||||
.set_unlocked(keys);
|
||||
}
|
||||
}
|
||||
|
||||
impl Agent for &BitwardenDesktopAgent {
|
||||
async fn list_keys(&self) -> Result<Vec<PublicKeyWithName>, anyhow::Error> {
|
||||
Ok(self
|
||||
.key_store
|
||||
.lock()
|
||||
.expect("Failed to lock key store")
|
||||
.list_keys())
|
||||
}
|
||||
|
||||
async fn find_ssh_item(
|
||||
&self,
|
||||
public_key: &protocol::types::PublicKey,
|
||||
) -> Result<Option<UnlockedSshItem>, anyhow::Error> {
|
||||
Ok(self
|
||||
.key_store
|
||||
.lock()
|
||||
.expect("Failed to lock key store")
|
||||
.get_unlocked_keypair(public_key))
|
||||
}
|
||||
|
||||
async fn request_can_list(
|
||||
&self,
|
||||
connection_info: &protocol::connection::ConnectionInfo,
|
||||
) -> Result<bool, anyhow::Error> {
|
||||
Ok(self.ui_requester.request_list(connection_info).await)
|
||||
}
|
||||
|
||||
async fn request_can_sign(
|
||||
&self,
|
||||
public_key: &protocol::types::PublicKey,
|
||||
connection_info: &protocol::connection::ConnectionInfo,
|
||||
) -> Result<bool, anyhow::Error> {
|
||||
let id = self
|
||||
.key_store
|
||||
.lock()
|
||||
.expect("Failed to lock key store")
|
||||
.get_cipher_id(public_key);
|
||||
if let Some(cipher_id) = id {
|
||||
return Ok(self
|
||||
.ui_requester
|
||||
.request_sign(connection_info, cipher_id, "unknown".to_string())
|
||||
.await);
|
||||
} else {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
3
apps/desktop/desktop_native/ssh_agent/src/agent/mod.rs
Normal file
3
apps/desktop/desktop_native/ssh_agent/src/agent/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod agent;
|
||||
pub mod ui_requester;
|
||||
pub use agent::BitwardenDesktopAgent;
|
||||
@@ -0,0 +1,66 @@
|
||||
use tracing::info;
|
||||
use homedir::my_home;
|
||||
|
||||
use crate::{agent::{self, BitwardenDesktopAgent}, transport::unix_listener_stream::UnixListenerStream};
|
||||
|
||||
struct PlatformListener {
|
||||
}
|
||||
|
||||
impl PlatformListener {
|
||||
pub fn spawn_listeners(agent: BitwardenDesktopAgent) {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
Self::spawn_linux_listeners(agent);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
Self::spawn_macos_listeners(agent);
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_linux_listeners(agent: BitwardenDesktopAgent) {
|
||||
let ssh_agent_directory = match my_home() {
|
||||
Ok(Some(home)) => home,
|
||||
_ => {
|
||||
info!("Could not determine home directory");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let is_flatpak = std::env::var("container") == Ok("flatpak".to_string());
|
||||
let path = if !is_flatpak {
|
||||
ssh_agent_directory
|
||||
.join(".bitwarden-ssh-agent.sock")
|
||||
.to_str()
|
||||
.expect("Path should be valid")
|
||||
.to_owned()
|
||||
} else {
|
||||
ssh_agent_directory
|
||||
.join(".var/app/com.bitwarden.desktop/data/.bitwarden-ssh-agent.sock")
|
||||
.to_str()
|
||||
.expect("Path should be valid")
|
||||
.to_owned()
|
||||
};
|
||||
|
||||
tokio::spawn(UnixListenerStream::listen(path, agent));
|
||||
}
|
||||
|
||||
fn spawn_macos_listeners(agent: BitwardenDesktopAgent) {
|
||||
let ssh_agent_directory = match my_home() {
|
||||
Ok(Some(home)) => home,
|
||||
_ => {
|
||||
info!("Could not determine home directory");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let path = ssh_agent_directory
|
||||
.join(".bitwarden-ssh-agent.sock")
|
||||
.to_str()
|
||||
.expect("Path should be valid")
|
||||
.to_owned();
|
||||
|
||||
tokio::spawn(UnixListenerStream::listen(path, agent));
|
||||
}
|
||||
}
|
||||
100
apps/desktop/desktop_native/ssh_agent/src/agent/ui_requester.rs
Normal file
100
apps/desktop/desktop_native/ssh_agent/src/agent/ui_requester.rs
Normal file
@@ -0,0 +1,100 @@
|
||||
use std::sync::{atomic::AtomicU32, Arc};
|
||||
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::protocol::connection::ConnectionInfo;
|
||||
|
||||
const TIMEOUT: std::time::Duration = std::time::Duration::from_secs(60);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct UiRequester {
|
||||
show_ui_request_tx: tokio::sync::mpsc::Sender<UiRequestMessage>,
|
||||
get_ui_response_rx: Arc<Mutex<tokio::sync::broadcast::Receiver<(u32, bool)>>>,
|
||||
}
|
||||
|
||||
const REQUEST_ID_COUNTER: AtomicU32 = AtomicU32::new(0);
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum UiRequestMessage {
|
||||
ListRequest {
|
||||
request_id: u32,
|
||||
connection_info: ConnectionInfo,
|
||||
},
|
||||
AuthRequest {
|
||||
request_id: u32,
|
||||
connection_info: ConnectionInfo,
|
||||
cipher_id: String,
|
||||
},
|
||||
SignRequest {
|
||||
request_id: u32,
|
||||
connection_info: ConnectionInfo,
|
||||
cipher_id: String,
|
||||
namespace: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl UiRequestMessage {
|
||||
pub fn id(&self) -> u32 {
|
||||
match self {
|
||||
UiRequestMessage::ListRequest { request_id, .. } => *request_id,
|
||||
UiRequestMessage::AuthRequest { request_id, .. } => *request_id,
|
||||
UiRequestMessage::SignRequest { request_id, .. } => *request_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UiRequester {
|
||||
pub fn new(
|
||||
show_ui_request_tx: tokio::sync::mpsc::Sender<UiRequestMessage>,
|
||||
get_ui_response_rx: Arc<Mutex<tokio::sync::broadcast::Receiver<(u32, bool)>>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
show_ui_request_tx,
|
||||
get_ui_response_rx,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn request_list(&self, connection_info: &ConnectionInfo) -> bool {
|
||||
let request_id = REQUEST_ID_COUNTER.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
||||
self.request(UiRequestMessage::ListRequest {
|
||||
request_id,
|
||||
connection_info: connection_info.clone(),
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn request_sign(
|
||||
&self,
|
||||
connection_info: &ConnectionInfo,
|
||||
cipher_id: String,
|
||||
namespace: String,
|
||||
) -> bool {
|
||||
let request_id = REQUEST_ID_COUNTER.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
||||
self.request(UiRequestMessage::SignRequest {
|
||||
request_id,
|
||||
connection_info: connection_info.clone(),
|
||||
cipher_id,
|
||||
namespace,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn request(&self, request: UiRequestMessage) -> bool {
|
||||
let mut rx_channel = self.get_ui_response_rx.lock().await.resubscribe();
|
||||
self.show_ui_request_tx
|
||||
.send(request.clone())
|
||||
.await
|
||||
.expect("Should send request to ui");
|
||||
|
||||
tokio::time::timeout(TIMEOUT, async move {
|
||||
while let Ok((id, response)) = rx_channel.recv().await {
|
||||
if id == request.id() {
|
||||
return response;
|
||||
}
|
||||
}
|
||||
false
|
||||
})
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
4
apps/desktop/desktop_native/ssh_agent/src/lib.rs
Normal file
4
apps/desktop/desktop_native/ssh_agent/src/lib.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod agent;
|
||||
pub mod memory;
|
||||
pub mod protocol;
|
||||
pub mod transport;
|
||||
119
apps/desktop/desktop_native/ssh_agent/src/memory/mod.rs
Normal file
119
apps/desktop/desktop_native/ssh_agent/src/memory/mod.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
//! This module implements memory storage for the SSH agent. The current
|
||||
//! implementation caches the keys in memory, and ideally uses platform secure memory APIs.
|
||||
|
||||
use crate::protocol::types::{KeyPair, PublicKeyWithName};
|
||||
|
||||
struct LockedSshItem {
|
||||
public_key: PublicKeyWithName,
|
||||
cipher_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct UnlockedSshItem {
|
||||
pub(crate) key_pair: KeyPair,
|
||||
cipher_id: String,
|
||||
}
|
||||
|
||||
impl UnlockedSshItem {
|
||||
pub fn new(key_pair: KeyPair, cipher_id: String) -> Self {
|
||||
Self {
|
||||
key_pair,
|
||||
cipher_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct LockedKeyStore {
|
||||
keys: Vec<LockedSshItem>,
|
||||
}
|
||||
|
||||
struct UnlockedKeyStore {
|
||||
keys: Vec<UnlockedSshItem>,
|
||||
}
|
||||
|
||||
pub(crate) enum KeyStore {
|
||||
Locked(LockedKeyStore),
|
||||
Unlocked(UnlockedKeyStore),
|
||||
}
|
||||
|
||||
impl KeyStore {
|
||||
pub fn new() -> Self {
|
||||
KeyStore::Locked(LockedKeyStore { keys: vec![] })
|
||||
}
|
||||
|
||||
pub fn lock(&mut self) {
|
||||
if let KeyStore::Unlocked(unlocked) = self {
|
||||
let keys = unlocked
|
||||
.keys
|
||||
.iter()
|
||||
.map(|kp| LockedSshItem {
|
||||
public_key: PublicKeyWithName::new(
|
||||
kp.key_pair.public_key().clone(),
|
||||
kp.key_pair.name().to_string(),
|
||||
),
|
||||
cipher_id: kp.cipher_id.clone(),
|
||||
})
|
||||
.collect();
|
||||
*self = KeyStore::Locked(LockedKeyStore { keys });
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_unlocked(&mut self, keys: Vec<UnlockedSshItem>) {
|
||||
*self = KeyStore::Unlocked(UnlockedKeyStore { keys });
|
||||
}
|
||||
|
||||
pub fn list_keys(&self) -> Vec<PublicKeyWithName> {
|
||||
match self {
|
||||
KeyStore::Locked(locked) => locked
|
||||
.keys
|
||||
.iter()
|
||||
.map(|item| item.public_key.clone())
|
||||
.collect(),
|
||||
KeyStore::Unlocked(unlocked) => unlocked
|
||||
.keys
|
||||
.iter()
|
||||
.map(|item| {
|
||||
PublicKeyWithName::new(
|
||||
item.key_pair.public_key().clone(),
|
||||
item.key_pair.name().to_string(),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_unlocked_keypair(
|
||||
&self,
|
||||
public_key: &crate::protocol::types::PublicKey,
|
||||
) -> Option<UnlockedSshItem> {
|
||||
if let KeyStore::Unlocked(unlocked) = self {
|
||||
for item in &unlocked.keys {
|
||||
if *item.key_pair.public_key() == *public_key {
|
||||
return Some(item.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn get_cipher_id(&self, public_key: &crate::protocol::types::PublicKey) -> Option<String> {
|
||||
if let KeyStore::Locked(locked) = self {
|
||||
for item in &locked.keys {
|
||||
if item.public_key.key == *public_key {
|
||||
return Some(item.cipher_id.clone());
|
||||
}
|
||||
}
|
||||
} else if let KeyStore::Unlocked(unlocked) = self {
|
||||
for item in &unlocked.keys {
|
||||
if *item.key_pair.public_key() == *public_key {
|
||||
return Some(item.cipher_id.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn is_locked(&self) -> bool {
|
||||
matches!(self, KeyStore::Locked(_))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
use ssh_encoding::Decode;
|
||||
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
||||
|
||||
use crate::protocol::replies::ReplyFrame;
|
||||
|
||||
pub(crate) struct AsyncStreamWrapper<PeerStream>
|
||||
where
|
||||
PeerStream: AsyncRead + AsyncWrite + Send + Sync + Unpin,
|
||||
{
|
||||
stream: PeerStream,
|
||||
}
|
||||
|
||||
impl<PeerStream> AsyncStreamWrapper<PeerStream>
|
||||
where
|
||||
PeerStream: AsyncRead + AsyncWrite + Send + Sync + Unpin,
|
||||
{
|
||||
pub fn new(stream: PeerStream) -> Self {
|
||||
Self { stream }
|
||||
}
|
||||
|
||||
pub async fn read_u32(&mut self) -> Result<u32, anyhow::Error> {
|
||||
let mut buf = [0u8; 4];
|
||||
self.stream.read_exact(&mut buf).await?;
|
||||
u32::decode(&mut buf.as_slice()).map_err(|e| anyhow::anyhow!("Failed to decode u32: {}", e))
|
||||
}
|
||||
|
||||
pub async fn read_vec(&mut self, len: usize) -> Result<Vec<u8>, anyhow::Error> {
|
||||
let mut buf = vec![0u8; len];
|
||||
self.stream.read_exact(&mut buf).await?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
pub async fn read_message(&mut self) -> Result<Vec<u8>, anyhow::Error> {
|
||||
// An SSH agent message consists of a 32 bit integer denoting the length, followed by that many bytes
|
||||
let length = self.read_u32().await? as usize;
|
||||
self.read_vec(length).await
|
||||
}
|
||||
|
||||
pub async fn write_reply(&mut self, data: &ReplyFrame) -> Result<(), anyhow::Error> {
|
||||
let raw_frame: Vec<u8> = data.into();
|
||||
self.stream.write_u32(raw_frame.len() as u32).await?;
|
||||
self.stream.write_all(&raw_frame).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
|
||||
|
||||
use crate::{
|
||||
protocol::types::{PublicKey, SessionId},
|
||||
transport::peer_info::PeerInfo,
|
||||
};
|
||||
|
||||
// The connection id is global and increasing throughout the lifetime of the desktop app
|
||||
static CONNECTION_COUNTER: AtomicU32 = AtomicU32::new(0);
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ConnectionInfo {
|
||||
id: u32,
|
||||
peer_info: PeerInfo,
|
||||
|
||||
is_forwarding: bool,
|
||||
host_key: Option<PublicKey>,
|
||||
session_identifier: Option<SessionId>,
|
||||
}
|
||||
|
||||
impl ConnectionInfo {
|
||||
pub fn new(peer_info: PeerInfo) -> Self {
|
||||
let id = CONNECTION_COUNTER.fetch_add(1, Ordering::SeqCst);
|
||||
Self {
|
||||
id,
|
||||
peer_info,
|
||||
is_forwarding: false,
|
||||
host_key: None,
|
||||
session_identifier: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(&self) -> u32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn peer_info(&self) -> &PeerInfo {
|
||||
&self.peer_info
|
||||
}
|
||||
|
||||
pub fn is_forwarding(&self) -> bool {
|
||||
self.is_forwarding
|
||||
}
|
||||
|
||||
pub fn set_forwarding(&mut self) {
|
||||
self.is_forwarding = true;
|
||||
}
|
||||
|
||||
pub fn host_key(&self) -> Option<&PublicKey> {
|
||||
self.host_key.as_ref()
|
||||
}
|
||||
|
||||
pub fn set_host_key(&mut self, host_key: PublicKey) {
|
||||
self.host_key = Some(host_key);
|
||||
}
|
||||
|
||||
pub fn session_identifier(&self) -> Option<&SessionId> {
|
||||
self.session_identifier.as_ref()
|
||||
}
|
||||
|
||||
pub fn set_session_identifier(&mut self, session_id: SessionId) {
|
||||
self.session_identifier = Some(session_id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
use crate::{
|
||||
memory::UnlockedSshItem,
|
||||
protocol::{
|
||||
connection::ConnectionInfo,
|
||||
types::{KeyPair, PublicKey, PublicKeyWithName},
|
||||
},
|
||||
};
|
||||
|
||||
pub(crate) trait Agent: Send + Sync {
|
||||
async fn request_can_list(
|
||||
&self,
|
||||
connection_info: &ConnectionInfo,
|
||||
) -> Result<bool, anyhow::Error>;
|
||||
async fn list_keys(&self) -> Result<Vec<PublicKeyWithName>, anyhow::Error>;
|
||||
async fn request_can_sign(
|
||||
&self,
|
||||
public_key: &PublicKey,
|
||||
connection_info: &ConnectionInfo,
|
||||
) -> Result<bool, anyhow::Error>;
|
||||
async fn find_ssh_item(
|
||||
&self,
|
||||
public_key: &PublicKey,
|
||||
) -> Result<Option<UnlockedSshItem>, anyhow::Error>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
const PRIVATE_ED25519_KEY: &str = "-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||
QyNTUxOQAAACBDUDO7ChZIednIJxGA95T/ZTyREftahrFEJM/eeC8mmAAAAKByJoOYciaD
|
||||
mAAAAAtzc2gtZWQyNTUxOQAAACBDUDO7ChZIednIJxGA95T/ZTyREftahrFEJM/eeC8mmA
|
||||
AAAEBQK5JpycFzP/4rchfpZhbdwxjTwHNuGx2/kvG4i6xfp0NQM7sKFkh52cgnEYD3lP9l
|
||||
PJER+1qGsUQkz954LyaYAAAAHHF1ZXh0ZW5ATWFjQm9vay1Qcm8tMTYubG9jYWwB
|
||||
-----END OPENSSH PRIVATE KEY-----";
|
||||
@@ -0,0 +1,8 @@
|
||||
//! An implementation of ssh agent
|
||||
pub(crate) mod async_stream_wrapper;
|
||||
pub(crate) mod connection;
|
||||
pub(crate) mod key_store;
|
||||
pub(crate) mod protocol;
|
||||
pub(crate) mod replies;
|
||||
pub(crate) mod requests;
|
||||
pub mod types;
|
||||
121
apps/desktop/desktop_native/ssh_agent/src/protocol/protocol.rs
Normal file
121
apps/desktop/desktop_native/ssh_agent/src/protocol/protocol.rs
Normal file
@@ -0,0 +1,121 @@
|
||||
use futures::{Stream, StreamExt};
|
||||
use tokio::{
|
||||
io::{AsyncRead, AsyncWrite},
|
||||
select,
|
||||
};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::{
|
||||
protocol::{
|
||||
async_stream_wrapper::AsyncStreamWrapper,
|
||||
connection::ConnectionInfo,
|
||||
key_store::Agent,
|
||||
replies::{AgentFailure, IdentitiesReply, SshSignReply},
|
||||
requests::Request,
|
||||
},
|
||||
transport::peer_info::PeerInfo,
|
||||
};
|
||||
|
||||
pub async fn serve_listener<PeerStream, Listener>(
|
||||
mut listener: Listener,
|
||||
cancellation_token: CancellationToken,
|
||||
agent: impl Agent,
|
||||
) -> Result<(), anyhow::Error>
|
||||
where
|
||||
PeerStream: AsyncRead + AsyncWrite + Send + Sync + Unpin + 'static,
|
||||
Listener: Stream<Item = tokio::io::Result<(PeerStream, PeerInfo)>> + Unpin,
|
||||
{
|
||||
loop {
|
||||
select! {
|
||||
_ = cancellation_token.cancelled() => {
|
||||
break;
|
||||
}
|
||||
Some(Ok((stream, peer_info))) = listener.next() => {
|
||||
let mut stream = AsyncStreamWrapper::new(stream);
|
||||
let connection_info = ConnectionInfo::new(peer_info);
|
||||
info!("Accepted connection {} from {:?}", connection_info.id(), connection_info.peer_info());
|
||||
if let Err(e) = handle_connection(&agent, &mut stream, &connection_info).await {
|
||||
error!("Error handling request: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_connection(
|
||||
agent: &impl Agent,
|
||||
stream: &mut AsyncStreamWrapper<impl AsyncRead + AsyncWrite + Send + Sync + Unpin>,
|
||||
connection: &ConnectionInfo,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
loop {
|
||||
let span = tracing::info_span!("Connection", connection_id = connection.id());
|
||||
span.in_scope(|| info!("Waiting for request"));
|
||||
|
||||
let request = match stream.read_message().await {
|
||||
Ok(request) => request,
|
||||
Err(_) => {
|
||||
span.in_scope(|| info!("Connection closed"));
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
span.in_scope(|| info!("Request {:x?}", request));
|
||||
let Ok(request) = Request::try_from(request.as_slice()) else {
|
||||
span.in_scope(|| error!("Failed to parse request"));
|
||||
stream.write_reply(&AgentFailure::new().into()).await?;
|
||||
continue;
|
||||
};
|
||||
|
||||
let response = match request {
|
||||
Request::IdentitiesRequest => {
|
||||
span.in_scope(|| info!("Received IdentitiesRequest"));
|
||||
|
||||
let Ok(true) = agent.request_can_list(connection).await else {
|
||||
span.in_scope(|| error!("List keys request denied by UI"));
|
||||
return stream.write_reply(&AgentFailure::new().into()).await;
|
||||
};
|
||||
|
||||
IdentitiesReply::new(agent.list_keys().await?)
|
||||
.encode()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to encode identities reply: {e}"))
|
||||
}
|
||||
Request::SignRequest(sign_request) => {
|
||||
span.in_scope(|| info!("Received SignRequest {:?}", sign_request));
|
||||
|
||||
let Ok(true) = agent
|
||||
.request_can_sign(sign_request.public_key(), connection)
|
||||
.await
|
||||
else {
|
||||
span.in_scope(|| error!("Sign request denied by UI"));
|
||||
return stream.write_reply(&AgentFailure::new().into()).await;
|
||||
};
|
||||
|
||||
let ssh_item = agent
|
||||
.find_ssh_item(sign_request.public_key())
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
if let Some(ssh_item) = ssh_item {
|
||||
SshSignReply::new(
|
||||
&ssh_item.key_pair.private_key(),
|
||||
&sign_request.payload_to_sign(),
|
||||
sign_request.signing_scheme(),
|
||||
)
|
||||
.encode()
|
||||
} else {
|
||||
Ok(AgentFailure::new().into())
|
||||
}
|
||||
.map_err(|e| anyhow::anyhow!("Failed to create sign reply: {e}"))
|
||||
}
|
||||
}?;
|
||||
|
||||
let encoded: Vec<u8> = (&response).into();
|
||||
span.in_scope(|| info!("Sending response"));
|
||||
span.in_scope(|| info!("Response {:x?}", encoded));
|
||||
stream.write_reply(&response).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
131
apps/desktop/desktop_native/ssh_agent/src/protocol/replies.rs
Normal file
131
apps/desktop/desktop_native/ssh_agent/src/protocol/replies.rs
Normal file
@@ -0,0 +1,131 @@
|
||||
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||
use ssh_encoding::Encode;
|
||||
|
||||
use crate::protocol::types::{
|
||||
KeyPair, PrivateKey, PublicKey, PublicKeyWithName, RsaSigningScheme, Signature,
|
||||
};
|
||||
|
||||
/// `https://www.ietf.org/archive/id/draft-miller-ssh-agent-11.html#name-protocol-messages`
|
||||
/// The different types of replies that the SSH agent can send to a client.
|
||||
#[allow(non_camel_case_types)]
|
||||
#[derive(Debug, Eq, PartialEq, TryFromPrimitive, IntoPrimitive, Default)]
|
||||
#[repr(u8)]
|
||||
pub enum ReplyType {
|
||||
/// `https://www.ietf.org/archive/id/draft-miller-ssh-agent-11.html#name-generic-server-responses`
|
||||
/// Generic response indicating failure
|
||||
/// Unsupported extensions must be replied to with SSH_AGENT_FAILURE.
|
||||
SSH_AGENT_FAILURE = 5,
|
||||
/// Generic response indicating success
|
||||
SSH_AGENT_SUCCESS = 6,
|
||||
|
||||
/// `https://www.ietf.org/archive/id/draft-miller-ssh-agent-11.html#name-extension-mechanism`
|
||||
/// Failure within an extension are replied to with this message.
|
||||
SSH_AGENT_EXTENSION_FAILURE = 28,
|
||||
/// `https://www.ietf.org/archive/id/draft-miller-ssh-agent-11.html#name-requesting-a-list-of-keys`
|
||||
/// Response to `RequestType::SSH_AGENTC_REQUEST_IDENTITIES`
|
||||
SSH_AGENT_IDENTITIES_ANSWER = 12,
|
||||
/// `https://www.ietf.org/archive/id/draft-miller-ssh-agent-11.html#name-private-key-operations``
|
||||
/// Response to `RequestType::SSH_AGENTC_SIGN_REQUEST`
|
||||
SSH_AGENT_SIGN_RESPONSE = 14,
|
||||
/// Invalid reply type
|
||||
#[default]
|
||||
SSH_AGENT_INVALID = 0,
|
||||
}
|
||||
|
||||
/// A reply is structured as a single byte indicating the type, followed by a
|
||||
/// payload that is structured according to the type.
|
||||
pub struct ReplyFrame {
|
||||
/// The serialized frame structured as
|
||||
/// reply_type|payload
|
||||
raw_frame: Vec<u8>,
|
||||
}
|
||||
|
||||
impl ReplyFrame {
|
||||
pub fn new(reply: ReplyType, payload: Vec<u8>) -> Self {
|
||||
let mut raw_frame = Vec::new();
|
||||
Into::<u8>::into(reply)
|
||||
.encode(&mut raw_frame)
|
||||
.expect("Encoding into Vec cannot fail");
|
||||
raw_frame.extend_from_slice(&payload);
|
||||
Self { raw_frame }
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<Vec<u8>> for &ReplyFrame {
|
||||
fn into(self) -> Vec<u8> {
|
||||
self.raw_frame.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct IdentitiesReply {
|
||||
keys: Vec<PublicKeyWithName>,
|
||||
}
|
||||
|
||||
impl IdentitiesReply {
|
||||
pub fn new(keys: Vec<PublicKeyWithName>) -> Self {
|
||||
Self { keys }
|
||||
}
|
||||
|
||||
/// https://www.ietf.org/archive/id/draft-miller-ssh-agent-11.html#name-requesting-a-list-of-keys
|
||||
/// The reply to a request is structured as:
|
||||
///
|
||||
/// byte SSH_AGENT_IDENTITIES_ANSWER
|
||||
/// uint32 nkeys
|
||||
/// [
|
||||
/// string public key blob
|
||||
/// string comment (a utf-8 string)
|
||||
/// ... (nkeys times)
|
||||
/// ]
|
||||
pub fn encode(&self) -> Result<ReplyFrame, ssh_encoding::Error> {
|
||||
Ok(ReplyFrame::new(ReplyType::SSH_AGENT_IDENTITIES_ANSWER, {
|
||||
let mut reply_message = Vec::new();
|
||||
(self.keys.len() as u32).encode(&mut reply_message)?;
|
||||
for key in &self.keys {
|
||||
key.key.encode(&mut reply_message)?;
|
||||
key.name.encode(&mut reply_message)?;
|
||||
}
|
||||
reply_message
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct SshSignReply(Signature);
|
||||
|
||||
impl SshSignReply {
|
||||
pub fn new(
|
||||
private_key: &PrivateKey,
|
||||
data: &[u8],
|
||||
requested_signing_scheme: Option<RsaSigningScheme>,
|
||||
) -> Self {
|
||||
Self(
|
||||
// Note, this should take into account the extension / signing scheme.
|
||||
private_key.sign(data, requested_signing_scheme).unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
/// `https://www.ietf.org/archive/id/draft-miller-ssh-agent-11.html#name-private-key-operations`
|
||||
/// A reply to a sign request is structured as:
|
||||
///
|
||||
/// byte SSH_AGENT_SIGN_RESPONSE
|
||||
/// string signature blob
|
||||
pub fn encode(&self) -> Result<ReplyFrame, ssh_encoding::Error> {
|
||||
Ok(ReplyFrame::new(ReplyType::SSH_AGENT_SIGN_RESPONSE, {
|
||||
let mut reply_payload = Vec::new();
|
||||
self.0.encode().unwrap().encode(&mut reply_payload)?;
|
||||
reply_payload
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct AgentFailure;
|
||||
impl AgentFailure {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AgentFailure> for ReplyFrame {
|
||||
fn from(_value: AgentFailure) -> Self {
|
||||
ReplyFrame::new(ReplyType::SSH_AGENT_EXTENSION_FAILURE, Vec::new())
|
||||
}
|
||||
}
|
||||
334
apps/desktop/desktop_native/ssh_agent/src/protocol/requests.rs
Normal file
334
apps/desktop/desktop_native/ssh_agent/src/protocol/requests.rs
Normal file
@@ -0,0 +1,334 @@
|
||||
//! This file contains parsing logic for requests sent to the SSH agent.
|
||||
//! Parsers must include test vectors recorded from real SSH operations.
|
||||
|
||||
use byteorder::ReadBytesExt;
|
||||
use bytes::{Buf, Bytes};
|
||||
use log::info;
|
||||
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||
|
||||
use crate::protocol::types::{PublicKey, RsaSigningScheme, SessionId, Signature};
|
||||
|
||||
/// `https://www.ietf.org/archive/id/draft-miller-ssh-agent-11.html#name-protocol-messages`
|
||||
/// The different types of requests that a client can send to the SSH agent.
|
||||
#[allow(non_camel_case_types)]
|
||||
#[derive(Debug, Eq, PartialEq, TryFromPrimitive, IntoPrimitive, Default)]
|
||||
#[repr(u8)]
|
||||
pub(crate) enum RequestType {
|
||||
/// `https://www.ietf.org/archive/id/draft-miller-ssh-agent-11.html#name-requesting-a-list-of-keys`
|
||||
/// Request the list of keys the agent is holding
|
||||
SSH_AGENTC_REQUEST_IDENTITIES = 11,
|
||||
/// `https://www.ietf.org/archive/id/draft-miller-ssh-agent-11.html#name-private-key-operations`
|
||||
/// Sign an authentication request or SSHSIG request
|
||||
SSH_AGENTC_SIGN_REQUEST = 13,
|
||||
/// `https://www.ietf.org/archive/id/draft-miller-ssh-agent-11.html#name-extension-mechanism`
|
||||
/// Handle vendor specific extensions such as session binding
|
||||
SSH_AGENTC_EXTENSION = 27,
|
||||
/// An invalid request
|
||||
#[default]
|
||||
SSH_AGENTC_INVALID = 0,
|
||||
}
|
||||
|
||||
/// `https://www.ietf.org/archive/id/draft-miller-ssh-agent-11.html#name-signature-flags`
|
||||
///
|
||||
/// There are currently two flags defined which control which signature method
|
||||
/// are used for RSA. These have no effect on other key types. If neither of these is defined,
|
||||
/// RSA is used with SHA1, however this is deprecated and should not be used.
|
||||
#[allow(non_camel_case_types)]
|
||||
#[derive(Debug, Eq, PartialEq, TryFromPrimitive, IntoPrimitive)]
|
||||
#[repr(u8)]
|
||||
pub(crate) enum SshSignFlags {
|
||||
/// Sign with SHA256 if RSA is used
|
||||
SSH_AGENT_RSA_SHA2_256 = 2,
|
||||
/// Sign with SHA512 if RSA is used
|
||||
SSH_AGENT_RSA_SHA2_512 = 4,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum Request {
|
||||
/// Request the list of keys the agent is holding
|
||||
IdentitiesRequest,
|
||||
/// Sign an authentication request or SSHSIG request
|
||||
SignRequest(SshSignRequest),
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for Request {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
// A protocol message consists of
|
||||
//
|
||||
// uint32 length
|
||||
// byte type
|
||||
// byte[length-1] contents
|
||||
//
|
||||
// The length is already stripped of in the `async_stream_wrapper::read_message`, so
|
||||
// the message is just type|contents.
|
||||
fn try_from(message: &[u8]) -> Result<Self, Self::Error> {
|
||||
if message.is_empty() {
|
||||
return Err(anyhow::anyhow!("Empty request"));
|
||||
}
|
||||
|
||||
let r#type = RequestType::try_from_primitive(message[0])?;
|
||||
let contents = message[1..].to_vec();
|
||||
|
||||
match r#type {
|
||||
RequestType::SSH_AGENTC_REQUEST_IDENTITIES => Ok(Request::IdentitiesRequest),
|
||||
RequestType::SSH_AGENTC_SIGN_REQUEST => {
|
||||
Ok(Request::SignRequest(contents.as_slice().try_into()?))
|
||||
}
|
||||
RequestType::SSH_AGENTC_EXTENSION => {
|
||||
// Only support session bind for now
|
||||
let _extension_request: SessionBindRequest = contents.as_slice().try_into()?;
|
||||
info!("Received extension request: {:?}", _extension_request);
|
||||
Err(anyhow::anyhow!("Unsupported extension request"))
|
||||
}
|
||||
_ => Err(anyhow::anyhow!("Unsupported request type: {:?}", r#type)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A sign request requests the agent to sign a blob of data with a specific key. The key is
|
||||
/// referenced by its public key blob. The payload usually has a specific structure for auth
|
||||
/// requests or SSHSIG requests. There are also flags supported that control signing behavior.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct SshSignRequest {
|
||||
public_key: PublicKey,
|
||||
payload_to_sign: Vec<u8>,
|
||||
parsed_sign_request: ParsedSignRequest,
|
||||
flags: u32,
|
||||
}
|
||||
|
||||
impl SshSignRequest {
|
||||
pub fn is_flag_set(&self, flag: SshSignFlags) -> bool {
|
||||
(self.flags & (flag as u32)) != 0
|
||||
}
|
||||
|
||||
pub fn signing_scheme(&self) -> Option<RsaSigningScheme> {
|
||||
if self.is_flag_set(SshSignFlags::SSH_AGENT_RSA_SHA2_256) {
|
||||
Some(RsaSigningScheme::Pkcs1v15Sha256)
|
||||
} else if self.is_flag_set(SshSignFlags::SSH_AGENT_RSA_SHA2_512) {
|
||||
Some(RsaSigningScheme::Pkcs1v15Sha512)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn public_key(&self) -> &PublicKey {
|
||||
&self.public_key
|
||||
}
|
||||
|
||||
pub fn payload_to_sign(&self) -> &[u8] {
|
||||
&self.payload_to_sign
|
||||
}
|
||||
|
||||
pub fn parsed_payload(&self) -> &ParsedSignRequest {
|
||||
&self.parsed_sign_request
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for SshSignRequest {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
/// `https://www.ietf.org/archive/id/draft-miller-ssh-agent-11.html#name-private-key-operations`
|
||||
/// A private key operation is structured as follows:
|
||||
///
|
||||
/// byte SSH_AGENTC_SIGN_REQUEST
|
||||
/// string key blob
|
||||
/// string data
|
||||
/// uint32 flags
|
||||
///
|
||||
/// In this case, the message already has the leading byte stripped off by the previous parsing code.
|
||||
fn try_from(mut message: &[u8]) -> Result<Self, Self::Error> {
|
||||
let public_key_blob = read_bytes(&mut message)?.to_vec();
|
||||
let data = read_bytes(&mut message)?;
|
||||
let flags = message
|
||||
.read_u32::<byteorder::BigEndian>()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to read flags from sign request: {e}"))?;
|
||||
|
||||
Ok(SshSignRequest {
|
||||
public_key: public_key_blob.try_into()?,
|
||||
payload_to_sign: data.clone(),
|
||||
parsed_sign_request: data.as_slice().try_into()?,
|
||||
flags,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum ParsedSignRequest {
|
||||
SshSigRequest { namespace: String },
|
||||
SignRequest {},
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a [u8]> for ParsedSignRequest {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(data: &'a [u8]) -> Result<Self, Self::Error> {
|
||||
let mut data = Bytes::copy_from_slice(data);
|
||||
let magic_header = "SSHSIG";
|
||||
let header = data.split_to(magic_header.len());
|
||||
|
||||
// sshsig; based on https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.sshsig
|
||||
if header == magic_header.as_bytes() {
|
||||
let _version = data.get_u32();
|
||||
|
||||
// read until null byte
|
||||
let namespace = data
|
||||
.into_iter()
|
||||
.take_while(|&x| x != 0)
|
||||
.collect::<Vec<u8>>();
|
||||
let namespace =
|
||||
String::from_utf8(namespace).map_err(|_| anyhow::anyhow!("Invalid namespace"))?;
|
||||
|
||||
Ok(ParsedSignRequest::SshSigRequest { namespace })
|
||||
} else {
|
||||
Ok(ParsedSignRequest::SignRequest {})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_bool(data: &mut &[u8]) -> Result<bool, anyhow::Error> {
|
||||
let byte = data
|
||||
.read_u8()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to read bool: {e}"))?;
|
||||
match byte {
|
||||
0 => Ok(false),
|
||||
1 => Ok(true),
|
||||
_ => Err(anyhow::anyhow!("Invalid boolean value")),
|
||||
}
|
||||
}
|
||||
|
||||
/// A helper function to read a length prefixed byte array
|
||||
pub(super) fn read_bytes(data: &mut &[u8]) -> Result<Vec<u8>, anyhow::Error> {
|
||||
let length = data
|
||||
.read_u32::<byteorder::BigEndian>()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to read length: {e}"))?;
|
||||
let mut buf = vec![
|
||||
0;
|
||||
length
|
||||
.try_into()
|
||||
.map_err(|_| anyhow::anyhow!("Invalid length"))?
|
||||
];
|
||||
std::io::Read::read_exact(data, &mut buf)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to read exact bytes: {e}"))?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
enum Extension {
|
||||
SessionBind,
|
||||
Unsupported,
|
||||
}
|
||||
|
||||
impl From<String> for Extension {
|
||||
fn from(value: String) -> Self {
|
||||
match value.as_str() {
|
||||
"session-bind@openssh.com" => Extension::SessionBind,
|
||||
_ => Extension::Unsupported,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// https://www.openssh.com/agent-restrict.html
|
||||
/// byte SSH_AGENTC_EXTENSION (0x1b)
|
||||
/// string session-bind@openssh.com
|
||||
/// string hostkey
|
||||
/// string session identifier
|
||||
/// string signature
|
||||
/// bool is_forwarding
|
||||
#[derive(Debug)]
|
||||
struct SessionBindRequest {
|
||||
host_key: PublicKey,
|
||||
session_id: SessionId,
|
||||
signature: Signature,
|
||||
is_forwarding: bool,
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for SessionBindRequest {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(mut message: &[u8]) -> Result<Self, Self::Error> {
|
||||
let extension_name = String::from_utf8(read_bytes(&mut message)?)
|
||||
.map_err(|_| anyhow::anyhow!("Invalid extension name"))?;
|
||||
match Extension::from(extension_name) {
|
||||
Extension::SessionBind => {
|
||||
let host_key = read_bytes(&mut message)?.try_into()?;
|
||||
let session_id = read_bytes(&mut message)?;
|
||||
let signature = read_bytes(&mut message)?;
|
||||
let is_forwarding = read_bool(&mut message)?;
|
||||
|
||||
Ok(SessionBindRequest {
|
||||
host_key,
|
||||
session_id: SessionId::from(session_id),
|
||||
signature: Signature::try_from(signature.as_slice())?,
|
||||
is_forwarding,
|
||||
})
|
||||
}
|
||||
Extension::Unsupported => Err(anyhow::anyhow!("Unsupported extension")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// Protocol Request Messages
|
||||
const TEST_VECTOR_REQUEST_LIST: &[u8] = &[11];
|
||||
const TEST_VECTOR_REQUEST_SIGN: &[u8] = &[
|
||||
13, 0, 0, 0, 51, 0, 0, 0, 11, 115, 115, 104, 45, 101, 100, 50, 53, 53, 49, 57, 0, 0, 0, 32,
|
||||
29, 223, 117, 159, 179, 182, 138, 116, 19, 26, 175, 28, 112, 116, 125, 161, 73, 110, 213,
|
||||
155, 210, 209, 216, 151, 51, 134, 209, 95, 89, 119, 233, 120, 0, 0, 0, 146, 0, 0, 0, 32,
|
||||
181, 207, 94, 63, 132, 40, 223, 192, 113, 235, 146, 168, 148, 99, 10, 232, 43, 52, 136,
|
||||
115, 113, 29, 242, 9, 69, 130, 8, 140, 111, 100, 189, 9, 50, 0, 0, 0, 3, 103, 105, 116, 0,
|
||||
0, 0, 14, 115, 115, 104, 45, 99, 111, 110, 110, 101, 99, 116, 105, 111, 110, 0, 0, 0, 9,
|
||||
112, 117, 98, 108, 105, 99, 107, 101, 121, 1, 0, 0, 0, 11, 115, 115, 104, 45, 101, 100, 50,
|
||||
53, 53, 49, 57, 0, 0, 0, 51, 0, 0, 0, 11, 115, 115, 104, 45, 101, 100, 50, 53, 53, 49, 57,
|
||||
0, 0, 0, 32, 29, 223, 117, 159, 179, 182, 138, 116, 19, 26, 175, 28, 112, 116, 125, 161,
|
||||
73, 110, 213, 155, 210, 209, 216, 151, 51, 134, 209, 95, 89, 119, 233, 120, 0, 0, 0, 0,
|
||||
];
|
||||
|
||||
// Inner messages for the sign request
|
||||
const TEST_VECTOR_REQUEST_SIGN_AUTHENTICATE: &[u8] = &[
|
||||
0, 0, 0, 32, 181, 207, 94, 63, 132, 40, 223, 192, 113, 235, 146, 168, 148, 99, 10, 232, 43,
|
||||
52, 136, 115, 113, 29, 242, 9, 69, 130, 8, 140, 111, 100, 189, 9, 50, 0, 0, 0, 3, 103, 105,
|
||||
116, 0, 0, 0, 14, 115, 115, 104, 45, 99, 111, 110, 110, 101, 99, 116, 105, 111, 110, 0, 0,
|
||||
0, 9, 112, 117, 98, 108, 105, 99, 107, 101, 121, 1, 0, 0, 0, 11, 115, 115, 104, 45, 101,
|
||||
100, 50, 53, 53, 49, 57, 0, 0, 0, 51, 0, 0, 0, 11, 115, 115, 104, 45, 101, 100, 50, 53, 53,
|
||||
49, 57, 0, 0, 0, 32, 29, 223, 117, 159, 179, 182, 138, 116, 19, 26, 175, 28, 112, 116, 125,
|
||||
161, 73, 110, 213, 155, 210, 209, 216, 151, 51, 134, 209, 95, 89, 119, 233, 120,
|
||||
];
|
||||
const TEST_VECTOR_REQUEST_SIGN_SSHSIG_GIT: &[u8] = &[
|
||||
83, 83, 72, 83, 73, 71, 0, 0, 0, 3, 103, 105, 116, 0, 0, 0, 0, 0, 0, 0, 6, 115, 104, 97,
|
||||
53, 49, 50, 0, 0, 0, 64, 30, 64, 7, 140, 213, 231, 218, 138, 18, 144, 116, 7, 182, 82, 23,
|
||||
205, 39, 91, 32, 189, 66, 61, 26, 22, 93, 175, 87, 211, 52, 127, 62, 223, 177, 70, 125, 65,
|
||||
44, 147, 16, 177, 89, 5, 162, 230, 184, 137, 234, 155, 152, 93, 161, 105, 254, 223, 93,
|
||||
178, 118, 238, 176, 38, 145, 49, 56, 92,
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn test_parse_identities_request() {
|
||||
let req = Request::try_from(TEST_VECTOR_REQUEST_LIST).expect("Should parse");
|
||||
assert!(matches!(req, Request::IdentitiesRequest));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_sign_request() {
|
||||
let req = Request::try_from(TEST_VECTOR_REQUEST_SIGN).expect("Should parse");
|
||||
assert!(matches!(req, Request::SignRequest { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_sign_authenticate_request() {
|
||||
let req = ParsedSignRequest::try_from(TEST_VECTOR_REQUEST_SIGN_AUTHENTICATE)
|
||||
.expect("Should parse");
|
||||
assert!(matches!(req, ParsedSignRequest::SignRequest {}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_sign_sshsig_git_request() {
|
||||
let req =
|
||||
ParsedSignRequest::try_from(TEST_VECTOR_REQUEST_SIGN_SSHSIG_GIT).expect("Should parse");
|
||||
assert!(
|
||||
matches!(req, ParsedSignRequest::SshSigRequest { namespace } if namespace == "git".to_string())
|
||||
);
|
||||
}
|
||||
}
|
||||
380
apps/desktop/desktop_native/ssh_agent/src/protocol/types.rs
Normal file
380
apps/desktop/desktop_native/ssh_agent/src/protocol/types.rs
Normal file
@@ -0,0 +1,380 @@
|
||||
use std::fmt::Debug;
|
||||
use std::fmt::Formatter;
|
||||
|
||||
use base64::prelude::BASE64_STANDARD;
|
||||
use base64::Engine as _;
|
||||
use rsa::Pkcs1v15Sign;
|
||||
use ssh_key::private::KeypairData;
|
||||
use ssh_key::{Algorithm, EcdsaCurve, HashAlg};
|
||||
|
||||
use rsa::sha2::{self, Digest};
|
||||
use signature::Signer;
|
||||
use ssh_encoding::Encode;
|
||||
use ssh_key::private::{EcdsaKeypair, Ed25519Keypair, RsaKeypair};
|
||||
|
||||
use crate::protocol::requests::read_bytes;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PublicKeyWithName {
|
||||
pub key: PublicKey,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl PublicKeyWithName {
|
||||
pub fn new(key: PublicKey, name: String) -> Self {
|
||||
Self { key, name }
|
||||
}
|
||||
}
|
||||
|
||||
/// A named SSH key pair consisting of a public and private key, and a name (comment).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct KeyPair {
|
||||
private_key: PrivateKey,
|
||||
public_key: PublicKey,
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl KeyPair {
|
||||
pub fn new(private_key: PrivateKey, name: String) -> Self {
|
||||
KeyPair {
|
||||
public_key: private_key.public_key(),
|
||||
private_key,
|
||||
name,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn public_key(&self) -> &PublicKey {
|
||||
&self.public_key
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
pub fn private_key(&self) -> &PrivateKey {
|
||||
&self.private_key
|
||||
}
|
||||
}
|
||||
|
||||
/// A detached SSH signature, containing the signature scheme and blob.
|
||||
pub struct Signature(ssh_key::Signature);
|
||||
|
||||
impl Signature {
|
||||
pub(crate) fn encode(&self) -> Result<Vec<u8>, ssh_encoding::Error> {
|
||||
let mut buffer = Vec::new();
|
||||
self.0.algorithm().as_str().encode(&mut buffer)?;
|
||||
self.0.as_bytes().encode(&mut buffer)?;
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
pub(crate) fn verify(
|
||||
&self,
|
||||
public_key: &PublicKey,
|
||||
data: &[u8],
|
||||
) -> Result<bool, anyhow::Error> {
|
||||
let public_key_parsed =
|
||||
ssh_key::PublicKey::from_bytes(public_key.blob()).map_err(|e| anyhow::anyhow!(e))?;
|
||||
|
||||
match self.0.algorithm() {
|
||||
Algorithm::Ed25519 => {
|
||||
let verifying_key = public_key_parsed
|
||||
.key_data()
|
||||
.ed25519()
|
||||
.ok_or(anyhow::anyhow!("Public key is not Ed25519"))?;
|
||||
let signature = &ed25519_dalek::Signature::from_slice(self.0.as_bytes())?;
|
||||
ed25519_dalek::VerifyingKey::from_bytes(&verifying_key.0)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to parse Ed25519 key: {e}"))?
|
||||
.verify_strict(data, signature)?;
|
||||
return Ok(true);
|
||||
}
|
||||
Algorithm::Rsa { hash: Some(alg) } => {
|
||||
let verifying_key: Result<rsa::RsaPublicKey, _> = public_key_parsed
|
||||
.key_data()
|
||||
.rsa()
|
||||
.ok_or(anyhow::anyhow!("Public key is not RSA"))?
|
||||
.try_into();
|
||||
let verifying_key =
|
||||
verifying_key.map_err(|e| anyhow::anyhow!("Failed to parse RSA key: {e}"))?;
|
||||
|
||||
match alg {
|
||||
HashAlg::Sha256 => verifying_key.verify(
|
||||
Pkcs1v15Sign::new::<sha2::Sha256>(),
|
||||
sha2::Sha256::digest(data).as_slice(),
|
||||
self.0.as_bytes(),
|
||||
),
|
||||
HashAlg::Sha512 => verifying_key.verify(
|
||||
Pkcs1v15Sign::new::<sha2::Sha512>(),
|
||||
sha2::Sha512::digest(data).as_slice(),
|
||||
self.0.as_bytes(),
|
||||
),
|
||||
_ => return Ok(false),
|
||||
}
|
||||
.map_err(|e| anyhow::anyhow!("RSA signature verification failed: {e}"))?;
|
||||
return Ok(true);
|
||||
}
|
||||
Algorithm::Ecdsa { curve } => {
|
||||
let sec1_bytes = public_key_parsed
|
||||
.key_data()
|
||||
.ecdsa()
|
||||
.unwrap()
|
||||
.as_sec1_bytes();
|
||||
match curve {
|
||||
EcdsaCurve::NistP256 => {
|
||||
use p256::ecdsa::signature::Verifier;
|
||||
p256::ecdsa::VerifyingKey::from_sec1_bytes(sec1_bytes)?
|
||||
.verify(
|
||||
data,
|
||||
&p256::ecdsa::Signature::from_slice(self.0.as_bytes())?,
|
||||
)
|
||||
.map_err(|e| {
|
||||
anyhow::anyhow!("ECDSA P-256 signature verification failed: {e}")
|
||||
})?;
|
||||
return Ok(true);
|
||||
}
|
||||
EcdsaCurve::NistP384 => {
|
||||
use p384::ecdsa::signature::Verifier;
|
||||
p384::ecdsa::VerifyingKey::from_sec1_bytes(sec1_bytes)?
|
||||
.verify(
|
||||
data,
|
||||
&p384::ecdsa::Signature::from_slice(self.0.as_bytes())?,
|
||||
)
|
||||
.map_err(|e| {
|
||||
anyhow::anyhow!("ECDSA P-384 signature verification failed: {e}")
|
||||
})?;
|
||||
return Ok(true);
|
||||
}
|
||||
EcdsaCurve::NistP521 => {
|
||||
use p521::ecdsa::signature::Verifier;
|
||||
p521::ecdsa::VerifyingKey::from_sec1_bytes(sec1_bytes)?
|
||||
.verify(
|
||||
data,
|
||||
&p521::ecdsa::Signature::from_slice(self.0.as_bytes())?,
|
||||
)
|
||||
.map_err(|e| {
|
||||
anyhow::anyhow!("ECDSA P-521 signature verification failed: {e}")
|
||||
})?;
|
||||
return Ok(true);
|
||||
}
|
||||
_ => return Ok(false),
|
||||
}
|
||||
}
|
||||
_ => return Ok(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Signature {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"SshSignature(\"{} {}\")",
|
||||
self.0.algorithm().as_str(),
|
||||
BASE64_STANDARD.encode(self.0.as_bytes())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for Signature {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
|
||||
let mut buffer = bytes;
|
||||
let alg = Algorithm::new(
|
||||
&String::from_utf8_lossy(read_bytes(&mut buffer).unwrap().as_slice()).to_string(),
|
||||
)?;
|
||||
let sig = read_bytes(&mut buffer).unwrap();
|
||||
Ok(Signature(ssh_key::Signature::new(alg, sig)?))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) struct SessionId(Vec<u8>);
|
||||
|
||||
impl From<Vec<u8>> for SessionId {
|
||||
fn from(v: Vec<u8>) -> Self {
|
||||
SessionId(v)
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for SessionId {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "SessionId(\"{}\")", BASE64_STANDARD.encode(&self.0))
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub enum RsaSigningScheme {
|
||||
Pkcs1v15Sha512,
|
||||
Pkcs1v15Sha256,
|
||||
// Sha1 is not supported because it is deprecated
|
||||
}
|
||||
|
||||
impl RsaSigningScheme {
|
||||
fn to_hash_alg(&self) -> HashAlg {
|
||||
match self {
|
||||
RsaSigningScheme::Pkcs1v15Sha512 => HashAlg::Sha512,
|
||||
RsaSigningScheme::Pkcs1v15Sha256 => HashAlg::Sha256,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum PrivateKey {
|
||||
Ed25519(Ed25519Keypair),
|
||||
Rsa(RsaKeypair),
|
||||
Ecdsa(EcdsaKeypair),
|
||||
}
|
||||
|
||||
impl Debug for PrivateKey {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
PrivateKey::Ed25519(_key) => write!(f, "Ed25519()"),
|
||||
PrivateKey::Rsa(_key) => write!(f, "Rsa()"),
|
||||
PrivateKey::Ecdsa(_key) => write!(f, "Ecdsa()"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PrivateKey {
|
||||
fn public_key(&self) -> PublicKey {
|
||||
let private_key = match self {
|
||||
PrivateKey::Ed25519(key) => ssh_key::private::PrivateKey::from(key.to_owned()),
|
||||
PrivateKey::Rsa(key) => ssh_key::private::PrivateKey::from(key.to_owned()),
|
||||
PrivateKey::Ecdsa(key) => ssh_key::private::PrivateKey::from(key.to_owned()),
|
||||
};
|
||||
|
||||
private_key
|
||||
.public_key()
|
||||
.to_bytes()
|
||||
.map(PublicKey::try_from)
|
||||
.expect("Key is always valid")
|
||||
.expect("Key is always valid")
|
||||
}
|
||||
|
||||
pub(crate) fn sign(
|
||||
&self,
|
||||
data: &[u8],
|
||||
scheme: Option<RsaSigningScheme>,
|
||||
) -> Result<Signature, anyhow::Error> {
|
||||
let private_key = match self {
|
||||
PrivateKey::Ed25519(key) => ssh_key::private::PrivateKey::from(key.clone()),
|
||||
PrivateKey::Rsa(key) => ssh_key::private::PrivateKey::from(key.clone()),
|
||||
PrivateKey::Ecdsa(key) => ssh_key::private::PrivateKey::from(key.clone()),
|
||||
};
|
||||
let result: Result<ssh_key::Signature, _> =
|
||||
if let KeypairData::Rsa(keypair) = private_key.key_data() {
|
||||
(keypair, scheme.map(|s| s.to_hash_alg())).try_sign(data)
|
||||
} else {
|
||||
private_key.try_sign(data)
|
||||
};
|
||||
result.map(Signature).map_err(|e| anyhow::anyhow!(e))
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for PrivateKey {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(pem: String) -> Result<Self, Self::Error> {
|
||||
let parsed_key = parse_key_safe(&pem)?;
|
||||
Self::try_from(parsed_key)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<ssh_key::private::PrivateKey> for PrivateKey {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(key: ssh_key::private::PrivateKey) -> Result<Self, Self::Error> {
|
||||
match key.algorithm() {
|
||||
ssh_key::Algorithm::Ed25519 => {
|
||||
Ok(Self::Ed25519(key.key_data().ed25519().unwrap().to_owned()))
|
||||
}
|
||||
ssh_key::Algorithm::Rsa { hash: _ } => {
|
||||
Ok(Self::Rsa(key.key_data().rsa().unwrap().to_owned()))
|
||||
}
|
||||
ssh_key::Algorithm::Ecdsa { curve: _ } => {
|
||||
Ok(Self::Ecdsa(key.key_data().ecdsa().unwrap().to_owned()))
|
||||
}
|
||||
_ => Err(anyhow::anyhow!("Unsupported key type")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct PublicKey {
|
||||
alg: String,
|
||||
blob: Vec<u8>,
|
||||
}
|
||||
|
||||
impl PublicKey {
|
||||
pub(super) fn encode(
|
||||
&self,
|
||||
writer: &mut impl ssh_encoding::Writer,
|
||||
) -> Result<(), ssh_encoding::Error> {
|
||||
let mut buf = Vec::new();
|
||||
self.alg().as_bytes().encode(&mut buf)?;
|
||||
self.blob().encode(&mut buf)?;
|
||||
buf.encode(writer)?;
|
||||
Ok(())
|
||||
}
|
||||
fn try_read_from(mut bytes: &[u8]) -> Result<Self, anyhow::Error> {
|
||||
let alg = String::from_utf8_lossy(read_bytes(&mut bytes)?.as_slice()).to_string();
|
||||
let blob = read_bytes(&mut bytes)?;
|
||||
Ok(PublicKey { alg, blob })
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<PublicKey> for ssh_key::PublicKey {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(key: PublicKey) -> Result<Self, Self::Error> {
|
||||
ssh_key::PublicKey::from_bytes(&key.blob).map_err(|e| anyhow::anyhow!(e))
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Vec<u8>> for PublicKey {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
|
||||
PublicKey::try_read_from(&bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for PublicKey {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "SshPublicKey(\"{} {}\")", self.alg(), self.blob_b64())
|
||||
}
|
||||
}
|
||||
|
||||
impl PublicKey {
|
||||
fn alg(&self) -> &str {
|
||||
&self.alg
|
||||
}
|
||||
|
||||
fn blob(&self) -> &[u8] {
|
||||
&self.blob
|
||||
}
|
||||
|
||||
fn blob_b64(&self) -> String {
|
||||
BASE64_STANDARD.encode(self.blob())
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_key_safe(pem: &str) -> Result<ssh_key::private::PrivateKey, anyhow::Error> {
|
||||
match ssh_key::private::PrivateKey::from_openssh(pem) {
|
||||
Ok(key) => match key.public_key().to_bytes() {
|
||||
Ok(_) => Ok(key),
|
||||
Err(e) => Err(anyhow::Error::msg(format!(
|
||||
"Failed to parse public key: {e}"
|
||||
))),
|
||||
},
|
||||
Err(e) => Err(anyhow::Error::msg(format!("Failed to parse key: {e}"))),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_keypair_creation() {
|
||||
let private_key = PrivateKey::try_from(PRIVATE_ED25519_KEY.to_string())
|
||||
.expect("Test key is always valid");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
#[cfg(windows)]
|
||||
mod named_pipe_listener_stream;
|
||||
pub mod peer_info;
|
||||
pub mod unix_listener_stream;
|
||||
@@ -0,0 +1,102 @@
|
||||
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 tracing::{error, info};
|
||||
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, PeerInfo)>,
|
||||
}
|
||||
|
||||
impl NamedPipeServerStream {
|
||||
// FIXME: Remove unwraps! They panic and terminate the whole application.
|
||||
#[allow(clippy::unwrap_used)]
|
||||
pub fn new(cancellation_token: CancellationToken, is_running: Arc<AtomicBool>) -> Self {
|
||||
let (tx, rx) = tokio::sync::mpsc::channel(16);
|
||||
tokio::spawn(async move {
|
||||
info!("Creating named pipe server on {}", PIPE_NAME);
|
||||
let mut listener = match ServerOptions::new().create(PIPE_NAME) {
|
||||
Ok(pipe) => pipe,
|
||||
Err(e) => {
|
||||
error!(error = %e, "Encountered an error creating the first pipe. The system's openssh service must likely be disabled");
|
||||
cancellation_token.cancel();
|
||||
is_running.store(false, Ordering::Relaxed);
|
||||
return;
|
||||
}
|
||||
};
|
||||
loop {
|
||||
info!("Waiting for connection");
|
||||
select! {
|
||||
_ = cancellation_token.cancelled() => {
|
||||
info!("[SSH Agent Native Module] Cancellation token triggered, stopping named pipe server");
|
||||
break;
|
||||
}
|
||||
_ = listener.connect() => {
|
||||
info!("[SSH Agent Native Module] Incoming connection");
|
||||
let handle = HANDLE(listener.as_raw_handle());
|
||||
let mut pid = 0;
|
||||
unsafe {
|
||||
if let Err(e) = GetNamedPipeClientProcessId(handle, &mut pid) {
|
||||
error!(error = %e, pid, "Faile to get named pipe client process id");
|
||||
continue
|
||||
}
|
||||
};
|
||||
|
||||
let peer_info = peerinfo::gather::get_peer_info(pid);
|
||||
let peer_info = match peer_info {
|
||||
Err(e) => {
|
||||
error!(error = %e, pid = %pid, "Failed getting process info");
|
||||
continue
|
||||
},
|
||||
Ok(info) => info,
|
||||
};
|
||||
|
||||
tx.send((listener, peer_info)).await.unwrap();
|
||||
|
||||
listener = match ServerOptions::new().create(PIPE_NAME) {
|
||||
Ok(pipe) => pipe,
|
||||
Err(e) => {
|
||||
error!(error = %e, "Encountered an error creating a new pipe");
|
||||
cancellation_token.cancel();
|
||||
is_running.store(false, Ordering::Relaxed);
|
||||
return;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
Self { rx }
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for NamedPipeServerStream {
|
||||
type Item = io::Result<(NamedPipeServer, PeerInfo)>;
|
||||
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<io::Result<(NamedPipeServer, PeerInfo)>>> {
|
||||
let this = self.project();
|
||||
|
||||
this.rx.poll_recv(cx).map(|v| v.map(Ok))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
use std::fmt::Debug;
|
||||
|
||||
use sysinfo::{Pid, System};
|
||||
|
||||
/// 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(Clone)]
|
||||
pub struct PeerInfo {
|
||||
uid: u32,
|
||||
pid: u32,
|
||||
process_name: String,
|
||||
peer_type: PeerType,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum PeerType {
|
||||
#[cfg(windows)]
|
||||
NamedPipe,
|
||||
UnixSocket,
|
||||
}
|
||||
|
||||
impl PeerInfo {
|
||||
pub fn new(pid: u32, peer_type: PeerType) -> Self {
|
||||
Self::from_pid(pid, peer_type).unwrap_or_else(|_| PeerInfo::unknown())
|
||||
}
|
||||
|
||||
fn from_pid(peer_pid: u32, peer_type: PeerType) -> Result<Self, ()> {
|
||||
let mut system = System::new();
|
||||
system.refresh_processes(
|
||||
sysinfo::ProcessesToUpdate::Some(&[Pid::from_u32(peer_pid)]),
|
||||
true,
|
||||
);
|
||||
if let Some(process) = system.process(Pid::from_u32(peer_pid)) {
|
||||
return Ok(Self {
|
||||
uid: **process.user_id().ok_or(())?,
|
||||
pid: peer_pid,
|
||||
process_name: process.name().to_str().ok_or(())?.to_string(),
|
||||
peer_type,
|
||||
});
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unknown() -> Self {
|
||||
Self {
|
||||
uid: 0,
|
||||
pid: 0,
|
||||
process_name: "Unknown application".to_string(),
|
||||
peer_type: PeerType::UnixSocket,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn uid(&self) -> u32 {
|
||||
self.uid
|
||||
}
|
||||
|
||||
pub fn pid(&self) -> u32 {
|
||||
self.pid
|
||||
}
|
||||
|
||||
pub fn process_name(&self) -> &str {
|
||||
&self.process_name
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for PeerInfo {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("PeerInfo")
|
||||
.field("uid", &self.uid)
|
||||
.field("pid", &self.pid)
|
||||
.field("process_name", &self.process_name)
|
||||
.field("peer_type", &self.peer_type)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
use futures::Stream;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
use std::{fs, io};
|
||||
use tokio::net::{UnixListener, UnixStream};
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::agent::agent::BitwardenDesktopAgent;
|
||||
use crate::transport::peer_info::{PeerInfo, PeerType};
|
||||
|
||||
pub struct UnixListenerStream {
|
||||
inner: tokio::net::UnixListener,
|
||||
}
|
||||
|
||||
impl UnixListenerStream {
|
||||
fn new(listener: tokio::net::UnixListener) -> Self {
|
||||
Self { inner: listener }
|
||||
}
|
||||
|
||||
/// Start listening on the given Unix socket path.
|
||||
/// This will return only once the lister stops. Returning will attempt to clean up the socket file.
|
||||
pub async fn listen(
|
||||
ssh_path: String,
|
||||
agent: BitwardenDesktopAgent,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
info!(socket = %ssh_path, "Starting SSH Unix listener");
|
||||
|
||||
// Remove existing socket file if it exists
|
||||
let socket_path = std::path::Path::new(&ssh_path);
|
||||
if let Err(e) = std::fs::remove_file(socket_path) {
|
||||
error!(error = %e, socket = %ssh_path, "Could not remove existing socket file");
|
||||
if e.kind() != std::io::ErrorKind::NotFound {
|
||||
return Err(anyhow::Error::new(e));
|
||||
}
|
||||
}
|
||||
|
||||
match UnixListener::bind(socket_path) {
|
||||
Ok(listener) => {
|
||||
// Only the current user should be able to access the socket
|
||||
if let Err(e) = fs::set_permissions(socket_path, fs::Permissions::from_mode(0o600))
|
||||
{
|
||||
error!(error = %e, socket = ?socket_path, "Could not set socket permissions");
|
||||
return Err(anyhow::Error::new(e));
|
||||
}
|
||||
|
||||
let stream = Self::new(listener);
|
||||
agent.serve(stream).await;
|
||||
}
|
||||
Err(e) => {
|
||||
error!(error = %e, socket = %ssh_path, "Unable to start start agent server");
|
||||
}
|
||||
}
|
||||
|
||||
let _ = std::fs::remove_file(socket_path);
|
||||
info!(socket = %ssh_path, "SSH Unix listener stopped");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for UnixListenerStream {
|
||||
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(Ok((stream, PeerInfo::unknown()))));
|
||||
}
|
||||
},
|
||||
Err(_) => return Poll::Ready(Some(Ok((stream, PeerInfo::unknown())))),
|
||||
};
|
||||
Poll::Ready(Some(Ok((
|
||||
stream,
|
||||
PeerInfo::new(pid as u32, PeerType::UnixSocket),
|
||||
))))
|
||||
}
|
||||
Poll::Ready(Err(err)) => Poll::Ready(Some(Err(err))),
|
||||
Poll::Pending => Poll::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<tokio::net::UnixListener> for UnixListenerStream {
|
||||
fn as_ref(&self) -> &tokio::net::UnixListener {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl AsMut<tokio::net::UnixListener> for UnixListenerStream {
|
||||
fn as_mut(&mut self) -> &mut tokio::net::UnixListener {
|
||||
&mut self.inner
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { concatMap, delay, filter, firstValueFrom, from, race, take, timer } fro
|
||||
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { sshagent } from "@bitwarden/desktop-napi";
|
||||
import { sshagent, sshagent_v2 } from "@bitwarden/desktop-napi";
|
||||
|
||||
class AgentResponse {
|
||||
requestId: number;
|
||||
@@ -19,7 +19,8 @@ export class MainSshAgentService {
|
||||
|
||||
private requestResponses: AgentResponse[] = [];
|
||||
private request_id = 0;
|
||||
private agentState: sshagent.SshAgentState;
|
||||
private agentStateV1: sshagent.SshAgentState;
|
||||
private agentStateV2: sshagent_v2.SshAgentState;
|
||||
|
||||
constructor(
|
||||
private logService: LogService,
|
||||
@@ -30,11 +31,15 @@ export class MainSshAgentService {
|
||||
});
|
||||
|
||||
ipcMain.handle("sshagent.isloaded", async (event: any) => {
|
||||
return this.agentState != null;
|
||||
return this.agentStateV1 != null && this.agentStateV2 != null;
|
||||
});
|
||||
}
|
||||
|
||||
init() {
|
||||
this.init_v2();
|
||||
}
|
||||
|
||||
init_v1() {
|
||||
// handle sign request passing to UI
|
||||
sshagent
|
||||
.serve(async (err: Error, sshUiRequest: sshagent.SshUiRequest) => {
|
||||
@@ -83,7 +88,7 @@ export class MainSshAgentService {
|
||||
return response.accepted;
|
||||
})
|
||||
.then((agentState: sshagent.SshAgentState) => {
|
||||
this.agentState = agentState;
|
||||
this.agentStateV1 = agentState;
|
||||
this.logService.info("SSH agent started");
|
||||
})
|
||||
.catch((e) => {
|
||||
@@ -93,8 +98,8 @@ export class MainSshAgentService {
|
||||
ipcMain.handle(
|
||||
"sshagent.setkeys",
|
||||
async (event: any, keys: { name: string; privateKey: string; cipherId: string }[]) => {
|
||||
if (this.agentState != null && (await sshagent.isRunning(this.agentState))) {
|
||||
sshagent.setKeys(this.agentState, keys);
|
||||
if (this.agentStateV1 != null && (await sshagent.isRunning(this.agentStateV1))) {
|
||||
sshagent.setKeys(this.agentStateV1, keys);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -106,14 +111,99 @@ export class MainSshAgentService {
|
||||
);
|
||||
|
||||
ipcMain.handle("sshagent.lock", async (event: any) => {
|
||||
if (this.agentState != null && (await sshagent.isRunning(this.agentState))) {
|
||||
sshagent.lock(this.agentState);
|
||||
if (this.agentStateV1 != null && (await sshagent.isRunning(this.agentStateV1))) {
|
||||
sshagent.lock(this.agentStateV1);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("sshagent.clearkeys", async (event: any) => {
|
||||
if (this.agentState != null) {
|
||||
sshagent.clearKeys(this.agentState);
|
||||
if (this.agentStateV1 != null) {
|
||||
sshagent.clearKeys(this.agentStateV1);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
init_v2() {
|
||||
// handle sign request passing to UI
|
||||
sshagent_v2
|
||||
.serve(async (err: Error, sshUiRequest: sshagent_v2.SshUiRequest) => {
|
||||
// clear all old (> SIGN_TIMEOUT) requests
|
||||
this.requestResponses = this.requestResponses.filter(
|
||||
(response) => response.timestamp > new Date(Date.now() - this.SIGN_TIMEOUT),
|
||||
);
|
||||
|
||||
this.request_id += 1;
|
||||
const id_for_this_request = this.request_id;
|
||||
this.messagingService.send("sshagent.signrequest", {
|
||||
cipherId: sshUiRequest.cipherId,
|
||||
isListRequest: sshUiRequest.isList,
|
||||
requestId: id_for_this_request,
|
||||
processName: sshUiRequest.processName,
|
||||
isAgentForwarding: sshUiRequest.isForwarding,
|
||||
namespace: sshUiRequest.namespace,
|
||||
});
|
||||
|
||||
const result = await firstValueFrom(
|
||||
race(
|
||||
from([false]).pipe(delay(this.SIGN_TIMEOUT)),
|
||||
|
||||
//poll for response
|
||||
timer(0, this.REQUEST_POLL_INTERVAL).pipe(
|
||||
concatMap(() => from(this.requestResponses)),
|
||||
filter((response) => response.requestId == id_for_this_request),
|
||||
take(1),
|
||||
concatMap(() => from([true])),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const response = this.requestResponses.find(
|
||||
(response) => response.requestId == id_for_this_request,
|
||||
);
|
||||
|
||||
this.requestResponses = this.requestResponses.filter(
|
||||
(response) => response.requestId != id_for_this_request,
|
||||
);
|
||||
|
||||
return response.accepted;
|
||||
})
|
||||
.then((agentState: sshagent_v2.SshAgentState) => {
|
||||
this.agentStateV2 = agentState;
|
||||
this.logService.info("SSH agent started");
|
||||
})
|
||||
.catch((e) => {
|
||||
this.logService.error("SSH agent encountered an error: ", e);
|
||||
});
|
||||
|
||||
ipcMain.handle(
|
||||
"sshagent.setkeys",
|
||||
async (event: any, keys: { name: string; privateKey: string; cipherId: string }[]) => {
|
||||
if (this.agentStateV2 != null && (await sshagent_v2.isRunning(this.agentStateV2))) {
|
||||
sshagent_v2.setKeys(this.agentStateV2, keys);
|
||||
}
|
||||
},
|
||||
);
|
||||
ipcMain.handle(
|
||||
"sshagent.signrequestresponse",
|
||||
async (event: any, { requestId, accepted }: { requestId: number; accepted: boolean }) => {
|
||||
this.requestResponses.push({ requestId, accepted, timestamp: new Date() });
|
||||
},
|
||||
);
|
||||
|
||||
ipcMain.handle("sshagent.lock", async (event: any) => {
|
||||
if (this.agentStateV2 != null && (await sshagent_v2.isRunning(this.agentStateV2))) {
|
||||
sshagent_v2.lock(this.agentStateV2);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("sshagent.clearkeys", async (event: any) => {
|
||||
if (this.agentStateV2 != null) {
|
||||
sshagent_v2.clearKeys(this.agentStateV2);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ export class SshAgentService implements OnDestroy {
|
||||
|
||||
private authorizedSshKeys: Record<string, Date> = {};
|
||||
|
||||
private isFeatureFlagEnabled = false;
|
||||
private isFeatureFlagEnabled = true;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
@@ -272,6 +272,7 @@ export class SshAgentService implements OnDestroy {
|
||||
cipherId: cipher.id,
|
||||
};
|
||||
});
|
||||
//this.logService.info(`Setting ${keys.length} SSH keys in agent renderer`);
|
||||
await ipc.platform.sshAgent.setKeys(keys);
|
||||
}),
|
||||
takeUntil(this.destroy$),
|
||||
|
||||
@@ -33,6 +33,7 @@ export class DefaultProcessReloadService implements ProcessReloadServiceAbstract
|
||||
) {}
|
||||
|
||||
async startProcessReload(authService: AuthService): Promise<void> {
|
||||
return;
|
||||
const accounts = await firstValueFrom(this.accountService.accounts$);
|
||||
if (accounts != null) {
|
||||
const keys = Object.keys(accounts);
|
||||
|
||||
Reference in New Issue
Block a user