diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index 0839535bd0..cbc73a8d70 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -210,6 +210,8 @@ export default class RuntimeBackground { break; case "fido2RegisterCredentialRequest": return await this.main.fido2Service.createCredential(msg.data); + case "fido2GetCredentialRequest": + return await this.main.fido2Service.assertCredential(msg.data); } return undefined; } diff --git a/apps/browser/src/browser/webauthn-utils.ts b/apps/browser/src/browser/webauthn-utils.ts index 7783c8a1f7..f59816a3a4 100644 --- a/apps/browser/src/browser/webauthn-utils.ts +++ b/apps/browser/src/browser/webauthn-utils.ts @@ -1,5 +1,7 @@ import { Fido2Utils } from "@bitwarden/common/abstractions/fido2/fido2-utils"; import { + CredentialAssertParams, + CredentialAssertResult, CredentialRegistrationParams, CredentialRegistrationResult, } from "@bitwarden/common/abstractions/fido2/fido2.service.abstraction"; @@ -62,4 +64,38 @@ export class WebauthnUtils { getClientExtensionResults: () => ({}), }; } + + static mapCredentialRequestOptions( + options: CredentialRequestOptions, + origin: string + ): CredentialAssertParams { + const keyOptions = options.publicKey; + + if (keyOptions == undefined) { + throw new Error("Public-key options not found"); + } + + return { + origin, + allowedCredentialIds: + keyOptions.allowCredentials?.map((c) => Fido2Utils.bufferToString(c.id)) ?? [], + challenge: Fido2Utils.bufferToString(keyOptions.challenge), + rpId: keyOptions.rpId, + }; + } + + static mapCredentialAssertResult(result: CredentialAssertResult): PublicKeyCredential { + return { + id: result.credentialId, + rawId: Fido2Utils.stringToBuffer(result.credentialId), + type: "public-key", + response: { + authenticatorData: Fido2Utils.stringToBuffer(result.authenticatorData), + clientDataJSON: Fido2Utils.stringToBuffer(result.clientDataJSON), + signature: Fido2Utils.stringToBuffer(result.signature), + userHandle: Fido2Utils.stringToBuffer(result.userHandle), + } as AuthenticatorAssertionResponse, + getClientExtensionResults: () => ({}), + }; + } } diff --git a/apps/browser/src/content/webauthn/content-script.ts b/apps/browser/src/content/webauthn/content-script.ts index 91ab565b6a..6e3c04f1b5 100644 --- a/apps/browser/src/content/webauthn/content-script.ts +++ b/apps/browser/src/content/webauthn/content-script.ts @@ -29,5 +29,22 @@ messenger.addHandler(async (message) => { }); } + if (message.type === MessageType.CredentialGetRequest) { + return new Promise((resolve, reject) => { + chrome.runtime.sendMessage( + { + command: "fido2GetCredentialRequest", + data: message.data, + }, + (response) => { + resolve({ + type: MessageType.CredentialGetResponse, + result: response, + }); + } + ); + }); + } + return undefined; }); diff --git a/apps/browser/src/content/webauthn/messaging/message.ts b/apps/browser/src/content/webauthn/messaging/message.ts index 92c83f9669..06d0c29674 100644 --- a/apps/browser/src/content/webauthn/messaging/message.ts +++ b/apps/browser/src/content/webauthn/messaging/message.ts @@ -1,4 +1,6 @@ import { + CredentialAssertParams, + CredentialAssertResult, CredentialRegistrationParams, CredentialRegistrationResult, } from "@bitwarden/common/abstractions/fido2/fido2.service.abstraction"; @@ -25,10 +27,12 @@ export type CredentialCreationResponse = { export type CredentialGetRequest = { type: MessageType.CredentialGetRequest; + data: CredentialAssertParams; }; export type CredentialGetResponse = { type: MessageType.CredentialGetResponse; + result?: CredentialAssertResult; }; export type AbortRequest = { diff --git a/apps/browser/src/content/webauthn/page-script.ts b/apps/browser/src/content/webauthn/page-script.ts index 1081a8db11..b5438cdfc8 100644 --- a/apps/browser/src/content/webauthn/page-script.ts +++ b/apps/browser/src/content/webauthn/page-script.ts @@ -27,5 +27,14 @@ navigator.credentials.create = async (options?: CredentialCreationOptions): Prom }; navigator.credentials.get = async (options?: CredentialRequestOptions): Promise => { - return await browserCredentials.get(options); + const response = await messenger.request({ + type: MessageType.CredentialGetRequest, + data: WebauthnUtils.mapCredentialRequestOptions(options, window.location.origin), + }); + + if (response.type !== MessageType.CredentialGetResponse) { + return await browserCredentials.get(options); + } + + return WebauthnUtils.mapCredentialAssertResult(response.result); }; diff --git a/libs/common/src/abstractions/fido2/fido2.service.abstraction.ts b/libs/common/src/abstractions/fido2/fido2.service.abstraction.ts index 2e43e34ee2..9466cb6cb2 100644 --- a/libs/common/src/abstractions/fido2/fido2.service.abstraction.ts +++ b/libs/common/src/abstractions/fido2/fido2.service.abstraction.ts @@ -39,7 +39,22 @@ export interface CredentialRegistrationResult { attestationObject: string; } +export interface CredentialAssertParams { + allowedCredentialIds: string[]; + rpId: string; + origin: string; + challenge: string; +} + +export interface CredentialAssertResult { + credentialId: string; + clientDataJSON: string; + authenticatorData: string; + signature: string; + userHandle: string; +} + export abstract class Fido2Service { createCredential: (params: CredentialRegistrationParams) => Promise; - assertCredential: () => unknown; + assertCredential: (params: CredentialAssertParams) => Promise; } diff --git a/libs/common/src/services/fido2/fido2.service.ts b/libs/common/src/services/fido2/fido2.service.ts index 37fc2d1b63..4ac6845a54 100644 --- a/libs/common/src/services/fido2/fido2.service.ts +++ b/libs/common/src/services/fido2/fido2.service.ts @@ -3,6 +3,8 @@ import { CBOR } from "cbor-redux"; import { Fido2UserInterfaceService } from "../../abstractions/fido2/fido2-user-interface.service.abstraction"; import { Fido2Utils } from "../../abstractions/fido2/fido2-utils"; import { + CredentialAssertParams, + CredentialAssertResult, CredentialRegistrationParams, CredentialRegistrationResult, Fido2Service as Fido2ServiceAbstraction, @@ -102,7 +104,74 @@ export class Fido2Service implements Fido2ServiceAbstraction { }; } - assertCredential(): unknown { + async assertCredential(params: CredentialAssertParams): Promise { + let credential: BitCredential | undefined; + + if (params.allowedCredentialIds && params.allowedCredentialIds.length > 0) { + // We're looking for regular non-resident keys + credential = this.getCredential(params.allowedCredentialIds); + } else { + // We're looking for a resident key + credential = this.getCredentialByRp(params.rpId); + } + + if (credential === undefined) { + throw new Error("No valid credentials found"); + } + + if (credential.origin !== params.origin) { + throw new Error("Not allowed: Origin mismatch"); + } + + const encoder = new TextEncoder(); + const clientData = encoder.encode( + JSON.stringify({ + type: "webauthn.get", + challenge: params.challenge, + origin: params.origin, + }) + ); + + const authData = await generateAuthData({ + credentialId: credential.credentialId, + rpId: params.rpId, + userPresence: true, + userVerification: true, + }); + + const signature = await generateSignature({ + authData, + clientData, + keyPair: credential.keyPair, + }); + + return { + credentialId: credential.credentialId.encoded, + clientDataJSON: Fido2Utils.bufferToString(clientData), + authenticatorData: Fido2Utils.bufferToString(authData), + signature: Fido2Utils.bufferToString(signature), + userHandle: Fido2Utils.bufferToString(credential.userHandle), + }; + } + + private getCredential(allowedCredentialIds: string[]): BitCredential | undefined { + let credential: BitCredential | undefined; + for (const allowedCredential of allowedCredentialIds) { + const id = new CredentialId(allowedCredential); + if (this.credentials.has(id.encoded)) { + credential = this.credentials.get(id.encoded); + break; + } + } + return credential; + } + + private getCredentialByRp(rpId: string): BitCredential | undefined { + for (const credential of this.credentials.values()) { + if (credential.rpId === rpId) { + return credential; + } + } return undefined; } }