From 55cd736ec372d5938e7fc3a885151cb6ceeaef43 Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Tue, 4 Apr 2023 16:21:43 +0200 Subject: [PATCH] [EC-598] feat: fully wokring non-discoverable implementation --- .../browser-fido2-user-interface.service.ts | 50 ++++++- .../webauthn/popup/fido2/fido2.component.html | 7 +- .../webauthn/popup/fido2/fido2.component.ts | 22 ++- libs/common/src/models/api/login.api.ts | 7 + .../src/vault/models/data/cipher.data.ts | 2 + .../src/vault/models/data/login.data.ts | 6 + libs/common/src/vault/models/domain/login.ts | 20 ++- .../vault/models/request/cipher.request.ts | 38 ++++++ .../src/vault/models/view/login.view.ts | 6 +- .../src/vault/services/cipher.service.ts | 21 +++ .../fido2-authenticator.service.spec.ts | 128 +++++++++++------- .../services/fido2-authenticator.service.ts | 77 ++++++----- .../noop-fido2-user-interface.service.ts | 22 ++- 13 files changed, 313 insertions(+), 93 deletions(-) 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 ff882353f64..c378cf1adae 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 @@ -47,6 +47,15 @@ export type BrowserFido2Message = { requestId: string } & ( | { type: "ConfirmNewCredentialResponse"; } + | { + type: "ConfirmNewNonDiscoverableCredentialRequest"; + credentialName: string; + userName: string; + } + | { + type: "ConfirmNewNonDiscoverableCredentialResponse"; + cipherId: string; + } | { type: "AbortRequest"; } @@ -201,10 +210,47 @@ export class BrowserFido2UserInterfaceService implements Fido2UserInterfaceServi } async confirmNewNonDiscoverableCredential( - params: NewCredentialParams, + { credentialName, userName }: NewCredentialParams, abortController?: AbortController ): Promise { - return null; + const requestId = Utils.newGuid(); + const data: BrowserFido2Message = { + type: "ConfirmNewNonDiscoverableCredentialRequest", + requestId, + credentialName, + userName, + }; + const queryParams = new URLSearchParams({ data: JSON.stringify(data) }).toString(); + + const abortHandler = () => + BrowserFido2UserInterfaceService.sendMessage({ type: "AbortRequest", requestId }); + abortController.signal.addEventListener("abort", abortHandler); + + this.popupUtilsService.popOut( + null, + `popup/index.html?uilocation=popout#/fido2?${queryParams}`, + { center: true } + ); + + const response = await lastValueFrom( + this.messages$.pipe( + filter((msg) => msg.requestId === requestId), + first(), + takeUntil(this.destroy$) + ) + ); + + if (response.type === "ConfirmNewNonDiscoverableCredentialResponse") { + return response.cipherId; + } + + if (response.type === "AbortResponse") { + throw new RequestAbortedError(response.fallbackRequested); + } + + abortController.signal.removeEventListener("abort", abortHandler); + + return undefined; } async informExcludedCredential( diff --git a/apps/browser/src/webauthn/popup/fido2/fido2.component.html b/apps/browser/src/webauthn/popup/fido2/fido2.component.html index 49bd634abfa..7127e58de72 100644 --- a/apps/browser/src/webauthn/popup/fido2/fido2.component.html +++ b/apps/browser/src/webauthn/popup/fido2/fido2.component.html @@ -11,7 +11,12 @@ Authenticate - + A site is asking for authentication, please choose one of the following credentials to use:
diff --git a/apps/browser/src/webauthn/popup/fido2/fido2.component.ts b/apps/browser/src/webauthn/popup/fido2/fido2.component.ts index 53a010f94ea..d8dd452147b 100644 --- a/apps/browser/src/webauthn/popup/fido2/fido2.component.ts +++ b/apps/browser/src/webauthn/popup/fido2/fido2.component.ts @@ -48,6 +48,10 @@ export class Fido2Component implements OnInit, OnDestroy { return cipher.decrypt(); }) ); + } else if (this.data?.type === "ConfirmNewNonDiscoverableCredentialRequest") { + this.ciphers = (await this.cipherService.getAllDecrypted()).filter( + (cipher) => cipher.type === CipherType.Login && !cipher.isDeleted + ); } }), takeUntil(this.destroy$) @@ -66,11 +70,19 @@ export class Fido2Component implements OnInit, OnDestroy { } async pick(cipher: CipherView) { - BrowserFido2UserInterfaceService.sendMessage({ - requestId: this.data.requestId, - cipherId: cipher.id, - type: "PickCredentialResponse", - }); + if (this.data?.type === "PickCredentialRequest") { + BrowserFido2UserInterfaceService.sendMessage({ + requestId: this.data.requestId, + cipherId: cipher.id, + type: "PickCredentialResponse", + }); + } else if (this.data?.type === "ConfirmNewNonDiscoverableCredentialRequest") { + BrowserFido2UserInterfaceService.sendMessage({ + requestId: this.data.requestId, + cipherId: cipher.id, + type: "ConfirmNewNonDiscoverableCredentialResponse", + }); + } window.close(); } diff --git a/libs/common/src/models/api/login.api.ts b/libs/common/src/models/api/login.api.ts index 82e28dd0a35..7845c9e2ec6 100644 --- a/libs/common/src/models/api/login.api.ts +++ b/libs/common/src/models/api/login.api.ts @@ -1,3 +1,4 @@ +import { Fido2KeyApi } from "../../webauthn/models/api/fido2-key.api"; import { BaseResponse } from "../response/base.response"; import { LoginUriApi } from "./login-uri.api"; @@ -9,6 +10,7 @@ export class LoginApi extends BaseResponse { passwordRevisionDate: string; totp: string; autofillOnPageLoad: boolean; + fido2Key?: Fido2KeyApi; constructor(data: any = null) { super(data); @@ -25,5 +27,10 @@ export class LoginApi extends BaseResponse { if (uris != null) { this.uris = uris.map((u: any) => new LoginUriApi(u)); } + + const fido2Key = this.getResponseProperty("Fido2Key"); + if (fido2Key != null) { + this.fido2Key = new Fido2KeyApi(fido2Key); + } } } diff --git a/libs/common/src/vault/models/data/cipher.data.ts b/libs/common/src/vault/models/data/cipher.data.ts index 7c2d4bc3915..eac82cf2536 100644 --- a/libs/common/src/vault/models/data/cipher.data.ts +++ b/libs/common/src/vault/models/data/cipher.data.ts @@ -60,6 +60,8 @@ export class CipherData { switch (this.type) { case CipherType.Login: this.login = new LoginData(response.login); + this.fido2Key = + response.fido2Key != undefined ? new Fido2KeyData(response.fido2Key) : undefined; break; case CipherType.SecureNote: this.secureNote = new SecureNoteData(response.secureNote); diff --git a/libs/common/src/vault/models/data/login.data.ts b/libs/common/src/vault/models/data/login.data.ts index 585b46ac05b..8de44975260 100644 --- a/libs/common/src/vault/models/data/login.data.ts +++ b/libs/common/src/vault/models/data/login.data.ts @@ -1,4 +1,5 @@ import { LoginApi } from "../../../models/api/login.api"; +import { Fido2KeyData } from "../../../webauthn/models/data/fido2-key.data"; import { LoginUriData } from "./login-uri.data"; @@ -9,6 +10,7 @@ export class LoginData { passwordRevisionDate: string; totp: string; autofillOnPageLoad: boolean; + fido2Key?: Fido2KeyData; constructor(data?: LoginApi) { if (data == null) { @@ -24,5 +26,9 @@ export class LoginData { if (data.uris) { this.uris = data.uris.map((u) => new LoginUriData(u)); } + + if (data.fido2Key) { + this.fido2Key = new Fido2KeyData(data.fido2Key); + } } } diff --git a/libs/common/src/vault/models/domain/login.ts b/libs/common/src/vault/models/domain/login.ts index 763fba212f0..6e4e1e21cd7 100644 --- a/libs/common/src/vault/models/domain/login.ts +++ b/libs/common/src/vault/models/domain/login.ts @@ -3,6 +3,7 @@ import { Jsonify } from "type-fest"; import Domain from "../../../models/domain/domain-base"; import { EncString } from "../../../models/domain/enc-string"; import { SymmetricCryptoKey } from "../../../models/domain/symmetric-crypto-key"; +import { Fido2Key } from "../../../webauthn/models/domain/fido2-key"; import { LoginData } from "../data/login.data"; import { LoginView } from "../view/login.view"; @@ -15,6 +16,7 @@ export class Login extends Domain { passwordRevisionDate?: Date; totp: EncString; autofillOnPageLoad: boolean; + fido2Key: Fido2Key; constructor(obj?: LoginData) { super(); @@ -42,6 +44,10 @@ export class Login extends Domain { this.uris.push(new LoginUri(u)); }); } + + if (obj.fido2Key) { + this.fido2Key = new Fido2Key(obj.fido2Key); + } } async decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise { @@ -64,6 +70,10 @@ export class Login extends Domain { } } + if (this.fido2Key != null) { + view.fido2Key = await this.fido2Key.decrypt(orgId, encKey); + } + return view; } @@ -85,6 +95,10 @@ export class Login extends Domain { }); } + if (this.fido2Key != null) { + l.fido2Key = this.fido2Key.toFido2KeyData(); + } + return l; } @@ -99,13 +113,15 @@ export class Login extends Domain { const passwordRevisionDate = obj.passwordRevisionDate == null ? null : new Date(obj.passwordRevisionDate); const uris = obj.uris?.map((uri: any) => LoginUri.fromJSON(uri)); + const fido2Key = obj.fido2Key == null ? null : Fido2Key.fromJSON(obj.fido2Key); return Object.assign(new Login(), obj, { username, password, totp, - passwordRevisionDate: passwordRevisionDate, - uris: uris, + passwordRevisionDate, + uris, + fido2Key, }); } } diff --git a/libs/common/src/vault/models/request/cipher.request.ts b/libs/common/src/vault/models/request/cipher.request.ts index 26c9663dbd3..967464ce23a 100644 --- a/libs/common/src/vault/models/request/cipher.request.ts +++ b/libs/common/src/vault/models/request/cipher.request.ts @@ -63,6 +63,44 @@ export class CipherRequest { return uri; }); } + + if (cipher.login.fido2Key != null) { + this.login.fido2Key = new Fido2KeyApi(); + this.login.fido2Key.nonDiscoverableId = + cipher.login.fido2Key.nonDiscoverableId != null + ? cipher.login.fido2Key.nonDiscoverableId.encryptedString + : null; + this.login.fido2Key.keyType = + cipher.login.fido2Key.keyType != null + ? (cipher.login.fido2Key.keyType.encryptedString as "public-key") + : null; + this.login.fido2Key.keyAlgorithm = + cipher.login.fido2Key.keyAlgorithm != null + ? (cipher.login.fido2Key.keyAlgorithm.encryptedString as "ECDSA") + : null; + this.login.fido2Key.keyCurve = + cipher.login.fido2Key.keyCurve != null + ? (cipher.login.fido2Key.keyCurve.encryptedString as "P-256") + : null; + this.login.fido2Key.keyValue = + cipher.login.fido2Key.keyValue != null + ? cipher.login.fido2Key.keyValue.encryptedString + : null; + this.login.fido2Key.rpId = + cipher.login.fido2Key.rpId != null ? cipher.login.fido2Key.rpId.encryptedString : null; + this.login.fido2Key.rpName = + cipher.login.fido2Key.rpName != null + ? cipher.login.fido2Key.rpName.encryptedString + : null; + this.login.fido2Key.userHandle = + cipher.login.fido2Key.userHandle != null + ? cipher.login.fido2Key.userHandle.encryptedString + : null; + this.login.fido2Key.userName = + cipher.login.fido2Key.userName != null + ? cipher.login.fido2Key.userName.encryptedString + : null; + } break; case CipherType.SecureNote: this.secureNote = new SecureNoteApi(); diff --git a/libs/common/src/vault/models/view/login.view.ts b/libs/common/src/vault/models/view/login.view.ts index 3aea2205833..4b5b46740e5 100644 --- a/libs/common/src/vault/models/view/login.view.ts +++ b/libs/common/src/vault/models/view/login.view.ts @@ -3,6 +3,7 @@ import { Jsonify } from "type-fest"; import { LoginLinkedId as LinkedId } from "../../../enums/linkedIdType"; import { linkedFieldOption } from "../../../misc/linkedFieldOption.decorator"; import { Utils } from "../../../misc/utils"; +import { Fido2KeyView } from "../../../webauthn/models/view/fido2-key.view"; import { Login } from "../domain/login"; import { ItemView } from "./item.view"; @@ -18,6 +19,7 @@ export class LoginView extends ItemView { totp: string = null; uris: LoginUriView[] = null; autofillOnPageLoad: boolean = null; + fido2Key?: Fido2KeyView; constructor(l?: Login) { super(); @@ -67,10 +69,12 @@ export class LoginView extends ItemView { const passwordRevisionDate = obj.passwordRevisionDate == null ? null : new Date(obj.passwordRevisionDate); const uris = obj.uris?.map((uri: any) => LoginUriView.fromJSON(uri)); + const fido2Key = obj.fido2Key == null ? null : Fido2KeyView.fromJSON(obj.fido2Key); return Object.assign(new LoginView(), obj, { passwordRevisionDate: passwordRevisionDate, - uris: uris, + uris, + fido2Key, }); } } diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index ab1c96a54fe..189c494cb93 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -1117,6 +1117,27 @@ export class CipherService implements CipherServiceAbstraction { cipher.login.uris.push(loginUri); } } + + if (model.login.fido2Key != null) { + cipher.login.fido2Key = new Fido2Key(); + await this.encryptObjProperty( + model.login.fido2Key, + cipher.login.fido2Key, + { + nonDiscoverableId: null, + keyType: null, + keyAlgorithm: null, + keyCurve: null, + keyValue: null, + rpId: null, + rpName: null, + userHandle: null, + userName: null, + origin: null, + }, + key + ); + } return; case CipherType.SecureNote: cipher.secureNote = new SecureNote(); 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 144946d238c..4264cccf345 100644 --- a/libs/common/src/webauthn/services/fido2-authenticator.service.spec.ts +++ b/libs/common/src/webauthn/services/fido2-authenticator.service.spec.ts @@ -104,7 +104,7 @@ describe("FidoAuthenticatorService", () => { params = await createParams({ excludeCredentialDescriptorList: [ { - id: Utils.guidToRawFormat(excludedCipher.fido2Key.nonDiscoverableId), + id: Utils.guidToRawFormat(excludedCipher.login.fido2Key.nonDiscoverableId), type: "public-key", }, ], @@ -162,15 +162,16 @@ describe("FidoAuthenticatorService", () => { let params: Fido2AuthenticatorMakeCredentialsParams; beforeEach(async () => { - const excludedCipher = createCipherView(); - excludedCipherView = await excludedCipher; + excludedCipherView = createCipherView(); params = await createParams({ excludeCredentialDescriptorList: [ - { id: Utils.guidToRawFormat(excludedCipher.id), type: "public-key" }, + { id: Utils.guidToRawFormat(excludedCipherView.id), type: "public-key" }, ], }); cipherService.get.mockImplementation(async (id) => - id === excludedCipher.id ? excludedCipher : undefined + id === excludedCipherView.id + ? ({ decrypt: async () => excludedCipherView } as any) + : undefined ); cipherService.getAllDecrypted.mockResolvedValue([excludedCipherView]); }); @@ -237,12 +238,15 @@ describe("FidoAuthenticatorService", () => { return cipher; }); - await authenticator.makeCredential(params); + await authenticator.makeCredential(params, new AbortController()); - expect(userInterface.confirmNewCredential).toHaveBeenCalledWith({ - credentialName: params.rpEntity.name, - userName: params.userEntity.displayName, - } as NewCredentialParams); + expect(userInterface.confirmNewCredential).toHaveBeenCalledWith( + { + credentialName: params.rpEntity.name, + userName: params.userEntity.displayName, + } as NewCredentialParams, + expect.anything() + ); }); it("should save credential to vault if request confirmed by user", async () => { @@ -320,12 +324,15 @@ describe("FidoAuthenticatorService", () => { it("should request confirmation from user", async () => { userInterface.confirmNewNonDiscoverableCredential.mockResolvedValue(existingCipher.id); - await authenticator.makeCredential(params); + await authenticator.makeCredential(params, new AbortController()); - expect(userInterface.confirmNewNonDiscoverableCredential).toHaveBeenCalledWith({ - credentialName: params.rpEntity.name, - userName: params.userEntity.displayName, - } as NewCredentialParams); + expect(userInterface.confirmNewNonDiscoverableCredential).toHaveBeenCalledWith( + { + credentialName: params.rpEntity.name, + userName: params.userEntity.displayName, + } as NewCredentialParams, + expect.anything() + ); }); it("should save credential to vault if request confirmed by user", async () => { @@ -341,16 +348,18 @@ describe("FidoAuthenticatorService", () => { type: CipherType.Login, name: existingCipher.name, - fido2Key: expect.objectContaining({ - nonDiscoverableId: expect.anything(), - keyType: "public-key", - keyAlgorithm: "ECDSA", - keyCurve: "P-256", - rpId: params.rpEntity.id, - rpName: params.rpEntity.name, - userHandle: Fido2Utils.bufferToString(params.userEntity.id), - counter: 0, - userName: params.userEntity.displayName, + login: expect.objectContaining({ + fido2Key: expect.objectContaining({ + nonDiscoverableId: expect.anything(), + keyType: "public-key", + keyAlgorithm: "ECDSA", + keyCurve: "P-256", + rpId: params.rpEntity.id, + rpName: params.rpEntity.name, + userHandle: Fido2Utils.bufferToString(params.userEntity.id), + counter: 0, + userName: params.userEntity.displayName, + }), }), }) ); @@ -406,7 +415,9 @@ describe("FidoAuthenticatorService", () => { ); cipherService.getAllDecrypted.mockResolvedValue([await cipher]); cipherService.encrypt.mockImplementation(async (cipher) => { - cipher.fido2Key.nonDiscoverableId = nonDiscoverableId; // Replace id for testability + if (!requireResidentKey) { + cipher.login.fido2Key.nonDiscoverableId = nonDiscoverableId; // Replace id for testability + } return {} as any; }); cipherService.createWithServer.mockImplementation(async (cipher) => { @@ -561,8 +572,8 @@ describe("FidoAuthenticatorService", () => { it("should throw error if credential exists but rpId does not match", async () => { const cipher = await createCipherView({ type: CipherType.Login }); - cipher.fido2Key.nonDiscoverableId = credentialId; - cipher.fido2Key.rpId = "mismatch-rpid"; + cipher.login.fido2Key.nonDiscoverableId = credentialId; + cipher.login.fido2Key.rpId = "mismatch-rpid"; cipherService.getAllDecrypted.mockResolvedValue([cipher]); const result = async () => await authenticator.getAssertion(params); @@ -639,6 +650,7 @@ describe("FidoAuthenticatorService", () => { let credentialIds: string[]; let selectedCredentialId: string; let ciphers: CipherView[]; + let fido2Keys: Fido2KeyView[]; let params: Fido2AuthenticatorGetAssertionParams; beforeEach(async () => { @@ -654,6 +666,7 @@ describe("FidoAuthenticatorService", () => { { rpId: RpId, counter: 9000, keyValue } ) ); + fido2Keys = ciphers.map((c) => c.fido2Key); selectedCredentialId = ciphers[0].id; params = await createParams({ allowCredentialDescriptorList: undefined, @@ -666,6 +679,7 @@ describe("FidoAuthenticatorService", () => { { nonDiscoverableId: id, rpId: RpId, counter: 9000 } ) ); + fido2Keys = ciphers.map((c) => c.login.fido2Key); selectedCredentialId = credentialIds[0]; params = await createParams({ allowCredentialDescriptorList: credentialIds.map((credentialId) => ({ @@ -686,15 +700,28 @@ describe("FidoAuthenticatorService", () => { await authenticator.getAssertion(params); - expect(cipherService.encrypt).toHaveBeenCalledWith( - expect.objectContaining({ - id: ciphers[0].id, - fido2Key: expect.objectContaining({ - counter: 9001, - }), - }) - ); expect(cipherService.updateWithServer).toHaveBeenCalledWith(encrypted); + if (residentKey) { + expect(cipherService.encrypt).toHaveBeenCalledWith( + expect.objectContaining({ + id: ciphers[0].id, + fido2Key: expect.objectContaining({ + counter: 9001, + }), + }) + ); + } else { + expect(cipherService.encrypt).toHaveBeenCalledWith( + expect.objectContaining({ + id: ciphers[0].id, + login: expect.objectContaining({ + fido2Key: expect.objectContaining({ + counter: 9001, + }), + }), + }) + ); + } }); it("should return an assertion result", async () => { @@ -707,7 +734,7 @@ describe("FidoAuthenticatorService", () => { expect(result.selectedCredential.id).toEqual(Utils.guidToRawFormat(selectedCredentialId)); expect(result.selectedCredential.userHandle).toEqual( - Fido2Utils.stringToBuffer(ciphers[0].fido2Key.userHandle) + Fido2Utils.stringToBuffer(fido2Keys[0].userHandle) ); expect(rpIdHash).toEqual( new Uint8Array([ @@ -779,18 +806,25 @@ function createCipherView( cipher.id = data.id ?? Utils.newGuid(); cipher.type = data.type ?? CipherType.Fido2Key; cipher.localData = {}; - cipher.login = data.type ?? data.type === CipherType.Login ? new LoginView() : null; - cipher.fido2Key = new Fido2KeyView(); - cipher.fido2Key.nonDiscoverableId = fido2Key.nonDiscoverableId; - cipher.fido2Key.rpId = fido2Key.rpId ?? RpId; - cipher.fido2Key.counter = fido2Key.counter ?? 0; - cipher.fido2Key.userHandle = fido2Key.userHandle ?? Fido2Utils.bufferToString(randomBytes(16)); - cipher.fido2Key.keyAlgorithm = fido2Key.keyAlgorithm ?? "ECDSA"; - cipher.fido2Key.keyCurve = fido2Key.keyCurve ?? "P-256"; - cipher.fido2Key.keyValue = - fido2Key.keyValue ?? + + const fido2KeyView = new Fido2KeyView(); + fido2KeyView.nonDiscoverableId = fido2Key.nonDiscoverableId; + fido2KeyView.rpId = fido2Key.rpId ?? RpId; + fido2KeyView.counter = fido2Key.counter ?? 0; + fido2KeyView.userHandle = fido2Key.userHandle ?? Fido2Utils.bufferToString(randomBytes(16)); + fido2KeyView.keyAlgorithm = fido2Key.keyAlgorithm ?? "ECDSA"; + fido2KeyView.keyCurve = fido2Key.keyCurve ?? "P-256"; + fido2KeyView.keyValue = + fido2KeyView.keyValue ?? "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgTC-7XDZipXbaVBlnkjlBgO16ZmqBZWejK2iYo6lV0dehRANCAASOcM2WduNq1DriRYN7ZekvZz-bRhA-qNT4v0fbp5suUFJyWmgOQ0bybZcLXHaerK5Ep1JiSrQcewtQNgLtry7f"; + if (cipher.type === CipherType.Login) { + cipher.login = new LoginView(); + cipher.login.fido2Key = fido2KeyView; + } else { + cipher.fido2Key = fido2KeyView; + } + return cipher; } diff --git a/libs/common/src/webauthn/services/fido2-authenticator.service.ts b/libs/common/src/webauthn/services/fido2-authenticator.service.ts index 166dfc4517f..745b9570cbf 100644 --- a/libs/common/src/webauthn/services/fido2-authenticator.service.ts +++ b/libs/common/src/webauthn/services/fido2-authenticator.service.ts @@ -38,7 +38,8 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr private userInterface: Fido2UserInterfaceService ) {} async makeCredential( - params: Fido2AuthenticatorMakeCredentialsParams + params: Fido2AuthenticatorMakeCredentialsParams, + abortController?: AbortController ): Promise { if (params.credTypesAndPubKeyAlgs.every((p) => p.alg !== Fido2AlgorithmIdentifier.ES256)) { throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotSupported); @@ -66,19 +67,24 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr { credentialName: params.rpEntity.name, userName: params.userEntity.displayName, - } + }, + abortController ); throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotAllowed); } let cipher: CipherView; + let fido2Key: Fido2KeyView; let keyPair: CryptoKeyPair; if (params.requireResidentKey) { - const userVerification = await this.userInterface.confirmNewCredential({ - credentialName: params.rpEntity.name, - userName: params.userEntity.displayName, - }); + const userVerification = await this.userInterface.confirmNewCredential( + { + credentialName: params.rpEntity.name, + userName: params.userEntity.displayName, + }, + abortController + ); if (!userVerification) { throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotAllowed); @@ -90,7 +96,7 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr cipher = new CipherView(); cipher.type = CipherType.Fido2Key; cipher.name = params.rpEntity.name; - cipher.fido2Key = await createKeyView(params, keyPair.privateKey); + cipher.fido2Key = fido2Key = await createKeyView(params, keyPair.privateKey); const encrypted = await this.cipherService.encrypt(cipher); await this.cipherService.createWithServer(encrypted); // encrypted.id is assigned inside here cipher.id = encrypted.id; @@ -98,10 +104,13 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.Unknown); } } else { - const cipherId = await this.userInterface.confirmNewNonDiscoverableCredential({ - credentialName: params.rpEntity.name, - userName: params.userEntity.displayName, - }); + const cipherId = await this.userInterface.confirmNewNonDiscoverableCredential( + { + credentialName: params.rpEntity.name, + userName: params.userEntity.displayName, + }, + abortController + ); if (cipherId === undefined) { throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotAllowed); @@ -112,19 +121,21 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr const encrypted = await this.cipherService.get(cipherId); cipher = await encrypted.decrypt(); - cipher.fido2Key = await createKeyView(params, keyPair.privateKey); + cipher.login.fido2Key = fido2Key = await createKeyView(params, keyPair.privateKey); const reencrypted = await this.cipherService.encrypt(cipher); await this.cipherService.updateWithServer(reencrypted); - } catch (error) { + } catch { throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.Unknown); } } - const credentialId = params.requireResidentKey ? cipher.id : cipher.fido2Key.nonDiscoverableId; + const credentialId = + cipher.type === CipherType.Fido2Key ? cipher.id : cipher.login.fido2Key.nonDiscoverableId; + const authData = await generateAuthData({ rpId: params.rpEntity.id, credentialId: Utils.guidToRawFormat(credentialId), - counter: cipher.fido2Key.counter, + counter: fido2Key.counter, userPresence: true, userVerification: false, keyPair, @@ -185,12 +196,16 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr } try { + const selectedFido2Key = + selectedCipher.type === CipherType.Login + ? selectedCipher.login.fido2Key + : selectedCipher.fido2Key; const selectedCredentialId = - params.allowCredentialDescriptorList?.length > 0 - ? selectedCipher.fido2Key.nonDiscoverableId + selectedCipher.type === CipherType.Login + ? selectedFido2Key.nonDiscoverableId : selectedCipher.id; - ++selectedCipher.fido2Key.counter; + ++selectedFido2Key.counter; selectedCipher.localData = { ...selectedCipher.localData, @@ -200,9 +215,9 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr await this.cipherService.updateWithServer(encrypted); const authenticatorData = await generateAuthData({ - rpId: selectedCipher.fido2Key.rpId, + rpId: selectedFido2Key.rpId, credentialId: Utils.guidToRawFormat(selectedCredentialId), - counter: selectedCipher.fido2Key.counter, + counter: selectedFido2Key.counter, userPresence: true, userVerification: false, }); @@ -210,14 +225,14 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr const signature = await generateSignature({ authData: authenticatorData, clientDataHash: params.hash, - privateKey: await getPrivateKeyFromCipher(selectedCipher), + privateKey: await getPrivateKeyFromFido2Key(selectedFido2Key), }); return { authenticatorData, selectedCredential: { id: Utils.guidToRawFormat(selectedCredentialId), - userHandle: Fido2Utils.stringToBuffer(selectedCipher.fido2Key.userHandle), + userHandle: Fido2Utils.stringToBuffer(selectedFido2Key.userHandle), }, signature, }; @@ -247,8 +262,8 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr (cipher) => (cipher.type === CipherType.Fido2Key && ids.includes(cipher.id)) || (cipher.type === CipherType.Login && - cipher.fido2Key != undefined && - ids.includes(cipher.fido2Key.nonDiscoverableId)) + cipher.login.fido2Key != undefined && + ids.includes(cipher.login.fido2Key.nonDiscoverableId)) ); } @@ -274,9 +289,9 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr (cipher) => !cipher.isDeleted && cipher.type === CipherType.Login && - cipher.fido2Key != undefined && - cipher.fido2Key.rpId === rpId && - ids.includes(cipher.fido2Key.nonDiscoverableId) + cipher.login.fido2Key != undefined && + cipher.login.fido2Key.rpId === rpId && + ids.includes(cipher.login.fido2Key.nonDiscoverableId) ); } @@ -324,14 +339,14 @@ async function createKeyView( return fido2Key; } -async function getPrivateKeyFromCipher(cipher: CipherView): Promise { - const keyBuffer = Fido2Utils.stringToBuffer(cipher.fido2Key.keyValue); +async function getPrivateKeyFromFido2Key(fido2Key: Fido2KeyView): Promise { + const keyBuffer = Fido2Utils.stringToBuffer(fido2Key.keyValue); return await crypto.subtle.importKey( "pkcs8", keyBuffer, { - name: cipher.fido2Key.keyAlgorithm, - namedCurve: cipher.fido2Key.keyCurve, + name: fido2Key.keyAlgorithm, + namedCurve: fido2Key.keyCurve, } as EcKeyImportParams, true, KeyUsages 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 517402d111b..513c8c451de 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 @@ -1,5 +1,8 @@ -import { Fido2UserInterfaceService as Fido2UserInterfaceServiceAbstraction } from "../abstractions/fido2-user-interface.service.abstraction"; -import { RequestAbortedError } from "../abstractions/fido2.service.abstraction"; +import { RequestAbortedError } from "../abstractions/fido2-client.service.abstraction"; +import { + Fido2UserInterfaceService as Fido2UserInterfaceServiceAbstraction, + NewCredentialParams, +} from "../abstractions/fido2-user-interface.service.abstraction"; export class Fido2UserInterfaceService implements Fido2UserInterfaceServiceAbstraction { async confirmCredential(): Promise { @@ -14,7 +17,18 @@ export class Fido2UserInterfaceService implements Fido2UserInterfaceServiceAbstr return false; } - async confirmDuplicateCredential() { - return false; + async confirmNewNonDiscoverableCredential( + params: NewCredentialParams, + abortController?: AbortController + ): Promise { + return null; + } + + async informExcludedCredential( + existingCipherIds: string[], + newCredential: NewCredentialParams, + abortController?: AbortController + ): Promise { + // Not Implemented } }