From 8b5e6adc376d21552137e31ca63b8939f343f447 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 21 Jul 2025 15:52:38 +0200 Subject: [PATCH] [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 --- .../local-backed-session-storage.service.ts | 8 +- apps/desktop/src/main.ts | 7 + .../src/services/main-sdk-load-service.ts | 9 + apps/desktop/webpack.main.js | 3 + .../abstractions/crypto-function.service.ts | 1 - .../crypto/abstractions/encrypt.service.ts | 14 - ...bulk-encrypt.service.impementation.spec.ts | 170 ---- .../bulk-encrypt.service.implementation.ts | 162 +--- .../encrypt.service.implementation.ts | 424 +++------- .../crypto/services/encrypt.service.spec.ts | 729 ++++-------------- .../fallback-bulk-encrypt.service.spec.ts | 97 --- .../services/fallback-bulk-encrypt.service.ts | 20 +- ...ead-encrypt.service.implementation.spec.ts | 123 --- ...tithread-encrypt.service.implementation.ts | 99 +-- .../web-crypto-function.service.spec.ts | 42 - .../services/web-crypto-function.service.ts | 8 - .../src/tools/send/services/send.service.ts | 3 + libs/key-management/src/key.service.ts | 15 +- 18 files changed, 306 insertions(+), 1628 deletions(-) create mode 100644 apps/desktop/src/services/main-sdk-load-service.ts delete mode 100644 libs/common/src/key-management/crypto/services/bulk-encrypt.service.impementation.spec.ts delete mode 100644 libs/common/src/key-management/crypto/services/fallback-bulk-encrypt.service.spec.ts delete mode 100644 libs/common/src/key-management/crypto/services/multithread-encrypt.service.implementation.spec.ts diff --git a/apps/browser/src/platform/services/local-backed-session-storage.service.ts b/apps/browser/src/platform/services/local-backed-session-storage.service.ts index ee5a53b0ad2..978b993fa4d 100644 --- a/apps/browser/src/platform/services/local-backed-session-storage.service.ts +++ b/apps/browser/src/platform/services/local-backed-session-storage.service.ts @@ -118,14 +118,14 @@ export class LocalBackedSessionStorageService return null; } - const valueJson = await this.encryptService.decryptString(new EncString(local), encKey); - if (valueJson == null) { + try { + 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 await this.localStorage.remove(this.sessionStorageKey(key)); return null; } - - return JSON.parse(valueJson); } private async updateLocalSessionValue(key: string, value: unknown): Promise { diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 8c92131617a..eee5e3c84f2 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -12,6 +12,7 @@ import { AccountServiceImplementation } from "@bitwarden/common/auth/services/ac import { ClientType } from "@bitwarden/common/enums"; import { EncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/encrypt.service.implementation"; 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"; // eslint-disable-next-line no-restricted-imports -- For dependency creation 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 { SSOLocalhostCallbackService } from "./platform/services/sso-localhost-callback.service"; import { ElectronMainMessagingService } from "./services/electron-main-messaging.service"; +import { MainSdkLoadService } from "./services/main-sdk-load-service"; import { isMacAppStore } from "./utils"; export class Main { @@ -88,6 +90,7 @@ export class Main { desktopAutofillSettingsService: DesktopAutofillSettingsService; versionMain: VersionMain; sshAgentService: MainSshAgentService; + sdkLoadService: SdkLoadService; mainDesktopAutotypeService: MainDesktopAutotypeService; constructor() { @@ -144,6 +147,8 @@ export class Main { this.i18nService = new I18nMainService("en", "./locales/", globalStateProvider); + this.sdkLoadService = new MainSdkLoadService(); + this.mainCryptoFunctionService = new NodeCryptoFunctionService(); const stateEventRegistrarService = new StateEventRegistrarService( @@ -386,6 +391,8 @@ export class Main { this.windowMain.win.on("minimize", () => { this.messagingService.send("windowHidden"); }); + + await this.sdkLoadService.loadAndInit(); }, (e: any) => { this.logService.error("Error while running migrations:", e); diff --git a/apps/desktop/src/services/main-sdk-load-service.ts b/apps/desktop/src/services/main-sdk-load-service.ts new file mode 100644 index 00000000000..847505f68fe --- /dev/null +++ b/apps/desktop/src/services/main-sdk-load-service.ts @@ -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 { + const module = await import("@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm"); + (sdk as any).init(module); + } +} diff --git a/apps/desktop/webpack.main.js b/apps/desktop/webpack.main.js index 25a68d8c867..166fba95d52 100644 --- a/apps/desktop/webpack.main.js +++ b/apps/desktop/webpack.main.js @@ -65,6 +65,9 @@ const main = { }, ], }, + experiments: { + asyncWebAssembly: true, + }, plugins: [ new CopyWebpackPlugin({ patterns: [ diff --git a/libs/common/src/key-management/crypto/abstractions/crypto-function.service.ts b/libs/common/src/key-management/crypto/abstractions/crypto-function.service.ts index d9541481083..5e4fa86a684 100644 --- a/libs/common/src/key-management/crypto/abstractions/crypto-function.service.ts +++ b/libs/common/src/key-management/crypto/abstractions/crypto-function.service.ts @@ -41,7 +41,6 @@ export abstract class CryptoFunctionService { algorithm: "sha1" | "sha256" | "sha512", ): Promise; abstract compareFast(a: Uint8Array | string, b: Uint8Array | string): Promise; - abstract aesEncrypt(data: Uint8Array, iv: Uint8Array, key: Uint8Array): Promise; abstract aesDecryptFastParameters( data: string, iv: string, diff --git a/libs/common/src/key-management/crypto/abstractions/encrypt.service.ts b/libs/common/src/key-management/crypto/abstractions/encrypt.service.ts index 93d9cb64ce1..67db3591e74 100644 --- a/libs/common/src/key-management/crypto/abstractions/encrypt.service.ts +++ b/libs/common/src/key-management/crypto/abstractions/encrypt.service.ts @@ -7,20 +7,6 @@ import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-cr import { EncString } from "../models/enc-string"; 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; - /** - * @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; /** * @deprecated * Decrypts an EncString to a string diff --git a/libs/common/src/key-management/crypto/services/bulk-encrypt.service.impementation.spec.ts b/libs/common/src/key-management/crypto/services/bulk-encrypt.service.impementation.spec.ts deleted file mode 100644 index bc77cfb410f..00000000000 --- a/libs/common/src/key-management/crypto/services/bulk-encrypt.service.impementation.spec.ts +++ /dev/null @@ -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(); - const logService = mock(); - - let sut: BulkEncryptServiceImplementation; - - beforeEach(() => { - sut = new BulkEncryptServiceImplementation(cryptoFunctionService, logService); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - describe("decryptItems", () => { - const key = mock(); - const serverConfig = mock(); - const mockWorker = mock(); - 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(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(); - - 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(); - const mockWorker = mock(); - (sut as any).workers = [mockWorker]; - - sut.onServerConfigChange(newConfig); - - expect(mockWorker.postMessage).toHaveBeenCalledWith(buildSetConfigMessage({ newConfig })); - }); - }); -}); - -function createMockDecryptable( - returnValue: any, -): MockProxy> { - const mockDecryptable = mock>(); - mockDecryptable.decrypt.mockResolvedValue(returnValue); - return mockDecryptable; -} diff --git a/libs/common/src/key-management/crypto/services/bulk-encrypt.service.implementation.ts b/libs/common/src/key-management/crypto/services/bulk-encrypt.service.implementation.ts index 7421ae1eb20..1bc1827a07a 100644 --- a/libs/common/src/key-management/crypto/services/bulk-encrypt.service.implementation.ts +++ b/libs/common/src/key-management/crypto/services/bulk-encrypt.service.implementation.ts @@ -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 { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface"; import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { getClassInitializer } from "@bitwarden/common/platform/services/cryptography/get-class-initializer"; -import { - DefaultFeatureFlagValue, - FeatureFlag, - getFeatureFlagValue, -} from "../../../enums/feature-flag.enum"; +import { DefaultFeatureFlagValue, FeatureFlag } from "../../../enums/feature-flag.enum"; 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 { - private workers: Worker[] = []; - private timeout: any; - private currentServerConfig: ServerConfig | undefined = undefined; protected useSDKForDecryption: boolean = DefaultFeatureFlagValue[FeatureFlag.UseSDKForDecryption]; - private clear$ = new Subject(); - constructor( protected cryptoFunctionService: CryptoFunctionService, protected logService: LogService, @@ -54,139 +35,12 @@ export class BulkEncryptServiceImplementation implements BulkEncryptService { 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( - items: Decryptable[], - key: SymmetricCryptoKey, - ): Promise { - if (items == null || items.length < 1) { - return []; - } - - this.clearTimeout(); - - const hardwareConcurrency = navigator.hardwareConcurrency || 1; - let numberOfWorkers = Math.min(hardwareConcurrency, maxWorkers); - if (items.length < minNumberOfItemsForMultithreading) { - numberOfWorkers = 1; - } - - this.logService.info( - `Starting decryption using multithreading with ${numberOfWorkers} workers for ${items.length} items`, - ); - - if (this.workers.length == 0) { - for (let i = 0; i < numberOfWorkers; i++) { - this.workers.push( - new Worker( - new URL( - /* webpackChunkName: 'encrypt-worker' */ - "@bitwarden/common/key-management/crypto/services/encrypt.worker.ts", - import.meta.url, - ), - ), - ); - } - if (this.currentServerConfig != undefined) { - this.updateWorkerServerConfigs(this.currentServerConfig); - } - } - - const itemsPerWorker = Math.floor(items.length / this.workers.length); const results = []; - - for (const [i, worker] of this.workers.entries()) { - const start = i * itemsPerWorker; - const end = start + itemsPerWorker; - const itemsForWorker = items.slice(start, end); - - // push the remaining items to the last worker - if (i == this.workers.length - 1) { - itemsForWorker.push(...items.slice(end)); - } - - const id = Utils.newGuid(); - const request = buildDecryptMessage({ - id, - items: itemsForWorker, - key: key, - }); - - worker.postMessage(request); - results.push( - firstValueFrom( - fromEvent(worker, "message").pipe( - filter((response: MessageEvent) => response.data?.id === id), - map((response) => JSON.parse(response.data.items)), - map((items) => - items.map((jsonItem: Jsonify) => { - const initializer = getClassInitializer(jsonItem.initializerKey); - return initializer(jsonItem); - }), - ), - takeUntil(this.clear$), - defaultIfEmpty([]), - ), - ), - ); + for (let i = 0; i < items.length; i++) { + results.push(await items[i].decrypt(key)); } - - const decryptedItems = (await Promise.all(results)).flat(); - this.logService.info( - `Finished decrypting ${decryptedItems.length} items using ${numberOfWorkers} workers`, - ); - - this.restartTimeout(); - - return decryptedItems; + return results; } - private updateWorkerServerConfigs(newConfig: ServerConfig) { - this.workers.forEach((worker) => { - const request = buildSetConfigMessage({ newConfig }); - worker.postMessage(request); - }); - } - - private clear() { - this.clear$.next(); - for (const worker of this.workers) { - worker.terminate(); - } - this.workers = []; - this.clearTimeout(); - } - - private restartTimeout() { - this.clearTimeout(); - this.timeout = setTimeout(() => this.clear(), workerTTL); - } - - private clearTimeout() { - if (this.timeout != null) { - clearTimeout(this.timeout); - } - } + onServerConfigChange(newConfig: ServerConfig): void {} } diff --git a/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts b/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts index a68ecd2db54..3e36fd334ec 100644 --- a/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts +++ b/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts @@ -3,36 +3,20 @@ import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { - EncryptionType, - encryptionTypeToString as encryptionTypeName, -} from "@bitwarden/common/platform/enums"; +import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; +import { EncryptionType } from "@bitwarden/common/platform/enums"; import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface"; import { Encrypted } from "@bitwarden/common/platform/interfaces/encrypted"; import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer"; -import { EncryptedObject } from "@bitwarden/common/platform/models/domain/encrypted-object"; -import { - Aes256CbcHmacKey, - Aes256CbcKey, - SymmetricCryptoKey, -} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { PureCrypto } from "@bitwarden/sdk-internal"; -import { - DefaultFeatureFlagValue, - FeatureFlag, - getFeatureFlagValue, -} from "../../../enums/feature-flag.enum"; import { ServerConfig } from "../../../platform/abstractions/config/server-config"; -import { SdkLoadService } from "../../../platform/abstractions/sdk/sdk-load.service"; import { EncryptService } from "../abstractions/encrypt.service"; export class EncryptServiceImplementation implements EncryptService { - protected useSDKForDecryption: boolean = DefaultFeatureFlagValue[FeatureFlag.UseSDKForDecryption]; - private blockType0: boolean = DefaultFeatureFlagValue[FeatureFlag.PM17987_BlockType0]; - constructor( protected cryptoFunctionService: CryptoFunctionService, 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 async encryptString(plainValue: string, key: SymmetricCryptoKey): Promise { - 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 { - 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 { - 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 { - return this.decryptToUtf8(encString, key); + await SdkLoadService.Ready; + return PureCrypto.symmetric_decrypt_string(encString.encryptedString, key.toEncoded()); } async decryptBytes(encString: EncString, key: SymmetricCryptoKey): Promise { - return this.decryptToBytes(encString, key); + await SdkLoadService.Ready; + return PureCrypto.symmetric_decrypt_bytes(encString.encryptedString, key.toEncoded()); } async decryptFileData(encBuffer: EncArrayBuffer, key: SymmetricCryptoKey): Promise { - return this.decryptToBytes(encBuffer, key); + await SdkLoadService.Ready; + return PureCrypto.symmetric_decrypt_filedata(encBuffer.buffer, key.toEncoded()); } async wrapDecapsulationKey( @@ -76,7 +73,10 @@ export class EncryptServiceImplementation implements EncryptService { 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( @@ -91,7 +91,10 @@ export class EncryptServiceImplementation implements EncryptService { 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( @@ -106,26 +109,61 @@ export class EncryptServiceImplementation implements EncryptService { 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( wrappedDecapsulationKey: EncString, wrappingKey: SymmetricCryptoKey, ): Promise { - 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( wrappedEncapsulationKey: EncString, wrappingKey: SymmetricCryptoKey, ): Promise { - 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( keyToBeUnwrapped: EncString, wrappingKey: SymmetricCryptoKey, ): Promise { - 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 { @@ -134,261 +172,33 @@ export class EncryptServiceImplementation implements EncryptService { } // Handle updating private properties to turn on/off feature flags. - 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 { - 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 { - 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 { - 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); - } - } + onServerConfigChange(newConfig: ServerConfig): void {} async decryptToUtf8( encString: EncString, key: SymmetricCryptoKey, - decryptContext: string = "no context", + _decryptContext: string = "no context", ): Promise { - if (this.useSDKForDecryption) { - this.logService.debug("decrypting with SDK"); - 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`); - } + await SdkLoadService.Ready; + return PureCrypto.symmetric_decrypt(encString.encryptedString, key.toEncoded()); } async decryptToBytes( encThing: Encrypted, key: SymmetricCryptoKey, - decryptContext: string = "no context", + _decryptContext: string = "no context", ): Promise { - if (this.useSDKForDecryption) { - this.logService.debug("[EncryptService] Decrypting bytes with SDK"); - 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", - ); + 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()); } async encapsulateKeyUnsigned( @@ -398,14 +208,31 @@ export class EncryptServiceImplementation implements EncryptService { if (sharedKey == null) { 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( encryptedSharedKey: EncString, decapsulationKey: Uint8Array, ): Promise { - 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); } @@ -428,51 +255,6 @@ export class EncryptServiceImplementation implements EncryptService { return results; } - private async aesEncrypt(data: Uint8Array, key: Aes256CbcHmacKey): Promise { - 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 { - 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 { if (data == null) { throw new Error("No data provided for encryption."); diff --git a/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts b/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts index 68021086bb1..4cc76d45f50 100644 --- a/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts +++ b/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts @@ -3,20 +3,14 @@ import { mockReset, mock } from "jest-mock-extended"; import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; 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 { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer"; -import { - Aes256CbcHmacKey, - SymmetricCryptoKey, -} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { CsprngArray } from "@bitwarden/common/types/csprng"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { PureCrypto } from "@bitwarden/sdk-internal"; 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"; @@ -26,24 +20,50 @@ describe("EncryptService", () => { let encryptService: EncryptServiceImplementation; + const testEncBuffer = EncArrayBuffer.fromParts( + EncryptionType.AesCbc256_HmacSha256_B64, + new Uint8Array(16), + new Uint8Array(32), + new Uint8Array(32), + ); + beforeEach(() => { mockReset(cryptoFunctionService); 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); }); describe("wrapSymmetricKey", () => { - it("roundtrip encrypts and decrypts a symmetric key", async () => { - cryptoFunctionService.aesEncrypt.mockResolvedValue(makeStaticByteArray(64, 0)); - cryptoFunctionService.randomBytes.mockResolvedValue(makeStaticByteArray(16) as CsprngArray); - cryptoFunctionService.hmac.mockResolvedValue(makeStaticByteArray(32)); - + it("is a proxy to PureCrypto", async () => { const key = new SymmetricCryptoKey(makeStaticByteArray(64)); const wrappingKey = new SymmetricCryptoKey(makeStaticByteArray(64)); - const encString = await encryptService.wrapSymmetricKey(key, wrappingKey); - expect(encString.encryptionType).toEqual(EncryptionType.AesCbc256_HmacSha256_B64); - expect(encString.data).toEqual(Utils.fromBufferToB64(makeStaticByteArray(64, 0))); + await encryptService.wrapSymmetricKey(key, wrappingKey); + expect(PureCrypto.wrap_symmetric_key).toHaveBeenCalledWith( + key.toEncoded(), + wrappingKey.toEncoded(), + ); }); it("fails if key toBeWrapped is null", async () => { const wrappingKey = new SymmetricCryptoKey(makeStaticByteArray(64)); @@ -57,33 +77,17 @@ describe("EncryptService", () => { "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(); - mock32Key.inner.mockReturnValue({ - type: 0, - encryptionKey: makeStaticByteArray(32), - }); - - await expect(encryptService.wrapSymmetricKey(mock32Key, mock32Key)).rejects.toThrow( - "Type 0 encryption is not supported.", - ); - }); }); describe("wrapDecapsulationKey", () => { - it("roundtrip encrypts and decrypts a decapsulation key", async () => { - cryptoFunctionService.aesEncrypt.mockResolvedValue(makeStaticByteArray(64, 0)); - cryptoFunctionService.randomBytes.mockResolvedValue(makeStaticByteArray(16) as CsprngArray); - cryptoFunctionService.hmac.mockResolvedValue(makeStaticByteArray(32)); - + it("is a proxy to PureCrypto", async () => { + const decapsulationKey = makeStaticByteArray(10); const wrappingKey = new SymmetricCryptoKey(makeStaticByteArray(64)); - const encString = await encryptService.wrapDecapsulationKey( - makeStaticByteArray(64), - wrappingKey, + await encryptService.wrapDecapsulationKey(decapsulationKey, wrappingKey); + expect(PureCrypto.wrap_decapsulation_key).toHaveBeenCalledWith( + 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 () => { const wrappingKey = new SymmetricCryptoKey(makeStaticByteArray(64)); @@ -97,33 +101,17 @@ describe("EncryptService", () => { "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(); - mock32Key.inner.mockReturnValue({ - type: 0, - encryptionKey: makeStaticByteArray(32), - }); - - await expect( - encryptService.wrapDecapsulationKey(new Uint8Array(200), mock32Key), - ).rejects.toThrow("Type 0 encryption is not supported."); - }); }); describe("wrapEncapsulationKey", () => { - it("roundtrip encrypts and decrypts an encapsulationKey key", async () => { - cryptoFunctionService.aesEncrypt.mockResolvedValue(makeStaticByteArray(64, 0)); - cryptoFunctionService.randomBytes.mockResolvedValue(makeStaticByteArray(16) as CsprngArray); - cryptoFunctionService.hmac.mockResolvedValue(makeStaticByteArray(32)); - + it("is a proxy to PureCrypto", async () => { + const encapsulationKey = makeStaticByteArray(10); const wrappingKey = new SymmetricCryptoKey(makeStaticByteArray(64)); - const encString = await encryptService.wrapEncapsulationKey( - makeStaticByteArray(64), - wrappingKey, + await encryptService.wrapEncapsulationKey(encapsulationKey, wrappingKey); + expect(PureCrypto.wrap_encapsulation_key).toHaveBeenCalledWith( + 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 () => { const wrappingKey = new SymmetricCryptoKey(makeStaticByteArray(64)); @@ -137,535 +125,152 @@ describe("EncryptService", () => { "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(); - 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(); - - 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(); - 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(); - 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", () => { - it("is a proxy to encrypt", async () => { + it("is a proxy to PureCrypto", async () => { const key = new SymmetricCryptoKey(makeStaticByteArray(64)); const plainValue = "data"; - encryptService.encrypt = jest.fn(); - await encryptService.encryptString(plainValue, key); - expect(encryptService.encrypt).toHaveBeenCalledWith(plainValue, key); + const result = await encryptService.encryptString(plainValue, key); + expect(result).toEqual(new EncString("encrypted_string")); + expect(PureCrypto.symmetric_encrypt_string).toHaveBeenCalledWith(plainValue, key.toEncoded()); }); }); describe("encryptBytes", () => { - it("is a proxy to encrypt", async () => { + it("is a proxy to PureCrypto", async () => { const key = new SymmetricCryptoKey(makeStaticByteArray(64)); const plainValue = makeStaticByteArray(16, 1); - encryptService.encrypt = jest.fn(); - await encryptService.encryptBytes(plainValue, key); - expect(encryptService.encrypt).toHaveBeenCalledWith(plainValue, key); + const result = await encryptService.encryptBytes(plainValue, key); + expect(result).toEqual(new EncString("encrypted_bytes")); + expect(PureCrypto.symmetric_encrypt_bytes).toHaveBeenCalledWith(plainValue, key.toEncoded()); }); }); describe("encryptFileData", () => { - it("is a proxy to encryptToBytes", async () => { + it("is a proxy to PureCrypto", async () => { const key = new SymmetricCryptoKey(makeStaticByteArray(64)); const plainValue = makeStaticByteArray(16, 1); - encryptService.encryptToBytes = jest.fn(); - await encryptService.encryptFileData(plainValue, key); - expect(encryptService.encryptToBytes).toHaveBeenCalledWith(plainValue, key); + const result = await encryptService.encryptFileData(plainValue, key); + expect(result).toEqual(testEncBuffer); + expect(PureCrypto.symmetric_encrypt_filedata).toHaveBeenCalledWith( + plainValue, + key.toEncoded(), + ); }); }); describe("decryptString", () => { - it("is a proxy to decryptToUtf8", async () => { + it("is a proxy to PureCrypto", async () => { const key = new SymmetricCryptoKey(makeStaticByteArray(64)); - const encString = new EncString(EncryptionType.AesCbc256_B64, "data"); - encryptService.decryptToUtf8 = jest.fn(); - await encryptService.decryptString(encString, key); - expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encString, key); + const encString = new EncString("encrypted_string"); + const result = await encryptService.decryptString(encString, key); + expect(result).toEqual("decrypted_string"); + expect(PureCrypto.symmetric_decrypt_string).toHaveBeenCalledWith( + encString.encryptedString, + key.toEncoded(), + ); }); }); describe("decryptBytes", () => { - it("is a proxy to decryptToBytes", async () => { + it("is a proxy to PureCrypto", async () => { const key = new SymmetricCryptoKey(makeStaticByteArray(64)); - const encString = new EncString(EncryptionType.AesCbc256_B64, "data"); - encryptService.decryptToBytes = jest.fn(); - await encryptService.decryptBytes(encString, key); - expect(encryptService.decryptToBytes).toHaveBeenCalledWith(encString, key); + const encString = new EncString("encrypted_bytes"); + const result = await encryptService.decryptBytes(encString, key); + expect(result).toEqual(new Uint8Array(3)); + expect(PureCrypto.symmetric_decrypt_bytes).toHaveBeenCalledWith( + encString.encryptedString, + key.toEncoded(), + ); }); }); describe("decryptFileData", () => { - it("is a proxy to decrypt", async () => { + it("is a proxy to PureCrypto", async () => { const key = new SymmetricCryptoKey(makeStaticByteArray(64)); - const encString = new EncArrayBuffer(makeStaticByteArray(60, EncryptionType.AesCbc256_B64)); - encryptService.decryptToBytes = jest.fn(); - await encryptService.decryptFileData(encString, key); - expect(encryptService.decryptToBytes).toHaveBeenCalledWith(encString, key); + const encString = new EncArrayBuffer(testEncBuffer.buffer); + const result = await encryptService.decryptFileData(encString, key); + expect(result).toEqual(new Uint8Array(1)); + expect(PureCrypto.symmetric_decrypt_filedata).toHaveBeenCalledWith( + encString.buffer, + key.toEncoded(), + ); }); }); describe("unwrapDecapsulationKey", () => { - it("is a proxy to decryptBytes", async () => { + it("is a proxy to PureCrypto", async () => { const key = new SymmetricCryptoKey(makeStaticByteArray(64)); - const encString = new EncString(EncryptionType.AesCbc256_B64, "data"); - encryptService.decryptBytes = jest.fn(); - await encryptService.unwrapDecapsulationKey(encString, key); - expect(encryptService.decryptBytes).toHaveBeenCalledWith(encString, key); + const encString = new EncString("wrapped_decapsulation_key"); + const result = await encryptService.unwrapDecapsulationKey(encString, key); + expect(result).toEqual(new Uint8Array(4)); + 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", () => { - it("is a proxy to decryptBytes", async () => { + it("is a proxy to PureCrypto", async () => { const key = new SymmetricCryptoKey(makeStaticByteArray(64)); - const encString = new EncString(EncryptionType.AesCbc256_B64, "data"); - encryptService.decryptBytes = jest.fn(); - await encryptService.unwrapEncapsulationKey(encString, key); - expect(encryptService.decryptBytes).toHaveBeenCalledWith(encString, key); + const encString = new EncString("wrapped_encapsulation_key"); + const result = await encryptService.unwrapEncapsulationKey(encString, key); + expect(result).toEqual(new Uint8Array(5)); + 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", () => { - it("is a proxy to decryptBytes", async () => { + it("is a proxy to PureCrypto", async () => { const key = new SymmetricCryptoKey(makeStaticByteArray(64)); - const encString = new EncString(EncryptionType.AesCbc256_B64, "data"); - const jestFn = jest.fn(); - jestFn.mockResolvedValue(new Uint8Array(64)); - encryptService.decryptBytes = jestFn; - await encryptService.unwrapSymmetricKey(encString, key); - expect(encryptService.decryptBytes).toHaveBeenCalledWith(encString, key); + const encString = new EncString("wrapped_symmetric_key"); + const result = await encryptService.unwrapSymmetricKey(encString, key); + expect(result).toEqual(new SymmetricCryptoKey(new Uint8Array(64))); + expect(PureCrypto.unwrap_symmetric_key).toHaveBeenCalledWith( + encString.encryptedString, + 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", () => { return expect(encryptService.encapsulateKeyUnsigned(testKey, null)).rejects.toThrow( - "No public key", + "No encapsulationKey provided for encapsulation", ); }); it("encrypts data with provided key", async () => { - cryptoFunctionService.rsaEncrypt.mockResolvedValue(encryptedData); - const actual = await encryptService.encapsulateKeyUnsigned(testKey, publicKey); - - expect(cryptoFunctionService.rsaEncrypt).toBeCalledWith( - expect.toEqualBuffer(testKey.toEncoded()), - expect.toEqualBuffer(publicKey), - "sha1", - ); - - expect(actual).toEqual(encString); - expect(actual.dataBytes).toEqualBuffer(encryptedData); + expect(actual).toEqual(new EncString("encapsulated_key_unsigned")); }); it("throws if no data was provided", () => { @@ -719,39 +314,19 @@ describe("EncryptService", () => { describe("decapsulateKeyUnsigned", () => { it("throws if no data is provided", () => { return expect(encryptService.decapsulateKeyUnsigned(null, privateKey)).rejects.toThrow( - "No data", + "No encryptedSharedKey provided for decapsulation", ); }); it("throws if no private key is provided", () => { 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 () => { - cryptoFunctionService.rsaDecrypt.mockResolvedValue(data); - const actual = await encryptService.decapsulateKeyUnsigned(makeEncString(data), privateKey); - - expect(cryptoFunctionService.rsaDecrypt).toBeCalledWith( - expect.toEqualBuffer(data), - expect.toEqualBuffer(privateKey), - "sha1", - ); - - expect(actual.toEncoded()).toEqualBuffer(data); + expect(actual.toEncoded()).toEqualBuffer(new Uint8Array(64)); }); }); }); diff --git a/libs/common/src/key-management/crypto/services/fallback-bulk-encrypt.service.spec.ts b/libs/common/src/key-management/crypto/services/fallback-bulk-encrypt.service.spec.ts deleted file mode 100644 index c016724e652..00000000000 --- a/libs/common/src/key-management/crypto/services/fallback-bulk-encrypt.service.spec.ts +++ /dev/null @@ -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(); - const featureFlagEncryptService = mock(); - const serverConfig = mock(); - - let sut: FallbackBulkEncryptService; - - beforeEach(() => { - sut = new FallbackBulkEncryptService(encryptService); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - describe("decryptItems", () => { - const key = mock(); - 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); - }); - }); -}); diff --git a/libs/common/src/key-management/crypto/services/fallback-bulk-encrypt.service.ts b/libs/common/src/key-management/crypto/services/fallback-bulk-encrypt.service.ts index 7eefa896e6a..0a05bff4422 100644 --- a/libs/common/src/key-management/crypto/services/fallback-bulk-encrypt.service.ts +++ b/libs/common/src/key-management/crypto/services/fallback-bulk-encrypt.service.ts @@ -9,7 +9,7 @@ import { ServerConfig } from "../../../platform/abstractions/config/server-confi 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 { private featureFlagEncryptService: BulkEncryptService; @@ -25,22 +25,10 @@ export class FallbackBulkEncryptService implements BulkEncryptService { items: Decryptable[], key: SymmetricCryptoKey, ): Promise { - if (this.featureFlagEncryptService != null) { - return await this.featureFlagEncryptService.decryptItems(items, key); - } else { - return await this.encryptService.decryptItems(items, key); - } + return await this.encryptService.decryptItems(items, key); } - async setFeatureFlagEncryptService(featureFlagEncryptService: BulkEncryptService) { - if (this.currentServerConfig !== undefined) { - featureFlagEncryptService.onServerConfigChange(this.currentServerConfig); - } - this.featureFlagEncryptService = featureFlagEncryptService; - } + async setFeatureFlagEncryptService(featureFlagEncryptService: BulkEncryptService) {} - onServerConfigChange(newConfig: ServerConfig): void { - this.currentServerConfig = newConfig; - (this.featureFlagEncryptService ?? this.encryptService).onServerConfigChange(newConfig); - } + onServerConfigChange(newConfig: ServerConfig): void {} } diff --git a/libs/common/src/key-management/crypto/services/multithread-encrypt.service.implementation.spec.ts b/libs/common/src/key-management/crypto/services/multithread-encrypt.service.implementation.spec.ts deleted file mode 100644 index bb966c32507..00000000000 --- a/libs/common/src/key-management/crypto/services/multithread-encrypt.service.implementation.spec.ts +++ /dev/null @@ -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(); - const logService = mock(); - const serverConfig = mock(); - - let sut: MultithreadEncryptServiceImplementation; - - beforeEach(() => { - sut = new MultithreadEncryptServiceImplementation(cryptoFunctionService, logService, true); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - describe("decryptItems", () => { - const key = mock(); - const mockWorker = mock(); - - 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[]; - 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>(), mock>()], 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>(), mock>()], 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>(), mock>()], 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(); - (sut as any).worker = mockWorker; - - sut.onServerConfigChange(serverConfig); - - expect(mockWorker.postMessage).toHaveBeenCalledTimes(1); - expect(mockWorker.postMessage).toHaveBeenCalledWith( - buildSetConfigMessage({ newConfig: serverConfig }), - ); - }); - }); -}); diff --git a/libs/common/src/key-management/crypto/services/multithread-encrypt.service.implementation.ts b/libs/common/src/key-management/crypto/services/multithread-encrypt.service.implementation.ts index a3a663f72a9..ab65074af3b 100644 --- a/libs/common/src/key-management/crypto/services/multithread-encrypt.service.implementation.ts +++ b/libs/common/src/key-management/crypto/services/multithread-encrypt.service.implementation.ts @@ -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 { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { getClassInitializer } from "@bitwarden/common/platform/services/cryptography/get-class-initializer"; import { ServerConfig } from "../../../platform/abstractions/config/server-config"; -import { buildDecryptMessage, buildSetConfigMessage } from "../types/worker-command.type"; import { EncryptServiceImplementation } from "./encrypt.service.implementation"; -// TTL (time to live) is not strictly required but avoids tying up memory resources if inactive -const workerTTL = 3 * 60000; // 3 minutes - /** - * @deprecated Replaced by BulkEncryptionService (PM-4154) + * @deprecated Will be deleted in an immediate subsequent PR */ export class MultithreadEncryptServiceImplementation extends EncryptServiceImplementation { - private worker: Worker; - private timeout: any; - private currentServerConfig: ServerConfig | undefined = undefined; - - private clear$ = new Subject(); + protected useSDKForDecryption: boolean = true; /** * Sends items to a web worker to decrypt them. @@ -35,84 +20,8 @@ export class MultithreadEncryptServiceImplementation extends EncryptServiceImple items: Decryptable[], key: SymmetricCryptoKey, ): Promise { - if (items == null || items.length < 1) { - 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) => { - const initializer = getClassInitializer(jsonItem.initializerKey); - return initializer(jsonItem); - }), - ), - takeUntil(this.clear$), - defaultIfEmpty([]), - ), - ); + return await super.decryptItems(items, key); } - 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); - } - } + override onServerConfigChange(newConfig: ServerConfig): void {} } diff --git a/libs/common/src/key-management/crypto/services/web-crypto-function.service.spec.ts b/libs/common/src/key-management/crypto/services/web-crypto-function.service.spec.ts index 0037ba3391d..ae968fc6844 100644 --- a/libs/common/src/key-management/crypto/services/web-crypto-function.service.spec.ts +++ b/libs/common/src/key-management/crypto/services/web-crypto-function.service.spec.ts @@ -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", () => { it("should successfully decrypt data", async () => { const cryptoFunctionService = getWebCryptoFunctionService(); diff --git a/libs/common/src/key-management/crypto/services/web-crypto-function.service.ts b/libs/common/src/key-management/crypto/services/web-crypto-function.service.ts index 32cbd58dde2..80a2a31dea6 100644 --- a/libs/common/src/key-management/crypto/services/web-crypto-function.service.ts +++ b/libs/common/src/key-management/crypto/services/web-crypto-function.service.ts @@ -204,14 +204,6 @@ export class WebCryptoFunctionService implements CryptoFunctionService { return equals; } - async aesEncrypt(data: Uint8Array, iv: Uint8Array, key: Uint8Array): Promise { - 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( data: string, iv: string, diff --git a/libs/common/src/tools/send/services/send.service.ts b/libs/common/src/tools/send/services/send.service.ts index 97470b23697..57463b3b42b 100644 --- a/libs/common/src/tools/send/services/send.service.ts +++ b/libs/common/src/tools/send/services/send.service.ts @@ -89,10 +89,13 @@ export class SendService implements InternalSendServiceAbstraction { } // Key is not a SymmetricCryptoKey, but key material used to derive the cryptoKey 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); + // FIXME: model.notes can be null. encryptString should not be called with null values. send.notes = await this.encryptService.encryptString(model.notes, model.cryptoKey); if (send.type === SendType.Text) { 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.hidden = model.text.hidden; } else if (send.type === SendType.File) { diff --git a/libs/key-management/src/key.service.ts b/libs/key-management/src/key.service.ts index 45aba21c0cf..fca080252f6 100644 --- a/libs/key-management/src/key.service.ts +++ b/libs/key-management/src/key.service.ts @@ -3,10 +3,13 @@ import { NEVER, Observable, combineLatest, + distinctUntilChanged, + filter, firstValueFrom, forkJoin, map, of, + shareReplay, switchMap, } from "rxjs"; @@ -83,6 +86,9 @@ export class DefaultKeyService implements KeyServiceAbstraction { ) { this.activeUserOrgKeys$ = this.stateProvider.activeUserId$.pipe( switchMap((userId) => (userId != null ? this.orgKeys$(userId) : NEVER)), + filter((orgKeys) => orgKeys != null), + distinctUntilChanged(), + shareReplay({ bufferSize: 1, refCount: false }), ) as Observable>; } @@ -402,12 +408,9 @@ export class DefaultKeyService implements KeyServiceAbstraction { } async getOrgKey(orgId: OrganizationId): Promise { - const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$); - if (activeUserId == 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; + return await firstValueFrom( + this.activeUserOrgKeys$.pipe(map((orgKeys) => orgKeys[orgId] ?? null)), + ); } async makeDataEncKey(