From 93833f743d3f6551beb088616f81bf668c10efc7 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Tue, 25 Nov 2025 09:55:15 -0600 Subject: [PATCH] Use WebAuthn client window for silent assertions --- apps/desktop/desktop_native/napi/index.d.ts | 3 +++ apps/desktop/desktop_native/napi/src/lib.rs | 3 +++ .../windows_plugin_authenticator/src/assert.rs | 3 +++ .../src/ipc2/assertion.rs | 2 ++ .../src/ipc2/registration.rs | 1 + .../src/make_credential.rs | 2 ++ .../modal/credentials/fido2-create.component.ts | 2 +- .../autofill/services/desktop-autofill.service.ts | 9 ++++++--- .../services/desktop-fido2-user-interface.service.ts | 12 ++++++++---- 9 files changed, 29 insertions(+), 8 deletions(-) diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index f58573d8dde..25cc8553663 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -165,6 +165,7 @@ export declare namespace autofill { supportedAlgorithms: Array windowXy: Position excludedCredentials: Array> + clientWindowHandle?: Array context?: string } export interface PasskeyRegistrationResponse { @@ -179,6 +180,7 @@ export declare namespace autofill { userVerification: UserVerification allowedCredentials: Array> windowXy: Position + clientWindowHandle?: Array context?: string } export interface PasskeyAssertionWithoutUserInterfaceRequest { @@ -190,6 +192,7 @@ export declare namespace autofill { clientDataHash: Array userVerification: UserVerification windowXy: Position + clientWindowHandle?: Array context?: string } export interface NativeStatus { diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index ee2b2aaebf9..12476820b3b 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -695,6 +695,7 @@ pub mod autofill { pub supported_algorithms: Vec, pub window_xy: Position, pub excluded_credentials: Vec>, + pub client_window_handle: Option>, pub context: Option, } @@ -717,6 +718,7 @@ pub mod autofill { pub user_verification: UserVerification, pub allowed_credentials: Vec>, pub window_xy: Position, + pub client_window_handle: Option>, pub context: Option, //extension_input: Vec, TODO: Implement support for extensions } @@ -733,6 +735,7 @@ pub mod autofill { pub client_data_hash: Vec, pub user_verification: UserVerification, pub window_xy: Position, + pub client_window_handle: Option>, pub context: Option, } diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/assert.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/assert.rs index 63be191155a..32dcf625aca 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/assert.rs +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/assert.rs @@ -44,6 +44,7 @@ pub fn get_assertion( .map(|id| id.to_vec()) .collect(); + let client_window_handle = request.window_handle.0.addr().to_le_bytes().to_vec(); let client_pos = request .window_handle .center_position() @@ -62,6 +63,7 @@ pub fn get_assertion( client_data_hash, allowed_credentials: allowed_credential_ids, user_verification, + client_window_handle, window_xy: Position { x: client_pos.0, y: client_pos.1, @@ -109,6 +111,7 @@ fn send_assertion_request( credential_id: request.allowed_credentials[0].clone(), client_data_hash: request.client_data_hash, user_verification: request.user_verification, + client_window_handle: request.client_window_handle, window_xy: request.window_xy, context: request.context, }; diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/ipc2/assertion.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/ipc2/assertion.rs index 3820d3c0b9c..85dded177aa 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/ipc2/assertion.rs +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/ipc2/assertion.rs @@ -12,6 +12,7 @@ pub struct PasskeyAssertionRequest { pub user_verification: UserVerification, pub allowed_credentials: Vec>, pub window_xy: Position, + pub client_window_handle: Vec, pub context: String, // pub extension_input: Vec, TODO: Implement support for extensions } @@ -24,6 +25,7 @@ pub struct PasskeyAssertionWithoutUserInterfaceRequest { pub client_data_hash: Vec, pub user_verification: UserVerification, pub window_xy: Position, + pub client_window_handle: Vec, pub context: String, } diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/ipc2/registration.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/ipc2/registration.rs index 1c10ecb95d0..50adccbc8e2 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/ipc2/registration.rs +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/ipc2/registration.rs @@ -14,6 +14,7 @@ pub struct PasskeyRegistrationRequest { pub user_verification: UserVerification, pub supported_algorithms: Vec, pub window_xy: Position, + pub client_window_handle: Vec, pub excluded_credentials: Vec>, pub context: String, } diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/make_credential.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/make_credential.rs index 6abe52f8867..f6dc17c4c5f 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/make_credential.rs +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/make_credential.rs @@ -87,6 +87,7 @@ pub fn make_credential( ); } + let client_window_handle = request.window_handle.0.addr().to_le_bytes().to_vec(); let client_pos = request .window_handle .center_position() @@ -104,6 +105,7 @@ pub fn make_credential( excluded_credentials, user_verification: user_verification, supported_algorithms, + client_window_handle, window_xy: Position { x: client_pos.0, y: client_pos.1, diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts index b2e59ed38d5..42cbbcb8e6d 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts +++ b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts @@ -213,7 +213,7 @@ export class Fido2CreateComponent implements OnInit, OnDestroy { let cred = cipher.login.fido2Credentials[0]; const username = cred.userName ?? cred.userDisplayName - return this.session.promptForUserVerification(username, "Verify it's you to update a new credential") + return this.session.promptForUserVerification(username, "Verify it's you to create a new credential") } private async showErrorDialog(config: SimpleDialogOptions): Promise { diff --git a/apps/desktop/src/autofill/services/desktop-autofill.service.ts b/apps/desktop/src/autofill/services/desktop-autofill.service.ts index 51cae002eda..5d30712f659 100644 --- a/apps/desktop/src/autofill/services/desktop-autofill.service.ts +++ b/apps/desktop/src/autofill/services/desktop-autofill.service.ts @@ -215,10 +215,11 @@ export class DesktopAutofillService implements OnDestroy { this.inFlightRequests[request.context] = controller; } + const clientHandle = request.clientWindowHandle ? new Uint8Array(request.clientWindowHandle) : null; try { const response = await this.fido2AuthenticatorService.makeCredential( this.convertRegistrationRequest(request), - { windowXy: request.windowXy }, + { windowXy: request.windowXy, handle: clientHandle }, controller, request.context, ); @@ -293,9 +294,10 @@ export class DesktopAutofillService implements OnDestroy { ); } + const clientHandle = request.clientWindowHandle ? new Uint8Array(request.clientWindowHandle) : null; const response = await this.fido2AuthenticatorService.getAssertion( this.convertAssertionRequest(request, true), - { windowXy: request.windowXy }, + { windowXy: request.windowXy, handle: clientHandle }, controller, request.context ); @@ -329,10 +331,11 @@ export class DesktopAutofillService implements OnDestroy { this.inFlightRequests[request.context] = controller; } + const clientHandle = request.clientWindowHandle ? new Uint8Array(request.clientWindowHandle) : null; try { const response = await this.fido2AuthenticatorService.getAssertion( this.convertAssertionRequest(request), - { windowXy: request.windowXy }, + { windowXy: request.windowXy, handle: clientHandle }, controller, request.context, ); diff --git a/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts b/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts index 14320b2c51f..656d101dda3 100644 --- a/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts +++ b/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts @@ -43,6 +43,7 @@ export type NativeWindowObject = { * The position of the window, first entry is the x position, second is the y position */ windowXy?: { x: number; y: number }; + handle?: Uint8Array; }; export class DesktopFido2UserInterfaceService @@ -153,7 +154,7 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi const username = cred.userName ?? cred.userDisplayName // TODO: internationalization try { - const isConfirmed = await this.promptForUserVerification(username, "Verify it's you to log in with Bitwarden."); + const isConfirmed = await this.promptForUserVerification(username, "Verify it's you to log in with Bitwarden.", this.windowObject.handle); return { cipherId: cipherIds[0], userVerified: isConfirmed }; } catch (e) { @@ -161,7 +162,7 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi } } else { - this.logService.debug( + this.logService.warning( "shortcut - Assuming user presence and returning cipherId", cipherIds[0], ); @@ -371,9 +372,12 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi } /** Called by the UI to prompt the user for verification. May be fulfilled by the OS. */ - async promptForUserVerification(username: string, displayHint: string): Promise { + async promptForUserVerification(username: string, displayHint: string, clientWindowHandle?: Uint8Array): Promise { this.logService.info("DesktopFido2UserInterfaceSession] Prompting for user verification") - let windowHandle = await ipc.platform.getNativeWindowHandle(); + // Use the client window handle if we're not showing UI; otherwise use our modal window. + // For Windows, if the selected window handle is not in the foreground, then the Windows + // Hello dialog will also be in the background. + let windowHandle = clientWindowHandle ?? await ipc.platform.getNativeWindowHandle(); const uvResult = await ipc.autofill.runCommand({ namespace: "autofill",