1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-02-01 23:01:28 +01:00

[EC-598] feat: add support for non-discoverable credentials

This commit is contained in:
Andreas Coroiu 2023-03-23 10:47:58 +01:00
parent f49822989c
commit 6bf680cacc
No known key found for this signature in database
GPG Key ID: E70B5FFC81DFEC1A
3 changed files with 126 additions and 33 deletions

View File

@ -10,6 +10,10 @@ export abstract class Fido2UserInterfaceService {
params: NewCredentialParams,
abortController?: AbortController
) => Promise<boolean>;
confirmNewNonDiscoverableCredential: (
params: NewCredentialParams,
abortController?: AbortController
) => Promise<string | undefined>;
informExcludedCredential: (
existingCipherIds: string[],
newCredential: NewCredentialParams,

View File

@ -6,6 +6,7 @@ import { Utils } from "../../misc/utils";
import { CipherService } from "../../vault/abstractions/cipher.service";
import { CipherType } from "../../vault/enums/cipher-type";
import { Cipher } from "../../vault/models/domain/cipher";
import { Login } from "../../vault/models/domain/login";
import { CipherView } from "../../vault/models/view/cipher.view";
import {
Fido2AutenticatorErrorCode,
@ -153,10 +154,15 @@ describe("FidoAuthenticatorService", () => {
});
describe("creation of discoverable credential", () => {
let params: Fido2AuthenticatorMakeCredentialsParams;
beforeEach(async () => {
params = await createCredentialParams({ options: { rk: true } });
});
/** Spec: show the items contained within the user and rp parameter structures to the user. */
it("should request confirmation from user", async () => {
userInterface.confirmNewCredential.mockResolvedValue(true);
const params = await createCredentialParams();
await authenticator.makeCredential(params);
@ -170,7 +176,6 @@ describe("FidoAuthenticatorService", () => {
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);
@ -196,6 +201,72 @@ describe("FidoAuthenticatorService", () => {
/** 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 result = async () => await authenticator.makeCredential(params);
await expect(result).rejects.toThrowError(
Fido2AutenticatorErrorCode[Fido2AutenticatorErrorCode.CTAP2_ERR_OPERATION_DENIED]
);
});
});
describe("creation of non-discoverable credential", () => {
let existingCipherView: CipherView;
let params: Fido2AuthenticatorMakeCredentialsParams;
beforeEach(async () => {
const existingCipher = createCipher({ type: CipherType.Login });
existingCipher.login = new Login();
existingCipher.fido2Key = undefined;
existingCipherView = await existingCipher.decrypt();
params = await createCredentialParams();
cipherService.get.mockImplementation(async (id) =>
id === existingCipher.id ? existingCipher : undefined
);
cipherService.getAllDecrypted.mockResolvedValue([existingCipherView]);
});
/** Spec: show the items contained within the user and rp parameter structures to the user. */
it("should request confirmation from user", async () => {
userInterface.confirmNewNonDiscoverableCredential.mockResolvedValue(existingCipherView.id);
await authenticator.makeCredential(params);
expect(userInterface.confirmNewNonDiscoverableCredential).toHaveBeenCalledWith({
credentialName: params.rp.name,
userName: params.user.name,
} as NewCredentialParams);
});
it("should save credential to vault if request confirmed by user", async () => {
const encryptedCipher = Symbol();
userInterface.confirmNewNonDiscoverableCredential.mockResolvedValue(existingCipherView.id);
cipherService.encrypt.mockResolvedValue(encryptedCipher as unknown as Cipher);
await authenticator.makeCredential(params);
const saved = cipherService.encrypt.mock.lastCall?.[0];
expect(saved).toEqual(
expect.objectContaining({
type: CipherType.Login,
name: existingCipherView.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.updateWithServer).toHaveBeenCalledWith(encryptedCipher);
});
/** Spec: If the user declines permission, return the CTAP2_ERR_OPERATION_DENIED error. */
it("should throw error if user denies creation request", async () => {
userInterface.confirmNewNonDiscoverableCredential.mockResolvedValue(undefined);
const params = await createCredentialParams();
const result = async () => await authenticator.makeCredential(params);
@ -262,11 +333,11 @@ async function createInvalidParams() {
};
}
function createCipher(id = Utils.newGuid()): Cipher {
function createCipher(data: Partial<Cipher> = {}): Cipher {
const cipher = new Cipher();
cipher.id = id;
cipher.type = CipherType.Fido2Key;
cipher.fido2Key = new Fido2Key();
cipher.id = data.id ?? Utils.newGuid();
cipher.type = data.type ?? CipherType.Fido2Key;
cipher.fido2Key = data.fido2Key ?? new Fido2Key();
return cipher;
}

View File

@ -57,20 +57,42 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.CTAP2_ERR_CREDENTIAL_EXCLUDED);
}
const userVerification = await this.userInterface.confirmNewCredential({
credentialName: params.rp.name,
userName: params.user.name,
});
if (params.options?.rk) {
const userVerification = await this.userInterface.confirmNewCredential({
credentialName: params.rp.name,
userName: params.user.name,
});
if (!userVerification) {
throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.CTAP2_ERR_OPERATION_DENIED);
if (!userVerification) {
throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.CTAP2_ERR_OPERATION_DENIED);
}
const keyPair = await this.createKeyPair();
const cipher = new CipherView();
cipher.type = CipherType.Fido2Key;
cipher.name = params.rp.name;
cipher.fido2Key = await this.createKeyView(params, keyPair.privateKey);
const encrypted = await this.cipherService.encrypt(cipher);
await this.cipherService.createWithServer(encrypted);
} else {
const cipherId = await this.userInterface.confirmNewNonDiscoverableCredential({
credentialName: params.rp.name,
userName: params.user.name,
});
if (cipherId === undefined) {
throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.CTAP2_ERR_OPERATION_DENIED);
}
const keyPair = await this.createKeyPair();
const encrypted = await this.cipherService.get(cipherId);
const cipher = await encrypted.decrypt();
cipher.fido2Key = await this.createKeyView(params, keyPair.privateKey);
const reencrypted = await this.cipherService.encrypt(cipher);
await this.cipherService.updateWithServer(reencrypted);
}
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<boolean> {
@ -94,25 +116,21 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
);
}
private async createVaultItem(
private async createKeyView(
params: Fido2AuthenticatorMakeCredentialsParams,
keyValue: CryptoKey
): Promise<CipherView> {
): Promise<Fido2KeyView> {
const pcks8Key = await crypto.subtle.exportKey("pkcs8", keyValue);
const view = new CipherView();
view.type = CipherType.Fido2Key;
view.name = params.rp.name;
const fido2Key = new Fido2KeyView();
fido2Key.keyType = "ECDSA";
fido2Key.keyCurve = "P-256";
fido2Key.keyValue = Fido2Utils.bufferToString(pcks8Key);
fido2Key.rpId = params.rp.id;
fido2Key.rpName = params.rp.name;
fido2Key.userHandle = Fido2Utils.bufferToString(params.user.id);
fido2Key.userName = params.user.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;
return fido2Key;
}
}