1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-15 15:53:27 +00:00

[PM-15994] Move encrypt service to km ownership (#13220)

* Move encrypt service to km ownership

* Update imports for encrypt service abstraction and move bulk encrypt service abstraction

* Fix imports

* Fix further imports

* Fix imports

* Fix worker import
This commit is contained in:
Bernd Schoolmann
2025-02-05 17:39:11 +01:00
committed by GitHub
parent bd6506dcb1
commit 2f8a7a95bd
160 changed files with 240 additions and 232 deletions

View File

@@ -0,0 +1,10 @@
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";
export abstract class BulkEncryptService {
abstract decryptItems<T extends InitializerMetadata>(
items: Decryptable<T>[],
key: SymmetricCryptoKey,
): Promise<T[]>;
}

View File

@@ -0,0 +1,58 @@
import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface";
import { Encrypted } from "@bitwarden/common/platform/interfaces/encrypted";
import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface";
import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
export abstract class EncryptService {
abstract encrypt(plainValue: string | Uint8Array, key: SymmetricCryptoKey): Promise<EncString>;
abstract encryptToBytes(plainValue: Uint8Array, key: SymmetricCryptoKey): Promise<EncArrayBuffer>;
/**
* Decrypts an EncString to a string
* @param encString - The EncString to decrypt
* @param key - The key to decrypt the EncString with
* @param decryptTrace - A string to identify the context of the object being decrypted. This can include: field name, encryption type, cipher id, key type, but should not include
* sensitive information like encryption keys or data. This is used for logging when decryption errors occur in order to identify what failed to decrypt
* @returns The decrypted string
*/
abstract decryptToUtf8(
encString: EncString,
key: SymmetricCryptoKey,
decryptTrace?: string,
): Promise<string>;
/**
* Decrypts an Encrypted object to a Uint8Array
* @param encThing - The Encrypted object to decrypt
* @param key - The key to decrypt the Encrypted object with
* @param decryptTrace - A string to identify the context of the object being decrypted. This can include: field name, encryption type, cipher id, key type, but should not include
* sensitive information like encryption keys or data. This is used for logging when decryption errors occur in order to identify what failed to decrypt
* @returns The decrypted Uint8Array
*/
abstract decryptToBytes(
encThing: Encrypted,
key: SymmetricCryptoKey,
decryptTrace?: string,
): Promise<Uint8Array>;
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,
): Promise<T[]>;
/**
* Generates a base64-encoded hash of the given value
* @param value The value to hash
* @param algorithm The hashing algorithm to use
*/
abstract hash(
value: string | Uint8Array,
algorithm: "sha1" | "sha256" | "sha512",
): Promise<string>;
}

View File

@@ -0,0 +1,165 @@
// 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/platform/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";
// 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/key-management/crypto/services/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);
}
}
}

View File

@@ -0,0 +1,316 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import {
EncryptionType,
encryptionTypeToString as encryptionTypeName,
} from "@bitwarden/common/platform/enums";
import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface";
import { Encrypted } from "@bitwarden/common/platform/interfaces/encrypted";
import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface";
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 { EncryptedObject } from "@bitwarden/common/platform/models/domain/encrypted-object";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { EncryptService } from "../abstractions/encrypt.service";
export class EncryptServiceImplementation implements EncryptService {
constructor(
protected cryptoFunctionService: CryptoFunctionService,
protected logService: LogService,
protected logMacFailures: boolean,
) {}
async encrypt(plainValue: string | Uint8Array, key: SymmetricCryptoKey): Promise<EncString> {
if (key == null) {
throw new Error("No encryption key provided.");
}
if (plainValue == null) {
return Promise.resolve(null);
}
let plainBuf: Uint8Array;
if (typeof plainValue === "string") {
plainBuf = Utils.fromUtf8ToArray(plainValue);
} else {
plainBuf = plainValue;
}
const encObj = await this.aesEncrypt(plainBuf, key);
const iv = Utils.fromBufferToB64(encObj.iv);
const data = Utils.fromBufferToB64(encObj.data);
const mac = encObj.mac != null ? Utils.fromBufferToB64(encObj.mac) : null;
return new EncString(encObj.key.encType, data, iv, mac);
}
async encryptToBytes(plainValue: Uint8Array, key: SymmetricCryptoKey): Promise<EncArrayBuffer> {
if (key == null) {
throw new Error("No encryption key provided.");
}
const encValue = await this.aesEncrypt(plainValue, key);
let macLen = 0;
if (encValue.mac != null) {
macLen = encValue.mac.byteLength;
}
const encBytes = new Uint8Array(1 + encValue.iv.byteLength + macLen + encValue.data.byteLength);
encBytes.set([encValue.key.encType]);
encBytes.set(new Uint8Array(encValue.iv), 1);
if (encValue.mac != null) {
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);
}
async decryptToUtf8(
encString: EncString,
key: SymmetricCryptoKey,
decryptContext: string = "no context",
): Promise<string> {
if (key == null) {
throw new Error("No key provided for decryption.");
}
key = this.resolveLegacyKey(key, encString);
// DO NOT REMOVE OR MOVE. This prevents downgrade to mac-less CBC, which would compromise integrity and confidentiality.
if (key.macKey != null && encString?.mac == null) {
this.logService.error(
"[Encrypt service] Key has mac key but payload is missing mac bytes. Key type " +
encryptionTypeName(key.encType) +
"Payload type " +
encryptionTypeName(encString.encryptionType),
"Decrypt context: " + decryptContext,
);
return null;
}
if (key.encType !== encString.encryptionType) {
this.logService.error(
"[Encrypt service] Key encryption type does not match payload encryption type. Key type " +
encryptionTypeName(key.encType) +
"Payload type " +
encryptionTypeName(encString.encryptionType),
"Decrypt context: " + decryptContext,
);
return null;
}
const fastParams = this.cryptoFunctionService.aesDecryptFastParameters(
encString.data,
encString.iv,
encString.mac,
key,
);
if (fastParams.macKey != null && fastParams.mac != null) {
const computedMac = await this.cryptoFunctionService.hmacFast(
fastParams.macData,
fastParams.macKey,
"sha256",
);
const macsEqual = await this.cryptoFunctionService.compareFast(fastParams.mac, computedMac);
if (!macsEqual) {
this.logMacFailed(
"[Encrypt service] decryptToUtf8 MAC comparison failed. Key or payload has changed. Key type " +
encryptionTypeName(key.encType) +
"Payload type " +
encryptionTypeName(encString.encryptionType) +
" Decrypt context: " +
decryptContext,
);
return null;
}
}
return await this.cryptoFunctionService.aesDecryptFast({ mode: "cbc", parameters: fastParams });
}
async decryptToBytes(
encThing: Encrypted,
key: SymmetricCryptoKey,
decryptContext: string = "no context",
): Promise<Uint8Array> {
if (key == null) {
throw new Error("No encryption key provided.");
}
if (encThing == null) {
throw new Error("Nothing provided for decryption.");
}
key = this.resolveLegacyKey(key, encThing);
// DO NOT REMOVE OR MOVE. This prevents downgrade to mac-less CBC, which would compromise integrity and confidentiality.
if (key.macKey != null && encThing.macBytes == null) {
this.logService.error(
"[Encrypt service] Key has mac key but payload is missing mac bytes. Key type " +
encryptionTypeName(key.encType) +
" Payload type " +
encryptionTypeName(encThing.encryptionType) +
" Decrypt context: " +
decryptContext,
);
return null;
}
if (key.encType !== encThing.encryptionType) {
this.logService.error(
"[Encrypt service] Key encryption type does not match payload encryption type. Key type " +
encryptionTypeName(key.encType) +
" Payload type " +
encryptionTypeName(encThing.encryptionType) +
" Decrypt context: " +
decryptContext,
);
return null;
}
if (key.macKey != null && encThing.macBytes != 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, key.macKey, "sha256");
if (computedMac === null) {
this.logMacFailed(
"[Encrypt service#decryptToBytes] Failed to compute MAC." +
" Key type " +
encryptionTypeName(key.encType) +
" Payload type " +
encryptionTypeName(encThing.encryptionType) +
" Decrypt context: " +
decryptContext,
);
return null;
}
const macsMatch = await this.cryptoFunctionService.compare(encThing.macBytes, computedMac);
if (!macsMatch) {
this.logMacFailed(
"[Encrypt service#decryptToBytes]: MAC comparison failed. Key or payload has changed." +
" Key type " +
encryptionTypeName(key.encType) +
" Payload type " +
encryptionTypeName(encThing.encryptionType) +
" Decrypt context: " +
decryptContext,
);
return null;
}
}
const result = await this.cryptoFunctionService.aesDecrypt(
encThing.dataBytes,
encThing.ivBytes,
key.encKey,
"cbc",
);
return result ?? null;
}
async rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise<EncString> {
if (data == null) {
throw new Error("No data provided for encryption.");
}
if (publicKey == null) {
throw new Error("No public key provided for encryption.");
}
const encrypted = await this.cryptoFunctionService.rsaEncrypt(data, publicKey, "sha1");
return new EncString(EncryptionType.Rsa2048_OaepSha1_B64, Utils.fromBufferToB64(encrypted));
}
async rsaDecrypt(data: EncString, privateKey: Uint8Array): Promise<Uint8Array> {
if (data == null) {
throw new Error("[Encrypt service] rsaDecrypt: No data provided for decryption.");
}
let algorithm: "sha1" | "sha256";
switch (data.encryptionType) {
case EncryptionType.Rsa2048_OaepSha1_B64:
case EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64:
algorithm = "sha1";
break;
case EncryptionType.Rsa2048_OaepSha256_B64:
case EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64:
algorithm = "sha256";
break;
default:
throw new Error("Invalid encryption type.");
}
if (privateKey == null) {
throw new Error("[Encrypt service] rsaDecrypt: No private key provided for decryption.");
}
return this.cryptoFunctionService.rsaDecrypt(data.dataBytes, privateKey, algorithm);
}
/**
* @deprecated Replaced by BulkEncryptService (PM-4154)
*/
async decryptItems<T extends InitializerMetadata>(
items: Decryptable<T>[],
key: SymmetricCryptoKey,
): Promise<T[]> {
if (items == null || items.length < 1) {
return [];
}
// don't use promise.all because this task is not io bound
const results = [];
for (let i = 0; i < items.length; i++) {
results.push(await items[i].decrypt(key));
}
return results;
}
async hash(value: string | Uint8Array, algorithm: "sha1" | "sha256" | "sha512"): Promise<string> {
const hashArray = await this.cryptoFunctionService.hash(value, algorithm);
return Utils.fromBufferToB64(hashArray);
}
private async aesEncrypt(data: Uint8Array, key: SymmetricCryptoKey): Promise<EncryptedObject> {
const obj = new EncryptedObject();
obj.key = key;
obj.iv = await this.cryptoFunctionService.randomBytes(16);
obj.data = await this.cryptoFunctionService.aesEncrypt(data, obj.iv, obj.key.encKey);
if (obj.key.macKey != null) {
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, obj.key.macKey, "sha256");
}
return obj;
}
private logMacFailed(msg: string) {
if (this.logMacFailures) {
this.logService.error(msg);
}
}
/**
* Transform into new key for the old encrypt-then-mac scheme if required, otherwise return the current key unchanged
* @param encThing The encrypted object (e.g. encString or encArrayBuffer) that you want to decrypt
*/
resolveLegacyKey(key: SymmetricCryptoKey, encThing: Encrypted): SymmetricCryptoKey {
if (
encThing.encryptionType === EncryptionType.AesCbc128_HmacSha256_B64 &&
key.encType === EncryptionType.AesCbc256_B64
) {
return new SymmetricCryptoKey(key.key, EncryptionType.AesCbc128_HmacSha256_B64);
}
return key;
}
}

View File

@@ -0,0 +1,265 @@
import { mockReset, mock } from "jest-mock-extended";
import { CryptoFunctionService } from "@bitwarden/common/platform/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 { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { CsprngArray } from "@bitwarden/common/types/csprng";
import { makeStaticByteArray } from "../../../../spec";
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("encryptToBytes", () => {
const plainValue = makeStaticByteArray(16, 1);
const iv = makeStaticByteArray(16, 30);
const mac = makeStaticByteArray(32, 40);
const encryptedData = makeStaticByteArray(20, 50);
it("throws if no key is provided", () => {
return expect(encryptService.encryptToBytes(plainValue, null)).rejects.toThrow(
"No encryption key",
);
});
describe("encrypts data", () => {
beforeEach(() => {
cryptoFunctionService.randomBytes.calledWith(16).mockResolvedValueOnce(iv as CsprngArray);
cryptoFunctionService.aesEncrypt.mockResolvedValue(encryptedData);
});
it("using a key which supports mac", async () => {
const key = mock<SymmetricCryptoKey>();
const encType = EncryptionType.AesCbc128_HmacSha256_B64;
key.encType = encType;
key.macKey = makeStaticByteArray(16, 20);
cryptoFunctionService.hmac.mockResolvedValue(mac);
const actual = await encryptService.encryptToBytes(plainValue, key);
expect(actual.encryptionType).toEqual(encType);
expect(actual.ivBytes).toEqualBuffer(iv);
expect(actual.macBytes).toEqualBuffer(mac);
expect(actual.dataBytes).toEqualBuffer(encryptedData);
expect(actual.buffer.byteLength).toEqual(
1 + iv.byteLength + mac.byteLength + encryptedData.byteLength,
);
});
it("using a key which doesn't support mac", async () => {
const key = mock<SymmetricCryptoKey>();
const encType = EncryptionType.AesCbc256_B64;
key.encType = encType;
key.macKey = null;
const actual = await encryptService.encryptToBytes(plainValue, key);
expect(cryptoFunctionService.hmac).not.toBeCalled();
expect(actual.encryptionType).toEqual(encType);
expect(actual.ivBytes).toEqualBuffer(iv);
expect(actual.macBytes).toBeNull();
expect(actual.dataBytes).toEqualBuffer(encryptedData);
expect(actual.buffer.byteLength).toEqual(1 + iv.byteLength + encryptedData.byteLength);
});
});
});
describe("decryptToBytes", () => {
const encType = EncryptionType.AesCbc256_HmacSha256_B64;
const key = new SymmetricCryptoKey(makeStaticByteArray(64, 100), encType);
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", 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.encKey),
"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.macKey,
"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 encTypes don't match", async () => {
key.encType = EncryptionType.AesCbc256_B64;
cryptoFunctionService.compare.mockResolvedValue(true);
const actual = await encryptService.decryptToBytes(encBuffer, key);
expect(actual).toBeNull();
expect(cryptoFunctionService.aesDecrypt).not.toHaveBeenCalled();
});
});
describe("rsa", () => {
const data = makeStaticByteArray(10, 100);
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("rsaEncrypt", () => {
it("throws if no data is provided", () => {
return expect(encryptService.rsaEncrypt(null, publicKey)).rejects.toThrow("No data");
});
it("throws if no public key is provided", () => {
return expect(encryptService.rsaEncrypt(data, null)).rejects.toThrow("No public key");
});
it("encrypts data with provided key", async () => {
cryptoFunctionService.rsaEncrypt.mockResolvedValue(encryptedData);
const actual = await encryptService.rsaEncrypt(data, publicKey);
expect(cryptoFunctionService.rsaEncrypt).toBeCalledWith(
expect.toEqualBuffer(data),
expect.toEqualBuffer(publicKey),
"sha1",
);
expect(actual).toEqual(encString);
expect(actual.dataBytes).toEqualBuffer(encryptedData);
});
});
describe("rsaDecrypt", () => {
it("throws if no data is provided", () => {
return expect(encryptService.rsaDecrypt(null, privateKey)).rejects.toThrow("No data");
});
it("throws if no private key is provided", () => {
return expect(encryptService.rsaDecrypt(encString, null)).rejects.toThrow("No private key");
});
it.each([
EncryptionType.AesCbc256_B64,
EncryptionType.AesCbc128_HmacSha256_B64,
EncryptionType.AesCbc256_HmacSha256_B64,
])("throws if encryption type is %s", async (encType) => {
encString.encryptionType = encType;
await expect(encryptService.rsaDecrypt(encString, privateKey)).rejects.toThrow(
"Invalid encryption type",
);
});
it("decrypts data with provided key", async () => {
cryptoFunctionService.rsaDecrypt.mockResolvedValue(data);
const actual = await encryptService.rsaDecrypt(makeEncString(data), privateKey);
expect(cryptoFunctionService.rsaDecrypt).toBeCalledWith(
expect.toEqualBuffer(data),
expect.toEqualBuffer(privateKey),
"sha1",
);
expect(actual).toEqualBuffer(data);
});
});
});
describe("resolveLegacyKey", () => {
it("creates a legacy key if required", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(32), EncryptionType.AesCbc256_B64);
const encString = mock<EncString>();
encString.encryptionType = EncryptionType.AesCbc128_HmacSha256_B64;
const actual = encryptService.resolveLegacyKey(key, encString);
const expected = new SymmetricCryptoKey(key.key, EncryptionType.AesCbc128_HmacSha256_B64);
expect(actual).toEqual(expected);
});
it("does not create a legacy key if not required", async () => {
const encType = EncryptionType.AesCbc256_HmacSha256_B64;
const key = new SymmetricCryptoKey(makeStaticByteArray(64), encType);
const encString = mock<EncString>();
encString.encryptionType = encType;
const actual = encryptService.resolveLegacyKey(key, encString);
expect(actual).toEqual(key);
});
});
});

View File

@@ -0,0 +1,58 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Jsonify } from "type-fest";
import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
import { ContainerService } from "@bitwarden/common/platform/services/container.service";
import { getClassInitializer } from "@bitwarden/common/platform/services/cryptography/get-class-initializer";
import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service";
import { EncryptServiceImplementation } from "./encrypt.service.implementation";
const workerApi: Worker = self as any;
let inited = false;
let encryptService: EncryptServiceImplementation;
/**
* Bootstrap the worker environment with services required for decryption
*/
export function init() {
const cryptoFunctionService = new WebCryptoFunctionService(self);
const 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: {
id: string;
items: Jsonify<Decryptable<any>>[];
key: Jsonify<SymmetricCryptoKey>;
} = JSON.parse(event.data);
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),
});
});

View File

@@ -0,0 +1,36 @@
// 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 { 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;
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;
}
}

View File

@@ -0,0 +1,91 @@
// 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 { 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 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");
this.worker ??= new Worker(
new URL(
/* webpackChunkName: 'encrypt-worker' */
"@bitwarden/common/key-management/crypto/services/encrypt.worker.ts",
import.meta.url,
),
);
this.restartTimeout();
const request = {
id: Utils.newGuid(),
items: items,
key: key,
};
this.worker.postMessage(JSON.stringify(request));
return await firstValueFrom(
fromEvent(this.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([]),
),
);
}
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);
}
}
}