diff --git a/libs/common/src/platform/abstractions/crypto-function.service.ts b/libs/common/src/platform/abstractions/crypto-function.service.ts index 3597c208c3c..2f506cb8765 100644 --- a/libs/common/src/platform/abstractions/crypto-function.service.ts +++ b/libs/common/src/platform/abstractions/crypto-function.service.ts @@ -46,6 +46,20 @@ export abstract class CryptoFunctionService { ): Promise; abstract compareFast(a: Uint8Array | string, b: Uint8Array | string): Promise; abstract aesEncrypt(data: Uint8Array, iv: Uint8Array, key: Uint8Array): Promise; + /** + * encrypt data using AES-256-GCM. + * + * @param data data to encrypt + * @param iv the iv to use for encryption + * @param key the key to use for encryption + * @returns the encrypted data in the form of data + tag + iv + */ + abstract aesGcmEncrypt( + data: Uint8Array, + iv: Uint8Array, + key: Uint8Array, + additionalData?: Uint8Array, + ): Promise; abstract aesDecryptFastParameters( data: string, iv: string, @@ -53,11 +67,10 @@ export abstract class CryptoFunctionService { key: SymmetricCryptoKey, ): DecryptParameters; /** - * Decrypts AES encrypted data using Forge in the web. Available modes are CBC, ECB, and GCM. + * Decrypts AES encrypted data using Forge in the web. Available modes are CBC and ECB. * - * GCM mode supports only GCM 256 with a 12 byte IV and a 16 byte tag. * - * @param data the data to decrypt. For CBC and ECB mode, this should be the ciphertext. For GCM mode, this should be the ciphertext + tag. + * @param data the data to decrypt. * @param iv the initialization vector to use for decryption * @param key the key to use for decryption * @param mode the mode to use for decryption @@ -65,7 +78,8 @@ export abstract class CryptoFunctionService { abstract aesDecryptFast( parameters: DecryptParameters, - mode: "cbc" | "ecb" | "gcm", + mode: "cbc" | "ecb", + additionalData?: Uint8Array, ): Promise; /** * Decrypts AES encrypted data. Available modes are CBC, ECB, and GCM. @@ -76,12 +90,14 @@ export abstract class CryptoFunctionService { * @param iv the initialization vector to use for decryption * @param key the key to use for decryption * @param mode the mode to use for decryption + * @param additionalData additional data to use for decryption in GCM mode. Ignored for CBC and ECB mode. */ abstract aesDecrypt( data: Uint8Array, iv: Uint8Array, key: Uint8Array, mode: "cbc" | "ecb" | "gcm", + additionalData?: Uint8Array, ): Promise; abstract rsaEncrypt( data: Uint8Array, diff --git a/libs/common/src/platform/abstractions/encrypt.service.ts b/libs/common/src/platform/abstractions/encrypt.service.ts index dd6457d14b7..2cea0c8c78d 100644 --- a/libs/common/src/platform/abstractions/encrypt.service.ts +++ b/libs/common/src/platform/abstractions/encrypt.service.ts @@ -23,8 +23,28 @@ export abstract class EncryptService { * * @param data The encrypted data + tag + iv * @param key The key to decrypt the data + * @param additionalData Additional data to authenticate */ - abstract aesGcmDecryptToBytes(data: Uint8Array, key: Uint8Array): Promise; + abstract aesGcmDecryptToBytes( + data: Uint8Array, + key: Uint8Array, + additionalData?: Uint8Array, + ): Promise; + /** + * Encrypts data using AES-256-GCM. + * + * @remarks this is currently used only for Key Connector communications. Do not use for general encryption. + * + * @param data data to encrypt + * @param key key to encrypt with + * @param additionalData additional data to authenticate + * @returns the encrypted data in the form of data + tag + iv + */ + abstract aesGcmEncryptToBytes( + data: Uint8Array, + key: Uint8Array, + additionalData?: Uint8Array, + ): Promise; abstract rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise; abstract rsaDecrypt(data: EncString, privateKey: Uint8Array): Promise; abstract resolveLegacyKey(key: SymmetricCryptoKey, encThing: Encrypted): SymmetricCryptoKey; diff --git a/libs/common/src/platform/services/cryptography/encrypt.service.implementation.ts b/libs/common/src/platform/services/cryptography/encrypt.service.implementation.ts index fb481d45386..f60c504187b 100644 --- a/libs/common/src/platform/services/cryptography/encrypt.service.implementation.ts +++ b/libs/common/src/platform/services/cryptography/encrypt.service.implementation.ts @@ -126,7 +126,28 @@ export class EncryptServiceImplementation implements EncryptService { return await this.cryptoFunctionService.aesDecryptFast(fastParams, "cbc"); } - async aesGcmDecryptToBytes(data: Uint8Array, key: Uint8Array): Promise { + async aesGcmEncryptToBytes( + data: Uint8Array, + key: Uint8Array, + additionalData?: Uint8Array, + ): Promise { + if (key == null) { + throw new Error("No encryption key provided."); + } + + if (data == null) { + throw new Error("Nothing provided for encryption."); + } + + const iv = await this.cryptoFunctionService.randomBytes(12); + return await this.cryptoFunctionService.aesGcmEncrypt(data, iv, key, additionalData); + } + + async aesGcmDecryptToBytes( + data: Uint8Array, + key: Uint8Array, + additionalData?: Uint8Array, + ): Promise { if (key == null) { throw new Error("No encryption key provided."); } @@ -140,7 +161,7 @@ export class EncryptServiceImplementation implements EncryptService { const iv = data.slice(-12); // aesDecrypt expects cipher + tag, but iv split - return await this.cryptoFunctionService.aesDecrypt(dataAndTag, iv, key, "gcm"); + return await this.cryptoFunctionService.aesDecrypt(dataAndTag, iv, key, "gcm", additionalData); } async decryptToBytes(encThing: Encrypted, key: SymmetricCryptoKey): Promise { diff --git a/libs/common/src/platform/services/encrypt.service.spec.ts b/libs/common/src/platform/services/encrypt.service.spec.ts index bd97b24594a..4741b575ca7 100644 --- a/libs/common/src/platform/services/encrypt.service.spec.ts +++ b/libs/common/src/platform/services/encrypt.service.spec.ts @@ -82,38 +82,96 @@ describe("EncryptService", () => { }); }); - describe("aesGcmDecryptToBytes", () => { + describe("aesGcm", () => { const data = makeStaticByteArray(10, 100); const key = makeStaticByteArray(32, 200); + const iv = makeStaticByteArray(12, 300); const encryptedData = new Uint8Array(1); beforeEach(() => { + cryptoFunctionService.randomBytes.calledWith(12).mockResolvedValueOnce(iv as CsprngArray); + cryptoFunctionService.aesGcmEncrypt.mockResolvedValue(encryptedData); cryptoFunctionService.aesDecrypt.mockResolvedValue(data); }); - it("throws if no data is provided", () => { - return expect(encryptService.aesGcmDecryptToBytes(null, key)).rejects.toThrow( - "Nothing provided for decryption", - ); + describe("aesGcmEncryptToBytes", () => { + it("throws if no data is provided", () => { + return expect(encryptService.aesGcmEncryptToBytes(null, key)).rejects.toThrow( + "Nothing provided for encryption", + ); + }); + + it("throws if no key is provided", () => { + return expect(encryptService.aesGcmEncryptToBytes(data, null)).rejects.toThrow( + "No encryption key", + ); + }); + + it("encrypts data with provided key", async () => { + const actual = await encryptService.aesGcmEncryptToBytes(data, key); + + expect(cryptoFunctionService.aesGcmEncrypt).toHaveBeenCalledWith( + expect.toEqualBuffer(data), + expect.toEqualBuffer(iv), + expect.toEqualBuffer(key), + undefined, + ); + + expect(actual).toEqualBuffer(encryptedData); + }); + + it("encrypts data with the provided additional data", async () => { + const additionalData = makeStaticByteArray(10, 400); + await encryptService.aesGcmEncryptToBytes(data, key, additionalData); + + expect(cryptoFunctionService.aesGcmEncrypt).toHaveBeenCalledWith( + expect.toEqualBuffer(data), + expect.toEqualBuffer(iv), + expect.toEqualBuffer(key), + expect.toEqualBuffer(additionalData), + ); + }); }); - it("throws if no key is provided", () => { - return expect(encryptService.aesGcmDecryptToBytes(encryptedData, null)).rejects.toThrow( - "No encryption key", - ); - }); + describe("aesGcmDecryptToBytes", () => { + it("throws if no data is provided", () => { + return expect(encryptService.aesGcmDecryptToBytes(null, key)).rejects.toThrow( + "Nothing provided for decryption", + ); + }); - it("strips off the tag and decrypts data with provided key and iv", async () => { - const actual = await encryptService.aesGcmDecryptToBytes(encryptedData, key); + it("throws if no key is provided", () => { + return expect(encryptService.aesGcmDecryptToBytes(encryptedData, null)).rejects.toThrow( + "No encryption key", + ); + }); - expect(cryptoFunctionService.aesDecrypt).toHaveBeenCalledWith( - expect.toEqualBuffer(encryptedData.slice(0, -12)), - expect.toEqualBuffer(encryptedData.slice(-12)), - expect.toEqualBuffer(key), - "gcm", - ); + it("strips off the tag and decrypts data with provided key and iv", async () => { + const actual = await encryptService.aesGcmDecryptToBytes(encryptedData, key); - expect(actual).toEqualBuffer(data); + expect(cryptoFunctionService.aesDecrypt).toHaveBeenCalledWith( + expect.toEqualBuffer(encryptedData.slice(0, -12)), + expect.toEqualBuffer(encryptedData.slice(-12)), + expect.toEqualBuffer(key), + "gcm", + undefined, + ); + + expect(actual).toEqualBuffer(data); + }); + + it("decrypts data with the provided additional data", async () => { + const additionalData = makeStaticByteArray(10, 400); + await encryptService.aesGcmDecryptToBytes(encryptedData, key, additionalData); + + expect(cryptoFunctionService.aesDecrypt).toHaveBeenCalledWith( + expect.toEqualBuffer(encryptedData.slice(0, -12)), + expect.toEqualBuffer(encryptedData.slice(-12)), + expect.toEqualBuffer(key), + "gcm", + expect.toEqualBuffer(additionalData), + ); + }); }); }); diff --git a/libs/common/src/platform/services/web-crypto-function.service.spec.ts b/libs/common/src/platform/services/web-crypto-function.service.spec.ts index 022684e213d..269e0162c89 100644 --- a/libs/common/src/platform/services/web-crypto-function.service.spec.ts +++ b/libs/common/src/platform/services/web-crypto-function.service.spec.ts @@ -309,6 +309,21 @@ describe("WebCrypto Function Service", () => { const decValue = await cryptoFunctionService.aesDecrypt(envValue, iv, key, "gcm"); expect(Utils.fromBufferToUtf8(decValue)).toBe(value); }); + + it("should successfully encrypt and then decrypt data with aad", async () => { + const cryptoFunctionService = getWebCryptoFunctionService(); + const iv = makeStaticByteArray(12); + const key = makeStaticByteArray(32); + const value = "EncryptMe!"; + const data = Utils.fromUtf8ToArray(value); + const aad = Utils.fromUtf8ToArray("aad"); + const encAndIv = new Uint8Array( + await cryptoFunctionService.aesGcmEncrypt(data, iv, key, aad), + ); + const envValue = encAndIv.slice(0, encAndIv.length - 12); + const decValue = await cryptoFunctionService.aesDecrypt(envValue, iv, key, "gcm", aad); + expect(Utils.fromBufferToUtf8(decValue)).toBe(value); + }); }); describe("aesDecryptFast CBC mode", () => { @@ -336,17 +351,6 @@ describe("WebCrypto Function Service", () => { }); }); - describe("aesDecryptFast GCM mode", () => { - it("should successfully decrypt data", async () => { - const cryptoFunctionService = getWebCryptoFunctionService(); - const iv = makeStaticByteArray(12); - const key = makeStaticByteArray(32); - const data = Utils.fromB64ToArray("Amy1abyVtlboYFBtLnDAzAwAgb3Qg2m4fMo="); - const decValue = await cryptoFunctionService.aesDecrypt(data, iv, key, "gcm"); - expect(Utils.fromBufferToUtf8(decValue)).toBe("EncryptMe!"); - }); - }); - describe("aesDecrypt CBC mode", () => { it("should successfully decrypt data", async () => { const cryptoFunctionService = getWebCryptoFunctionService(); diff --git a/libs/common/src/platform/services/web-crypto-function.service.ts b/libs/common/src/platform/services/web-crypto-function.service.ts index c760bb2b3ee..2e2fb54cd04 100644 --- a/libs/common/src/platform/services/web-crypto-function.service.ts +++ b/libs/common/src/platform/services/web-crypto-function.service.ts @@ -293,16 +293,13 @@ export class WebCryptoFunctionService implements CryptoFunctionService { return p; } - aesDecryptFast( - parameters: DecryptParameters, - mode: "cbc" | "ecb" | "gcm", - ): Promise { + aesDecryptFast(parameters: DecryptParameters, mode: "cbc" | "ecb"): Promise { const decipher = (forge as any).cipher.createDecipher( this.toWebCryptoAesMode(mode), parameters.encKey, ); const options = {} as any; - if (mode === "cbc" || mode === "gcm") { + if (mode === "cbc") { options.iv = parameters.iv; } const dataBuffer = (forge as any).util.createBuffer(parameters.data); @@ -318,6 +315,7 @@ export class WebCryptoFunctionService implements CryptoFunctionService { iv: Uint8Array, key: Uint8Array, mode: "cbc" | "ecb" | "gcm", + additionalData?: Uint8Array, ): Promise { switch (mode) { case "ecb": { @@ -337,11 +335,15 @@ export class WebCryptoFunctionService implements CryptoFunctionService { false, ["decrypt"], ); - const buffer = await this.subtle.decrypt( - { name: this.toWebCryptoAesMode(mode), iv: iv }, - impKey, - data, - ); + const parameters: AesGcmParams | AesCbcParams = { + name: this.toWebCryptoAesMode(mode), + iv: iv, + }; + if (mode === "gcm") { + (parameters as AesGcmParams).tagLength = 128; + (parameters as AesGcmParams).additionalData = additionalData; + } + const buffer = await this.subtle.decrypt(parameters, impKey, data); return new Uint8Array(buffer); } } diff --git a/libs/node/src/services/node-crypto-function.service.spec.ts b/libs/node/src/services/node-crypto-function.service.spec.ts index a6feeba3a57..2be2b5039c2 100644 --- a/libs/node/src/services/node-crypto-function.service.spec.ts +++ b/libs/node/src/services/node-crypto-function.service.spec.ts @@ -226,6 +226,21 @@ describe("NodeCrypto Function Service", () => { const decValue = await cryptoFunctionService.aesDecrypt(envValue, iv, key, "gcm"); expect(Utils.fromBufferToUtf8(decValue)).toBe(value); }); + + it("should successfully encrypt and then decrypt data with aad", async () => { + const cryptoFunctionService = new NodeCryptoFunctionService(); + const iv = makeStaticByteArray(12); + const key = makeStaticByteArray(32); + const value = "EncryptMe!"; + const data = Utils.fromUtf8ToArray(value); + const aad = Utils.fromUtf8ToArray("aad"); + const encAndIv = new Uint8Array( + await cryptoFunctionService.aesGcmEncrypt(data, iv, key, aad), + ); + const envValue = encAndIv.slice(0, encAndIv.length - 12); + const decValue = await cryptoFunctionService.aesDecrypt(envValue, iv, key, "gcm", aad); + expect(Utils.fromBufferToUtf8(decValue)).toBe(value); + }); }); describe("aesDecryptFast CBC mode", () => { @@ -251,18 +266,6 @@ describe("NodeCrypto Function Service", () => { }); }); - describe("aesDecryptFast GCM mode", () => { - it("successfully decrypts data", async () => { - const nodeCryptoFunctionService = new NodeCryptoFunctionService(); - const iv = Utils.fromBufferToB64(makeStaticByteArray(12)); - const symKey = new SymmetricCryptoKey(makeStaticByteArray(32)); - const data = "Amy1abyVtlboYFBtLnDAzAwAgb3Qg2m4fMo="; - const params = nodeCryptoFunctionService.aesDecryptFastParameters(data, iv, null, symKey); - const decValue = await nodeCryptoFunctionService.aesDecryptFast(params, "gcm"); - expect(decValue).toBe("EncryptMe!"); - }); - }); - describe("aesDecrypt CBC mode", () => { it("should successfully decrypt data", async () => { const nodeCryptoFunctionService = new NodeCryptoFunctionService(); diff --git a/libs/node/src/services/node-crypto-function.service.ts b/libs/node/src/services/node-crypto-function.service.ts index 3225d1d2808..a40492055ff 100644 --- a/libs/node/src/services/node-crypto-function.service.ts +++ b/libs/node/src/services/node-crypto-function.service.ts @@ -210,7 +210,7 @@ export class NodeCryptoFunctionService implements CryptoFunctionService { async aesDecryptFast( parameters: DecryptParameters, - mode: "cbc" | "ecb" | "gcm", + mode: "cbc" | "ecb", ): Promise { const decBuf = await this.aesDecrypt(parameters.data, parameters.iv, parameters.encKey, mode); return Utils.fromBufferToUtf8(decBuf); @@ -221,15 +221,22 @@ export class NodeCryptoFunctionService implements CryptoFunctionService { iv: Uint8Array, key: Uint8Array, mode: "cbc" | "ecb" | "gcm", + additionalData?: Uint8Array, ): Promise { const nodeData = mode !== "gcm" ? this.toNodeBuffer(data) : this.toNodeBuffer(data.slice(0, -16)); // remove gcm tag const nodeIv = mode === "ecb" ? null : this.toNodeBuffer(iv); const nodeKey = this.toNodeBuffer(key); const decipher = crypto.createDecipheriv(this.toNodeCryptoAesMode(mode), nodeKey, nodeIv); + if (mode === "gcm") { (decipher as crypto.DecipherGCM).setAuthTag(data.slice(-16)); } + if (additionalData != null && mode === "gcm") { + const nodeAdditionalData = this.toNodeBuffer(additionalData); + (decipher as crypto.DecipherGCM).setAAD(nodeAdditionalData); + } + const decBuf = Buffer.concat([decipher.update(nodeData), decipher.final()]); return Promise.resolve(this.toUint8Buffer(decBuf)); }