mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-22 11:45:59 +01:00
[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
This commit is contained in:
parent
9b50e5c496
commit
84b719d797
@ -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 { DefaultConfigService } from "@bitwarden/common/platform/services/config/default-config.service";
|
||||||
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
|
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
|
||||||
import { ContainerService } from "@bitwarden/common/platform/services/container.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 { 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 { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation";
|
||||||
import { Fido2AuthenticatorService } from "@bitwarden/common/platform/services/fido2/fido2-authenticator.service";
|
import { Fido2AuthenticatorService } from "@bitwarden/common/platform/services/fido2/fido2-authenticator.service";
|
||||||
import { Fido2ClientService } from "@bitwarden/common/platform/services/fido2/fido2-client.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 IdleBackground from "./idle.background";
|
||||||
import { NativeMessagingBackground } from "./nativeMessaging.background";
|
import { NativeMessagingBackground } from "./nativeMessaging.background";
|
||||||
import RuntimeBackground from "./runtime.background";
|
import RuntimeBackground from "./runtime.background";
|
||||||
|
|
||||||
export default class MainBackground {
|
export default class MainBackground {
|
||||||
messagingService: MessageSender;
|
messagingService: MessageSender;
|
||||||
storageService: BrowserLocalStorageService;
|
storageService: BrowserLocalStorageService;
|
||||||
@ -306,6 +307,7 @@ export default class MainBackground {
|
|||||||
vaultFilterService: VaultFilterService;
|
vaultFilterService: VaultFilterService;
|
||||||
usernameGenerationService: UsernameGenerationServiceAbstraction;
|
usernameGenerationService: UsernameGenerationServiceAbstraction;
|
||||||
encryptService: EncryptService;
|
encryptService: EncryptService;
|
||||||
|
bulkEncryptService: FallbackBulkEncryptService;
|
||||||
folderApiService: FolderApiServiceAbstraction;
|
folderApiService: FolderApiServiceAbstraction;
|
||||||
policyApiService: PolicyApiServiceAbstraction;
|
policyApiService: PolicyApiServiceAbstraction;
|
||||||
sendApiService: SendApiServiceAbstraction;
|
sendApiService: SendApiServiceAbstraction;
|
||||||
@ -744,6 +746,7 @@ export default class MainBackground {
|
|||||||
this.stateService,
|
this.stateService,
|
||||||
this.autofillSettingsService,
|
this.autofillSettingsService,
|
||||||
this.encryptService,
|
this.encryptService,
|
||||||
|
this.bulkEncryptService,
|
||||||
this.cipherFileUploadService,
|
this.cipherFileUploadService,
|
||||||
this.configService,
|
this.configService,
|
||||||
this.stateProvider,
|
this.stateProvider,
|
||||||
@ -1227,6 +1230,15 @@ export default class MainBackground {
|
|||||||
this.webRequestBackground?.startListening();
|
this.webRequestBackground?.startListening();
|
||||||
this.syncServiceListener?.listener$().subscribe();
|
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<void>((resolve) => {
|
return new Promise<void>((resolve) => {
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
await this.refreshBadge();
|
await this.refreshBadge();
|
||||||
|
@ -75,6 +75,7 @@ import { DefaultConfigService } from "@bitwarden/common/platform/services/config
|
|||||||
import { ContainerService } from "@bitwarden/common/platform/services/container.service";
|
import { ContainerService } from "@bitwarden/common/platform/services/container.service";
|
||||||
import { CryptoService } from "@bitwarden/common/platform/services/crypto.service";
|
import { CryptoService } from "@bitwarden/common/platform/services/crypto.service";
|
||||||
import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/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 { DefaultEnvironmentService } from "@bitwarden/common/platform/services/default-environment.service";
|
import { DefaultEnvironmentService } from "@bitwarden/common/platform/services/default-environment.service";
|
||||||
import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service";
|
import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service";
|
||||||
import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service";
|
import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service";
|
||||||
@ -605,6 +606,7 @@ export class ServiceContainer {
|
|||||||
this.stateService,
|
this.stateService,
|
||||||
this.autofillSettingsService,
|
this.autofillSettingsService,
|
||||||
this.encryptService,
|
this.encryptService,
|
||||||
|
new FallbackBulkEncryptService(this.encryptService),
|
||||||
this.cipherFileUploadService,
|
this.cipherFileUploadService,
|
||||||
this.configService,
|
this.configService,
|
||||||
this.stateProvider,
|
this.stateProvider,
|
||||||
|
@ -4,6 +4,8 @@ import mock from "jest-mock-extended/lib/Mock";
|
|||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||||
import { UserKeyResponse } from "@bitwarden/common/models/response/user-key.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 { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
@ -31,15 +33,18 @@ describe("EmergencyAccessService", () => {
|
|||||||
let apiService: MockProxy<ApiService>;
|
let apiService: MockProxy<ApiService>;
|
||||||
let cryptoService: MockProxy<CryptoService>;
|
let cryptoService: MockProxy<CryptoService>;
|
||||||
let encryptService: MockProxy<EncryptService>;
|
let encryptService: MockProxy<EncryptService>;
|
||||||
|
let bulkEncryptService: MockProxy<BulkEncryptService>;
|
||||||
let cipherService: MockProxy<CipherService>;
|
let cipherService: MockProxy<CipherService>;
|
||||||
let logService: MockProxy<LogService>;
|
let logService: MockProxy<LogService>;
|
||||||
let emergencyAccessService: EmergencyAccessService;
|
let emergencyAccessService: EmergencyAccessService;
|
||||||
|
let configService: ConfigService;
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
emergencyAccessApiService = mock<EmergencyAccessApiService>();
|
emergencyAccessApiService = mock<EmergencyAccessApiService>();
|
||||||
apiService = mock<ApiService>();
|
apiService = mock<ApiService>();
|
||||||
cryptoService = mock<CryptoService>();
|
cryptoService = mock<CryptoService>();
|
||||||
encryptService = mock<EncryptService>();
|
encryptService = mock<EncryptService>();
|
||||||
|
bulkEncryptService = mock<BulkEncryptService>();
|
||||||
cipherService = mock<CipherService>();
|
cipherService = mock<CipherService>();
|
||||||
logService = mock<LogService>();
|
logService = mock<LogService>();
|
||||||
|
|
||||||
@ -48,8 +53,10 @@ describe("EmergencyAccessService", () => {
|
|||||||
apiService,
|
apiService,
|
||||||
cryptoService,
|
cryptoService,
|
||||||
encryptService,
|
encryptService,
|
||||||
|
bulkEncryptService,
|
||||||
cipherService,
|
cipherService,
|
||||||
logService,
|
logService,
|
||||||
|
configService,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -9,6 +9,9 @@ import {
|
|||||||
KdfConfig,
|
KdfConfig,
|
||||||
PBKDF2KdfConfig,
|
PBKDF2KdfConfig,
|
||||||
} from "@bitwarden/common/auth/models/domain/kdf-config";
|
} 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 { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
@ -45,8 +48,10 @@ export class EmergencyAccessService
|
|||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private cryptoService: CryptoService,
|
private cryptoService: CryptoService,
|
||||||
private encryptService: EncryptService,
|
private encryptService: EncryptService,
|
||||||
|
private bulkEncryptService: BulkEncryptService,
|
||||||
private cipherService: CipherService,
|
private cipherService: CipherService,
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
|
private configService: ConfigService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -225,10 +230,18 @@ export class EmergencyAccessService
|
|||||||
);
|
);
|
||||||
const grantorUserKey = new SymmetricCryptoKey(grantorKeyBuffer) as UserKey;
|
const grantorUserKey = new SymmetricCryptoKey(grantorKeyBuffer) as UserKey;
|
||||||
|
|
||||||
const ciphers = await this.encryptService.decryptItems(
|
let ciphers: CipherView[] = [];
|
||||||
response.ciphers.map((c) => new Cipher(c)),
|
if (await this.configService.getFeatureFlag(FeatureFlag.PM4154_BulkEncryptionService)) {
|
||||||
grantorUserKey,
|
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());
|
return ciphers.sort(this.cipherService.getLocaleSortingFunction());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 { StripeService } from "@bitwarden/common/billing/services/payment-processors/stripe.service";
|
||||||
import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service";
|
import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.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 { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction";
|
||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.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 { DefaultConfigService } from "@bitwarden/common/platform/services/config/default-config.service";
|
||||||
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
|
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
|
||||||
import { CryptoService } from "@bitwarden/common/platform/services/crypto.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 { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation";
|
||||||
import { DefaultBroadcasterService } from "@bitwarden/common/platform/services/default-broadcaster.service";
|
import { DefaultBroadcasterService } from "@bitwarden/common/platform/services/default-broadcaster.service";
|
||||||
import { DefaultEnvironmentService } from "@bitwarden/common/platform/services/default-environment.service";
|
import { DefaultEnvironmentService } from "@bitwarden/common/platform/services/default-environment.service";
|
||||||
@ -437,6 +439,7 @@ const safeProviders: SafeProvider[] = [
|
|||||||
stateService: StateServiceAbstraction,
|
stateService: StateServiceAbstraction,
|
||||||
autofillSettingsService: AutofillSettingsServiceAbstraction,
|
autofillSettingsService: AutofillSettingsServiceAbstraction,
|
||||||
encryptService: EncryptService,
|
encryptService: EncryptService,
|
||||||
|
bulkEncryptService: BulkEncryptService,
|
||||||
fileUploadService: CipherFileUploadServiceAbstraction,
|
fileUploadService: CipherFileUploadServiceAbstraction,
|
||||||
configService: ConfigService,
|
configService: ConfigService,
|
||||||
stateProvider: StateProvider,
|
stateProvider: StateProvider,
|
||||||
@ -450,6 +453,7 @@ const safeProviders: SafeProvider[] = [
|
|||||||
stateService,
|
stateService,
|
||||||
autofillSettingsService,
|
autofillSettingsService,
|
||||||
encryptService,
|
encryptService,
|
||||||
|
bulkEncryptService,
|
||||||
fileUploadService,
|
fileUploadService,
|
||||||
configService,
|
configService,
|
||||||
stateProvider,
|
stateProvider,
|
||||||
@ -463,6 +467,7 @@ const safeProviders: SafeProvider[] = [
|
|||||||
StateServiceAbstraction,
|
StateServiceAbstraction,
|
||||||
AutofillSettingsServiceAbstraction,
|
AutofillSettingsServiceAbstraction,
|
||||||
EncryptService,
|
EncryptService,
|
||||||
|
BulkEncryptService,
|
||||||
CipherFileUploadServiceAbstraction,
|
CipherFileUploadServiceAbstraction,
|
||||||
ConfigService,
|
ConfigService,
|
||||||
StateProvider,
|
StateProvider,
|
||||||
@ -832,6 +837,11 @@ const safeProviders: SafeProvider[] = [
|
|||||||
useClass: MultithreadEncryptServiceImplementation,
|
useClass: MultithreadEncryptServiceImplementation,
|
||||||
deps: [CryptoFunctionServiceAbstraction, LogService, LOG_MAC_FAILURES],
|
deps: [CryptoFunctionServiceAbstraction, LogService, LOG_MAC_FAILURES],
|
||||||
}),
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: BulkEncryptService,
|
||||||
|
useClass: BulkEncryptServiceImplementation,
|
||||||
|
deps: [CryptoFunctionServiceAbstraction, LogService],
|
||||||
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: EventUploadServiceAbstraction,
|
provide: EventUploadServiceAbstraction,
|
||||||
useClass: EventUploadService,
|
useClass: EventUploadService,
|
||||||
|
@ -14,6 +14,7 @@ export enum FeatureFlag {
|
|||||||
EnableDeleteProvider = "AC-1218-delete-provider",
|
EnableDeleteProvider = "AC-1218-delete-provider",
|
||||||
ExtensionRefresh = "extension-refresh",
|
ExtensionRefresh = "extension-refresh",
|
||||||
RestrictProviderAccess = "restrict-provider-access",
|
RestrictProviderAccess = "restrict-provider-access",
|
||||||
|
PM4154_BulkEncryptionService = "PM-4154-bulk-encryption-service",
|
||||||
UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection",
|
UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection",
|
||||||
EmailVerification = "email-verification",
|
EmailVerification = "email-verification",
|
||||||
InlineMenuFieldQualification = "inline-menu-field-qualification",
|
InlineMenuFieldQualification = "inline-menu-field-qualification",
|
||||||
@ -49,6 +50,7 @@ export const DefaultFeatureFlagValue = {
|
|||||||
[FeatureFlag.EnableDeleteProvider]: FALSE,
|
[FeatureFlag.EnableDeleteProvider]: FALSE,
|
||||||
[FeatureFlag.ExtensionRefresh]: FALSE,
|
[FeatureFlag.ExtensionRefresh]: FALSE,
|
||||||
[FeatureFlag.RestrictProviderAccess]: FALSE,
|
[FeatureFlag.RestrictProviderAccess]: FALSE,
|
||||||
|
[FeatureFlag.PM4154_BulkEncryptionService]: FALSE,
|
||||||
[FeatureFlag.UseTreeWalkerApiForPageDetailsCollection]: FALSE,
|
[FeatureFlag.UseTreeWalkerApiForPageDetailsCollection]: FALSE,
|
||||||
[FeatureFlag.EmailVerification]: FALSE,
|
[FeatureFlag.EmailVerification]: FALSE,
|
||||||
[FeatureFlag.InlineMenuFieldQualification]: FALSE,
|
[FeatureFlag.InlineMenuFieldQualification]: FALSE,
|
||||||
|
@ -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<T extends InitializerMetadata>(
|
||||||
|
items: Decryptable<T>[],
|
||||||
|
key: SymmetricCryptoKey,
|
||||||
|
): Promise<T[]>;
|
||||||
|
}
|
@ -13,6 +13,11 @@ export abstract class EncryptService {
|
|||||||
abstract rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise<EncString>;
|
abstract rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise<EncString>;
|
||||||
abstract rsaDecrypt(data: EncString, privateKey: Uint8Array): Promise<Uint8Array>;
|
abstract rsaDecrypt(data: EncString, privateKey: Uint8Array): Promise<Uint8Array>;
|
||||||
abstract resolveLegacyKey(key: SymmetricCryptoKey, encThing: Encrypted): SymmetricCryptoKey;
|
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<T extends InitializerMetadata>(
|
abstract decryptItems<T extends InitializerMetadata>(
|
||||||
items: Decryptable<T>[],
|
items: Decryptable<T>[],
|
||||||
key: SymmetricCryptoKey,
|
key: SymmetricCryptoKey,
|
||||||
|
@ -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<void>();
|
||||||
|
|
||||||
|
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<T extends InitializerMetadata>(
|
||||||
|
items: Decryptable<T>[],
|
||||||
|
key: SymmetricCryptoKey,
|
||||||
|
): Promise<T[]> {
|
||||||
|
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<T extends InitializerMetadata>(
|
||||||
|
items: Decryptable<T>[],
|
||||||
|
key: SymmetricCryptoKey,
|
||||||
|
): Promise<T[]> {
|
||||||
|
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<T>) => {
|
||||||
|
const initializer = getClassInitializer<T>(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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -185,6 +185,9 @@ export class EncryptServiceImplementation implements EncryptService {
|
|||||||
return this.cryptoFunctionService.rsaDecrypt(data.dataBytes, privateKey, algorithm);
|
return this.cryptoFunctionService.rsaDecrypt(data.dataBytes, privateKey, algorithm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Replaced by BulkEncryptService (PM-4154)
|
||||||
|
*/
|
||||||
async decryptItems<T extends InitializerMetadata>(
|
async decryptItems<T extends InitializerMetadata>(
|
||||||
items: Decryptable<T>[],
|
items: Decryptable<T>[],
|
||||||
key: SymmetricCryptoKey,
|
key: SymmetricCryptoKey,
|
||||||
|
@ -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<T extends InitializerMetadata>(
|
||||||
|
items: Decryptable<T>[],
|
||||||
|
key: SymmetricCryptoKey,
|
||||||
|
): Promise<T[]> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
// TTL (time to live) is not strictly required but avoids tying up memory resources if inactive
|
||||||
const workerTTL = 3 * 60000; // 3 minutes
|
const workerTTL = 3 * 60000; // 3 minutes
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Replaced by BulkEncryptionService (PM-4154)
|
||||||
|
*/
|
||||||
export class MultithreadEncryptServiceImplementation extends EncryptServiceImplementation {
|
export class MultithreadEncryptServiceImplementation extends EncryptServiceImplementation {
|
||||||
private worker: Worker;
|
private worker: Worker;
|
||||||
private timeout: any;
|
private timeout: any;
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { mock } from "jest-mock-extended";
|
import { mock } from "jest-mock-extended";
|
||||||
import { BehaviorSubject, of } from "rxjs";
|
import { BehaviorSubject, of } from "rxjs";
|
||||||
|
|
||||||
|
import { BulkEncryptService } from "@bitwarden/common/platform/abstractions/bulk-encrypt.service";
|
||||||
|
|
||||||
import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service";
|
import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service";
|
||||||
import { FakeStateProvider } from "../../../spec/fake-state-provider";
|
import { FakeStateProvider } from "../../../spec/fake-state-provider";
|
||||||
import { makeStaticByteArray } from "../../../spec/utils";
|
import { makeStaticByteArray } from "../../../spec/utils";
|
||||||
@ -114,6 +116,7 @@ describe("Cipher Service", () => {
|
|||||||
const i18nService = mock<I18nService>();
|
const i18nService = mock<I18nService>();
|
||||||
const searchService = mock<SearchService>();
|
const searchService = mock<SearchService>();
|
||||||
const encryptService = mock<EncryptService>();
|
const encryptService = mock<EncryptService>();
|
||||||
|
const bulkEncryptService = mock<BulkEncryptService>();
|
||||||
const configService = mock<ConfigService>();
|
const configService = mock<ConfigService>();
|
||||||
accountService = mockAccountServiceWith(mockUserId);
|
accountService = mockAccountServiceWith(mockUserId);
|
||||||
const stateProvider = new FakeStateProvider(accountService);
|
const stateProvider = new FakeStateProvider(accountService);
|
||||||
@ -136,6 +139,7 @@ describe("Cipher Service", () => {
|
|||||||
stateService,
|
stateService,
|
||||||
autofillSettingsService,
|
autofillSettingsService,
|
||||||
encryptService,
|
encryptService,
|
||||||
|
bulkEncryptService,
|
||||||
cipherFileUploadService,
|
cipherFileUploadService,
|
||||||
configService,
|
configService,
|
||||||
stateProvider,
|
stateProvider,
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import { firstValueFrom, map, Observable, skipWhile, switchMap } from "rxjs";
|
import { firstValueFrom, map, Observable, skipWhile, switchMap } from "rxjs";
|
||||||
import { SemVer } from "semver";
|
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 { ApiService } from "../../abstractions/api.service";
|
||||||
import { SearchService } from "../../abstractions/search.service";
|
import { SearchService } from "../../abstractions/search.service";
|
||||||
import { AutofillSettingsServiceAbstraction } from "../../autofill/services/autofill-settings.service";
|
import { AutofillSettingsServiceAbstraction } from "../../autofill/services/autofill-settings.service";
|
||||||
@ -102,6 +105,7 @@ export class CipherService implements CipherServiceAbstraction {
|
|||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
private autofillSettingsService: AutofillSettingsServiceAbstraction,
|
private autofillSettingsService: AutofillSettingsServiceAbstraction,
|
||||||
private encryptService: EncryptService,
|
private encryptService: EncryptService,
|
||||||
|
private bulkEncryptService: BulkEncryptService,
|
||||||
private cipherFileUploadService: CipherFileUploadService,
|
private cipherFileUploadService: CipherFileUploadService,
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
private stateProvider: StateProvider,
|
private stateProvider: StateProvider,
|
||||||
@ -397,12 +401,19 @@ export class CipherService implements CipherServiceAbstraction {
|
|||||||
|
|
||||||
const decCiphers = (
|
const decCiphers = (
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
Object.entries(grouped).map(([orgId, groupedCiphers]) =>
|
Object.entries(grouped).map(async ([orgId, groupedCiphers]) => {
|
||||||
this.encryptService.decryptItems(
|
if (await this.configService.getFeatureFlag(FeatureFlag.PM4154_BulkEncryptionService)) {
|
||||||
groupedCiphers,
|
return await this.bulkEncryptService.decryptItems(
|
||||||
keys.orgKeys[orgId as OrganizationId] ?? keys.userKey,
|
groupedCiphers,
|
||||||
),
|
keys.orgKeys[orgId as OrganizationId] ?? keys.userKey,
|
||||||
),
|
);
|
||||||
|
} else {
|
||||||
|
return await this.encryptService.decryptItems(
|
||||||
|
groupedCiphers,
|
||||||
|
keys.orgKeys[orgId as OrganizationId] ?? keys.userKey,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.flat()
|
.flat()
|
||||||
@ -515,7 +526,12 @@ export class CipherService implements CipherServiceAbstraction {
|
|||||||
|
|
||||||
const ciphers = response.data.map((cr) => new Cipher(new CipherData(cr)));
|
const ciphers = response.data.map((cr) => new Cipher(new CipherData(cr)));
|
||||||
const key = await this.cryptoService.getOrgKey(organizationId);
|
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());
|
decCiphers.sort(this.getLocaleSortingFunction());
|
||||||
return decCiphers;
|
return decCiphers;
|
||||||
|
Loading…
Reference in New Issue
Block a user