mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
[PM-19287] Feature flag for encrypt service (#13894)
* Extract getFeatureFlagValue to pure function Co-authored-by: Matt Gibson <mgibson@bitwarden.com> * Add broadcasting abstractions and OnServerConfigChange interface. Co-authored-by: Matt Gibson <mgibson@bitwarden.com> * Add implementation of onServerConfigChange on encrypt services Co-authored-by: Matt Gibson <mgibson@bitwarden.com> * Add onServerConfigChange implementation for encrypt worker Co-authored-by: Matt Gibson <mgibson@bitwarden.com> * Wire up broadcasting in dependency injection Co-authored-by: Matt Gibson <mgibson@bitwarden.com> * Add unit tests * Handle subscribing for onServerConfigChange in init services --------- Co-authored-by: Matt Gibson <mgibson@bitwarden.com>
This commit is contained in:
@@ -1309,6 +1309,13 @@ export default class MainBackground {
|
||||
// Only the "true" background should run migrations
|
||||
await this.stateService.init({ runMigrations: true });
|
||||
|
||||
this.configService.serverConfig$.subscribe((newConfig) => {
|
||||
if (newConfig != null) {
|
||||
this.encryptService.onServerConfigChange(newConfig);
|
||||
this.bulkEncryptService.onServerConfigChange(newConfig);
|
||||
}
|
||||
});
|
||||
|
||||
// This is here instead of in in the InitService b/c we don't plan for
|
||||
// side effects to run in the Browser InitService.
|
||||
const accounts = await firstValueFrom(this.accountService.accounts$);
|
||||
|
||||
@@ -3,6 +3,9 @@ import { inject, Inject, Injectable } from "@angular/core";
|
||||
|
||||
import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { BulkEncryptService } from "@bitwarden/common/key-management/crypto/abstractions/bulk-encrypt.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService as LogServiceAbstraction } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
@@ -27,6 +30,9 @@ export class InitService {
|
||||
private themingService: AbstractThemingService,
|
||||
private sdkLoadService: SdkLoadService,
|
||||
private viewCacheService: PopupViewCacheService,
|
||||
private configService: ConfigService,
|
||||
private encryptService: EncryptService,
|
||||
private bulkEncryptService: BulkEncryptService,
|
||||
@Inject(DOCUMENT) private document: Document,
|
||||
) {}
|
||||
|
||||
@@ -34,6 +40,12 @@ export class InitService {
|
||||
return async () => {
|
||||
await this.sdkLoadService.loadAndInit();
|
||||
await this.stateService.init({ runMigrations: false }); // Browser background is responsible for migrations
|
||||
this.configService.serverConfig$.subscribe((newConfig) => {
|
||||
if (newConfig != null) {
|
||||
this.encryptService.onServerConfigChange(newConfig);
|
||||
this.bulkEncryptService.onServerConfigChange(newConfig);
|
||||
}
|
||||
});
|
||||
await this.i18nService.init();
|
||||
this.twoFactorService.init();
|
||||
await this.viewCacheService.init();
|
||||
|
||||
@@ -283,6 +283,7 @@ export class ServiceContainer {
|
||||
cipherAuthorizationService: CipherAuthorizationService;
|
||||
ssoUrlService: SsoUrlService;
|
||||
masterPasswordApiService: MasterPasswordApiServiceAbstraction;
|
||||
bulkEncryptService: FallbackBulkEncryptService;
|
||||
|
||||
constructor() {
|
||||
let p = null;
|
||||
@@ -314,6 +315,7 @@ export class ServiceContainer {
|
||||
this.logService,
|
||||
true,
|
||||
);
|
||||
this.bulkEncryptService = new FallbackBulkEncryptService(this.encryptService);
|
||||
this.storageService = new LowdbStorageService(this.logService, null, p, false, true);
|
||||
this.secureStorageService = new NodeEnvSecureStorageService(
|
||||
this.storageService,
|
||||
@@ -686,7 +688,7 @@ export class ServiceContainer {
|
||||
this.stateService,
|
||||
this.autofillSettingsService,
|
||||
this.encryptService,
|
||||
new FallbackBulkEncryptService(this.encryptService),
|
||||
this.bulkEncryptService,
|
||||
this.cipherFileUploadService,
|
||||
this.configService,
|
||||
this.stateProvider,
|
||||
@@ -885,6 +887,12 @@ export class ServiceContainer {
|
||||
await this.sdkLoadService.loadAndInit();
|
||||
await this.storageService.init();
|
||||
await this.stateService.init();
|
||||
this.configService.serverConfig$.subscribe((newConfig) => {
|
||||
if (newConfig != null) {
|
||||
this.encryptService.onServerConfigChange(newConfig);
|
||||
this.bulkEncryptService.onServerConfigChange(newConfig);
|
||||
}
|
||||
});
|
||||
this.containerService.attachToGlobal(global);
|
||||
await this.i18nService.init();
|
||||
this.twoFactorService.init();
|
||||
|
||||
@@ -7,8 +7,10 @@ import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
|
||||
import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { BulkEncryptService } from "@bitwarden/common/key-management/crypto/abstractions/bulk-encrypt.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { DefaultVaultTimeoutService } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
|
||||
@@ -49,6 +51,8 @@ export class InitService {
|
||||
private sshAgentService: SshAgentService,
|
||||
private autofillService: DesktopAutofillService,
|
||||
private sdkLoadService: SdkLoadService,
|
||||
private configService: ConfigService,
|
||||
private bulkEncryptService: BulkEncryptService,
|
||||
@Inject(DOCUMENT) private document: Document,
|
||||
) {}
|
||||
|
||||
@@ -59,6 +63,13 @@ export class InitService {
|
||||
this.nativeMessagingService.init();
|
||||
await this.stateService.init({ runMigrations: false }); // Desktop will run them in main process
|
||||
|
||||
this.configService.serverConfig$.subscribe((newConfig) => {
|
||||
if (newConfig != null) {
|
||||
this.encryptService.onServerConfigChange(newConfig);
|
||||
this.bulkEncryptService.onServerConfigChange(newConfig);
|
||||
}
|
||||
});
|
||||
|
||||
const accounts = await firstValueFrom(this.accountService.accounts$);
|
||||
const setUserKeyInMemoryPromises = [];
|
||||
for (const userId of Object.keys(accounts) as UserId[]) {
|
||||
|
||||
@@ -7,8 +7,10 @@ import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
|
||||
import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { BulkEncryptService } from "@bitwarden/common/key-management/crypto/abstractions/bulk-encrypt.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { DefaultVaultTimeoutService } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
|
||||
import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
@@ -37,6 +39,8 @@ export class InitService {
|
||||
private accountService: AccountService,
|
||||
private versionService: VersionService,
|
||||
private sdkLoadService: SdkLoadService,
|
||||
private configService: ConfigService,
|
||||
private bulkEncryptService: BulkEncryptService,
|
||||
@Inject(DOCUMENT) private document: Document,
|
||||
) {}
|
||||
|
||||
@@ -45,6 +49,13 @@ export class InitService {
|
||||
await this.sdkLoadService.loadAndInit();
|
||||
await this.stateService.init();
|
||||
|
||||
this.configService.serverConfig$.subscribe((newConfig) => {
|
||||
if (newConfig != null) {
|
||||
this.encryptService.onServerConfigChange(newConfig);
|
||||
this.bulkEncryptService.onServerConfigChange(newConfig);
|
||||
}
|
||||
});
|
||||
|
||||
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||
if (activeAccount) {
|
||||
// If there is an active account, we must await the process of setting the user key in memory
|
||||
|
||||
38
libs/common/src/enums/feature-flag.enum.spec.ts
Normal file
38
libs/common/src/enums/feature-flag.enum.spec.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { ServerConfig } from "../platform/abstractions/config/server-config";
|
||||
|
||||
import { getFeatureFlagValue, FeatureFlag, DefaultFeatureFlagValue } from "./feature-flag.enum";
|
||||
|
||||
describe("getFeatureFlagValue", () => {
|
||||
const testFlag = Object.values(FeatureFlag)[0];
|
||||
const testFlagDefaultValue = DefaultFeatureFlagValue[testFlag];
|
||||
|
||||
it("returns default flag value when serverConfig is null", () => {
|
||||
const result = getFeatureFlagValue(null, testFlag);
|
||||
expect(result).toBe(testFlagDefaultValue);
|
||||
});
|
||||
|
||||
it("returns default flag value when serverConfig.featureStates is undefined", () => {
|
||||
const serverConfig = {} as ServerConfig;
|
||||
const result = getFeatureFlagValue(serverConfig, testFlag);
|
||||
expect(result).toBe(testFlagDefaultValue);
|
||||
});
|
||||
|
||||
it("returns default flag value when the feature flag is not in serverConfig.featureStates", () => {
|
||||
const serverConfig = mock<ServerConfig>();
|
||||
serverConfig.featureStates = {};
|
||||
|
||||
const result = getFeatureFlagValue(serverConfig, testFlag);
|
||||
expect(result).toBe(testFlagDefaultValue);
|
||||
});
|
||||
|
||||
it("returns the flag value from serverConfig.featureStates when the feature flag exists", () => {
|
||||
const expectedValue = true;
|
||||
const serverConfig = mock<ServerConfig>();
|
||||
serverConfig.featureStates = { [testFlag]: expectedValue };
|
||||
|
||||
const result = getFeatureFlagValue(serverConfig, testFlag);
|
||||
expect(result).toBe(expectedValue);
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,5 @@
|
||||
import { ServerConfig } from "../platform/abstractions/config/server-config";
|
||||
|
||||
/**
|
||||
* Feature flags.
|
||||
*
|
||||
@@ -123,3 +125,14 @@ export const DefaultFeatureFlagValue = {
|
||||
export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;
|
||||
|
||||
export type FeatureFlagValueType<Flag extends FeatureFlag> = DefaultFeatureFlagValueType[Flag];
|
||||
|
||||
export function getFeatureFlagValue<Flag extends FeatureFlag>(
|
||||
serverConfig: ServerConfig | null,
|
||||
flag: Flag,
|
||||
) {
|
||||
if (serverConfig?.featureStates == null || serverConfig.featureStates[flag] == null) {
|
||||
return DefaultFeatureFlagValue[flag];
|
||||
}
|
||||
|
||||
return serverConfig.featureStates[flag] as FeatureFlagValueType<Flag>;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface";
|
||||
import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
|
||||
import { Decryptable } from "../../../platform/interfaces/decryptable.interface";
|
||||
import { InitializerMetadata } from "../../../platform/interfaces/initializer-metadata.interface";
|
||||
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
|
||||
export abstract class BulkEncryptService {
|
||||
abstract decryptItems<T extends InitializerMetadata>(
|
||||
items: Decryptable<T>[],
|
||||
key: SymmetricCryptoKey,
|
||||
): Promise<T[]>;
|
||||
|
||||
abstract onServerConfigChange(newConfig: ServerConfig): void;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface";
|
||||
import { Encrypted } from "@bitwarden/common/platform/interfaces/encrypted";
|
||||
import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface";
|
||||
import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
|
||||
import { Decryptable } from "../../../platform/interfaces/decryptable.interface";
|
||||
import { Encrypted } from "../../../platform/interfaces/encrypted";
|
||||
import { InitializerMetadata } from "../../../platform/interfaces/initializer-metadata.interface";
|
||||
import { EncArrayBuffer } from "../../../platform/models/domain/enc-array-buffer";
|
||||
import { EncString } from "../../../platform/models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
|
||||
export abstract class EncryptService {
|
||||
abstract encrypt(plainValue: string | Uint8Array, key: SymmetricCryptoKey): Promise<EncString>;
|
||||
@@ -54,4 +55,6 @@ export abstract class EncryptService {
|
||||
value: string | Uint8Array,
|
||||
algorithm: "sha1" | "sha256" | "sha512",
|
||||
): Promise<string>;
|
||||
|
||||
abstract onServerConfigChange(newConfig: ServerConfig): void;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import * as rxjs from "rxjs";
|
||||
|
||||
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
|
||||
import { CryptoFunctionService } from "../../../platform/abstractions/crypto-function.service";
|
||||
import { LogService } from "../../../platform/abstractions/log.service";
|
||||
import { Decryptable } from "../../../platform/interfaces/decryptable.interface";
|
||||
import { InitializerMetadata } from "../../../platform/interfaces/initializer-metadata.interface";
|
||||
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
import { buildSetConfigMessage } from "../types/worker-command.type";
|
||||
|
||||
import { BulkEncryptServiceImplementation } from "./bulk-encrypt.service.implementation";
|
||||
|
||||
describe("BulkEncryptServiceImplementation", () => {
|
||||
const cryptoFunctionService = mock<CryptoFunctionService>();
|
||||
const logService = mock<LogService>();
|
||||
|
||||
let sut: BulkEncryptServiceImplementation;
|
||||
|
||||
beforeEach(() => {
|
||||
sut = new BulkEncryptServiceImplementation(cryptoFunctionService, logService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe("decryptItems", () => {
|
||||
const key = mock<SymmetricCryptoKey>();
|
||||
const serverConfig = mock<ServerConfig>();
|
||||
const mockWorker = mock<Worker>();
|
||||
let globalWindow: any;
|
||||
|
||||
beforeEach(() => {
|
||||
globalWindow = global.window;
|
||||
|
||||
// Mock creating a worker.
|
||||
global.Worker = jest.fn().mockImplementation(() => mockWorker);
|
||||
global.URL = jest.fn().mockImplementation(() => "url") as unknown as typeof URL;
|
||||
global.URL.createObjectURL = jest.fn().mockReturnValue("blob:url");
|
||||
global.URL.revokeObjectURL = jest.fn();
|
||||
global.URL.canParse = jest.fn().mockReturnValue(true);
|
||||
|
||||
// Mock the workers returned response.
|
||||
const mockMessageEvent = {
|
||||
id: "mock-guid",
|
||||
data: ["decrypted1", "decrypted2"],
|
||||
};
|
||||
const mockMessageEvent$ = rxjs.from([mockMessageEvent]);
|
||||
jest.spyOn(rxjs, "fromEvent").mockReturnValue(mockMessageEvent$);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
global.window = globalWindow;
|
||||
});
|
||||
|
||||
it("throws error if key is null", async () => {
|
||||
const nullKey = null as unknown as SymmetricCryptoKey;
|
||||
await expect(sut.decryptItems([], nullKey)).rejects.toThrow("No encryption key provided.");
|
||||
});
|
||||
|
||||
it("returns an empty array when items is null", async () => {
|
||||
const result = await sut.decryptItems(null as any, key);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns an empty array when items is empty", async () => {
|
||||
const result = await sut.decryptItems([], key);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("decrypts items sequentially when window is undefined", async () => {
|
||||
// Make global window undefined.
|
||||
delete (global as any).window;
|
||||
|
||||
const mockItems = [createMockDecryptable("item1"), createMockDecryptable("item2")];
|
||||
|
||||
const result = await sut.decryptItems(mockItems, key);
|
||||
|
||||
expect(logService.info).toHaveBeenCalledWith(
|
||||
"Window not available in BulkEncryptService, decrypting sequentially",
|
||||
);
|
||||
expect(result).toEqual(["item1", "item2"]);
|
||||
expect(mockItems[0].decrypt).toHaveBeenCalledWith(key);
|
||||
expect(mockItems[1].decrypt).toHaveBeenCalledWith(key);
|
||||
});
|
||||
|
||||
it("uses workers for decryption when window is available", async () => {
|
||||
const mockDecryptedItems = ["decrypted1", "decrypted2"];
|
||||
jest
|
||||
.spyOn<any, any>(sut, "getDecryptedItemsFromWorkers")
|
||||
.mockResolvedValue(mockDecryptedItems);
|
||||
|
||||
const mockItems = [createMockDecryptable("item1"), createMockDecryptable("item2")];
|
||||
|
||||
const result = await sut.decryptItems(mockItems, key);
|
||||
|
||||
expect(sut["getDecryptedItemsFromWorkers"]).toHaveBeenCalledWith(mockItems, key);
|
||||
expect(result).toEqual(mockDecryptedItems);
|
||||
});
|
||||
|
||||
it("creates new worker when none exist", async () => {
|
||||
(sut as any).currentServerConfig = undefined;
|
||||
const mockItems = [createMockDecryptable("item1"), createMockDecryptable("item2")];
|
||||
|
||||
await sut.decryptItems(mockItems, key);
|
||||
|
||||
expect(global.Worker).toHaveBeenCalled();
|
||||
expect(mockWorker.postMessage).toHaveBeenCalledTimes(1);
|
||||
expect(mockWorker.postMessage).not.toHaveBeenCalledWith(
|
||||
buildSetConfigMessage({ newConfig: serverConfig }),
|
||||
);
|
||||
});
|
||||
|
||||
it("sends a SetConfigMessage to the new worker when there is a current server config", async () => {
|
||||
(sut as any).currentServerConfig = serverConfig;
|
||||
const mockItems = [createMockDecryptable("item1"), createMockDecryptable("item2")];
|
||||
|
||||
await sut.decryptItems(mockItems, key);
|
||||
|
||||
expect(global.Worker).toHaveBeenCalled();
|
||||
expect(mockWorker.postMessage).toHaveBeenCalledTimes(2);
|
||||
expect(mockWorker.postMessage).toHaveBeenCalledWith(
|
||||
buildSetConfigMessage({ newConfig: serverConfig }),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not create worker if one exists", async () => {
|
||||
(sut as any).currentServerConfig = serverConfig;
|
||||
(sut as any).workers = [mockWorker];
|
||||
const mockItems = [createMockDecryptable("item1"), createMockDecryptable("item2")];
|
||||
|
||||
await sut.decryptItems(mockItems, key);
|
||||
|
||||
expect(global.Worker).not.toHaveBeenCalled();
|
||||
expect(mockWorker.postMessage).toHaveBeenCalledTimes(1);
|
||||
expect(mockWorker.postMessage).not.toHaveBeenCalledWith(
|
||||
buildSetConfigMessage({ newConfig: serverConfig }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("onServerConfigChange", () => {
|
||||
it("updates internal currentServerConfig to new config", () => {
|
||||
const newConfig = mock<ServerConfig>();
|
||||
|
||||
sut.onServerConfigChange(newConfig);
|
||||
|
||||
expect((sut as any).currentServerConfig).toBe(newConfig);
|
||||
});
|
||||
|
||||
it("does send a SetConfigMessage to workers when there is a worker", () => {
|
||||
const newConfig = mock<ServerConfig>();
|
||||
const mockWorker = mock<Worker>();
|
||||
(sut as any).workers = [mockWorker];
|
||||
|
||||
sut.onServerConfigChange(newConfig);
|
||||
|
||||
expect(mockWorker.postMessage).toHaveBeenCalledWith(buildSetConfigMessage({ newConfig }));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createMockDecryptable<T extends InitializerMetadata>(
|
||||
returnValue: any,
|
||||
): MockProxy<Decryptable<T>> {
|
||||
const mockDecryptable = mock<Decryptable<T>>();
|
||||
mockDecryptable.decrypt.mockResolvedValue(returnValue);
|
||||
return mockDecryptable;
|
||||
}
|
||||
@@ -12,6 +12,9 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { getClassInitializer } from "@bitwarden/common/platform/services/cryptography/get-class-initializer";
|
||||
|
||||
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
|
||||
import { buildDecryptMessage, buildSetConfigMessage } from "../types/worker-command.type";
|
||||
|
||||
// TTL (time to live) is not strictly required but avoids tying up memory resources if inactive
|
||||
const workerTTL = 60000; // 1 minute
|
||||
const maxWorkers = 8;
|
||||
@@ -20,6 +23,7 @@ const minNumberOfItemsForMultithreading = 400;
|
||||
export class BulkEncryptServiceImplementation implements BulkEncryptService {
|
||||
private workers: Worker[] = [];
|
||||
private timeout: any;
|
||||
private currentServerConfig: ServerConfig | undefined = undefined;
|
||||
|
||||
private clear$ = new Subject<void>();
|
||||
|
||||
@@ -57,6 +61,11 @@ export class BulkEncryptServiceImplementation implements BulkEncryptService {
|
||||
return decryptedItems;
|
||||
}
|
||||
|
||||
onServerConfigChange(newConfig: ServerConfig): void {
|
||||
this.currentServerConfig = newConfig;
|
||||
this.updateWorkerServerConfigs(newConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends items to a set of web workers to decrypt them. This utilizes multiple workers to decrypt items
|
||||
* faster without interrupting other operations (e.g. updating UI).
|
||||
@@ -93,6 +102,9 @@ export class BulkEncryptServiceImplementation implements BulkEncryptService {
|
||||
),
|
||||
);
|
||||
}
|
||||
if (this.currentServerConfig != undefined) {
|
||||
this.updateWorkerServerConfigs(this.currentServerConfig);
|
||||
}
|
||||
}
|
||||
|
||||
const itemsPerWorker = Math.floor(items.length / this.workers.length);
|
||||
@@ -108,17 +120,18 @@ export class BulkEncryptServiceImplementation implements BulkEncryptService {
|
||||
itemsForWorker.push(...items.slice(end));
|
||||
}
|
||||
|
||||
const request = {
|
||||
id: Utils.newGuid(),
|
||||
const id = Utils.newGuid();
|
||||
const request = buildDecryptMessage({
|
||||
id,
|
||||
items: itemsForWorker,
|
||||
key: key,
|
||||
};
|
||||
});
|
||||
|
||||
worker.postMessage(JSON.stringify(request));
|
||||
worker.postMessage(request);
|
||||
results.push(
|
||||
firstValueFrom(
|
||||
fromEvent(worker, "message").pipe(
|
||||
filter((response: MessageEvent) => response.data?.id === request.id),
|
||||
filter((response: MessageEvent) => response.data?.id === id),
|
||||
map((response) => JSON.parse(response.data.items)),
|
||||
map((items) =>
|
||||
items.map((jsonItem: Jsonify<T>) => {
|
||||
@@ -143,6 +156,13 @@ export class BulkEncryptServiceImplementation implements BulkEncryptService {
|
||||
return decryptedItems;
|
||||
}
|
||||
|
||||
private updateWorkerServerConfigs(newConfig: ServerConfig) {
|
||||
this.workers.forEach((worker) => {
|
||||
const request = buildSetConfigMessage({ newConfig });
|
||||
worker.postMessage(request);
|
||||
});
|
||||
}
|
||||
|
||||
private clear() {
|
||||
this.clear$.next();
|
||||
for (const worker of this.workers) {
|
||||
|
||||
@@ -15,6 +15,7 @@ import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { EncryptedObject } from "@bitwarden/common/platform/models/domain/encrypted-object";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
|
||||
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
|
||||
import { EncryptService } from "../abstractions/encrypt.service";
|
||||
|
||||
export class EncryptServiceImplementation implements EncryptService {
|
||||
@@ -24,6 +25,11 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
protected logMacFailures: boolean,
|
||||
) {}
|
||||
|
||||
// Handle updating private properties to turn on/off feature flags.
|
||||
onServerConfigChange(newConfig: ServerConfig): void {
|
||||
return;
|
||||
}
|
||||
|
||||
async encrypt(plainValue: string | Uint8Array, key: SymmetricCryptoKey): Promise<EncString> {
|
||||
if (key == null) {
|
||||
throw new Error("No encryption key provided.");
|
||||
|
||||
@@ -2,12 +2,19 @@
|
||||
// @ts-strict-ignore
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
|
||||
import { ContainerService } from "@bitwarden/common/platform/services/container.service";
|
||||
import { getClassInitializer } from "@bitwarden/common/platform/services/cryptography/get-class-initializer";
|
||||
import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service";
|
||||
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
|
||||
import { LogService } from "../../../platform/abstractions/log.service";
|
||||
import { Decryptable } from "../../../platform/interfaces/decryptable.interface";
|
||||
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
import { ConsoleLogService } from "../../../platform/services/console-log.service";
|
||||
import { ContainerService } from "../../../platform/services/container.service";
|
||||
import { getClassInitializer } from "../../../platform/services/cryptography/get-class-initializer";
|
||||
import { WebCryptoFunctionService } from "../../../platform/services/web-crypto-function.service";
|
||||
import {
|
||||
DECRYPT_COMMAND,
|
||||
SET_CONFIG_COMMAND,
|
||||
ParsedDecryptCommandData,
|
||||
} from "../types/worker-command.type";
|
||||
|
||||
import { EncryptServiceImplementation } from "./encrypt.service.implementation";
|
||||
|
||||
@@ -15,13 +22,14 @@ const workerApi: Worker = self as any;
|
||||
|
||||
let inited = false;
|
||||
let encryptService: EncryptServiceImplementation;
|
||||
let logService: LogService;
|
||||
|
||||
/**
|
||||
* Bootstrap the worker environment with services required for decryption
|
||||
*/
|
||||
export function init() {
|
||||
const cryptoFunctionService = new WebCryptoFunctionService(self);
|
||||
const logService = new ConsoleLogService(false);
|
||||
logService = new ConsoleLogService(false);
|
||||
encryptService = new EncryptServiceImplementation(cryptoFunctionService, logService, true);
|
||||
|
||||
const bitwardenContainerService = new ContainerService(null, encryptService);
|
||||
@@ -39,11 +47,22 @@ workerApi.addEventListener("message", async (event: { data: string }) => {
|
||||
}
|
||||
|
||||
const request: {
|
||||
id: string;
|
||||
items: Jsonify<Decryptable<any>>[];
|
||||
key: Jsonify<SymmetricCryptoKey>;
|
||||
command: string;
|
||||
} = JSON.parse(event.data);
|
||||
|
||||
switch (request.command) {
|
||||
case DECRYPT_COMMAND:
|
||||
return await handleDecrypt(request as unknown as ParsedDecryptCommandData);
|
||||
case SET_CONFIG_COMMAND: {
|
||||
const newConfig = (request as unknown as { newConfig: Jsonify<ServerConfig> }).newConfig;
|
||||
return await handleSetConfig(newConfig);
|
||||
}
|
||||
default:
|
||||
logService.error(`[EncryptWorker] unknown worker command`, request.command, request);
|
||||
}
|
||||
});
|
||||
|
||||
async function handleDecrypt(request: ParsedDecryptCommandData) {
|
||||
const key = SymmetricCryptoKey.fromJSON(request.key);
|
||||
const items = request.items.map((jsonItem) => {
|
||||
const initializer = getClassInitializer<Decryptable<any>>(jsonItem.initializerKey);
|
||||
@@ -55,4 +74,8 @@ workerApi.addEventListener("message", async (event: { data: string }) => {
|
||||
id: request.id,
|
||||
items: JSON.stringify(result),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function handleSetConfig(newConfig: Jsonify<ServerConfig>) {
|
||||
encryptService.onServerConfigChange(ServerConfig.fromJSON(newConfig));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
|
||||
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
import { BulkEncryptService } from "../abstractions/bulk-encrypt.service";
|
||||
import { EncryptService } from "../abstractions/encrypt.service";
|
||||
|
||||
import { FallbackBulkEncryptService } from "./fallback-bulk-encrypt.service";
|
||||
|
||||
describe("FallbackBulkEncryptService", () => {
|
||||
const encryptService = mock<EncryptService>();
|
||||
const featureFlagEncryptService = mock<BulkEncryptService>();
|
||||
const serverConfig = mock<ServerConfig>();
|
||||
|
||||
let sut: FallbackBulkEncryptService;
|
||||
|
||||
beforeEach(() => {
|
||||
sut = new FallbackBulkEncryptService(encryptService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe("decryptItems", () => {
|
||||
const key = mock<SymmetricCryptoKey>();
|
||||
const mockItems = [{ id: "guid", name: "encryptedValue" }] as any[];
|
||||
const mockDecryptedItems = [{ id: "guid", name: "decryptedValue" }] as any[];
|
||||
|
||||
it("calls decryptItems on featureFlagEncryptService when it is set", async () => {
|
||||
featureFlagEncryptService.decryptItems.mockResolvedValue(mockDecryptedItems);
|
||||
await sut.setFeatureFlagEncryptService(featureFlagEncryptService);
|
||||
|
||||
const result = await sut.decryptItems(mockItems, key);
|
||||
|
||||
expect(featureFlagEncryptService.decryptItems).toHaveBeenCalledWith(mockItems, key);
|
||||
expect(encryptService.decryptItems).not.toHaveBeenCalled();
|
||||
expect(result).toEqual(mockDecryptedItems);
|
||||
});
|
||||
|
||||
it("calls decryptItems on encryptService when featureFlagEncryptService is not set", async () => {
|
||||
encryptService.decryptItems.mockResolvedValue(mockDecryptedItems);
|
||||
|
||||
const result = await sut.decryptItems(mockItems, key);
|
||||
|
||||
expect(encryptService.decryptItems).toHaveBeenCalledWith(mockItems, key);
|
||||
expect(result).toEqual(mockDecryptedItems);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setFeatureFlagEncryptService", () => {
|
||||
it("sets the featureFlagEncryptService property", async () => {
|
||||
await sut.setFeatureFlagEncryptService(featureFlagEncryptService);
|
||||
|
||||
expect((sut as any).featureFlagEncryptService).toBe(featureFlagEncryptService);
|
||||
});
|
||||
|
||||
it("does not call onServerConfigChange when currentServerConfig is undefined", async () => {
|
||||
await sut.setFeatureFlagEncryptService(featureFlagEncryptService);
|
||||
|
||||
expect(featureFlagEncryptService.onServerConfigChange).not.toHaveBeenCalled();
|
||||
expect((sut as any).featureFlagEncryptService).toBe(featureFlagEncryptService);
|
||||
});
|
||||
|
||||
it("calls onServerConfigChange with currentServerConfig when it is defined", async () => {
|
||||
sut.onServerConfigChange(serverConfig);
|
||||
|
||||
await sut.setFeatureFlagEncryptService(featureFlagEncryptService);
|
||||
|
||||
expect(featureFlagEncryptService.onServerConfigChange).toHaveBeenCalledWith(serverConfig);
|
||||
expect((sut as any).featureFlagEncryptService).toBe(featureFlagEncryptService);
|
||||
});
|
||||
});
|
||||
|
||||
describe("onServerConfigChange", () => {
|
||||
it("updates internal currentServerConfig to new config", async () => {
|
||||
sut.onServerConfigChange(serverConfig);
|
||||
|
||||
expect((sut as any).currentServerConfig).toBe(serverConfig);
|
||||
});
|
||||
|
||||
it("calls onServerConfigChange on featureFlagEncryptService when it is set", async () => {
|
||||
await sut.setFeatureFlagEncryptService(featureFlagEncryptService);
|
||||
|
||||
sut.onServerConfigChange(serverConfig);
|
||||
|
||||
expect(featureFlagEncryptService.onServerConfigChange).toHaveBeenCalledWith(serverConfig);
|
||||
expect(encryptService.onServerConfigChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calls onServerConfigChange on encryptService when featureFlagEncryptService is not set", () => {
|
||||
sut.onServerConfigChange(serverConfig);
|
||||
|
||||
expect(encryptService.onServerConfigChange).toHaveBeenCalledWith(serverConfig);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,6 +5,7 @@ import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.i
|
||||
import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
|
||||
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
|
||||
import { EncryptService } from "../abstractions/encrypt.service";
|
||||
|
||||
/**
|
||||
@@ -12,6 +13,7 @@ import { EncryptService } from "../abstractions/encrypt.service";
|
||||
*/
|
||||
export class FallbackBulkEncryptService implements BulkEncryptService {
|
||||
private featureFlagEncryptService: BulkEncryptService;
|
||||
private currentServerConfig: ServerConfig | undefined = undefined;
|
||||
|
||||
constructor(protected encryptService: EncryptService) {}
|
||||
|
||||
@@ -31,6 +33,14 @@ export class FallbackBulkEncryptService implements BulkEncryptService {
|
||||
}
|
||||
|
||||
async setFeatureFlagEncryptService(featureFlagEncryptService: BulkEncryptService) {
|
||||
if (this.currentServerConfig !== undefined) {
|
||||
featureFlagEncryptService.onServerConfigChange(this.currentServerConfig);
|
||||
}
|
||||
this.featureFlagEncryptService = featureFlagEncryptService;
|
||||
}
|
||||
|
||||
onServerConfigChange(newConfig: ServerConfig): void {
|
||||
this.currentServerConfig = newConfig;
|
||||
(this.featureFlagEncryptService ?? this.encryptService).onServerConfigChange(newConfig);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
import * as rxjs from "rxjs";
|
||||
|
||||
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
|
||||
import { CryptoFunctionService } from "../../../platform/abstractions/crypto-function.service";
|
||||
import { LogService } from "../../../platform/abstractions/log.service";
|
||||
import { Decryptable } from "../../../platform/interfaces/decryptable.interface";
|
||||
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
import { buildSetConfigMessage } from "../types/worker-command.type";
|
||||
|
||||
import { EncryptServiceImplementation } from "./encrypt.service.implementation";
|
||||
import { MultithreadEncryptServiceImplementation } from "./multithread-encrypt.service.implementation";
|
||||
|
||||
describe("MultithreadEncryptServiceImplementation", () => {
|
||||
const cryptoFunctionService = mock<CryptoFunctionService>();
|
||||
const logService = mock<LogService>();
|
||||
const serverConfig = mock<ServerConfig>();
|
||||
|
||||
let sut: MultithreadEncryptServiceImplementation;
|
||||
|
||||
beforeEach(() => {
|
||||
sut = new MultithreadEncryptServiceImplementation(cryptoFunctionService, logService, true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe("decryptItems", () => {
|
||||
const key = mock<SymmetricCryptoKey>();
|
||||
const mockWorker = mock<Worker>();
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock creating a worker.
|
||||
global.Worker = jest.fn().mockImplementation(() => mockWorker);
|
||||
global.URL = jest.fn().mockImplementation(() => "url") as unknown as typeof URL;
|
||||
global.URL.createObjectURL = jest.fn().mockReturnValue("blob:url");
|
||||
global.URL.revokeObjectURL = jest.fn();
|
||||
global.URL.canParse = jest.fn().mockReturnValue(true);
|
||||
|
||||
// Mock the workers returned response.
|
||||
const mockMessageEvent = {
|
||||
id: "mock-guid",
|
||||
data: ["decrypted1", "decrypted2"],
|
||||
};
|
||||
const mockMessageEvent$ = rxjs.from([mockMessageEvent]);
|
||||
jest.spyOn(rxjs, "fromEvent").mockReturnValue(mockMessageEvent$);
|
||||
});
|
||||
|
||||
it("returns empty array if items is null", async () => {
|
||||
const items = null as unknown as Decryptable<any>[];
|
||||
const result = await sut.decryptItems(items, key);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns empty array if items is empty", async () => {
|
||||
const result = await sut.decryptItems([], key);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("creates worker if none exists", async () => {
|
||||
// Make sure currentServerConfig is undefined so a SetConfigMessage is not sent.
|
||||
(sut as any).currentServerConfig = undefined;
|
||||
|
||||
await sut.decryptItems([mock<Decryptable<any>>(), mock<Decryptable<any>>()], key);
|
||||
|
||||
expect(global.Worker).toHaveBeenCalled();
|
||||
expect(mockWorker.postMessage).toHaveBeenCalledTimes(1);
|
||||
expect(mockWorker.postMessage).not.toHaveBeenCalledWith(
|
||||
buildSetConfigMessage({ newConfig: serverConfig }),
|
||||
);
|
||||
});
|
||||
|
||||
it("sends a SetConfigMessage to the new worker when there is a current server config", async () => {
|
||||
// Populate currentServerConfig so a SetConfigMessage is sent.
|
||||
(sut as any).currentServerConfig = serverConfig;
|
||||
|
||||
await sut.decryptItems([mock<Decryptable<any>>(), mock<Decryptable<any>>()], key);
|
||||
|
||||
expect(global.Worker).toHaveBeenCalled();
|
||||
expect(mockWorker.postMessage).toHaveBeenCalledTimes(2);
|
||||
expect(mockWorker.postMessage).toHaveBeenCalledWith(
|
||||
buildSetConfigMessage({ newConfig: serverConfig }),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not create worker if one exists", async () => {
|
||||
(sut as any).currentServerConfig = serverConfig;
|
||||
(sut as any).worker = mockWorker;
|
||||
|
||||
await sut.decryptItems([mock<Decryptable<any>>(), mock<Decryptable<any>>()], key);
|
||||
|
||||
expect(global.Worker).not.toHaveBeenCalled();
|
||||
expect(mockWorker.postMessage).toHaveBeenCalledTimes(1);
|
||||
expect(mockWorker.postMessage).not.toHaveBeenCalledWith(
|
||||
buildSetConfigMessage({ newConfig: serverConfig }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("onServerConfigChange", () => {
|
||||
it("updates internal currentServerConfig to new config and calls super", () => {
|
||||
const superSpy = jest.spyOn(EncryptServiceImplementation.prototype, "onServerConfigChange");
|
||||
|
||||
sut.onServerConfigChange(serverConfig);
|
||||
|
||||
expect(superSpy).toHaveBeenCalledWith(serverConfig);
|
||||
expect((sut as any).currentServerConfig).toBe(serverConfig);
|
||||
});
|
||||
|
||||
it("sends config update to worker if worker exists", () => {
|
||||
const mockWorker = mock<Worker>();
|
||||
(sut as any).worker = mockWorker;
|
||||
|
||||
sut.onServerConfigChange(serverConfig);
|
||||
|
||||
expect(mockWorker.postMessage).toHaveBeenCalledTimes(1);
|
||||
expect(mockWorker.postMessage).toHaveBeenCalledWith(
|
||||
buildSetConfigMessage({ newConfig: serverConfig }),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -9,6 +9,9 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { getClassInitializer } from "@bitwarden/common/platform/services/cryptography/get-class-initializer";
|
||||
|
||||
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
|
||||
import { buildDecryptMessage, buildSetConfigMessage } from "../types/worker-command.type";
|
||||
|
||||
import { EncryptServiceImplementation } from "./encrypt.service.implementation";
|
||||
|
||||
// TTL (time to live) is not strictly required but avoids tying up memory resources if inactive
|
||||
@@ -20,6 +23,7 @@ const workerTTL = 3 * 60000; // 3 minutes
|
||||
export class MultithreadEncryptServiceImplementation extends EncryptServiceImplementation {
|
||||
private worker: Worker;
|
||||
private timeout: any;
|
||||
private currentServerConfig: ServerConfig | undefined = undefined;
|
||||
|
||||
private clear$ = new Subject<void>();
|
||||
|
||||
@@ -37,27 +41,33 @@ export class MultithreadEncryptServiceImplementation extends EncryptServiceImple
|
||||
|
||||
this.logService.info("Starting decryption using multithreading");
|
||||
|
||||
this.worker ??= new Worker(
|
||||
new URL(
|
||||
/* webpackChunkName: 'encrypt-worker' */
|
||||
"@bitwarden/common/key-management/crypto/services/encrypt.worker.ts",
|
||||
import.meta.url,
|
||||
),
|
||||
);
|
||||
if (this.worker == null) {
|
||||
this.worker = new Worker(
|
||||
new URL(
|
||||
/* webpackChunkName: 'encrypt-worker' */
|
||||
"@bitwarden/common/key-management/crypto/services/encrypt.worker.ts",
|
||||
import.meta.url,
|
||||
),
|
||||
);
|
||||
if (this.currentServerConfig !== undefined) {
|
||||
this.updateWorkerServerConfig(this.currentServerConfig);
|
||||
}
|
||||
}
|
||||
|
||||
this.restartTimeout();
|
||||
|
||||
const request = {
|
||||
id: Utils.newGuid(),
|
||||
const id = Utils.newGuid();
|
||||
const request = buildDecryptMessage({
|
||||
id,
|
||||
items: items,
|
||||
key: key,
|
||||
};
|
||||
});
|
||||
|
||||
this.worker.postMessage(JSON.stringify(request));
|
||||
this.worker.postMessage(request);
|
||||
|
||||
return await firstValueFrom(
|
||||
fromEvent(this.worker, "message").pipe(
|
||||
filter((response: MessageEvent) => response.data?.id === request.id),
|
||||
filter((response: MessageEvent) => response.data?.id === id),
|
||||
map((response) => JSON.parse(response.data.items)),
|
||||
map((items) =>
|
||||
items.map((jsonItem: Jsonify<T>) => {
|
||||
@@ -71,6 +81,19 @@ export class MultithreadEncryptServiceImplementation extends EncryptServiceImple
|
||||
);
|
||||
}
|
||||
|
||||
override onServerConfigChange(newConfig: ServerConfig): void {
|
||||
this.currentServerConfig = newConfig;
|
||||
super.onServerConfigChange(newConfig);
|
||||
this.updateWorkerServerConfig(newConfig);
|
||||
}
|
||||
|
||||
private updateWorkerServerConfig(newConfig: ServerConfig) {
|
||||
if (this.worker != null) {
|
||||
const request = buildSetConfigMessage({ newConfig });
|
||||
this.worker.postMessage(request);
|
||||
}
|
||||
}
|
||||
|
||||
private clear() {
|
||||
this.clear$.next();
|
||||
this.worker?.terminate();
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { makeStaticByteArray } from "../../../../spec";
|
||||
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
|
||||
import { Decryptable } from "../../../platform/interfaces/decryptable.interface";
|
||||
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
|
||||
import {
|
||||
DECRYPT_COMMAND,
|
||||
DecryptCommandData,
|
||||
SET_CONFIG_COMMAND,
|
||||
buildDecryptMessage,
|
||||
buildSetConfigMessage,
|
||||
} from "./worker-command.type";
|
||||
|
||||
describe("Worker command types", () => {
|
||||
describe("buildDecryptMessage", () => {
|
||||
it("builds a message with the correct command", () => {
|
||||
const commandData = createDecryptCommandData();
|
||||
|
||||
const result = buildDecryptMessage(commandData);
|
||||
|
||||
const parsedResult = JSON.parse(result);
|
||||
expect(parsedResult.command).toBe(DECRYPT_COMMAND);
|
||||
});
|
||||
|
||||
it("includes the provided data in the message", () => {
|
||||
const mockItems = [{ encrypted: "test-encrypted" } as unknown as Decryptable<any>];
|
||||
const commandData = createDecryptCommandData(mockItems);
|
||||
|
||||
const result = buildDecryptMessage(commandData);
|
||||
|
||||
const parsedResult = JSON.parse(result);
|
||||
expect(parsedResult.command).toBe(DECRYPT_COMMAND);
|
||||
expect(parsedResult.id).toBe("test-id");
|
||||
expect(parsedResult.items).toEqual(mockItems);
|
||||
expect(SymmetricCryptoKey.fromJSON(parsedResult.key)).toEqual(commandData.key);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildSetConfigMessage", () => {
|
||||
it("builds a message with the correct command", () => {
|
||||
const result = buildSetConfigMessage({ newConfig: mock<ServerConfig>() });
|
||||
|
||||
const parsedResult = JSON.parse(result);
|
||||
expect(parsedResult.command).toBe(SET_CONFIG_COMMAND);
|
||||
});
|
||||
|
||||
it("includes the provided data in the message", () => {
|
||||
const serverConfig = { version: "test-version" } as unknown as ServerConfig;
|
||||
|
||||
const result = buildSetConfigMessage({ newConfig: serverConfig });
|
||||
|
||||
const parsedResult = JSON.parse(result);
|
||||
expect(parsedResult.command).toBe(SET_CONFIG_COMMAND);
|
||||
expect(ServerConfig.fromJSON(parsedResult.newConfig).version).toEqual(serverConfig.version);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createDecryptCommandData(items?: Decryptable<any>[]): DecryptCommandData {
|
||||
return {
|
||||
id: "test-id",
|
||||
items: items ?? [],
|
||||
key: new SymmetricCryptoKey(makeStaticByteArray(64)),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
|
||||
import { Decryptable } from "../../../platform/interfaces/decryptable.interface";
|
||||
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
|
||||
export const DECRYPT_COMMAND = "decrypt";
|
||||
export const SET_CONFIG_COMMAND = "updateConfig";
|
||||
|
||||
export type DecryptCommandData = {
|
||||
id: string;
|
||||
items: Decryptable<any>[];
|
||||
key: SymmetricCryptoKey;
|
||||
};
|
||||
|
||||
export type ParsedDecryptCommandData = {
|
||||
id: string;
|
||||
items: Jsonify<Decryptable<any>>[];
|
||||
key: Jsonify<SymmetricCryptoKey>;
|
||||
};
|
||||
|
||||
type SetConfigCommandData = { newConfig: ServerConfig };
|
||||
|
||||
export function buildDecryptMessage(data: DecryptCommandData): string {
|
||||
return JSON.stringify({
|
||||
command: DECRYPT_COMMAND,
|
||||
...data,
|
||||
});
|
||||
}
|
||||
|
||||
export function buildSetConfigMessage(data: SetConfigCommandData): string {
|
||||
return JSON.stringify({
|
||||
command: SET_CONFIG_COMMAND,
|
||||
...data,
|
||||
});
|
||||
}
|
||||
@@ -17,11 +17,7 @@ import { SemVer } from "semver";
|
||||
|
||||
import { AuthService } from "../../../auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
|
||||
import {
|
||||
DefaultFeatureFlagValue,
|
||||
FeatureFlag,
|
||||
FeatureFlagValueType,
|
||||
} from "../../../enums/feature-flag.enum";
|
||||
import { FeatureFlag, getFeatureFlagValue } from "../../../enums/feature-flag.enum";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { ConfigApiServiceAbstraction } from "../../abstractions/config/config-api.service.abstraction";
|
||||
import { ConfigService } from "../../abstractions/config/config.service";
|
||||
@@ -123,26 +119,13 @@ export class DefaultConfigService implements ConfigService {
|
||||
}
|
||||
|
||||
getFeatureFlag$<Flag extends FeatureFlag>(key: Flag) {
|
||||
return this.serverConfig$.pipe(
|
||||
map((serverConfig) => this.getFeatureFlagValue(serverConfig, key)),
|
||||
);
|
||||
}
|
||||
|
||||
private getFeatureFlagValue<Flag extends FeatureFlag>(
|
||||
serverConfig: ServerConfig | null,
|
||||
flag: Flag,
|
||||
) {
|
||||
if (serverConfig?.featureStates == null || serverConfig.featureStates[flag] == null) {
|
||||
return DefaultFeatureFlagValue[flag];
|
||||
}
|
||||
|
||||
return serverConfig.featureStates[flag] as FeatureFlagValueType<Flag>;
|
||||
return this.serverConfig$.pipe(map((serverConfig) => getFeatureFlagValue(serverConfig, key)));
|
||||
}
|
||||
|
||||
userCachedFeatureFlag$<Flag extends FeatureFlag>(key: Flag, userId: UserId) {
|
||||
return this.stateProvider
|
||||
.getUser(userId, USER_SERVER_CONFIG)
|
||||
.state$.pipe(map((config) => this.getFeatureFlagValue(config, key)));
|
||||
.state$.pipe(map((config) => getFeatureFlagValue(config, key)));
|
||||
}
|
||||
|
||||
async getFeatureFlag<Flag extends FeatureFlag>(key: Flag) {
|
||||
|
||||
Reference in New Issue
Block a user