A passkey already exists in Bitwarden for this account
diff --git a/apps/browser/src/vault/popup/components/fido2/fido2.component.ts b/apps/browser/src/vault/popup/components/fido2/fido2.component.ts
index 8a847c4b923..74ed60cce17 100644
--- a/apps/browser/src/vault/popup/components/fido2/fido2.component.ts
+++ b/apps/browser/src/vault/popup/components/fido2/fido2.component.ts
@@ -16,7 +16,6 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
import { PasswordRepromptService } from "@bitwarden/common/vault/abstractions/password-reprompt.service";
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
-import { Fido2KeyView } from "@bitwarden/common/vault/models/view/fido2-key.view";
import { BrowserApi } from "../../../../platform/browser/browser-api";
import {
@@ -80,12 +79,9 @@ export class Fido2Component implements OnInit, OnDestroy {
filter((message) => message != undefined),
concatMap(async (message) => {
if (message.type === "ConfirmNewCredentialRequest") {
- const cipher = new CipherView();
- cipher.name = message.credentialName;
- cipher.type = CipherType.Fido2Key;
- cipher.fido2Key = new Fido2KeyView();
- cipher.fido2Key.userDisplayName = message.userName;
- this.ciphers = [cipher];
+ this.ciphers = (await this.cipherService.getAllDecrypted()).filter(
+ (cipher) => cipher.type === CipherType.Login && !cipher.isDeleted
+ );
} else if (message.type === "PickCredentialRequest") {
this.ciphers = await Promise.all(
message.cipherIds.map(async (cipherId) => {
@@ -93,10 +89,6 @@ export class Fido2Component implements OnInit, OnDestroy {
return cipher.decrypt();
})
);
- } else if (message.type === "ConfirmNewNonDiscoverableCredentialRequest") {
- this.ciphers = (await this.cipherService.getAllDecrypted()).filter(
- (cipher) => cipher.type === CipherType.Login && !cipher.isDeleted
- );
} else if (message.type === "InformExcludedCredentialRequest") {
this.ciphers = await Promise.all(
message.existingCipherIds.map(async (cipherId) => {
@@ -140,7 +132,7 @@ export class Fido2Component implements OnInit, OnDestroy {
type: "PickCredentialResponse",
userVerified,
});
- } else if (data?.type === "ConfirmNewNonDiscoverableCredentialRequest") {
+ } else if (data?.type === "ConfirmNewCredentialRequest") {
let userVerified = false;
if (data.userVerification) {
userVerified = await this.passwordRepromptService.showPasswordPrompt();
@@ -149,7 +141,7 @@ export class Fido2Component implements OnInit, OnDestroy {
this.send({
sessionId: this.sessionId,
cipherId: cipher.id,
- type: "ConfirmNewNonDiscoverableCredentialResponse",
+ type: "ConfirmNewCredentialResponse",
userVerified,
});
}
@@ -157,25 +149,6 @@ export class Fido2Component implements OnInit, OnDestroy {
this.loading = true;
}
- async confirm() {
- const data = this.message$.value;
- if (data.type !== "ConfirmNewCredentialRequest") {
- return;
- }
-
- let userVerified = false;
- if (data.userVerification) {
- userVerified = await this.passwordRepromptService.showPasswordPrompt();
- }
-
- this.send({
- sessionId: this.sessionId,
- type: "ConfirmNewCredentialResponse",
- userVerified,
- });
- this.loading = true;
- }
-
abort(fallback: boolean) {
this.unload(fallback);
window.close();
diff --git a/libs/common/src/vault/abstractions/fido2/fido2-user-interface.service.abstraction.ts b/libs/common/src/vault/abstractions/fido2/fido2-user-interface.service.abstraction.ts
index 3d0db42d3c1..d5c31caae55 100644
--- a/libs/common/src/vault/abstractions/fido2/fido2-user-interface.service.abstraction.ts
+++ b/libs/common/src/vault/abstractions/fido2/fido2-user-interface.service.abstraction.ts
@@ -27,10 +27,6 @@ export abstract class Fido2UserInterfaceSession {
confirmNewCredential: (
params: NewCredentialParams,
abortController?: AbortController
- ) => Promise<{ confirmed: boolean; userVerified: boolean }>;
- confirmNewNonDiscoverableCredential: (
- params: NewCredentialParams,
- abortController?: AbortController
) => Promise<{ cipherId: string; userVerified: boolean }>;
informExcludedCredential: (
existingCipherIds: string[],
diff --git a/libs/common/src/vault/services/fido2/fido2-authenticator.service.spec.ts b/libs/common/src/vault/services/fido2/fido2-authenticator.service.spec.ts
index 35a1442db6e..74e674f9c55 100644
--- a/libs/common/src/vault/services/fido2/fido2-authenticator.service.spec.ts
+++ b/libs/common/src/vault/services/fido2/fido2-authenticator.service.spec.ts
@@ -84,7 +84,7 @@ describe("FidoAuthenticatorService", () => {
it("should not request confirmation from user", async () => {
userInterfaceSession.confirmNewCredential.mockResolvedValue({
- confirmed: true,
+ cipherId: "75280e7e-a72e-4d6c-bf1e-d37238352f9b",
userVerified: false,
});
const invalidParams = await createInvalidParams();
@@ -256,110 +256,7 @@ describe("FidoAuthenticatorService", () => {
);
});
- describe("creation of discoverable credential", () => {
- let params: Fido2AuthenticatorMakeCredentialsParams;
-
- beforeEach(async () => {
- params = await createParams({ requireResidentKey: true });
- cipherService.getAllDecrypted.mockResolvedValue([]);
- });
-
- /**
- * Spec: Collect an authorization gesture confirming user consent for creating a new credential. The prompt for the authorization gesture is shown by the authenticator if it has its own output capability. The prompt SHOULD display rpEntity.id, rpEntity.name, userEntity.name and userEntity.displayName, if possible.
- * If requireUserVerification is true, the authorization gesture MUST include user verification.
- * Deviation: Only `rpEntity.name` and `userEntity.name` is shown.
- * */
- for (const userVerification of [true, false]) {
- it(`should request confirmation from user when user verification is ${userVerification}`, async () => {
- params.requireUserVerification = userVerification;
- userInterfaceSession.confirmNewCredential.mockResolvedValue({
- confirmed: true,
- userVerified: userVerification,
- });
- cipherService.encrypt.mockResolvedValue({} as unknown as Cipher);
- cipherService.createWithServer.mockImplementation(async (cipher) => {
- cipher.id = Utils.newGuid();
- return cipher;
- });
-
- await authenticator.makeCredential(params, new AbortController());
-
- expect(userInterfaceSession.confirmNewCredential).toHaveBeenCalledWith(
- {
- credentialName: params.rpEntity.name,
- userName: params.userEntity.displayName,
- userVerification,
- } as NewCredentialParams,
- expect.anything()
- );
- });
- }
-
- it("should save credential to vault if request confirmed by user", async () => {
- const encryptedCipher = {};
- userInterfaceSession.confirmNewCredential.mockResolvedValue({
- confirmed: true,
- userVerified: false,
- });
- cipherService.encrypt.mockResolvedValue(encryptedCipher as unknown as Cipher);
- cipherService.createWithServer.mockImplementation(async (cipher) => {
- cipher.id = Utils.newGuid();
- return cipher;
- });
-
- await authenticator.makeCredential(params);
-
- const saved = cipherService.encrypt.mock.lastCall?.[0];
- expect(saved).toEqual(
- expect.objectContaining({
- type: CipherType.Fido2Key,
- name: params.rpEntity.name,
-
- fido2Key: expect.objectContaining({
- credentialId: 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,
- userDisplayName: params.userEntity.displayName,
- }),
- })
- );
- expect(cipherService.createWithServer).toHaveBeenCalledWith(encryptedCipher);
- });
-
- /** Spec: If the user does not consent or if user verification fails, return an error code equivalent to "NotAllowedError" and terminate the operation. */
- it("should throw error if user denies creation request", async () => {
- userInterfaceSession.confirmNewCredential.mockResolvedValue({
- confirmed: false,
- userVerified: false,
- });
-
- const result = async () => await authenticator.makeCredential(params);
-
- await expect(result).rejects.toThrowError(Fido2AutenticatorErrorCode.NotAllowed);
- });
-
- /** Spec: If any error occurred while creating the new credential object, return an error code equivalent to "UnknownError" and terminate the operation. */
- it("should throw unkown error if creation fails", async () => {
- const encryptedCipher = {};
- userInterfaceSession.confirmNewCredential.mockResolvedValue({
- confirmed: true,
- userVerified: false,
- });
- cipherService.encrypt.mockResolvedValue(encryptedCipher as unknown as Cipher);
- cipherService.createWithServer.mockRejectedValue(new Error("Internal error"));
-
- const result = async () => await authenticator.makeCredential(params);
-
- await expect(result).rejects.toThrowError(Fido2AutenticatorErrorCode.Unknown);
- });
- });
-
- describe("creation of non-discoverable credential", () => {
+ describe("credential creation", () => {
let existingCipher: CipherView;
let params: Fido2AuthenticatorMakeCredentialsParams;
@@ -379,14 +276,14 @@ describe("FidoAuthenticatorService", () => {
for (const userVerification of [true, false]) {
it(`should request confirmation from user when user verification is ${userVerification}`, async () => {
params.requireUserVerification = userVerification;
- userInterfaceSession.confirmNewNonDiscoverableCredential.mockResolvedValue({
+ userInterfaceSession.confirmNewCredential.mockResolvedValue({
cipherId: existingCipher.id,
userVerified: userVerification,
});
await authenticator.makeCredential(params, new AbortController());
- expect(userInterfaceSession.confirmNewNonDiscoverableCredential).toHaveBeenCalledWith(
+ expect(userInterfaceSession.confirmNewCredential).toHaveBeenCalledWith(
{
credentialName: params.rpEntity.name,
userName: params.userEntity.displayName,
@@ -399,7 +296,7 @@ describe("FidoAuthenticatorService", () => {
it("should save credential to vault if request confirmed by user", async () => {
const encryptedCipher = Symbol();
- userInterfaceSession.confirmNewNonDiscoverableCredential.mockResolvedValue({
+ userInterfaceSession.confirmNewCredential.mockResolvedValue({
cipherId: existingCipher.id,
userVerified: false,
});
@@ -433,7 +330,7 @@ describe("FidoAuthenticatorService", () => {
/** Spec: If the user does not consent or if user verification fails, return an error code equivalent to "NotAllowedError" and terminate the operation. */
it("should throw error if user denies creation request", async () => {
- userInterfaceSession.confirmNewNonDiscoverableCredential.mockResolvedValue({
+ userInterfaceSession.confirmNewCredential.mockResolvedValue({
cipherId: undefined,
userVerified: false,
});
@@ -447,7 +344,7 @@ describe("FidoAuthenticatorService", () => {
/** Spec: If any error occurred while creating the new credential object, return an error code equivalent to "UnknownError" and terminate the operation. */
it("should throw unkown error if creation fails", async () => {
const encryptedCipher = Symbol();
- userInterfaceSession.confirmNewNonDiscoverableCredential.mockResolvedValue({
+ userInterfaceSession.confirmNewCredential.mockResolvedValue({
cipherId: existingCipher.id,
userVerified: false,
});
@@ -460,86 +357,74 @@ describe("FidoAuthenticatorService", () => {
});
});
- for (const requireResidentKey of [true, false]) {
- describe(`attestation of new ${
- requireResidentKey ? "discoverable" : "non-discoverable"
- } credential`, () => {
- const cipherId = "75280e7e-a72e-4d6c-bf1e-d37238352f9b";
- const credentialId = "52217b91-73f1-4fea-b3f2-54a7959fd5aa";
- const credentialIdBytes = new Uint8Array([
- 0x52, 0x21, 0x7b, 0x91, 0x73, 0xf1, 0x4f, 0xea, 0xb3, 0xf2, 0x54, 0xa7, 0x95, 0x9f, 0xd5,
- 0xaa,
- ]);
- let params: Fido2AuthenticatorMakeCredentialsParams;
+ describe(`attestation of new credential`, () => {
+ const cipherId = "75280e7e-a72e-4d6c-bf1e-d37238352f9b";
+ const credentialId = "52217b91-73f1-4fea-b3f2-54a7959fd5aa";
+ const credentialIdBytes = new Uint8Array([
+ 0x52, 0x21, 0x7b, 0x91, 0x73, 0xf1, 0x4f, 0xea, 0xb3, 0xf2, 0x54, 0xa7, 0x95, 0x9f, 0xd5,
+ 0xaa,
+ ]);
+ let params: Fido2AuthenticatorMakeCredentialsParams;
- beforeEach(async () => {
- const cipher = createCipherView({ id: cipherId, type: CipherType.Login });
- params = await createParams({ requireResidentKey });
- userInterfaceSession.confirmNewNonDiscoverableCredential.mockResolvedValue({
- cipherId,
- userVerified: false,
- });
- userInterfaceSession.confirmNewCredential.mockResolvedValue({
- confirmed: true,
- userVerified: false,
- });
- cipherService.get.mockImplementation(async (cipherId) =>
- cipherId === cipher.id ? ({ decrypt: () => cipher } as any) : undefined
- );
- cipherService.getAllDecrypted.mockResolvedValue([await cipher]);
- cipherService.encrypt.mockImplementation(async (cipher) => {
- if (!requireResidentKey) {
- cipher.login.fido2Key.credentialId = credentialId; // Replace id for testability
- } else {
- cipher.fido2Key.credentialId = credentialId;
- }
- return {} as any;
- });
- cipherService.createWithServer.mockImplementation(async (cipher) => {
- cipher.id = cipherId;
- return cipher;
- });
- cipherService.updateWithServer.mockImplementation(async (cipher) => {
- cipher.id = cipherId;
- return cipher;
- });
+ beforeEach(async () => {
+ const cipher = createCipherView({ id: cipherId, type: CipherType.Login });
+ params = await createParams();
+ userInterfaceSession.confirmNewCredential.mockResolvedValue({
+ cipherId,
+ userVerified: false,
});
-
- it("should return attestation object", async () => {
- const result = await authenticator.makeCredential(params);
-
- const attestationObject = CBOR.decode(
- Fido2Utils.bufferSourceToUint8Array(result.attestationObject).buffer
- );
-
- const encAuthData: Uint8Array = attestationObject.authData;
- const rpIdHash = encAuthData.slice(0, 32);
- const flags = encAuthData.slice(32, 33);
- const counter = encAuthData.slice(33, 37);
- const aaguid = encAuthData.slice(37, 53);
- const credentialIdLength = encAuthData.slice(53, 55);
- const credentialId = encAuthData.slice(55, 71);
- // Unsure how to test public key
- // const publicKey = encAuthData.slice(87);
-
- expect(encAuthData.length).toBe(71 + 77);
- expect(attestationObject.fmt).toBe("none");
- expect(attestationObject.attStmt).toEqual({});
- expect(rpIdHash).toEqual(
- new Uint8Array([
- 0x22, 0x6b, 0xb3, 0x92, 0x02, 0xff, 0xf9, 0x22, 0xdc, 0x74, 0x05, 0xcd, 0x28, 0xa8,
- 0x34, 0x5a, 0xc4, 0xf2, 0x64, 0x51, 0xd7, 0x3d, 0x0b, 0x40, 0xef, 0xf3, 0x1d, 0xc1,
- 0xd0, 0x5c, 0x3d, 0xc3,
- ])
- );
- expect(flags).toEqual(new Uint8Array([0b01000001])); // UP = true, AD = true
- expect(counter).toEqual(new Uint8Array([0, 0, 0, 0])); // 0 because of new counter
- expect(aaguid).toEqual(AAGUID);
- expect(credentialIdLength).toEqual(new Uint8Array([0, 16])); // 16 bytes because we're using GUIDs
- expect(credentialId).toEqual(credentialIdBytes);
+ cipherService.get.mockImplementation(async (cipherId) =>
+ cipherId === cipher.id ? ({ decrypt: () => cipher } as any) : undefined
+ );
+ cipherService.getAllDecrypted.mockResolvedValue([await cipher]);
+ cipherService.encrypt.mockImplementation(async (cipher) => {
+ cipher.login.fido2Key.credentialId = credentialId; // Replace id for testability
+ return {} as any;
+ });
+ cipherService.createWithServer.mockImplementation(async (cipher) => {
+ cipher.id = cipherId;
+ return cipher;
+ });
+ cipherService.updateWithServer.mockImplementation(async (cipher) => {
+ cipher.id = cipherId;
+ return cipher;
});
});
- }
+
+ it("should return attestation object", async () => {
+ const result = await authenticator.makeCredential(params);
+
+ const attestationObject = CBOR.decode(
+ Fido2Utils.bufferSourceToUint8Array(result.attestationObject).buffer
+ );
+
+ const encAuthData: Uint8Array = attestationObject.authData;
+ const rpIdHash = encAuthData.slice(0, 32);
+ const flags = encAuthData.slice(32, 33);
+ const counter = encAuthData.slice(33, 37);
+ const aaguid = encAuthData.slice(37, 53);
+ const credentialIdLength = encAuthData.slice(53, 55);
+ const credentialId = encAuthData.slice(55, 71);
+ // Unsure how to test public key
+ // const publicKey = encAuthData.slice(87);
+
+ expect(encAuthData.length).toBe(71 + 77);
+ expect(attestationObject.fmt).toBe("none");
+ expect(attestationObject.attStmt).toEqual({});
+ expect(rpIdHash).toEqual(
+ new Uint8Array([
+ 0x22, 0x6b, 0xb3, 0x92, 0x02, 0xff, 0xf9, 0x22, 0xdc, 0x74, 0x05, 0xcd, 0x28, 0xa8,
+ 0x34, 0x5a, 0xc4, 0xf2, 0x64, 0x51, 0xd7, 0x3d, 0x0b, 0x40, 0xef, 0xf3, 0x1d, 0xc1,
+ 0xd0, 0x5c, 0x3d, 0xc3,
+ ])
+ );
+ expect(flags).toEqual(new Uint8Array([0b01000001])); // UP = true, AD = true
+ expect(counter).toEqual(new Uint8Array([0, 0, 0, 0])); // 0 because of new counter
+ expect(aaguid).toEqual(AAGUID);
+ expect(credentialIdLength).toEqual(new Uint8Array([0, 16])); // 16 bytes because we're using GUIDs
+ expect(credentialId).toEqual(credentialIdBytes);
+ });
+ });
async function createParams(
params: Partial = {}
@@ -701,7 +586,7 @@ describe("FidoAuthenticatorService", () => {
{ credentialId: credentialIds[0], rpId: RpId }
),
await createCipherView(
- { type: CipherType.Fido2Key },
+ { type: CipherType.Login },
{ credentialId: credentialIds[1], rpId: RpId }
),
];
@@ -746,157 +631,128 @@ describe("FidoAuthenticatorService", () => {
});
});
- for (const residentKey of [true, false]) {
- describe(`assertion of ${
- residentKey ? "discoverable" : "non-discoverable"
- } credential`, () => {
- let keyPair: CryptoKeyPair;
- let credentialIds: string[];
- let selectedCredentialId: string;
- let ciphers: CipherView[];
- let fido2Keys: Fido2KeyView[];
- let params: Fido2AuthenticatorGetAssertionParams;
+ describe("assertion of credential", () => {
+ let keyPair: CryptoKeyPair;
+ let credentialIds: string[];
+ let selectedCredentialId: string;
+ let ciphers: CipherView[];
+ let fido2Keys: Fido2KeyView[];
+ let params: Fido2AuthenticatorGetAssertionParams;
- const init = async () => {
- keyPair = await createKeyPair();
- credentialIds = [Utils.newGuid(), Utils.newGuid()];
- const keyValue = Fido2Utils.bufferToString(
- await crypto.subtle.exportKey("pkcs8", keyPair.privateKey)
- );
- if (residentKey) {
- ciphers = credentialIds.map((id) =>
- createCipherView(
- { type: CipherType.Fido2Key },
- { credentialId: id, rpId: RpId, counter: 9000, keyValue }
- )
- );
- fido2Keys = ciphers.map((c) => c.fido2Key);
- selectedCredentialId = credentialIds[0];
- params = await createParams({
- allowCredentialDescriptorList: undefined,
- rpId: RpId,
- });
- } else {
- ciphers = credentialIds.map((id) =>
- createCipherView(
- { type: CipherType.Login },
- { credentialId: id, rpId: RpId, counter: 9000 }
- )
- );
- fido2Keys = ciphers.map((c) => c.login.fido2Key);
- selectedCredentialId = credentialIds[0];
- params = await createParams({
- allowCredentialDescriptorList: credentialIds.map((credentialId) => ({
- id: guidToRawFormat(credentialId),
- type: "public-key",
- })),
- rpId: RpId,
- });
- }
- cipherService.getAllDecrypted.mockResolvedValue(ciphers);
- userInterfaceSession.pickCredential.mockResolvedValue({
- cipherId: ciphers[0].id,
- userVerified: false,
- });
- };
- beforeEach(init);
-
- /** Spec: Increment the credential associated signature counter */
- it("should increment counter", async () => {
- const encrypted = Symbol();
- cipherService.encrypt.mockResolvedValue(encrypted as any);
-
- await authenticator.getAssertion(params);
-
- 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,
- }),
- }),
- })
- );
- }
+ const init = async () => {
+ keyPair = await createKeyPair();
+ credentialIds = [Utils.newGuid(), Utils.newGuid()];
+ const keyValue = Fido2Utils.bufferToString(
+ await crypto.subtle.exportKey("pkcs8", keyPair.privateKey)
+ );
+ ciphers = credentialIds.map((id) =>
+ createCipherView(
+ { type: CipherType.Login },
+ { credentialId: id, rpId: RpId, counter: 9000, keyValue }
+ )
+ );
+ fido2Keys = ciphers.map((c) => c.login.fido2Key);
+ selectedCredentialId = credentialIds[0];
+ params = await createParams({
+ allowCredentialDescriptorList: credentialIds.map((credentialId) => ({
+ id: guidToRawFormat(credentialId),
+ type: "public-key",
+ })),
+ rpId: RpId,
});
+ cipherService.getAllDecrypted.mockResolvedValue(ciphers);
+ userInterfaceSession.pickCredential.mockResolvedValue({
+ cipherId: ciphers[0].id,
+ userVerified: false,
+ });
+ };
+ beforeEach(init);
- it("should return an assertion result", async () => {
+ /** Spec: Increment the credential associated signature counter */
+ it("should increment counter", async () => {
+ const encrypted = Symbol();
+ cipherService.encrypt.mockResolvedValue(encrypted as any);
+
+ await authenticator.getAssertion(params);
+
+ expect(cipherService.updateWithServer).toHaveBeenCalledWith(encrypted);
+
+ 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 () => {
+ const result = await authenticator.getAssertion(params);
+
+ const encAuthData = result.authenticatorData;
+ const rpIdHash = encAuthData.slice(0, 32);
+ const flags = encAuthData.slice(32, 33);
+ const counter = encAuthData.slice(33, 37);
+
+ expect(result.selectedCredential.id).toEqual(guidToRawFormat(selectedCredentialId));
+ expect(result.selectedCredential.userHandle).toEqual(
+ Fido2Utils.stringToBuffer(fido2Keys[0].userHandle)
+ );
+ expect(rpIdHash).toEqual(
+ new Uint8Array([
+ 0x22, 0x6b, 0xb3, 0x92, 0x02, 0xff, 0xf9, 0x22, 0xdc, 0x74, 0x05, 0xcd, 0x28, 0xa8,
+ 0x34, 0x5a, 0xc4, 0xf2, 0x64, 0x51, 0xd7, 0x3d, 0x0b, 0x40, 0xef, 0xf3, 0x1d, 0xc1,
+ 0xd0, 0x5c, 0x3d, 0xc3,
+ ])
+ );
+ expect(flags).toEqual(new Uint8Array([0b00000001])); // UP = true
+ expect(counter).toEqual(new Uint8Array([0, 0, 0x23, 0x29])); // 9001 in hex
+
+ // Verify signature
+ // TODO: Cannot verify signature because it has been converted into DER format
+ // const sigBase = new Uint8Array([
+ // ...result.authenticatorData,
+ // ...Fido2Utils.bufferSourceToUint8Array(params.hash),
+ // ]);
+ // const isValidSignature = await crypto.subtle.verify(
+ // { name: "ECDSA", hash: { name: "SHA-256" } },
+ // keyPair.publicKey,
+ // result.signature,
+ // sigBase
+ // );
+ // expect(isValidSignature).toBe(true);
+ });
+
+ it("should always generate unique signatures even if the input is the same", async () => {
+ const signatures = new Set();
+
+ for (let i = 0; i < 10; ++i) {
+ await init(); // Reset inputs
const result = await authenticator.getAssertion(params);
- const encAuthData = result.authenticatorData;
- const rpIdHash = encAuthData.slice(0, 32);
- const flags = encAuthData.slice(32, 33);
- const counter = encAuthData.slice(33, 37);
+ const counter = result.authenticatorData.slice(33, 37);
+ expect(counter).toEqual(new Uint8Array([0, 0, 0x23, 0x29])); // double check that the counter doesn't change
- expect(result.selectedCredential.id).toEqual(guidToRawFormat(selectedCredentialId));
- expect(result.selectedCredential.userHandle).toEqual(
- Fido2Utils.stringToBuffer(fido2Keys[0].userHandle)
- );
- expect(rpIdHash).toEqual(
- new Uint8Array([
- 0x22, 0x6b, 0xb3, 0x92, 0x02, 0xff, 0xf9, 0x22, 0xdc, 0x74, 0x05, 0xcd, 0x28, 0xa8,
- 0x34, 0x5a, 0xc4, 0xf2, 0x64, 0x51, 0xd7, 0x3d, 0x0b, 0x40, 0xef, 0xf3, 0x1d, 0xc1,
- 0xd0, 0x5c, 0x3d, 0xc3,
- ])
- );
- expect(flags).toEqual(new Uint8Array([0b00000001])); // UP = true
- expect(counter).toEqual(new Uint8Array([0, 0, 0x23, 0x29])); // 9001 in hex
-
- // Verify signature
- // TODO: Cannot verify signature because it has been converted into DER format
- // const sigBase = new Uint8Array([
- // ...result.authenticatorData,
- // ...Fido2Utils.bufferSourceToUint8Array(params.hash),
- // ]);
- // const isValidSignature = await crypto.subtle.verify(
- // { name: "ECDSA", hash: { name: "SHA-256" } },
- // keyPair.publicKey,
- // result.signature,
- // sigBase
- // );
- // expect(isValidSignature).toBe(true);
- });
-
- it("should always generate unique signatures even if the input is the same", async () => {
- const signatures = new Set();
-
- for (let i = 0; i < 10; ++i) {
- await init(); // Reset inputs
- const result = await authenticator.getAssertion(params);
-
- const counter = result.authenticatorData.slice(33, 37);
- expect(counter).toEqual(new Uint8Array([0, 0, 0x23, 0x29])); // double check that the counter doesn't change
-
- const signature = Fido2Utils.bufferToString(result.signature);
- if (signatures.has(signature)) {
- throw new Error("Found duplicate signature");
- }
- signatures.add(signature);
+ const signature = Fido2Utils.bufferToString(result.signature);
+ if (signatures.has(signature)) {
+ throw new Error("Found duplicate signature");
}
- });
-
- /** Spec: If any error occurred while generating the assertion signature, return an error code equivalent to "UnknownError" and terminate the operation. */
- it("should throw unkown error if creation fails", async () => {
- cipherService.updateWithServer.mockRejectedValue(new Error("Internal error"));
-
- const result = async () => await authenticator.getAssertion(params);
-
- await expect(result).rejects.toThrowError(Fido2AutenticatorErrorCode.Unknown);
- });
+ signatures.add(signature);
+ }
});
- }
+
+ /** Spec: If any error occurred while generating the assertion signature, return an error code equivalent to "UnknownError" and terminate the operation. */
+ it("should throw unkown error if creation fails", async () => {
+ cipherService.updateWithServer.mockRejectedValue(new Error("Internal error"));
+
+ const result = async () => await authenticator.getAssertion(params);
+
+ await expect(result).rejects.toThrowError(Fido2AutenticatorErrorCode.Unknown);
+ });
+ });
async function createParams(
params: Partial = {}
diff --git a/libs/common/src/vault/services/fido2/fido2-authenticator.service.ts b/libs/common/src/vault/services/fido2/fido2-authenticator.service.ts
index 65802f2d28d..b945fcc972c 100644
--- a/libs/common/src/vault/services/fido2/fido2-authenticator.service.ts
+++ b/libs/common/src/vault/services/fido2/fido2-authenticator.service.ts
@@ -97,90 +97,45 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
let keyPair: CryptoKeyPair;
let userVerified = false;
let credentialId: string;
- if (params.requireResidentKey) {
- const response = await userInterfaceSession.confirmNewCredential(
- {
- credentialName: params.rpEntity.name,
- userName: params.userEntity.displayName,
- userVerification: params.requireUserVerification,
- },
- abortController
+ const response = await userInterfaceSession.confirmNewCredential(
+ {
+ credentialName: params.rpEntity.name,
+ userName: params.userEntity.displayName,
+ userVerification: params.requireUserVerification,
+ },
+ abortController
+ );
+ const cipherId = response.cipherId;
+ userVerified = response.userVerified;
+
+ if (cipherId === undefined) {
+ this.logService?.warning(
+ `[Fido2Authenticator] Aborting because user confirmation was not recieved.`
);
- const userConfirmation = response.confirmed;
- userVerified = response.userVerified;
+ throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotAllowed);
+ }
- if (!userConfirmation) {
- this.logService?.warning(
- `[Fido2Authenticator] Aborting because user confirmation was not recieved.`
- );
- throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotAllowed);
- }
-
- if (params.requireUserVerification && !userVerified) {
- this.logService?.warning(
- `[Fido2Authenticator] Aborting because user verification was not successful.`
- );
- throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotAllowed);
- }
-
- try {
- keyPair = await createKeyPair();
-
- cipher = new CipherView();
- cipher.type = CipherType.Fido2Key;
- cipher.name = params.rpEntity.name;
- 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;
- credentialId = cipher.fido2Key.credentialId;
- } catch (error) {
- this.logService?.error(
- `[Fido2Authenticator] Aborting because of unknown error when creating discoverable credential: ${error}`
- );
- throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.Unknown);
- }
- } else {
- const response = await userInterfaceSession.confirmNewNonDiscoverableCredential(
- {
- credentialName: params.rpEntity.name,
- userName: params.userEntity.displayName,
- userVerification: params.requireUserVerification,
- },
- abortController
+ if (params.requireUserVerification && !userVerified) {
+ this.logService?.warning(
+ `[Fido2Authenticator] Aborting because user verification was unsuccessful.`
);
- const cipherId = response.cipherId;
- userVerified = response.userVerified;
+ throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotAllowed);
+ }
- if (cipherId === undefined) {
- this.logService?.warning(
- `[Fido2Authenticator] Aborting because user confirmation was not recieved.`
- );
- throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotAllowed);
- }
+ try {
+ keyPair = await createKeyPair();
- if (params.requireUserVerification && !userVerified) {
- this.logService?.warning(
- `[Fido2Authenticator] Aborting because user verification was unsuccessful.`
- );
- throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotAllowed);
- }
-
- try {
- keyPair = await createKeyPair();
-
- const encrypted = await this.cipherService.get(cipherId);
- cipher = await encrypted.decrypt();
- cipher.login.fido2Key = fido2Key = await createKeyView(params, keyPair.privateKey);
- const reencrypted = await this.cipherService.encrypt(cipher);
- await this.cipherService.updateWithServer(reencrypted);
- credentialId = cipher.login.fido2Key.credentialId;
- } catch (error) {
- this.logService?.error(
- `[Fido2Authenticator] Aborting because of unknown error when creating non-discoverable credential: ${error}`
- );
- throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.Unknown);
- }
+ const encrypted = await this.cipherService.get(cipherId);
+ cipher = await encrypted.decrypt();
+ cipher.login.fido2Key = fido2Key = await createKeyView(params, keyPair.privateKey);
+ const reencrypted = await this.cipherService.encrypt(cipher);
+ await this.cipherService.updateWithServer(reencrypted);
+ credentialId = cipher.login.fido2Key.credentialId;
+ } catch (error) {
+ this.logService?.error(
+ `[Fido2Authenticator] Aborting because of unknown error when creating credential: ${error}`
+ );
+ throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.Unknown);
}
const authData = await generateAuthData({
@@ -374,14 +329,11 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
const ciphers = await this.cipherService.getAllDecrypted();
return ciphers.filter(
(cipher) =>
- (!cipher.isDeleted &&
- cipher.type === CipherType.Login &&
- cipher.login.fido2Key != undefined &&
- cipher.login.fido2Key.rpId === rpId &&
- ids.includes(cipher.login.fido2Key.credentialId)) ||
- (cipher.type === CipherType.Fido2Key &&
- cipher.fido2Key.rpId === rpId &&
- ids.includes(cipher.fido2Key.credentialId))
+ !cipher.isDeleted &&
+ cipher.type === CipherType.Login &&
+ cipher.login.fido2Key != undefined &&
+ cipher.login.fido2Key.rpId === rpId &&
+ ids.includes(cipher.login.fido2Key.credentialId)
);
}
@@ -389,7 +341,10 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
const ciphers = await this.cipherService.getAllDecrypted();
return ciphers.filter(
(cipher) =>
- !cipher.isDeleted && cipher.type === CipherType.Fido2Key && cipher.fido2Key.rpId === rpId
+ !cipher.isDeleted &&
+ cipher.type === CipherType.Login &&
+ cipher.login.fido2Key != undefined &&
+ cipher.login.fido2Key.rpId === rpId
);
}
}