From 4926278fb919c7fdb7ca8b92a37fd34879bfaa9f Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Wed, 22 Mar 2023 15:37:16 +0100 Subject: [PATCH] [EC-598] feat: add support for saving discoverable credential --- .../fido2-authenticator.service.spec.ts | 31 ++++++++++- .../services/fido2-authenticator.service.ts | 52 +++++++++++++++++-- 2 files changed, 78 insertions(+), 5 deletions(-) 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 abf90a45944..40be5872539 100644 --- a/libs/common/src/webauthn/services/fido2-authenticator.service.spec.ts +++ b/libs/common/src/webauthn/services/fido2-authenticator.service.spec.ts @@ -163,7 +163,7 @@ describe("FidoAuthenticatorService", () => { } as NewCredentialParams); }); - /** Spec: If the user declines permission */ + /** Spec: If the user declines permission, return the CTAP2_ERR_OPERATION_DENIED error. */ it("should throw error if user denies creation request", async () => { userInterface.confirmNewCredential.mockResolvedValue(false); const params = await createCredentialParams(); @@ -175,6 +175,35 @@ describe("FidoAuthenticatorService", () => { ); }); }); + + describe("creation of discoverable credential", () => { + it("should save credential to vault if request confirmed by user", async () => { + const encryptedCipher = Symbol(); + userInterface.confirmNewCredential.mockResolvedValue(true); + cipherService.encrypt.mockResolvedValue(encryptedCipher as unknown as Cipher); + const params = await createCredentialParams({ options: { rk: true } }); + + await authenticator.makeCredential(params); + + const saved = cipherService.encrypt.mock.lastCall?.[0]; + expect(saved).toEqual( + expect.objectContaining({ + type: CipherType.Fido2Key, + name: params.rp.name, + + fido2Key: expect.objectContaining({ + keyType: "ECDSA", + keyCurve: "P-256", + rpId: params.rp.id, + rpName: params.rp.name, + userHandle: Fido2Utils.bufferToString(params.user.id), + userName: params.user.name, + }), + }) + ); + expect(cipherService.createWithServer).toHaveBeenCalledWith(encryptedCipher); + }); + }); }); }); diff --git a/libs/common/src/webauthn/services/fido2-authenticator.service.ts b/libs/common/src/webauthn/services/fido2-authenticator.service.ts index c9ef2a7e70f..98f8d68a94c 100644 --- a/libs/common/src/webauthn/services/fido2-authenticator.service.ts +++ b/libs/common/src/webauthn/services/fido2-authenticator.service.ts @@ -1,3 +1,5 @@ +import { CipherType } from "../../vault/enums/cipher-type"; +import { CipherView } from "../../vault/models/view/cipher.view"; import { CipherService } from "../../vault/services/cipher.service"; import { Fido2AlgorithmIdentifier, @@ -8,6 +10,9 @@ import { } 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"; + +const KeyUsages: KeyUsage[] = ["sign"]; /** * Bitwarden implementation of the Authenticator API described by the FIDO Alliance @@ -40,12 +45,12 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr // We deviate from this because we allow duplicates to be created if the user confirms it, // and we don't want to ask the user for confirmation if the input params haven't already // been verified. - const duplicateExists = await this.vaultContainsId( + const isExcluded = await this.vaultContainsId( params.excludeList.map((key) => Fido2Utils.bufferToString(key.id)) ); let userVerification = false; - if (duplicateExists) { + if (isExcluded) { userVerification = await this.userInterface.confirmDuplicateCredential( [Fido2Utils.bufferToString(params.excludeList[0].id)], { @@ -60,11 +65,17 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr }); } - if (!userVerification && duplicateExists) { + if (!userVerification && isExcluded) { throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.CTAP2_ERR_CREDENTIAL_EXCLUDED); - } else if (!userVerification && !duplicateExists) { + } else if (!userVerification && !isExcluded) { throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.CTAP2_ERR_OPERATION_DENIED); } + + const keyPair = await this.createKeyPair(); + const vaultItem = await this.createVaultItem(params, keyPair.privateKey); + + const encrypted = await this.cipherService.encrypt(vaultItem); + await this.cipherService.createWithServer(encrypted); } private async vaultContainsId(ids: string[]): Promise { @@ -76,4 +87,37 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr return false; } + + private async createKeyPair() { + return await crypto.subtle.generateKey( + { + name: "ECDSA", + namedCurve: "P-256", + }, + true, + KeyUsages + ); + } + + private async createVaultItem( + params: Fido2AuthenticatorMakeCredentialsParams, + keyValue: CryptoKey + ): Promise { + const pcks8Key = await crypto.subtle.exportKey("pkcs8", keyValue); + + const view = new CipherView(); + view.type = CipherType.Fido2Key; + view.name = params.rp.name; + + view.fido2Key = new Fido2KeyView(); + view.fido2Key.keyType = "ECDSA"; + view.fido2Key.keyCurve = "P-256"; + view.fido2Key.keyValue = Fido2Utils.bufferToString(pcks8Key); + view.fido2Key.rpId = params.rp.id; + view.fido2Key.rpName = params.rp.name; + view.fido2Key.userHandle = Fido2Utils.bufferToString(params.user.id); + view.fido2Key.userName = params.user.name; + + return view; + } }