Skip to content

Commit b114034

Browse files
committed
add hmac verification for aes cbc
1 parent 6a21112 commit b114034

File tree

2 files changed

+142
-0
lines changed

2 files changed

+142
-0
lines changed

lib/mcapi/crypto/jwe-crypto.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ function JweCrypto(config) {
2525
computePublicFingerprint(config, this.encryptionCertificate);
2626

2727
this.encryptedValueFieldName = config.encryptedValueFieldName;
28+
this.enableHmacVerification = config.enableHmacVerification;
2829

2930
/**
3031
* Perform data encryption
@@ -135,6 +136,9 @@ function JweCrypto(config) {
135136
decryptionEncoding = "AES-128-CBC";
136137
secretKey = secretKey.slice(16, 32);
137138
gcmMode = false;
139+
if(this.enableHmacVerification) {
140+
verifyCbcHmac(jweTokenParts[0], iv, encryptedText, authTag, secretKey);
141+
}
138142
break;
139143
default:
140144
throw new Error(
@@ -160,6 +164,28 @@ function JweCrypto(config) {
160164
};
161165
}
162166

167+
function verifyCbcHmac(encodedHeaderB64Url, iv, ciphertext, authTag, secretKey) {
168+
const macKey = secretKey.slice(0, 16);
169+
const aad = Buffer.from(encodedHeaderB64Url, c.ASCII);
170+
const al = Buffer.alloc(8);
171+
const aadBits = aad.length * 8;
172+
al.writeUInt32BE(Math.floor(aadBits / Math.pow(2, 32)), 0);
173+
al.writeUInt32BE(aadBits >>> 0, 4);
174+
175+
const hmac = nodeCrypto.createHmac("sha256", macKey);
176+
hmac.update(aad);
177+
hmac.update(iv);
178+
hmac.update(ciphertext);
179+
hmac.update(al);
180+
const fullTag = hmac.digest();
181+
182+
const expectedTag = fullTag.subarray(0, 16);
183+
184+
if(expectedTag.length !== authTag.length || !nodeCrypto.timingSafeEqual(expectedTag, authTag)) {
185+
throw new Error("Authentication tag verification failed");
186+
}
187+
}
188+
163189
function serialize(header, encryptedSecretKey, iv, encryptedText, authTag) {
164190
return (
165191
header +

test/jwe-crypto.test.js

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ const assert = require("assert");
22
const rewire = require("rewire");
33
const Crypto = rewire("../lib/mcapi/crypto/jwe-crypto");
44
const utils = require("../lib/mcapi/utils/utils");
5+
const c = require("../lib/mcapi/utils/constants");
6+
const nodeCrypto = require("crypto");
57

68
const testConfig = require("./mock/jwe-config");
79

@@ -257,6 +259,120 @@ describe("JWE Crypto", () => {
257259

258260
});
259261

262+
describe("verifyCbcHmac()", () => {
263+
let encodedHeaderB64Url;
264+
let ciphertext;
265+
let secretKey;
266+
let iv;
267+
let fullTag;
268+
let authTag;
269+
270+
before(() => {
271+
const headerJson = JSON.stringify({ alg: "RSA-OAEP-256", enc: "A128CBC-HS256" });
272+
encodedHeaderB64Url = Buffer.from(headerJson, c.UTF8)
273+
.toString(c.BASE64)
274+
.replace(/\+/g, "-")
275+
.replace(/\//g, "_")
276+
.replace(/=/g, "");
277+
278+
iv = nodeCrypto.randomBytes(16);
279+
ciphertext = nodeCrypto.randomBytes(32);
280+
281+
const macKey = nodeCrypto.randomBytes(16);
282+
const encKey = nodeCrypto.randomBytes(16);
283+
secretKey = Buffer.concat([macKey, encKey]);
284+
285+
const aad = Buffer.from(encodedHeaderB64Url, c.ASCII);
286+
const al = Buffer.alloc(8);
287+
const aadBits = aad.length * 8;
288+
al.writeUInt32BE(Math.floor(aadBits / Math.pow(2, 32)), 0);
289+
al.writeUInt32BE(aadBits >>> 0, 4);
290+
291+
const hmac = nodeCrypto.createHmac("sha256", macKey);
292+
hmac.update(aad);
293+
hmac.update(iv);
294+
hmac.update(ciphertext);
295+
hmac.update(al);
296+
fullTag = hmac.digest();
297+
authTag = fullTag.slice(0, 16);
298+
});
299+
300+
it("should NOT throw when HMAC tag is valid", () => {
301+
const verifyCbcHmac = Crypto.__get__("verifyCbcHmac");
302+
303+
assert.doesNotThrow(() => {
304+
verifyCbcHmac(encodedHeaderB64Url, iv, ciphertext, authTag, secretKey);
305+
});
306+
});
307+
308+
it("should throw when HMAC tag is invalid", () => {
309+
const verifyCbcHmac = Crypto.__get__("verifyCbcHmac");
310+
311+
const tamperedTag = Buffer.from(authTag);
312+
tamperedTag[0] ^= 0xff;
313+
314+
assert.throws(() => {
315+
verifyCbcHmac(encodedHeaderB64Url, iv, ciphertext, tamperedTag, secretKey);
316+
}, /Authentication tag verification failed/);
317+
});
318+
});
319+
320+
describe("HMAC verification toggle (A128CBC-HS256)", () => {
321+
let CryptoRewired;
322+
let verifySpy;
323+
let token;
324+
before(() => {
325+
CryptoRewired = rewire("../lib/mcapi/crypto/jwe-crypto");
326+
verifySpy = { called: false };
327+
CryptoRewired.__set__("verifyCbcHmac", () => { verifySpy.called = true; });
328+
329+
CryptoRewired.__set__("nodeCrypto", {
330+
constants: { RSA_PKCS1_OAEP_PADDING: 4 },
331+
privateDecrypt: () => {
332+
return Buffer.alloc(32, 1);
333+
},
334+
createDecipheriv: () => ({
335+
setAAD: () => {},
336+
setAuthTag: () => {},
337+
update: () => "",
338+
final: () => "test"
339+
})
340+
});
341+
342+
const header = Buffer.from(JSON.stringify({ alg: "RSA-OAEP-256", enc: "A128CBC-HS256" }), c.UTF8)
343+
.toString(c.BASE64)
344+
.replace(/\+/g, "-")
345+
.replace(/\//g, "_")
346+
.replace(/=/g, "");
347+
const ek = Buffer.from("ek").toString(c.BASE64).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
348+
const iv = Buffer.from("1234567890123456").toString(c.BASE64).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
349+
const ct = Buffer.from("ciphertext").toString(c.BASE64).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
350+
const tag = Buffer.alloc(16).toString(c.BASE64).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
351+
token = `${header}.${ek}.${iv}.${ct}.${tag}`;
352+
});
353+
354+
it("does NOT call verifyCbcHmac by default", () => {
355+
const cfg = JSON.parse(JSON.stringify(testConfig));
356+
357+
const crypto = new CryptoRewired(cfg);
358+
crypto.decryptData(token);
359+
360+
assert.strictEqual(verifySpy.called, false, "verifyCbcHmac should not be called by default");
361+
});
362+
363+
it("calls verifyCbcHmac when config.enableHmacVerification is true", () => {
364+
const cfg = JSON.parse(JSON.stringify(testConfig));
365+
cfg.enableHmacVerification = true;
366+
367+
const crypto = new CryptoRewired(cfg);
368+
crypto.decryptData(token);
369+
370+
assert.strictEqual(verifySpy.called, true, "verifyCbcHmac should be called when enabled");
371+
});
372+
});
373+
374+
375+
260376
describe("#readPublicCertificate", () => {
261377
it("not valid key", () => {
262378
const readPublicCertificate = Crypto.__get__("readPublicCertificate");

0 commit comments

Comments
 (0)