[PM-7382] Add support for non-UUID credential (#11993)
* feat: add tests for guidToRawFormat * feat: add support for parsing b64 credential ids * refactor: change interface to use Uint8Array for simplification Technically this deviates from the specification, but nobody is going to be using the authenticator directly but us so it shouldn't matter. We're gonna switch to `passkey-rs` anyways so * feat: change how the authenticator parses credential ids to support b64
This commit is contained in:
parent
38c4eeb27d
commit
acf5b1e9e6
|
@ -64,7 +64,7 @@ export class Fido2AuthenticatorError extends Error {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PublicKeyCredentialDescriptor {
|
export interface PublicKeyCredentialDescriptor {
|
||||||
id: BufferSource;
|
id: Uint8Array;
|
||||||
transports?: ("ble" | "hybrid" | "internal" | "nfc" | "usb")[];
|
transports?: ("ble" | "hybrid" | "internal" | "nfc" | "usb")[];
|
||||||
type: "public-key";
|
type: "public-key";
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
import { compareCredentialIds, parseCredentialId } from "./credential-id-utils";
|
||||||
|
|
||||||
|
describe("credential-id-utils", () => {
|
||||||
|
describe("parseCredentialId", () => {
|
||||||
|
it("returns credentialId in binary format when given a valid UUID string", () => {
|
||||||
|
const result = parseCredentialId("08d70b74-e9f5-4522-a425-e5dcd40107e7");
|
||||||
|
|
||||||
|
expect(result).toEqual(
|
||||||
|
new Uint8Array([
|
||||||
|
0x08, 0xd7, 0x0b, 0x74, 0xe9, 0xf5, 0x45, 0x22, 0xa4, 0x25, 0xe5, 0xdc, 0xd4, 0x01, 0x07,
|
||||||
|
0xe7,
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns credentialId in binary format when given a valid Base64Url string", () => {
|
||||||
|
const result = parseCredentialId("b64.CNcLdOn1RSKkJeXc1AEH5w");
|
||||||
|
|
||||||
|
expect(result).toEqual(
|
||||||
|
new Uint8Array([
|
||||||
|
0x08, 0xd7, 0x0b, 0x74, 0xe9, 0xf5, 0x45, 0x22, 0xa4, 0x25, 0xe5, 0xdc, 0xd4, 0x01, 0x07,
|
||||||
|
0xe7,
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns undefined when given an invalid Base64 string", () => {
|
||||||
|
const result = parseCredentialId("b64.#$%&");
|
||||||
|
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns undefined when given an invalid UUID string", () => {
|
||||||
|
const result = parseCredentialId("invalid");
|
||||||
|
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("compareCredentialIds", () => {
|
||||||
|
it("returns true when the two credential IDs are equal", () => {
|
||||||
|
const a = new Uint8Array([0x01, 0x02, 0x03]);
|
||||||
|
const b = new Uint8Array([0x01, 0x02, 0x03]);
|
||||||
|
|
||||||
|
const result = compareCredentialIds(a, b);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false when the two credential IDs are not equal", () => {
|
||||||
|
const a = new Uint8Array([0x01, 0x02, 0x03]);
|
||||||
|
const b = new Uint8Array([0x01, 0x02, 0x04]);
|
||||||
|
|
||||||
|
const result = compareCredentialIds(a, b);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false when the two credential IDs have different lengths", () => {
|
||||||
|
const a = new Uint8Array([0x01, 0x02, 0x03]);
|
||||||
|
const b = new Uint8Array([0x01, 0x02, 0x03, 0x04]);
|
||||||
|
|
||||||
|
const result = compareCredentialIds(a, b);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { Fido2Utils } from "./fido2-utils";
|
||||||
|
import { guidToRawFormat } from "./guid-utils";
|
||||||
|
|
||||||
|
export function parseCredentialId(encodedCredentialId: string): Uint8Array {
|
||||||
|
try {
|
||||||
|
if (encodedCredentialId.startsWith("b64.")) {
|
||||||
|
return Fido2Utils.stringToBuffer(encodedCredentialId.slice(4));
|
||||||
|
}
|
||||||
|
|
||||||
|
return guidToRawFormat(encodedCredentialId);
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares two credential IDs for equality.
|
||||||
|
*/
|
||||||
|
export function compareCredentialIds(a: Uint8Array, b: Uint8Array): boolean {
|
||||||
|
if (a.length !== b.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < a.length; i++) {
|
||||||
|
if (a[i] !== b[i]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
|
@ -26,9 +26,9 @@ import {
|
||||||
import { Utils } from "../../misc/utils";
|
import { Utils } from "../../misc/utils";
|
||||||
|
|
||||||
import { CBOR } from "./cbor";
|
import { CBOR } from "./cbor";
|
||||||
|
import { parseCredentialId } from "./credential-id-utils";
|
||||||
import { AAGUID, Fido2AuthenticatorService } from "./fido2-authenticator.service";
|
import { AAGUID, Fido2AuthenticatorService } from "./fido2-authenticator.service";
|
||||||
import { Fido2Utils } from "./fido2-utils";
|
import { Fido2Utils } from "./fido2-utils";
|
||||||
import { guidToRawFormat } from "./guid-utils";
|
|
||||||
|
|
||||||
const RpId = "bitwarden.com";
|
const RpId = "bitwarden.com";
|
||||||
|
|
||||||
|
@ -139,7 +139,7 @@ describe("FidoAuthenticatorService", () => {
|
||||||
params = await createParams({
|
params = await createParams({
|
||||||
excludeCredentialDescriptorList: [
|
excludeCredentialDescriptorList: [
|
||||||
{
|
{
|
||||||
id: guidToRawFormat(excludedCipher.login.fido2Credentials[0].credentialId),
|
id: parseCredentialId(excludedCipher.login.fido2Credentials[0].credentialId),
|
||||||
type: "public-key",
|
type: "public-key",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -482,7 +482,7 @@ describe("FidoAuthenticatorService", () => {
|
||||||
credentialId = Utils.newGuid();
|
credentialId = Utils.newGuid();
|
||||||
params = await createParams({
|
params = await createParams({
|
||||||
allowCredentialDescriptorList: [
|
allowCredentialDescriptorList: [
|
||||||
{ id: guidToRawFormat(credentialId), type: "public-key" },
|
{ id: parseCredentialId(credentialId), type: "public-key" },
|
||||||
],
|
],
|
||||||
rpId: RpId,
|
rpId: RpId,
|
||||||
});
|
});
|
||||||
|
@ -546,7 +546,7 @@ describe("FidoAuthenticatorService", () => {
|
||||||
let params: Fido2AuthenticatorGetAssertionParams;
|
let params: Fido2AuthenticatorGetAssertionParams;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
credentialIds = [Utils.newGuid(), Utils.newGuid()];
|
credentialIds = [Utils.newGuid(), "b64.Lb5SVTumSV6gYJpeWh3laA"];
|
||||||
ciphers = [
|
ciphers = [
|
||||||
await createCipherView(
|
await createCipherView(
|
||||||
{ type: CipherType.Login },
|
{ type: CipherType.Login },
|
||||||
|
@ -559,7 +559,7 @@ describe("FidoAuthenticatorService", () => {
|
||||||
];
|
];
|
||||||
params = await createParams({
|
params = await createParams({
|
||||||
allowCredentialDescriptorList: credentialIds.map((credentialId) => ({
|
allowCredentialDescriptorList: credentialIds.map((credentialId) => ({
|
||||||
id: guidToRawFormat(credentialId),
|
id: parseCredentialId(credentialId),
|
||||||
type: "public-key",
|
type: "public-key",
|
||||||
})),
|
})),
|
||||||
rpId: RpId,
|
rpId: RpId,
|
||||||
|
@ -667,7 +667,7 @@ describe("FidoAuthenticatorService", () => {
|
||||||
selectedCredentialId = credentialIds[0];
|
selectedCredentialId = credentialIds[0];
|
||||||
params = await createParams({
|
params = await createParams({
|
||||||
allowCredentialDescriptorList: credentialIds.map((credentialId) => ({
|
allowCredentialDescriptorList: credentialIds.map((credentialId) => ({
|
||||||
id: guidToRawFormat(credentialId),
|
id: parseCredentialId(credentialId),
|
||||||
type: "public-key",
|
type: "public-key",
|
||||||
})),
|
})),
|
||||||
rpId: RpId,
|
rpId: RpId,
|
||||||
|
@ -723,7 +723,7 @@ describe("FidoAuthenticatorService", () => {
|
||||||
const flags = encAuthData.slice(32, 33);
|
const flags = encAuthData.slice(32, 33);
|
||||||
const counter = encAuthData.slice(33, 37);
|
const counter = encAuthData.slice(33, 37);
|
||||||
|
|
||||||
expect(result.selectedCredential.id).toEqual(guidToRawFormat(selectedCredentialId));
|
expect(result.selectedCredential.id).toEqual(parseCredentialId(selectedCredentialId));
|
||||||
expect(result.selectedCredential.userHandle).toEqual(
|
expect(result.selectedCredential.userHandle).toEqual(
|
||||||
Fido2Utils.stringToBuffer(fido2Credentials[0].userHandle),
|
Fido2Utils.stringToBuffer(fido2Credentials[0].userHandle),
|
||||||
);
|
);
|
||||||
|
|
|
@ -23,9 +23,10 @@ import { LogService } from "../../abstractions/log.service";
|
||||||
import { Utils } from "../../misc/utils";
|
import { Utils } from "../../misc/utils";
|
||||||
|
|
||||||
import { CBOR } from "./cbor";
|
import { CBOR } from "./cbor";
|
||||||
|
import { compareCredentialIds, parseCredentialId } from "./credential-id-utils";
|
||||||
import { p1363ToDer } from "./ecdsa-utils";
|
import { p1363ToDer } from "./ecdsa-utils";
|
||||||
import { Fido2Utils } from "./fido2-utils";
|
import { Fido2Utils } from "./fido2-utils";
|
||||||
import { guidToRawFormat, guidToStandardFormat } from "./guid-utils";
|
import { guidToStandardFormat } from "./guid-utils";
|
||||||
|
|
||||||
// AAGUID: d548826e-79b4-db40-a3d8-11116f7e8349
|
// AAGUID: d548826e-79b4-db40-a3d8-11116f7e8349
|
||||||
export const AAGUID = new Uint8Array([
|
export const AAGUID = new Uint8Array([
|
||||||
|
@ -178,7 +179,7 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
|
||||||
|
|
||||||
const authData = await generateAuthData({
|
const authData = await generateAuthData({
|
||||||
rpId: params.rpEntity.id,
|
rpId: params.rpEntity.id,
|
||||||
credentialId: guidToRawFormat(credentialId),
|
credentialId: parseCredentialId(credentialId),
|
||||||
counter: fido2Credential.counter,
|
counter: fido2Credential.counter,
|
||||||
userPresence: true,
|
userPresence: true,
|
||||||
userVerification: userVerified,
|
userVerification: userVerified,
|
||||||
|
@ -193,7 +194,7 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
credentialId: guidToRawFormat(credentialId),
|
credentialId: parseCredentialId(credentialId),
|
||||||
attestationObject,
|
attestationObject,
|
||||||
authData,
|
authData,
|
||||||
publicKey: pubKeyDer,
|
publicKey: pubKeyDer,
|
||||||
|
@ -313,7 +314,7 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
|
||||||
|
|
||||||
const authenticatorData = await generateAuthData({
|
const authenticatorData = await generateAuthData({
|
||||||
rpId: selectedFido2Credential.rpId,
|
rpId: selectedFido2Credential.rpId,
|
||||||
credentialId: guidToRawFormat(selectedCredentialId),
|
credentialId: parseCredentialId(selectedCredentialId),
|
||||||
counter: selectedFido2Credential.counter,
|
counter: selectedFido2Credential.counter,
|
||||||
userPresence: true,
|
userPresence: true,
|
||||||
userVerification: userVerified,
|
userVerification: userVerified,
|
||||||
|
@ -328,7 +329,7 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
|
||||||
return {
|
return {
|
||||||
authenticatorData,
|
authenticatorData,
|
||||||
selectedCredential: {
|
selectedCredential: {
|
||||||
id: guidToRawFormat(selectedCredentialId),
|
id: parseCredentialId(selectedCredentialId),
|
||||||
userHandle: Fido2Utils.stringToBuffer(selectedFido2Credential.userHandle),
|
userHandle: Fido2Utils.stringToBuffer(selectedFido2Credential.userHandle),
|
||||||
},
|
},
|
||||||
signature,
|
signature,
|
||||||
|
@ -412,16 +413,7 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
|
||||||
credentials: PublicKeyCredentialDescriptor[],
|
credentials: PublicKeyCredentialDescriptor[],
|
||||||
rpId: string,
|
rpId: string,
|
||||||
): Promise<CipherView[]> {
|
): Promise<CipherView[]> {
|
||||||
const ids: string[] = [];
|
if (credentials.length === 0) {
|
||||||
|
|
||||||
for (const credential of credentials) {
|
|
||||||
try {
|
|
||||||
ids.push(guidToStandardFormat(credential.id));
|
|
||||||
// eslint-disable-next-line no-empty
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ids.length === 0) {
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -432,7 +424,12 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
|
||||||
cipher.type === CipherType.Login &&
|
cipher.type === CipherType.Login &&
|
||||||
cipher.login.hasFido2Credentials &&
|
cipher.login.hasFido2Credentials &&
|
||||||
cipher.login.fido2Credentials[0].rpId === rpId &&
|
cipher.login.fido2Credentials[0].rpId === rpId &&
|
||||||
ids.includes(cipher.login.fido2Credentials[0].credentialId),
|
credentials.some((credential) =>
|
||||||
|
compareCredentialIds(
|
||||||
|
credential.id,
|
||||||
|
parseCredentialId(cipher.login.fido2Credentials[0].credentialId),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { guidToRawFormat } from "./guid-utils";
|
||||||
|
|
||||||
|
describe("guid-utils", () => {
|
||||||
|
describe("guidToRawFormat", () => {
|
||||||
|
it.each([
|
||||||
|
[
|
||||||
|
"00000000-0000-0000-0000-000000000000",
|
||||||
|
[
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00,
|
||||||
|
],
|
||||||
|
"08d70b74-e9f5-4522-a425-e5dcd40107e7",
|
||||||
|
[
|
||||||
|
0x08, 0xd7, 0x0b, 0x74, 0xe9, 0xf5, 0x45, 0x22, 0xa4, 0x25, 0xe5, 0xdc, 0xd4, 0x01, 0x07,
|
||||||
|
0xe7,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
])("returns UUID in binary format when given a valid UUID string", (input, expected) => {
|
||||||
|
const result = guidToRawFormat(input);
|
||||||
|
|
||||||
|
expect(result).toEqual(new Uint8Array(expected));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws an error when given an invalid UUID string", () => {
|
||||||
|
expect(() => guidToRawFormat("invalid")).toThrow(TypeError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue