From 17f661e3d1e7a507f790f2ce7eb29f2be5d62fcd Mon Sep 17 00:00:00 2001 From: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Date: Tue, 1 Apr 2025 14:14:00 -0500 Subject: [PATCH] [PM-19287] Feature flag for encrypt service (#13894) * Extract getFeatureFlagValue to pure function Co-authored-by: Matt Gibson * Add broadcasting abstractions and OnServerConfigChange interface. Co-authored-by: Matt Gibson * Add implementation of onServerConfigChange on encrypt services Co-authored-by: Matt Gibson * Add onServerConfigChange implementation for encrypt worker Co-authored-by: Matt Gibson * Wire up broadcasting in dependency injection Co-authored-by: Matt Gibson * Add unit tests * Handle subscribing for onServerConfigChange in init services --------- Co-authored-by: Matt Gibson --- .../browser/src/background/main.background.ts | 7 + .../src/popup/services/init.service.ts | 12 ++ .../service-container/service-container.ts | 10 +- apps/desktop/src/app/services/init.service.ts | 11 ++ apps/web/src/app/core/init.service.ts | 11 ++ .../src/enums/feature-flag.enum.spec.ts | 38 ++++ libs/common/src/enums/feature-flag.enum.ts | 13 ++ .../abstractions/bulk-encrypt.service.ts | 9 +- .../crypto/abstractions/encrypt.service.ts | 15 +- ...bulk-encrypt.service.impementation.spec.ts | 170 ++++++++++++++++++ .../bulk-encrypt.service.implementation.ts | 30 +++- .../encrypt.service.implementation.ts | 6 + .../crypto/services/encrypt.worker.ts | 45 +++-- .../fallback-bulk-encrypt.service.spec.ts | 97 ++++++++++ .../services/fallback-bulk-encrypt.service.ts | 10 ++ ...ead-encrypt.service.implementation.spec.ts | 123 +++++++++++++ ...tithread-encrypt.service.implementation.ts | 47 +++-- .../crypto/types/worker-command.type.spec.ts | 67 +++++++ .../crypto/types/worker-command.type.ts | 36 ++++ .../services/config/default-config.service.ts | 23 +-- 20 files changed, 722 insertions(+), 58 deletions(-) create mode 100644 libs/common/src/enums/feature-flag.enum.spec.ts create mode 100644 libs/common/src/key-management/crypto/services/bulk-encrypt.service.impementation.spec.ts create mode 100644 libs/common/src/key-management/crypto/services/fallback-bulk-encrypt.service.spec.ts create mode 100644 libs/common/src/key-management/crypto/services/multithread-encrypt.service.implementation.spec.ts create mode 100644 libs/common/src/key-management/crypto/types/worker-command.type.spec.ts create mode 100644 libs/common/src/key-management/crypto/types/worker-command.type.ts diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 5cc964c2c2d..ac62ad1c9a8 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -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$); diff --git a/apps/browser/src/popup/services/init.service.ts b/apps/browser/src/popup/services/init.service.ts index fe6fba85a4b..c9fe7161259 100644 --- a/apps/browser/src/popup/services/init.service.ts +++ b/apps/browser/src/popup/services/init.service.ts @@ -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(); diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index 6a4651bcd5a..0642c1f3e62 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -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(); diff --git a/apps/desktop/src/app/services/init.service.ts b/apps/desktop/src/app/services/init.service.ts index 08efa4a592e..e68491faaa3 100644 --- a/apps/desktop/src/app/services/init.service.ts +++ b/apps/desktop/src/app/services/init.service.ts @@ -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[]) { diff --git a/apps/web/src/app/core/init.service.ts b/apps/web/src/app/core/init.service.ts index 1990ce1e1ce..24b5631abcc 100644 --- a/apps/web/src/app/core/init.service.ts +++ b/apps/web/src/app/core/init.service.ts @@ -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 diff --git a/libs/common/src/enums/feature-flag.enum.spec.ts b/libs/common/src/enums/feature-flag.enum.spec.ts new file mode 100644 index 00000000000..687c9158fd6 --- /dev/null +++ b/libs/common/src/enums/feature-flag.enum.spec.ts @@ -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.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.featureStates = { [testFlag]: expectedValue }; + + const result = getFeatureFlagValue(serverConfig, testFlag); + expect(result).toBe(expectedValue); + }); +}); diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 88a52f2948d..59efbee8760 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -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 = DefaultFeatureFlagValueType[Flag]; + +export function getFeatureFlagValue( + serverConfig: ServerConfig | null, + flag: Flag, +) { + if (serverConfig?.featureStates == null || serverConfig.featureStates[flag] == null) { + return DefaultFeatureFlagValue[flag]; + } + + return serverConfig.featureStates[flag] as FeatureFlagValueType; +} diff --git a/libs/common/src/key-management/crypto/abstractions/bulk-encrypt.service.ts b/libs/common/src/key-management/crypto/abstractions/bulk-encrypt.service.ts index 3e47ccdb5f2..399ad75231e 100644 --- a/libs/common/src/key-management/crypto/abstractions/bulk-encrypt.service.ts +++ b/libs/common/src/key-management/crypto/abstractions/bulk-encrypt.service.ts @@ -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( items: Decryptable[], key: SymmetricCryptoKey, ): Promise; + + abstract onServerConfigChange(newConfig: ServerConfig): void; } diff --git a/libs/common/src/key-management/crypto/abstractions/encrypt.service.ts b/libs/common/src/key-management/crypto/abstractions/encrypt.service.ts index f7f064f5251..91e10f19069 100644 --- a/libs/common/src/key-management/crypto/abstractions/encrypt.service.ts +++ b/libs/common/src/key-management/crypto/abstractions/encrypt.service.ts @@ -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; @@ -54,4 +55,6 @@ export abstract class EncryptService { value: string | Uint8Array, algorithm: "sha1" | "sha256" | "sha512", ): Promise; + + abstract onServerConfigChange(newConfig: ServerConfig): void; } diff --git a/libs/common/src/key-management/crypto/services/bulk-encrypt.service.impementation.spec.ts b/libs/common/src/key-management/crypto/services/bulk-encrypt.service.impementation.spec.ts new file mode 100644 index 00000000000..1c27524c937 --- /dev/null +++ b/libs/common/src/key-management/crypto/services/bulk-encrypt.service.impementation.spec.ts @@ -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(); + const logService = mock(); + + let sut: BulkEncryptServiceImplementation; + + beforeEach(() => { + sut = new BulkEncryptServiceImplementation(cryptoFunctionService, logService); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe("decryptItems", () => { + const key = mock(); + const serverConfig = mock(); + const mockWorker = mock(); + 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(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(); + + 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(); + const mockWorker = mock(); + (sut as any).workers = [mockWorker]; + + sut.onServerConfigChange(newConfig); + + expect(mockWorker.postMessage).toHaveBeenCalledWith(buildSetConfigMessage({ newConfig })); + }); + }); +}); + +function createMockDecryptable( + returnValue: any, +): MockProxy> { + const mockDecryptable = mock>(); + mockDecryptable.decrypt.mockResolvedValue(returnValue); + return mockDecryptable; +} diff --git a/libs/common/src/key-management/crypto/services/bulk-encrypt.service.implementation.ts b/libs/common/src/key-management/crypto/services/bulk-encrypt.service.implementation.ts index 1d1e0f52279..a5149c59265 100644 --- a/libs/common/src/key-management/crypto/services/bulk-encrypt.service.implementation.ts +++ b/libs/common/src/key-management/crypto/services/bulk-encrypt.service.implementation.ts @@ -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(); @@ -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) => { @@ -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) { diff --git a/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts b/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts index d426340c277..7c5bc15a511 100644 --- a/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts +++ b/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts @@ -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 { if (key == null) { throw new Error("No encryption key provided."); diff --git a/libs/common/src/key-management/crypto/services/encrypt.worker.ts b/libs/common/src/key-management/crypto/services/encrypt.worker.ts index 84ffcf56934..a31a65eaffc 100644 --- a/libs/common/src/key-management/crypto/services/encrypt.worker.ts +++ b/libs/common/src/key-management/crypto/services/encrypt.worker.ts @@ -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>[]; - key: Jsonify; + 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 }).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>(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) { + encryptService.onServerConfigChange(ServerConfig.fromJSON(newConfig)); +} diff --git a/libs/common/src/key-management/crypto/services/fallback-bulk-encrypt.service.spec.ts b/libs/common/src/key-management/crypto/services/fallback-bulk-encrypt.service.spec.ts new file mode 100644 index 00000000000..c016724e652 --- /dev/null +++ b/libs/common/src/key-management/crypto/services/fallback-bulk-encrypt.service.spec.ts @@ -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(); + const featureFlagEncryptService = mock(); + const serverConfig = mock(); + + let sut: FallbackBulkEncryptService; + + beforeEach(() => { + sut = new FallbackBulkEncryptService(encryptService); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe("decryptItems", () => { + const key = mock(); + 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); + }); + }); +}); diff --git a/libs/common/src/key-management/crypto/services/fallback-bulk-encrypt.service.ts b/libs/common/src/key-management/crypto/services/fallback-bulk-encrypt.service.ts index 80fdd27895d..7eefa896e6a 100644 --- a/libs/common/src/key-management/crypto/services/fallback-bulk-encrypt.service.ts +++ b/libs/common/src/key-management/crypto/services/fallback-bulk-encrypt.service.ts @@ -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); + } } diff --git a/libs/common/src/key-management/crypto/services/multithread-encrypt.service.implementation.spec.ts b/libs/common/src/key-management/crypto/services/multithread-encrypt.service.implementation.spec.ts new file mode 100644 index 00000000000..b46334f1470 --- /dev/null +++ b/libs/common/src/key-management/crypto/services/multithread-encrypt.service.implementation.spec.ts @@ -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(); + const logService = mock(); + const serverConfig = mock(); + + let sut: MultithreadEncryptServiceImplementation; + + beforeEach(() => { + sut = new MultithreadEncryptServiceImplementation(cryptoFunctionService, logService, true); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe("decryptItems", () => { + const key = mock(); + const mockWorker = mock(); + + 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[]; + 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>(), mock>()], 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>(), mock>()], 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>(), mock>()], 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(); + (sut as any).worker = mockWorker; + + sut.onServerConfigChange(serverConfig); + + expect(mockWorker.postMessage).toHaveBeenCalledTimes(1); + expect(mockWorker.postMessage).toHaveBeenCalledWith( + buildSetConfigMessage({ newConfig: serverConfig }), + ); + }); + }); +}); diff --git a/libs/common/src/key-management/crypto/services/multithread-encrypt.service.implementation.ts b/libs/common/src/key-management/crypto/services/multithread-encrypt.service.implementation.ts index 0bf96851563..589450b23cc 100644 --- a/libs/common/src/key-management/crypto/services/multithread-encrypt.service.implementation.ts +++ b/libs/common/src/key-management/crypto/services/multithread-encrypt.service.implementation.ts @@ -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(); @@ -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) => { @@ -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(); diff --git a/libs/common/src/key-management/crypto/types/worker-command.type.spec.ts b/libs/common/src/key-management/crypto/types/worker-command.type.spec.ts new file mode 100644 index 00000000000..e820543caa9 --- /dev/null +++ b/libs/common/src/key-management/crypto/types/worker-command.type.spec.ts @@ -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]; + 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() }); + + 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[]): DecryptCommandData { + return { + id: "test-id", + items: items ?? [], + key: new SymmetricCryptoKey(makeStaticByteArray(64)), + }; +} diff --git a/libs/common/src/key-management/crypto/types/worker-command.type.ts b/libs/common/src/key-management/crypto/types/worker-command.type.ts new file mode 100644 index 00000000000..e058bf3eaac --- /dev/null +++ b/libs/common/src/key-management/crypto/types/worker-command.type.ts @@ -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[]; + key: SymmetricCryptoKey; +}; + +export type ParsedDecryptCommandData = { + id: string; + items: Jsonify>[]; + key: Jsonify; +}; + +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, + }); +} diff --git a/libs/common/src/platform/services/config/default-config.service.ts b/libs/common/src/platform/services/config/default-config.service.ts index cc52a5b8dad..33f86d30885 100644 --- a/libs/common/src/platform/services/config/default-config.service.ts +++ b/libs/common/src/platform/services/config/default-config.service.ts @@ -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$(key: Flag) { - return this.serverConfig$.pipe( - map((serverConfig) => this.getFeatureFlagValue(serverConfig, key)), - ); - } - - private getFeatureFlagValue( - serverConfig: ServerConfig | null, - flag: Flag, - ) { - if (serverConfig?.featureStates == null || serverConfig.featureStates[flag] == null) { - return DefaultFeatureFlagValue[flag]; - } - - return serverConfig.featureStates[flag] as FeatureFlagValueType; + return this.serverConfig$.pipe(map((serverConfig) => getFeatureFlagValue(serverConfig, key))); } userCachedFeatureFlag$(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(key: Flag) {