Skip to content

Commit 3df6254

Browse files
To align with Mastercard’s encryption policy requiring authenticated encryption for data at rest, all CaaS clients currently using AES in CBC mode must migrate to AES-CBC + HMAC. From this release forward that support will be added, and will be optional to ensure backward compatibility with the existing clients.
1 parent f91ae60 commit 3df6254

File tree

9 files changed

+340
-6
lines changed

9 files changed

+340
-6
lines changed

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,37 @@ JweConfig config = JweConfigBuilder.aJweEncryptionConfig()
178178
.build();
179179
```
180180

181+
**AES-CBC HMAC Authentication (A128CBC-HS256)**
182+
183+
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).
184+
185+
By default, HMAC verification is **disabled** for backward compatibility. To enable it:
186+
187+
```java
188+
JweConfig config = JweConfigBuilder.aJweEncryptionConfig()
189+
.withEncryptionCertificate(encryptionCertificate)
190+
.withDecryptionKey(decryptionKey)
191+
.withEnableCbcHmacVerification(true) // Enable HMAC authentication
192+
.build();
193+
```
194+
195+
**When to enable HMAC verification:**
196+
- ✅ New integrations with systems that properly implement JWE A128CBC-HS256
197+
- ✅ When security and data authenticity are critical
198+
- ✅ When working with compliant JWE encryption sources
199+
200+
**When to keep it disabled (default):**
201+
- ⚠️ Legacy systems that don't compute HMAC tags correctly
202+
- ⚠️ Maintaining backward compatibility with existing deployments
203+
- ⚠️ Encryption sources that don't fully follow the JWE specification
204+
205+
**Technical Details:**
206+
When enabled, the library:
207+
- Splits the 256-bit Content Encryption Key (CEK) into a 128-bit HMAC key and 128-bit AES key
208+
- Computes HMAC-SHA256 over: AAD || IV || Ciphertext || AL (AAD length in bits)
209+
- Verifies the authentication tag (first 128 bits of HMAC output) before decryption
210+
- Throws an `EncryptionException` if the authentication tag is invalid
211+
181212
##### • Performing JWE Encryption <a name="performing-jwe-encryption"></a>
182213

183214
Call `JweEncryption.encryptPayload` with a JSON request payload and a `JweConfig` instance.

src/main/java/com/mastercard/developer/encryption/EncryptionConfig.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,14 @@ public enum Scheme {
5353

5454
Integer ivSize = 16;
5555

56+
/**
57+
* Enable HMAC authentication tag verification for AES-CBC mode (A128CBC-HS256).
58+
* When true, authentication tags are verified during decryption.
59+
* Default is false for backward compatibility with systems that don't compute HMAC tags.
60+
* Set to true to enable proper HMAC verification according to JWE spec.
61+
*/
62+
Boolean enableCbcHmacVerification = false;
63+
5664
/**
5765
* A list of JSON paths to encrypt in request payloads.
5866
* Example:
@@ -116,4 +124,6 @@ String getEncryptedValueFieldName() {
116124
}
117125

118126
public Integer getIVSize() { return ivSize; }
127+
128+
public Boolean getEnableCbcHmacVerification() { return enableCbcHmacVerification; }
119129
}

src/main/java/com/mastercard/developer/encryption/EncryptionConfigBuilder.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ abstract class EncryptionConfigBuilder {
2424
protected String encryptedValueFieldName;
2525

2626
protected Integer ivSize = 16;
27+
protected Boolean enableCbcHmacVerification = false;
2728

2829
void computeEncryptionKeyFingerprintWhenNeeded() throws EncryptionException {
2930
try {

src/main/java/com/mastercard/developer/encryption/JweConfigBuilder.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public JweConfig build() throws EncryptionException {
3434
config.encryptedValueFieldName = this.encryptedValueFieldName == null ? "encryptedData" : this.encryptedValueFieldName;
3535
config.scheme = EncryptionConfig.Scheme.JWE;
3636
config.ivSize = ivSize;
37+
config.enableCbcHmacVerification = enableCbcHmacVerification;
3738
return config;
3839
}
3940

@@ -105,6 +106,17 @@ public JweConfigBuilder withEncryptionIVSize(Integer ivSize) {
105106
}
106107
throw new IllegalArgumentException("Supported IV Sizes are either 12 or 16!");
107108
}
109+
/**
110+
* See: {@link EncryptionConfig#enableCbcHmacVerification}.
111+
* Enable or disable HMAC authentication tag verification for AES-CBC mode (A128CBC-HS256).
112+
* Default is false (disabled) for backward compatibility.
113+
* Set to true to enable proper HMAC verification according to JWE spec.
114+
*/
115+
public JweConfigBuilder withEnableCbcHmacVerification(Boolean enableCbcHmacVerification) {
116+
this.enableCbcHmacVerification = enableCbcHmacVerification;
117+
return this;
118+
}
119+
108120

109121
private void checkParameterValues() {
110122
if (decryptionKey == null && encryptionCertificate == null && encryptionKey == null) {
Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,84 @@
11
package com.mastercard.developer.encryption.aes;
22

3+
import com.mastercard.developer.encryption.EncryptionException;
34
import com.mastercard.developer.encryption.jwe.JweObject;
5+
import com.mastercard.developer.utils.ByteUtils;
46
import com.mastercard.developer.utils.EncodingUtils;
57

68
import javax.crypto.Cipher;
9+
import javax.crypto.Mac;
710
import javax.crypto.spec.IvParameterSpec;
811
import javax.crypto.spec.SecretKeySpec;
12+
import java.nio.ByteBuffer;
13+
import java.nio.charset.StandardCharsets;
914
import java.security.GeneralSecurityException;
1015
import java.security.Key;
16+
import java.security.MessageDigest;
1117
import java.security.spec.AlgorithmParameterSpec;
1218

1319
public class AESCBC {
1420

1521
private AESCBC() {
1622
}
1723

18-
private static final String CYPHER = "AES/CBC/PKCS5Padding";
24+
private static final String CIPHER = "AES/CBC/PKCS5Padding";
25+
private static final String HMAC_ALGORITHM = "HmacSHA256";
1926

2027
@java.lang.SuppressWarnings("squid:S3329")
21-
public static byte[] decrypt(Key secretKey, JweObject object) throws GeneralSecurityException {
22-
// First 16 bytes are the MAC key, so we only use the second 16 bytes
23-
SecretKeySpec aesKey = new SecretKeySpec(secretKey.getEncoded(), 16, 16, "AES");
28+
public static byte[] decrypt(Key secretKey, JweObject object, boolean enableHmacVerification) throws GeneralSecurityException, EncryptionException {
29+
byte[] cek = secretKey.getEncoded();
30+
31+
// For A128CBC-HS256: First 16 bytes are HMAC key, second 16 bytes are AES key
32+
int keyLength = cek.length / 2;
33+
SecretKeySpec aesKey = new SecretKeySpec(cek, keyLength, keyLength, "AES");
34+
2435
byte[] cipherText = EncodingUtils.base64Decode(object.getCipherText());
2536
byte[] iv = EncodingUtils.base64Decode(object.getIv());
2637

38+
// Only verify authentication tag if enabled
39+
if (enableHmacVerification) {
40+
SecretKeySpec hmacKey = new SecretKeySpec(cek, 0, keyLength, HMAC_ALGORITHM);
41+
byte[] authTag = EncodingUtils.base64Decode(object.getAuthTag());
42+
byte[] aad = object.getRawHeader().getBytes(StandardCharsets.US_ASCII);
43+
44+
byte[] expectedTag = computeAuthTag(hmacKey, aad, iv, cipherText, keyLength);
45+
if (!MessageDigest.isEqual(authTag, expectedTag)) {
46+
throw new EncryptionException("Authentication tag verification failed");
47+
}
48+
}
49+
2750
return cipher(aesKey, new IvParameterSpec(iv), cipherText, Cipher.DECRYPT_MODE);
2851
}
2952

3053
public static byte[] cipher(Key key, AlgorithmParameterSpec iv, byte[] bytes, int mode) throws GeneralSecurityException {
31-
Cipher cipher = Cipher.getInstance(CYPHER);
54+
Cipher cipher = Cipher.getInstance(CIPHER);
3255
cipher.init(mode, key, iv);
3356
return cipher.doFinal(bytes);
3457
}
58+
59+
/**
60+
* Computes the authentication tag for AES-CBC-HMAC-SHA2
61+
* HMAC is computed over: AAD || IV || Ciphertext || AL
62+
* where AL is the length of AAD in bits expressed as a 64-bit big-endian integer
63+
*/
64+
private static byte[] computeAuthTag(SecretKeySpec hmacKey, byte[] aad, byte[] iv, byte[] cipherText, int tagLength)
65+
throws GeneralSecurityException {
66+
Mac mac = Mac.getInstance(HMAC_ALGORITHM);
67+
mac.init(hmacKey);
68+
69+
// Compute AL (AAD Length in bits as 64-bit big-endian)
70+
long aadLengthBits = (long) aad.length * 8;
71+
byte[] al = ByteBuffer.allocate(8).putLong(aadLengthBits).array();
72+
73+
// HMAC input: AAD || IV || Ciphertext || AL
74+
mac.update(aad);
75+
mac.update(iv);
76+
mac.update(cipherText);
77+
mac.update(al);
78+
79+
byte[] hmacOutput = mac.doFinal();
80+
81+
// Return first half (tagLength bytes) as the authentication tag
82+
return ByteUtils.subArray(hmacOutput, 0, tagLength);
83+
}
3584
}

src/main/java/com/mastercard/developer/encryption/jwe/JweObject.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public String decrypt(JweConfig config) throws EncryptionException, GeneralSecur
4949
if (AES_GCM_ENCRYPTION_METHODS.contains(encryptionMethod)) {
5050
plainText = AESGCM.decrypt(cek, this);
5151
} else if (encryptionMethod.equals(A128CBC_HS256)) {
52-
plainText = AESCBC.decrypt(cek, this);
52+
plainText = AESCBC.decrypt(cek, this, config.getEnableCbcHmacVerification());
5353
} else {
5454
throw new EncryptionException(String.format("Encryption method %s not supported", encryptionMethod));
5555
}

src/test/java/com/mastercard/developer/encryption/JweConfigBuilderTest.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,35 @@ public void testBuild_ShouldThrowIllegalArgumentException_WhenNotHavingWildcardO
193193
.build();
194194
}
195195

196+
@Test
197+
public void testBuild_ShouldDisableCbcHmacVerificationByDefault() throws Exception {
198+
JweConfig config = JweConfigBuilder.aJweEncryptionConfig()
199+
.withEncryptionCertificate(TestUtils.getTestEncryptionCertificate())
200+
.withDecryptionKey(TestUtils.getTestDecryptionKey())
201+
.build();
202+
Assert.assertFalse("HMAC verification should be disabled by default for backward compatibility", config.getEnableCbcHmacVerification());
203+
}
204+
205+
@Test
206+
public void testBuild_ShouldAllowDisablingCbcHmacVerification() throws Exception {
207+
JweConfig config = JweConfigBuilder.aJweEncryptionConfig()
208+
.withEncryptionCertificate(TestUtils.getTestEncryptionCertificate())
209+
.withDecryptionKey(TestUtils.getTestDecryptionKey())
210+
.withEnableCbcHmacVerification(false)
211+
.build();
212+
Assert.assertFalse("HMAC verification should be disabled when explicitly set to false", config.getEnableCbcHmacVerification());
213+
}
214+
215+
@Test
216+
public void testBuild_ShouldAllowEnablingCbcHmacVerificationExplicitly() throws Exception {
217+
JweConfig config = JweConfigBuilder.aJweEncryptionConfig()
218+
.withEncryptionCertificate(TestUtils.getTestEncryptionCertificate())
219+
.withDecryptionKey(TestUtils.getTestDecryptionKey())
220+
.withEnableCbcHmacVerification(true)
221+
.build();
222+
Assert.assertTrue("HMAC verification should be enabled when explicitly set to true", config.getEnableCbcHmacVerification());
223+
}
224+
196225
@Test
197226
public void testBuild_ShouldThrowIllegalArgumentException_WhenMultipleWildcardsOnEncryptionPaths() throws Exception {
198227
expectedException.expect(IllegalArgumentException.class);

0 commit comments

Comments
 (0)