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..955beb682ea 100644 --- a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift +++ b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift @@ -94,8 +94,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) { @@ -110,6 +112,7 @@ class CredentialProviderViewController: ASCredentialProviderViewController { } func onError(error: BitwardenError) { + logger.log("[autofill-extension] ERROR HAPPENED in swift error \(error)") ctx.cancelRequest(withError: error) } } @@ -123,7 +126,7 @@ class CredentialProviderViewController: ASCredentialProviderViewController { UserVerification.discouraged } - let req = PasskeyAssertionRequest( + let req = PasskeyAssertionWithoutUserInterfaceRequest( rpId: passkeyIdentity.relyingPartyIdentifier, credentialId: passkeyIdentity.credentialID, userName: passkeyIdentity.userName, @@ -133,7 +136,7 @@ class CredentialProviderViewController: ASCredentialProviderViewController { userVerification: userVerification ) - CredentialProviderViewController.client.preparePasskeyAssertion(request: req, callback: CallbackImpl(self.extensionContext)) + CredentialProviderViewController.client.preparePasskeyAssertionWithoutUserInterface(request: req, callback: CallbackImpl(self.extensionContext, self.logger)) return } } @@ -282,20 +285,12 @@ 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)) 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..cf95e4714cc 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,38 +157,85 @@ 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; - } + // 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 activeUserId = await firstValueFrom( + // this.accountService.activeAccount$.pipe(map((a) => a?.id)), + // ); - const decrypted = await cipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), - ); + // 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; - } + // 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), - ); - } + // request.credentialId = Array.from( + // guidToRawFormat(decrypted.login.fido2Credentials?.[0].credentialId), + // ); + // } const controller = new AbortController(); void this.fido2AuthenticatorService @@ -245,17 +292,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 +323,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); }); }