mirror of
https://github.com/bitwarden/browser.git
synced 2025-02-28 03:21:40 +01:00
[PM-15934] Add agent-forwarding detection and git signing detection parsers (#12371)
* Add agent-forwarding detection and git signing detection parsers * Cleanup * Pin russh version * Run cargo fmt * Fix build * Update apps/desktop/desktop_native/core/src/ssh_agent/mod.rs Co-authored-by: Daniel García <dani-garcia@users.noreply.github.com> * Pass through entire namespace * Move to bytes crate * Fix clippy errors * Fix clippy warning * Run cargo fmt * Fix build * Add renovate for bytes * Fix clippy warn --------- Co-authored-by: Daniel García <dani-garcia@users.noreply.github.com>
This commit is contained in:
parent
ce5a5e3649
commit
cb028eadb5
1
.github/renovate.json5
vendored
1
.github/renovate.json5
vendored
@ -123,6 +123,7 @@
|
||||
matchPackageNames: [
|
||||
"@emotion/css",
|
||||
"@webcomponents/custom-elements",
|
||||
"bytes",
|
||||
"concurrently",
|
||||
"cross-env",
|
||||
"del",
|
||||
|
3
apps/desktop/desktop_native/Cargo.lock
generated
3
apps/desktop/desktop_native/Cargo.lock
generated
@ -439,7 +439,7 @@ checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
|
||||
[[package]]
|
||||
name = "bitwarden-russh"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/bitwarden/bitwarden-russh.git?rev=23b50e3bbe6d56ef19ab0e98e8bb1462cb6d77ae#23b50e3bbe6d56ef19ab0e98e8bb1462cb6d77ae"
|
||||
source = "git+https://github.com/bitwarden/bitwarden-russh.git?rev=3d48f140fd506412d186203238993163a8c4e536#3d48f140fd506412d186203238993163a8c4e536"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"byteorder",
|
||||
@ -942,6 +942,7 @@ dependencies = [
|
||||
"base64",
|
||||
"bitwarden-russh",
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"cbc",
|
||||
"core-foundation",
|
||||
"desktop_objc",
|
||||
|
@ -21,7 +21,7 @@ manual_test = []
|
||||
aes = "=0.8.4"
|
||||
anyhow = { workspace = true }
|
||||
arboard = { version = "=3.4.1", default-features = false, features = [
|
||||
"wayland-data-control",
|
||||
"wayland-data-control",
|
||||
] }
|
||||
argon2 = { version = "=0.5.3", features = ["zeroize"] }
|
||||
base64 = "=0.22.1"
|
||||
@ -39,12 +39,12 @@ scopeguard = "=1.2.0"
|
||||
sha2 = "=0.10.8"
|
||||
ssh-encoding = "=0.2.0"
|
||||
ssh-key = { version = "=0.6.7", default-features = false, features = [
|
||||
"encryption",
|
||||
"ed25519",
|
||||
"rsa",
|
||||
"getrandom",
|
||||
"encryption",
|
||||
"ed25519",
|
||||
"rsa",
|
||||
"getrandom",
|
||||
] }
|
||||
bitwarden-russh = { git = "https://github.com/bitwarden/bitwarden-russh.git", rev = "23b50e3bbe6d56ef19ab0e98e8bb1462cb6d77ae" }
|
||||
bitwarden-russh = { git = "https://github.com/bitwarden/bitwarden-russh.git", rev = "3d48f140fd506412d186203238993163a8c4e536" }
|
||||
tokio = { workspace = true, features = ["io-util", "sync", "macros", "net"] }
|
||||
tokio-stream = { workspace = true, features = ["net"] }
|
||||
tokio-util = { workspace = true, features = ["codec"] }
|
||||
@ -53,21 +53,22 @@ typenum = "=1.17.0"
|
||||
pkcs8 = { version = "=0.10.2", features = ["alloc", "encryption", "pem"] }
|
||||
rsa = "=0.9.6"
|
||||
ed25519 = { version = "=2.2.3", features = ["pkcs8"] }
|
||||
sysinfo = { version = "=0.33.1", features = ["windows"] }
|
||||
bytes = "1.9.0"
|
||||
sysinfo = { version = "0.33.1", features = ["windows"] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
widestring = { version = "=1.1.0", optional = true }
|
||||
windows = { version = "=0.58.0", features = [
|
||||
"Foundation",
|
||||
"Security_Credentials_UI",
|
||||
"Security_Cryptography",
|
||||
"Storage_Streams",
|
||||
"Win32_Foundation",
|
||||
"Win32_Security_Credentials",
|
||||
"Win32_System_WinRT",
|
||||
"Win32_UI_Input_KeyboardAndMouse",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
"Win32_System_Pipes",
|
||||
"Foundation",
|
||||
"Security_Credentials_UI",
|
||||
"Security_Cryptography",
|
||||
"Storage_Streams",
|
||||
"Win32_Foundation",
|
||||
"Win32_Security_Credentials",
|
||||
"Win32_System_WinRT",
|
||||
"Win32_UI_Input_KeyboardAndMouse",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
"Win32_System_Pipes",
|
||||
], optional = true }
|
||||
|
||||
[target.'cfg(windows)'.dev-dependencies]
|
||||
|
@ -18,6 +18,8 @@ mod peercred_unix_listener_stream;
|
||||
|
||||
pub mod importer;
|
||||
pub mod peerinfo;
|
||||
mod request_parser;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct BitwardenDesktopAgent {
|
||||
keystore: ssh_agent::KeyStore,
|
||||
@ -35,19 +37,37 @@ pub struct SshAgentUIRequest {
|
||||
pub cipher_id: Option<String>,
|
||||
pub process_name: String,
|
||||
pub is_list: bool,
|
||||
pub namespace: Option<String>,
|
||||
pub is_forwarding: bool,
|
||||
}
|
||||
|
||||
impl ssh_agent::Agent<peerinfo::models::PeerInfo> for BitwardenDesktopAgent {
|
||||
async fn confirm(&self, ssh_key: Key, info: &peerinfo::models::PeerInfo) -> bool {
|
||||
async fn confirm(&self, ssh_key: Key, data: &[u8], 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;
|
||||
let request_data = match request_parser::parse_request(data) {
|
||||
Ok(data) => data,
|
||||
Err(e) => {
|
||||
println!("[SSH Agent] Error while parsing request: {}", e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
let namespace = match request_data {
|
||||
request_parser::SshAgentSignRequest::SshSigRequest(ref req) => {
|
||||
Some(req.namespace.clone())
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
println!(
|
||||
"[SSH Agent] Confirming request from application: {}",
|
||||
info.process_name()
|
||||
"[SSH Agent] Confirming request from application: {}, is_forwarding: {}, namespace: {}",
|
||||
info.process_name(),
|
||||
info.is_forwarding(),
|
||||
namespace.clone().unwrap_or_default(),
|
||||
);
|
||||
|
||||
let mut rx_channel = self.get_ui_response_rx.lock().await.resubscribe();
|
||||
@ -57,6 +77,8 @@ impl ssh_agent::Agent<peerinfo::models::PeerInfo> for BitwardenDesktopAgent {
|
||||
cipher_id: Some(ssh_key.cipher_uuid.clone()),
|
||||
process_name: info.process_name().to_string(),
|
||||
is_list: false,
|
||||
namespace,
|
||||
is_forwarding: info.is_forwarding(),
|
||||
})
|
||||
.await
|
||||
.expect("Should send request to ui");
|
||||
@ -81,6 +103,8 @@ impl ssh_agent::Agent<peerinfo::models::PeerInfo> for BitwardenDesktopAgent {
|
||||
cipher_id: None,
|
||||
process_name: info.process_name().to_string(),
|
||||
is_list: true,
|
||||
namespace: None,
|
||||
is_forwarding: info.is_forwarding(),
|
||||
};
|
||||
self.show_ui_request_tx
|
||||
.send(message)
|
||||
@ -93,6 +117,17 @@ impl ssh_agent::Agent<peerinfo::models::PeerInfo> for BitwardenDesktopAgent {
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
async fn set_is_forwarding(
|
||||
&self,
|
||||
is_forwarding: bool,
|
||||
connection_info: &peerinfo::models::PeerInfo,
|
||||
) {
|
||||
// is_forwarding can only be added but never removed from a connection
|
||||
if is_forwarding {
|
||||
connection_info.set_forwarding(is_forwarding);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BitwardenDesktopAgent {
|
||||
|
@ -34,9 +34,7 @@ impl Stream for PeercredUnixListenerStream {
|
||||
return Poll::Ready(Some(Ok((stream, PeerInfo::unknown()))));
|
||||
}
|
||||
},
|
||||
Err(_) => {
|
||||
return Poll::Ready(Some(Ok((stream, PeerInfo::unknown()))));
|
||||
}
|
||||
Err(_) => return Poll::Ready(Some(Ok((stream, PeerInfo::unknown())))),
|
||||
};
|
||||
let peer_info = peerinfo::gather::get_peer_info(pid as u32);
|
||||
match peer_info {
|
||||
|
@ -1,3 +1,5 @@
|
||||
use std::sync::{atomic::AtomicBool, Arc};
|
||||
|
||||
/**
|
||||
* 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.
|
||||
@ -7,6 +9,7 @@ pub struct PeerInfo {
|
||||
uid: u32,
|
||||
pid: u32,
|
||||
process_name: String,
|
||||
is_forwarding: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl PeerInfo {
|
||||
@ -15,6 +18,16 @@ impl PeerInfo {
|
||||
uid,
|
||||
pid,
|
||||
process_name,
|
||||
is_forwarding: Arc::new(AtomicBool::new(false)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unknown() -> Self {
|
||||
Self {
|
||||
uid: 0,
|
||||
pid: 0,
|
||||
process_name: "Unknown application".to_string(),
|
||||
is_forwarding: Arc::new(AtomicBool::new(false)),
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,7 +43,13 @@ impl PeerInfo {
|
||||
&self.process_name
|
||||
}
|
||||
|
||||
pub fn unknown() -> Self {
|
||||
Self::new(0, 0, "Unknown application".to_string())
|
||||
pub fn is_forwarding(&self) -> bool {
|
||||
self.is_forwarding
|
||||
.load(std::sync::atomic::Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub fn set_forwarding(&self, value: bool) {
|
||||
self.is_forwarding
|
||||
.store(value, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,41 @@
|
||||
use bytes::{Buf, Bytes};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct SshSigRequest {
|
||||
pub namespace: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct SignRequest {}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum SshAgentSignRequest {
|
||||
SshSigRequest(SshSigRequest),
|
||||
SignRequest(SignRequest),
|
||||
}
|
||||
|
||||
pub(crate) fn parse_request(data: &[u8]) -> Result<SshAgentSignRequest, anyhow::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(SshAgentSignRequest::SshSigRequest(SshSigRequest {
|
||||
namespace,
|
||||
}))
|
||||
} else {
|
||||
// regular sign request
|
||||
Ok(SshAgentSignRequest::SignRequest(SignRequest {}))
|
||||
}
|
||||
}
|
9
apps/desktop/desktop_native/napi/index.d.ts
vendored
9
apps/desktop/desktop_native/napi/index.d.ts
vendored
@ -67,7 +67,14 @@ export declare namespace sshagent {
|
||||
status: SshKeyImportStatus
|
||||
sshKey?: SshKey
|
||||
}
|
||||
export function serve(callback: (err: Error | null, arg0: string | undefined | null, arg1: boolean, arg2: string) => any): Promise<SshAgentState>
|
||||
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
|
||||
|
@ -243,9 +243,18 @@ pub mod sshagent {
|
||||
}
|
||||
}
|
||||
|
||||
#[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>,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub async fn serve(
|
||||
callback: ThreadsafeFunction<(Option<String>, bool, String), CalleeHandled>,
|
||||
callback: ThreadsafeFunction<SshUIRequest, CalleeHandled>,
|
||||
) -> napi::Result<SshAgentState> {
|
||||
let (auth_request_tx, mut auth_request_rx) =
|
||||
tokio::sync::mpsc::channel::<desktop_core::ssh_agent::SshAgentUIRequest>(32);
|
||||
@ -262,11 +271,13 @@ pub mod sshagent {
|
||||
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((
|
||||
request.cipher_id,
|
||||
request.is_list,
|
||||
request.process_name,
|
||||
)))
|
||||
.call_async(Ok(SshUIRequest {
|
||||
cipher_id: request.cipher_id,
|
||||
is_list: request.is_list,
|
||||
process_name: request.process_name,
|
||||
is_forwarding: request.is_forwarding,
|
||||
namespace: request.namespace,
|
||||
}))
|
||||
.await;
|
||||
match promise_result {
|
||||
Ok(promise_result) => match promise_result.await {
|
||||
|
@ -3509,9 +3509,27 @@
|
||||
"sshkeyApprovalTitle": {
|
||||
"message": "Confirm SSH key usage"
|
||||
},
|
||||
"agentForwardingWarningTitle": {
|
||||
"message": "Warning: Agent Forwarding"
|
||||
},
|
||||
"agentForwardingWarningText": {
|
||||
"message": "This request comes from a remote device that you are logged into"
|
||||
},
|
||||
"sshkeyApprovalMessageInfix": {
|
||||
"message": "is requesting access to"
|
||||
},
|
||||
"sshkeyApprovalMessageSuffix": {
|
||||
"message": "in order to"
|
||||
},
|
||||
"sshActionLogin": {
|
||||
"message": "authenticate to a server"
|
||||
},
|
||||
"sshActionSign": {
|
||||
"message": "sign a message"
|
||||
},
|
||||
"sshActionGitSign": {
|
||||
"message": "sign a git commit"
|
||||
},
|
||||
"unknownApplication": {
|
||||
"message": "An application"
|
||||
},
|
||||
|
@ -2,8 +2,17 @@
|
||||
<bit-dialog>
|
||||
<div class="tw-font-semibold" bitDialogTitle>{{ "sshkeyApprovalTitle" | i18n }}</div>
|
||||
<div bitDialogContent>
|
||||
<app-callout
|
||||
type="warning"
|
||||
title="{{ 'agentForwardingWarningTitle' | i18n }}"
|
||||
*ngIf="params.isAgentForwarding"
|
||||
>
|
||||
{{ 'agentForwardingWarningText' | i18n }}
|
||||
</app-callout>
|
||||
|
||||
<b>{{params.applicationName}}</b> {{ "sshkeyApprovalMessageInfix" | i18n }}
|
||||
<b>{{params.cipherName}}</b>.
|
||||
<b>{{params.cipherName}}</b>
|
||||
{{ "sshkeyApprovalMessageSuffix" | i18n }} {{ params.action | i18n }}
|
||||
</div>
|
||||
<div bitDialogFooter>
|
||||
<button type="submit" bitButton bitFormButton buttonType="primary">
|
||||
|
@ -17,6 +17,8 @@ import { CipherFormGeneratorComponent } from "@bitwarden/vault";
|
||||
export interface ApproveSshRequestParams {
|
||||
cipherName: string;
|
||||
applicationName: string;
|
||||
isAgentForwarding: boolean;
|
||||
action: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
@ -44,11 +46,26 @@ export class ApproveSshRequestComponent {
|
||||
private formBuilder: FormBuilder,
|
||||
) {}
|
||||
|
||||
static open(dialogService: DialogService, cipherName: string, applicationName: string) {
|
||||
static open(
|
||||
dialogService: DialogService,
|
||||
cipherName: string,
|
||||
applicationName: string,
|
||||
isAgentForwarding: boolean,
|
||||
namespace: string,
|
||||
) {
|
||||
let actioni18nKey = "sshActionLogin";
|
||||
if (namespace === "git") {
|
||||
actioni18nKey = "sshActionGitSign";
|
||||
} else if (namespace != null && namespace != "") {
|
||||
actioni18nKey = "sshActionSign";
|
||||
}
|
||||
|
||||
return dialogService.open<boolean, ApproveSshRequestParams>(ApproveSshRequestComponent, {
|
||||
data: {
|
||||
cipherName,
|
||||
applicationName,
|
||||
isAgentForwarding,
|
||||
action: actioni18nKey,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ export class MainSshAgentService {
|
||||
init() {
|
||||
// handle sign request passing to UI
|
||||
sshagent
|
||||
.serve(async (err: Error, cipherId: string, isListRequest: boolean, processName: string) => {
|
||||
.serve(async (err: Error, sshUiRequest: sshagent.SshUiRequest) => {
|
||||
// clear all old (> SIGN_TIMEOUT) requests
|
||||
this.requestResponses = this.requestResponses.filter(
|
||||
(response) => response.timestamp > new Date(Date.now() - this.SIGN_TIMEOUT),
|
||||
@ -56,10 +56,12 @@ export class MainSshAgentService {
|
||||
this.request_id += 1;
|
||||
const id_for_this_request = this.request_id;
|
||||
this.messagingService.send("sshagent.signrequest", {
|
||||
cipherId,
|
||||
isListRequest,
|
||||
cipherId: sshUiRequest.cipherId,
|
||||
isListRequest: sshUiRequest.isList,
|
||||
requestId: id_for_this_request,
|
||||
processName,
|
||||
processName: sshUiRequest.processName,
|
||||
isAgentForwarding: sshUiRequest.isForwarding,
|
||||
namespace: sshUiRequest.namespace,
|
||||
});
|
||||
|
||||
const result = await firstValueFrom(
|
||||
|
@ -148,6 +148,8 @@ export class SshAgentService implements OnDestroy {
|
||||
const isListRequest = message.isListRequest as boolean;
|
||||
const requestId = message.requestId as number;
|
||||
let application = message.processName as string;
|
||||
const namespace = message.namespace as string;
|
||||
const isAgentForwarding = message.isAgentForwarding as boolean;
|
||||
if (application == "") {
|
||||
application = this.i18nService.t("unknownApplication");
|
||||
}
|
||||
@ -181,6 +183,8 @@ export class SshAgentService implements OnDestroy {
|
||||
this.dialogService,
|
||||
cipher.name,
|
||||
application,
|
||||
isAgentForwarding,
|
||||
namespace,
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(dialogRef.closed);
|
||||
|
Loading…
Reference in New Issue
Block a user