From 84b719d797291c75db959f29f587a0e42479caf8 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 18 Jul 2024 14:56:22 +0200 Subject: [PATCH] [PM-4154] Introduce Bulk Encrypt Service for Faster Unlock Times (#6465) * Implement multi-worker encryption service * Fix feature flag being flipped and check for empty input earlier * Add tests * Small cleanup * Remove restricted import * Rename feature flag * Refactor to BulkEncryptService * Rename feature flag * Fix cipher service spec * Implement browser bulk encryption service * Un-deprecate browserbulkencryptservice * Load browser bulk encrypt service on feature flag asynchronously * Fix bulk encryption service factories * Deprecate BrowserMultithreadEncryptServiceImplementation * Copy tests for browser-bulk-encrypt-service-implementation from browser-multithread-encrypt-service-implementation * Make sure desktop uses non-bulk fallback during feature rollout * Rename FallbackBulkEncryptService and fix service dependency issue * Disable bulk encrypt service on mv3 * Change condition order to avoid expensive api call * Set default hardware concurrency to 1 if not available * Make getdecrypteditemfromworker private * Fix cli build * Add check for key being null --- .../browser/src/background/main.background.ts | 14 +- apps/cli/src/service-container.ts | 2 + .../services/emergency-access.service.spec.ts | 7 + .../services/emergency-access.service.ts | 21 ++- .../src/services/jslib-services.module.ts | 10 ++ libs/common/src/enums/feature-flag.enum.ts | 2 + .../abstractions/bulk-encrypt.service.ts | 10 ++ .../platform/abstractions/encrypt.service.ts | 5 + .../bulk-encrypt.service.implementation.ts | 164 ++++++++++++++++++ .../encrypt.service.implementation.ts | 3 + .../fallback-bulk-encrypt.service.ts | 33 ++++ ...tithread-encrypt.service.implementation.ts | 3 + .../src/vault/services/cipher.service.spec.ts | 4 + .../src/vault/services/cipher.service.ts | 30 +++- 14 files changed, 296 insertions(+), 12 deletions(-) create mode 100644 libs/common/src/platform/abstractions/bulk-encrypt.service.ts create mode 100644 libs/common/src/platform/services/cryptography/bulk-encrypt.service.implementation.ts create mode 100644 libs/common/src/platform/services/cryptography/fallback-bulk-encrypt.service.ts diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 9aac8464ab..c271dd29db 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -112,7 +112,9 @@ import { ConfigApiService } from "@bitwarden/common/platform/services/config/con import { DefaultConfigService } from "@bitwarden/common/platform/services/config/default-config.service"; import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; +import { BulkEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/bulk-encrypt.service.implementation"; import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation"; +import { FallbackBulkEncryptService } from "@bitwarden/common/platform/services/cryptography/fallback-bulk-encrypt.service"; import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation"; import { Fido2AuthenticatorService } from "@bitwarden/common/platform/services/fido2/fido2-authenticator.service"; import { Fido2ClientService } from "@bitwarden/common/platform/services/fido2/fido2-client.service"; @@ -247,7 +249,6 @@ import CommandsBackground from "./commands.background"; import IdleBackground from "./idle.background"; import { NativeMessagingBackground } from "./nativeMessaging.background"; import RuntimeBackground from "./runtime.background"; - export default class MainBackground { messagingService: MessageSender; storageService: BrowserLocalStorageService; @@ -306,6 +307,7 @@ export default class MainBackground { vaultFilterService: VaultFilterService; usernameGenerationService: UsernameGenerationServiceAbstraction; encryptService: EncryptService; + bulkEncryptService: FallbackBulkEncryptService; folderApiService: FolderApiServiceAbstraction; policyApiService: PolicyApiServiceAbstraction; sendApiService: SendApiServiceAbstraction; @@ -744,6 +746,7 @@ export default class MainBackground { this.stateService, this.autofillSettingsService, this.encryptService, + this.bulkEncryptService, this.cipherFileUploadService, this.configService, this.stateProvider, @@ -1227,6 +1230,15 @@ export default class MainBackground { this.webRequestBackground?.startListening(); this.syncServiceListener?.listener$().subscribe(); + if ( + BrowserApi.isManifestVersion(2) && + (await this.configService.getFeatureFlag(FeatureFlag.PM4154_BulkEncryptionService)) + ) { + await this.bulkEncryptService.setFeatureFlagEncryptService( + new BulkEncryptServiceImplementation(this.cryptoFunctionService, this.logService), + ); + } + return new Promise((resolve) => { setTimeout(async () => { await this.refreshBadge(); diff --git a/apps/cli/src/service-container.ts b/apps/cli/src/service-container.ts index 3d53013ef0..cfe310318f 100644 --- a/apps/cli/src/service-container.ts +++ b/apps/cli/src/service-container.ts @@ -75,6 +75,7 @@ import { DefaultConfigService } from "@bitwarden/common/platform/services/config import { ContainerService } from "@bitwarden/common/platform/services/container.service"; import { CryptoService } from "@bitwarden/common/platform/services/crypto.service"; import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation"; +import { FallbackBulkEncryptService } from "@bitwarden/common/platform/services/cryptography/fallback-bulk-encrypt.service"; import { DefaultEnvironmentService } from "@bitwarden/common/platform/services/default-environment.service"; import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service"; import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service"; @@ -605,6 +606,7 @@ export class ServiceContainer { this.stateService, this.autofillSettingsService, this.encryptService, + new FallbackBulkEncryptService(this.encryptService), this.cipherFileUploadService, this.configService, this.stateProvider, diff --git a/apps/web/src/app/auth/emergency-access/services/emergency-access.service.spec.ts b/apps/web/src/app/auth/emergency-access/services/emergency-access.service.spec.ts index 7906731d81..6f68821943 100644 --- a/apps/web/src/app/auth/emergency-access/services/emergency-access.service.spec.ts +++ b/apps/web/src/app/auth/emergency-access/services/emergency-access.service.spec.ts @@ -4,6 +4,8 @@ import mock from "jest-mock-extended/lib/Mock"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { UserKeyResponse } from "@bitwarden/common/models/response/user-key.response"; +import { BulkEncryptService } from "@bitwarden/common/platform/abstractions/bulk-encrypt.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -31,15 +33,18 @@ describe("EmergencyAccessService", () => { let apiService: MockProxy; let cryptoService: MockProxy; let encryptService: MockProxy; + let bulkEncryptService: MockProxy; let cipherService: MockProxy; let logService: MockProxy; let emergencyAccessService: EmergencyAccessService; + let configService: ConfigService; beforeAll(() => { emergencyAccessApiService = mock(); apiService = mock(); cryptoService = mock(); encryptService = mock(); + bulkEncryptService = mock(); cipherService = mock(); logService = mock(); @@ -48,8 +53,10 @@ describe("EmergencyAccessService", () => { apiService, cryptoService, encryptService, + bulkEncryptService, cipherService, logService, + configService, ); }); diff --git a/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts b/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts index 362b1dec3c..5b9d73c75e 100644 --- a/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts +++ b/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts @@ -9,6 +9,9 @@ import { KdfConfig, PBKDF2KdfConfig, } from "@bitwarden/common/auth/models/domain/kdf-config"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { BulkEncryptService } from "@bitwarden/common/platform/abstractions/bulk-encrypt.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -45,8 +48,10 @@ export class EmergencyAccessService private apiService: ApiService, private cryptoService: CryptoService, private encryptService: EncryptService, + private bulkEncryptService: BulkEncryptService, private cipherService: CipherService, private logService: LogService, + private configService: ConfigService, ) {} /** @@ -225,10 +230,18 @@ export class EmergencyAccessService ); const grantorUserKey = new SymmetricCryptoKey(grantorKeyBuffer) as UserKey; - const ciphers = await this.encryptService.decryptItems( - response.ciphers.map((c) => new Cipher(c)), - grantorUserKey, - ); + let ciphers: CipherView[] = []; + if (await this.configService.getFeatureFlag(FeatureFlag.PM4154_BulkEncryptionService)) { + ciphers = await this.bulkEncryptService.decryptItems( + response.ciphers.map((c) => new Cipher(c)), + grantorUserKey, + ); + } else { + ciphers = await this.encryptService.decryptItems( + response.ciphers.map((c) => new Cipher(c)), + grantorUserKey, + ); + } return ciphers.sort(this.cipherService.getLocaleSortingFunction()); } diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index fa53246062..1b534b6f77 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -131,6 +131,7 @@ import { BraintreeService } from "@bitwarden/common/billing/services/payment-pro import { StripeService } from "@bitwarden/common/billing/services/payment-processors/stripe.service"; import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; +import { BulkEncryptService } from "@bitwarden/common/platform/abstractions/bulk-encrypt.service"; import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service"; @@ -166,6 +167,7 @@ import { ConfigApiService } from "@bitwarden/common/platform/services/config/con import { DefaultConfigService } from "@bitwarden/common/platform/services/config/default-config.service"; import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; import { CryptoService } from "@bitwarden/common/platform/services/crypto.service"; +import { BulkEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/bulk-encrypt.service.implementation"; import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation"; import { DefaultBroadcasterService } from "@bitwarden/common/platform/services/default-broadcaster.service"; import { DefaultEnvironmentService } from "@bitwarden/common/platform/services/default-environment.service"; @@ -437,6 +439,7 @@ const safeProviders: SafeProvider[] = [ stateService: StateServiceAbstraction, autofillSettingsService: AutofillSettingsServiceAbstraction, encryptService: EncryptService, + bulkEncryptService: BulkEncryptService, fileUploadService: CipherFileUploadServiceAbstraction, configService: ConfigService, stateProvider: StateProvider, @@ -450,6 +453,7 @@ const safeProviders: SafeProvider[] = [ stateService, autofillSettingsService, encryptService, + bulkEncryptService, fileUploadService, configService, stateProvider, @@ -463,6 +467,7 @@ const safeProviders: SafeProvider[] = [ StateServiceAbstraction, AutofillSettingsServiceAbstraction, EncryptService, + BulkEncryptService, CipherFileUploadServiceAbstraction, ConfigService, StateProvider, @@ -832,6 +837,11 @@ const safeProviders: SafeProvider[] = [ useClass: MultithreadEncryptServiceImplementation, deps: [CryptoFunctionServiceAbstraction, LogService, LOG_MAC_FAILURES], }), + safeProvider({ + provide: BulkEncryptService, + useClass: BulkEncryptServiceImplementation, + deps: [CryptoFunctionServiceAbstraction, LogService], + }), safeProvider({ provide: EventUploadServiceAbstraction, useClass: EventUploadService, diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index fb4bd1f966..7e88af236f 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -14,6 +14,7 @@ export enum FeatureFlag { EnableDeleteProvider = "AC-1218-delete-provider", ExtensionRefresh = "extension-refresh", RestrictProviderAccess = "restrict-provider-access", + PM4154_BulkEncryptionService = "PM-4154-bulk-encryption-service", UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection", EmailVerification = "email-verification", InlineMenuFieldQualification = "inline-menu-field-qualification", @@ -49,6 +50,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.EnableDeleteProvider]: FALSE, [FeatureFlag.ExtensionRefresh]: FALSE, [FeatureFlag.RestrictProviderAccess]: FALSE, + [FeatureFlag.PM4154_BulkEncryptionService]: FALSE, [FeatureFlag.UseTreeWalkerApiForPageDetailsCollection]: FALSE, [FeatureFlag.EmailVerification]: FALSE, [FeatureFlag.InlineMenuFieldQualification]: FALSE, diff --git a/libs/common/src/platform/abstractions/bulk-encrypt.service.ts b/libs/common/src/platform/abstractions/bulk-encrypt.service.ts new file mode 100644 index 0000000000..4cdff0c769 --- /dev/null +++ b/libs/common/src/platform/abstractions/bulk-encrypt.service.ts @@ -0,0 +1,10 @@ +import { Decryptable } from "../interfaces/decryptable.interface"; +import { InitializerMetadata } from "../interfaces/initializer-metadata.interface"; +import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; + +export abstract class BulkEncryptService { + abstract decryptItems( + items: Decryptable[], + key: SymmetricCryptoKey, + ): Promise; +} diff --git a/libs/common/src/platform/abstractions/encrypt.service.ts b/libs/common/src/platform/abstractions/encrypt.service.ts index bc526e3578..c70042e418 100644 --- a/libs/common/src/platform/abstractions/encrypt.service.ts +++ b/libs/common/src/platform/abstractions/encrypt.service.ts @@ -13,6 +13,11 @@ export abstract class EncryptService { abstract rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise; abstract rsaDecrypt(data: EncString, privateKey: Uint8Array): Promise; abstract resolveLegacyKey(key: SymmetricCryptoKey, encThing: Encrypted): SymmetricCryptoKey; + /** + * @deprecated Replaced by BulkEncryptService, remove once the feature is tested and the featureflag PM-4154-multi-worker-encryption-service is removed + * @param items The items to decrypt + * @param key The key to decrypt the items with + */ abstract decryptItems( items: Decryptable[], key: SymmetricCryptoKey, diff --git a/libs/common/src/platform/services/cryptography/bulk-encrypt.service.implementation.ts b/libs/common/src/platform/services/cryptography/bulk-encrypt.service.implementation.ts new file mode 100644 index 0000000000..d3bbc82905 --- /dev/null +++ b/libs/common/src/platform/services/cryptography/bulk-encrypt.service.implementation.ts @@ -0,0 +1,164 @@ +import { firstValueFrom, fromEvent, filter, map, takeUntil, defaultIfEmpty, Subject } from "rxjs"; +import { Jsonify } from "type-fest"; + +import { BulkEncryptService } from "../../abstractions/bulk-encrypt.service"; +import { CryptoFunctionService } from "../../abstractions/crypto-function.service"; +import { LogService } from "../../abstractions/log.service"; +import { Decryptable } from "../../interfaces/decryptable.interface"; +import { InitializerMetadata } from "../../interfaces/initializer-metadata.interface"; +import { Utils } from "../../misc/utils"; +import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key"; + +import { getClassInitializer } from "./get-class-initializer"; + +// TTL (time to live) is not strictly required but avoids tying up memory resources if inactive +const workerTTL = 60000; // 1 minute +const maxWorkers = 8; +const minNumberOfItemsForMultithreading = 400; + +export class BulkEncryptServiceImplementation implements BulkEncryptService { + private workers: Worker[] = []; + private timeout: any; + + private clear$ = new Subject(); + + constructor( + protected cryptoFunctionService: CryptoFunctionService, + protected logService: LogService, + ) {} + + /** + * Decrypts items using a web worker if the environment supports it. + * Will fall back to the main thread if the window object is not available. + */ + async decryptItems( + items: Decryptable[], + key: SymmetricCryptoKey, + ): Promise { + if (key == null) { + throw new Error("No encryption key provided."); + } + + if (items == null || items.length < 1) { + return []; + } + + if (typeof window === "undefined") { + this.logService.info("Window not available in BulkEncryptService, decrypting sequentially"); + const results = []; + for (let i = 0; i < items.length; i++) { + results.push(await items[i].decrypt(key)); + } + return results; + } + + const decryptedItems = await this.getDecryptedItemsFromWorkers(items, key); + return decryptedItems; + } + + /** + * 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). + */ + private async getDecryptedItemsFromWorkers( + items: Decryptable[], + key: SymmetricCryptoKey, + ): Promise { + if (items == null || items.length < 1) { + return []; + } + + this.clearTimeout(); + + const hardwareConcurrency = navigator.hardwareConcurrency || 1; + let numberOfWorkers = Math.min(hardwareConcurrency, maxWorkers); + if (items.length < minNumberOfItemsForMultithreading) { + numberOfWorkers = 1; + } + + this.logService.info( + `Starting decryption using multithreading with ${numberOfWorkers} workers for ${items.length} items`, + ); + + if (this.workers.length == 0) { + for (let i = 0; i < numberOfWorkers; i++) { + this.workers.push( + new Worker( + new URL( + /* webpackChunkName: 'encrypt-worker' */ + "@bitwarden/common/platform/services/cryptography/encrypt.worker.ts", + import.meta.url, + ), + ), + ); + } + } + + const itemsPerWorker = Math.floor(items.length / this.workers.length); + const results = []; + + for (const [i, worker] of this.workers.entries()) { + const start = i * itemsPerWorker; + const end = start + itemsPerWorker; + const itemsForWorker = items.slice(start, end); + + // push the remaining items to the last worker + if (i == this.workers.length - 1) { + itemsForWorker.push(...items.slice(end)); + } + + const request = { + id: Utils.newGuid(), + items: itemsForWorker, + key: key, + }; + + worker.postMessage(JSON.stringify(request)); + results.push( + firstValueFrom( + fromEvent(worker, "message").pipe( + filter((response: MessageEvent) => response.data?.id === request.id), + map((response) => JSON.parse(response.data.items)), + map((items) => + items.map((jsonItem: Jsonify) => { + const initializer = getClassInitializer(jsonItem.initializerKey); + return initializer(jsonItem); + }), + ), + takeUntil(this.clear$), + defaultIfEmpty([]), + ), + ), + ); + } + + const decryptedItems = (await Promise.all(results)).flat(); + this.logService.info( + `Finished decrypting ${decryptedItems.length} items using ${numberOfWorkers} workers`, + ); + + this.restartTimeout(); + + return decryptedItems; + } + + private clear() { + this.clear$.next(); + for (const worker of this.workers) { + worker.terminate(); + } + this.workers = []; + this.clearTimeout(); + } + + private restartTimeout() { + this.clearTimeout(); + this.timeout = setTimeout(() => this.clear(), workerTTL); + } + + private clearTimeout() { + if (this.timeout != null) { + clearTimeout(this.timeout); + } + } +} diff --git a/libs/common/src/platform/services/cryptography/encrypt.service.implementation.ts b/libs/common/src/platform/services/cryptography/encrypt.service.implementation.ts index 862ae2bc0e..228f0c5417 100644 --- a/libs/common/src/platform/services/cryptography/encrypt.service.implementation.ts +++ b/libs/common/src/platform/services/cryptography/encrypt.service.implementation.ts @@ -185,6 +185,9 @@ export class EncryptServiceImplementation implements EncryptService { return this.cryptoFunctionService.rsaDecrypt(data.dataBytes, privateKey, algorithm); } + /** + * @deprecated Replaced by BulkEncryptService (PM-4154) + */ async decryptItems( items: Decryptable[], key: SymmetricCryptoKey, diff --git a/libs/common/src/platform/services/cryptography/fallback-bulk-encrypt.service.ts b/libs/common/src/platform/services/cryptography/fallback-bulk-encrypt.service.ts new file mode 100644 index 0000000000..44dc5a8bf7 --- /dev/null +++ b/libs/common/src/platform/services/cryptography/fallback-bulk-encrypt.service.ts @@ -0,0 +1,33 @@ +import { BulkEncryptService } from "../../abstractions/bulk-encrypt.service"; +import { EncryptService } from "../../abstractions/encrypt.service"; +import { Decryptable } from "../../interfaces/decryptable.interface"; +import { InitializerMetadata } from "../../interfaces/initializer-metadata.interface"; +import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key"; + +/** + * @deprecated For the feature flag from PM-4154, remove once feature is rolled out + */ +export class FallbackBulkEncryptService implements BulkEncryptService { + private featureFlagEncryptService: BulkEncryptService; + + constructor(protected encryptService: EncryptService) {} + + /** + * Decrypts items using a web worker if the environment supports it. + * Will fall back to the main thread if the window object is not available. + */ + async decryptItems( + items: Decryptable[], + key: SymmetricCryptoKey, + ): Promise { + if (this.featureFlagEncryptService != null) { + return await this.featureFlagEncryptService.decryptItems(items, key); + } else { + return await this.encryptService.decryptItems(items, key); + } + } + + async setFeatureFlagEncryptService(featureFlagEncryptService: BulkEncryptService) { + this.featureFlagEncryptService = featureFlagEncryptService; + } +} diff --git a/libs/common/src/platform/services/cryptography/multithread-encrypt.service.implementation.ts b/libs/common/src/platform/services/cryptography/multithread-encrypt.service.implementation.ts index 6ac343bcb6..227db77526 100644 --- a/libs/common/src/platform/services/cryptography/multithread-encrypt.service.implementation.ts +++ b/libs/common/src/platform/services/cryptography/multithread-encrypt.service.implementation.ts @@ -12,6 +12,9 @@ import { getClassInitializer } from "./get-class-initializer"; // TTL (time to live) is not strictly required but avoids tying up memory resources if inactive const workerTTL = 3 * 60000; // 3 minutes +/** + * @deprecated Replaced by BulkEncryptionService (PM-4154) + */ export class MultithreadEncryptServiceImplementation extends EncryptServiceImplementation { private worker: Worker; private timeout: any; diff --git a/libs/common/src/vault/services/cipher.service.spec.ts b/libs/common/src/vault/services/cipher.service.spec.ts index c84fd066b2..e3019ab48d 100644 --- a/libs/common/src/vault/services/cipher.service.spec.ts +++ b/libs/common/src/vault/services/cipher.service.spec.ts @@ -1,6 +1,8 @@ import { mock } from "jest-mock-extended"; import { BehaviorSubject, of } from "rxjs"; +import { BulkEncryptService } from "@bitwarden/common/platform/abstractions/bulk-encrypt.service"; + import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service"; import { FakeStateProvider } from "../../../spec/fake-state-provider"; import { makeStaticByteArray } from "../../../spec/utils"; @@ -114,6 +116,7 @@ describe("Cipher Service", () => { const i18nService = mock(); const searchService = mock(); const encryptService = mock(); + const bulkEncryptService = mock(); const configService = mock(); accountService = mockAccountServiceWith(mockUserId); const stateProvider = new FakeStateProvider(accountService); @@ -136,6 +139,7 @@ describe("Cipher Service", () => { stateService, autofillSettingsService, encryptService, + bulkEncryptService, cipherFileUploadService, configService, stateProvider, diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 1d06ae1dd0..aa1d3a1827 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -1,6 +1,9 @@ import { firstValueFrom, map, Observable, skipWhile, switchMap } from "rxjs"; import { SemVer } from "semver"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { BulkEncryptService } from "@bitwarden/common/platform/abstractions/bulk-encrypt.service"; + import { ApiService } from "../../abstractions/api.service"; import { SearchService } from "../../abstractions/search.service"; import { AutofillSettingsServiceAbstraction } from "../../autofill/services/autofill-settings.service"; @@ -102,6 +105,7 @@ export class CipherService implements CipherServiceAbstraction { private stateService: StateService, private autofillSettingsService: AutofillSettingsServiceAbstraction, private encryptService: EncryptService, + private bulkEncryptService: BulkEncryptService, private cipherFileUploadService: CipherFileUploadService, private configService: ConfigService, private stateProvider: StateProvider, @@ -397,12 +401,19 @@ export class CipherService implements CipherServiceAbstraction { const decCiphers = ( await Promise.all( - Object.entries(grouped).map(([orgId, groupedCiphers]) => - this.encryptService.decryptItems( - groupedCiphers, - keys.orgKeys[orgId as OrganizationId] ?? keys.userKey, - ), - ), + Object.entries(grouped).map(async ([orgId, groupedCiphers]) => { + if (await this.configService.getFeatureFlag(FeatureFlag.PM4154_BulkEncryptionService)) { + return await this.bulkEncryptService.decryptItems( + groupedCiphers, + keys.orgKeys[orgId as OrganizationId] ?? keys.userKey, + ); + } else { + return await this.encryptService.decryptItems( + groupedCiphers, + keys.orgKeys[orgId as OrganizationId] ?? keys.userKey, + ); + } + }), ) ) .flat() @@ -515,7 +526,12 @@ export class CipherService implements CipherServiceAbstraction { const ciphers = response.data.map((cr) => new Cipher(new CipherData(cr))); const key = await this.cryptoService.getOrgKey(organizationId); - const decCiphers = await this.encryptService.decryptItems(ciphers, key); + let decCiphers: CipherView[] = []; + if (await this.configService.getFeatureFlag(FeatureFlag.PM4154_BulkEncryptionService)) { + decCiphers = await this.bulkEncryptService.decryptItems(ciphers, key); + } else { + decCiphers = await this.encryptService.decryptItems(ciphers, key); + } decCiphers.sort(this.getLocaleSortingFunction()); return decCiphers;