1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-24 08:33:29 +00:00

[PM-31880 | BEEEP] Move random out of keyservice to PureCrypto (#18838)

* Move random to PureCrypto

* Rename and fix builds

* Fix tests

* Fix build

* Prettier

* Fix tests
This commit is contained in:
Bernd Schoolmann
2026-02-20 19:09:52 +01:00
committed by GitHub
parent 38bcc92398
commit 0569ec9517
38 changed files with 187 additions and 94 deletions

View File

@@ -316,7 +316,6 @@ export abstract class KeyService {
* @throws Error when provided userId is null or undefined
*/
abstract clearKeys(userId: UserId): Promise<void>;
abstract randomNumber(min: number, max: number): Promise<number>;
/**
* Generates a new cipher key
* @returns A new cipher key

View File

@@ -493,41 +493,6 @@ export class DefaultKeyService implements KeyServiceAbstraction {
await this.accountCryptographyStateService.clearAccountCryptographicState(userId);
}
// EFForg/OpenWireless
// ref https://github.com/EFForg/OpenWireless/blob/master/app/js/diceware.js
async randomNumber(min: number, max: number): Promise<number> {
let rval = 0;
const range = max - min + 1;
const bitsNeeded = Math.ceil(Math.log2(range));
if (bitsNeeded > 53) {
throw new Error("We cannot generate numbers larger than 53 bits.");
}
const bytesNeeded = Math.ceil(bitsNeeded / 8);
const mask = Math.pow(2, bitsNeeded) - 1;
// 7776 -> (2^13 = 8192) -1 == 8191 or 0x00001111 11111111
// Fill a byte array with N random numbers
const byteArray = new Uint8Array(await this.cryptoFunctionService.randomBytes(bytesNeeded));
let p = (bytesNeeded - 1) * 8;
for (let i = 0; i < bytesNeeded; i++) {
rval += byteArray[i] * Math.pow(2, p);
p -= 8;
}
// Use & to apply the mask and reduce the number of recursive lookups
rval = rval & mask;
if (rval >= range) {
// Integer out of acceptable range
return this.randomNumber(min, max);
}
// Return an integer that falls within the range
return min + rval;
}
// ---HELPERS---
async validateUserKey(key: UserKey | MasterKey | null, userId: UserId): Promise<boolean> {
if (key == null) {

View File

@@ -57,7 +57,7 @@ export const SYSTEM_SERVICE_PROVIDER = new SafeInjectionToken<SystemServiceProvi
safeProvider({
provide: RANDOMIZER,
useFactory: createRandomizer,
deps: [KeyService],
deps: [],
}),
safeProvider({
provide: LegacyEncryptorProvider,

View File

@@ -1,3 +1,7 @@
/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally
import { TextEncoder, TextDecoder } from "util";
Object.assign(global, { TextDecoder, TextEncoder });
import { EmailCalculator } from "./email-calculator";
describe("EmailCalculator", () => {

View File

@@ -1,3 +1,7 @@
/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally
import { TextEncoder, TextDecoder } from "util";
Object.assign(global, { TextDecoder, TextEncoder });
import { mock } from "jest-mock-extended";
import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist";

View File

@@ -1,3 +1,7 @@
/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally
import { TextEncoder, TextDecoder } from "util";
Object.assign(global, { TextDecoder, TextEncoder });
import { mock } from "jest-mock-extended";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";

View File

@@ -1,4 +1,4 @@
export { KeyServiceRandomizer } from "./key-service-randomizer";
export { PureCryptoRandomizer } from "./purecrypto-randomizer";
export { ForwarderConfiguration, AccountRequest } from "./forwarder-configuration";
export { ForwarderContext } from "./forwarder-context";
export * from "./settings";

View File

@@ -1,3 +1,7 @@
/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally
import { TextEncoder, TextDecoder } from "util";
Object.assign(global, { TextDecoder, TextEncoder });
import { mock } from "jest-mock-extended";
import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist";

View File

@@ -1,19 +1,28 @@
import { mock } from "jest-mock-extended";
import { PureCryptoRandomizer } from "./purecrypto-randomizer";
import { KeyService } from "@bitwarden/key-management";
jest.mock("@bitwarden/sdk-internal", () => ({
PureCrypto: {
random_number: jest.fn(),
},
}));
import { KeyServiceRandomizer } from "./key-service-randomizer";
jest.mock("@bitwarden/common/platform/abstractions/sdk/sdk-load.service", () => ({
SdkLoadService: {
Ready: Promise.resolve(),
},
}));
describe("KeyServiceRandomizer", () => {
const keyService = mock<KeyService>();
const mockRandomNumber = jest.requireMock("@bitwarden/sdk-internal").PureCrypto
.random_number as jest.Mock;
describe("PureCryptoRandomizer", () => {
afterEach(() => {
jest.resetAllMocks();
});
describe("pick", () => {
it.each([[null], [undefined], [[]]])("throws when the list is %p", async (list) => {
const randomizer = new KeyServiceRandomizer(keyService);
const randomizer = new PureCryptoRandomizer();
await expect(() => randomizer.pick(list)).rejects.toBeInstanceOf(Error);
@@ -21,8 +30,8 @@ describe("KeyServiceRandomizer", () => {
});
it("picks an item from the list", async () => {
const randomizer = new KeyServiceRandomizer(keyService);
keyService.randomNumber.mockResolvedValue(1);
const randomizer = new PureCryptoRandomizer();
mockRandomNumber.mockReturnValue(1);
const result = await randomizer.pick([0, 1]);
@@ -32,7 +41,7 @@ describe("KeyServiceRandomizer", () => {
describe("pickWord", () => {
it.each([[null], [undefined], [[]]])("throws when the list is %p", async (list) => {
const randomizer = new KeyServiceRandomizer(keyService);
const randomizer = new PureCryptoRandomizer();
await expect(() => randomizer.pickWord(list)).rejects.toBeInstanceOf(Error);
@@ -40,8 +49,8 @@ describe("KeyServiceRandomizer", () => {
});
it("picks a word from the list", async () => {
const randomizer = new KeyServiceRandomizer(keyService);
keyService.randomNumber.mockResolvedValue(1);
const randomizer = new PureCryptoRandomizer();
mockRandomNumber.mockReturnValue(1);
const result = await randomizer.pickWord(["foo", "bar"]);
@@ -49,8 +58,8 @@ describe("KeyServiceRandomizer", () => {
});
it("capitalizes the word when options.titleCase is true", async () => {
const randomizer = new KeyServiceRandomizer(keyService);
keyService.randomNumber.mockResolvedValue(1);
const randomizer = new PureCryptoRandomizer();
mockRandomNumber.mockReturnValue(1);
const result = await randomizer.pickWord(["foo", "bar"], { titleCase: true });
@@ -58,9 +67,9 @@ describe("KeyServiceRandomizer", () => {
});
it("appends a random number when options.number is true", async () => {
const randomizer = new KeyServiceRandomizer(keyService);
keyService.randomNumber.mockResolvedValueOnce(1);
keyService.randomNumber.mockResolvedValueOnce(2);
const randomizer = new PureCryptoRandomizer();
mockRandomNumber.mockReturnValueOnce(1);
mockRandomNumber.mockReturnValueOnce(2);
const result = await randomizer.pickWord(["foo", "bar"], { number: true });
@@ -70,7 +79,7 @@ describe("KeyServiceRandomizer", () => {
describe("shuffle", () => {
it.each([[null], [undefined], [[]]])("throws when the list is %p", async (list) => {
const randomizer = new KeyServiceRandomizer(keyService);
const randomizer = new PureCryptoRandomizer();
await expect(() => randomizer.shuffle(list)).rejects.toBeInstanceOf(Error);
@@ -78,18 +87,18 @@ describe("KeyServiceRandomizer", () => {
});
it("returns a copy of the list without shuffling it when theres only one entry", async () => {
const randomizer = new KeyServiceRandomizer(keyService);
const randomizer = new PureCryptoRandomizer();
const result = await randomizer.shuffle(["foo"]);
expect(result).toEqual(["foo"]);
expect(result).not.toBe(["foo"]);
expect(keyService.randomNumber).not.toHaveBeenCalled();
expect(mockRandomNumber).not.toHaveBeenCalled();
});
it("shuffles the tail of the list", async () => {
const randomizer = new KeyServiceRandomizer(keyService);
keyService.randomNumber.mockResolvedValueOnce(0);
const randomizer = new PureCryptoRandomizer();
mockRandomNumber.mockReturnValueOnce(0);
const result = await randomizer.shuffle(["bar", "foo"]);
@@ -97,9 +106,9 @@ describe("KeyServiceRandomizer", () => {
});
it("shuffles the list", async () => {
const randomizer = new KeyServiceRandomizer(keyService);
keyService.randomNumber.mockResolvedValueOnce(0);
keyService.randomNumber.mockResolvedValueOnce(1);
const randomizer = new PureCryptoRandomizer();
mockRandomNumber.mockReturnValueOnce(0);
mockRandomNumber.mockReturnValueOnce(1);
const result = await randomizer.shuffle(["baz", "bar", "foo"]);
@@ -107,8 +116,8 @@ describe("KeyServiceRandomizer", () => {
});
it("returns the input list when options.copy is false", async () => {
const randomizer = new KeyServiceRandomizer(keyService);
keyService.randomNumber.mockResolvedValueOnce(0);
const randomizer = new PureCryptoRandomizer();
mockRandomNumber.mockReturnValueOnce(0);
const expectedResult = ["foo"];
const result = await randomizer.shuffle(expectedResult, { copy: false });
@@ -119,7 +128,7 @@ describe("KeyServiceRandomizer", () => {
describe("chars", () => {
it("returns an empty string when the length is 0", async () => {
const randomizer = new KeyServiceRandomizer(keyService);
const randomizer = new PureCryptoRandomizer();
const result = await randomizer.chars(0);
@@ -127,8 +136,8 @@ describe("KeyServiceRandomizer", () => {
});
it("returns an arbitrary lowercase ascii character", async () => {
const randomizer = new KeyServiceRandomizer(keyService);
keyService.randomNumber.mockResolvedValueOnce(0);
const randomizer = new PureCryptoRandomizer();
mockRandomNumber.mockReturnValueOnce(0);
const result = await randomizer.chars(1);
@@ -136,38 +145,38 @@ describe("KeyServiceRandomizer", () => {
});
it("returns a number of ascii characters based on the length", async () => {
const randomizer = new KeyServiceRandomizer(keyService);
keyService.randomNumber.mockResolvedValue(0);
const randomizer = new PureCryptoRandomizer();
mockRandomNumber.mockReturnValue(0);
const result = await randomizer.chars(2);
expect(result).toEqual("aa");
expect(keyService.randomNumber).toHaveBeenCalledTimes(2);
expect(mockRandomNumber).toHaveBeenCalledTimes(2);
});
it("returns a new random character each time its called", async () => {
const randomizer = new KeyServiceRandomizer(keyService);
keyService.randomNumber.mockResolvedValueOnce(0);
keyService.randomNumber.mockResolvedValueOnce(1);
const randomizer = new PureCryptoRandomizer();
mockRandomNumber.mockReturnValueOnce(0);
mockRandomNumber.mockReturnValueOnce(1);
const resultA = await randomizer.chars(1);
const resultB = await randomizer.chars(1);
expect(resultA).toEqual("a");
expect(resultB).toEqual("b");
expect(keyService.randomNumber).toHaveBeenCalledTimes(2);
expect(mockRandomNumber).toHaveBeenCalledTimes(2);
});
});
describe("uniform", () => {
it("forwards requests to the crypto service", async () => {
const randomizer = new KeyServiceRandomizer(keyService);
keyService.randomNumber.mockResolvedValue(5);
const randomizer = new PureCryptoRandomizer();
mockRandomNumber.mockReturnValue(5);
const result = await randomizer.uniform(0, 5);
expect(result).toBe(5);
expect(keyService.randomNumber).toHaveBeenCalledWith(0, 5);
expect(mockRandomNumber).toHaveBeenCalledWith(0, 5);
});
});
});

View File

@@ -1,14 +1,18 @@
import { KeyService } from "@bitwarden/key-management";
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
import { PureCrypto } from "@bitwarden/sdk-internal";
import { Randomizer } from "../abstractions";
import { WordOptions } from "../types";
/** A randomizer backed by a KeyService. */
export class KeyServiceRandomizer implements Randomizer {
/** instantiates the type.
* @param keyService generates random numbers
/**
* A randomizer backed by the SDK.
* Note: This should be replaced by higher level functions in the SDK eventually.
**/
export class PureCryptoRandomizer implements Randomizer {
/**
* instantiates the type.
*/
constructor(private keyService: KeyService) {}
constructor() {}
async pick<Entry>(list: Array<Entry>): Promise<Entry> {
const length = list?.length ?? 0;
@@ -28,7 +32,8 @@ export class KeyServiceRandomizer implements Randomizer {
}
if (options?.number ?? false) {
const num = await this.keyService.randomNumber(1, 9);
await SdkLoadService.Ready;
const num = PureCrypto.random_number(0, 9);
word = word + num.toString();
}
@@ -63,6 +68,7 @@ export class KeyServiceRandomizer implements Randomizer {
}
async uniform(min: number, max: number) {
return this.keyService.randomNumber(min, max);
await SdkLoadService.Ready;
return PureCrypto.random_number(min, max);
}
}

View File

@@ -1,3 +1,7 @@
/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally
import { TextEncoder, TextDecoder } from "util";
Object.assign(global, { TextDecoder, TextEncoder });
import { mock } from "jest-mock-extended";
import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist";

View File

@@ -1,11 +1,9 @@
// contains logic that constructs generator services dynamically given
// a generator id.
import { KeyService } from "@bitwarden/key-management";
import { Randomizer } from "./abstractions";
import { KeyServiceRandomizer } from "./engine/key-service-randomizer";
import { PureCryptoRandomizer } from "./engine/purecrypto-randomizer";
export function createRandomizer(keyService: KeyService): Randomizer {
return new KeyServiceRandomizer(keyService);
export function createRandomizer(): Randomizer {
return new PureCryptoRandomizer();
}

View File

@@ -1,3 +1,7 @@
/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally
import { TextEncoder, TextDecoder } from "util";
Object.assign(global, { TextDecoder, TextEncoder });
import { mock } from "jest-mock-extended";
import { EmailRandomizer } from "../../engine";

View File

@@ -1,3 +1,7 @@
/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally
import { TextEncoder, TextDecoder } from "util";
Object.assign(global, { TextDecoder, TextEncoder });
import { mock } from "jest-mock-extended";
import { EmailRandomizer } from "../../engine";

View File

@@ -1,3 +1,7 @@
/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally
import { TextEncoder, TextDecoder } from "util";
Object.assign(global, { TextDecoder, TextEncoder });
import { mock } from "jest-mock-extended";
import { PolicyType } from "@bitwarden/common/admin-console/enums";

View File

@@ -1,3 +1,7 @@
/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally
import { TextEncoder, TextDecoder } from "util";
Object.assign(global, { TextDecoder, TextEncoder });
import { mock } from "jest-mock-extended";
import { PolicyType } from "@bitwarden/common/admin-console/enums";

View File

@@ -1,3 +1,7 @@
/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally
import { TextEncoder, TextDecoder } from "util";
Object.assign(global, { TextDecoder, TextEncoder });
import { mock } from "jest-mock-extended";
import { IdentityConstraint } from "@bitwarden/common/tools/state/identity-state-constraint";

View File

@@ -1,3 +1,7 @@
/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally
import { TextEncoder, TextDecoder } from "util";
Object.assign(global, { TextDecoder, TextEncoder });
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { PolicyId } from "@bitwarden/common/types/guid";

View File

@@ -1,3 +1,7 @@
/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally
import { TextEncoder, TextDecoder } from "util";
Object.assign(global, { TextDecoder, TextEncoder });
import { ObjectKey } from "@bitwarden/common/tools/state/object-key";
import { BuiltIn, Profile } from "../metadata";

View File

@@ -1,3 +1,7 @@
/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally
import { TextEncoder, TextDecoder } from "util";
Object.assign(global, { TextDecoder, TextEncoder });
import { BuiltIn, Profile } from "../metadata";
import { PassphrasePolicyConstraints } from "./passphrase-policy-constraints";

View File

@@ -1,3 +1,7 @@
/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally
import { TextEncoder, TextDecoder } from "util";
Object.assign(global, { TextDecoder, TextEncoder });
import { AlgorithmsByType, Type } from "../metadata";
import { CredentialPreference } from "../types";

View File

@@ -1,3 +1,7 @@
/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally
import { TextEncoder, TextDecoder } from "util";
Object.assign(global, { TextDecoder, TextEncoder });
import { mock } from "jest-mock-extended";
import { BehaviorSubject, ReplaySubject, firstValueFrom } from "rxjs";

View File

@@ -1,3 +1,7 @@
/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally
import { TextEncoder, TextDecoder } from "util";
Object.assign(global, { TextDecoder, TextEncoder });
import { BehaviorSubject, Subject, firstValueFrom, of } from "rxjs";
import { Account } from "@bitwarden/common/auth/abstractions/account.service";

View File

@@ -1,3 +1,7 @@
/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally
import { TextEncoder, TextDecoder } from "util";
Object.assign(global, { TextDecoder, TextEncoder });
import { mock } from "jest-mock-extended";
import { of, firstValueFrom } from "rxjs";

View File

@@ -1,3 +1,7 @@
/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally
import { TextEncoder, TextDecoder } from "util";
Object.assign(global, { TextDecoder, TextEncoder });
import { mock } from "jest-mock-extended";
import { of, firstValueFrom } from "rxjs";

View File

@@ -1,3 +1,7 @@
/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally
import { TextEncoder, TextDecoder } from "util";
Object.assign(global, { TextDecoder, TextEncoder });
import { mock } from "jest-mock-extended";
import { of, firstValueFrom } from "rxjs";

View File

@@ -1,3 +1,7 @@
/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally
import { TextEncoder, TextDecoder } from "util";
Object.assign(global, { TextDecoder, TextEncoder });
import { mock } from "jest-mock-extended";
import { of, firstValueFrom } from "rxjs";

View File

@@ -1,3 +1,7 @@
/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally
import { TextEncoder, TextDecoder } from "util";
Object.assign(global, { TextDecoder, TextEncoder });
import { mock } from "jest-mock-extended";
import { of, firstValueFrom } from "rxjs";

View File

@@ -1,3 +1,7 @@
/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally
import { TextEncoder, TextDecoder } from "util";
Object.assign(global, { TextDecoder, TextEncoder });
import { mock } from "jest-mock-extended";
import { of, firstValueFrom } from "rxjs";

View File

@@ -1,3 +1,7 @@
/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally
import { TextEncoder, TextDecoder } from "util";
Object.assign(global, { TextDecoder, TextEncoder });
import { Type } from "../metadata";
import { GeneratedCredential } from "./generated-credential";

View File

@@ -1,3 +1,7 @@
/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally
import { TextEncoder, TextDecoder } from "util";
Object.assign(global, { TextDecoder, TextEncoder });
import { Type } from "@bitwarden/generator-core";
import { GeneratedCredential } from ".";

View File

@@ -1,3 +1,7 @@
/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally
import { TextEncoder, TextDecoder } from "util";
Object.assign(global, { TextDecoder, TextEncoder });
import { mock } from "jest-mock-extended";
import { firstValueFrom, of } from "rxjs";

View File

@@ -13,7 +13,7 @@ import { LegacyPasswordGenerationService } from "./legacy-password-generation.se
import { PasswordGenerationServiceAbstraction } from "./password-generation.service.abstraction";
const { PassphraseGeneratorStrategy, PasswordGeneratorStrategy } = strategies;
const { KeyServiceRandomizer, PasswordRandomizer } = engine;
const { PureCryptoRandomizer, PasswordRandomizer } = engine;
const DefaultGeneratorService = services.DefaultGeneratorService;
@@ -24,7 +24,7 @@ export function legacyPasswordGenerationServiceFactory(
accountService: AccountService,
stateProvider: StateProvider,
): PasswordGenerationServiceAbstraction {
const randomizer = new KeyServiceRandomizer(keyService);
const randomizer = new PureCryptoRandomizer();
const passwordRandomizer = new PasswordRandomizer(randomizer, Date.now);
const passwords = new DefaultGeneratorService(

View File

@@ -14,7 +14,7 @@ import { KeyService } from "@bitwarden/key-management";
import { LegacyUsernameGenerationService } from "./legacy-username-generation.service";
import { UsernameGenerationServiceAbstraction } from "./username-generation.service.abstraction";
const { KeyServiceRandomizer, UsernameRandomizer, EmailRandomizer, EmailCalculator } = engine;
const { PureCryptoRandomizer, UsernameRandomizer, EmailRandomizer, EmailCalculator } = engine;
const DefaultGeneratorService = services.DefaultGeneratorService;
const {
CatchallGeneratorStrategy,
@@ -32,7 +32,7 @@ export function legacyUsernameGenerationServiceFactory(
accountService: AccountService,
stateProvider: StateProvider,
): UsernameGenerationServiceAbstraction {
const randomizer = new KeyServiceRandomizer(keyService);
const randomizer = new PureCryptoRandomizer();
const restClient = new RestClient(apiService, i18nService);
const usernameRandomizer = new UsernameRandomizer(randomizer);
const emailRandomizer = new EmailRandomizer(randomizer);

View File

@@ -1,3 +1,7 @@
/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally
import { TextEncoder, TextDecoder } from "util";
Object.assign(global, { TextDecoder, TextEncoder });
import { mock } from "jest-mock-extended";
import { of } from "rxjs";

View File

@@ -1,3 +1,7 @@
/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally
import { TextEncoder, TextDecoder } from "util";
Object.assign(global, { TextDecoder, TextEncoder });
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { mock } from "jest-mock-extended";

View File

@@ -1,3 +1,7 @@
/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally
import { TextEncoder, TextDecoder } from "util";
Object.assign(global, { TextDecoder, TextEncoder });
import { mock } from "jest-mock-extended";
import { firstValueFrom, of } from "rxjs";

View File

@@ -1,3 +1,7 @@
/// SDK/WASM code relies on TextEncoder/TextDecoder being available globally
import { TextEncoder, TextDecoder } from "util";
Object.assign(global, { TextDecoder, TextEncoder });
import { DefaultGeneratorNavigation } from "./default-generator-navigation";
import { GeneratorNavigationEvaluator } from "./generator-navigation-evaluator";