mirror of
https://github.com/bitwarden/browser
synced 2026-02-01 09:13:54 +00:00
First pass at user verification
This commit is contained in:
@@ -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<u8>,
|
||||
#[serde(rename = "transactionContext", deserialize_with = "deserialize_b64")]
|
||||
pub(crate) transaction_context: Vec<u8>,
|
||||
#[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<SyncResponse> for CommandResponse {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<UserVerificationResponse> for CommandResponse {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(response: UserVerificationResponse) -> Result<Self, anyhow::Error> {
|
||||
Ok(Self::Success {
|
||||
value: serde_json::to_value(response)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_b64<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Vec<u8>, D::Error> {
|
||||
deserializer.deserialize_str(Base64Visitor {})
|
||||
}
|
||||
|
||||
struct Base64Visitor;
|
||||
impl<'de> Visitor<'de> for Base64Visitor {
|
||||
type Value = Vec<u8>;
|
||||
|
||||
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
f.write_str("A valid base64 string")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
use base64::{engine::general_purpose::STANDARD, Engine as _};
|
||||
STANDARD.decode(v).map_err(|err| E::custom(err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String> {
|
||||
// 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<String> {
|
||||
.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<SyncResponse> {
|
||||
@@ -78,13 +54,6 @@ fn handle_sync_request(params: SyncParameters) -> Result<SyncResponse> {
|
||||
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<StatusResponse> {
|
||||
@@ -98,136 +67,44 @@ fn handle_status_request() -> Result<StatusResponse> {
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
async fn handleAssertionRequest(request: autofill.PasskeyAssertionRequest): Promise<string> {
|
||||
this.logService.info("Handling assertion request for rpId:", request.rpId);
|
||||
fn handle_user_verification_request(
|
||||
request: UserVerificationParameters,
|
||||
) -> Result<UserVerificationResponse> {
|
||||
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.PasskeyAssertionResponse>(
|
||||
"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::<WebAuthNPluginPerformUserVerification>(
|
||||
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<string> {
|
||||
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.PasskeyRegistrationResponse>(
|
||||
"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<string> {
|
||||
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<passkey_authenticator.PasskeySyncResponse>(
|
||||
"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<SyncCredential> 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);
|
||||
|
||||
2
apps/desktop/desktop_native/napi/index.d.ts
vendored
2
apps/desktop/desktop_native/napi/index.d.ts
vendored
@@ -178,6 +178,7 @@ export declare namespace autofill {
|
||||
userVerification: UserVerification
|
||||
allowedCredentials: Array<Array<number>>
|
||||
windowXy: Position
|
||||
context?: Array<number>
|
||||
}
|
||||
export interface PasskeyAssertionWithoutUserInterfaceRequest {
|
||||
rpId: string
|
||||
@@ -188,6 +189,7 @@ export declare namespace autofill {
|
||||
clientDataHash: Array<number>
|
||||
userVerification: UserVerification
|
||||
windowXy: Position
|
||||
context?: Array<number>
|
||||
}
|
||||
export interface NativeStatus {
|
||||
key: string
|
||||
|
||||
@@ -716,6 +716,7 @@ pub mod autofill {
|
||||
pub user_verification: UserVerification,
|
||||
pub allowed_credentials: Vec<Vec<u8>>,
|
||||
pub window_xy: Position,
|
||||
pub context: Option<Vec<u8>>,
|
||||
//extension_input: Vec<u8>, TODO: Implement support for extensions
|
||||
}
|
||||
|
||||
@@ -731,6 +732,7 @@ pub mod autofill {
|
||||
pub client_data_hash: Vec<u8>,
|
||||
pub user_verification: UserVerification,
|
||||
pub window_xy: Position,
|
||||
pub context: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -12,6 +12,7 @@ pub struct PasskeyAssertionRequest {
|
||||
pub user_verification: UserVerification,
|
||||
pub allowed_credentials: Vec<Vec<u8>>,
|
||||
pub window_xy: Position,
|
||||
pub context: Vec<u8>,
|
||||
// pub extension_input: Vec<u8>, TODO: Implement support for extensions
|
||||
}
|
||||
|
||||
@@ -26,6 +27,7 @@ pub struct PasskeyAssertionWithoutUserInterfaceRequest {
|
||||
pub client_data_hash: Vec<u8>,
|
||||
pub user_verification: UserVerification,
|
||||
pub window_xy: Position,
|
||||
pub context: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -153,8 +153,8 @@ export class Fido2VaultComponent implements OnInit, OnDestroy {
|
||||
private async validateCipherAccess(cipher: CipherView): Promise<boolean> {
|
||||
if (cipher.reprompt !== CipherRepromptType.None) {
|
||||
return this.passwordRepromptService.showPasswordPrompt();
|
||||
} else {
|
||||
return this.session.promptForUserVerification(cipher)
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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<DesktopFido2UserInterfaceSession> {
|
||||
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<boolean>();
|
||||
@@ -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<boolean> {
|
||||
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<NativeAutofillUserVerificationCommand>({
|
||||
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<void> {
|
||||
this.logService.info("updateCredential");
|
||||
await firstValueFrom(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<C extends CommandDefinition> = (
|
||||
) => Promise<CommandOutput<C["output"]>>;
|
||||
|
||||
/** A list of all available commands */
|
||||
export type Command = NativeAutofillSyncCommand | NativeAutofillStatusCommand;
|
||||
export type Command = NativeAutofillSyncCommand | NativeAutofillStatusCommand | NativeAutofillUserVerificationCommand;
|
||||
|
||||
@@ -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<{}>;
|
||||
@@ -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: {
|
||||
|
||||
@@ -33,6 +33,7 @@ export abstract class Fido2AuthenticatorService<ParentWindowReference> {
|
||||
params: Fido2AuthenticatorGetAssertionParams,
|
||||
window: ParentWindowReference,
|
||||
abortController?: AbortController,
|
||||
transactionContext?: ArrayBuffer,
|
||||
): Promise<Fido2AuthenticatorGetAssertionResult>;
|
||||
|
||||
/**
|
||||
|
||||
@@ -71,6 +71,7 @@ export abstract class Fido2UserInterfaceService<ParentWindowReference> {
|
||||
fallbackSupported: boolean,
|
||||
window: ParentWindowReference,
|
||||
abortController?: AbortController,
|
||||
transactionContext?: ArrayBuffer,
|
||||
): Promise<Fido2UserInterfaceSession>;
|
||||
}
|
||||
|
||||
|
||||
@@ -230,11 +230,13 @@ export class Fido2AuthenticatorService<ParentWindowReference>
|
||||
params: Fido2AuthenticatorGetAssertionParams,
|
||||
window: ParentWindowReference,
|
||||
abortController?: AbortController,
|
||||
transactionContext?: ArrayBuffer,
|
||||
): Promise<Fido2AuthenticatorGetAssertionResult> {
|
||||
const userInterfaceSession = await this.userInterface.newSession(
|
||||
params.fallbackSupported,
|
||||
window,
|
||||
abortController,
|
||||
transactionContext,
|
||||
);
|
||||
try {
|
||||
if (
|
||||
|
||||
Reference in New Issue
Block a user