1
0
mirror of https://github.com/bitwarden/jslib synced 2025-12-24 04:04:38 +00:00

WIP start building out CryptoWorkerService

This commit is contained in:
Thomas Rittson
2022-03-24 17:22:05 +10:00
parent 08611e60bb
commit 8b8acc15d2
8 changed files with 147 additions and 82 deletions

View File

@@ -0,0 +1,7 @@
import { CipherData } from '../models/data/cipherData';
import { CipherView } from '../models/view/cipherView';
export abstract class CryptoWorkerService {
decryptCiphers: (cipherData: CipherData[]) => Promise<CipherView[]>;
terminateAll: () => Promise<void[]>;
}

View File

@@ -24,6 +24,7 @@ export abstract class PlatformUtilsService {
getApplicationVersion: () => Promise<string>;
supportsWebAuthn: (win: Window) => boolean;
supportsDuo: () => boolean;
supportsWorkers: () => boolean;
showToast: (
type: "error" | "success" | "warning" | "info",
title: string,

View File

@@ -1,5 +0,0 @@
export abstract class WebWorkerService {
create: (name: string) => Worker;
terminate: (name: string) => Promise<void>;
terminateAll: () => Promise<void[]>;
}

View File

@@ -11,6 +11,7 @@ export class Utils {
static isBrowser = true;
static isMobileBrowser = false;
static isAppleMobileBrowser = false;
static isWorker = false;
static global: any = null;
static tldEndingRegex =
/.*\.(com|net|org|edu|uk|gov|ca|de|jp|fr|au|ru|ch|io|es|us|co|xyz|info|ly|mil)$/;
@@ -31,7 +32,8 @@ export class Utils {
Utils.isBrowser = typeof window !== "undefined";
Utils.isMobileBrowser = Utils.isBrowser && this.isMobile(window);
Utils.isAppleMobileBrowser = Utils.isBrowser && this.isAppleMobile(window);
Utils.global = Utils.isNode && !Utils.isBrowser ? global : window;
Utils.isWorker = !Utils.isBrowser && (self as any).WorkerGlobalScope != null;
Utils.global = Utils.isNode && !Utils.isBrowser ? global : (Utils.isWorker ? self : window);
}
static fromB64ToArray(str: string): Uint8Array {

View File

@@ -4,10 +4,11 @@ import { CryptoService } from "../abstractions/crypto.service";
import { FileUploadService } from "../abstractions/fileUpload.service";
import { I18nService } from "../abstractions/i18n.service";
import { LogService } from "../abstractions/log.service";
import { PlatformUtilsService } from '../abstractions/platformUtils.service';
import { SearchService } from "../abstractions/search.service";
import { SettingsService } from "../abstractions/settings.service";
import { StateService } from "../abstractions/state.service";
import { WebWorkerService } from '../abstractions/webWorker.service';
import { CryptoWorkerService } from '../abstractions/cryptoWorker.service';
import { CipherType } from "../enums/cipherType";
import { FieldType } from "../enums/fieldType";
import { UriMatchType } from "../enums/uriMatchType";
@@ -49,6 +50,8 @@ const DomainMatchBlacklist = new Map<string, Set<string>>([
["google.com", new Set(["script.google.com"])],
]);
const workerThreshold = 0; // Testing only, should be 250
export class CipherService implements CipherServiceAbstraction {
private sortedCiphersCache: SortedCiphersCache = new SortedCiphersCache(
this.sortCiphersByLastUsed
@@ -63,7 +66,8 @@ export class CipherService implements CipherServiceAbstraction {
private searchService: () => SearchService,
private logService: LogService,
private stateService: StateService,
private webWorkerService: WebWorkerService
private cryptoWorkerService: CryptoWorkerService,
private platformUtilsService: PlatformUtilsService
) {}
async getDecryptedCipherCache(): Promise<CipherView[]> {
@@ -326,25 +330,33 @@ export class CipherService implements CipherServiceAbstraction {
@sequentialize(() => "getAllDecrypted")
async getAllDecrypted(): Promise<CipherView[]> {
// TEST
this.webWorkerService.create('test');
const userId = await this.stateService.getUserId();
if ((await this.getDecryptedCipherCache()) != null) {
if (
this.searchService != null &&
(this.searchService().indexedEntityId ?? userId) !== userId
) {
await this.searchService().indexCiphers(userId, await this.getDecryptedCipherCache());
// testing only, remove if statement in prod
if (false) {
const userId = await this.stateService.getUserId();
if ((await this.getDecryptedCipherCache()) != null) {
if (
this.searchService != null &&
(this.searchService().indexedEntityId ?? userId) !== userId
) {
await this.searchService().indexCiphers(userId, await this.getDecryptedCipherCache());
}
return await this.getDecryptedCipherCache();
}
const decCiphers: CipherView[] = [];
const hasKey = await this.cryptoService.hasKey();
if (!hasKey) {
throw new Error("No key.");
}
return await this.getDecryptedCipherCache();
}
const decCiphers: CipherView[] = [];
const hasKey = await this.cryptoService.hasKey();
if (!hasKey) {
throw new Error("No key.");
const cipherData = await this.stateService.getEncryptedCiphers();
const supportsWorkers = this.platformUtilsService.supportsWorkers();
if (supportsWorkers && Object.values(cipherData).length > workerThreshold) {
// Test
// this.cryptoWorkerService.create('test');
// Do stuff
// return this.cryptoWorkerService.
}
const promises: any[] = [];

View File

@@ -0,0 +1,106 @@
import { CryptoService } from '../abstractions/crypto.service';
import { CryptoWorkerService as CryptoWorkerServiceAbstraction } from '../abstractions/cryptoWorker.service';
import { StateService } from '../abstractions/state.service';
import { CipherData } from '../models/data/cipherData';
import { Cipher } from '../models/domain/cipher';
import { CipherView } from '../models/view/cipherView';
interface EncryptionKeys {
key: string,
encKey: string,
orgKeys: string,
privateKey: string
}
export class CryptoWorkerService implements CryptoWorkerServiceAbstraction {
private currentWorkers: Worker[] = [];
constructor(
private cryptoService: CryptoService,
private stateService: StateService,
) { }
// Callers should be sequentialized to prevent duplicate concurrent calls
// Consider whether we should utilize existing workers instead of creating new ones
async decryptCiphers(cipherData: CipherData[]): Promise<CipherView[]> {
if (cipherData == null || cipherData.length === 0) {
return null;
}
const key = (await this.cryptoService.getKey()).keyB64;
const encKey = (await this.stateService.getEncryptedCryptoSymmetricKey());
const orgKeys = (await this.stateService.getEncryptedOrganizationKeys());
const privateKey = (await this.stateService.getEncryptedPrivateKey());
const encryptionKeys: EncryptionKeys = {
key: key,
encKey: encKey,
orgKeys: orgKeys,
privateKey: privateKey
}
const message = {
command: 'decryptCiphers',
ciphers: JSON.stringify(cipherData),
keys: JSON.stringify(encryptionKeys),
}
return new Promise((resolve, reject) => {
const worker = this.createWorker();
worker.addEventListener('message', response => {
this.terminate(worker);
resolve(this.processDecryptedCiphers(response))
});
worker.postMessage(message);
})
}
private async processDecryptedCiphers(response: any): Promise<CipherView[]> {
// TODO: transform from serialized data to CipherViews
return null;
}
private createWorker() {
const worker = new Worker(new URL('../workers/crypto.worker.ts', import.meta.url));
this.currentWorkers.push(worker);
return worker;
}
terminateAll(): Promise<void[]> {
if (this.currentWorkers.length === 0) {
return;
}
const terminateResponses = this.currentWorkers.map((worker: Worker) => this.terminate(worker));
return Promise.all(terminateResponses);
}
private terminate(worker: Worker) {
return new Promise<void>((resolve, reject) => {
const killWorker = () => {
worker.terminate();
const index = this.currentWorkers.indexOf(worker);
if (index > -1) {
this.currentWorkers.splice(index);
}
clearTimeout(terminateWorkerTimeout);
resolve();
}
worker.addEventListener('message', event => {
if (event.data.command === 'clearCacheResponse') {
killWorker();
}
});
worker.postMessage({
command: 'clearCacheRequest',
});
const terminateWorkerTimeout = setTimeout(() => {
killWorker();
}, 250);
});
}
}

View File

@@ -1,56 +0,0 @@
import { WebWorkerService as WebWorkerServiceAbstraction } from '../abstractions/webWorker.service';
export class WebWorkerService implements WebWorkerServiceAbstraction {
constructor() {
console.log('service up')
}
workers = new Map<string, Worker>();
create(name: string) {
if (!this.workers.has(name)) {
// const worker = new Worker(new URL('../workers/crypto.worker.ts', import.meta.url));
// worker.addEventListener('message', (e) => console.log(e));
// this.workers.set(name, worker);
}
return this.workers.get(name);
}
terminate(name: string): Promise<void> {
const worker = this.workers.get(name);
if (worker == null) {
return;
}
worker.postMessage({
type: 'clearCacheRequest',
});
return new Promise((resolve, reject) => {
const terminateWorkerTimeout = setTimeout(() => {
worker.terminate();
this.workers.delete(name);
resolve();
}, 250);
worker.addEventListener('message', event => {
if (event.data.type === 'clearCacheResponse') {
worker.terminate();
this.workers.delete(name);
clearTimeout(terminateWorkerTimeout);
resolve();
}
});
});
}
terminateAll(): Promise<void[]> {
const promises: Promise<void>[] = [];
this.workers.forEach((worker, name) => {
promises.push(this.terminate(name));
});
if (promises.length === 0) {
return;
}
return Promise.all(promises);
}
}

View File

@@ -9,8 +9,6 @@ import { CryptoService } from '../services/crypto.service';
import { WebCryptoFunctionService } from '../services/webCryptoFunction.service';
import { WorkerLogService } from '../services/workerLogService';
import { Utils } from '../misc/utils';
const workerApi: Worker = self as any;
// const Keys = ConstantsService.cryptoKeys;
let firstRun = true;