From 411b2bcf880d38af336ca965f8a9ba57b6970a90 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Tue, 2 Dec 2025 11:30:58 -0600 Subject: [PATCH] Add native transfer_focus() method --- apps/desktop/desktop_native/Cargo.lock | 1 + apps/desktop/desktop_native/napi/Cargo.toml | 1 + apps/desktop/desktop_native/napi/index.d.ts | 1 + apps/desktop/desktop_native/napi/src/lib.rs | 8 ++++++ .../passkey_authenticator_internal/dummy.rs | 4 +++ .../passkey_authenticator_internal/windows.rs | 26 +++++++++++++++++++ apps/desktop/src/autofill/preload.ts | 2 ++ .../desktop-fido2-user-interface.service.ts | 14 ++++------ .../main/autofill/native-autofill.main.ts | 17 ++++++++++++ 9 files changed, 65 insertions(+), 9 deletions(-) diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 249bbfb56a4..973f5748776 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -938,6 +938,7 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber", + "windows 0.61.3", "windows-registry", "windows_plugin_authenticator", ] diff --git a/apps/desktop/desktop_native/napi/Cargo.toml b/apps/desktop/desktop_native/napi/Cargo.toml index b5847a602d5..b02a1a717ad 100644 --- a/apps/desktop/desktop_native/napi/Cargo.toml +++ b/apps/desktop/desktop_native/napi/Cargo.toml @@ -27,6 +27,7 @@ tracing = { workspace = true } tracing-subscriber = { workspace = true } [target.'cfg(windows)'.dependencies] +windows = { workspace = true } windows-registry = { workspace = true } windows_plugin_authenticator = { path = "../windows_plugin_authenticator" } diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index cde03c95005..07e41978652 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -147,6 +147,7 @@ export declare namespace autostart { } export declare namespace autofill { export function runCommand(value: string): Promise + export function transferFocus(handle: Array): Promise export const enum UserVerification { Preferred = 'preferred', Required = 'required', diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index fea199a7806..9e22101b60e 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -636,6 +636,8 @@ pub mod autofill { use serde::{de::DeserializeOwned, Deserialize, Serialize}; use tracing::error; + use crate::passkey_authenticator_internal; + #[napi] pub async fn run_command(value: String) -> napi::Result { desktop_core::autofill::run_command(value) @@ -643,6 +645,12 @@ pub mod autofill { .map_err(|e| napi::Error::from_reason(e.to_string())) } + #[napi] + pub async fn transfer_focus(handle: Vec) -> napi::Result<()> { + passkey_authenticator_internal::transfer_focus(handle) + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + #[derive(Debug, serde::Serialize, serde:: Deserialize)] pub enum BitwardenError { Internal(String), diff --git a/apps/desktop/desktop_native/napi/src/passkey_authenticator_internal/dummy.rs b/apps/desktop/desktop_native/napi/src/passkey_authenticator_internal/dummy.rs index bcd929c16b4..1f19e3132f1 100644 --- a/apps/desktop/desktop_native/napi/src/passkey_authenticator_internal/dummy.rs +++ b/apps/desktop/desktop_native/napi/src/passkey_authenticator_internal/dummy.rs @@ -3,3 +3,7 @@ use anyhow::{bail, Result}; pub fn register() -> Result<()> { bail!("Not implemented") } + +pub fn transfer_focus(handle: Vec) -> Result<()> { + bail!("Not implemented") +} diff --git a/apps/desktop/desktop_native/napi/src/passkey_authenticator_internal/windows.rs b/apps/desktop/desktop_native/napi/src/passkey_authenticator_internal/windows.rs index 4ff51f5bce4..5a41a49688e 100644 --- a/apps/desktop/desktop_native/napi/src/passkey_authenticator_internal/windows.rs +++ b/apps/desktop/desktop_native/napi/src/passkey_authenticator_internal/windows.rs @@ -1,7 +1,33 @@ use anyhow::{anyhow, Result}; +use windows::Win32::{ + Foundation::HWND, + UI::{Input::KeyboardAndMouse::SetFocus, WindowsAndMessaging::BringWindowToTop}, +}; pub fn register() -> Result<()> { windows_plugin_authenticator::register().map_err(|e| anyhow!(e))?; Ok(()) } + +pub fn transfer_focus(handle: Vec) -> Result<()> { + unsafe { + // SAFETY: We check to make sure that the vec is the expected size + // before converting it. If the handle is invalid when passed to + // Windows, the request will be rejected. + if handle.len() != size_of::() { + return Err(anyhow!("Invalid window handle received: {:?}", handle)); + } + + let hwnd = *handle.as_ptr().cast(); + + tracing::debug!("Transferring focus to {hwnd:?}"); + let result = SetFocus(Some(hwnd)); + tracing::debug!("SetFocus? {result:?}"); + + let result = BringWindowToTop(hwnd); + tracing::debug!("BringWindowToTop? {result:?}"); + } + + Ok(()) +} diff --git a/apps/desktop/src/autofill/preload.ts b/apps/desktop/src/autofill/preload.ts index 8fc62d229da..0ceae80dfe3 100644 --- a/apps/desktop/src/autofill/preload.ts +++ b/apps/desktop/src/autofill/preload.ts @@ -12,6 +12,8 @@ export default { runCommand: (params: RunCommandParams): Promise> => ipcRenderer.invoke("autofill.runCommand", params), + transferFocus: (handle: Uint8Array) => ipcRenderer.invoke("autofill.transferFocus", handle), + listenerReady: () => ipcRenderer.send("autofill.listenerReady"), listenPasskeyRegistration: ( 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 253b2ea498d..9b02d848b79 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 @@ -383,9 +383,11 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi // TODO: modalState is just a proxy for what we actually want: whether the window is visible. // We should add a way for services to query window visibility. const modalState = await firstValueFrom(this.desktopSettingsService.modalMode$); - const windowHandle = modalState.isModalModeActive ? await ipc.platform.getNativeWindowHandle() : this.windowObject.handle; - - const uvRequest = ipc.autofill.runCommand({ + await ipc.autofill.transferFocus(windowHandle); + // Ensure our window is hidden when showing the OS user verification dialog. + this.logService.debug("Hiding UI"); + this.hideUi(); + const uvResult = await ipc.autofill.runCommand({ namespace: "autofill", command: "user-verification", params: { @@ -395,12 +397,6 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi displayHint, }, }); - // Ensure our window is hidden when showing the OS user verification dialog. - // TODO: This is prone to data races and, on Windows, may cause the Windows - // Hello dialog not to have keyboard input focus. We need a better solution - // than this. - this.hideUi(); - const uvResult = await uvRequest; if (uvResult.type === "error") { this.logService.error("Error getting user verification", uvResult.error); return false; diff --git a/apps/desktop/src/platform/main/autofill/native-autofill.main.ts b/apps/desktop/src/platform/main/autofill/native-autofill.main.ts index e82ee9a4bfb..bf2772058ec 100644 --- a/apps/desktop/src/platform/main/autofill/native-autofill.main.ts +++ b/apps/desktop/src/platform/main/autofill/native-autofill.main.ts @@ -84,6 +84,17 @@ export class NativeAutofillMain { }, ); + ipcMain.handle( + "autofill.transferFocus", + ( + _event: any, + handle: Uint8Array, + ): Promise => { + return this.transferFocus(handle); + }, + ); + + this.ipcServer = await autofill.IpcServer.listen( "af", // RegistrationCallback @@ -228,4 +239,10 @@ export class NativeAutofillMain { return { type: "error", error: String(e) } as RunCommandResult; } } + + private transferFocus(handle: Uint8Array): Promise { + const h = Array.from(handle); + this.logService.debug("Transferring focus to", h); + return autofill.transferFocus(h); + } } \ No newline at end of file