From 92127515538772cb7ab5b9156bdb2738a2c1c407 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 4 Oct 2023 16:58:47 -0400 Subject: [PATCH] add support for decrypting AES-ECB mode (#6476) --- .../abstractions/crypto-function.service.ts | 12 +++++- .../encrypt.service.implementation.ts | 5 ++- .../platform/services/encrypt.service.spec.ts | 3 +- .../web-crypto-function.service.spec.ts | 38 +++++++++++++++---- .../services/web-crypto-function.service.ts | 34 ++++++++++++++--- .../node-crypto-function.service.spec.ts | 34 ++++++++++++++--- .../services/node-crypto-function.service.ts | 22 ++++++++--- 7 files changed, 120 insertions(+), 28 deletions(-) diff --git a/libs/common/src/platform/abstractions/crypto-function.service.ts b/libs/common/src/platform/abstractions/crypto-function.service.ts index 9f171872ae5..755eaeb1a75 100644 --- a/libs/common/src/platform/abstractions/crypto-function.service.ts +++ b/libs/common/src/platform/abstractions/crypto-function.service.ts @@ -52,8 +52,16 @@ export abstract class CryptoFunctionService { mac: string, key: SymmetricCryptoKey ) => DecryptParameters; - aesDecryptFast: (parameters: DecryptParameters) => Promise; - aesDecrypt: (data: Uint8Array, iv: Uint8Array, key: Uint8Array) => Promise; + aesDecryptFast: ( + parameters: DecryptParameters, + mode: "cbc" | "ecb" + ) => Promise; + aesDecrypt: ( + data: Uint8Array, + iv: Uint8Array, + key: Uint8Array, + mode: "cbc" | "ecb" + ) => Promise; rsaEncrypt: ( data: Uint8Array, publicKey: Uint8Array, 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 d847dcf77b4..2df24bb33fd 100644 --- a/libs/common/src/platform/services/cryptography/encrypt.service.implementation.ts +++ b/libs/common/src/platform/services/cryptography/encrypt.service.implementation.ts @@ -99,7 +99,7 @@ export class EncryptServiceImplementation implements EncryptService { } } - return await this.cryptoFunctionService.aesDecryptFast(fastParams); + return await this.cryptoFunctionService.aesDecryptFast(fastParams, "cbc"); } async decryptToBytes(encThing: Encrypted, key: SymmetricCryptoKey): Promise { @@ -140,7 +140,8 @@ export class EncryptServiceImplementation implements EncryptService { const result = await this.cryptoFunctionService.aesDecrypt( encThing.dataBytes, encThing.ivBytes, - key.encKey + key.encKey, + "cbc" ); return result ?? null; diff --git a/libs/common/src/platform/services/encrypt.service.spec.ts b/libs/common/src/platform/services/encrypt.service.spec.ts index bc1be50a164..aa3039a4708 100644 --- a/libs/common/src/platform/services/encrypt.service.spec.ts +++ b/libs/common/src/platform/services/encrypt.service.spec.ts @@ -115,7 +115,8 @@ describe("EncryptService", () => { expect(cryptoFunctionService.aesDecrypt).toBeCalledWith( expect.toEqualBuffer(encBuffer.dataBytes), expect.toEqualBuffer(encBuffer.ivBytes), - expect.toEqualBuffer(key.encKey) + expect.toEqualBuffer(key.encKey), + "cbc" ); expect(actual).toEqualBuffer(decryptedBytes); 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 a4b7a3ccaf4..986aecb4c40 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 @@ -3,6 +3,7 @@ import { Substitute } from "@fluffy-spoon/substitute"; import { Utils } from "../../platform/misc/utils"; import { PlatformUtilsService } from "../abstractions/platform-utils.service"; +import { DecryptParameters } from "../models/domain/decrypt-parameters"; import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; import { WebCryptoFunctionService } from "./web-crypto-function.service"; @@ -233,7 +234,7 @@ describe("WebCrypto Function Service", () => { }); }); - describe("aesEncrypt", () => { + describe("aesEncrypt CBC mode", () => { it("should successfully encrypt data", async () => { const cryptoFunctionService = getWebCryptoFunctionService(); const iv = makeStaticByteArray(16); @@ -254,7 +255,7 @@ describe("WebCrypto Function Service", () => { const b64Iv = Utils.fromBufferToB64(iv); const symKey = new SymmetricCryptoKey(key); const params = cryptoFunctionService.aesDecryptFastParameters(encData, b64Iv, null, symKey); - const decValue = await cryptoFunctionService.aesDecryptFast(params); + const decValue = await cryptoFunctionService.aesDecryptFast(params, "cbc"); expect(decValue).toBe(value); }); @@ -265,30 +266,53 @@ describe("WebCrypto Function Service", () => { 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); + const decValue = await cryptoFunctionService.aesDecrypt(encValue, iv, key, "cbc"); expect(Utils.fromBufferToUtf8(decValue)).toBe(value); }); }); - describe("aesDecryptFast", () => { + describe("aesDecryptFast CBC mode", () => { it("should successfully decrypt data", async () => { const cryptoFunctionService = getWebCryptoFunctionService(); const iv = Utils.fromBufferToB64(makeStaticByteArray(16)); const symKey = new SymmetricCryptoKey(makeStaticByteArray(32)); const data = "ByUF8vhyX4ddU9gcooznwA=="; const params = cryptoFunctionService.aesDecryptFastParameters(data, iv, null, symKey); - const decValue = await cryptoFunctionService.aesDecryptFast(params); + const decValue = await cryptoFunctionService.aesDecryptFast(params, "cbc"); expect(decValue).toBe("EncryptMe!"); }); }); - describe("aesDecrypt", () => { + describe("aesDecryptFast ECB mode", () => { + it("should successfully decrypt data", async () => { + const cryptoFunctionService = getWebCryptoFunctionService(); + const key = makeStaticByteArray(32); + const data = Utils.fromB64ToArray("z5q2XSxYCdQFdI+qK2yLlw=="); + const params = new DecryptParameters(); + params.encKey = Utils.fromBufferToByteString(key); + params.data = Utils.fromBufferToByteString(data); + const decValue = await cryptoFunctionService.aesDecryptFast(params, "ecb"); + expect(decValue).toBe("EncryptMe!"); + }); + }); + + describe("aesDecrypt CBC mode", () => { it("should successfully decrypt data", async () => { const cryptoFunctionService = getWebCryptoFunctionService(); const iv = makeStaticByteArray(16); const key = makeStaticByteArray(32); const data = Utils.fromB64ToArray("ByUF8vhyX4ddU9gcooznwA=="); - const decValue = await cryptoFunctionService.aesDecrypt(data, iv, key); + const decValue = await cryptoFunctionService.aesDecrypt(data, iv, key, "cbc"); + expect(Utils.fromBufferToUtf8(decValue)).toBe("EncryptMe!"); + }); + }); + + describe("aesDecrypt ECB mode", () => { + it("should successfully decrypt data", async () => { + const cryptoFunctionService = getWebCryptoFunctionService(); + const key = makeStaticByteArray(32); + const data = Utils.fromB64ToArray("z5q2XSxYCdQFdI+qK2yLlw=="); + const decValue = await cryptoFunctionService.aesDecrypt(data, null, key, "ecb"); expect(Utils.fromBufferToUtf8(decValue)).toBe("EncryptMe!"); }); }); 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 7c52cae543e..93b8ba26664 100644 --- a/libs/common/src/platform/services/web-crypto-function.service.ts +++ b/libs/common/src/platform/services/web-crypto-function.service.ts @@ -273,17 +273,37 @@ export class WebCryptoFunctionService implements CryptoFunctionService { return p; } - aesDecryptFast(parameters: DecryptParameters): Promise { - const dataBuffer = forge.util.createBuffer(parameters.data); - const decipher = forge.cipher.createDecipher("AES-CBC", parameters.encKey); - decipher.start({ iv: parameters.iv }); + 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") { + options.iv = parameters.iv; + } + const dataBuffer = (forge as any).util.createBuffer(parameters.data); + decipher.start(options); decipher.update(dataBuffer); decipher.finish(); const val = decipher.output.toString(); return Promise.resolve(val); } - async aesDecrypt(data: Uint8Array, iv: Uint8Array, key: Uint8Array): Promise { + async aesDecrypt( + data: Uint8Array, + iv: Uint8Array, + key: Uint8Array, + mode: "cbc" | "ecb" + ): Promise { + if (mode === "ecb") { + // Web crypto does not support AES-ECB mode, so we need to do this in forge. + const params = new DecryptParameters(); + params.data = this.toByteString(data); + params.encKey = this.toByteString(key); + const result = await this.aesDecryptFast(params, "ecb"); + return Utils.fromByteStringToArray(result); + } const impKey = await this.subtle.importKey("raw", key, { name: "AES-CBC" } as any, false, [ "decrypt", ]); @@ -411,6 +431,10 @@ export class WebCryptoFunctionService implements CryptoFunctionService { return algorithm === "sha1" ? "SHA-1" : algorithm === "sha256" ? "SHA-256" : "SHA-512"; } + private toWebCryptoAesMode(mode: "cbc" | "ecb"): string { + return mode === "cbc" ? "AES-CBC" : "AES-ECB"; + } + // ref: https://stackoverflow.com/a/47880734/1090359 private checkIfWasmSupported(): boolean { try { 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 051c45712f8..50c75d639a7 100644 --- a/libs/node/src/services/node-crypto-function.service.spec.ts +++ b/libs/node/src/services/node-crypto-function.service.spec.ts @@ -1,4 +1,5 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { DecryptParameters } from "@bitwarden/common/platform/models/domain/decrypt-parameters"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { NodeCryptoFunctionService } from "./node-crypto-function.service"; @@ -164,7 +165,7 @@ describe("NodeCrypto Function Service", () => { testCompare(true); }); - describe("aesEncrypt", () => { + describe("aesEncrypt CBC mode", () => { it("should successfully encrypt data", async () => { const nodeCryptoFunctionService = new NodeCryptoFunctionService(); const iv = makeStaticByteArray(16); @@ -181,30 +182,51 @@ describe("NodeCrypto Function Service", () => { const value = "EncryptMe!"; const data = Utils.fromUtf8ToArray(value); const encValue = await nodeCryptoFunctionService.aesEncrypt(data, iv, key); - const decValue = await nodeCryptoFunctionService.aesDecrypt(encValue, iv, key); + const decValue = await nodeCryptoFunctionService.aesDecrypt(encValue, iv, key, "cbc"); expect(Utils.fromBufferToUtf8(decValue)).toBe(value); }); }); - describe("aesDecryptFast", () => { + describe("aesDecryptFast CBC mode", () => { it("should successfully decrypt data", async () => { const nodeCryptoFunctionService = new NodeCryptoFunctionService(); const iv = Utils.fromBufferToB64(makeStaticByteArray(16)); const symKey = new SymmetricCryptoKey(makeStaticByteArray(32)); const data = "ByUF8vhyX4ddU9gcooznwA=="; const params = nodeCryptoFunctionService.aesDecryptFastParameters(data, iv, null, symKey); - const decValue = await nodeCryptoFunctionService.aesDecryptFast(params); + const decValue = await nodeCryptoFunctionService.aesDecryptFast(params, "cbc"); expect(decValue).toBe("EncryptMe!"); }); }); - describe("aesDecrypt", () => { + describe("aesDecryptFast ECB mode", () => { + it("should successfully decrypt data", async () => { + const nodeCryptoFunctionService = new NodeCryptoFunctionService(); + const params = new DecryptParameters(); + params.encKey = makeStaticByteArray(32); + params.data = Utils.fromB64ToArray("z5q2XSxYCdQFdI+qK2yLlw=="); + const decValue = await nodeCryptoFunctionService.aesDecryptFast(params, "ecb"); + expect(decValue).toBe("EncryptMe!"); + }); + }); + + describe("aesDecrypt CBC mode", () => { it("should successfully decrypt data", async () => { const nodeCryptoFunctionService = new NodeCryptoFunctionService(); const iv = makeStaticByteArray(16); const key = makeStaticByteArray(32); const data = Utils.fromB64ToArray("ByUF8vhyX4ddU9gcooznwA=="); - const decValue = await nodeCryptoFunctionService.aesDecrypt(data, iv, key); + const decValue = await nodeCryptoFunctionService.aesDecrypt(data, iv, key, "cbc"); + expect(Utils.fromBufferToUtf8(decValue)).toBe("EncryptMe!"); + }); + }); + + describe("aesDecrypt ECB mode", () => { + it("should successfully decrypt data", async () => { + const nodeCryptoFunctionService = new NodeCryptoFunctionService(); + const key = makeStaticByteArray(32); + const data = Utils.fromB64ToArray("z5q2XSxYCdQFdI+qK2yLlw=="); + const decValue = await nodeCryptoFunctionService.aesDecrypt(data, null, key, "ecb"); expect(Utils.fromBufferToUtf8(decValue)).toBe("EncryptMe!"); }); }); diff --git a/libs/node/src/services/node-crypto-function.service.ts b/libs/node/src/services/node-crypto-function.service.ts index 9c0e49d00d0..25806352a67 100644 --- a/libs/node/src/services/node-crypto-function.service.ts +++ b/libs/node/src/services/node-crypto-function.service.ts @@ -189,16 +189,24 @@ export class NodeCryptoFunctionService implements CryptoFunctionService { return p; } - async aesDecryptFast(parameters: DecryptParameters): Promise { - const decBuf = await this.aesDecrypt(parameters.data, parameters.iv, parameters.encKey); + async aesDecryptFast( + parameters: DecryptParameters, + mode: "cbc" | "ecb" + ): Promise { + const decBuf = await this.aesDecrypt(parameters.data, parameters.iv, parameters.encKey, mode); return Utils.fromBufferToUtf8(decBuf); } - aesDecrypt(data: Uint8Array, iv: Uint8Array, key: Uint8Array): Promise { + aesDecrypt( + data: Uint8Array, + iv: Uint8Array, + key: Uint8Array, + mode: "cbc" | "ecb" + ): Promise { const nodeData = this.toNodeBuffer(data); - const nodeIv = this.toNodeBuffer(iv); + const nodeIv = mode === "ecb" ? null : this.toNodeBuffer(iv); const nodeKey = this.toNodeBuffer(key); - const decipher = crypto.createDecipheriv("aes-256-cbc", nodeKey, nodeIv); + const decipher = crypto.createDecipheriv(this.toNodeCryptoAesMode(mode), nodeKey, nodeIv); const decBuf = Buffer.concat([decipher.update(nodeData), decipher.final()]); return Promise.resolve(this.toUint8Buffer(decBuf)); } @@ -326,4 +334,8 @@ export class NodeCryptoFunctionService implements CryptoFunctionService { const publicKey = forge.pki.publicKeyFromAsn1(asn1); return forge.pki.publicKeyToPem(publicKey); } + + private toNodeCryptoAesMode(mode: "cbc" | "ecb"): string { + return mode === "cbc" ? "aes-256-cbc" : "aes-256-ecb"; + } }