From b114034baaa8acf7fbd62365d19017fcd5f1e97f Mon Sep 17 00:00:00 2001 From: "Dalton, Cormac" Date: Mon, 15 Dec 2025 11:51:22 +0000 Subject: [PATCH 1/3] add hmac verification for aes cbc --- lib/mcapi/crypto/jwe-crypto.js | 26 ++++++++ test/jwe-crypto.test.js | 116 +++++++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+) diff --git a/lib/mcapi/crypto/jwe-crypto.js b/lib/mcapi/crypto/jwe-crypto.js index e14e6a9..f865ca0 100644 --- a/lib/mcapi/crypto/jwe-crypto.js +++ b/lib/mcapi/crypto/jwe-crypto.js @@ -25,6 +25,7 @@ function JweCrypto(config) { computePublicFingerprint(config, this.encryptionCertificate); this.encryptedValueFieldName = config.encryptedValueFieldName; + this.enableHmacVerification = config.enableHmacVerification; /** * Perform data encryption @@ -135,6 +136,9 @@ function JweCrypto(config) { decryptionEncoding = "AES-128-CBC"; secretKey = secretKey.slice(16, 32); gcmMode = false; + if(this.enableHmacVerification) { + verifyCbcHmac(jweTokenParts[0], iv, encryptedText, authTag, secretKey); + } break; default: throw new Error( @@ -160,6 +164,28 @@ function JweCrypto(config) { }; } +function verifyCbcHmac(encodedHeaderB64Url, iv, ciphertext, authTag, secretKey) { + const macKey = secretKey.slice(0, 16); + const aad = Buffer.from(encodedHeaderB64Url, c.ASCII); + const al = Buffer.alloc(8); + const aadBits = aad.length * 8; + al.writeUInt32BE(Math.floor(aadBits / Math.pow(2, 32)), 0); + al.writeUInt32BE(aadBits >>> 0, 4); + + const hmac = nodeCrypto.createHmac("sha256", macKey); + hmac.update(aad); + hmac.update(iv); + hmac.update(ciphertext); + hmac.update(al); + const fullTag = hmac.digest(); + + const expectedTag = fullTag.subarray(0, 16); + + if(expectedTag.length !== authTag.length || !nodeCrypto.timingSafeEqual(expectedTag, authTag)) { + throw new Error("Authentication tag verification failed"); + } +} + function serialize(header, encryptedSecretKey, iv, encryptedText, authTag) { return ( header + diff --git a/test/jwe-crypto.test.js b/test/jwe-crypto.test.js index 684f0ea..d57f3d7 100644 --- a/test/jwe-crypto.test.js +++ b/test/jwe-crypto.test.js @@ -2,6 +2,8 @@ const assert = require("assert"); const rewire = require("rewire"); const Crypto = rewire("../lib/mcapi/crypto/jwe-crypto"); const utils = require("../lib/mcapi/utils/utils"); +const c = require("../lib/mcapi/utils/constants"); +const nodeCrypto = require("crypto"); const testConfig = require("./mock/jwe-config"); @@ -257,6 +259,120 @@ describe("JWE Crypto", () => { }); + describe("verifyCbcHmac()", () => { + let encodedHeaderB64Url; + let ciphertext; + let secretKey; + let iv; + let fullTag; + let authTag; + + before(() => { + const headerJson = JSON.stringify({ alg: "RSA-OAEP-256", enc: "A128CBC-HS256" }); + encodedHeaderB64Url = Buffer.from(headerJson, c.UTF8) + .toString(c.BASE64) + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=/g, ""); + + iv = nodeCrypto.randomBytes(16); + ciphertext = nodeCrypto.randomBytes(32); + + const macKey = nodeCrypto.randomBytes(16); + const encKey = nodeCrypto.randomBytes(16); + secretKey = Buffer.concat([macKey, encKey]); + + const aad = Buffer.from(encodedHeaderB64Url, c.ASCII); + const al = Buffer.alloc(8); + const aadBits = aad.length * 8; + al.writeUInt32BE(Math.floor(aadBits / Math.pow(2, 32)), 0); + al.writeUInt32BE(aadBits >>> 0, 4); + + const hmac = nodeCrypto.createHmac("sha256", macKey); + hmac.update(aad); + hmac.update(iv); + hmac.update(ciphertext); + hmac.update(al); + fullTag = hmac.digest(); + authTag = fullTag.slice(0, 16); + }); + + it("should NOT throw when HMAC tag is valid", () => { + const verifyCbcHmac = Crypto.__get__("verifyCbcHmac"); + + assert.doesNotThrow(() => { + verifyCbcHmac(encodedHeaderB64Url, iv, ciphertext, authTag, secretKey); + }); + }); + + it("should throw when HMAC tag is invalid", () => { + const verifyCbcHmac = Crypto.__get__("verifyCbcHmac"); + + const tamperedTag = Buffer.from(authTag); + tamperedTag[0] ^= 0xff; + + assert.throws(() => { + verifyCbcHmac(encodedHeaderB64Url, iv, ciphertext, tamperedTag, secretKey); + }, /Authentication tag verification failed/); + }); + }); + + describe("HMAC verification toggle (A128CBC-HS256)", () => { + let CryptoRewired; + let verifySpy; + let token; + before(() => { + CryptoRewired = rewire("../lib/mcapi/crypto/jwe-crypto"); + verifySpy = { called: false }; + CryptoRewired.__set__("verifyCbcHmac", () => { verifySpy.called = true; }); + + CryptoRewired.__set__("nodeCrypto", { + constants: { RSA_PKCS1_OAEP_PADDING: 4 }, + privateDecrypt: () => { + return Buffer.alloc(32, 1); + }, + createDecipheriv: () => ({ + setAAD: () => {}, + setAuthTag: () => {}, + update: () => "", + final: () => "test" + }) + }); + + const header = Buffer.from(JSON.stringify({ alg: "RSA-OAEP-256", enc: "A128CBC-HS256" }), c.UTF8) + .toString(c.BASE64) + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=/g, ""); + const ek = Buffer.from("ek").toString(c.BASE64).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); + const iv = Buffer.from("1234567890123456").toString(c.BASE64).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); + const ct = Buffer.from("ciphertext").toString(c.BASE64).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); + const tag = Buffer.alloc(16).toString(c.BASE64).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); + token = `${header}.${ek}.${iv}.${ct}.${tag}`; + }); + + it("does NOT call verifyCbcHmac by default", () => { + const cfg = JSON.parse(JSON.stringify(testConfig)); + + const crypto = new CryptoRewired(cfg); + crypto.decryptData(token); + + assert.strictEqual(verifySpy.called, false, "verifyCbcHmac should not be called by default"); + }); + + it("calls verifyCbcHmac when config.enableHmacVerification is true", () => { + const cfg = JSON.parse(JSON.stringify(testConfig)); + cfg.enableHmacVerification = true; + + const crypto = new CryptoRewired(cfg); + crypto.decryptData(token); + + assert.strictEqual(verifySpy.called, true, "verifyCbcHmac should be called when enabled"); + }); + }); + + + describe("#readPublicCertificate", () => { it("not valid key", () => { const readPublicCertificate = Crypto.__get__("readPublicCertificate"); From 3e10b18070d94727e931fb019e9cfd03a56b85a1 Mon Sep 17 00:00:00 2001 From: "Dalton, Cormac" Date: Mon, 15 Dec 2025 12:05:00 +0000 Subject: [PATCH 2/3] update README --- README.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/README.md b/README.md index 2f57f9f..3e8bed6 100644 --- a/README.md +++ b/README.md @@ -259,6 +259,33 @@ const jwe = new clientEncryption.JweEncryption(config); let responsePayload = jwe.decrypt(encryptedResponsePayload); ``` +**AES-CBC HMAC Authentication (A128CBC-HS256)** + +For enhanced security when using AES-CBC mode (A128CBC-HS256), you can enable HMAC authentication tag verification. This ensures data authenticity and integrity according to the JWE specification (RFC 7516). + +By default, HMAC verification is **disabled** for backward compatibility. To enable it: + +```js +config.enableHmacVerification = true; +``` + +**When to enable HMAC verification:** +- ✅ New integrations with systems that properly implement JWE A128CBC-HS256 +- ✅ When security and data authenticity are critical +- ✅ When working with compliant JWE encryption sources + +**When to keep it disabled (default):** +- ⚠️ Legacy systems that don't compute HMAC tags correctly +- ⚠️ Maintaining backward compatibility with existing deployments +- ⚠️ Encryption sources that don't fully follow the JWE specification + +**Technical Details:** +When enabled, the library: +- Splits the 256-bit Content Encryption Key (CEK) into a 128-bit HMAC key and 128-bit AES key +- Computes HMAC-SHA256 over: AAD || IV || Ciphertext || AL (AAD length in bits) +- Verifies the authentication tag (first 128 bits of HMAC output) before decryption +- Throws an `Error` if the authentication tag is invalid + ##### • Configuring the JWE Encryption `JweEncryption` needs a config object to instruct how to decrypt/decrypt the payloads. Example: From 58eb32e770acc6fe34cb6d186030c6a1e2bef59c Mon Sep 17 00:00:00 2001 From: "Dalton, Cormac" Date: Thu, 18 Dec 2025 10:45:26 +0000 Subject: [PATCH 3/3] use correct key --- lib/mcapi/crypto/jwe-crypto.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/mcapi/crypto/jwe-crypto.js b/lib/mcapi/crypto/jwe-crypto.js index f865ca0..69100e6 100644 --- a/lib/mcapi/crypto/jwe-crypto.js +++ b/lib/mcapi/crypto/jwe-crypto.js @@ -133,12 +133,12 @@ function JweCrypto(config) { decryptionEncoding = "AES-256-GCM"; break; case "A128CBC-HS256": - decryptionEncoding = "AES-128-CBC"; - secretKey = secretKey.slice(16, 32); - gcmMode = false; if(this.enableHmacVerification) { verifyCbcHmac(jweTokenParts[0], iv, encryptedText, authTag, secretKey); } + decryptionEncoding = "AES-128-CBC"; + secretKey = secretKey.slice(16, 32); + gcmMode = false; break; default: throw new Error(