1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-08 20:50:28 +00:00

Remove legacy encryption services

This commit is contained in:
Bernd Schoolmann
2025-04-29 17:56:21 +02:00
parent 69a10b2526
commit 2dd4781e5e
25 changed files with 10 additions and 1886 deletions

View File

@@ -3,7 +3,6 @@ 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";
@@ -32,7 +31,6 @@ export class InitService {
private viewCacheService: PopupViewCacheService,
private configService: ConfigService,
private encryptService: EncryptService,
private bulkEncryptService: BulkEncryptService,
@Inject(DOCUMENT) private document: Document,
) {}
@@ -43,7 +41,6 @@ export class InitService {
this.configService.serverConfig$.subscribe((newConfig) => {
if (newConfig != null) {
this.encryptService.onServerConfigChange(newConfig);
this.bulkEncryptService.onServerConfigChange(newConfig);
}
});
await this.i18nService.init();

View File

@@ -54,6 +54,5 @@
"src",
"../../libs/common/src/autofill/constants",
"../../libs/common/custom-matchers.d.ts",
"../../libs/common/src/key-management/crypto/services/encrypt.worker.ts"
]
}

View File

@@ -201,7 +201,6 @@ const mainConfig = {
"./src/autofill/overlay/inline-menu/pages/list/bootstrap-autofill-inline-menu-list.ts",
"overlay/menu":
"./src/autofill/overlay/inline-menu/pages/menu-container/bootstrap-autofill-inline-menu-container.ts",
"encrypt-worker": "../../libs/common/src/key-management/crypto/services/encrypt.worker.ts",
"content/send-on-installed-message": "./src/vault/content/send-on-installed-message.ts",
"content/send-popup-open-message": "./src/vault/content/send-popup-open-message.ts",
},

View File

@@ -60,7 +60,6 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs
import { DefaultBillingAccountProfileStateService } from "@bitwarden/common/billing/services/account/billing-account-profile-state.service";
import { ClientType } from "@bitwarden/common/enums";
import { EncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/encrypt.service.implementation";
import { FallbackBulkEncryptService } from "@bitwarden/common/key-management/crypto/services/fallback-bulk-encrypt.service";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
import { DeviceTrustService } from "@bitwarden/common/key-management/device-trust/services/device-trust.service.implementation";
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/services/key-connector.service";

View File

@@ -48,5 +48,5 @@
"angularCompilerOptions": {
"strictTemplates": true
},
"include": ["src", "../../libs/common/src/key-management/crypto/services/encrypt.worker.ts"]
"include": ["src"]
}

View File

@@ -6,7 +6,6 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { PolicyData } from "@bitwarden/common/admin-console/models/data/policy.data";
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
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 { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@@ -58,7 +57,6 @@ export class EmergencyAccessService
private apiService: ApiService,
private keyService: KeyService,
private encryptService: EncryptService,
private bulkEncryptService: BulkEncryptService,
private cipherService: CipherService,
private logService: LogService,
private configService: ConfigService,

View File

@@ -7,7 +7,6 @@ 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";
@@ -42,7 +41,6 @@ export class InitService {
private ipcService: IpcService,
private sdkLoadService: SdkLoadService,
private configService: ConfigService,
private bulkEncryptService: BulkEncryptService,
@Inject(DOCUMENT) private document: Document,
) {}

View File

@@ -2,7 +2,6 @@
"extends": "./tsconfig.json",
"files": ["src/polyfills.ts", "src/main.ts", "src/theme.ts"],
"include": [
"src/connectors/*.ts",
"../../libs/common/src/key-management/crypto/services/encrypt.worker.ts"
"src/connectors/*.ts"
]
}

View File

@@ -42,7 +42,6 @@
"include": [
"src/connectors/*.ts",
"src/**/*.stories.ts",
"src/**/*.spec.ts",
"../../libs/common/src/key-management/crypto/services/encrypt.worker.ts"
"src/**/*.spec.ts"
]
}

View File

@@ -8,7 +8,6 @@
"../../bitwarden_license/bit-web/src/main.ts"
],
"include": [
"../../apps/web/src/connectors/*.ts",
"../../libs/common/src/key-management/crypto/services/encrypt.worker.ts"
"../../apps/web/src/connectors/*.ts"
]
}

View File

@@ -47,7 +47,6 @@
"../../apps/web/src/connectors/*.ts",
"../../apps/web/src/**/*.stories.ts",
"../../apps/web/src/**/*.spec.ts",
"../../libs/common/src/key-management/crypto/services/encrypt.worker.ts",
"src/**/*.stories.ts",
"src/**/*.spec.ts"

View File

@@ -143,11 +143,8 @@ import { BillingApiService } from "@bitwarden/common/billing/services/billing-ap
import { OrganizationBillingApiService } from "@bitwarden/common/billing/services/organization/organization-billing-api.service";
import { OrganizationBillingService } from "@bitwarden/common/billing/services/organization-billing.service";
import { TaxService } from "@bitwarden/common/billing/services/tax.service";
import { BulkEncryptService } from "@bitwarden/common/key-management/crypto/abstractions/bulk-encrypt.service";
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { BulkEncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/bulk-encrypt.service.implementation";
import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/multithread-encrypt.service.implementation";
import { WebCryptoFunctionService } from "@bitwarden/common/key-management/crypto/services/web-crypto-function.service";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
import { DeviceTrustService } from "@bitwarden/common/key-management/device-trust/services/device-trust.service.implementation";
@@ -499,7 +496,6 @@ const safeProviders: SafeProvider[] = [
stateService: StateServiceAbstraction,
autofillSettingsService: AutofillSettingsServiceAbstraction,
encryptService: EncryptService,
bulkEncryptService: BulkEncryptService,
fileUploadService: CipherFileUploadServiceAbstraction,
configService: ConfigService,
stateProvider: StateProvider,
@@ -515,7 +511,6 @@ const safeProviders: SafeProvider[] = [
stateService,
autofillSettingsService,
encryptService,
bulkEncryptService,
fileUploadService,
configService,
stateProvider,
@@ -531,7 +526,6 @@ const safeProviders: SafeProvider[] = [
StateServiceAbstraction,
AutofillSettingsServiceAbstraction,
EncryptService,
BulkEncryptService,
CipherFileUploadServiceAbstraction,
ConfigService,
StateProvider,
@@ -916,14 +910,9 @@ const safeProviders: SafeProvider[] = [
}),
safeProvider({
provide: EncryptService,
useClass: MultithreadEncryptServiceImplementation,
useClass: EncryptServiceImplementation,
deps: [CryptoFunctionServiceAbstraction, LogService, LOG_MAC_FAILURES],
}),
safeProvider({
provide: BulkEncryptService,
useClass: BulkEncryptServiceImplementation,
deps: [CryptoFunctionServiceAbstraction, LogService],
}),
safeProvider({
provide: EventUploadServiceAbstraction,
useClass: EventUploadService,

View File

@@ -1,13 +0,0 @@
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<T extends InitializerMetadata>(
items: Decryptable<T>[],
key: SymmetricCryptoKey,
): Promise<T[]>;
abstract onServerConfigChange(newConfig: ServerConfig): void;
}

View File

@@ -1,8 +1,3 @@
import {
CbcDecryptParameters,
EcbDecryptParameters,
} from "../../../platform/models/domain/decrypt-parameters";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { CsprngArray } from "../../../types/csprng";
export abstract class CryptoFunctionService {
@@ -48,19 +43,6 @@ export abstract class CryptoFunctionService {
algorithm: "sha1" | "sha256" | "sha512",
): Promise<Uint8Array | string>;
abstract compareFast(a: Uint8Array | string, b: Uint8Array | string): Promise<boolean>;
abstract aesEncrypt(data: Uint8Array, iv: Uint8Array, key: Uint8Array): Promise<Uint8Array>;
abstract aesDecryptFastParameters(
data: string,
iv: string,
mac: string,
key: SymmetricCryptoKey,
): CbcDecryptParameters<Uint8Array | string>;
abstract aesDecryptFast({
mode,
parameters,
}:
| { mode: "cbc"; parameters: CbcDecryptParameters<Uint8Array | string> }
| { mode: "ecb"; parameters: EcbDecryptParameters<Uint8Array | string> }): Promise<string>;
abstract aesDecrypt(
data: Uint8Array,
iv: Uint8Array,

View File

@@ -1,170 +0,0 @@
import { mock, MockProxy } from "jest-mock-extended";
import * as rxjs from "rxjs";
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
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 { CryptoFunctionService } from "../abstractions/crypto-function.service";
import { buildSetConfigMessage } from "../types/worker-command.type";
import { BulkEncryptServiceImplementation } from "./bulk-encrypt.service.implementation";
describe("BulkEncryptServiceImplementation", () => {
const cryptoFunctionService = mock<CryptoFunctionService>();
const logService = mock<LogService>();
let sut: BulkEncryptServiceImplementation;
beforeEach(() => {
sut = new BulkEncryptServiceImplementation(cryptoFunctionService, logService);
});
afterEach(() => {
jest.resetAllMocks();
});
describe("decryptItems", () => {
const key = mock<SymmetricCryptoKey>();
const serverConfig = mock<ServerConfig>();
const mockWorker = mock<Worker>();
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<any, any>(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<ServerConfig>();
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<ServerConfig>();
const mockWorker = mock<Worker>();
(sut as any).workers = [mockWorker];
sut.onServerConfigChange(newConfig);
expect(mockWorker.postMessage).toHaveBeenCalledWith(buildSetConfigMessage({ newConfig }));
});
});
});
function createMockDecryptable<T extends InitializerMetadata>(
returnValue: any,
): MockProxy<Decryptable<T>> {
const mockDecryptable = mock<Decryptable<T>>();
mockDecryptable.decrypt.mockResolvedValue(returnValue);
return mockDecryptable;
}

View File

@@ -1,185 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { firstValueFrom, fromEvent, filter, map, takeUntil, defaultIfEmpty, Subject } from "rxjs";
import { Jsonify } from "type-fest";
import { BulkEncryptService } from "@bitwarden/common/key-management/crypto/abstractions/bulk-encrypt.service";
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface";
import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface";
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;
const minNumberOfItemsForMultithreading = 400;
export class BulkEncryptServiceImplementation implements BulkEncryptService {
private workers: Worker[] = [];
private timeout: any;
private currentServerConfig: ServerConfig | undefined = undefined;
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;
}
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).
*/
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/key-management/crypto/services/encrypt.worker.ts",
import.meta.url,
),
),
);
}
if (this.currentServerConfig != undefined) {
this.updateWorkerServerConfigs(this.currentServerConfig);
}
}
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 id = Utils.newGuid();
const request = buildDecryptMessage({
id,
items: itemsForWorker,
key: key,
});
worker.postMessage(request);
results.push(
firstValueFrom(
fromEvent(worker, "message").pipe(
filter((response: MessageEvent) => response.data?.id === 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 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) {
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);
}
}
}

View File

@@ -150,222 +150,6 @@ export class EncryptServiceImplementation implements EncryptService {
this.blockType0 = getFeatureFlagValue(newConfig, FeatureFlag.PM17987_BlockType0);
}
async encrypt(plainValue: string | Uint8Array, key: SymmetricCryptoKey): Promise<EncString> {
if (key == null) {
throw new Error("No encryption key provided.");
}
if (this.blockType0) {
if (key.inner().type === EncryptionType.AesCbc256_B64) {
throw new Error("Type 0 encryption is not supported.");
}
}
if (plainValue == null) {
return Promise.resolve(null);
}
if (typeof plainValue === "string") {
return this.encryptUint8Array(Utils.fromUtf8ToArray(plainValue), key);
} else {
return this.encryptUint8Array(plainValue, key);
}
}
private async encryptUint8Array(
plainValue: Uint8Array,
key: SymmetricCryptoKey,
): Promise<EncString> {
if (key == null) {
throw new Error("No encryption key provided.");
}
if (this.blockType0) {
if (key.inner().type === EncryptionType.AesCbc256_B64) {
throw new Error("Type 0 encryption is not supported.");
}
}
if (plainValue == null) {
return Promise.resolve(null);
}
const innerKey = key.inner();
if (innerKey.type === EncryptionType.AesCbc256_HmacSha256_B64) {
const encObj = await this.aesEncrypt(plainValue, innerKey);
const iv = Utils.fromBufferToB64(encObj.iv);
const data = Utils.fromBufferToB64(encObj.data);
const mac = Utils.fromBufferToB64(encObj.mac);
return new EncString(innerKey.type, data, iv, mac);
} else if (innerKey.type === EncryptionType.AesCbc256_B64) {
const encObj = await this.aesEncryptLegacy(plainValue, innerKey);
const iv = Utils.fromBufferToB64(encObj.iv);
const data = Utils.fromBufferToB64(encObj.data);
return new EncString(innerKey.type, data, iv);
}
}
async encryptToBytes(plainValue: Uint8Array, key: SymmetricCryptoKey): Promise<EncArrayBuffer> {
if (key == null) {
throw new Error("No encryption key provided.");
}
if (this.blockType0) {
if (key.inner().type === EncryptionType.AesCbc256_B64) {
throw new Error("Type 0 encryption is not supported.");
}
}
const innerKey = key.inner();
if (innerKey.type === EncryptionType.AesCbc256_HmacSha256_B64) {
const encValue = await this.aesEncrypt(plainValue, innerKey);
const macLen = encValue.mac.length;
const encBytes = new Uint8Array(
1 + encValue.iv.byteLength + macLen + encValue.data.byteLength,
);
encBytes.set([innerKey.type]);
encBytes.set(new Uint8Array(encValue.iv), 1);
encBytes.set(new Uint8Array(encValue.mac), 1 + encValue.iv.byteLength);
encBytes.set(new Uint8Array(encValue.data), 1 + encValue.iv.byteLength + macLen);
return new EncArrayBuffer(encBytes);
} else if (innerKey.type === EncryptionType.AesCbc256_B64) {
const encValue = await this.aesEncryptLegacy(plainValue, innerKey);
const encBytes = new Uint8Array(1 + encValue.iv.byteLength + encValue.data.byteLength);
encBytes.set([innerKey.type]);
encBytes.set(new Uint8Array(encValue.iv), 1);
encBytes.set(new Uint8Array(encValue.data), 1 + encValue.iv.byteLength);
return new EncArrayBuffer(encBytes);
}
}
async decryptToUtf8(
encString: EncString,
key: SymmetricCryptoKey,
decryptContext: string = "no context",
): Promise<string> {
if (key == null) {
throw new Error("No key provided for decryption.");
}
const innerKey = key.inner();
if (encString.encryptionType !== innerKey.type) {
this.logDecryptError(
"Key encryption type does not match payload encryption type",
innerKey.type,
encString.encryptionType,
decryptContext,
);
return null;
}
if (innerKey.type === EncryptionType.AesCbc256_HmacSha256_B64) {
const fastParams = this.cryptoFunctionService.aesDecryptFastParameters(
encString.data,
encString.iv,
encString.mac,
key,
);
const computedMac = await this.cryptoFunctionService.hmacFast(
fastParams.macData,
fastParams.macKey,
"sha256",
);
const macsEqual = await this.cryptoFunctionService.compareFast(fastParams.mac, computedMac);
if (!macsEqual) {
this.logMacFailed(
"decryptToUtf8 MAC comparison failed. Key or payload has changed.",
innerKey.type,
encString.encryptionType,
decryptContext,
);
return null;
}
return await this.cryptoFunctionService.aesDecryptFast({
mode: "cbc",
parameters: fastParams,
});
} else if (innerKey.type === EncryptionType.AesCbc256_B64) {
const fastParams = this.cryptoFunctionService.aesDecryptFastParameters(
encString.data,
encString.iv,
undefined,
key,
);
return await this.cryptoFunctionService.aesDecryptFast({
mode: "cbc",
parameters: fastParams,
});
} else {
throw new Error(`Unsupported encryption type`);
}
}
async decryptToBytes(
encThing: Encrypted,
key: SymmetricCryptoKey,
decryptContext: string = "no context",
): Promise<Uint8Array | null> {
if (key == null) {
throw new Error("No encryption key provided.");
}
if (encThing == null) {
throw new Error("Nothing provided for decryption.");
}
const inner = key.inner();
if (encThing.encryptionType !== inner.type) {
this.logDecryptError(
"Encryption key type mismatch",
inner.type,
encThing.encryptionType,
decryptContext,
);
return null;
}
if (inner.type === EncryptionType.AesCbc256_HmacSha256_B64) {
if (encThing.macBytes == null) {
this.logDecryptError("Mac missing", inner.type, encThing.encryptionType, decryptContext);
return null;
}
const macData = new Uint8Array(encThing.ivBytes.byteLength + encThing.dataBytes.byteLength);
macData.set(new Uint8Array(encThing.ivBytes), 0);
macData.set(new Uint8Array(encThing.dataBytes), encThing.ivBytes.byteLength);
const computedMac = await this.cryptoFunctionService.hmac(
macData,
inner.authenticationKey,
"sha256",
);
const macsMatch = await this.cryptoFunctionService.compare(encThing.macBytes, computedMac);
if (!macsMatch) {
this.logMacFailed(
"MAC comparison failed. Key or payload has changed.",
inner.type,
encThing.encryptionType,
decryptContext,
);
return null;
}
return await this.cryptoFunctionService.aesDecrypt(
encThing.dataBytes,
encThing.ivBytes,
inner.encryptionKey,
"cbc",
);
} else if (inner.type === EncryptionType.AesCbc256_B64) {
return await this.cryptoFunctionService.aesDecrypt(
encThing.dataBytes,
encThing.ivBytes,
inner.encryptionKey,
"cbc",
);
}
}
async encapsulateKeyUnsigned(
sharedKey: SymmetricCryptoKey,
encapsulationKey: Uint8Array,
@@ -384,9 +168,6 @@ export class EncryptServiceImplementation implements EncryptService {
return new SymmetricCryptoKey(keyBytes);
}
/**
* @deprecated Replaced by BulkEncryptService (PM-4154)
*/
async decryptItems<T extends InitializerMetadata>(
items: Decryptable<T>[],
key: SymmetricCryptoKey,
@@ -403,29 +184,6 @@ export class EncryptServiceImplementation implements EncryptService {
return results;
}
private async aesEncrypt(data: Uint8Array, key: Aes256CbcHmacKey): Promise<EncryptedObject> {
const obj = new EncryptedObject();
obj.iv = await this.cryptoFunctionService.randomBytes(16);
obj.data = await this.cryptoFunctionService.aesEncrypt(data, obj.iv, key.encryptionKey);
const macData = new Uint8Array(obj.iv.byteLength + obj.data.byteLength);
macData.set(new Uint8Array(obj.iv), 0);
macData.set(new Uint8Array(obj.data), obj.iv.byteLength);
obj.mac = await this.cryptoFunctionService.hmac(macData, key.authenticationKey, "sha256");
return obj;
}
/**
* @deprecated Removed once AesCbc256_B64 support is removed
*/
private async aesEncryptLegacy(data: Uint8Array, key: Aes256CbcKey): Promise<EncryptedObject> {
const obj = new EncryptedObject();
obj.iv = await this.cryptoFunctionService.randomBytes(16);
obj.data = await this.cryptoFunctionService.aesEncrypt(data, obj.iv, key.encryptionKey);
return obj;
}
private logDecryptError(
msg: string,
keyEncType: EncryptionType,

View File

@@ -1,746 +0,0 @@
import { mockReset, mock } from "jest-mock-extended";
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { EncryptionType } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import {
Aes256CbcHmacKey,
SymmetricCryptoKey,
} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { CsprngArray } from "@bitwarden/common/types/csprng";
import { makeStaticByteArray } from "../../../../spec";
import { DefaultFeatureFlagValue, FeatureFlag } from "../../../enums/feature-flag.enum";
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
import { EncryptServiceImplementation } from "./encrypt.service.implementation";
describe("EncryptService", () => {
const cryptoFunctionService = mock<CryptoFunctionService>();
const logService = mock<LogService>();
let encryptService: EncryptServiceImplementation;
beforeEach(() => {
mockReset(cryptoFunctionService);
mockReset(logService);
encryptService = new EncryptServiceImplementation(cryptoFunctionService, logService, true);
});
describe("wrapSymmetricKey", () => {
it("roundtrip encrypts and decrypts a symmetric key", async () => {
cryptoFunctionService.aesEncrypt.mockResolvedValue(makeStaticByteArray(64, 0));
cryptoFunctionService.randomBytes.mockResolvedValue(makeStaticByteArray(16) as CsprngArray);
cryptoFunctionService.hmac.mockResolvedValue(makeStaticByteArray(32));
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
const wrappingKey = new SymmetricCryptoKey(makeStaticByteArray(64));
const encString = await encryptService.wrapSymmetricKey(key, wrappingKey);
expect(encString.encryptionType).toEqual(EncryptionType.AesCbc256_HmacSha256_B64);
expect(encString.data).toEqual(Utils.fromBufferToB64(makeStaticByteArray(64, 0)));
});
it("fails if key toBeWrapped is null", async () => {
const wrappingKey = new SymmetricCryptoKey(makeStaticByteArray(64));
await expect(encryptService.wrapSymmetricKey(null, wrappingKey)).rejects.toThrow(
"No keyToBeWrapped provided for wrapping.",
);
});
it("fails if wrapping key is null", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
await expect(encryptService.wrapSymmetricKey(key, null)).rejects.toThrow(
"No wrappingKey provided for wrapping.",
);
});
it("fails if type 0 key is provided with flag turned on", async () => {
(encryptService as any).blockType0 = true;
const mock32Key = mock<SymmetricCryptoKey>();
mock32Key.inner.mockReturnValue({
type: 0,
encryptionKey: makeStaticByteArray(32),
});
await expect(encryptService.wrapSymmetricKey(mock32Key, mock32Key)).rejects.toThrow(
"Type 0 encryption is not supported.",
);
});
});
describe("wrapDecapsulationKey", () => {
it("roundtrip encrypts and decrypts a decapsulation key", async () => {
cryptoFunctionService.aesEncrypt.mockResolvedValue(makeStaticByteArray(64, 0));
cryptoFunctionService.randomBytes.mockResolvedValue(makeStaticByteArray(16) as CsprngArray);
cryptoFunctionService.hmac.mockResolvedValue(makeStaticByteArray(32));
const wrappingKey = new SymmetricCryptoKey(makeStaticByteArray(64));
const encString = await encryptService.wrapDecapsulationKey(
makeStaticByteArray(64),
wrappingKey,
);
expect(encString.encryptionType).toEqual(EncryptionType.AesCbc256_HmacSha256_B64);
expect(encString.data).toEqual(Utils.fromBufferToB64(makeStaticByteArray(64, 0)));
});
it("fails if decapsulation key is null", async () => {
const wrappingKey = new SymmetricCryptoKey(makeStaticByteArray(64));
await expect(encryptService.wrapDecapsulationKey(null, wrappingKey)).rejects.toThrow(
"No decapsulation key provided for wrapping.",
);
});
it("fails if wrapping key is null", async () => {
const decapsulationKey = makeStaticByteArray(64);
await expect(encryptService.wrapDecapsulationKey(decapsulationKey, null)).rejects.toThrow(
"No wrappingKey provided for wrapping.",
);
});
it("throws if type 0 key is provided with flag turned on", async () => {
(encryptService as any).blockType0 = true;
const mock32Key = mock<SymmetricCryptoKey>();
mock32Key.inner.mockReturnValue({
type: 0,
encryptionKey: makeStaticByteArray(32),
});
await expect(
encryptService.wrapDecapsulationKey(new Uint8Array(200), mock32Key),
).rejects.toThrow("Type 0 encryption is not supported.");
});
});
describe("wrapEncapsulationKey", () => {
it("roundtrip encrypts and decrypts an encapsulationKey key", async () => {
cryptoFunctionService.aesEncrypt.mockResolvedValue(makeStaticByteArray(64, 0));
cryptoFunctionService.randomBytes.mockResolvedValue(makeStaticByteArray(16) as CsprngArray);
cryptoFunctionService.hmac.mockResolvedValue(makeStaticByteArray(32));
const wrappingKey = new SymmetricCryptoKey(makeStaticByteArray(64));
const encString = await encryptService.wrapEncapsulationKey(
makeStaticByteArray(64),
wrappingKey,
);
expect(encString.encryptionType).toEqual(EncryptionType.AesCbc256_HmacSha256_B64);
expect(encString.data).toEqual(Utils.fromBufferToB64(makeStaticByteArray(64, 0)));
});
it("fails if encapsulation key is null", async () => {
const wrappingKey = new SymmetricCryptoKey(makeStaticByteArray(64));
await expect(encryptService.wrapEncapsulationKey(null, wrappingKey)).rejects.toThrow(
"No encapsulation key provided for wrapping.",
);
});
it("fails if wrapping key is null", async () => {
const encapsulationKey = makeStaticByteArray(64);
await expect(encryptService.wrapEncapsulationKey(encapsulationKey, null)).rejects.toThrow(
"No wrappingKey provided for wrapping.",
);
});
it("throws if type 0 key is provided with flag turned on", async () => {
(encryptService as any).blockType0 = true;
const mock32Key = mock<SymmetricCryptoKey>();
mock32Key.inner.mockReturnValue({
type: 0,
encryptionKey: makeStaticByteArray(32),
});
await expect(
encryptService.wrapEncapsulationKey(new Uint8Array(200), mock32Key),
).rejects.toThrow("Type 0 encryption is not supported.");
});
});
describe("onServerConfigChange", () => {
const newConfig = mock<ServerConfig>();
afterEach(() => {
jest.resetAllMocks();
});
it("updates internal flag with default value when not present in config", () => {
encryptService.onServerConfigChange(newConfig);
expect((encryptService as any).blockType0).toBe(
DefaultFeatureFlagValue[FeatureFlag.PM17987_BlockType0],
);
});
test.each([true, false])("updates internal flag with value in config", (expectedValue) => {
newConfig.featureStates = { [FeatureFlag.PM17987_BlockType0]: expectedValue };
encryptService.onServerConfigChange(newConfig);
expect((encryptService as any).blockType0).toBe(expectedValue);
});
});
describe("encrypt", () => {
it("throws if no key is provided", () => {
return expect(encryptService.encrypt(null, null)).rejects.toThrow(
"No encryption key provided.",
);
});
it("throws if type 0 key is provided with flag turned on", async () => {
(encryptService as any).blockType0 = true;
const key = new SymmetricCryptoKey(makeStaticByteArray(32));
const mock32Key = mock<SymmetricCryptoKey>();
mock32Key.inner.mockReturnValue({
type: 0,
encryptionKey: makeStaticByteArray(32),
});
await expect(encryptService.encrypt(null!, key)).rejects.toThrow(
"Type 0 encryption is not supported.",
);
await expect(encryptService.encrypt(null!, mock32Key)).rejects.toThrow(
"Type 0 encryption is not supported.",
);
const plainValue = "data";
await expect(encryptService.encrypt(plainValue, key)).rejects.toThrow(
"Type 0 encryption is not supported.",
);
await expect(encryptService.encrypt(plainValue, mock32Key)).rejects.toThrow(
"Type 0 encryption is not supported.",
);
});
it("returns null if no data is provided with valid key", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
const actual = await encryptService.encrypt(null, key);
expect(actual).toBeNull();
});
it("creates an EncString for Aes256Cbc", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(32));
const plainValue = "data";
cryptoFunctionService.aesEncrypt.mockResolvedValue(makeStaticByteArray(4, 100));
cryptoFunctionService.randomBytes.mockResolvedValue(makeStaticByteArray(16) as CsprngArray);
const result = await encryptService.encrypt(plainValue, key);
expect(cryptoFunctionService.aesEncrypt).toHaveBeenCalledWith(
Utils.fromByteStringToArray(plainValue),
makeStaticByteArray(16),
makeStaticByteArray(32),
);
expect(cryptoFunctionService.hmac).not.toHaveBeenCalled();
expect(Utils.fromB64ToArray(result.data).length).toEqual(4);
expect(Utils.fromB64ToArray(result.iv).length).toEqual(16);
});
it("creates an EncString for Aes256Cbc_HmacSha256_B64", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
const plainValue = "data";
cryptoFunctionService.hmac.mockResolvedValue(makeStaticByteArray(32));
cryptoFunctionService.aesEncrypt.mockResolvedValue(makeStaticByteArray(4, 100));
cryptoFunctionService.randomBytes.mockResolvedValue(makeStaticByteArray(16) as CsprngArray);
const result = await encryptService.encrypt(plainValue, key);
expect(cryptoFunctionService.aesEncrypt).toHaveBeenCalledWith(
Utils.fromByteStringToArray(plainValue),
makeStaticByteArray(16),
makeStaticByteArray(32),
);
const macData = new Uint8Array(16 + 4);
macData.set(makeStaticByteArray(16));
macData.set(makeStaticByteArray(4, 100), 16);
expect(cryptoFunctionService.hmac).toHaveBeenCalledWith(
macData,
makeStaticByteArray(32, 32),
"sha256",
);
expect(Utils.fromB64ToArray(result.data).length).toEqual(4);
expect(Utils.fromB64ToArray(result.iv).length).toEqual(16);
expect(Utils.fromB64ToArray(result.mac).length).toEqual(32);
});
});
describe("encryptToBytes", () => {
const plainValue = makeStaticByteArray(16, 1);
it("throws if no key is provided", () => {
return expect(encryptService.encryptToBytes(plainValue, null)).rejects.toThrow(
"No encryption key",
);
});
it("throws if type 0 key provided with flag turned on", async () => {
(encryptService as any).blockType0 = true;
const key = new SymmetricCryptoKey(makeStaticByteArray(32));
const mock32Key = mock<SymmetricCryptoKey>();
mock32Key.inner.mockReturnValue({
type: 0,
encryptionKey: makeStaticByteArray(32),
});
await expect(encryptService.encryptToBytes(plainValue, key)).rejects.toThrow(
"Type 0 encryption is not supported.",
);
await expect(encryptService.encryptToBytes(plainValue, mock32Key)).rejects.toThrow(
"Type 0 encryption is not supported.",
);
});
it("encrypts data with provided Aes256Cbc key and returns correct encbuffer", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(32, 0));
const iv = makeStaticByteArray(16, 80);
const cipherText = makeStaticByteArray(20, 150);
cryptoFunctionService.randomBytes.mockResolvedValue(iv as CsprngArray);
cryptoFunctionService.aesEncrypt.mockResolvedValue(cipherText);
const actual = await encryptService.encryptToBytes(plainValue, key);
const expectedBytes = new Uint8Array(1 + iv.byteLength + cipherText.byteLength);
expectedBytes.set([EncryptionType.AesCbc256_B64]);
expectedBytes.set(iv, 1);
expectedBytes.set(cipherText, 1 + iv.byteLength);
expect(actual.buffer).toEqualBuffer(expectedBytes);
});
it("encrypts data with provided Aes256Cbc_HmacSha256 key and returns correct encbuffer", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64, 0));
const iv = makeStaticByteArray(16, 80);
const mac = makeStaticByteArray(32, 100);
const cipherText = makeStaticByteArray(20, 150);
cryptoFunctionService.randomBytes.mockResolvedValue(iv as CsprngArray);
cryptoFunctionService.aesEncrypt.mockResolvedValue(cipherText);
cryptoFunctionService.hmac.mockResolvedValue(mac);
const actual = await encryptService.encryptToBytes(plainValue, key);
const expectedBytes = new Uint8Array(
1 + iv.byteLength + mac.byteLength + cipherText.byteLength,
);
expectedBytes.set([EncryptionType.AesCbc256_HmacSha256_B64]);
expectedBytes.set(iv, 1);
expectedBytes.set(mac, 1 + iv.byteLength);
expectedBytes.set(cipherText, 1 + iv.byteLength + mac.byteLength);
expect(actual.buffer).toEqualBuffer(expectedBytes);
});
});
describe("decryptToBytes", () => {
const encType = EncryptionType.AesCbc256_HmacSha256_B64;
const key = new SymmetricCryptoKey(makeStaticByteArray(64, 100));
const computedMac = new Uint8Array(1);
const encBuffer = new EncArrayBuffer(makeStaticByteArray(60, encType));
beforeEach(() => {
cryptoFunctionService.hmac.mockResolvedValue(computedMac);
});
it("throws if no key is provided", () => {
return expect(encryptService.decryptToBytes(encBuffer, null)).rejects.toThrow(
"No encryption key",
);
});
it("throws if no encrypted value is provided", () => {
return expect(encryptService.decryptToBytes(null, key)).rejects.toThrow(
"Nothing provided for decryption",
);
});
it("decrypts data with provided key for Aes256CbcHmac", async () => {
const decryptedBytes = makeStaticByteArray(10, 200);
cryptoFunctionService.hmac.mockResolvedValue(makeStaticByteArray(1));
cryptoFunctionService.compare.mockResolvedValue(true);
cryptoFunctionService.aesDecrypt.mockResolvedValueOnce(decryptedBytes);
const actual = await encryptService.decryptToBytes(encBuffer, key);
expect(cryptoFunctionService.aesDecrypt).toBeCalledWith(
expect.toEqualBuffer(encBuffer.dataBytes),
expect.toEqualBuffer(encBuffer.ivBytes),
expect.toEqualBuffer(key.inner().encryptionKey),
"cbc",
);
expect(actual).toEqualBuffer(decryptedBytes);
});
it("decrypts data with provided key for Aes256Cbc", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(32, 0));
const encBuffer = new EncArrayBuffer(makeStaticByteArray(60, EncryptionType.AesCbc256_B64));
const decryptedBytes = makeStaticByteArray(10, 200);
cryptoFunctionService.hmac.mockResolvedValue(makeStaticByteArray(1));
cryptoFunctionService.compare.mockResolvedValue(true);
cryptoFunctionService.aesDecrypt.mockResolvedValueOnce(decryptedBytes);
const actual = await encryptService.decryptToBytes(encBuffer, key);
expect(cryptoFunctionService.aesDecrypt).toBeCalledWith(
expect.toEqualBuffer(encBuffer.dataBytes),
expect.toEqualBuffer(encBuffer.ivBytes),
expect.toEqualBuffer(key.inner().encryptionKey),
"cbc",
);
expect(actual).toEqualBuffer(decryptedBytes);
});
it("compares macs using CryptoFunctionService", async () => {
const expectedMacData = new Uint8Array(
encBuffer.ivBytes.byteLength + encBuffer.dataBytes.byteLength,
);
expectedMacData.set(new Uint8Array(encBuffer.ivBytes));
expectedMacData.set(new Uint8Array(encBuffer.dataBytes), encBuffer.ivBytes.byteLength);
await encryptService.decryptToBytes(encBuffer, key);
expect(cryptoFunctionService.hmac).toBeCalledWith(
expect.toEqualBuffer(expectedMacData),
(key.inner() as Aes256CbcHmacKey).authenticationKey,
"sha256",
);
expect(cryptoFunctionService.compare).toBeCalledWith(
expect.toEqualBuffer(encBuffer.macBytes),
expect.toEqualBuffer(computedMac),
);
});
it("returns null if macs don't match", async () => {
cryptoFunctionService.compare.mockResolvedValue(false);
const actual = await encryptService.decryptToBytes(encBuffer, key);
expect(cryptoFunctionService.compare).toHaveBeenCalled();
expect(cryptoFunctionService.aesDecrypt).not.toHaveBeenCalled();
expect(actual).toBeNull();
});
it("returns null if mac could not be calculated", async () => {
cryptoFunctionService.hmac.mockResolvedValue(null);
const actual = await encryptService.decryptToBytes(encBuffer, key);
expect(cryptoFunctionService.hmac).toHaveBeenCalled();
expect(cryptoFunctionService.aesDecrypt).not.toHaveBeenCalled();
expect(actual).toBeNull();
});
it("returns null if key is Aes256Cbc but encbuffer is Aes256Cbc_HmacSha256", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(32, 0));
cryptoFunctionService.compare.mockResolvedValue(true);
const actual = await encryptService.decryptToBytes(encBuffer, key);
expect(actual).toBeNull();
expect(cryptoFunctionService.aesDecrypt).not.toHaveBeenCalled();
});
it("returns null if key is Aes256Cbc_HmacSha256 but encbuffer is Aes256Cbc", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64, 0));
cryptoFunctionService.compare.mockResolvedValue(true);
const buffer = new EncArrayBuffer(makeStaticByteArray(200, EncryptionType.AesCbc256_B64));
const actual = await encryptService.decryptToBytes(buffer, key);
expect(actual).toBeNull();
expect(cryptoFunctionService.aesDecrypt).not.toHaveBeenCalled();
});
});
describe("decryptToUtf8", () => {
it("throws if no key is provided", () => {
return expect(encryptService.decryptToUtf8(null, null)).rejects.toThrow(
"No key provided for decryption.",
);
});
it("decrypts data with provided key for AesCbc256_HmacSha256", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64, 0));
const encString = new EncString(EncryptionType.AesCbc256_HmacSha256_B64, "data", "iv", "mac");
cryptoFunctionService.aesDecryptFastParameters.mockReturnValue({
macData: makeStaticByteArray(32, 0),
macKey: makeStaticByteArray(32, 0),
mac: makeStaticByteArray(32, 0),
} as any);
cryptoFunctionService.hmacFast.mockResolvedValue(makeStaticByteArray(32, 0));
cryptoFunctionService.compareFast.mockResolvedValue(true);
cryptoFunctionService.aesDecryptFast.mockResolvedValue("data");
const actual = await encryptService.decryptToUtf8(encString, key);
expect(actual).toEqual("data");
expect(cryptoFunctionService.compareFast).toHaveBeenCalledWith(
makeStaticByteArray(32, 0),
makeStaticByteArray(32, 0),
);
});
it("decrypts data with provided key for AesCbc256", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(32, 0));
const encString = new EncString(EncryptionType.AesCbc256_B64, "data");
cryptoFunctionService.aesDecryptFastParameters.mockReturnValue({
macData: makeStaticByteArray(32, 0),
macKey: makeStaticByteArray(32, 0),
mac: makeStaticByteArray(32, 0),
} as any);
cryptoFunctionService.hmacFast.mockResolvedValue(makeStaticByteArray(32, 0));
cryptoFunctionService.compareFast.mockResolvedValue(true);
cryptoFunctionService.aesDecryptFast.mockResolvedValue("data");
const actual = await encryptService.decryptToUtf8(encString, key);
expect(actual).toEqual("data");
expect(cryptoFunctionService.compareFast).not.toHaveBeenCalled();
});
it("returns null if key is AesCbc256_HMAC but encstring is AesCbc256", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64, 0));
const encString = new EncString(EncryptionType.AesCbc256_B64, "data");
const actual = await encryptService.decryptToUtf8(encString, key);
expect(actual).toBeNull();
expect(logService.error).toHaveBeenCalled();
});
it("returns null if key is AesCbc256 but encstring is AesCbc256_HMAC", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(32, 0));
const encString = new EncString(EncryptionType.AesCbc256_HmacSha256_B64, "data", "iv", "mac");
const actual = await encryptService.decryptToUtf8(encString, key);
expect(actual).toBeNull();
expect(logService.error).toHaveBeenCalled();
});
it("returns null if macs don't match", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64, 0));
const encString = new EncString(EncryptionType.AesCbc256_HmacSha256_B64, "data", "iv", "mac");
cryptoFunctionService.aesDecryptFastParameters.mockReturnValue({
macData: makeStaticByteArray(32, 0),
macKey: makeStaticByteArray(32, 0),
mac: makeStaticByteArray(32, 0),
} as any);
cryptoFunctionService.hmacFast.mockResolvedValue(makeStaticByteArray(32, 0));
cryptoFunctionService.compareFast.mockResolvedValue(false);
cryptoFunctionService.aesDecryptFast.mockResolvedValue("data");
const actual = await encryptService.decryptToUtf8(encString, key);
expect(actual).toBeNull();
});
});
describe("decryptToUtf8", () => {
it("throws if no key is provided", () => {
return expect(encryptService.decryptToUtf8(null, null)).rejects.toThrow(
"No key provided for decryption.",
);
});
it("returns null if key is mac key but encstring has no mac", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64, 0));
const encString = new EncString(EncryptionType.AesCbc256_B64, "data");
const actual = await encryptService.decryptToUtf8(encString, key);
expect(actual).toBeNull();
expect(logService.error).toHaveBeenCalled();
});
});
describe("encryptString", () => {
it("is a proxy to encrypt", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
const plainValue = "data";
encryptService.encrypt = jest.fn();
await encryptService.encryptString(plainValue, key);
expect(encryptService.encrypt).toHaveBeenCalledWith(plainValue, key);
});
});
describe("encryptBytes", () => {
it("is a proxy to encrypt", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
const plainValue = makeStaticByteArray(16, 1);
encryptService.encrypt = jest.fn();
await encryptService.encryptBytes(plainValue, key);
expect(encryptService.encrypt).toHaveBeenCalledWith(plainValue, key);
});
});
describe("encryptFileData", () => {
it("is a proxy to encryptToBytes", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
const plainValue = makeStaticByteArray(16, 1);
encryptService.encryptToBytes = jest.fn();
await encryptService.encryptFileData(plainValue, key);
expect(encryptService.encryptToBytes).toHaveBeenCalledWith(plainValue, key);
});
});
describe("decryptString", () => {
it("is a proxy to decryptToUtf8", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
const encString = new EncString(EncryptionType.AesCbc256_B64, "data");
encryptService.decryptToUtf8 = jest.fn();
await encryptService.decryptString(encString, key);
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encString, key);
});
});
describe("decryptBytes", () => {
it("is a proxy to decryptToBytes", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
const encString = new EncString(EncryptionType.AesCbc256_B64, "data");
encryptService.decryptToBytes = jest.fn();
await encryptService.decryptBytes(encString, key);
expect(encryptService.decryptToBytes).toHaveBeenCalledWith(encString, key);
});
});
describe("decryptFileData", () => {
it("is a proxy to decrypt", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
const encString = new EncArrayBuffer(makeStaticByteArray(60, EncryptionType.AesCbc256_B64));
encryptService.decryptToBytes = jest.fn();
await encryptService.decryptFileData(encString, key);
expect(encryptService.decryptToBytes).toHaveBeenCalledWith(encString, key);
});
});
describe("unwrapDecapsulationKey", () => {
it("is a proxy to decryptBytes", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
const encString = new EncString(EncryptionType.AesCbc256_B64, "data");
encryptService.decryptBytes = jest.fn();
await encryptService.unwrapDecapsulationKey(encString, key);
expect(encryptService.decryptBytes).toHaveBeenCalledWith(encString, key);
});
});
describe("unwrapEncapsulationKey", () => {
it("is a proxy to decryptBytes", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
const encString = new EncString(EncryptionType.AesCbc256_B64, "data");
encryptService.decryptBytes = jest.fn();
await encryptService.unwrapEncapsulationKey(encString, key);
expect(encryptService.decryptBytes).toHaveBeenCalledWith(encString, key);
});
});
describe("unwrapSymmetricKey", () => {
it("is a proxy to decryptBytes", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
const encString = new EncString(EncryptionType.AesCbc256_B64, "data");
const jestFn = jest.fn();
jestFn.mockResolvedValue(new Uint8Array(64));
encryptService.decryptBytes = jestFn;
await encryptService.unwrapSymmetricKey(encString, key);
expect(encryptService.decryptBytes).toHaveBeenCalledWith(encString, key);
});
});
describe("rsa", () => {
const data = makeStaticByteArray(64, 100);
const testKey = new SymmetricCryptoKey(data);
const encryptedData = makeStaticByteArray(10, 150);
const publicKey = makeStaticByteArray(10, 200);
const privateKey = makeStaticByteArray(10, 250);
const encString = makeEncString(encryptedData);
function makeEncString(data: Uint8Array): EncString {
return new EncString(EncryptionType.Rsa2048_OaepSha1_B64, Utils.fromBufferToB64(data));
}
describe("encapsulateKeyUnsigned", () => {
it("throws if no data is provided", () => {
return expect(encryptService.encapsulateKeyUnsigned(null, publicKey)).rejects.toThrow(
"No sharedKey provided for encapsulation",
);
});
it("throws if no public key is provided", () => {
return expect(encryptService.encapsulateKeyUnsigned(testKey, null)).rejects.toThrow(
"No public key",
);
});
it("encrypts data with provided key", async () => {
cryptoFunctionService.rsaEncrypt.mockResolvedValue(encryptedData);
const actual = await encryptService.encapsulateKeyUnsigned(testKey, publicKey);
expect(cryptoFunctionService.rsaEncrypt).toBeCalledWith(
expect.toEqualBuffer(testKey.toEncoded()),
expect.toEqualBuffer(publicKey),
"sha1",
);
expect(actual).toEqual(encString);
expect(actual.dataBytes).toEqualBuffer(encryptedData);
});
it("throws if no data was provided", () => {
return expect(encryptService.rsaEncrypt(null, new Uint8Array(32))).rejects.toThrow(
"No data provided for encryption",
);
});
});
describe("decapsulateKeyUnsigned", () => {
it("throws if no data is provided", () => {
return expect(encryptService.decapsulateKeyUnsigned(null, privateKey)).rejects.toThrow(
"No data",
);
});
it("throws if no private key is provided", () => {
return expect(encryptService.decapsulateKeyUnsigned(encString, null)).rejects.toThrow(
"No private key",
);
});
it.each([EncryptionType.AesCbc256_B64, EncryptionType.AesCbc256_HmacSha256_B64])(
"throws if encryption type is %s",
async (encType) => {
encString.encryptionType = encType;
await expect(
encryptService.decapsulateKeyUnsigned(encString, privateKey),
).rejects.toThrow("Invalid encryption type");
},
);
it("decrypts data with provided key", async () => {
cryptoFunctionService.rsaDecrypt.mockResolvedValue(data);
const actual = await encryptService.decapsulateKeyUnsigned(makeEncString(data), privateKey);
expect(cryptoFunctionService.rsaDecrypt).toBeCalledWith(
expect.toEqualBuffer(data),
expect.toEqualBuffer(privateKey),
"sha1",
);
expect(actual.toEncoded()).toEqualBuffer(data);
});
});
});
describe("hash", () => {
it("hashes a string and returns b64", async () => {
cryptoFunctionService.hash.mockResolvedValue(Uint8Array.from([1, 2, 3]));
expect(await encryptService.hash("test", "sha256")).toEqual("AQID");
expect(cryptoFunctionService.hash).toHaveBeenCalledWith("test", "sha256");
});
});
describe("decryptItems", () => {
it("returns empty array if no items are provided", async () => {
const key = mock<SymmetricCryptoKey>();
const actual = await encryptService.decryptItems(null, key);
expect(actual).toEqual([]);
});
it("returns items decrypted with provided key", async () => {
const key = mock<SymmetricCryptoKey>();
const decryptable = {
decrypt: jest.fn().mockResolvedValue("decrypted"),
};
const items = [decryptable];
const actual = await encryptService.decryptItems(items as any, key);
expect(actual).toEqual(["decrypted"]);
expect(decryptable.decrypt).toHaveBeenCalledWith(key);
});
});
});

View File

@@ -1,81 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Jsonify } from "type-fest";
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 {
DECRYPT_COMMAND,
SET_CONFIG_COMMAND,
ParsedDecryptCommandData,
} from "../types/worker-command.type";
import { EncryptServiceImplementation } from "./encrypt.service.implementation";
import { WebCryptoFunctionService } from "./web-crypto-function.service";
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);
logService = new ConsoleLogService(false);
encryptService = new EncryptServiceImplementation(cryptoFunctionService, logService, true);
const bitwardenContainerService = new ContainerService(null, encryptService);
bitwardenContainerService.attachToGlobal(self);
inited = true;
}
/**
* Listen for messages and decrypt their contents
*/
workerApi.addEventListener("message", async (event: { data: string }) => {
if (!inited) {
init();
}
const request: {
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<ServerConfig> }).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<Decryptable<any>>(jsonItem.initializerKey);
return initializer(jsonItem);
});
const result = await encryptService.decryptItems(items, key);
workerApi.postMessage({
id: request.id,
items: JSON.stringify(result),
});
}
async function handleSetConfig(newConfig: Jsonify<ServerConfig>) {
encryptService.onServerConfigChange(ServerConfig.fromJSON(newConfig));
}

View File

@@ -1,97 +0,0 @@
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<EncryptService>();
const featureFlagEncryptService = mock<BulkEncryptService>();
const serverConfig = mock<ServerConfig>();
let sut: FallbackBulkEncryptService;
beforeEach(() => {
sut = new FallbackBulkEncryptService(encryptService);
});
afterEach(() => {
jest.resetAllMocks();
});
describe("decryptItems", () => {
const key = mock<SymmetricCryptoKey>();
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);
});
});
});

View File

@@ -1,46 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { BulkEncryptService } from "@bitwarden/common/key-management/crypto/abstractions/bulk-encrypt.service";
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 { EncryptService } from "../abstractions/encrypt.service";
/**
* @deprecated For the feature flag from PM-4154, remove once feature is rolled out
*/
export class FallbackBulkEncryptService implements BulkEncryptService {
private featureFlagEncryptService: BulkEncryptService;
private currentServerConfig: ServerConfig | undefined = undefined;
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) {
if (this.currentServerConfig !== undefined) {
featureFlagEncryptService.onServerConfigChange(this.currentServerConfig);
}
this.featureFlagEncryptService = featureFlagEncryptService;
}
onServerConfigChange(newConfig: ServerConfig): void {
this.currentServerConfig = newConfig;
(this.featureFlagEncryptService ?? this.encryptService).onServerConfigChange(newConfig);
}
}

View File

@@ -1,123 +0,0 @@
import { mock } from "jest-mock-extended";
import * as rxjs from "rxjs";
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 { CryptoFunctionService } from "../abstractions/crypto-function.service";
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<CryptoFunctionService>();
const logService = mock<LogService>();
const serverConfig = mock<ServerConfig>();
let sut: MultithreadEncryptServiceImplementation;
beforeEach(() => {
sut = new MultithreadEncryptServiceImplementation(cryptoFunctionService, logService, true);
});
afterEach(() => {
jest.resetAllMocks();
});
describe("decryptItems", () => {
const key = mock<SymmetricCryptoKey>();
const mockWorker = mock<Worker>();
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<any>[];
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<Decryptable<any>>(), mock<Decryptable<any>>()], 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<Decryptable<any>>(), mock<Decryptable<any>>()], 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<Decryptable<any>>(), mock<Decryptable<any>>()], 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<Worker>();
(sut as any).worker = mockWorker;
sut.onServerConfigChange(serverConfig);
expect(mockWorker.postMessage).toHaveBeenCalledTimes(1);
expect(mockWorker.postMessage).toHaveBeenCalledWith(
buildSetConfigMessage({ newConfig: serverConfig }),
);
});
});
});

View File

@@ -1,114 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { defaultIfEmpty, filter, firstValueFrom, fromEvent, map, Subject, takeUntil } from "rxjs";
import { Jsonify } from "type-fest";
import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface";
import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface";
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
const workerTTL = 3 * 60000; // 3 minutes
/**
* @deprecated Replaced by BulkEncryptionService (PM-4154)
*/
export class MultithreadEncryptServiceImplementation extends EncryptServiceImplementation {
private worker: Worker;
private timeout: any;
private currentServerConfig: ServerConfig | undefined = undefined;
private clear$ = new Subject<void>();
/**
* Sends items to a web worker to decrypt them.
* This utilises multithreading to decrypt items faster without interrupting other operations (e.g. updating UI).
*/
async decryptItems<T extends InitializerMetadata>(
items: Decryptable<T>[],
key: SymmetricCryptoKey,
): Promise<T[]> {
if (items == null || items.length < 1) {
return [];
}
this.logService.info("Starting decryption using multithreading");
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 id = Utils.newGuid();
const request = buildDecryptMessage({
id,
items: items,
key: key,
});
this.worker.postMessage(request);
return await firstValueFrom(
fromEvent(this.worker, "message").pipe(
filter((response: MessageEvent) => response.data?.id === 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([]),
),
);
}
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();
this.worker = null;
this.clearTimeout();
}
private restartTimeout() {
this.clearTimeout();
this.timeout = setTimeout(() => this.clear(), workerTTL);
}
private clearTimeout() {
if (this.timeout != null) {
clearTimeout(this.timeout);
}
}
}

View File

@@ -11,7 +11,6 @@ import { ApiService } from "../../abstractions/api.service";
import { SearchService } from "../../abstractions/search.service";
import { AutofillSettingsService } from "../../autofill/services/autofill-settings.service";
import { DomainSettingsService } from "../../autofill/services/domain-settings.service";
import { BulkEncryptService } from "../../key-management/crypto/abstractions/bulk-encrypt.service";
import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service";
import { UriMatchStrategy } from "../../models/domain/domain-service";
import { ConfigService } from "../../platform/abstractions/config/config.service";
@@ -119,7 +118,6 @@ describe("Cipher Service", () => {
const i18nService = mock<I18nService>();
const searchService = mock<SearchService>();
const encryptService = mock<EncryptService>();
const bulkEncryptService = mock<BulkEncryptService>();
const configService = mock<ConfigService>();
accountService = mockAccountServiceWith(mockUserId);
const logService = mock<LogService>();
@@ -145,7 +143,6 @@ describe("Cipher Service", () => {
stateService,
autofillSettingsService,
encryptService,
bulkEncryptService,
cipherFileUploadService,
configService,
stateProvider,

View File

@@ -97,7 +97,6 @@ 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,
@@ -441,17 +440,10 @@ export class CipherService implements CipherServiceAbstraction {
const allCipherViews = (
await Promise.all(
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,
);
}
return await this.encryptService.decryptItems(
groupedCiphers,
keys.orgKeys[orgId as OrganizationId] ?? keys.userKey,
);
}),
)
)
@@ -593,12 +585,7 @@ export class CipherService implements CipherServiceAbstraction {
const ciphers = response.data.map((cr) => new Cipher(new CipherData(cr)));
const key = await this.keyService.getOrgKey(organizationId);
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);
}
const decCiphers: CipherView[] = await this.encryptService.decryptItems(ciphers, key);
decCiphers.sort(this.getLocaleSortingFunction());
return decCiphers;