1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-08 04:33:38 +00:00

Use Rust module to run commands

This commit is contained in:
Isaiah Inuwa
2025-11-06 14:58:12 -06:00
parent 51a97561df
commit 64aa3a28e1
5 changed files with 372 additions and 5 deletions

View File

@@ -1024,6 +1024,7 @@ dependencies = [
"widestring",
"windows 0.62.2",
"windows-future 0.3.2",
"windows_plugin_authenticator",
"zbus",
"zbus_polkit",
"zeroizing-alloc",

View File

@@ -75,6 +75,7 @@ windows = { workspace = true, features = [
"Win32_System_Pipes",
], optional = true }
windows-future = { workspace = true }
windows_plugin_authenticator = { path = "../windows_plugin_authenticator" }
[target.'cfg(windows)'.dev-dependencies]
keytar = { workspace = true }

View File

@@ -4,3 +4,125 @@
#[cfg_attr(target_os = "macos", path = "macos.rs")]
mod autofill;
pub use autofill::*;
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Deserialize)]
struct RunCommandRequest {
#[serde(rename = "namespace")]
namespace: String,
#[serde(rename = "command")]
command: RunCommand,
#[serde(rename = "params")]
params: Value,
}
#[derive(Deserialize)]
enum RunCommand {
#[serde(rename = "status")]
Status,
#[serde(rename = "sync")]
Sync,
}
#[derive(Debug, Deserialize)]
struct SyncParameters {
#[serde(rename = "credentials")]
pub(crate) credentials: Vec<SyncCredential>,
}
#[derive(Debug, Deserialize)]
#[serde(tag = "type")]
enum SyncCredential {
#[serde(rename = "login")]
Login {
#[serde(rename = "cipherId")]
cipher_id: String,
password: String,
uri: String,
username: String,
},
#[serde(rename = "fido2")]
Fido2 {
#[serde(rename = "cipherId")]
cipher_id: String,
#[serde(rename = "rpId")]
rp_id: String,
/// Base64-encoded
#[serde(rename = "credentialId")]
credential_id: String,
#[serde(rename = "userName")]
user_name: String,
/// Base64-encoded
#[serde(rename = "userHandle")]
user_handle: String,
},
}
#[derive(Serialize)]
struct StatusResponse {
support: StatusSupport,
state: StatusState,
}
#[derive(Serialize)]
struct StatusSupport {
fido2: bool,
password: bool,
#[serde(rename = "incrementalUpdates")]
incremental_updates: bool,
}
#[derive(Serialize)]
struct StatusState {
enabled: bool,
}
#[derive(Serialize)]
struct SyncResponse {
added: u32,
}
#[derive(Serialize)]
#[serde(tag = "type")]
enum CommandResponse {
#[serde(rename = "success")]
Success { value: Value },
#[serde(rename = "error")]
Error { error: String },
}
impl From<anyhow::Result<Value>> for CommandResponse {
fn from(value: anyhow::Result<Value>) -> Self {
match value {
Ok(response) => Self::Success { value: response },
Err(err) => Self::Error {
error: err.to_string(),
},
}
}
}
impl TryFrom<StatusResponse> for CommandResponse {
type Error = anyhow::Error;
fn try_from(response: StatusResponse) -> Result<Self, anyhow::Error> {
Ok(Self::Success {
value: serde_json::to_value(response)?,
})
}
}
impl TryFrom<SyncResponse> for CommandResponse {
type Error = anyhow::Error;
fn try_from(response: SyncResponse) -> Result<Self, anyhow::Error> {
Ok(Self::Success {
value: serde_json::to_value(response)?,
})
}
}

View File

@@ -1,6 +1,249 @@
use anyhow::Result;
use anyhow::{anyhow, Result};
use base64::engine::{general_purpose::URL_SAFE_NO_PAD, Engine};
use windows_plugin_authenticator::{self, SyncedCredential};
use crate::autofill::{
CommandResponse, RunCommand, RunCommandRequest, StatusResponse, StatusState, StatusSupport,
SyncCredential, SyncParameters, SyncResponse,
};
const PLUGIN_CLSID: &str = "0f7dc5d9-69ce-4652-8572-6877fd695062";
#[allow(clippy::unused_async)]
pub async fn run_command(_value: String) -> Result<String> {
todo!("Windows does not support autofill");
pub async fn run_command(value: String) -> Result<String> {
// this.logService.info("Passkey request received:", { error, event });
let request: RunCommandRequest = serde_json::from_str(&value)
.map_err(|e| anyhow!("Failed to deserialize passkey request: {e}"))?;
if request.namespace != "autofill" {
return Err(anyhow!("Unknown namespace: {}", request.namespace));
}
let response: CommandResponse = match request.command {
RunCommand::Status => handle_status_request()?.try_into()?,
RunCommand::Sync => {
let params: SyncParameters = serde_json::from_value(request.params)
.map_err(|e| anyhow!("Could not parse sync parameters: {e}"))?;
handle_sync_request(params)?.try_into()?
}
};
serde_json::to_string(&response).map_err(|e| anyhow!("Failed to serialize response: {e}"))
/*
try {
const request = JSON.parse(event.requestJson);
this.logService.info("Parsed passkey request:", { type: event.requestType, request });
// Handle different request types based on the requestType field
switch (event.requestType) {
case "assertion":
return await this.handleAssertionRequest(request);
case "registration":
return await this.handleRegistrationRequest(request);
case "sync":
return await this.handleSyncRequest(request);
default:
this.logService.error("Unknown passkey request type:", event.requestType);
return JSON.stringify({
type: "error",
message: `Unknown request type: ${event.requestType}`,
});
}
} catch (parseError) {
this.logService.error("Failed to parse passkey request:", parseError);
return JSON.stringify({
type: "error",
message: "Failed to parse request JSON",
});
}
*/
}
fn handle_sync_request(params: SyncParameters) -> Result<SyncResponse> {
let credentials: Vec<SyncedCredential> = params
.credentials
.into_iter()
.filter_map(|c| c.try_into().ok())
.collect();
let num_creds = credentials.len().try_into().unwrap_or(u32::MAX);
windows_plugin_authenticator::sync_credentials_to_windows(credentials, PLUGIN_CLSID)
.map_err(|e| anyhow!("Failed to sync credentials to Windows: {e}"))?;
Ok(SyncResponse { added: num_creds })
/*
let mut log_file = std::fs::File::options()
.append(true)
.open("C:\\temp\\bitwarden_windows_core.log")
.unwrap();
log_file.write_all(b"Made it to sync!");
*/
}
fn handle_status_request() -> Result<StatusResponse> {
Ok(StatusResponse {
support: StatusSupport {
fido2: true,
password: false,
incremental_updates: false,
},
state: StatusState { enabled: true },
})
}
/*
async fn handleAssertionRequest(request: autofill.PasskeyAssertionRequest): Promise<string> {
this.logService.info("Handling assertion request for rpId:", request.rpId);
try {
// Generate unique identifiers for tracking this request
const clientId = Date.now();
const sequenceNumber = Math.floor(Math.random() * 1000000);
// Send request and wait for response
const response = await this.sendAndOptionallyWait<autofill.PasskeyAssertionResponse>(
"autofill.passkeyAssertion",
{
clientId,
sequenceNumber,
request: request,
},
{ waitForResponse: true, timeout: 60000 },
);
if (response) {
// Convert the response to the format expected by the NAPI bridge
return JSON.stringify({
type: "assertion_response",
...response,
});
} else {
return JSON.stringify({
type: "error",
message: "No response received from renderer",
});
}
} catch (error) {
this.logService.error("Error in assertion request:", error);
return JSON.stringify({
type: "error",
message: `Assertion request failed: ${error.message}`,
});
}
}
private async handleRegistrationRequest(
request: autofill.PasskeyRegistrationRequest,
): Promise<string> {
this.logService.info("Handling registration request for rpId:", request.rpId);
try {
// Generate unique identifiers for tracking this request
const clientId = Date.now();
const sequenceNumber = Math.floor(Math.random() * 1000000);
// Send request and wait for response
const response = await this.sendAndOptionallyWait<autofill.PasskeyRegistrationResponse>(
"autofill.passkeyRegistration",
{
clientId,
sequenceNumber,
request: request,
},
{ waitForResponse: true, timeout: 60000 },
);
this.logService.info("Received response for registration request:", response);
if (response) {
// Convert the response to the format expected by the NAPI bridge
return JSON.stringify({
type: "registration_response",
...response,
});
} else {
return JSON.stringify({
type: "error",
message: "No response received from renderer",
});
}
} catch (error) {
this.logService.error("Error in registration request:", error);
return JSON.stringify({
type: "error",
message: `Registration request failed: ${error.message}`,
});
}
}
private async handleSyncRequest(
request: passkey_authenticator.PasskeySyncRequest,
): Promise<string> {
this.logService.info("Handling sync request for rpId:", request.rpId);
try {
// Generate unique identifiers for tracking this request
const clientId = Date.now();
const sequenceNumber = Math.floor(Math.random() * 1000000);
// Send sync request and wait for response
const response = await this.sendAndOptionallyWait<passkey_authenticator.PasskeySyncResponse>(
"autofill.passkeySync",
{
clientId,
sequenceNumber,
request: { rpId: request.rpId },
},
{ waitForResponse: true, timeout: 60000 },
);
this.logService.info("Received response for sync request:", response);
if (response && response.credentials) {
// Convert the response to the format expected by the NAPI bridge
return JSON.stringify({
type: "sync_response",
credentials: response.credentials,
});
} else {
return JSON.stringify({
type: "error",
message: "No credentials received from renderer",
});
}
} catch (error) {
this.logService.error("Error in sync request:", error);
return JSON.stringify({
type: "error",
message: `Sync request failed: ${error.message}`,
});
}
}
*/
impl TryFrom<SyncCredential> for SyncedCredential {
type Error = anyhow::Error;
fn try_from(value: SyncCredential) -> Result<Self, anyhow::Error> {
if let SyncCredential::Fido2 {
rp_id,
credential_id,
user_name,
user_handle,
..
} = value
{
Ok(Self {
credential_id: URL_SAFE_NO_PAD
.decode(credential_id)
.map_err(|e| anyhow!("Could not decode credential ID: {e}"))?,
rp_id: rp_id,
user_name: user_name,
user_handle: URL_SAFE_NO_PAD
.decode(&user_handle)
.map_err(|e| anyhow!("Could not decode user handle: {e}"))?,
})
} else {
Err(anyhow!("Only FIDO2 credentials are supported."))
}
}
}

View File

@@ -130,8 +130,8 @@ export class DesktopAutofillService implements OnDestroy {
return;
}
let fido2Credentials: NativeAutofillFido2Credential[];
let passwordCredentials: NativeAutofillPasswordCredential[];
let fido2Credentials: NativeAutofillFido2Credential[] = [];
let passwordCredentials: NativeAutofillPasswordCredential[] = [];
if (status.value.support.password) {
passwordCredentials = cipherViews