diff --git a/apps/desktop/desktop_native/macos_provider/src/assertion.rs b/apps/desktop/desktop_native/macos_provider/src/assertion.rs index 762ceaaed48..c31601b1f6d 100644 --- a/apps/desktop/desktop_native/macos_provider/src/assertion.rs +++ b/apps/desktop/desktop_native/macos_provider/src/assertion.rs @@ -7,6 +7,16 @@ use crate::{BitwardenError, Callback, 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>, + //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, diff --git a/apps/desktop/desktop_native/macos_provider/src/lib.rs b/apps/desktop/desktop_native/macos_provider/src/lib.rs index 5623436d874..d59a88b8ab6 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)] @@ -141,6 +144,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/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index 1f37563e4fe..79dccab6afe 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -144,6 +144,12 @@ export declare namespace autofill { attestationObject: Array } export interface PasskeyAssertionRequest { + rpId: string + clientDataHash: Array + userVerification: UserVerification + allowedCredentials: Array> + } + export interface PasskeyAssertionWithoutUserInterfaceRequest { rpId: string credentialId: Array userName: string @@ -167,7 +173,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 170d7bca4f9..a59833669a7 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -608,6 +608,17 @@ 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>, + //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, @@ -659,6 +670,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 { @@ -695,6 +713,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/CredentialProviderViewController.swift b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift index 5f59795eefa..8c2aa15550f 100644 --- a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift +++ b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift @@ -19,15 +19,15 @@ class CredentialProviderViewController: ASCredentialProviderViewController { // I think we still might want a static regardless, to be able to reuse the connection if possible. static let client: MacOsProviderClient = { let instance = MacOsProviderClient.connect() - // setup code - return instance - }() + // setup code + return instance + }() init() { logger = Logger(subsystem: "com.bitwarden.desktop.autofill-extension", category: "credential-provider") logger.log("[autofill-extension] initializing extension") - + super.init(nibName: nil, bundle: nil) } @@ -37,18 +37,19 @@ class CredentialProviderViewController: ASCredentialProviderViewController { deinit { logger.log("[autofill-extension] deinitializing extension") + self.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code: ASExtensionError.userCanceled.rawValue)) } @IBAction func cancel(_ sender: AnyObject?) { self.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code: ASExtensionError.userCanceled.rawValue)) } - + @IBAction func passwordSelected(_ sender: AnyObject?) { let passwordCredential = ASPasswordCredential(user: "j_appleseed", password: "apple1234") self.extensionContext.completeRequest(withSelectedCredential: passwordCredential, completionHandler: nil) } - + override func loadView() { let view = NSView() view.isHidden = true @@ -63,9 +64,9 @@ class CredentialProviderViewController: ASCredentialProviderViewController { Provide the password by completing the extension request with the associated ASPasswordCredential. If using the credential would require showing custom UI for authenticating the user, cancel the request with error code ASExtensionError.userInteractionRequired. - + */ - + // Deprecated override func provideCredentialWithoutUserInteraction(for credentialIdentity: ASPasswordCredentialIdentity) { logger.log("[autofill-extension] provideCredentialWithoutUserInteraction called \(credentialIdentity)") @@ -74,19 +75,19 @@ class CredentialProviderViewController: ASCredentialProviderViewController { logger.log("[autofill-extension] sid \(credentialIdentity.serviceIdentifier.identifier)") logger.log("[autofill-extension] sidt \(credentialIdentity.serviceIdentifier.type.rawValue)") -// let databaseIsUnlocked = true -// if (databaseIsUnlocked) { + // let databaseIsUnlocked = true + // if (databaseIsUnlocked) { let passwordCredential = ASPasswordCredential(user: credentialIdentity.user, password: "example1234") - self.extensionContext.completeRequest(withSelectedCredential: passwordCredential, completionHandler: nil) -// } else { -// self.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code:ASExtensionError.userInteractionRequired.rawValue)) -// } + self.extensionContext.completeRequest(withSelectedCredential: passwordCredential, completionHandler: nil) + // } else { + // self.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code:ASExtensionError.userInteractionRequired.rawValue)) + // } } override func provideCredentialWithoutUserInteraction(for credentialRequest: any ASCredentialRequest) { - + //logger.log("[autofill-extension] provideCredentialWithoutUserInteraction2(credentialRequest) called \(request)") - + if let request = credentialRequest as? ASPasskeyCredentialRequest { if let passkeyIdentity = request.credentialIdentity as? ASPasskeyCredentialIdentity { @@ -94,8 +95,10 @@ class CredentialProviderViewController: ASCredentialProviderViewController { class CallbackImpl: PreparePasskeyAssertionCallback { let ctx: ASCredentialProviderExtensionContext - required init(_ ctx: ASCredentialProviderExtensionContext) { + let logger: Logger + required init(_ ctx: ASCredentialProviderExtensionContext,_ logger: Logger) { self.ctx = ctx + self.logger = logger } func onComplete(credential: PasskeyAssertionResponse) { @@ -109,6 +112,102 @@ class CredentialProviderViewController: ASCredentialProviderViewController { )) } + func onError(error: BitwardenError) { + logger.log("[autofill-extension] ERROR HAPPENED in swift error \(error)") + ctx.cancelRequest(withError: error) + } + } + + let userVerification = switch request.userVerificationPreference { + case .preferred: + UserVerification.preferred + case .required: + UserVerification.required + default: + UserVerification.discouraged + } + + let req = PasskeyAssertionWithoutUserInterfaceRequest( + rpId: passkeyIdentity.relyingPartyIdentifier, + credentialId: passkeyIdentity.credentialID, + userName: passkeyIdentity.userName, + userHandle: passkeyIdentity.userHandle, + recordIdentifier: passkeyIdentity.recordIdentifier, + clientDataHash: request.clientDataHash, + userVerification: userVerification + ) + + CredentialProviderViewController.client.preparePasskeyAssertionWithoutUserInterface(request: req, callback: CallbackImpl(self.extensionContext, self.logger)) + return + } + } + + if let request = credentialRequest as? ASPasswordCredentialRequest { + logger.log("[autofill-extension] provideCredentialWithoutUserInteraction2(password) called \(request)") + return; + } + + logger.log("[autofill-extension] provideCredentialWithoutUserInteraction2 called wrong") + self.extensionContext.cancelRequest(withError: BitwardenError.Internal("Invalid authentication request")) + } + + /* + Implement this method if provideCredentialWithoutUserInteraction(for:) can fail with + ASExtensionError.userInteractionRequired. In this case, the system may present your extension's + UI and call this method. Show appropriate UI for authenticating the user then provide the password + by completing the extension request with the associated ASPasswordCredential. + + override func prepareInterfaceToProvideCredential(for credentialIdentity: ASPasswordCredentialIdentity) { + } + */ + + + override func prepareInterfaceForExtensionConfiguration() { + logger.log("[autofill-extension] prepareInterfaceForExtensionConfiguration called") + } + + override func prepareInterface(forPasskeyRegistration registrationRequest: ASCredentialRequest) { + logger.log("[autofill-extension] prepareInterface") + + // Create a timer for 20 second timeout + let timeoutTimer = DispatchWorkItem { [weak self] in + guard let self = self else { return } + logger.log("[autofill-extension] Registration timed out after 20 seconds") + self.extensionContext.cancelRequest(withError: BitwardenError.Internal("Registration timed out")) + } + + // Schedule the timeout + DispatchQueue.main.asyncAfter(deadline: .now() + 20, execute: timeoutTimer) + + // Create a timer to show UI after 10 seconds + DispatchQueue.main.asyncAfter(deadline: .now() + 10) { [weak self] in + guard let self = self else { return } + // Configure and show UI elements for manual cancellation + self.configureTimeoutUI() + } + + if let request = registrationRequest as? ASPasskeyCredentialRequest { + if let passkeyIdentity = registrationRequest.credentialIdentity as? ASPasskeyCredentialIdentity { + logger.log("[autofill-extension] prepareInterface(passkey) called \(request)") + + class CallbackImpl: PreparePasskeyRegistrationCallback { + let ctx: ASCredentialProviderExtensionContext + + required init(_ ctx: ASCredentialProviderExtensionContext) { + self.ctx = ctx + } + + func onComplete(credential: PasskeyRegistrationResponse) { + + + ctx.completeRegistrationRequest(using: ASPasskeyRegistrationCredential( + relyingParty: credential.rpId, + clientDataHash: credential.clientDataHash, + credentialID: credential.credentialId, + attestationObject: credential.attestationObject + )) + } + func onError(error: BitwardenError) { ctx.cancelRequest(withError: error) } @@ -123,88 +222,6 @@ class CredentialProviderViewController: ASCredentialProviderViewController { UserVerification.discouraged } - let req = PasskeyAssertionRequest( - rpId: passkeyIdentity.relyingPartyIdentifier, - credentialId: passkeyIdentity.credentialID, - userName: passkeyIdentity.userName, - userHandle: passkeyIdentity.userHandle, - recordIdentifier: passkeyIdentity.recordIdentifier, - clientDataHash: request.clientDataHash, - userVerification: userVerification - ) - - CredentialProviderViewController.client.preparePasskeyAssertion(request: req, callback: CallbackImpl(self.extensionContext)) - return - } - } - - if let request = credentialRequest as? ASPasswordCredentialRequest { - logger.log("[autofill-extension] provideCredentialWithoutUserInteraction2(password) called \(request)") - return; - } - - logger.log("[autofill-extension] provideCredentialWithoutUserInteraction2 called wrong") - self.extensionContext.cancelRequest(withError: BitwardenError.Internal("Invalid authentication request")) - } - - /* - Implement this method if provideCredentialWithoutUserInteraction(for:) can fail with - ASExtensionError.userInteractionRequired. In this case, the system may present your extension's - UI and call this method. Show appropriate UI for authenticating the user then provide the password - by completing the extension request with the associated ASPasswordCredential. - - override func prepareInterfaceToProvideCredential(for credentialIdentity: ASPasswordCredentialIdentity) { - } - */ - - - override func prepareInterfaceForExtensionConfiguration() { - logger.log("[autofill-extension] prepareInterfaceForExtensionConfiguration called") - } - - override func prepareInterface(forPasskeyRegistration registrationRequest: ASCredentialRequest) { - logger.log("[autofill-extension] prepareInterface") - - // Create a timer to show UI after 10 seconds - DispatchQueue.main.asyncAfter(deadline: .now() + 10) { [weak self] in - guard let self = self else { return } - // Configure and show UI elements for manual cancellation - self.configureTimeoutUI() - } - - if let request = registrationRequest as? ASPasskeyCredentialRequest { - if let passkeyIdentity = registrationRequest.credentialIdentity as? ASPasskeyCredentialIdentity { - logger.log("[autofill-extension] prepareInterface(passkey) called \(request)") - - class CallbackImpl: PreparePasskeyRegistrationCallback { - let ctx: ASCredentialProviderExtensionContext - required init(_ ctx: ASCredentialProviderExtensionContext) { - self.ctx = ctx - } - - func onComplete(credential: PasskeyRegistrationResponse) { - ctx.completeRegistrationRequest(using: ASPasskeyRegistrationCredential( - relyingParty: credential.rpId, - clientDataHash: credential.clientDataHash, - credentialID: credential.credentialId, - attestationObject: credential.attestationObject - )) - } - - func onError(error: BitwardenError) { - ctx.cancelRequest(withError: error) - } - } - - let userVerification = switch request.userVerificationPreference { - case .preferred: - UserVerification.preferred - case .required: - UserVerification.required - default: - UserVerification.discouraged - } - let req = PasskeyRegistrationRequest( rpId: passkeyIdentity.relyingPartyIdentifier, userName: passkeyIdentity.userName, @@ -217,31 +234,33 @@ class CredentialProviderViewController: ASCredentialProviderViewController { // Log details of the request logger.log("[autofill-extension] rpId: \(req.rpId)") logger.log("[autofill-extension] rpId: \(req.userName)") - + + CredentialProviderViewController.client.preparePasskeyRegistration(request: req, callback: CallbackImpl(self.extensionContext)) return } } - + logger.log("[autofill-extension] We didn't get a passkey") - + + timeoutTimer.cancel() // If we didn't get a passkey, return an error self.extensionContext.cancelRequest(withError: BitwardenError.Internal("Invalid registration request")) } - + /* - Prepare your UI to list available credentials for the user to choose from. The items in - 'serviceIdentifiers' describe the service the user is logging in to, so your extension can - prioritize the most relevant credentials in the list. + Prepare your UI to list available credentials for the user to choose from. The items in + 'serviceIdentifiers' describe the service the user is logging in to, so your extension can + prioritize the most relevant credentials in the list. */ override func prepareCredentialList(for serviceIdentifiers: [ASCredentialServiceIdentifier]) { logger.log("[autofill-extension] prepareCredentialList for serviceIdentifiers: \(serviceIdentifiers.count)") - + for serviceIdentifier in serviceIdentifiers { logger.log(" service: \(serviceIdentifier.identifier)") } } - + override func prepareCredentialList(for serviceIdentifiers: [ASCredentialServiceIdentifier], requestParameters: ASPasskeyCredentialRequestParameters) { logger.log("[autofill-extension] prepareCredentialList(passkey) for serviceIdentifiers: \(serviceIdentifiers.count)") logger.log("request parameters: \(requestParameters.relyingPartyIdentifier)") @@ -282,28 +301,20 @@ class CredentialProviderViewController: ASCredentialProviderViewController { UserVerification.discouraged } - // TODO: PasskeyAssertionRequest does not implement allowedCredentials, extensions and required credentialId, username, userhandle, recordIdentifier?? let req = PasskeyAssertionRequest( rpId: requestParameters.relyingPartyIdentifier, - - // TODO: remove once the PasskeyAssertionRequest type has been improved - credentialId: Data(), - userName: "", - userHandle: Data(), - recordIdentifier: "", - - //allowedCredentials: requestParameters.allowedCredentials, - //extensionInput: requestParameters.extensionInput, clientDataHash: requestParameters.clientDataHash, - userVerification: userVerification + userVerification: userVerification, + allowedCredentials: requestParameters.allowedCredentials + //extensionInput: requestParameters.extensionInput, ) CredentialProviderViewController.client.preparePasskeyAssertion(request: req, callback: CallbackImpl(self.extensionContext)) return } - + private func configureTimeoutUI() { self.view.isHidden = false; } - + } diff --git a/apps/desktop/src/autofill/preload.ts b/apps/desktop/src/autofill/preload.ts index 494544f5858..2c006b5c928 100644 --- a/apps/desktop/src/autofill/preload.ts +++ b/apps/desktop/src/autofill/preload.ts @@ -80,6 +80,44 @@ export default { return; } + ipcRenderer.send("autofill.completePasskeyAssertion", { + clientId, + sequenceNumber, + response, + }); + }); + }, + ); + }, + listenPasskeyAssertionWithoutUserInterface: ( + fn: ( + clientId: number, + sequenceNumber: number, + request: autofill.PasskeyAssertionWithoutUserInterfaceRequest, + completeCallback: (error: Error | null, response: autofill.PasskeyAssertionResponse) => void, + ) => void, + ) => { + ipcRenderer.on( + "autofill.passkeyAssertionWithoutUserInterface", + ( + event, + data: { + clientId: number; + sequenceNumber: number; + request: autofill.PasskeyAssertionWithoutUserInterfaceRequest; + }, + ) => { + const { clientId, sequenceNumber, request } = data; + fn(clientId, sequenceNumber, request, (error, response) => { + if (error) { + ipcRenderer.send("autofill.completeError", { + clientId, + sequenceNumber, + error: error.message, + }); + return; + } + ipcRenderer.send("autofill.completePasskeyAssertion", { clientId, sequenceNumber, diff --git a/apps/desktop/src/autofill/services/desktop-autofill.service.ts b/apps/desktop/src/autofill/services/desktop-autofill.service.ts index 86f7ef0a432..1a1c46c822a 100644 --- a/apps/desktop/src/autofill/services/desktop-autofill.service.ts +++ b/apps/desktop/src/autofill/services/desktop-autofill.service.ts @@ -7,7 +7,7 @@ import { map, mergeMap, switchMap, - takeUntil + takeUntil, } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; @@ -157,39 +157,56 @@ export class DesktopAutofillService implements OnDestroy { }); }); + ipc.autofill.listenPasskeyAssertionWithoutUserInterface( + async (clientId, sequenceNumber, request, callback) => { + this.logService.warning("listenPasskeyAssertion", clientId, sequenceNumber, request); + + // TODO: For some reason the credentialId is passed as an empty array in the request, so we need to + // get it from the cipher. For that we use the recordIdentifier, which is the cipherId. + if (request.recordIdentifier && request.credentialId.length === 0) { + const cipher = await this.cipherService.get(request.recordIdentifier); + if (!cipher) { + this.logService.error("listenPasskeyAssertion error", "Cipher not found"); + callback(new Error("Cipher not found"), null); + return; + } + + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + + const decrypted = await cipher.decrypt( + await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), + ); + + const fido2Credential = decrypted.login.fido2Credentials?.[0]; + if (!fido2Credential) { + this.logService.error("listenPasskeyAssertion error", "Fido2Credential not found"); + callback(new Error("Fido2Credential not found"), null); + return; + } + + request.credentialId = Array.from( + guidToRawFormat(decrypted.login.fido2Credentials?.[0].credentialId), + ); + } + + const controller = new AbortController(); + void this.fido2AuthenticatorService + .getAssertion(this.convertAssertionRequest(request), null, controller) + .then((response) => { + callback(null, this.convertAssertionResponse(request, response)); + }) + .catch((error) => { + this.logService.error("listenPasskeyAssertion error", error); + callback(error, null); + }); + }, + ); + ipc.autofill.listenPasskeyAssertion(async (clientId, sequenceNumber, request, callback) => { this.logService.warning("listenPasskeyAssertion", clientId, sequenceNumber, request); - // TODO: For some reason the credentialId is passed as an empty array in the request, so we need to - // get it from the cipher. For that we use the recordIdentifier, which is the cipherId. - if (request.recordIdentifier && request.credentialId.length === 0) { - const cipher = await this.cipherService.get(request.recordIdentifier); - if (!cipher) { - this.logService.error("listenPasskeyAssertion error", "Cipher not found"); - callback(new Error("Cipher not found"), null); - return; - } - - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); - - const decrypted = await cipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), - ); - - const fido2Credential = decrypted.login.fido2Credentials?.[0]; - if (!fido2Credential) { - this.logService.error("listenPasskeyAssertion error", "Fido2Credential not found"); - callback(new Error("Fido2Credential not found"), null); - return; - } - - request.credentialId = Array.from( - guidToRawFormat(decrypted.login.fido2Credentials?.[0].credentialId), - ); - } - const controller = new AbortController(); void this.fido2AuthenticatorService .getAssertion(this.convertAssertionRequest(request), null, controller) @@ -245,17 +262,29 @@ export class DesktopAutofillService implements OnDestroy { } private convertAssertionRequest( - request: autofill.PasskeyAssertionRequest, + request: + | autofill.PasskeyAssertionRequest + | autofill.PasskeyAssertionWithoutUserInterfaceRequest, ): Fido2AuthenticatorGetAssertionParams { + let allowedCredentials; + if ("credentialId" in request) { + allowedCredentials = [ + { + id: new Uint8Array(request.credentialId), + type: "public-key" as const, + }, + ]; + } else { + allowedCredentials = request.allowedCredentials.map((credentialId) => ({ + id: new Uint8Array(credentialId), + type: "public-key" as const, + })); + } + return { rpId: request.rpId, hash: new Uint8Array(request.clientDataHash), - allowCredentialDescriptorList: [ - { - id: new Uint8Array(request.credentialId), - type: "public-key", - }, - ], + allowCredentialDescriptorList: allowedCredentials, extensions: {}, requireUserVerification: request.userVerification === "required" || request.userVerification === "preferred", @@ -264,7 +293,9 @@ export class DesktopAutofillService implements OnDestroy { } private convertAssertionResponse( - request: autofill.PasskeyAssertionRequest, + request: + | autofill.PasskeyAssertionRequest + | autofill.PasskeyAssertionWithoutUserInterfaceRequest, response: Fido2AuthenticatorGetAssertionResult, ): autofill.PasskeyAssertionResponse { return { 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 1465831340f..fdd83f17f86 100644 --- a/apps/desktop/src/platform/main/autofill/native-autofill.main.ts +++ b/apps/desktop/src/platform/main/autofill/native-autofill.main.ts @@ -60,6 +60,18 @@ export class NativeAutofillMain { request, }); }, + // AssertionWihtoutUserInterfaceCallback + (error, clientId, sequenceNumber, request) => { + if (error) { + this.logService.error("autofill.IpcServer.assertion", error); + return; + } + this.windowMain.win.webContents.send("autofill.passkeyAssertionWithoutUserInterface", { + clientId, + sequenceNumber, + request, + }); + }, ); ipcMain.on("autofill.completePasskeyRegistration", (event, data) => { @@ -77,7 +89,7 @@ export class NativeAutofillMain { ipcMain.on("autofill.completeError", (event, data) => { this.logService.warning("autofill.completeError", data); const { clientId, sequenceNumber, error } = data; - this.ipcServer.completeAssertion(clientId, sequenceNumber, error); + this.ipcServer.completeError(clientId, sequenceNumber, error); }); }