diff --git a/apps/browser/src/services/fido2/browser-fido2-user-interface.service.ts b/apps/browser/src/services/fido2/browser-fido2-user-interface.service.ts index d3ea3c2ec6..b0e1beb519 100644 --- a/apps/browser/src/services/fido2/browser-fido2-user-interface.service.ts +++ b/apps/browser/src/services/fido2/browser-fido2-user-interface.service.ts @@ -189,6 +189,15 @@ export class BrowserFido2UserInterfaceService implements Fido2UserInterfaceServi return false; } + async confirmDuplicateCredential( + existingCipherIds: string[], + newCredential: NewCredentialParams, + abortController?: AbortController + ) { + // Not Implemented + return false; + } + private setAbortTimeout(abortController: AbortController) { return setTimeout(() => abortController.abort()); } diff --git a/libs/common/src/webauthn/abstractions/fido2-authenticator.service.abstraction.ts b/libs/common/src/webauthn/abstractions/fido2-authenticator.service.abstraction.ts index bd397f570d..2ab186abfd 100644 --- a/libs/common/src/webauthn/abstractions/fido2-authenticator.service.abstraction.ts +++ b/libs/common/src/webauthn/abstractions/fido2-authenticator.service.abstraction.ts @@ -18,18 +18,19 @@ export interface Fido2AuthenticatorMakeCredentialsParams { id?: string; }; user: { - name: string; - displayName: string; id: BufferSource; + name?: string; + displayName?: string; + icon?: string; }; pubKeyCredParams: { alg: number; - // type: "public-key"; // not used + type: "public-key"; // not used }[]; excludeList?: { id: BufferSource; transports?: ("ble" | "internal" | "nfc" | "usb")[]; - // type: "public-key"; // not used + type: "public-key"; // not used }[]; extensions?: { appid?: string; diff --git a/libs/common/src/webauthn/abstractions/fido2-user-interface.service.abstraction.ts b/libs/common/src/webauthn/abstractions/fido2-user-interface.service.abstraction.ts index cd08532fbf..f3f304bdc1 100644 --- a/libs/common/src/webauthn/abstractions/fido2-user-interface.service.abstraction.ts +++ b/libs/common/src/webauthn/abstractions/fido2-user-interface.service.abstraction.ts @@ -10,4 +10,9 @@ export abstract class Fido2UserInterfaceService { params: NewCredentialParams, abortController?: AbortController ) => Promise; + confirmDuplicateCredential: ( + existingCipherIds: string[], + newCredential: NewCredentialParams, + abortController?: AbortController + ) => Promise; } diff --git a/libs/common/src/webauthn/services/fido2-authenticator.service.spec.ts b/libs/common/src/webauthn/services/fido2-authenticator.service.spec.ts index 46f239aab6..c5cf76c624 100644 --- a/libs/common/src/webauthn/services/fido2-authenticator.service.spec.ts +++ b/libs/common/src/webauthn/services/fido2-authenticator.service.spec.ts @@ -1,5 +1,118 @@ +import { TextEncoder } from "util"; + +import { mock, MockProxy } from "jest-mock-extended"; + +import { Utils } from "../../misc/utils"; +import { CipherService } from "../../vault/abstractions/cipher.service"; +import { CipherType } from "../../vault/enums/cipher-type"; +import { CipherView } from "../../vault/models/view/cipher.view"; +import { Fido2AuthenticatorMakeCredentialsParams } from "../abstractions/fido2-authenticator.service.abstraction"; +import { Fido2UserInterfaceService } from "../abstractions/fido2-user-interface.service.abstraction"; +import { Fido2Utils } from "../abstractions/fido2-utils"; +import { Fido2KeyView } from "../models/view/fido2-key.view"; + +import { Fido2AuthenticatorService } from "./fido2-authenticator.service"; + +const RpId = "bitwarden.com"; + describe("FidoAuthenticatorService", () => { + let cipherService!: MockProxy; + let userInterface!: MockProxy; + let authenticator!: Fido2AuthenticatorService; + + beforeEach(() => { + cipherService = mock(); + userInterface = mock(); + authenticator = new Fido2AuthenticatorService(cipherService, userInterface); + }); + describe("authenticatorMakeCredential", () => { - test.skip("To be implemented"); + describe("when vault contains excluded credential", () => { + let excludedCipher: CipherView; + let params: Fido2AuthenticatorMakeCredentialsParams; + + beforeEach(async () => { + excludedCipher = createCipherView(); + params = await createCredentialParams({ + excludeList: [{ id: Fido2Utils.stringToBuffer(excludedCipher.id), type: "public-key" }], + }); + cipherService.getAllDecrypted.mockResolvedValue([excludedCipher]); + }); + + /** Spec: wait for user presence */ + it("should wait for confirmation from user", async () => { + userInterface.confirmDuplicateCredential.mockResolvedValue(true); + + await authenticator.makeCredential(params); + + expect(userInterface.confirmDuplicateCredential).toHaveBeenCalled(); + }); + }); }); }); + +async function createCredentialParams( + params: Partial = {} +): Promise { + return { + clientDataHash: params.clientDataHash ?? (await createClientDataHash()), + rp: params.rp ?? { + name: "Bitwarden", + id: RpId, + }, + user: params.user ?? { + id: randomBytes(64), + name: "jane.doe@bitwarden.com", + displayName: "Jane Doe", + icon: " data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAOhJREFUeNpiFI+9E8DAwDAfiAUYSAMfgDiQBVmzlSYnUTqPXf/OANWzngVZ87pKKaIMCGp/BjeEhRjFMKAjx8bQFC2CIs9CpHNxAiYGCsEQM4Cfiwm3AY9f/yZogIcRN4ZahAFv/jAcu4E7xMNtecEYpAakFqsX8me9Yvj07R+G5jR3foaJqWJgOZAaZMAIzAv/kQV05NgZ5hdIMMiKQJIIyEYrDU6wrYkTXjBcefQTvwGwwCoJFGJIBdoMArN3fmToWf+O4SMW14EMeI8rJ8Jcgexn9BwJCoNEaNbEACCN+DSDsjNAgAEAri9Zii/uDMsAAAAASUVORK5CYII=", + }, + pubKeyCredParams: params.pubKeyCredParams ?? [ + { + alg: -1, // ES256 + type: "public-key", + }, + ], + excludeList: params.excludeList ?? [ + { + id: randomBytes(16), + transports: ["internal"], + type: "public-key", + }, + ], + extensions: params.extensions ?? { + appid: undefined, + appidExclude: undefined, + credProps: undefined, + uvm: false as boolean, + }, + options: params.options ?? { + rk: false as boolean, + uv: false as boolean, + }, + }; +} + +function createCipherView(id = Utils.newGuid()): CipherView { + const cipher = new CipherView(); + cipher.id = id; + cipher.type = CipherType.Fido2Key; + cipher.fido2Key = new Fido2KeyView(); + return cipher; +} + +async function createClientDataHash() { + const encoder = new TextEncoder(); + const clientData = encoder.encode( + JSON.stringify({ + type: "webauthn.create", + challenge: Fido2Utils.bufferToString(randomBytes(16)), + origin: RpId, + crossOrigin: false, + }) + ); + return await crypto.subtle.digest({ name: "SHA-256" }, clientData); +} + +function randomBytes(length: number) { + return new Uint8Array(Array.from({ length }, () => Math.floor(Math.random() * 255))); +} diff --git a/libs/common/src/webauthn/services/fido2-authenticator.service.ts b/libs/common/src/webauthn/services/fido2-authenticator.service.ts index be8384e55d..c8b971e149 100644 --- a/libs/common/src/webauthn/services/fido2-authenticator.service.ts +++ b/libs/common/src/webauthn/services/fido2-authenticator.service.ts @@ -1,12 +1,28 @@ +import { CipherService } from "../../vault/services/cipher.service"; import { Fido2AuthenticatorMakeCredentialsParams, Fido2AuthenticatorService as Fido2AuthenticatorServiceAbstraction, } from "../abstractions/fido2-authenticator.service.abstraction"; +import { Fido2UserInterfaceService } from "../abstractions/fido2-user-interface.service.abstraction"; +import { Fido2Utils } from "../abstractions/fido2-utils"; /** * Bitwarden implementation of the Authenticator API described by the FIDO Alliance * https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html */ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstraction { - makeCredential: (params: Fido2AuthenticatorMakeCredentialsParams) => void; + constructor( + private cipherService: CipherService, + private userInterface: Fido2UserInterfaceService + ) {} + + async makeCredential(params: Fido2AuthenticatorMakeCredentialsParams): Promise { + this.userInterface.confirmDuplicateCredential( + [Fido2Utils.bufferToString(params.excludeList[0].id)], + { + credentialName: params.rp.name, + userName: params.user.name, + } + ); + } } diff --git a/libs/common/src/webauthn/services/noop-fido2-user-interface.service.ts b/libs/common/src/webauthn/services/noop-fido2-user-interface.service.ts index fdd32bf572..517402d111 100644 --- a/libs/common/src/webauthn/services/noop-fido2-user-interface.service.ts +++ b/libs/common/src/webauthn/services/noop-fido2-user-interface.service.ts @@ -2,15 +2,19 @@ import { Fido2UserInterfaceService as Fido2UserInterfaceServiceAbstraction } fro import { RequestAbortedError } from "../abstractions/fido2.service.abstraction"; export class Fido2UserInterfaceService implements Fido2UserInterfaceServiceAbstraction { - async confirmCredential(cipherId: string): Promise { + async confirmCredential(): Promise { return false; } - pickCredential(cipherIds: string[]): Promise { + pickCredential(): Promise { throw new RequestAbortedError(); } async confirmNewCredential(): Promise { return false; } + + async confirmDuplicateCredential() { + return false; + } }