From 3694903a2a738be7ae86310a2dcd70d97761926c Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 29 Apr 2025 17:04:47 +0200 Subject: [PATCH] [PM-20567] Add new encrypt service functions (#14398) * Add new encrypt service functions * Undo changes * Cleanup * Fix build * Fix comments --- .../crypto/abstractions/encrypt.service.ts | 141 ++++++++++++++---- .../encrypt.service.implementation.ts | 94 ++++++++---- .../crypto/services/encrypt.service.spec.ts | 92 ++++++++++++ 3 files changed, 271 insertions(+), 56 deletions(-) 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 5f21d86bc6a..7a6c9bcd800 100644 --- a/libs/common/src/key-management/crypto/abstractions/encrypt.service.ts +++ b/libs/common/src/key-management/crypto/abstractions/encrypt.service.ts @@ -8,17 +8,99 @@ import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-cr 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 + * @param encString - The EncString to decrypt + * @param key - The key to decrypt the EncString with + * @param decryptTrace - A string to identify the context of the object being decrypted. This can include: field name, encryption type, cipher id, key type, but should not include + * sensitive information like encryption keys or data. This is used for logging when decryption errors occur in order to identify what failed to decrypt + * @returns The decrypted string + */ + abstract decryptToUtf8( + encString: EncString, + key: SymmetricCryptoKey, + decryptTrace?: string, + ): Promise; + /** + * @deprecated + * Decrypts an Encrypted object to a Uint8Array + * @param encThing - The Encrypted object to decrypt + * @param key - The key to decrypt the Encrypted object with + * @param decryptTrace - A string to identify the context of the object being decrypted. This can include: field name, encryption type, cipher id, key type, but should not include + * sensitive information like encryption keys or data. This is used for logging when decryption errors occur in order to identify what failed to decrypt + * @returns The decrypted Uint8Array + */ + abstract decryptToBytes( + encThing: Encrypted, + key: SymmetricCryptoKey, + decryptTrace?: string, + ): Promise; + /** + * @deprecated Replaced by BulkEncryptService, remove once the feature is tested and the featureflag PM-4154-multi-worker-encryption-service is removed + * @param items The items to decrypt + * @param key The key to decrypt the items with + */ + abstract decryptItems( + items: Decryptable[], + key: SymmetricCryptoKey, + ): Promise; + + /** + * Encrypts a string to an EncString + * @param plainValue - The value to encrypt + * @param key - The key to encrypt the value with + */ + abstract encryptString(plainValue: string, key: SymmetricCryptoKey): Promise; + /** + * Encrypts bytes to an EncString + * @param plainValue - The value to encrypt + * @param key - The key to encrypt the value with + * @deprecated Bytes are not the right abstraction to encrypt in. Use e.g. key wrapping or file encryption instead + */ + abstract encryptBytes(plainValue: Uint8Array, key: SymmetricCryptoKey): Promise; + /** + * Encrypts a value to a Uint8Array + * @param plainValue - The value to encrypt + * @param key - The key to encrypt the value with + */ + abstract encryptFileData( + plainValue: Uint8Array, + key: SymmetricCryptoKey, + ): Promise; + + /** + * Decrypts an EncString to a string + * @param encString - The EncString containing the encrypted string. + * @param key - The key to decrypt the value with + */ + abstract decryptString(encString: EncString, key: SymmetricCryptoKey): Promise; + /** + * Decrypts an EncString to a Uint8Array + * @param encString - The EncString containing the encrypted bytes. + * @param key - The key to decrypt the value with + * @deprecated Bytes are not the right abstraction to encrypt in. Use e.g. key wrapping or file encryption instead + */ + abstract decryptBytes(encString: EncString, key: SymmetricCryptoKey): Promise; + /** + * Decrypts an EncArrayBuffer to a Uint8Array + * @param encBuffer - The EncArrayBuffer containing the encrypted file bytes. + * @param key - The key to decrypt the value with + */ + abstract decryptFileData(encBuffer: EncArrayBuffer, key: SymmetricCryptoKey): Promise; /** * Wraps a decapsulation key (Private key) with a symmetric key @@ -52,31 +134,35 @@ export abstract class EncryptService { ): Promise; /** - * Decrypts an EncString to a string - * @param encString - The EncString to decrypt - * @param key - The key to decrypt the EncString with - * @param decryptTrace - A string to identify the context of the object being decrypted. This can include: field name, encryption type, cipher id, key type, but should not include - * sensitive information like encryption keys or data. This is used for logging when decryption errors occur in order to identify what failed to decrypt - * @returns The decrypted string + * Unwraps a decapsulation key (Private key) with a symmetric key + * @see {@link https://en.wikipedia.org/wiki/Key_wrap} + * @param decapsulationKeyPcks8 - The private key in PKCS8 format + * @param wrappingKey - The symmetric key to wrap the private key with */ - abstract decryptToUtf8( - encString: EncString, - key: SymmetricCryptoKey, - decryptTrace?: string, - ): Promise; + abstract unwrapDecapsulationKey( + wrappedDecapsulationKey: EncString, + wrappingKey: SymmetricCryptoKey, + ): Promise; /** - * Decrypts an Encrypted object to a Uint8Array - * @param encThing - The Encrypted object to decrypt - * @param key - The key to decrypt the Encrypted object with - * @param decryptTrace - A string to identify the context of the object being decrypted. This can include: field name, encryption type, cipher id, key type, but should not include - * sensitive information like encryption keys or data. This is used for logging when decryption errors occur in order to identify what failed to decrypt - * @returns The decrypted Uint8Array + * Wraps an encapsulation key (Public key) with a symmetric key + * @see {@link https://en.wikipedia.org/wiki/Key_wrap} + * @param encapsulationKeySpki - The public key in SPKI format + * @param wrappingKey - The symmetric key to wrap the public key with */ - abstract decryptToBytes( - encThing: Encrypted, - key: SymmetricCryptoKey, - decryptTrace?: string, - ): Promise; + abstract unwrapEncapsulationKey( + wrappedEncapsulationKey: EncString, + wrappingKey: SymmetricCryptoKey, + ): Promise; + /** + * Unwraps a symmetric key with another symmetric key + * @see {@link https://en.wikipedia.org/wiki/Key_wrap} + * @param keyToBeWrapped - The symmetric key to wrap + * @param wrappingKey - The symmetric key to wrap the encapsulated key with + */ + abstract unwrapSymmetricKey( + keyToBeUnwrapped: EncString, + wrappingKey: SymmetricCryptoKey, + ): Promise; /** * Encapsulates a symmetric key with an asymmetric public key @@ -100,6 +186,7 @@ export abstract class EncryptService { encryptedSharedKey: EncString, decapsulationKey: Uint8Array, ): Promise; + /** * @deprecated Use @see {@link encapsulateKeyUnsigned} instead * @param data - The data to encrypt @@ -112,15 +199,7 @@ export abstract class EncryptService { * @param privateKey - The privateKey to decrypt with */ abstract rsaDecrypt(data: EncString, privateKey: Uint8Array): Promise; - /** - * @deprecated Replaced by BulkEncryptService, remove once the feature is tested and the featureflag PM-4154-multi-worker-encryption-service is removed - * @param items The items to decrypt - * @param key The key to decrypt the items with - */ - abstract decryptItems( - items: Decryptable[], - key: SymmetricCryptoKey, - ): Promise; + /** * Generates a base64-encoded hash of the given value * @param value The value to hash 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 addcc978c23..fb0649f7c5b 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 @@ -36,31 +36,29 @@ export class EncryptServiceImplementation implements EncryptService { protected logMacFailures: boolean, ) {} - // Handle updating private properties to turn on/off feature flags. - onServerConfigChange(newConfig: ServerConfig): void { - this.blockType0 = getFeatureFlagValue(newConfig, FeatureFlag.PM17987_BlockType0); + // 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); } - async encrypt(plainValue: string | Uint8Array, key: SymmetricCryptoKey): Promise { - if (key == null) { - throw new Error("No encryption key provided."); - } + async encryptBytes(plainValue: Uint8Array, key: SymmetricCryptoKey): Promise { + return this.encrypt(plainValue, key); + } - if (this.blockType0) { - if (key.inner().type === EncryptionType.AesCbc256_B64) { - throw new Error("Type 0 encryption is not supported."); - } - } + async encryptFileData(plainValue: Uint8Array, key: SymmetricCryptoKey): Promise { + return this.encryptToBytes(plainValue, key); + } - if (plainValue == null) { - return Promise.resolve(null); - } + async decryptString(encString: EncString, key: SymmetricCryptoKey): Promise { + return this.decryptToUtf8(encString, key); + } - if (typeof plainValue === "string") { - return this.encryptUint8Array(Utils.fromUtf8ToArray(plainValue), key); - } else { - return this.encryptUint8Array(plainValue, key); - } + async decryptBytes(encString: EncString, key: SymmetricCryptoKey): Promise { + return this.decryptToBytes(encString, key); + } + + async decryptFileData(encBuffer: EncArrayBuffer, key: SymmetricCryptoKey): Promise { + return this.decryptToBytes(encBuffer, key); } async wrapDecapsulationKey( @@ -108,6 +106,57 @@ export class EncryptServiceImplementation implements EncryptService { return await this.encryptUint8Array(keyToBeWrapped.toEncoded(), wrappingKey); } + async unwrapDecapsulationKey( + wrappedDecapsulationKey: EncString, + wrappingKey: SymmetricCryptoKey, + ): Promise { + return this.decryptBytes(wrappedDecapsulationKey, wrappingKey); + } + async unwrapEncapsulationKey( + wrappedEncapsulationKey: EncString, + wrappingKey: SymmetricCryptoKey, + ): Promise { + return this.decryptBytes(wrappedEncapsulationKey, wrappingKey); + } + async unwrapSymmetricKey( + keyToBeUnwrapped: EncString, + wrappingKey: SymmetricCryptoKey, + ): Promise { + return new SymmetricCryptoKey(await this.decryptBytes(keyToBeUnwrapped, wrappingKey)); + } + + async hash(value: string | Uint8Array, algorithm: "sha1" | "sha256" | "sha512"): Promise { + const hashArray = await this.cryptoFunctionService.hash(value, algorithm); + return Utils.fromBufferToB64(hashArray); + } + + // Handle updating private properties to turn on/off feature flags. + onServerConfigChange(newConfig: ServerConfig): void { + 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, @@ -339,11 +388,6 @@ export class EncryptServiceImplementation implements EncryptService { return results; } - async hash(value: string | Uint8Array, algorithm: "sha1" | "sha256" | "sha512"): Promise { - const hashArray = await this.cryptoFunctionService.hash(value, algorithm); - return Utils.fromBufferToB64(hashArray); - } - private async aesEncrypt(data: Uint8Array, key: Aes256CbcHmacKey): Promise { const obj = new EncryptedObject(); obj.iv = await this.cryptoFunctionService.randomBytes(16); 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 dd0b909f7fb..d19de6c0414 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 @@ -538,6 +538,98 @@ describe("EncryptService", () => { }); }); + describe("encryptString", () => { + it("is a proxy to encrypt", async () => { + const key = new SymmetricCryptoKey(makeStaticByteArray(64)); + const plainValue = "data"; + encryptService.encrypt = jest.fn(); + await encryptService.encryptString(plainValue, key); + expect(encryptService.encrypt).toHaveBeenCalledWith(plainValue, key); + }); + }); + + describe("encryptBytes", () => { + it("is a proxy to encrypt", async () => { + const key = new SymmetricCryptoKey(makeStaticByteArray(64)); + const plainValue = makeStaticByteArray(16, 1); + encryptService.encrypt = jest.fn(); + await encryptService.encryptBytes(plainValue, key); + expect(encryptService.encrypt).toHaveBeenCalledWith(plainValue, key); + }); + }); + + describe("encryptFileData", () => { + it("is a proxy to encryptToBytes", async () => { + const key = new SymmetricCryptoKey(makeStaticByteArray(64)); + const plainValue = makeStaticByteArray(16, 1); + encryptService.encryptToBytes = jest.fn(); + await encryptService.encryptFileData(plainValue, key); + expect(encryptService.encryptToBytes).toHaveBeenCalledWith(plainValue, key); + }); + }); + + describe("decryptString", () => { + it("is a proxy to decryptToUtf8", async () => { + const key = new SymmetricCryptoKey(makeStaticByteArray(64)); + const encString = new EncString(EncryptionType.AesCbc256_B64, "data"); + encryptService.decryptToUtf8 = jest.fn(); + await encryptService.decryptString(encString, key); + expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encString, key); + }); + }); + + describe("decryptBytes", () => { + it("is a proxy to decryptToBytes", async () => { + const key = new SymmetricCryptoKey(makeStaticByteArray(64)); + const encString = new EncString(EncryptionType.AesCbc256_B64, "data"); + encryptService.decryptToBytes = jest.fn(); + await encryptService.decryptBytes(encString, key); + expect(encryptService.decryptToBytes).toHaveBeenCalledWith(encString, key); + }); + }); + + describe("decryptFileData", () => { + it("is a proxy to decrypt", async () => { + const key = new SymmetricCryptoKey(makeStaticByteArray(64)); + const encString = new EncArrayBuffer(makeStaticByteArray(60, EncryptionType.AesCbc256_B64)); + encryptService.decryptToBytes = jest.fn(); + await encryptService.decryptFileData(encString, key); + expect(encryptService.decryptToBytes).toHaveBeenCalledWith(encString, key); + }); + }); + + describe("unwrapDecapsulationKey", () => { + it("is a proxy to decryptBytes", async () => { + const key = new SymmetricCryptoKey(makeStaticByteArray(64)); + const encString = new EncString(EncryptionType.AesCbc256_B64, "data"); + encryptService.decryptBytes = jest.fn(); + await encryptService.unwrapDecapsulationKey(encString, key); + expect(encryptService.decryptBytes).toHaveBeenCalledWith(encString, key); + }); + }); + + describe("unwrapEncapsulationKey", () => { + it("is a proxy to decryptBytes", async () => { + const key = new SymmetricCryptoKey(makeStaticByteArray(64)); + const encString = new EncString(EncryptionType.AesCbc256_B64, "data"); + encryptService.decryptBytes = jest.fn(); + await encryptService.unwrapEncapsulationKey(encString, key); + expect(encryptService.decryptBytes).toHaveBeenCalledWith(encString, key); + }); + }); + + describe("unwrapSymmetricKey", () => { + it("is a proxy to decryptBytes", async () => { + const key = new SymmetricCryptoKey(makeStaticByteArray(64)); + const encString = new EncString(EncryptionType.AesCbc256_B64, "data"); + const jestFn = jest.fn(); + jestFn.mockResolvedValue(new Uint8Array(64)); + encryptService.decryptBytes = jestFn; + await encryptService.unwrapSymmetricKey(encString, key); + expect(encryptService.decryptBytes).toHaveBeenCalledWith(encString, key); + }); + }); + describe("rsa", () => { const data = makeStaticByteArray(64, 100); const testKey = new SymmetricCryptoKey(data);