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:
parent
f49822989c
commit
6bf680cacc
@ -10,6 +10,10 @@ export abstract class Fido2UserInterfaceService {
|
|||||||
params: NewCredentialParams,
|
params: NewCredentialParams,
|
||||||
abortController?: AbortController
|
abortController?: AbortController
|
||||||
) => Promise<boolean>;
|
) => Promise<boolean>;
|
||||||
|
confirmNewNonDiscoverableCredential: (
|
||||||
|
params: NewCredentialParams,
|
||||||
|
abortController?: AbortController
|
||||||
|
) => Promise<string | undefined>;
|
||||||
informExcludedCredential: (
|
informExcludedCredential: (
|
||||||
existingCipherIds: string[],
|
existingCipherIds: string[],
|
||||||
newCredential: NewCredentialParams,
|
newCredential: NewCredentialParams,
|
||||||
|
@ -6,6 +6,7 @@ import { Utils } from "../../misc/utils";
|
|||||||
import { CipherService } from "../../vault/abstractions/cipher.service";
|
import { CipherService } from "../../vault/abstractions/cipher.service";
|
||||||
import { CipherType } from "../../vault/enums/cipher-type";
|
import { CipherType } from "../../vault/enums/cipher-type";
|
||||||
import { Cipher } from "../../vault/models/domain/cipher";
|
import { Cipher } from "../../vault/models/domain/cipher";
|
||||||
|
import { Login } from "../../vault/models/domain/login";
|
||||||
import { CipherView } from "../../vault/models/view/cipher.view";
|
import { CipherView } from "../../vault/models/view/cipher.view";
|
||||||
import {
|
import {
|
||||||
Fido2AutenticatorErrorCode,
|
Fido2AutenticatorErrorCode,
|
||||||
@ -153,10 +154,15 @@ describe("FidoAuthenticatorService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("creation of discoverable credential", () => {
|
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. */
|
/** Spec: show the items contained within the user and rp parameter structures to the user. */
|
||||||
it("should request confirmation from user", async () => {
|
it("should request confirmation from user", async () => {
|
||||||
userInterface.confirmNewCredential.mockResolvedValue(true);
|
userInterface.confirmNewCredential.mockResolvedValue(true);
|
||||||
const params = await createCredentialParams();
|
|
||||||
|
|
||||||
await authenticator.makeCredential(params);
|
await authenticator.makeCredential(params);
|
||||||
|
|
||||||
@ -170,7 +176,6 @@ describe("FidoAuthenticatorService", () => {
|
|||||||
const encryptedCipher = Symbol();
|
const encryptedCipher = Symbol();
|
||||||
userInterface.confirmNewCredential.mockResolvedValue(true);
|
userInterface.confirmNewCredential.mockResolvedValue(true);
|
||||||
cipherService.encrypt.mockResolvedValue(encryptedCipher as unknown as Cipher);
|
cipherService.encrypt.mockResolvedValue(encryptedCipher as unknown as Cipher);
|
||||||
const params = await createCredentialParams({ options: { rk: true } });
|
|
||||||
|
|
||||||
await authenticator.makeCredential(params);
|
await authenticator.makeCredential(params);
|
||||||
|
|
||||||
@ -196,6 +201,72 @@ describe("FidoAuthenticatorService", () => {
|
|||||||
/** Spec: If the user declines permission, return the CTAP2_ERR_OPERATION_DENIED error. */
|
/** Spec: If the user declines permission, return the CTAP2_ERR_OPERATION_DENIED error. */
|
||||||
it("should throw error if user denies creation request", async () => {
|
it("should throw error if user denies creation request", async () => {
|
||||||
userInterface.confirmNewCredential.mockResolvedValue(false);
|
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 params = await createCredentialParams();
|
||||||
|
|
||||||
const result = async () => await authenticator.makeCredential(params);
|
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();
|
const cipher = new Cipher();
|
||||||
cipher.id = id;
|
cipher.id = data.id ?? Utils.newGuid();
|
||||||
cipher.type = CipherType.Fido2Key;
|
cipher.type = data.type ?? CipherType.Fido2Key;
|
||||||
cipher.fido2Key = new Fido2Key();
|
cipher.fido2Key = data.fido2Key ?? new Fido2Key();
|
||||||
return cipher;
|
return cipher;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,6 +57,7 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
|
|||||||
throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.CTAP2_ERR_CREDENTIAL_EXCLUDED);
|
throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.CTAP2_ERR_CREDENTIAL_EXCLUDED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (params.options?.rk) {
|
||||||
const userVerification = await this.userInterface.confirmNewCredential({
|
const userVerification = await this.userInterface.confirmNewCredential({
|
||||||
credentialName: params.rp.name,
|
credentialName: params.rp.name,
|
||||||
userName: params.user.name,
|
userName: params.user.name,
|
||||||
@ -67,10 +68,31 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
|
|||||||
}
|
}
|
||||||
|
|
||||||
const keyPair = await this.createKeyPair();
|
const keyPair = await this.createKeyPair();
|
||||||
const vaultItem = await this.createVaultItem(params, keyPair.privateKey);
|
|
||||||
|
|
||||||
const encrypted = await this.cipherService.encrypt(vaultItem);
|
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);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async vaultContainsId(ids: string[]): Promise<boolean> {
|
private async vaultContainsId(ids: string[]): Promise<boolean> {
|
||||||
@ -94,25 +116,21 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createVaultItem(
|
private async createKeyView(
|
||||||
params: Fido2AuthenticatorMakeCredentialsParams,
|
params: Fido2AuthenticatorMakeCredentialsParams,
|
||||||
keyValue: CryptoKey
|
keyValue: CryptoKey
|
||||||
): Promise<CipherView> {
|
): Promise<Fido2KeyView> {
|
||||||
const pcks8Key = await crypto.subtle.exportKey("pkcs8", keyValue);
|
const pcks8Key = await crypto.subtle.exportKey("pkcs8", keyValue);
|
||||||
|
|
||||||
const view = new CipherView();
|
const fido2Key = new Fido2KeyView();
|
||||||
view.type = CipherType.Fido2Key;
|
fido2Key.keyType = "ECDSA";
|
||||||
view.name = params.rp.name;
|
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();
|
return fido2Key;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user