mirror of
https://github.com/bitwarden/browser
synced 2025-12-10 21:33:27 +00:00
[PM-21378] Switch encrypt service to use SDK functions (#14538)
* Add new encrypt service functions * Undo changes * Cleanup * Fix build * Fix comments * Switch encrypt service to use SDK functions * Move remaining functions to PureCrypto * Tests * Increase test coverage * Enforce sdk.ready and drop unused codepaths * Delete unused code * Add forgotten sdk init logic * Fix build error * Fix browser extension failing to unlock after process reload due to outdated usage of decryptString * Fix send encryption * Fix client key half decryption being stuck * Attempt to fix sharereplay * Fix build * Fix type / add filter / add distinctuntilchange * Fix capitalization
This commit is contained in:
@@ -118,14 +118,14 @@ export class LocalBackedSessionStorageService
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const valueJson = await this.encryptService.decryptString(new EncString(local), encKey);
|
try {
|
||||||
if (valueJson == null) {
|
const valueJson = await this.encryptService.decryptString(new EncString(local), encKey);
|
||||||
|
return JSON.parse(valueJson);
|
||||||
|
} catch {
|
||||||
// error with decryption, value is lost, delete state and start over
|
// error with decryption, value is lost, delete state and start over
|
||||||
await this.localStorage.remove(this.sessionStorageKey(key));
|
await this.localStorage.remove(this.sessionStorageKey(key));
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return JSON.parse(valueJson);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateLocalSessionValue(key: string, value: unknown): Promise<void> {
|
private async updateLocalSessionValue(key: string, value: unknown): Promise<void> {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { AccountServiceImplementation } from "@bitwarden/common/auth/services/ac
|
|||||||
import { ClientType } from "@bitwarden/common/enums";
|
import { ClientType } from "@bitwarden/common/enums";
|
||||||
import { EncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/encrypt.service.implementation";
|
import { EncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/encrypt.service.implementation";
|
||||||
import { RegionConfig } from "@bitwarden/common/platform/abstractions/environment.service";
|
import { RegionConfig } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
|
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
|
||||||
import { Message, MessageSender } from "@bitwarden/common/platform/messaging";
|
import { Message, MessageSender } from "@bitwarden/common/platform/messaging";
|
||||||
// eslint-disable-next-line no-restricted-imports -- For dependency creation
|
// eslint-disable-next-line no-restricted-imports -- For dependency creation
|
||||||
import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal";
|
import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal";
|
||||||
@@ -58,6 +59,7 @@ import { EphemeralValueStorageService } from "./platform/services/ephemeral-valu
|
|||||||
import { I18nMainService } from "./platform/services/i18n.main.service";
|
import { I18nMainService } from "./platform/services/i18n.main.service";
|
||||||
import { SSOLocalhostCallbackService } from "./platform/services/sso-localhost-callback.service";
|
import { SSOLocalhostCallbackService } from "./platform/services/sso-localhost-callback.service";
|
||||||
import { ElectronMainMessagingService } from "./services/electron-main-messaging.service";
|
import { ElectronMainMessagingService } from "./services/electron-main-messaging.service";
|
||||||
|
import { MainSdkLoadService } from "./services/main-sdk-load-service";
|
||||||
import { isMacAppStore } from "./utils";
|
import { isMacAppStore } from "./utils";
|
||||||
|
|
||||||
export class Main {
|
export class Main {
|
||||||
@@ -88,6 +90,7 @@ export class Main {
|
|||||||
desktopAutofillSettingsService: DesktopAutofillSettingsService;
|
desktopAutofillSettingsService: DesktopAutofillSettingsService;
|
||||||
versionMain: VersionMain;
|
versionMain: VersionMain;
|
||||||
sshAgentService: MainSshAgentService;
|
sshAgentService: MainSshAgentService;
|
||||||
|
sdkLoadService: SdkLoadService;
|
||||||
mainDesktopAutotypeService: MainDesktopAutotypeService;
|
mainDesktopAutotypeService: MainDesktopAutotypeService;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -144,6 +147,8 @@ export class Main {
|
|||||||
|
|
||||||
this.i18nService = new I18nMainService("en", "./locales/", globalStateProvider);
|
this.i18nService = new I18nMainService("en", "./locales/", globalStateProvider);
|
||||||
|
|
||||||
|
this.sdkLoadService = new MainSdkLoadService();
|
||||||
|
|
||||||
this.mainCryptoFunctionService = new NodeCryptoFunctionService();
|
this.mainCryptoFunctionService = new NodeCryptoFunctionService();
|
||||||
|
|
||||||
const stateEventRegistrarService = new StateEventRegistrarService(
|
const stateEventRegistrarService = new StateEventRegistrarService(
|
||||||
@@ -386,6 +391,8 @@ export class Main {
|
|||||||
this.windowMain.win.on("minimize", () => {
|
this.windowMain.win.on("minimize", () => {
|
||||||
this.messagingService.send("windowHidden");
|
this.messagingService.send("windowHidden");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await this.sdkLoadService.loadAndInit();
|
||||||
},
|
},
|
||||||
(e: any) => {
|
(e: any) => {
|
||||||
this.logService.error("Error while running migrations:", e);
|
this.logService.error("Error while running migrations:", e);
|
||||||
|
|||||||
9
apps/desktop/src/services/main-sdk-load-service.ts
Normal file
9
apps/desktop/src/services/main-sdk-load-service.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
|
||||||
|
import * as sdk from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
|
export class MainSdkLoadService extends SdkLoadService {
|
||||||
|
async load(): Promise<void> {
|
||||||
|
const module = await import("@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm");
|
||||||
|
(sdk as any).init(module);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -65,6 +65,9 @@ const main = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
experiments: {
|
||||||
|
asyncWebAssembly: true,
|
||||||
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new CopyWebpackPlugin({
|
new CopyWebpackPlugin({
|
||||||
patterns: [
|
patterns: [
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ export abstract class CryptoFunctionService {
|
|||||||
algorithm: "sha1" | "sha256" | "sha512",
|
algorithm: "sha1" | "sha256" | "sha512",
|
||||||
): Promise<Uint8Array | string>;
|
): Promise<Uint8Array | string>;
|
||||||
abstract compareFast(a: Uint8Array | string, b: Uint8Array | string): Promise<boolean>;
|
abstract compareFast(a: Uint8Array | string, b: Uint8Array | string): Promise<boolean>;
|
||||||
abstract aesEncrypt(data: Uint8Array, iv: Uint8Array, key: Uint8Array): Promise<Uint8Array>;
|
|
||||||
abstract aesDecryptFastParameters(
|
abstract aesDecryptFastParameters(
|
||||||
data: string,
|
data: string,
|
||||||
iv: string,
|
iv: string,
|
||||||
|
|||||||
@@ -7,20 +7,6 @@ import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-cr
|
|||||||
import { EncString } from "../models/enc-string";
|
import { EncString } from "../models/enc-string";
|
||||||
|
|
||||||
export abstract class EncryptService {
|
export abstract class EncryptService {
|
||||||
/**
|
|
||||||
* @deprecated
|
|
||||||
* Encrypts a string or Uint8Array to an EncString
|
|
||||||
* @param plainValue - The value to encrypt
|
|
||||||
* @param key - The key to encrypt the value with
|
|
||||||
*/
|
|
||||||
abstract encrypt(plainValue: string | Uint8Array, key: SymmetricCryptoKey): Promise<EncString>;
|
|
||||||
/**
|
|
||||||
* @deprecated
|
|
||||||
* Encrypts a value to a Uint8Array
|
|
||||||
* @param plainValue - The value to encrypt
|
|
||||||
* @param key - The key to encrypt the value with
|
|
||||||
*/
|
|
||||||
abstract encryptToBytes(plainValue: Uint8Array, key: SymmetricCryptoKey): Promise<EncArrayBuffer>;
|
|
||||||
/**
|
/**
|
||||||
* @deprecated
|
* @deprecated
|
||||||
* Decrypts an EncString to a string
|
* Decrypts an EncString to a string
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -1,38 +1,19 @@
|
|||||||
// 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 { BulkEncryptService } from "@bitwarden/common/key-management/crypto/abstractions/bulk-encrypt.service";
|
||||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface";
|
import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface";
|
||||||
import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.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 { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
import { getClassInitializer } from "@bitwarden/common/platform/services/cryptography/get-class-initializer";
|
|
||||||
|
|
||||||
import {
|
import { DefaultFeatureFlagValue, FeatureFlag } from "../../../enums/feature-flag.enum";
|
||||||
DefaultFeatureFlagValue,
|
|
||||||
FeatureFlag,
|
|
||||||
getFeatureFlagValue,
|
|
||||||
} from "../../../enums/feature-flag.enum";
|
|
||||||
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
|
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;
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Will be deleted in an immediate subsequent PR
|
||||||
|
*/
|
||||||
export class BulkEncryptServiceImplementation implements BulkEncryptService {
|
export class BulkEncryptServiceImplementation implements BulkEncryptService {
|
||||||
private workers: Worker[] = [];
|
|
||||||
private timeout: any;
|
|
||||||
private currentServerConfig: ServerConfig | undefined = undefined;
|
|
||||||
protected useSDKForDecryption: boolean = DefaultFeatureFlagValue[FeatureFlag.UseSDKForDecryption];
|
protected useSDKForDecryption: boolean = DefaultFeatureFlagValue[FeatureFlag.UseSDKForDecryption];
|
||||||
|
|
||||||
private clear$ = new Subject<void>();
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected cryptoFunctionService: CryptoFunctionService,
|
protected cryptoFunctionService: CryptoFunctionService,
|
||||||
protected logService: LogService,
|
protected logService: LogService,
|
||||||
@@ -54,139 +35,12 @@ export class BulkEncryptServiceImplementation implements BulkEncryptService {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof window === "undefined" || this.useSDKForDecryption) {
|
|
||||||
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.useSDKForDecryption = getFeatureFlagValue(newConfig, FeatureFlag.UseSDKForDecryption);
|
|
||||||
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 = [];
|
const results = [];
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
for (const [i, worker] of this.workers.entries()) {
|
results.push(await items[i].decrypt(key));
|
||||||
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([]),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
return results;
|
||||||
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) {
|
onServerConfigChange(newConfig: ServerConfig): void {}
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,36 +3,20 @@
|
|||||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import {
|
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
|
||||||
EncryptionType,
|
import { EncryptionType } from "@bitwarden/common/platform/enums";
|
||||||
encryptionTypeToString as encryptionTypeName,
|
|
||||||
} from "@bitwarden/common/platform/enums";
|
|
||||||
import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface";
|
import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface";
|
||||||
import { Encrypted } from "@bitwarden/common/platform/interfaces/encrypted";
|
import { Encrypted } from "@bitwarden/common/platform/interfaces/encrypted";
|
||||||
import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface";
|
import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer";
|
import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer";
|
||||||
import { EncryptedObject } from "@bitwarden/common/platform/models/domain/encrypted-object";
|
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
import {
|
|
||||||
Aes256CbcHmacKey,
|
|
||||||
Aes256CbcKey,
|
|
||||||
SymmetricCryptoKey,
|
|
||||||
} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
|
||||||
import { PureCrypto } from "@bitwarden/sdk-internal";
|
import { PureCrypto } from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
import {
|
|
||||||
DefaultFeatureFlagValue,
|
|
||||||
FeatureFlag,
|
|
||||||
getFeatureFlagValue,
|
|
||||||
} from "../../../enums/feature-flag.enum";
|
|
||||||
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
|
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
|
||||||
import { SdkLoadService } from "../../../platform/abstractions/sdk/sdk-load.service";
|
|
||||||
import { EncryptService } from "../abstractions/encrypt.service";
|
import { EncryptService } from "../abstractions/encrypt.service";
|
||||||
|
|
||||||
export class EncryptServiceImplementation implements EncryptService {
|
export class EncryptServiceImplementation implements EncryptService {
|
||||||
protected useSDKForDecryption: boolean = DefaultFeatureFlagValue[FeatureFlag.UseSDKForDecryption];
|
|
||||||
private blockType0: boolean = DefaultFeatureFlagValue[FeatureFlag.PM17987_BlockType0];
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected cryptoFunctionService: CryptoFunctionService,
|
protected cryptoFunctionService: CryptoFunctionService,
|
||||||
protected logService: LogService,
|
protected logService: LogService,
|
||||||
@@ -41,27 +25,40 @@ export class EncryptServiceImplementation implements EncryptService {
|
|||||||
|
|
||||||
// Proxy functions; Their implementation are temporary before moving at this level to the SDK
|
// Proxy functions; Their implementation are temporary before moving at this level to the SDK
|
||||||
async encryptString(plainValue: string, key: SymmetricCryptoKey): Promise<EncString> {
|
async encryptString(plainValue: string, key: SymmetricCryptoKey): Promise<EncString> {
|
||||||
return this.encrypt(plainValue, key);
|
if (plainValue == null) {
|
||||||
|
this.logService.warning(
|
||||||
|
"[EncryptService] WARNING: encryptString called with null value. Returning null, but this behavior is deprecated and will be removed.",
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await SdkLoadService.Ready;
|
||||||
|
return new EncString(PureCrypto.symmetric_encrypt_string(plainValue, key.toEncoded()));
|
||||||
}
|
}
|
||||||
|
|
||||||
async encryptBytes(plainValue: Uint8Array, key: SymmetricCryptoKey): Promise<EncString> {
|
async encryptBytes(plainValue: Uint8Array, key: SymmetricCryptoKey): Promise<EncString> {
|
||||||
return this.encrypt(plainValue, key);
|
await SdkLoadService.Ready;
|
||||||
|
return new EncString(PureCrypto.symmetric_encrypt_bytes(plainValue, key.toEncoded()));
|
||||||
}
|
}
|
||||||
|
|
||||||
async encryptFileData(plainValue: Uint8Array, key: SymmetricCryptoKey): Promise<EncArrayBuffer> {
|
async encryptFileData(plainValue: Uint8Array, key: SymmetricCryptoKey): Promise<EncArrayBuffer> {
|
||||||
return this.encryptToBytes(plainValue, key);
|
await SdkLoadService.Ready;
|
||||||
|
return new EncArrayBuffer(PureCrypto.symmetric_encrypt_filedata(plainValue, key.toEncoded()));
|
||||||
}
|
}
|
||||||
|
|
||||||
async decryptString(encString: EncString, key: SymmetricCryptoKey): Promise<string> {
|
async decryptString(encString: EncString, key: SymmetricCryptoKey): Promise<string> {
|
||||||
return this.decryptToUtf8(encString, key);
|
await SdkLoadService.Ready;
|
||||||
|
return PureCrypto.symmetric_decrypt_string(encString.encryptedString, key.toEncoded());
|
||||||
}
|
}
|
||||||
|
|
||||||
async decryptBytes(encString: EncString, key: SymmetricCryptoKey): Promise<Uint8Array> {
|
async decryptBytes(encString: EncString, key: SymmetricCryptoKey): Promise<Uint8Array> {
|
||||||
return this.decryptToBytes(encString, key);
|
await SdkLoadService.Ready;
|
||||||
|
return PureCrypto.symmetric_decrypt_bytes(encString.encryptedString, key.toEncoded());
|
||||||
}
|
}
|
||||||
|
|
||||||
async decryptFileData(encBuffer: EncArrayBuffer, key: SymmetricCryptoKey): Promise<Uint8Array> {
|
async decryptFileData(encBuffer: EncArrayBuffer, key: SymmetricCryptoKey): Promise<Uint8Array> {
|
||||||
return this.decryptToBytes(encBuffer, key);
|
await SdkLoadService.Ready;
|
||||||
|
return PureCrypto.symmetric_decrypt_filedata(encBuffer.buffer, key.toEncoded());
|
||||||
}
|
}
|
||||||
|
|
||||||
async wrapDecapsulationKey(
|
async wrapDecapsulationKey(
|
||||||
@@ -76,7 +73,10 @@ export class EncryptServiceImplementation implements EncryptService {
|
|||||||
throw new Error("No wrappingKey provided for wrapping.");
|
throw new Error("No wrappingKey provided for wrapping.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.encryptUint8Array(decapsulationKeyPkcs8, wrappingKey);
|
await SdkLoadService.Ready;
|
||||||
|
return new EncString(
|
||||||
|
PureCrypto.wrap_decapsulation_key(decapsulationKeyPkcs8, wrappingKey.toEncoded()),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async wrapEncapsulationKey(
|
async wrapEncapsulationKey(
|
||||||
@@ -91,7 +91,10 @@ export class EncryptServiceImplementation implements EncryptService {
|
|||||||
throw new Error("No wrappingKey provided for wrapping.");
|
throw new Error("No wrappingKey provided for wrapping.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.encryptUint8Array(encapsulationKeySpki, wrappingKey);
|
await SdkLoadService.Ready;
|
||||||
|
return new EncString(
|
||||||
|
PureCrypto.wrap_encapsulation_key(encapsulationKeySpki, wrappingKey.toEncoded()),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async wrapSymmetricKey(
|
async wrapSymmetricKey(
|
||||||
@@ -106,26 +109,61 @@ export class EncryptServiceImplementation implements EncryptService {
|
|||||||
throw new Error("No wrappingKey provided for wrapping.");
|
throw new Error("No wrappingKey provided for wrapping.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.encryptUint8Array(keyToBeWrapped.toEncoded(), wrappingKey);
|
await SdkLoadService.Ready;
|
||||||
|
return new EncString(
|
||||||
|
PureCrypto.wrap_symmetric_key(keyToBeWrapped.toEncoded(), wrappingKey.toEncoded()),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async unwrapDecapsulationKey(
|
async unwrapDecapsulationKey(
|
||||||
wrappedDecapsulationKey: EncString,
|
wrappedDecapsulationKey: EncString,
|
||||||
wrappingKey: SymmetricCryptoKey,
|
wrappingKey: SymmetricCryptoKey,
|
||||||
): Promise<Uint8Array> {
|
): Promise<Uint8Array> {
|
||||||
return this.decryptBytes(wrappedDecapsulationKey, wrappingKey);
|
if (wrappedDecapsulationKey == null) {
|
||||||
|
throw new Error("No wrappedDecapsulationKey provided for unwrapping.");
|
||||||
|
}
|
||||||
|
if (wrappingKey == null) {
|
||||||
|
throw new Error("No wrappingKey provided for unwrapping.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await SdkLoadService.Ready;
|
||||||
|
return PureCrypto.unwrap_decapsulation_key(
|
||||||
|
wrappedDecapsulationKey.encryptedString,
|
||||||
|
wrappingKey.toEncoded(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
async unwrapEncapsulationKey(
|
async unwrapEncapsulationKey(
|
||||||
wrappedEncapsulationKey: EncString,
|
wrappedEncapsulationKey: EncString,
|
||||||
wrappingKey: SymmetricCryptoKey,
|
wrappingKey: SymmetricCryptoKey,
|
||||||
): Promise<Uint8Array> {
|
): Promise<Uint8Array> {
|
||||||
return this.decryptBytes(wrappedEncapsulationKey, wrappingKey);
|
if (wrappedEncapsulationKey == null) {
|
||||||
|
throw new Error("No wrappedEncapsulationKey provided for unwrapping.");
|
||||||
|
}
|
||||||
|
if (wrappingKey == null) {
|
||||||
|
throw new Error("No wrappingKey provided for unwrapping.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await SdkLoadService.Ready;
|
||||||
|
return PureCrypto.unwrap_encapsulation_key(
|
||||||
|
wrappedEncapsulationKey.encryptedString,
|
||||||
|
wrappingKey.toEncoded(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
async unwrapSymmetricKey(
|
async unwrapSymmetricKey(
|
||||||
keyToBeUnwrapped: EncString,
|
keyToBeUnwrapped: EncString,
|
||||||
wrappingKey: SymmetricCryptoKey,
|
wrappingKey: SymmetricCryptoKey,
|
||||||
): Promise<SymmetricCryptoKey> {
|
): Promise<SymmetricCryptoKey> {
|
||||||
return new SymmetricCryptoKey(await this.decryptBytes(keyToBeUnwrapped, wrappingKey));
|
if (keyToBeUnwrapped == null) {
|
||||||
|
throw new Error("No keyToBeUnwrapped provided for unwrapping.");
|
||||||
|
}
|
||||||
|
if (wrappingKey == null) {
|
||||||
|
throw new Error("No wrappingKey provided for unwrapping.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await SdkLoadService.Ready;
|
||||||
|
return new SymmetricCryptoKey(
|
||||||
|
PureCrypto.unwrap_symmetric_key(keyToBeUnwrapped.encryptedString, wrappingKey.toEncoded()),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async hash(value: string | Uint8Array, algorithm: "sha1" | "sha256" | "sha512"): Promise<string> {
|
async hash(value: string | Uint8Array, algorithm: "sha1" | "sha256" | "sha512"): Promise<string> {
|
||||||
@@ -134,261 +172,33 @@ export class EncryptServiceImplementation implements EncryptService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle updating private properties to turn on/off feature flags.
|
// Handle updating private properties to turn on/off feature flags.
|
||||||
onServerConfigChange(newConfig: ServerConfig): void {
|
onServerConfigChange(newConfig: ServerConfig): void {}
|
||||||
const oldFlagValue = this.useSDKForDecryption;
|
|
||||||
this.useSDKForDecryption = getFeatureFlagValue(newConfig, FeatureFlag.UseSDKForDecryption);
|
|
||||||
this.logService.debug(
|
|
||||||
"[EncryptService] Updated sdk decryption flag",
|
|
||||||
oldFlagValue,
|
|
||||||
this.useSDKForDecryption,
|
|
||||||
);
|
|
||||||
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(
|
async decryptToUtf8(
|
||||||
encString: EncString,
|
encString: EncString,
|
||||||
key: SymmetricCryptoKey,
|
key: SymmetricCryptoKey,
|
||||||
decryptContext: string = "no context",
|
_decryptContext: string = "no context",
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
if (this.useSDKForDecryption) {
|
await SdkLoadService.Ready;
|
||||||
this.logService.debug("decrypting with SDK");
|
return PureCrypto.symmetric_decrypt(encString.encryptedString, key.toEncoded());
|
||||||
if (encString == null || encString.encryptedString == null) {
|
|
||||||
throw new Error("encString is null or undefined");
|
|
||||||
}
|
|
||||||
await SdkLoadService.Ready;
|
|
||||||
return PureCrypto.symmetric_decrypt(encString.encryptedString, key.toEncoded());
|
|
||||||
}
|
|
||||||
this.logService.debug("decrypting with javascript");
|
|
||||||
|
|
||||||
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(
|
async decryptToBytes(
|
||||||
encThing: Encrypted,
|
encThing: Encrypted,
|
||||||
key: SymmetricCryptoKey,
|
key: SymmetricCryptoKey,
|
||||||
decryptContext: string = "no context",
|
_decryptContext: string = "no context",
|
||||||
): Promise<Uint8Array | null> {
|
): Promise<Uint8Array | null> {
|
||||||
if (this.useSDKForDecryption) {
|
if (encThing.encryptionType == null || encThing.ivBytes == null || encThing.dataBytes == null) {
|
||||||
this.logService.debug("[EncryptService] Decrypting bytes with SDK");
|
throw new Error("Cannot decrypt, missing type, IV, or data bytes.");
|
||||||
if (
|
|
||||||
encThing.encryptionType == null ||
|
|
||||||
encThing.ivBytes == null ||
|
|
||||||
encThing.dataBytes == null
|
|
||||||
) {
|
|
||||||
throw new Error("Cannot decrypt, missing type, IV, or data bytes.");
|
|
||||||
}
|
|
||||||
const buffer = EncArrayBuffer.fromParts(
|
|
||||||
encThing.encryptionType,
|
|
||||||
encThing.ivBytes,
|
|
||||||
encThing.dataBytes,
|
|
||||||
encThing.macBytes,
|
|
||||||
).buffer;
|
|
||||||
await SdkLoadService.Ready;
|
|
||||||
return PureCrypto.symmetric_decrypt_array_buffer(buffer, key.toEncoded());
|
|
||||||
}
|
|
||||||
this.logService.debug("[EncryptService] Decrypting bytes with javascript");
|
|
||||||
|
|
||||||
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",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
const buffer = EncArrayBuffer.fromParts(
|
||||||
|
encThing.encryptionType,
|
||||||
|
encThing.ivBytes,
|
||||||
|
encThing.dataBytes,
|
||||||
|
encThing.macBytes,
|
||||||
|
).buffer;
|
||||||
|
await SdkLoadService.Ready;
|
||||||
|
return PureCrypto.symmetric_decrypt_array_buffer(buffer, key.toEncoded());
|
||||||
}
|
}
|
||||||
|
|
||||||
async encapsulateKeyUnsigned(
|
async encapsulateKeyUnsigned(
|
||||||
@@ -398,14 +208,31 @@ export class EncryptServiceImplementation implements EncryptService {
|
|||||||
if (sharedKey == null) {
|
if (sharedKey == null) {
|
||||||
throw new Error("No sharedKey provided for encapsulation");
|
throw new Error("No sharedKey provided for encapsulation");
|
||||||
}
|
}
|
||||||
return await this.rsaEncrypt(sharedKey.toEncoded(), encapsulationKey);
|
if (encapsulationKey == null) {
|
||||||
|
throw new Error("No encapsulationKey provided for encapsulation");
|
||||||
|
}
|
||||||
|
await SdkLoadService.Ready;
|
||||||
|
return new EncString(
|
||||||
|
PureCrypto.encapsulate_key_unsigned(sharedKey.toEncoded(), encapsulationKey),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async decapsulateKeyUnsigned(
|
async decapsulateKeyUnsigned(
|
||||||
encryptedSharedKey: EncString,
|
encryptedSharedKey: EncString,
|
||||||
decapsulationKey: Uint8Array,
|
decapsulationKey: Uint8Array,
|
||||||
): Promise<SymmetricCryptoKey> {
|
): Promise<SymmetricCryptoKey> {
|
||||||
const keyBytes = await this.rsaDecrypt(encryptedSharedKey, decapsulationKey);
|
if (encryptedSharedKey == null) {
|
||||||
|
throw new Error("No encryptedSharedKey provided for decapsulation");
|
||||||
|
}
|
||||||
|
if (decapsulationKey == null) {
|
||||||
|
throw new Error("No decapsulationKey provided for decapsulation");
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyBytes = PureCrypto.decapsulate_key_unsigned(
|
||||||
|
encryptedSharedKey.encryptedString,
|
||||||
|
decapsulationKey,
|
||||||
|
);
|
||||||
|
await SdkLoadService.Ready;
|
||||||
return new SymmetricCryptoKey(keyBytes);
|
return new SymmetricCryptoKey(keyBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -428,51 +255,6 @@ export class EncryptServiceImplementation implements EncryptService {
|
|||||||
return results;
|
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,
|
|
||||||
dataEncType: EncryptionType,
|
|
||||||
decryptContext: string,
|
|
||||||
) {
|
|
||||||
this.logService.error(
|
|
||||||
`[Encrypt service] ${msg} Key type ${encryptionTypeName(keyEncType)} Payload type ${encryptionTypeName(dataEncType)} Decrypt context: ${decryptContext}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private logMacFailed(
|
|
||||||
msg: string,
|
|
||||||
keyEncType: EncryptionType,
|
|
||||||
dataEncType: EncryptionType,
|
|
||||||
decryptContext: string,
|
|
||||||
) {
|
|
||||||
if (this.logMacFailures) {
|
|
||||||
this.logDecryptError(msg, keyEncType, dataEncType, decryptContext);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise<EncString> {
|
async rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise<EncString> {
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
throw new Error("No data provided for encryption.");
|
throw new Error("No data provided for encryption.");
|
||||||
|
|||||||
@@ -3,20 +3,14 @@ import { mockReset, mock } from "jest-mock-extended";
|
|||||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
|
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
|
||||||
import { EncryptionType } from "@bitwarden/common/platform/enums";
|
import { EncryptionType } from "@bitwarden/common/platform/enums";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer";
|
import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer";
|
||||||
import {
|
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
Aes256CbcHmacKey,
|
|
||||||
SymmetricCryptoKey,
|
|
||||||
} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
|
||||||
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
|
||||||
import { PureCrypto } from "@bitwarden/sdk-internal";
|
import { PureCrypto } from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
import { makeStaticByteArray } from "../../../../spec";
|
import { makeStaticByteArray } from "../../../../spec";
|
||||||
import { DefaultFeatureFlagValue, FeatureFlag } from "../../../enums/feature-flag.enum";
|
|
||||||
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
|
|
||||||
import { SdkLoadService } from "../../../platform/abstractions/sdk/sdk-load.service";
|
|
||||||
|
|
||||||
import { EncryptServiceImplementation } from "./encrypt.service.implementation";
|
import { EncryptServiceImplementation } from "./encrypt.service.implementation";
|
||||||
|
|
||||||
@@ -26,24 +20,50 @@ describe("EncryptService", () => {
|
|||||||
|
|
||||||
let encryptService: EncryptServiceImplementation;
|
let encryptService: EncryptServiceImplementation;
|
||||||
|
|
||||||
|
const testEncBuffer = EncArrayBuffer.fromParts(
|
||||||
|
EncryptionType.AesCbc256_HmacSha256_B64,
|
||||||
|
new Uint8Array(16),
|
||||||
|
new Uint8Array(32),
|
||||||
|
new Uint8Array(32),
|
||||||
|
);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockReset(cryptoFunctionService);
|
mockReset(cryptoFunctionService);
|
||||||
mockReset(logService);
|
mockReset(logService);
|
||||||
|
|
||||||
|
jest.spyOn(PureCrypto, "symmetric_decrypt_array_buffer").mockReturnValue(new Uint8Array(1));
|
||||||
|
jest.spyOn(PureCrypto, "symmetric_decrypt").mockReturnValue("decrypted_string");
|
||||||
|
|
||||||
|
jest.spyOn(PureCrypto, "symmetric_decrypt_filedata").mockReturnValue(new Uint8Array(1));
|
||||||
|
jest.spyOn(PureCrypto, "symmetric_encrypt_filedata").mockReturnValue(testEncBuffer.buffer);
|
||||||
|
jest.spyOn(PureCrypto, "symmetric_decrypt_string").mockReturnValue("decrypted_string");
|
||||||
|
jest.spyOn(PureCrypto, "symmetric_encrypt_string").mockReturnValue("encrypted_string");
|
||||||
|
jest.spyOn(PureCrypto, "symmetric_decrypt_bytes").mockReturnValue(new Uint8Array(3));
|
||||||
|
jest.spyOn(PureCrypto, "symmetric_encrypt_bytes").mockReturnValue("encrypted_bytes");
|
||||||
|
|
||||||
|
jest.spyOn(PureCrypto, "wrap_decapsulation_key").mockReturnValue("wrapped_decapsulation_key");
|
||||||
|
jest.spyOn(PureCrypto, "wrap_encapsulation_key").mockReturnValue("wrapped_encapsulation_key");
|
||||||
|
jest.spyOn(PureCrypto, "wrap_symmetric_key").mockReturnValue("wrapped_symmetric_key");
|
||||||
|
jest.spyOn(PureCrypto, "unwrap_decapsulation_key").mockReturnValue(new Uint8Array(4));
|
||||||
|
jest.spyOn(PureCrypto, "unwrap_encapsulation_key").mockReturnValue(new Uint8Array(5));
|
||||||
|
jest.spyOn(PureCrypto, "unwrap_symmetric_key").mockReturnValue(new Uint8Array(64));
|
||||||
|
|
||||||
|
jest.spyOn(PureCrypto, "decapsulate_key_unsigned").mockReturnValue(new Uint8Array(64));
|
||||||
|
jest.spyOn(PureCrypto, "encapsulate_key_unsigned").mockReturnValue("encapsulated_key_unsigned");
|
||||||
|
(SdkLoadService as any).Ready = jest.fn().mockResolvedValue(true);
|
||||||
|
|
||||||
encryptService = new EncryptServiceImplementation(cryptoFunctionService, logService, true);
|
encryptService = new EncryptServiceImplementation(cryptoFunctionService, logService, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("wrapSymmetricKey", () => {
|
describe("wrapSymmetricKey", () => {
|
||||||
it("roundtrip encrypts and decrypts a symmetric key", async () => {
|
it("is a proxy to PureCrypto", 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 key = new SymmetricCryptoKey(makeStaticByteArray(64));
|
||||||
const wrappingKey = new SymmetricCryptoKey(makeStaticByteArray(64));
|
const wrappingKey = new SymmetricCryptoKey(makeStaticByteArray(64));
|
||||||
const encString = await encryptService.wrapSymmetricKey(key, wrappingKey);
|
await encryptService.wrapSymmetricKey(key, wrappingKey);
|
||||||
expect(encString.encryptionType).toEqual(EncryptionType.AesCbc256_HmacSha256_B64);
|
expect(PureCrypto.wrap_symmetric_key).toHaveBeenCalledWith(
|
||||||
expect(encString.data).toEqual(Utils.fromBufferToB64(makeStaticByteArray(64, 0)));
|
key.toEncoded(),
|
||||||
|
wrappingKey.toEncoded(),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
it("fails if key toBeWrapped is null", async () => {
|
it("fails if key toBeWrapped is null", async () => {
|
||||||
const wrappingKey = new SymmetricCryptoKey(makeStaticByteArray(64));
|
const wrappingKey = new SymmetricCryptoKey(makeStaticByteArray(64));
|
||||||
@@ -57,33 +77,17 @@ describe("EncryptService", () => {
|
|||||||
"No wrappingKey provided for wrapping.",
|
"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", () => {
|
describe("wrapDecapsulationKey", () => {
|
||||||
it("roundtrip encrypts and decrypts a decapsulation key", async () => {
|
it("is a proxy to PureCrypto", async () => {
|
||||||
cryptoFunctionService.aesEncrypt.mockResolvedValue(makeStaticByteArray(64, 0));
|
const decapsulationKey = makeStaticByteArray(10);
|
||||||
cryptoFunctionService.randomBytes.mockResolvedValue(makeStaticByteArray(16) as CsprngArray);
|
|
||||||
cryptoFunctionService.hmac.mockResolvedValue(makeStaticByteArray(32));
|
|
||||||
|
|
||||||
const wrappingKey = new SymmetricCryptoKey(makeStaticByteArray(64));
|
const wrappingKey = new SymmetricCryptoKey(makeStaticByteArray(64));
|
||||||
const encString = await encryptService.wrapDecapsulationKey(
|
await encryptService.wrapDecapsulationKey(decapsulationKey, wrappingKey);
|
||||||
makeStaticByteArray(64),
|
expect(PureCrypto.wrap_decapsulation_key).toHaveBeenCalledWith(
|
||||||
wrappingKey,
|
decapsulationKey,
|
||||||
|
wrappingKey.toEncoded(),
|
||||||
);
|
);
|
||||||
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 () => {
|
it("fails if decapsulation key is null", async () => {
|
||||||
const wrappingKey = new SymmetricCryptoKey(makeStaticByteArray(64));
|
const wrappingKey = new SymmetricCryptoKey(makeStaticByteArray(64));
|
||||||
@@ -97,33 +101,17 @@ describe("EncryptService", () => {
|
|||||||
"No wrappingKey provided for wrapping.",
|
"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", () => {
|
describe("wrapEncapsulationKey", () => {
|
||||||
it("roundtrip encrypts and decrypts an encapsulationKey key", async () => {
|
it("is a proxy to PureCrypto", async () => {
|
||||||
cryptoFunctionService.aesEncrypt.mockResolvedValue(makeStaticByteArray(64, 0));
|
const encapsulationKey = makeStaticByteArray(10);
|
||||||
cryptoFunctionService.randomBytes.mockResolvedValue(makeStaticByteArray(16) as CsprngArray);
|
|
||||||
cryptoFunctionService.hmac.mockResolvedValue(makeStaticByteArray(32));
|
|
||||||
|
|
||||||
const wrappingKey = new SymmetricCryptoKey(makeStaticByteArray(64));
|
const wrappingKey = new SymmetricCryptoKey(makeStaticByteArray(64));
|
||||||
const encString = await encryptService.wrapEncapsulationKey(
|
await encryptService.wrapEncapsulationKey(encapsulationKey, wrappingKey);
|
||||||
makeStaticByteArray(64),
|
expect(PureCrypto.wrap_encapsulation_key).toHaveBeenCalledWith(
|
||||||
wrappingKey,
|
encapsulationKey,
|
||||||
|
wrappingKey.toEncoded(),
|
||||||
);
|
);
|
||||||
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 () => {
|
it("fails if encapsulation key is null", async () => {
|
||||||
const wrappingKey = new SymmetricCryptoKey(makeStaticByteArray(64));
|
const wrappingKey = new SymmetricCryptoKey(makeStaticByteArray(64));
|
||||||
@@ -137,535 +125,152 @@ describe("EncryptService", () => {
|
|||||||
"No wrappingKey provided for wrapping.",
|
"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("calls PureCrypto when useSDKForDecryption is true", async () => {
|
|
||||||
(encryptService as any).useSDKForDecryption = true;
|
|
||||||
const decryptedBytes = makeStaticByteArray(10, 200);
|
|
||||||
Object.defineProperty(SdkLoadService, "Ready", {
|
|
||||||
value: Promise.resolve(),
|
|
||||||
configurable: true,
|
|
||||||
});
|
|
||||||
jest.spyOn(PureCrypto, "symmetric_decrypt_array_buffer").mockReturnValue(decryptedBytes);
|
|
||||||
|
|
||||||
const actual = await encryptService.decryptToBytes(encBuffer, key);
|
|
||||||
|
|
||||||
expect(PureCrypto.symmetric_decrypt_array_buffer).toHaveBeenCalledWith(
|
|
||||||
encBuffer.buffer,
|
|
||||||
key.toEncoded(),
|
|
||||||
);
|
|
||||||
expect(actual).toEqualBuffer(decryptedBytes);
|
|
||||||
});
|
|
||||||
|
|
||||||
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("calls PureCrypto when useSDKForDecryption is true", async () => {
|
|
||||||
(encryptService as any).useSDKForDecryption = true;
|
|
||||||
const key = new SymmetricCryptoKey(makeStaticByteArray(64, 0));
|
|
||||||
const encString = new EncString(EncryptionType.AesCbc256_HmacSha256_B64, "data", "iv", "mac");
|
|
||||||
Object.defineProperty(SdkLoadService, "Ready", {
|
|
||||||
value: Promise.resolve(),
|
|
||||||
configurable: true,
|
|
||||||
});
|
|
||||||
jest.spyOn(PureCrypto, "symmetric_decrypt").mockReturnValue("data");
|
|
||||||
|
|
||||||
const actual = await encryptService.decryptToUtf8(encString, key);
|
|
||||||
|
|
||||||
expect(actual).toEqual("data");
|
|
||||||
expect(PureCrypto.symmetric_decrypt).toHaveBeenCalledWith(
|
|
||||||
encString.encryptedString,
|
|
||||||
key.toEncoded(),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
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", () => {
|
describe("encryptString", () => {
|
||||||
it("is a proxy to encrypt", async () => {
|
it("is a proxy to PureCrypto", async () => {
|
||||||
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
|
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
|
||||||
const plainValue = "data";
|
const plainValue = "data";
|
||||||
encryptService.encrypt = jest.fn();
|
const result = await encryptService.encryptString(plainValue, key);
|
||||||
await encryptService.encryptString(plainValue, key);
|
expect(result).toEqual(new EncString("encrypted_string"));
|
||||||
expect(encryptService.encrypt).toHaveBeenCalledWith(plainValue, key);
|
expect(PureCrypto.symmetric_encrypt_string).toHaveBeenCalledWith(plainValue, key.toEncoded());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("encryptBytes", () => {
|
describe("encryptBytes", () => {
|
||||||
it("is a proxy to encrypt", async () => {
|
it("is a proxy to PureCrypto", async () => {
|
||||||
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
|
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
|
||||||
const plainValue = makeStaticByteArray(16, 1);
|
const plainValue = makeStaticByteArray(16, 1);
|
||||||
encryptService.encrypt = jest.fn();
|
const result = await encryptService.encryptBytes(plainValue, key);
|
||||||
await encryptService.encryptBytes(plainValue, key);
|
expect(result).toEqual(new EncString("encrypted_bytes"));
|
||||||
expect(encryptService.encrypt).toHaveBeenCalledWith(plainValue, key);
|
expect(PureCrypto.symmetric_encrypt_bytes).toHaveBeenCalledWith(plainValue, key.toEncoded());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("encryptFileData", () => {
|
describe("encryptFileData", () => {
|
||||||
it("is a proxy to encryptToBytes", async () => {
|
it("is a proxy to PureCrypto", async () => {
|
||||||
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
|
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
|
||||||
const plainValue = makeStaticByteArray(16, 1);
|
const plainValue = makeStaticByteArray(16, 1);
|
||||||
encryptService.encryptToBytes = jest.fn();
|
const result = await encryptService.encryptFileData(plainValue, key);
|
||||||
await encryptService.encryptFileData(plainValue, key);
|
expect(result).toEqual(testEncBuffer);
|
||||||
expect(encryptService.encryptToBytes).toHaveBeenCalledWith(plainValue, key);
|
expect(PureCrypto.symmetric_encrypt_filedata).toHaveBeenCalledWith(
|
||||||
|
plainValue,
|
||||||
|
key.toEncoded(),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("decryptString", () => {
|
describe("decryptString", () => {
|
||||||
it("is a proxy to decryptToUtf8", async () => {
|
it("is a proxy to PureCrypto", async () => {
|
||||||
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
|
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
|
||||||
const encString = new EncString(EncryptionType.AesCbc256_B64, "data");
|
const encString = new EncString("encrypted_string");
|
||||||
encryptService.decryptToUtf8 = jest.fn();
|
const result = await encryptService.decryptString(encString, key);
|
||||||
await encryptService.decryptString(encString, key);
|
expect(result).toEqual("decrypted_string");
|
||||||
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encString, key);
|
expect(PureCrypto.symmetric_decrypt_string).toHaveBeenCalledWith(
|
||||||
|
encString.encryptedString,
|
||||||
|
key.toEncoded(),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("decryptBytes", () => {
|
describe("decryptBytes", () => {
|
||||||
it("is a proxy to decryptToBytes", async () => {
|
it("is a proxy to PureCrypto", async () => {
|
||||||
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
|
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
|
||||||
const encString = new EncString(EncryptionType.AesCbc256_B64, "data");
|
const encString = new EncString("encrypted_bytes");
|
||||||
encryptService.decryptToBytes = jest.fn();
|
const result = await encryptService.decryptBytes(encString, key);
|
||||||
await encryptService.decryptBytes(encString, key);
|
expect(result).toEqual(new Uint8Array(3));
|
||||||
expect(encryptService.decryptToBytes).toHaveBeenCalledWith(encString, key);
|
expect(PureCrypto.symmetric_decrypt_bytes).toHaveBeenCalledWith(
|
||||||
|
encString.encryptedString,
|
||||||
|
key.toEncoded(),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("decryptFileData", () => {
|
describe("decryptFileData", () => {
|
||||||
it("is a proxy to decrypt", async () => {
|
it("is a proxy to PureCrypto", async () => {
|
||||||
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
|
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
|
||||||
const encString = new EncArrayBuffer(makeStaticByteArray(60, EncryptionType.AesCbc256_B64));
|
const encString = new EncArrayBuffer(testEncBuffer.buffer);
|
||||||
encryptService.decryptToBytes = jest.fn();
|
const result = await encryptService.decryptFileData(encString, key);
|
||||||
await encryptService.decryptFileData(encString, key);
|
expect(result).toEqual(new Uint8Array(1));
|
||||||
expect(encryptService.decryptToBytes).toHaveBeenCalledWith(encString, key);
|
expect(PureCrypto.symmetric_decrypt_filedata).toHaveBeenCalledWith(
|
||||||
|
encString.buffer,
|
||||||
|
key.toEncoded(),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("unwrapDecapsulationKey", () => {
|
describe("unwrapDecapsulationKey", () => {
|
||||||
it("is a proxy to decryptBytes", async () => {
|
it("is a proxy to PureCrypto", async () => {
|
||||||
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
|
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
|
||||||
const encString = new EncString(EncryptionType.AesCbc256_B64, "data");
|
const encString = new EncString("wrapped_decapsulation_key");
|
||||||
encryptService.decryptBytes = jest.fn();
|
const result = await encryptService.unwrapDecapsulationKey(encString, key);
|
||||||
await encryptService.unwrapDecapsulationKey(encString, key);
|
expect(result).toEqual(new Uint8Array(4));
|
||||||
expect(encryptService.decryptBytes).toHaveBeenCalledWith(encString, key);
|
expect(PureCrypto.unwrap_decapsulation_key).toHaveBeenCalledWith(
|
||||||
|
encString.encryptedString,
|
||||||
|
key.toEncoded(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it("throws if wrappedDecapsulationKey is null", () => {
|
||||||
|
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
|
||||||
|
return expect(encryptService.unwrapDecapsulationKey(null, key)).rejects.toThrow(
|
||||||
|
"No wrappedDecapsulationKey provided for unwrapping.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it("throws if wrappingKey is null", () => {
|
||||||
|
const encString = new EncString("wrapped_decapsulation_key");
|
||||||
|
return expect(encryptService.unwrapDecapsulationKey(encString, null)).rejects.toThrow(
|
||||||
|
"No wrappingKey provided for unwrapping.",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("unwrapEncapsulationKey", () => {
|
describe("unwrapEncapsulationKey", () => {
|
||||||
it("is a proxy to decryptBytes", async () => {
|
it("is a proxy to PureCrypto", async () => {
|
||||||
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
|
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
|
||||||
const encString = new EncString(EncryptionType.AesCbc256_B64, "data");
|
const encString = new EncString("wrapped_encapsulation_key");
|
||||||
encryptService.decryptBytes = jest.fn();
|
const result = await encryptService.unwrapEncapsulationKey(encString, key);
|
||||||
await encryptService.unwrapEncapsulationKey(encString, key);
|
expect(result).toEqual(new Uint8Array(5));
|
||||||
expect(encryptService.decryptBytes).toHaveBeenCalledWith(encString, key);
|
expect(PureCrypto.unwrap_encapsulation_key).toHaveBeenCalledWith(
|
||||||
|
encString.encryptedString,
|
||||||
|
key.toEncoded(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it("throws if wrappedEncapsulationKey is null", () => {
|
||||||
|
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
|
||||||
|
return expect(encryptService.unwrapEncapsulationKey(null, key)).rejects.toThrow(
|
||||||
|
"No wrappedEncapsulationKey provided for unwrapping.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it("throws if wrappingKey is null", () => {
|
||||||
|
const encString = new EncString("wrapped_encapsulation_key");
|
||||||
|
return expect(encryptService.unwrapEncapsulationKey(encString, null)).rejects.toThrow(
|
||||||
|
"No wrappingKey provided for unwrapping.",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("unwrapSymmetricKey", () => {
|
describe("unwrapSymmetricKey", () => {
|
||||||
it("is a proxy to decryptBytes", async () => {
|
it("is a proxy to PureCrypto", async () => {
|
||||||
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
|
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
|
||||||
const encString = new EncString(EncryptionType.AesCbc256_B64, "data");
|
const encString = new EncString("wrapped_symmetric_key");
|
||||||
const jestFn = jest.fn();
|
const result = await encryptService.unwrapSymmetricKey(encString, key);
|
||||||
jestFn.mockResolvedValue(new Uint8Array(64));
|
expect(result).toEqual(new SymmetricCryptoKey(new Uint8Array(64)));
|
||||||
encryptService.decryptBytes = jestFn;
|
expect(PureCrypto.unwrap_symmetric_key).toHaveBeenCalledWith(
|
||||||
await encryptService.unwrapSymmetricKey(encString, key);
|
encString.encryptedString,
|
||||||
expect(encryptService.decryptBytes).toHaveBeenCalledWith(encString, key);
|
key.toEncoded(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it("throws if keyToBeUnwrapped is null", () => {
|
||||||
|
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
|
||||||
|
return expect(encryptService.unwrapSymmetricKey(null, key)).rejects.toThrow(
|
||||||
|
"No keyToBeUnwrapped provided for unwrapping.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it("throws if wrappingKey is null", () => {
|
||||||
|
const encString = new EncString("wrapped_symmetric_key");
|
||||||
|
return expect(encryptService.unwrapSymmetricKey(encString, null)).rejects.toThrow(
|
||||||
|
"No wrappingKey provided for unwrapping.",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -690,23 +295,13 @@ describe("EncryptService", () => {
|
|||||||
|
|
||||||
it("throws if no public key is provided", () => {
|
it("throws if no public key is provided", () => {
|
||||||
return expect(encryptService.encapsulateKeyUnsigned(testKey, null)).rejects.toThrow(
|
return expect(encryptService.encapsulateKeyUnsigned(testKey, null)).rejects.toThrow(
|
||||||
"No public key",
|
"No encapsulationKey provided for encapsulation",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("encrypts data with provided key", async () => {
|
it("encrypts data with provided key", async () => {
|
||||||
cryptoFunctionService.rsaEncrypt.mockResolvedValue(encryptedData);
|
|
||||||
|
|
||||||
const actual = await encryptService.encapsulateKeyUnsigned(testKey, publicKey);
|
const actual = await encryptService.encapsulateKeyUnsigned(testKey, publicKey);
|
||||||
|
expect(actual).toEqual(new EncString("encapsulated_key_unsigned"));
|
||||||
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", () => {
|
it("throws if no data was provided", () => {
|
||||||
@@ -719,39 +314,19 @@ describe("EncryptService", () => {
|
|||||||
describe("decapsulateKeyUnsigned", () => {
|
describe("decapsulateKeyUnsigned", () => {
|
||||||
it("throws if no data is provided", () => {
|
it("throws if no data is provided", () => {
|
||||||
return expect(encryptService.decapsulateKeyUnsigned(null, privateKey)).rejects.toThrow(
|
return expect(encryptService.decapsulateKeyUnsigned(null, privateKey)).rejects.toThrow(
|
||||||
"No data",
|
"No encryptedSharedKey provided for decapsulation",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws if no private key is provided", () => {
|
it("throws if no private key is provided", () => {
|
||||||
return expect(encryptService.decapsulateKeyUnsigned(encString, null)).rejects.toThrow(
|
return expect(encryptService.decapsulateKeyUnsigned(encString, null)).rejects.toThrow(
|
||||||
"No private key",
|
"No decapsulationKey provided for decapsulation",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
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 () => {
|
it("decrypts data with provided key", async () => {
|
||||||
cryptoFunctionService.rsaDecrypt.mockResolvedValue(data);
|
|
||||||
|
|
||||||
const actual = await encryptService.decapsulateKeyUnsigned(makeEncString(data), privateKey);
|
const actual = await encryptService.decapsulateKeyUnsigned(makeEncString(data), privateKey);
|
||||||
|
expect(actual.toEncoded()).toEqualBuffer(new Uint8Array(64));
|
||||||
expect(cryptoFunctionService.rsaDecrypt).toBeCalledWith(
|
|
||||||
expect.toEqualBuffer(data),
|
|
||||||
expect.toEqualBuffer(privateKey),
|
|
||||||
"sha1",
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(actual.toEncoded()).toEqualBuffer(data);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -9,7 +9,7 @@ import { ServerConfig } from "../../../platform/abstractions/config/server-confi
|
|||||||
import { EncryptService } from "../abstractions/encrypt.service";
|
import { EncryptService } from "../abstractions/encrypt.service";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated For the feature flag from PM-4154, remove once feature is rolled out
|
* @deprecated Will be deleted in an immediate subsequent PR
|
||||||
*/
|
*/
|
||||||
export class FallbackBulkEncryptService implements BulkEncryptService {
|
export class FallbackBulkEncryptService implements BulkEncryptService {
|
||||||
private featureFlagEncryptService: BulkEncryptService;
|
private featureFlagEncryptService: BulkEncryptService;
|
||||||
@@ -25,22 +25,10 @@ export class FallbackBulkEncryptService implements BulkEncryptService {
|
|||||||
items: Decryptable<T>[],
|
items: Decryptable<T>[],
|
||||||
key: SymmetricCryptoKey,
|
key: SymmetricCryptoKey,
|
||||||
): Promise<T[]> {
|
): Promise<T[]> {
|
||||||
if (this.featureFlagEncryptService != null) {
|
return await this.encryptService.decryptItems(items, key);
|
||||||
return await this.featureFlagEncryptService.decryptItems(items, key);
|
|
||||||
} else {
|
|
||||||
return await this.encryptService.decryptItems(items, key);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async setFeatureFlagEncryptService(featureFlagEncryptService: BulkEncryptService) {
|
async setFeatureFlagEncryptService(featureFlagEncryptService: BulkEncryptService) {}
|
||||||
if (this.currentServerConfig !== undefined) {
|
|
||||||
featureFlagEncryptService.onServerConfigChange(this.currentServerConfig);
|
|
||||||
}
|
|
||||||
this.featureFlagEncryptService = featureFlagEncryptService;
|
|
||||||
}
|
|
||||||
|
|
||||||
onServerConfigChange(newConfig: ServerConfig): void {
|
onServerConfigChange(newConfig: ServerConfig): void {}
|
||||||
this.currentServerConfig = newConfig;
|
|
||||||
(this.featureFlagEncryptService ?? this.encryptService).onServerConfigChange(newConfig);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,31 +1,16 @@
|
|||||||
// 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 { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface";
|
||||||
import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.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 { 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 { ServerConfig } from "../../../platform/abstractions/config/server-config";
|
||||||
import { buildDecryptMessage, buildSetConfigMessage } from "../types/worker-command.type";
|
|
||||||
|
|
||||||
import { EncryptServiceImplementation } from "./encrypt.service.implementation";
|
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)
|
* @deprecated Will be deleted in an immediate subsequent PR
|
||||||
*/
|
*/
|
||||||
export class MultithreadEncryptServiceImplementation extends EncryptServiceImplementation {
|
export class MultithreadEncryptServiceImplementation extends EncryptServiceImplementation {
|
||||||
private worker: Worker;
|
protected useSDKForDecryption: boolean = true;
|
||||||
private timeout: any;
|
|
||||||
private currentServerConfig: ServerConfig | undefined = undefined;
|
|
||||||
|
|
||||||
private clear$ = new Subject<void>();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends items to a web worker to decrypt them.
|
* Sends items to a web worker to decrypt them.
|
||||||
@@ -35,84 +20,8 @@ export class MultithreadEncryptServiceImplementation extends EncryptServiceImple
|
|||||||
items: Decryptable<T>[],
|
items: Decryptable<T>[],
|
||||||
key: SymmetricCryptoKey,
|
key: SymmetricCryptoKey,
|
||||||
): Promise<T[]> {
|
): Promise<T[]> {
|
||||||
if (items == null || items.length < 1) {
|
return await super.decryptItems(items, key);
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.useSDKForDecryption) {
|
|
||||||
return await super.decryptItems(items, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -233,48 +233,6 @@ describe("WebCrypto Function Service", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("aesEncrypt CBC mode", () => {
|
|
||||||
it("should successfully encrypt data", async () => {
|
|
||||||
const cryptoFunctionService = getWebCryptoFunctionService();
|
|
||||||
const iv = makeStaticByteArray(16);
|
|
||||||
const key = makeStaticByteArray(32);
|
|
||||||
const data = Utils.fromUtf8ToArray("EncryptMe!");
|
|
||||||
const encValue = await cryptoFunctionService.aesEncrypt(data, iv, key);
|
|
||||||
expect(Utils.fromBufferToB64(encValue)).toBe("ByUF8vhyX4ddU9gcooznwA==");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should successfully encrypt and then decrypt data fast", async () => {
|
|
||||||
const cryptoFunctionService = getWebCryptoFunctionService();
|
|
||||||
const iv = makeStaticByteArray(16);
|
|
||||||
const key = makeStaticByteArray(32);
|
|
||||||
const value = "EncryptMe!";
|
|
||||||
const data = Utils.fromUtf8ToArray(value);
|
|
||||||
const encValue = await cryptoFunctionService.aesEncrypt(data, iv, key);
|
|
||||||
const encData = Utils.fromBufferToB64(encValue);
|
|
||||||
const b64Iv = Utils.fromBufferToB64(iv);
|
|
||||||
const symKey = new SymmetricCryptoKey(key);
|
|
||||||
const parameters = cryptoFunctionService.aesDecryptFastParameters(
|
|
||||||
encData,
|
|
||||||
b64Iv,
|
|
||||||
null,
|
|
||||||
symKey,
|
|
||||||
);
|
|
||||||
const decValue = await cryptoFunctionService.aesDecryptFast({ mode: "cbc", parameters });
|
|
||||||
expect(decValue).toBe(value);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should successfully encrypt and then decrypt data", async () => {
|
|
||||||
const cryptoFunctionService = getWebCryptoFunctionService();
|
|
||||||
const iv = makeStaticByteArray(16);
|
|
||||||
const key = makeStaticByteArray(32);
|
|
||||||
const value = "EncryptMe!";
|
|
||||||
const data = Utils.fromUtf8ToArray(value);
|
|
||||||
const encValue = new Uint8Array(await cryptoFunctionService.aesEncrypt(data, iv, key));
|
|
||||||
const decValue = await cryptoFunctionService.aesDecrypt(encValue, iv, key, "cbc");
|
|
||||||
expect(Utils.fromBufferToUtf8(decValue)).toBe(value);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("aesDecryptFast CBC mode", () => {
|
describe("aesDecryptFast CBC mode", () => {
|
||||||
it("should successfully decrypt data", async () => {
|
it("should successfully decrypt data", async () => {
|
||||||
const cryptoFunctionService = getWebCryptoFunctionService();
|
const cryptoFunctionService = getWebCryptoFunctionService();
|
||||||
|
|||||||
@@ -204,14 +204,6 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
|
|||||||
return equals;
|
return equals;
|
||||||
}
|
}
|
||||||
|
|
||||||
async aesEncrypt(data: Uint8Array, iv: Uint8Array, key: Uint8Array): Promise<Uint8Array> {
|
|
||||||
const impKey = await this.subtle.importKey("raw", key, { name: "AES-CBC" } as any, false, [
|
|
||||||
"encrypt",
|
|
||||||
]);
|
|
||||||
const buffer = await this.subtle.encrypt({ name: "AES-CBC", iv: iv }, impKey, data);
|
|
||||||
return new Uint8Array(buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
aesDecryptFastParameters(
|
aesDecryptFastParameters(
|
||||||
data: string,
|
data: string,
|
||||||
iv: string,
|
iv: string,
|
||||||
|
|||||||
@@ -89,10 +89,13 @@ export class SendService implements InternalSendServiceAbstraction {
|
|||||||
}
|
}
|
||||||
// Key is not a SymmetricCryptoKey, but key material used to derive the cryptoKey
|
// Key is not a SymmetricCryptoKey, but key material used to derive the cryptoKey
|
||||||
send.key = await this.encryptService.encryptBytes(model.key, userKey);
|
send.key = await this.encryptService.encryptBytes(model.key, userKey);
|
||||||
|
// FIXME: model.name can be null. encryptString should not be called with null values.
|
||||||
send.name = await this.encryptService.encryptString(model.name, model.cryptoKey);
|
send.name = await this.encryptService.encryptString(model.name, model.cryptoKey);
|
||||||
|
// FIXME: model.notes can be null. encryptString should not be called with null values.
|
||||||
send.notes = await this.encryptService.encryptString(model.notes, model.cryptoKey);
|
send.notes = await this.encryptService.encryptString(model.notes, model.cryptoKey);
|
||||||
if (send.type === SendType.Text) {
|
if (send.type === SendType.Text) {
|
||||||
send.text = new SendText();
|
send.text = new SendText();
|
||||||
|
// FIXME: model.text.text can be null. encryptString should not be called with null values.
|
||||||
send.text.text = await this.encryptService.encryptString(model.text.text, model.cryptoKey);
|
send.text.text = await this.encryptService.encryptString(model.text.text, model.cryptoKey);
|
||||||
send.text.hidden = model.text.hidden;
|
send.text.hidden = model.text.hidden;
|
||||||
} else if (send.type === SendType.File) {
|
} else if (send.type === SendType.File) {
|
||||||
|
|||||||
@@ -3,10 +3,13 @@ import {
|
|||||||
NEVER,
|
NEVER,
|
||||||
Observable,
|
Observable,
|
||||||
combineLatest,
|
combineLatest,
|
||||||
|
distinctUntilChanged,
|
||||||
|
filter,
|
||||||
firstValueFrom,
|
firstValueFrom,
|
||||||
forkJoin,
|
forkJoin,
|
||||||
map,
|
map,
|
||||||
of,
|
of,
|
||||||
|
shareReplay,
|
||||||
switchMap,
|
switchMap,
|
||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
|
|
||||||
@@ -83,6 +86,9 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
|||||||
) {
|
) {
|
||||||
this.activeUserOrgKeys$ = this.stateProvider.activeUserId$.pipe(
|
this.activeUserOrgKeys$ = this.stateProvider.activeUserId$.pipe(
|
||||||
switchMap((userId) => (userId != null ? this.orgKeys$(userId) : NEVER)),
|
switchMap((userId) => (userId != null ? this.orgKeys$(userId) : NEVER)),
|
||||||
|
filter((orgKeys) => orgKeys != null),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
shareReplay({ bufferSize: 1, refCount: false }),
|
||||||
) as Observable<Record<OrganizationId, OrgKey>>;
|
) as Observable<Record<OrganizationId, OrgKey>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -402,12 +408,9 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getOrgKey(orgId: OrganizationId): Promise<OrgKey | null> {
|
async getOrgKey(orgId: OrganizationId): Promise<OrgKey | null> {
|
||||||
const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$);
|
return await firstValueFrom(
|
||||||
if (activeUserId == null) {
|
this.activeUserOrgKeys$.pipe(map((orgKeys) => orgKeys[orgId] ?? null)),
|
||||||
throw new Error("A user must be active to retrieve an org key");
|
);
|
||||||
}
|
|
||||||
const orgKeys = await firstValueFrom(this.orgKeys$(activeUserId));
|
|
||||||
return orgKeys?.[orgId] ?? null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async makeDataEncKey<T extends OrgKey | UserKey>(
|
async makeDataEncKey<T extends OrgKey | UserKey>(
|
||||||
|
|||||||
Reference in New Issue
Block a user