1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-17 08:43:33 +00:00
Files
browser/libs/common/src/platform/services/fido2/fido2-client.service.spec.ts
Cesar Gonzalez 2827d338ee [PM-11419] Fix issues encountered with inline menu passkeys (#10892)
* [PM-11419] Login items do not display after adding passkey

* [PM-11419] Login items do not display after adding passkey

* [PM-11419] Incorporating fixes for deleting a cipher from the inline menu as well as authenticating using passkeys

* [PM-11419] Fixing an issue where master password reprompt is ignored for a set passkey cipher

* [PM-11419] Fixing an issue where saving a passkey does not trigger a clearing of cached cipher values

* [PM-11419] Refactoring implementation

* [PM-11419] Ensuring that passkeys must be enabled in order for ciphers to appear

* [PM-11419] Adding an abort event from the active request manager

* [PM-11419] Adding an abort event from the active request manager

* [PM-11419] Working through jest tests within implementation

* [PM-11419] Fixing jest tests within Fido2ClientService and Fido2AuthenticatorService

* [PM-11419] Adding jest tests for added logic within OverlayBackground

* [PM-11419] Adding jest tests for added logic within OverlayBackground

* [PM-11419] Reworking how we handle assuming user presence when master password reprompt is required

* [PM-11419] Reworking how we handle assuming user presence when master password reprompt is required

* [PM-11419] Reworking how we handle assuming user presence when master password reprompt is required

* [PM-11419] Refactoring implementation

* [PM-11419] Incorporating suggestion for reporting failed passkey authentication from the inline menu

* [PM-11419] Reworking positioning of the abort controller that informs the background script of an error

* [PM-11419] Scoping down the behavior surrounding master password reprompt a bit more tightly

* [PM-11419] Reworking how we handle reacting to active fido2 requests to avoid ambiguity

* [PM-11419] Reworking how we handle reacting to active fido2 requests to avoid ambiguity

* [PM-11419] Adjusting implementation to ensure we clear any active requests when the passkeys setting is modified
2024-09-09 08:44:08 -04:00

692 lines
27 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject, of } from "rxjs";
import { AuthService } from "../../../auth/abstractions/auth.service";
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
import { DomainSettingsService } from "../../../autofill/services/domain-settings.service";
import { Utils } from "../../../platform/misc/utils";
import { VaultSettingsService } from "../../../vault/abstractions/vault-settings/vault-settings.service";
import { ConfigService } from "../../abstractions/config/config.service";
import {
ActiveRequest,
Fido2ActiveRequestEvents,
Fido2ActiveRequestManager,
RequestResult,
} from "../../abstractions/fido2/fido2-active-request-manager.abstraction";
import {
Fido2AuthenticatorError,
Fido2AuthenticatorErrorCode,
Fido2AuthenticatorGetAssertionResult,
Fido2AuthenticatorMakeCredentialResult,
} from "../../abstractions/fido2/fido2-authenticator.service.abstraction";
import {
AssertCredentialParams,
CreateCredentialParams,
FallbackRequestedError,
} from "../../abstractions/fido2/fido2-client.service.abstraction";
import { TaskSchedulerService } from "../../scheduling/task-scheduler.service";
import * as DomainUtils from "./domain-utils";
import { Fido2AuthenticatorService } from "./fido2-authenticator.service";
import { Fido2ClientService } from "./fido2-client.service";
import { Fido2Utils } from "./fido2-utils";
import { guidToRawFormat } from "./guid-utils";
const RpId = "bitwarden.com";
const Origin = "https://bitwarden.com";
const VaultUrl = "https://vault.bitwarden.com";
describe("FidoAuthenticatorService", () => {
let authenticator!: MockProxy<Fido2AuthenticatorService>;
let configService!: MockProxy<ConfigService>;
let authService!: MockProxy<AuthService>;
let vaultSettingsService: MockProxy<VaultSettingsService>;
let domainSettingsService: MockProxy<DomainSettingsService>;
let taskSchedulerService: MockProxy<TaskSchedulerService>;
let activeRequest!: MockProxy<ActiveRequest>;
let requestManager!: MockProxy<Fido2ActiveRequestManager>;
let client!: Fido2ClientService;
let tab!: chrome.tabs.Tab;
let isValidRpId!: jest.SpyInstance;
beforeEach(async () => {
authenticator = mock<Fido2AuthenticatorService>();
configService = mock<ConfigService>();
authService = mock<AuthService>();
vaultSettingsService = mock<VaultSettingsService>();
domainSettingsService = mock<DomainSettingsService>();
taskSchedulerService = mock<TaskSchedulerService>();
activeRequest = mock<ActiveRequest>({
subject: new BehaviorSubject<RequestResult>({
type: Fido2ActiveRequestEvents.Continue,
credentialId: "",
}),
});
requestManager = mock<Fido2ActiveRequestManager>({
getActiveRequest$: (tabId: number) => new BehaviorSubject(activeRequest),
getActiveRequest: (tabId: number) => activeRequest,
});
isValidRpId = jest.spyOn(DomainUtils, "isValidRpId");
client = new Fido2ClientService(
authenticator,
configService,
authService,
vaultSettingsService,
domainSettingsService,
taskSchedulerService,
requestManager,
);
configService.serverConfig$ = of({ environment: { vault: VaultUrl } } as any);
vaultSettingsService.enablePasskeys$ = of(true);
domainSettingsService.neverDomains$ = of({});
authService.activeAccountStatus$ = of(AuthenticationStatus.Unlocked);
tab = { id: 123, windowId: 456 } as chrome.tabs.Tab;
});
afterEach(() => {
isValidRpId.mockRestore();
});
describe("createCredential", () => {
describe("input parameters validation", () => {
// Spec: If sameOriginWithAncestors is false, return a "NotAllowedError" DOMException.
it("should throw error if sameOriginWithAncestors is false", async () => {
const params = createParams({ sameOriginWithAncestors: false });
const result = async () => await client.createCredential(params, tab);
const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "NotAllowedError" });
await rejects.toBeInstanceOf(DOMException);
});
// Spec: If the length of options.user.id is not between 1 and 64 bytes (inclusive) then return a TypeError.
it("should throw error if user.id is too small", async () => {
const params = createParams({ user: { id: "", displayName: "displayName", name: "name" } });
const result = async () => await client.createCredential(params, tab);
await expect(result).rejects.toBeInstanceOf(TypeError);
});
// Spec: If the length of options.user.id is not between 1 and 64 bytes (inclusive) then return a TypeError.
it("should throw error if user.id is too large", async () => {
const params = createParams({
user: {
id: "YWJzb2x1dGVseS13YXktd2F5LXRvby1sYXJnZS1iYXNlNjQtZW5jb2RlZC11c2VyLWlkLWJpbmFyeS1zZXF1ZW5jZQ",
displayName: "displayName",
name: "name",
},
});
const result = async () => await client.createCredential(params, tab);
await expect(result).rejects.toBeInstanceOf(TypeError);
});
// Spec: If callerOrigin is an opaque origin, return a DOMException whose name is "NotAllowedError", and terminate this algorithm.
// Not sure how to check this, or if it matters.
it.todo("should throw error if origin is an opaque origin");
// Spec: Let effectiveDomain be the callerOrigins effective domain. If effective domain is not a valid domain, then return a DOMException whose name is "SecurityError" and terminate this algorithm.
it("should throw error if origin is not a valid domain name", async () => {
const params = createParams({
origin: "invalid-domain-name",
});
const result = async () => await client.createCredential(params, tab);
const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "SecurityError" });
await rejects.toBeInstanceOf(DOMException);
});
// Spec: If options.rp.id is not a registrable domain suffix of and is not equal to effectiveDomain, return a DOMException whose name is "SecurityError", and terminate this algorithm.
// This is actually checked by `isValidRpId` function, but we'll test it here as well
it("should throw error if rp.id is not valid for this origin", async () => {
const params = createParams({
origin: "https://passwordless.dev",
rp: { id: "bitwarden.com", name: "Bitwarden" },
});
const result = async () => await client.createCredential(params, tab);
const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "SecurityError" });
await rejects.toBeInstanceOf(DOMException);
});
// Sanity check to make sure that we use `isValidRpId` to validate the rp.id
it("should throw if isValidRpId returns false", async () => {
const params = createParams();
authenticator.makeCredential.mockResolvedValue(createAuthenticatorMakeResult());
// `params` actually has a valid rp.id, but we're mocking the function to return false
isValidRpId.mockReturnValue(false);
const result = async () => await client.createCredential(params, tab);
const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "SecurityError" });
await rejects.toBeInstanceOf(DOMException);
});
it("should fallback if origin hostname is found in neverDomains", async () => {
const params = createParams({
origin: "https://bitwarden.com",
rp: { id: "bitwarden.com", name: "Bitwarden" },
});
domainSettingsService.neverDomains$ = of({ "bitwarden.com": null });
const result = async () => await client.createCredential(params, tab);
await expect(result).rejects.toThrow(FallbackRequestedError);
});
it("should throw error if origin is not an https domain", async () => {
const params = createParams({
origin: "http://passwordless.dev",
rp: { id: "bitwarden.com", name: "Bitwarden" },
});
const result = async () => await client.createCredential(params, tab);
const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "SecurityError" });
await rejects.toBeInstanceOf(DOMException);
});
it("should not throw error if localhost is http", async () => {
const params = createParams({
origin: "http://localhost",
rp: { id: undefined, name: "localhost" },
});
authenticator.makeCredential.mockResolvedValue(createAuthenticatorMakeResult());
await client.createCredential(params, tab);
});
// Spec: If credTypesAndPubKeyAlgs is empty, return a DOMException whose name is "NotSupportedError", and terminate this algorithm.
it("should throw error if no support key algorithms were found", async () => {
const params = createParams({
pubKeyCredParams: [
{ alg: -9001, type: "public-key" },
{ alg: -7, type: "not-supported" as any },
],
});
const result = async () => await client.createCredential(params, tab);
const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "NotSupportedError" });
await rejects.toBeInstanceOf(DOMException);
});
});
describe("aborting", () => {
// Spec: If the options.signal is present and its aborted flag is set to true, return a DOMException whose name is "AbortError" and terminate this algorithm.
it("should throw error if aborting using abort controller", async () => {
const params = createParams({});
const abortController = new AbortController();
abortController.abort();
const result = async () => await client.createCredential(params, tab, abortController);
const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "AbortError" });
await rejects.toBeInstanceOf(DOMException);
});
});
describe("creating a new credential", () => {
it("should call authenticator.makeCredential", async () => {
const params = createParams({
authenticatorSelection: { residentKey: "required", userVerification: "required" },
});
authenticator.makeCredential.mockResolvedValue(createAuthenticatorMakeResult());
await client.createCredential(params, tab);
expect(authenticator.makeCredential).toHaveBeenCalledWith(
expect.objectContaining({
requireResidentKey: true,
requireUserVerification: true,
rpEntity: expect.objectContaining({
id: RpId,
}),
userEntity: expect.objectContaining({
displayName: params.user.displayName,
}),
}),
tab,
expect.anything(),
);
});
it("should return credProps.rk = true when creating a discoverable credential", async () => {
const params = createParams({
authenticatorSelection: { residentKey: "required", userVerification: "required" },
extensions: { credProps: true },
});
authenticator.makeCredential.mockResolvedValue(createAuthenticatorMakeResult());
const result = await client.createCredential(params, tab);
expect(result.extensions.credProps?.rk).toBe(true);
});
it("should return credProps.rk = false when creating a non-discoverable credential", async () => {
const params = createParams({
authenticatorSelection: { residentKey: "discouraged", userVerification: "required" },
extensions: { credProps: true },
});
authenticator.makeCredential.mockResolvedValue(createAuthenticatorMakeResult());
const result = await client.createCredential(params, tab);
expect(result.extensions.credProps?.rk).toBe(false);
});
it("should return credProps = undefiend when the extension is not requested", async () => {
const params = createParams({
authenticatorSelection: { residentKey: "required", userVerification: "required" },
extensions: {},
});
authenticator.makeCredential.mockResolvedValue(createAuthenticatorMakeResult());
const result = await client.createCredential(params, tab);
expect(result.extensions.credProps).toBeUndefined();
});
// Spec: If any authenticator returns an error status equivalent to "InvalidStateError", Return a DOMException whose name is "InvalidStateError" and terminate this algorithm.
it("should throw error if authenticator throws InvalidState", async () => {
const params = createParams();
authenticator.makeCredential.mockRejectedValue(
new Fido2AuthenticatorError(Fido2AuthenticatorErrorCode.InvalidState),
);
const result = async () => await client.createCredential(params, tab);
const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "InvalidStateError" });
await rejects.toBeInstanceOf(DOMException);
});
// This keeps sensetive information form leaking
it("should throw NotAllowedError if authenticator throws unknown error", async () => {
const params = createParams();
authenticator.makeCredential.mockRejectedValue(new Error("unknown error"));
const result = async () => await client.createCredential(params, tab);
const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "NotAllowedError" });
await rejects.toBeInstanceOf(DOMException);
});
it("should throw FallbackRequestedError if passkeys state is not enabled", async () => {
const params = createParams();
vaultSettingsService.enablePasskeys$ = of(false);
const result = async () => await client.createCredential(params, tab);
const rejects = expect(result).rejects;
await rejects.toThrow(FallbackRequestedError);
});
it("should throw FallbackRequestedError if user is logged out", async () => {
const params = createParams();
authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.LoggedOut);
const result = async () => await client.createCredential(params, tab);
const rejects = expect(result).rejects;
await rejects.toThrow(FallbackRequestedError);
});
it("should throw FallbackRequestedError if origin equals the bitwarden vault", async () => {
const params = createParams({ origin: VaultUrl });
const result = async () => await client.createCredential(params, tab);
const rejects = expect(result).rejects;
await rejects.toThrow(FallbackRequestedError);
});
});
function createParams(params: Partial<CreateCredentialParams> = {}): CreateCredentialParams {
return {
origin: params.origin ?? "https://bitwarden.com",
sameOriginWithAncestors: params.sameOriginWithAncestors ?? true,
attestation: params.attestation,
authenticatorSelection: params.authenticatorSelection,
challenge: params.challenge ?? "MzItYnl0ZXMtYmFzZTY0LWVuY29kZS1jaGFsbGVuZ2U",
excludeCredentials: params.excludeCredentials,
extensions: params.extensions,
pubKeyCredParams: params.pubKeyCredParams ?? [
{
alg: -7,
type: "public-key",
},
],
rp: params.rp ?? {
id: RpId,
name: "Bitwarden",
},
user: params.user ?? {
id: "YmFzZTY0LWVuY29kZWQtdXNlci1pZA",
displayName: "User Name",
name: "name",
},
fallbackSupported: params.fallbackSupported ?? false,
timeout: params.timeout,
};
}
function createAuthenticatorMakeResult(): Fido2AuthenticatorMakeCredentialResult {
return {
credentialId: guidToRawFormat(Utils.newGuid()),
attestationObject: randomBytes(128),
authData: randomBytes(64),
publicKey: randomBytes(64),
publicKeyAlgorithm: -7,
};
}
});
describe("assertCredential", () => {
describe("invalid params", () => {
// Spec: If callerOrigin is an opaque origin, return a DOMException whose name is "NotAllowedError", and terminate this algorithm.
// Not sure how to check this, or if it matters.
it.todo("should throw error if origin is an opaque origin");
// Spec: Let effectiveDomain be the callerOrigins effective domain. If effective domain is not a valid domain, then return a DOMException whose name is "SecurityError" and terminate this algorithm.
it("should throw error if origin is not a valid domain name", async () => {
const params = createParams({
origin: "invalid-domain-name",
});
const result = async () => await client.assertCredential(params, tab);
const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "SecurityError" });
await rejects.toBeInstanceOf(DOMException);
});
// Spec: If options.rp.id is not a registrable domain suffix of and is not equal to effectiveDomain, return a DOMException whose name is "SecurityError", and terminate this algorithm.
// This is actually checked by `isValidRpId` function, but we'll test it here as well
it("should throw error if rp.id is not valid for this origin", async () => {
const params = createParams({
origin: "https://passwordless.dev",
rpId: "bitwarden.com",
});
const result = async () => await client.assertCredential(params, tab);
const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "SecurityError" });
await rejects.toBeInstanceOf(DOMException);
});
// Sanity check to make sure that we use `isValidRpId` to validate the rp.id
it("should throw if isValidRpId returns false", async () => {
const params = createParams();
authenticator.getAssertion.mockResolvedValue(createAuthenticatorAssertResult());
// `params` actually has a valid rp.id, but we're mocking the function to return false
isValidRpId.mockReturnValue(false);
const result = async () => await client.assertCredential(params, tab);
const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "SecurityError" });
await rejects.toBeInstanceOf(DOMException);
});
it("should fallback if origin hostname is found in neverDomains", async () => {
const params = createParams({
origin: "https://bitwarden.com",
});
domainSettingsService.neverDomains$ = of({ "bitwarden.com": null });
const result = async () => await client.assertCredential(params, tab);
await expect(result).rejects.toThrow(FallbackRequestedError);
});
it("should throw error if origin is not an http domain", async () => {
const params = createParams({
origin: "http://passwordless.dev",
rpId: "bitwarden.com",
});
const result = async () => await client.assertCredential(params, tab);
const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "SecurityError" });
await rejects.toBeInstanceOf(DOMException);
});
});
describe("aborting", () => {
// Spec: If the options.signal is present and its aborted flag is set to true, return a DOMException whose name is "AbortError" and terminate this algorithm.
it("should throw error if aborting using abort controller", async () => {
const params = createParams({});
const abortController = new AbortController();
abortController.abort();
const result = async () => await client.assertCredential(params, tab, abortController);
const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "AbortError" });
await rejects.toBeInstanceOf(DOMException);
});
});
describe("assert credential", () => {
// Spec: If any authenticator returns an error status equivalent to "InvalidStateError", Return a DOMException whose name is "InvalidStateError" and terminate this algorithm.
it("should throw error if authenticator throws InvalidState", async () => {
const params = createParams();
authenticator.getAssertion.mockRejectedValue(
new Fido2AuthenticatorError(Fido2AuthenticatorErrorCode.InvalidState),
);
const result = async () => await client.assertCredential(params, tab);
const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "InvalidStateError" });
await rejects.toBeInstanceOf(DOMException);
});
// This keeps sensetive information form leaking
it("should throw NotAllowedError if authenticator throws unknown error", async () => {
const params = createParams();
authenticator.getAssertion.mockRejectedValue(new Error("unknown error"));
const result = async () => await client.assertCredential(params, tab);
const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "NotAllowedError" });
await rejects.toBeInstanceOf(DOMException);
});
it("should throw FallbackRequestedError if passkeys state is not enabled", async () => {
const params = createParams();
vaultSettingsService.enablePasskeys$ = of(false);
const result = async () => await client.assertCredential(params, tab);
const rejects = expect(result).rejects;
await rejects.toThrow(FallbackRequestedError);
});
it("should throw FallbackRequestedError if user is logged out", async () => {
const params = createParams();
authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.LoggedOut);
const result = async () => await client.assertCredential(params, tab);
const rejects = expect(result).rejects;
await rejects.toThrow(FallbackRequestedError);
});
it("should throw FallbackRequestedError if origin equals the bitwarden vault", async () => {
const params = createParams({ origin: VaultUrl });
const result = async () => await client.assertCredential(params, tab);
const rejects = expect(result).rejects;
await rejects.toThrow(FallbackRequestedError);
});
});
describe("assert non-discoverable credential", () => {
it("should call authenticator.assertCredential", async () => {
const allowedCredentialIds = [
Fido2Utils.bufferToString(guidToRawFormat(Utils.newGuid())),
Fido2Utils.bufferToString(guidToRawFormat(Utils.newGuid())),
Fido2Utils.bufferToString(Utils.fromByteStringToArray("not-a-guid")),
];
const params = createParams({
userVerification: "required",
allowedCredentialIds,
});
authenticator.getAssertion.mockResolvedValue(createAuthenticatorAssertResult());
await client.assertCredential(params, tab);
expect(authenticator.getAssertion).toHaveBeenCalledWith(
expect.objectContaining({
requireUserVerification: true,
rpId: RpId,
allowCredentialDescriptorList: [
expect.objectContaining({
id: Fido2Utils.stringToBuffer(allowedCredentialIds[0]),
}),
expect.objectContaining({
id: Fido2Utils.stringToBuffer(allowedCredentialIds[1]),
}),
expect.objectContaining({
id: Fido2Utils.stringToBuffer(allowedCredentialIds[2]),
}),
],
}),
tab,
expect.anything(),
);
});
it("should not throw error if localhost is http", async () => {
const params = createParams({
origin: "http://localhost",
});
params.rpId = undefined;
authenticator.getAssertion.mockResolvedValue(createAuthenticatorAssertResult());
await client.assertCredential(params, tab);
});
});
describe("assert discoverable credential", () => {
it("should call authenticator.assertCredential", async () => {
const params = createParams({
userVerification: "required",
allowedCredentialIds: [],
});
authenticator.getAssertion.mockResolvedValue(createAuthenticatorAssertResult());
await client.assertCredential(params, tab);
expect(authenticator.getAssertion).toHaveBeenCalledWith(
expect.objectContaining({
requireUserVerification: true,
rpId: RpId,
allowCredentialDescriptorList: [],
}),
tab,
expect.anything(),
);
});
});
describe("assert mediated conditional ui credential", () => {
const params = createParams({
userVerification: "required",
mediation: "conditional",
allowedCredentialIds: [],
});
beforeEach(() => {
requestManager.newActiveRequest.mockResolvedValue({
type: Fido2ActiveRequestEvents.Continue,
credentialId: crypto.randomUUID(),
});
authenticator.getAssertion.mockResolvedValue(createAuthenticatorAssertResult());
});
it("creates an active mediated conditional request", async () => {
await client.assertCredential(params, tab);
expect(requestManager.newActiveRequest).toHaveBeenCalled();
expect(authenticator.getAssertion).toHaveBeenCalledWith(
expect.objectContaining({
assumeUserPresence: true,
rpId: RpId,
}),
tab,
);
});
it("restarts the mediated conditional request if a user aborts the request", async () => {
authenticator.getAssertion.mockRejectedValueOnce(new Error());
await client.assertCredential(params, tab);
expect(authenticator.getAssertion).toHaveBeenCalledTimes(2);
});
it("restarts the mediated conditional request if a the abort controller aborts the request", async () => {
const abortController = new AbortController();
abortController.abort();
authenticator.getAssertion.mockRejectedValueOnce(new DOMException("AbortError"));
await client.assertCredential(params, tab);
expect(authenticator.getAssertion).toHaveBeenCalledTimes(2);
});
});
function createParams(params: Partial<AssertCredentialParams> = {}): AssertCredentialParams {
return {
allowedCredentialIds: params.allowedCredentialIds ?? [],
challenge: params.challenge ?? Fido2Utils.bufferToString(randomBytes(16)),
origin: params.origin ?? Origin,
rpId: params.rpId ?? RpId,
timeout: params.timeout,
userVerification: params.userVerification,
sameOriginWithAncestors: true,
fallbackSupported: params.fallbackSupported ?? false,
mediation: params.mediation,
};
}
function createAuthenticatorAssertResult(): Fido2AuthenticatorGetAssertionResult {
return {
selectedCredential: {
id: randomBytes(32),
userHandle: randomBytes(32),
},
authenticatorData: randomBytes(64),
signature: randomBytes(64),
};
}
});
});
/** This is a fake function that always returns the same byte sequence */
function randomBytes(length: number) {
return new Uint8Array(Array.from({ length }, (_, k) => k % 255));
}