mirror of
https://github.com/bitwarden/browser
synced 2025-12-16 16:23:44 +00:00
[EC-598] feat: implement assertion
This commit is contained in:
@@ -98,9 +98,9 @@ export interface Fido2AuthenticatorGetAssertionParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Fido2AuthenticatorGetAssertionResult {
|
export interface Fido2AuthenticatorGetAssertionResult {
|
||||||
selectedCredential?: {
|
selectedCredential: {
|
||||||
id: string;
|
id: string;
|
||||||
userHandle: Uint8Array;
|
userHandle?: Uint8Array;
|
||||||
};
|
};
|
||||||
authenticatorData: Uint8Array;
|
authenticatorData: Uint8Array;
|
||||||
signature: Uint8Array;
|
signature: Uint8Array;
|
||||||
|
|||||||
@@ -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 credentialIds: string[];
|
||||||
|
let selectedCredentialId: string;
|
||||||
let ciphers: CipherView[];
|
let ciphers: CipherView[];
|
||||||
let params: Fido2AuthenticatorGetAssertionParams;
|
let params: Fido2AuthenticatorGetAssertionParams;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
credentialIds = [Utils.newGuid(), Utils.newGuid()];
|
credentialIds = [Utils.newGuid(), Utils.newGuid()];
|
||||||
ciphers = await Promise.all(
|
if (residentKey) {
|
||||||
credentialIds.map((id) =>
|
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(
|
createCipherView(
|
||||||
{ type: CipherType.Login },
|
{ type: CipherType.Login },
|
||||||
{ nonDiscoverableId: id, rpId: RpId, counter: 9000 }
|
{ nonDiscoverableId: id, rpId: RpId, counter: 9000 }
|
||||||
)
|
)
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
selectedCredentialId = credentialIds[0];
|
||||||
params = await createParams({
|
params = await createParams({
|
||||||
allowCredentialDescriptorList: credentialIds.map((credentialId) => ({
|
allowCredentialDescriptorList: credentialIds.map((credentialId) => ({
|
||||||
id: Utils.guidToRawFormat(credentialId),
|
id: Utils.guidToRawFormat(credentialId),
|
||||||
@@ -651,6 +664,7 @@ describe("FidoAuthenticatorService", () => {
|
|||||||
})),
|
})),
|
||||||
rpId: RpId,
|
rpId: RpId,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
cipherService.getAllDecrypted.mockResolvedValue(ciphers);
|
cipherService.getAllDecrypted.mockResolvedValue(ciphers);
|
||||||
userInterface.pickCredential.mockResolvedValue(ciphers[0].id);
|
userInterface.pickCredential.mockResolvedValue(ciphers[0].id);
|
||||||
});
|
});
|
||||||
@@ -672,7 +686,28 @@ describe("FidoAuthenticatorService", () => {
|
|||||||
);
|
);
|
||||||
expect(cipherService.updateWithServer).toHaveBeenCalledWith(encrypted);
|
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(
|
async function createParams(
|
||||||
params: Partial<Fido2AuthenticatorGetAssertionParams> = {}
|
params: Partial<Fido2AuthenticatorGetAssertionParams> = {}
|
||||||
@@ -713,6 +748,7 @@ function createCipherView(
|
|||||||
cipher.fido2Key.nonDiscoverableId = fido2Key.nonDiscoverableId;
|
cipher.fido2Key.nonDiscoverableId = fido2Key.nonDiscoverableId;
|
||||||
cipher.fido2Key.rpId = fido2Key.rpId ?? RpId;
|
cipher.fido2Key.rpId = fido2Key.rpId ?? RpId;
|
||||||
cipher.fido2Key.counter = fido2Key.counter ?? 0;
|
cipher.fido2Key.counter = fido2Key.counter ?? 0;
|
||||||
|
cipher.fido2Key.userHandle = Fido2Utils.bufferToString(randomBytes(16));
|
||||||
return cipher;
|
return cipher;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -729,6 +765,7 @@ async function createClientDataHash() {
|
|||||||
return await crypto.subtle.digest({ name: "SHA-256" }, clientData);
|
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) {
|
function randomBytes(length: number) {
|
||||||
return new Uint8Array(Array.from({ length }, () => Math.floor(Math.random() * 255)));
|
return new Uint8Array(Array.from({ length }, (_, k) => k % 255));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -146,35 +146,63 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
|
|||||||
throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.Constraint);
|
throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.Constraint);
|
||||||
}
|
}
|
||||||
|
|
||||||
let credentialOptions: CipherView[];
|
let cipherOptions: CipherView[];
|
||||||
|
|
||||||
// eslint-disable-next-line no-empty
|
// eslint-disable-next-line no-empty
|
||||||
if (params.allowCredentialDescriptorList?.length > 0) {
|
if (params.allowCredentialDescriptorList?.length > 0) {
|
||||||
credentialOptions = await this.findNonDiscoverableCredentials(
|
cipherOptions = await this.findNonDiscoverableCredentials(
|
||||||
params.allowCredentialDescriptorList,
|
params.allowCredentialDescriptorList,
|
||||||
params.rpId
|
params.rpId
|
||||||
);
|
);
|
||||||
} else {
|
} 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);
|
throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotAllowed);
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedCredentialId = await this.userInterface.pickCredential(
|
const selectedCipherId = await this.userInterface.pickCredential(
|
||||||
credentialOptions.map((cipher) => cipher.id)
|
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);
|
throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotAllowed);
|
||||||
}
|
}
|
||||||
|
|
||||||
++selectedCredential.fido2Key.counter;
|
const selectedCredentialId =
|
||||||
selectedCredential.localData.lastUsedDate = new Date().getTime();
|
params.allowCredentialDescriptorList?.length > 0
|
||||||
const encrypted = await this.cipherService.encrypt(selectedCredential);
|
? 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);
|
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(
|
private async vaultContainsCredentials(
|
||||||
@@ -305,6 +333,7 @@ async function generateAuthData(params: AuthDataParams) {
|
|||||||
counter & 0x000000ff
|
counter & 0x000000ff
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (params.keyPair) {
|
||||||
// attestedCredentialData
|
// attestedCredentialData
|
||||||
const attestedCredentialData: Array<number> = [];
|
const attestedCredentialData: Array<number> = [];
|
||||||
|
|
||||||
@@ -316,7 +345,6 @@ async function generateAuthData(params: AuthDataParams) {
|
|||||||
attestedCredentialData.push(...credentialIdLength);
|
attestedCredentialData.push(...credentialIdLength);
|
||||||
attestedCredentialData.push(...rawId);
|
attestedCredentialData.push(...rawId);
|
||||||
|
|
||||||
if (params.keyPair) {
|
|
||||||
const publicKeyJwk = await crypto.subtle.exportKey("jwk", params.keyPair.publicKey);
|
const publicKeyJwk = await crypto.subtle.exportKey("jwk", params.keyPair.publicKey);
|
||||||
// COSE format of the EC256 key
|
// COSE format of the EC256 key
|
||||||
const keyX = Utils.fromUrlB64ToArray(publicKeyJwk.x);
|
const keyX = Utils.fromUrlB64ToArray(publicKeyJwk.x);
|
||||||
|
|||||||
Reference in New Issue
Block a user