mirror of
https://github.com/bitwarden/browser
synced 2025-12-14 07:13:32 +00: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:
@@ -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 rsaDecrypt(data: EncString, privateKey: Uint8Array): Promise<Uint8Array>;
|
||||
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>(
|
||||
items: Decryptable<T>[],
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Replaced by BulkEncryptService (PM-4154)
|
||||
*/
|
||||
async decryptItems<T extends InitializerMetadata>(
|
||||
items: Decryptable<T>[],
|
||||
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
|
||||
const workerTTL = 3 * 60000; // 3 minutes
|
||||
|
||||
/**
|
||||
* @deprecated Replaced by BulkEncryptionService (PM-4154)
|
||||
*/
|
||||
export class MultithreadEncryptServiceImplementation extends EncryptServiceImplementation {
|
||||
private worker: Worker;
|
||||
private timeout: any;
|
||||
|
||||
Reference in New Issue
Block a user