mirror of
https://github.com/bitwarden/browser.git
synced 2025-01-31 22:51:28 +01:00
[EC-598] feat: add support for non-discoverable credentials
This commit is contained in:
parent
f49822989c
commit
6bf680cacc
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user