From 441f6540fef16019b54759eb223e667da71de871 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Mon, 10 Nov 2025 21:55:28 -0600 Subject: [PATCH] First pass at user verification --- .../desktop_native/core/src/autofill/mod.rs | 48 +++- .../core/src/autofill/windows.rs | 235 +++++------------- apps/desktop/desktop_native/napi/index.d.ts | 2 + apps/desktop/desktop_native/napi/src/lib.rs | 2 + .../src/assert.rs | 5 +- .../src/ipc2/assertion.rs | 2 + .../src/ipc2/mod.rs | 3 +- .../credentials/fido2-vault.component.ts | 4 +- .../services/desktop-autofill.service.ts | 6 +- .../desktop-fido2-user-interface.service.ts | 37 ++- apps/desktop/src/main/window.main.ts | 4 + .../src/platform/main/autofill/command.ts | 3 +- .../autofill/user-verification.command.ts | 19 ++ apps/desktop/src/platform/preload.ts | 1 + ...fido2-authenticator.service.abstraction.ts | 1 + ...ido2-user-interface.service.abstraction.ts | 1 + .../fido2/fido2-authenticator.service.ts | 2 + 17 files changed, 198 insertions(+), 177 deletions(-) create mode 100644 apps/desktop/src/platform/main/autofill/user-verification.command.ts diff --git a/apps/desktop/desktop_native/core/src/autofill/mod.rs b/apps/desktop/desktop_native/core/src/autofill/mod.rs index c6a9cdc6625..2a7a10491ec 100644 --- a/apps/desktop/desktop_native/core/src/autofill/mod.rs +++ b/apps/desktop/desktop_native/core/src/autofill/mod.rs @@ -4,7 +4,7 @@ #[cfg_attr(target_os = "macos", path = "macos.rs")] mod autofill; pub use autofill::*; -use serde::{Deserialize, Serialize}; +use serde::{de::Visitor, Deserialize, Deserializer, Serialize}; use serde_json::Value; #[derive(Deserialize)] @@ -23,6 +23,8 @@ enum RunCommand { Status, #[serde(rename = "sync")] Sync, + #[serde(rename = "user-verification")] + UserVerification, } #[derive(Debug, Deserialize)] @@ -87,6 +89,19 @@ struct SyncResponse { added: u32, } +#[derive(Debug, Deserialize)] +struct UserVerificationParameters { + #[serde(rename = "windowHandle", deserialize_with = "deserialize_b64")] + window_handle: Vec, + #[serde(rename = "transactionContext", deserialize_with = "deserialize_b64")] + pub(crate) transaction_context: Vec, + #[serde(rename = "displayHint")] + pub(crate) display_hint: String, + pub(crate) username: String, +} +#[derive(Serialize)] +struct UserVerificationResponse {} + #[derive(Serialize)] #[serde(tag = "type")] enum CommandResponse { @@ -126,3 +141,34 @@ impl TryFrom for CommandResponse { }) } } + +impl TryFrom for CommandResponse { + type Error = anyhow::Error; + + fn try_from(response: UserVerificationResponse) -> Result { + Ok(Self::Success { + value: serde_json::to_value(response)?, + }) + } +} + +fn deserialize_b64<'de, D: Deserializer<'de>>(deserializer: D) -> Result, D::Error> { + deserializer.deserialize_str(Base64Visitor {}) +} + +struct Base64Visitor; +impl<'de> Visitor<'de> for Base64Visitor { + type Value = Vec; + + fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.write_str("A valid base64 string") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + use base64::{engine::general_purpose::STANDARD, Engine as _}; + STANDARD.decode(v).map_err(|err| E::custom(err)) + } +} diff --git a/apps/desktop/desktop_native/core/src/autofill/windows.rs b/apps/desktop/desktop_native/core/src/autofill/windows.rs index 4beac2c4c52..868c570b125 100644 --- a/apps/desktop/desktop_native/core/src/autofill/windows.rs +++ b/apps/desktop/desktop_native/core/src/autofill/windows.rs @@ -5,7 +5,7 @@ use std::ptr::NonNull; use anyhow::{anyhow, Result}; use base64::engine::{general_purpose::URL_SAFE_NO_PAD, Engine}; use windows::core::s; -use windows::Win32::Foundation::FreeLibrary; +use windows::Win32::Foundation::{FreeLibrary, HWND}; use windows::{ core::{GUID, HRESULT, PCSTR}, Win32::System::{Com::CoTaskMemAlloc, LibraryLoader::*}, @@ -13,15 +13,15 @@ use windows::{ use crate::autofill::{ CommandResponse, RunCommand, RunCommandRequest, StatusResponse, StatusState, StatusSupport, - SyncCredential, SyncParameters, SyncResponse, + SyncCredential, SyncParameters, SyncResponse, UserVerificationParameters, + UserVerificationResponse, }; const PLUGIN_CLSID: &str = "0f7dc5d9-69ce-4652-8572-6877fd695062"; #[allow(clippy::unused_async)] pub async fn run_command(value: String) -> Result { - // this.logService.info("Passkey request received:", { error, event }); - + tracing::debug!("Received command request: {value}"); let request: RunCommandRequest = serde_json::from_str(&value) .map_err(|e| anyhow!("Failed to deserialize passkey request: {e}"))?; @@ -35,37 +35,13 @@ pub async fn run_command(value: String) -> Result { .map_err(|e| anyhow!("Could not parse sync parameters: {e}"))?; handle_sync_request(params)?.try_into()? } + RunCommand::UserVerification => { + let params: UserVerificationParameters = serde_json::from_value(request.params) + .map_err(|e| anyhow!("Could not parse user verification parameters: {e}"))?; + handle_user_verification_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 { @@ -78,13 +54,6 @@ fn handle_sync_request(params: SyncParameters) -> Result { 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 { @@ -98,136 +67,44 @@ fn handle_status_request() -> Result { }) } -/* -async fn handleAssertionRequest(request: autofill.PasskeyAssertionRequest): Promise { - this.logService.info("Handling assertion request for rpId:", request.rpId); +fn handle_user_verification_request( + request: UserVerificationParameters, +) -> Result { + tracing::debug!(?request, "Handling user verification request"); + unsafe { + let hwnd: HWND = *request.window_handle.as_ptr().cast(); - try { - // Generate unique identifiers for tracking this request - const clientId = Date.now(); - const sequenceNumber = Math.floor(Math.random() * 1000000); + let (buf, _) = request.transaction_context[..16].split_at(16); + let guid_u128 = buf + .try_into() + .map_err(|e| anyhow!("Failed to parse transaction ID as u128: {e}"))?; + let transaction_id = GUID::from_u128(u128::from_le_bytes(guid_u128)); - // 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}`, - }); + let uv_request = WebAuthNPluginUserVerificationRequest { + hwnd, + rguidTransactionId: (&transaction_id) as *const GUID, + pwszUsername: request.username.to_com_utf16().0, + pwszDisplayHint: request.display_hint.to_com_utf16().0, + }; + let uv_fn = delay_load::( + s!("webauthn.dll"), + s!("WebAuthNPluginPerformUserVerification"), + ) + .ok_or(anyhow!( + "Could not load WebAuthNPluginPerformUserVerification" + ))?; + let mut uv_response_len: u32 = 0; + let mut uv_response: *mut u8 = std::ptr::null_mut(); + uv_fn( + std::ptr::from_ref(&uv_request), + &mut uv_response_len as *mut u32, + &mut uv_response as *mut *mut u8, + ) + .ok() + .map_err(|err| anyhow!("User Verification request failed: {err}"))?; } - } - - 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}`, - }); - } - } - -*/ + return Ok(UserVerificationResponse {}); +} impl TryFrom for SyncedCredential { type Error = anyhow::Error; @@ -623,3 +500,27 @@ fn add_credentials( type WebAuthNPluginAuthenticatorRemoveAllCredentialsFnDeclaration = unsafe extern "cdecl" fn(rclsid: *const GUID) -> HRESULT; + +#[repr(C)] +#[derive(Debug)] +struct WebAuthNPluginUserVerificationRequest { + /// Windows handle of the top-level window displayed by the plugin and currently is in foreground as part of the ongoing webauthn operation. + hwnd: HWND, + + /// The webauthn transaction id from the WEBAUTHN_PLUGIN_OPERATION_REQUEST + rguidTransactionId: *const GUID, + + /// The username attached to the credential that is in use for this webauthn operation + pwszUsername: *const u16, + + /// A text hint displayed on the windows hello prompt + pwszDisplayHint: *const u16, +} + +type WebAuthNPluginPerformUserVerification = unsafe extern "cdecl" fn( + pPluginUserVerification: *const WebAuthNPluginUserVerificationRequest, + pcbResponse: *mut u32, + ppbResponse: *mut *mut u8, +) -> HRESULT; + +type WebAuthNPluginFreeUserVerificationResponse = unsafe extern "cdecl" fn(ppbResponse: *mut u8); diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index fada951b7ec..26a1c028261 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -178,6 +178,7 @@ export declare namespace autofill { userVerification: UserVerification allowedCredentials: Array> windowXy: Position + context?: Array } export interface PasskeyAssertionWithoutUserInterfaceRequest { rpId: string @@ -188,6 +189,7 @@ export declare namespace autofill { clientDataHash: Array userVerification: UserVerification windowXy: Position + context?: Array } export interface NativeStatus { key: string diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index 85447dcbdac..35542f385c2 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -716,6 +716,7 @@ pub mod autofill { pub user_verification: UserVerification, pub allowed_credentials: Vec>, pub window_xy: Position, + pub context: Option>, //extension_input: Vec, TODO: Implement support for extensions } @@ -731,6 +732,7 @@ pub mod autofill { pub client_data_hash: Vec, pub user_verification: UserVerification, pub window_xy: Position, + pub context: Option>, } #[napi(object)] 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 142fd129f5d..301b90e7753 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/assert.rs +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/assert.rs @@ -148,6 +148,7 @@ fn send_assertion_request( client_data_hash: request.client_data_hash, user_verification: request.user_verification, window_xy: request.window_xy, + context: request.context, }; ipc_client.prepare_passkey_assertion_without_user_interface(request, callback.clone()); } else { @@ -349,15 +350,17 @@ pub unsafe fn plugin_get_assertion( let allowed_credentials = parse_credential_list(&decoded_request.CredentialList); // Create Windows assertion request + let transaction_id = req.transaction_id.to_u128().to_le_bytes().to_vec(); let assertion_request = PasskeyAssertionRequest { rp_id: rpid.clone(), client_data_hash, allowed_credentials: allowed_credentials.clone(), + user_verification, window_xy: Position { x: coords.0, y: coords.1, }, - user_verification, + context: transaction_id, }; tracing::debug!( 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 367ee51bfc8..660a5971a37 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 context: Vec, // pub extension_input: Vec, TODO: Implement support for extensions } @@ -26,6 +27,7 @@ pub struct PasskeyAssertionWithoutUserInterfaceRequest { pub client_data_hash: Vec, pub user_verification: UserVerification, pub window_xy: Position, + pub context: Vec, } #[derive(Debug, Serialize, Deserialize)] diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/ipc2/mod.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/ipc2/mod.rs index a079067e015..b4ecedab27f 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/ipc2/mod.rs +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/ipc2/mod.rs @@ -18,6 +18,7 @@ mod assertion; mod lock_status; mod registration; +use crate::ipc2::lock_status::{GetLockStatusCallback, LockStatusRequest}; pub use assertion::{ PasskeyAssertionRequest, PasskeyAssertionResponse, PasskeyAssertionWithoutUserInterfaceRequest, PreparePasskeyAssertionCallback, @@ -26,8 +27,6 @@ pub use registration::{ PasskeyRegistrationRequest, PasskeyRegistrationResponse, PreparePasskeyRegistrationCallback, }; -use crate::ipc2::lock_status::{GetLockStatusCallback, LockStatusRequest}; - #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub enum UserVerification { diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts index da1f1de57ba..2589e8cc456 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts +++ b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts @@ -153,8 +153,8 @@ export class Fido2VaultComponent implements OnInit, OnDestroy { private async validateCipherAccess(cipher: CipherView): Promise { if (cipher.reprompt !== CipherRepromptType.None) { return this.passwordRepromptService.showPasswordPrompt(); + } else { + return this.session.promptForUserVerification(cipher) } - - return true; } } diff --git a/apps/desktop/src/autofill/services/desktop-autofill.service.ts b/apps/desktop/src/autofill/services/desktop-autofill.service.ts index 20d85a66f77..0aa8b606a49 100644 --- a/apps/desktop/src/autofill/services/desktop-autofill.service.ts +++ b/apps/desktop/src/autofill/services/desktop-autofill.service.ts @@ -43,6 +43,7 @@ import { NativeAutofillPasswordCredential, NativeAutofillSyncCommand, } from "../../platform/main/autofill/sync.command"; +import { NativeAutofillUserVerificationCommand } from "../../platform/main/autofill/user-verification.command"; import type { NativeWindowObject } from "./desktop-fido2-user-interface.service"; import { DeviceType } from "@bitwarden/common/enums"; @@ -278,11 +279,13 @@ export class DesktopAutofillService implements OnDestroy { new Uint8Array(parseCredentialId(decrypted.login.fido2Credentials?.[0].credentialId)), ); } + const ctx = request.context ? new Uint8Array(request.context).buffer : null; const response = await this.fido2AuthenticatorService.getAssertion( this.convertAssertionRequest(request, true), { windowXy: request.windowXy }, controller, + ctx ); callback(null, this.convertAssertionResponse(request, response)); @@ -304,13 +307,14 @@ export class DesktopAutofillService implements OnDestroy { } this.logService.debug("listenPasskeyAssertion", clientId, sequenceNumber, request); - + const ctx = request.context ? new Uint8Array(request.context).buffer : null; const controller = new AbortController(); try { const response = await this.fido2AuthenticatorService.getAssertion( this.convertAssertionRequest(request), { windowXy: request.windowXy }, controller, + ctx ); callback(null, this.convertAssertionResponse(request, response)); 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 19946ab590c..833498f75fa 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 @@ -32,6 +32,8 @@ import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view"; import { DesktopSettingsService } from "../../platform/services/desktop-settings.service"; +import { NativeAutofillUserVerificationCommand } from "../../platform/main/autofill/user-verification.command"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; /** * This type is used to pass the window position from the native UI @@ -65,8 +67,9 @@ export class DesktopFido2UserInterfaceService fallbackSupported: boolean, nativeWindowObject: NativeWindowObject, abortController?: AbortController, + transactionContext?: ArrayBuffer, ): Promise { - this.logService.debug("newSession", fallbackSupported, abortController, nativeWindowObject); + this.logService.debug("newSession", fallbackSupported, abortController, nativeWindowObject, transactionContext); const session = new DesktopFido2UserInterfaceSession( this.authService, this.cipherService, @@ -75,6 +78,7 @@ export class DesktopFido2UserInterfaceService this.router, this.desktopSettingsService, nativeWindowObject, + transactionContext, ); this.currentSession = session; @@ -91,6 +95,7 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi private router: Router, private desktopSettingsService: DesktopSettingsService, private windowObject: NativeWindowObject, + private transactionContext: ArrayBuffer, ) {} private confirmCredentialSubject = new Subject(); @@ -126,7 +131,7 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi try { // Check if we can return the credential without user interaction await this.accountService.setShowHeader(false); - if (assumeUserPresence && cipherIds.length === 1 && !masterPasswordRepromptRequired) { + if (assumeUserPresence && cipherIds.length === 1 && !masterPasswordRepromptRequired && !userVerification) { this.logService.debug( "shortcut - Assuming user presence and returning cipherId", cipherIds[0], @@ -136,6 +141,10 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi this.logService.debug("Could not shortcut, showing UI"); + // TODO: We need to pass context from the original request whether this + // should be a silent request or not. Then, we can fail here if it's + // supposed to be silent. + // make the cipherIds available to the UI. this.availableCipherIdsSubject.next(cipherIds); @@ -312,6 +321,30 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi } } + /** Called by the UI to prompt the user for verification. May be fulfilled by the OS. */ + async promptForUserVerification(cipher: CipherView): Promise { + this.logService.info("DesktopFido2UserInterfaceSession] Prompting for user verification") + let cred = cipher.login.fido2Credentials[0]; + const username = cred.userName ?? cred.userDisplayName + let windowHandle = await ipc.platform.getNativeWindowHandle(); + + const uvResult = await ipc.autofill.runCommand({ + namespace: "autofill", + command: "user-verification", + params: { + windowHandle: Utils.fromBufferToB64(windowHandle), + transactionContext: Utils.fromBufferToB64(this.transactionContext), + username, + displayHint: `Logging in as ${cipher.name}`, + }, + }); + if (uvResult.type === "error") { + this.logService.error("Error getting user verification", uvResult.error) + return false + } + return uvResult.type === "success"; + } + async updateCredential(cipher: CipherView): Promise { this.logService.info("updateCredential"); await firstValueFrom( diff --git a/apps/desktop/src/main/window.main.ts b/apps/desktop/src/main/window.main.ts index 8de60fce568..ad1e50bf44f 100644 --- a/apps/desktop/src/main/window.main.ts +++ b/apps/desktop/src/main/window.main.ts @@ -403,6 +403,10 @@ export class WindowMain { if (this.createWindowCallback) { this.createWindowCallback(this.win); } + + ipcMain.handle("get-native-window-handle", (_event) => { + return this.win.getNativeWindowHandle().toString("base64"); + }); } // Retrieve the background color diff --git a/apps/desktop/src/platform/main/autofill/command.ts b/apps/desktop/src/platform/main/autofill/command.ts index a8b5548052b..2549e617679 100644 --- a/apps/desktop/src/platform/main/autofill/command.ts +++ b/apps/desktop/src/platform/main/autofill/command.ts @@ -1,5 +1,6 @@ import { NativeAutofillStatusCommand } from "./status.command"; import { NativeAutofillSyncCommand } from "./sync.command"; +import { NativeAutofillUserVerificationCommand } from "./user-verification.command"; export type CommandDefinition = { namespace: string; @@ -20,4 +21,4 @@ export type IpcCommandInvoker = ( ) => Promise>; /** A list of all available commands */ -export type Command = NativeAutofillSyncCommand | NativeAutofillStatusCommand; +export type Command = NativeAutofillSyncCommand | NativeAutofillStatusCommand | NativeAutofillUserVerificationCommand; diff --git a/apps/desktop/src/platform/main/autofill/user-verification.command.ts b/apps/desktop/src/platform/main/autofill/user-verification.command.ts new file mode 100644 index 00000000000..838c4e64d3f --- /dev/null +++ b/apps/desktop/src/platform/main/autofill/user-verification.command.ts @@ -0,0 +1,19 @@ +import { CommandDefinition, CommandOutput } from "./command"; + +export interface NativeAutofillUserVerificationCommand extends CommandDefinition { + name: "user-verification"; + input: NativeAutofillUserVerificationParams; + output: NativeAutofillUserVerificationResult; +} + +export type NativeAutofillUserVerificationParams = { + /** base64 string representing native window handle */ + windowHandle: string; + /** base64 string representing native transaction context */ + transactionContext: string; + displayHint: string; + username: string; +}; + + +export type NativeAutofillUserVerificationResult = CommandOutput<{}>; diff --git a/apps/desktop/src/platform/preload.ts b/apps/desktop/src/platform/preload.ts index 5af2fa571ec..a8dedc5e0e4 100644 --- a/apps/desktop/src/platform/preload.ts +++ b/apps/desktop/src/platform/preload.ts @@ -137,6 +137,7 @@ export default { hideWindow: () => ipcRenderer.send("window-hide"), log: (level: LogLevelType, message?: any, ...optionalParams: any[]) => ipcRenderer.invoke("ipc.log", { level, message, optionalParams }), + getNativeWindowHandle: async () => Buffer.from(await ipcRenderer.invoke("get-native-window-handle"), "base64"), openContextMenu: ( menu: { diff --git a/libs/common/src/platform/abstractions/fido2/fido2-authenticator.service.abstraction.ts b/libs/common/src/platform/abstractions/fido2/fido2-authenticator.service.abstraction.ts index 427266522e9..3fc6a346783 100644 --- a/libs/common/src/platform/abstractions/fido2/fido2-authenticator.service.abstraction.ts +++ b/libs/common/src/platform/abstractions/fido2/fido2-authenticator.service.abstraction.ts @@ -33,6 +33,7 @@ export abstract class Fido2AuthenticatorService { params: Fido2AuthenticatorGetAssertionParams, window: ParentWindowReference, abortController?: AbortController, + transactionContext?: ArrayBuffer, ): Promise; /** diff --git a/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts b/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts index b8be164c837..d949eec4326 100644 --- a/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts +++ b/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts @@ -71,6 +71,7 @@ export abstract class Fido2UserInterfaceService { fallbackSupported: boolean, window: ParentWindowReference, abortController?: AbortController, + transactionContext?: ArrayBuffer, ): Promise; } diff --git a/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts b/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts index 2e8abc77bbf..35b1758ef55 100644 --- a/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts +++ b/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts @@ -230,11 +230,13 @@ export class Fido2AuthenticatorService params: Fido2AuthenticatorGetAssertionParams, window: ParentWindowReference, abortController?: AbortController, + transactionContext?: ArrayBuffer, ): Promise { const userInterfaceSession = await this.userInterface.newSession( params.fallbackSupported, window, abortController, + transactionContext, ); try { if (