From 8e455007c028db3f98cd2affed6a7fd63e5f6331 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Mon, 24 Mar 2025 12:50:11 +0100 Subject: [PATCH] PM-19095: Wire passkey autofill to UI (#13051) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Passkey stuff Co-authored-by: Anders Ã…berg * Ugly hacks * Work On Modal State Management * Applying modalStyles * modal * Improved hide/show * fixed promise * File name * fix prettier * Protecting against null API's and undefined data * Only show fake popup to devs * cleanup mock code * rename minmimal-app to modal-app * Added comment * Added comment * removed old comment * Avoided changing minimum size * Add small comment * Rename component * adress feedback * Fixed uppercase file * Fixed build * Added codeowners * added void * commentary * feat: reset setting on app start * Moved reset to be in main / process launch * Add comment to create window * Added a little bit of styling * Use Messaging service to loadUrl * Enable passkeysautofill * Add logging * halfbaked * Integration working * And now it works without extra delay * Clean up * add note about messaging * lb * removed console.logs * Cleanup and adress review feedback * This hides the swift UI * pick credential, draft * Remove logger * a whole lot of wiring * not working * Improved wiring * Cancel after 90s * Introduced observable * Launching bitwarden if its not running * Passing position from native to electron * Rename inModalMode to modalMode * remove tap * revert spaces * added back isDev * cleaned up a bit * Cleanup swift file * tweaked logging * clean up * Update apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift Co-authored-by: Andreas Coroiu * Update apps/desktop/src/platform/main/autofill/native-autofill.main.ts Co-authored-by: Andreas Coroiu * Update apps/desktop/src/platform/services/desktop-settings.service.ts Co-authored-by: Andreas Coroiu * adress position feedback * Update apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift Co-authored-by: Andreas Coroiu * Removed extra logging * Adjusted error logging * Use .error to log errors * remove dead code * Update desktop-autofill.service.ts * use parseCredentialId instead of guidToRawFormat * Update apps/desktop/src/autofill/services/desktop-autofill.service.ts Co-authored-by: Andreas Coroiu * Change windowXy to a Record instead of [number,number] * Update apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts Co-authored-by: Andreas Coroiu * Remove unsued dep and comment * changed timeout to be spec recommended maxium, 10 minutes, for now. * Correctly assume UP * Removed extra cancelRequest in deinint * Add timeout and UV to confirmChoseCipher UV is performed by UI, not the service * Improved docs regarding undefined cipherId * cleanup: UP is no longer undefined * Run completeError if ipc messages conversion failed * don't throw, instead return undefined * Disabled passkey provider * Throw error if no activeUserId was found * removed comment * Fixed lint * removed unsued service * reset entitlement formatting * Update entitlements.mas.plist --------- Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> Co-authored-by: Colton Hurst Co-authored-by: Andreas Coroiu Co-authored-by: Andreas Coroiu --- .../macos_provider/src/assertion.rs | 14 +- .../desktop_native/macos_provider/src/lib.rs | 20 +- .../macos_provider/src/registration.rs | 3 +- apps/desktop/desktop_native/napi/index.d.ts | 15 +- apps/desktop/desktop_native/napi/src/lib.rs | 48 ++++ .../CredentialProviderViewController.xib | 23 +- .../CredentialProviderViewController.swift | 261 ++++++++++++------ apps/desktop/src/app/app-routing.module.ts | 4 + .../components/fido2placeholder.component.ts | 90 +++++- .../src/app/services/services.module.ts | 17 +- apps/desktop/src/autofill/preload.ts | 38 +++ .../services/desktop-autofill.service.ts | 153 ++++++---- .../desktop-fido2-user-interface.service.ts | 191 ++++++++++++- apps/desktop/src/main.ts | 12 +- apps/desktop/src/main/messaging.main.ts | 4 + apps/desktop/src/main/tray.main.ts | 36 +-- apps/desktop/src/main/window.main.ts | 42 ++- .../main/autofill/native-autofill.main.ts | 17 +- .../platform/models/domain/window-state.ts | 5 + .../src/platform/popup-modal-styles.ts | 20 +- .../services/desktop-settings.service.ts | 19 +- ...ido2-user-interface.service.abstraction.ts | 2 +- 22 files changed, 826 insertions(+), 208 deletions(-) diff --git a/apps/desktop/desktop_native/macos_provider/src/assertion.rs b/apps/desktop/desktop_native/macos_provider/src/assertion.rs index 762ceaaed48..c5b43bb87fa 100644 --- a/apps/desktop/desktop_native/macos_provider/src/assertion.rs +++ b/apps/desktop/desktop_native/macos_provider/src/assertion.rs @@ -2,11 +2,22 @@ use std::sync::Arc; use serde::{Deserialize, Serialize}; -use crate::{BitwardenError, Callback, UserVerification}; +use crate::{BitwardenError, Callback, Position, UserVerification}; #[derive(uniffi::Record, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PasskeyAssertionRequest { + rp_id: String, + client_data_hash: Vec, + user_verification: UserVerification, + allowed_credentials: Vec>, + window_xy: Position, + //extension_input: Vec, TODO: Implement support for extensions +} + +#[derive(uniffi::Record, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PasskeyAssertionWithoutUserInterfaceRequest { rp_id: String, credential_id: Vec, user_name: String, @@ -14,6 +25,7 @@ pub struct PasskeyAssertionRequest { record_identifier: Option, client_data_hash: Vec, user_verification: UserVerification, + window_xy: Position, } #[derive(uniffi::Record, Serialize, Deserialize)] diff --git a/apps/desktop/desktop_native/macos_provider/src/lib.rs b/apps/desktop/desktop_native/macos_provider/src/lib.rs index 5623436d874..8f2499ae68d 100644 --- a/apps/desktop/desktop_native/macos_provider/src/lib.rs +++ b/apps/desktop/desktop_native/macos_provider/src/lib.rs @@ -15,7 +15,10 @@ uniffi::setup_scaffolding!(); mod assertion; mod registration; -use assertion::{PasskeyAssertionRequest, PreparePasskeyAssertionCallback}; +use assertion::{ + PasskeyAssertionRequest, PasskeyAssertionWithoutUserInterfaceRequest, + PreparePasskeyAssertionCallback, +}; use registration::{PasskeyRegistrationRequest, PreparePasskeyRegistrationCallback}; #[derive(uniffi::Enum, Debug, Serialize, Deserialize)] @@ -26,6 +29,13 @@ pub enum UserVerification { Discouraged, } +#[derive(uniffi::Record, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Position { + pub x: i32, + pub y: i32, +} + #[derive(Debug, uniffi::Error, Serialize, Deserialize)] pub enum BitwardenError { Internal(String), @@ -141,6 +151,14 @@ impl MacOSProviderClient { ) { self.send_message(request, Box::new(callback)); } + + pub fn prepare_passkey_assertion_without_user_interface( + &self, + request: PasskeyAssertionWithoutUserInterfaceRequest, + callback: Arc, + ) { + self.send_message(request, Box::new(callback)); + } } #[derive(Serialize, Deserialize)] diff --git a/apps/desktop/desktop_native/macos_provider/src/registration.rs b/apps/desktop/desktop_native/macos_provider/src/registration.rs index d484af58b6c..9e697b75c16 100644 --- a/apps/desktop/desktop_native/macos_provider/src/registration.rs +++ b/apps/desktop/desktop_native/macos_provider/src/registration.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use serde::{Deserialize, Serialize}; -use crate::{BitwardenError, Callback, UserVerification}; +use crate::{BitwardenError, Callback, Position, UserVerification}; #[derive(uniffi::Record, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -13,6 +13,7 @@ pub struct PasskeyRegistrationRequest { client_data_hash: Vec, user_verification: UserVerification, supported_algorithms: Vec, + window_xy: Position, } #[derive(uniffi::Record, Serialize, Deserialize)] diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index 92f31cf5f89..ca1fe29e254 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -118,6 +118,10 @@ export declare namespace autofill { Required = 'required', Discouraged = 'discouraged' } + export interface Position { + x: number + y: number + } export interface PasskeyRegistrationRequest { rpId: string userName: string @@ -125,6 +129,7 @@ export declare namespace autofill { clientDataHash: Array userVerification: UserVerification supportedAlgorithms: Array + windowXy: Position } export interface PasskeyRegistrationResponse { rpId: string @@ -133,6 +138,13 @@ export declare namespace autofill { attestationObject: Array } export interface PasskeyAssertionRequest { + rpId: string + clientDataHash: Array + userVerification: UserVerification + allowedCredentials: Array> + windowXy: Position + } + export interface PasskeyAssertionWithoutUserInterfaceRequest { rpId: string credentialId: Array userName: string @@ -140,6 +152,7 @@ export declare namespace autofill { recordIdentifier?: string clientDataHash: Array userVerification: UserVerification + windowXy: Position } export interface PasskeyAssertionResponse { rpId: string @@ -156,7 +169,7 @@ export declare namespace autofill { * @param name The endpoint name to listen on. This name uniquely identifies the IPC connection and must be the same for both the server and client. * @param callback This function will be called whenever a message is received from a client. */ - static listen(name: string, registrationCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyRegistrationRequest) => void, assertionCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionRequest) => void): Promise + static listen(name: string, registrationCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyRegistrationRequest) => void, assertionCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionRequest) => void, assertionWithoutUserInterfaceCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionWithoutUserInterfaceRequest) => void): Promise /** Return the path to the IPC server. */ getPath(): string /** Stop the IPC server. */ diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index d0c859d427c..f02be2b27b6 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -515,6 +515,14 @@ pub mod autofill { pub value: Result, } + #[napi(object)] + #[derive(Debug, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct Position { + pub x: i32, + pub y: i32, + } + #[napi(object)] #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -525,6 +533,7 @@ pub mod autofill { pub client_data_hash: Vec, pub user_verification: UserVerification, pub supported_algorithms: Vec, + pub window_xy: Position, } #[napi(object)] @@ -541,6 +550,18 @@ pub mod autofill { #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PasskeyAssertionRequest { + pub rp_id: String, + pub client_data_hash: Vec, + pub user_verification: UserVerification, + pub allowed_credentials: Vec>, + pub window_xy: Position, + //extension_input: Vec, TODO: Implement support for extensions + } + + #[napi(object)] + #[derive(Debug, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct PasskeyAssertionWithoutUserInterfaceRequest { pub rp_id: String, pub credential_id: Vec, pub user_name: String, @@ -548,6 +569,7 @@ pub mod autofill { pub record_identifier: Option, pub client_data_hash: Vec, pub user_verification: UserVerification, + pub window_xy: Position, } #[napi(object)] @@ -592,6 +614,13 @@ pub mod autofill { (u32, u32, PasskeyAssertionRequest), ErrorStrategy::CalleeHandled, >, + #[napi( + ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionWithoutUserInterfaceRequest) => void" + )] + assertion_without_user_interface_callback: ThreadsafeFunction< + (u32, u32, PasskeyAssertionWithoutUserInterfaceRequest), + ErrorStrategy::CalleeHandled, + >, ) -> napi::Result { let (send, mut recv) = tokio::sync::mpsc::channel::(32); tokio::spawn(async move { @@ -628,6 +657,25 @@ pub mod autofill { } } + match serde_json::from_str::< + PasskeyMessage, + >(&message) + { + Ok(msg) => { + let value = msg + .value + .map(|value| (client_id, msg.sequence_number, value)) + .map_err(|e| napi::Error::from_reason(format!("{e:?}"))); + + assertion_without_user_interface_callback + .call(value, ThreadsafeFunctionCallMode::NonBlocking); + continue; + } + Err(e) => { + println!("[ERROR] Error deserializing message1: {e}"); + } + } + match serde_json::from_str::>( &message, ) { diff --git a/apps/desktop/macos/autofill-extension/Base.lproj/CredentialProviderViewController.xib b/apps/desktop/macos/autofill-extension/Base.lproj/CredentialProviderViewController.xib index ace3497a58b..1e47cc54de2 100644 --- a/apps/desktop/macos/autofill-extension/Base.lproj/CredentialProviderViewController.xib +++ b/apps/desktop/macos/autofill-extension/Base.lproj/CredentialProviderViewController.xib @@ -1,22 +1,23 @@ - + - + + - + - +