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

[EC-598] feat: implement assertion

This commit is contained in:
Andreas Coroiu 2023-03-30 09:12:54 +02:00
parent 5bf4156fc6
commit 151afeb241
No known key found for this signature in database
GPG Key ID: E70B5FFC81DFEC1A
3 changed files with 131 additions and 66 deletions

View File

@ -98,9 +98,9 @@ export interface Fido2AuthenticatorGetAssertionParams {
}
export interface Fido2AuthenticatorGetAssertionResult {
selectedCredential?: {
selectedCredential: {
id: string;
userHandle: Uint8Array;
userHandle?: Uint8Array;
};
authenticatorData: Uint8Array;
signature: Uint8Array;

View File

@ -629,21 +629,34 @@ describe("FidoAuthenticatorService", () => {
});
});
describe("assertion of non-discoverable credential", () => {
for (const residentKey of [true, false]) {
describe(`assertion of ${
residentKey ? "discoverable" : "non-discoverable"
} credential`, () => {
let credentialIds: string[];
let selectedCredentialId: string;
let ciphers: CipherView[];
let params: Fido2AuthenticatorGetAssertionParams;
beforeEach(async () => {
credentialIds = [Utils.newGuid(), Utils.newGuid()];
ciphers = await Promise.all(
credentialIds.map((id) =>
if (residentKey) {
ciphers = credentialIds.map((id) =>
createCipherView({ type: CipherType.Fido2Key }, { rpId: RpId, counter: 9000 })
);
selectedCredentialId = ciphers[0].id;
params = await createParams({
allowCredentialDescriptorList: undefined,
rpId: RpId,
});
} else {
ciphers = credentialIds.map((id) =>
createCipherView(
{ type: CipherType.Login },
{ nonDiscoverableId: id, rpId: RpId, counter: 9000 }
)
)
);
selectedCredentialId = credentialIds[0];
params = await createParams({
allowCredentialDescriptorList: credentialIds.map((credentialId) => ({
id: Utils.guidToRawFormat(credentialId),
@ -651,6 +664,7 @@ describe("FidoAuthenticatorService", () => {
})),
rpId: RpId,
});
}
cipherService.getAllDecrypted.mockResolvedValue(ciphers);
userInterface.pickCredential.mockResolvedValue(ciphers[0].id);
});
@ -672,7 +686,28 @@ describe("FidoAuthenticatorService", () => {
);
expect(cipherService.updateWithServer).toHaveBeenCalledWith(encrypted);
});
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).toBe(selectedCredentialId);
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
});
});
}
async function createParams(
params: Partial<Fido2AuthenticatorGetAssertionParams> = {}
@ -713,6 +748,7 @@ function createCipherView(
cipher.fido2Key.nonDiscoverableId = fido2Key.nonDiscoverableId;
cipher.fido2Key.rpId = fido2Key.rpId ?? RpId;
cipher.fido2Key.counter = fido2Key.counter ?? 0;
cipher.fido2Key.userHandle = Fido2Utils.bufferToString(randomBytes(16));
return cipher;
}
@ -729,6 +765,7 @@ async function createClientDataHash() {
return await crypto.subtle.digest({ name: "SHA-256" }, clientData);
}
/** This is a fake function that always returns the same byte sequence */
function randomBytes(length: number) {
return new Uint8Array(Array.from({ length }, () => Math.floor(Math.random() * 255)));
return new Uint8Array(Array.from({ length }, (_, k) => k % 255));
}

View File

@ -146,35 +146,63 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.Constraint);
}
let credentialOptions: CipherView[];
let cipherOptions: CipherView[];
// eslint-disable-next-line no-empty
if (params.allowCredentialDescriptorList?.length > 0) {
credentialOptions = await this.findNonDiscoverableCredentials(
cipherOptions = await this.findNonDiscoverableCredentials(
params.allowCredentialDescriptorList,
params.rpId
);
} else {
credentialOptions = await this.findDiscoverableCredentials(params.rpId);
cipherOptions = await this.findDiscoverableCredentials(params.rpId);
}
if (credentialOptions.length === 0) {
if (cipherOptions.length === 0) {
throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotAllowed);
}
const selectedCredentialId = await this.userInterface.pickCredential(
credentialOptions.map((cipher) => cipher.id)
const selectedCipherId = await this.userInterface.pickCredential(
cipherOptions.map((cipher) => cipher.id)
);
const selectedCredential = credentialOptions.find((c) => c.id === selectedCredentialId);
const selectedCipher = cipherOptions.find((c) => c.id === selectedCipherId);
if (selectedCredential === undefined) {
if (selectedCipher === undefined) {
throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotAllowed);
}
++selectedCredential.fido2Key.counter;
selectedCredential.localData.lastUsedDate = new Date().getTime();
const encrypted = await this.cipherService.encrypt(selectedCredential);
const selectedCredentialId =
params.allowCredentialDescriptorList?.length > 0
? selectedCipher.fido2Key.nonDiscoverableId
: selectedCipher.id;
++selectedCipher.fido2Key.counter;
selectedCipher.localData.lastUsedDate = new Date().getTime();
const encrypted = await this.cipherService.encrypt(selectedCipher);
await this.cipherService.updateWithServer(encrypted);
const authenticatorData = await generateAuthData({
rpId: selectedCipher.fido2Key.rpId,
credentialId: selectedCredentialId,
counter: selectedCipher.fido2Key.counter,
userPresence: true,
userVerification: false,
});
// const signature = await generateSignature({
// authData,
// clientData,
// privateKey: credential.keyValue,
// });
return {
authenticatorData,
selectedCredential: {
id: selectedCredentialId,
userHandle: Fido2Utils.stringToBuffer(selectedCipher.fido2Key.userHandle),
},
signature: null,
};
}
private async vaultContainsCredentials(
@ -305,6 +333,7 @@ async function generateAuthData(params: AuthDataParams) {
counter & 0x000000ff
);
if (params.keyPair) {
// attestedCredentialData
const attestedCredentialData: Array<number> = [];
@ -316,7 +345,6 @@ async function generateAuthData(params: AuthDataParams) {
attestedCredentialData.push(...credentialIdLength);
attestedCredentialData.push(...rawId);
if (params.keyPair) {
const publicKeyJwk = await crypto.subtle.exportKey("jwk", params.keyPair.publicKey);
// COSE format of the EC256 key
const keyX = Utils.fromUrlB64ToArray(publicKeyJwk.x);