diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 4182de59382..5ceaf08016b 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -1024,6 +1024,7 @@ dependencies = [ "widestring", "windows 0.62.2", "windows-future 0.3.2", + "windows_plugin_authenticator", "zbus", "zbus_polkit", "zeroizing-alloc", diff --git a/apps/desktop/desktop_native/core/Cargo.toml b/apps/desktop/desktop_native/core/Cargo.toml index f6c9d669df6..d13244db6a2 100644 --- a/apps/desktop/desktop_native/core/Cargo.toml +++ b/apps/desktop/desktop_native/core/Cargo.toml @@ -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 } diff --git a/apps/desktop/desktop_native/core/src/autofill/mod.rs b/apps/desktop/desktop_native/core/src/autofill/mod.rs index aacec852e90..c6a9cdc6625 100644 --- a/apps/desktop/desktop_native/core/src/autofill/mod.rs +++ b/apps/desktop/desktop_native/core/src/autofill/mod.rs @@ -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, +} + +#[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> for CommandResponse { + fn from(value: anyhow::Result) -> Self { + match value { + Ok(response) => Self::Success { value: response }, + Err(err) => Self::Error { + error: err.to_string(), + }, + } + } +} + +impl TryFrom for CommandResponse { + type Error = anyhow::Error; + + fn try_from(response: StatusResponse) -> Result { + Ok(Self::Success { + value: serde_json::to_value(response)?, + }) + } +} + +impl TryFrom for CommandResponse { + type Error = anyhow::Error; + + fn try_from(response: SyncResponse) -> Result { + Ok(Self::Success { + value: serde_json::to_value(response)?, + }) + } +} diff --git a/apps/desktop/desktop_native/core/src/autofill/windows.rs b/apps/desktop/desktop_native/core/src/autofill/windows.rs index 09dc6867931..fffafd06d10 100644 --- a/apps/desktop/desktop_native/core/src/autofill/windows.rs +++ b/apps/desktop/desktop_native/core/src/autofill/windows.rs @@ -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 { - todo!("Windows does not support autofill"); +pub async fn run_command(value: String) -> Result { + // 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 { + let credentials: Vec = 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 { + Ok(StatusResponse { + support: StatusSupport { + fido2: true, + password: false, + incremental_updates: false, + }, + state: StatusState { enabled: true }, + }) +} + +/* +async fn handleAssertionRequest(request: autofill.PasskeyAssertionRequest): Promise { + 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.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 { + 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.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 { + 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( + "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 for SyncedCredential { + type Error = anyhow::Error; + + fn try_from(value: SyncCredential) -> Result { + 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.")) + } + } } diff --git a/apps/desktop/src/autofill/services/desktop-autofill.service.ts b/apps/desktop/src/autofill/services/desktop-autofill.service.ts index dae4e7f8d41..ecf4e528b43 100644 --- a/apps/desktop/src/autofill/services/desktop-autofill.service.ts +++ b/apps/desktop/src/autofill/services/desktop-autofill.service.ts @@ -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