Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,37 @@ JweConfig config = JweConfigBuilder.aJweEncryptionConfig()
.build();
```

**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:

```java
JweConfig config = JweConfigBuilder.aJweEncryptionConfig()
.withEncryptionCertificate(encryptionCertificate)
.withDecryptionKey(decryptionKey)
.withEnableCbcHmacVerification(true) // Enable HMAC authentication
.build();
```

**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 `EncryptionException` if the authentication tag is invalid

##### • Performing JWE Encryption <a name="performing-jwe-encryption"></a>

Call `JweEncryption.encryptPayload` with a JSON request payload and a `JweConfig` instance.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ public enum Scheme {

Integer ivSize = 16;

/**
* Enable HMAC authentication tag verification for AES-CBC mode (A128CBC-HS256).
* When true, authentication tags are verified during decryption.
* Default is false for backward compatibility with systems that don't compute HMAC tags.
* Set to true to enable proper HMAC verification according to JWE spec.
*/
Boolean enableCbcHmacVerification = false;

/**
* A list of JSON paths to encrypt in request payloads.
* Example:
Expand Down Expand Up @@ -116,4 +124,6 @@ String getEncryptedValueFieldName() {
}

public Integer getIVSize() { return ivSize; }

public Boolean getEnableCbcHmacVerification() { return enableCbcHmacVerification; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ abstract class EncryptionConfigBuilder {
protected String encryptedValueFieldName;

protected Integer ivSize = 16;
protected Boolean enableCbcHmacVerification = false;

void computeEncryptionKeyFingerprintWhenNeeded() throws EncryptionException {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public JweConfig build() throws EncryptionException {
config.encryptedValueFieldName = this.encryptedValueFieldName == null ? "encryptedData" : this.encryptedValueFieldName;
config.scheme = EncryptionConfig.Scheme.JWE;
config.ivSize = ivSize;
config.enableCbcHmacVerification = enableCbcHmacVerification;
return config;
}

Expand Down Expand Up @@ -105,6 +106,17 @@ public JweConfigBuilder withEncryptionIVSize(Integer ivSize) {
}
throw new IllegalArgumentException("Supported IV Sizes are either 12 or 16!");
}
/**
* See: {@link EncryptionConfig#enableCbcHmacVerification}.
* Enable or disable HMAC authentication tag verification for AES-CBC mode (A128CBC-HS256).
* Default is false (disabled) for backward compatibility.
* Set to true to enable proper HMAC verification according to JWE spec.
*/
public JweConfigBuilder withEnableCbcHmacVerification(Boolean enableCbcHmacVerification) {
this.enableCbcHmacVerification = enableCbcHmacVerification;
return this;
}


private void checkParameterValues() {
if (decryptionKey == null && encryptionCertificate == null && encryptionKey == null) {
Expand Down
59 changes: 54 additions & 5 deletions src/main/java/com/mastercard/developer/encryption/aes/AESCBC.java
Original file line number Diff line number Diff line change
@@ -1,35 +1,84 @@
package com.mastercard.developer.encryption.aes;

import com.mastercard.developer.encryption.EncryptionException;
import com.mastercard.developer.encryption.jwe.JweObject;
import com.mastercard.developer.utils.ByteUtils;
import com.mastercard.developer.utils.EncodingUtils;

import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.MessageDigest;
import java.security.spec.AlgorithmParameterSpec;

public class AESCBC {

private AESCBC() {
}

private static final String CYPHER = "AES/CBC/PKCS5Padding";
private static final String CIPHER = "AES/CBC/PKCS5Padding";
private static final String HMAC_ALGORITHM = "HmacSHA256";

@java.lang.SuppressWarnings("squid:S3329")
public static byte[] decrypt(Key secretKey, JweObject object) throws GeneralSecurityException {
// First 16 bytes are the MAC key, so we only use the second 16 bytes
SecretKeySpec aesKey = new SecretKeySpec(secretKey.getEncoded(), 16, 16, "AES");
public static byte[] decrypt(Key secretKey, JweObject object, boolean enableHmacVerification) throws GeneralSecurityException, EncryptionException {
byte[] cek = secretKey.getEncoded();

// For A128CBC-HS256: First 16 bytes are HMAC key, second 16 bytes are AES key
int keyLength = cek.length / 2;
Comment on lines +31 to +32
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should confirm that cek.length == 32 before using it. The rest of the calculations could work in unexpected ways of cek.length is not 32.

Suggested change
// For A128CBC-HS256: First 16 bytes are HMAC key, second 16 bytes are AES key
int keyLength = cek.length / 2;
if(cek.length != 32) {
throw new IllegalArgumentException("CEK should be of length 32")
}
// For A128CBC-HS256: First 16 bytes are HMAC key, second 16 bytes are AES key
int keyLength = cek.length / 2;

SecretKeySpec aesKey = new SecretKeySpec(cek, keyLength, keyLength, "AES");

byte[] cipherText = EncodingUtils.base64Decode(object.getCipherText());
byte[] iv = EncodingUtils.base64Decode(object.getIv());

// Only verify authentication tag if enabled
if (enableHmacVerification) {
SecretKeySpec hmacKey = new SecretKeySpec(cek, 0, keyLength, HMAC_ALGORITHM);
byte[] authTag = EncodingUtils.base64Decode(object.getAuthTag());
byte[] aad = object.getRawHeader().getBytes(StandardCharsets.US_ASCII);

byte[] expectedTag = computeAuthTag(hmacKey, aad, iv, cipherText, keyLength);
if (!MessageDigest.isEqual(authTag, expectedTag)) {
throw new EncryptionException("Authentication tag verification failed");
}
}

return cipher(aesKey, new IvParameterSpec(iv), cipherText, Cipher.DECRYPT_MODE);
}

public static byte[] cipher(Key key, AlgorithmParameterSpec iv, byte[] bytes, int mode) throws GeneralSecurityException {
Cipher cipher = Cipher.getInstance(CYPHER);
Cipher cipher = Cipher.getInstance(CIPHER);
cipher.init(mode, key, iv);
return cipher.doFinal(bytes);
}

/**
* Computes the authentication tag for AES-CBC-HMAC-SHA2
* HMAC is computed over: AAD || IV || Ciphertext || AL
* where AL is the length of AAD in bits expressed as a 64-bit big-endian integer
*/
private static byte[] computeAuthTag(SecretKeySpec hmacKey, byte[] aad, byte[] iv, byte[] cipherText, int tagLength)
throws GeneralSecurityException {
Mac mac = Mac.getInstance(HMAC_ALGORITHM);
mac.init(hmacKey);

// Compute AL (AAD Length in bits as 64-bit big-endian)
long aadLengthBits = (long) aad.length * 8;
byte[] al = ByteBuffer.allocate(8).putLong(aadLengthBits).array();

// HMAC input: AAD || IV || Ciphertext || AL
mac.update(aad);
mac.update(iv);
mac.update(cipherText);
mac.update(al);

byte[] hmacOutput = mac.doFinal();

// Return first half (tagLength bytes) as the authentication tag
return ByteUtils.subArray(hmacOutput, 0, tagLength);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public String decrypt(JweConfig config) throws EncryptionException, GeneralSecur
if (AES_GCM_ENCRYPTION_METHODS.contains(encryptionMethod)) {
plainText = AESGCM.decrypt(cek, this);
} else if (encryptionMethod.equals(A128CBC_HS256)) {
plainText = AESCBC.decrypt(cek, this);
plainText = AESCBC.decrypt(cek, this, config.getEnableCbcHmacVerification());
} else {
throw new EncryptionException(String.format("Encryption method %s not supported", encryptionMethod));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,35 @@ public void testBuild_ShouldThrowIllegalArgumentException_WhenNotHavingWildcardO
.build();
}

@Test
public void testBuild_ShouldDisableCbcHmacVerificationByDefault() throws Exception {
JweConfig config = JweConfigBuilder.aJweEncryptionConfig()
.withEncryptionCertificate(TestUtils.getTestEncryptionCertificate())
.withDecryptionKey(TestUtils.getTestDecryptionKey())
.build();
Assert.assertFalse("HMAC verification should be disabled by default for backward compatibility", config.getEnableCbcHmacVerification());
}

@Test
public void testBuild_ShouldAllowDisablingCbcHmacVerification() throws Exception {
JweConfig config = JweConfigBuilder.aJweEncryptionConfig()
.withEncryptionCertificate(TestUtils.getTestEncryptionCertificate())
.withDecryptionKey(TestUtils.getTestDecryptionKey())
.withEnableCbcHmacVerification(false)
.build();
Assert.assertFalse("HMAC verification should be disabled when explicitly set to false", config.getEnableCbcHmacVerification());
}

@Test
public void testBuild_ShouldAllowEnablingCbcHmacVerificationExplicitly() throws Exception {
JweConfig config = JweConfigBuilder.aJweEncryptionConfig()
.withEncryptionCertificate(TestUtils.getTestEncryptionCertificate())
.withDecryptionKey(TestUtils.getTestDecryptionKey())
.withEnableCbcHmacVerification(true)
.build();
Assert.assertTrue("HMAC verification should be enabled when explicitly set to true", config.getEnableCbcHmacVerification());
}

@Test
public void testBuild_ShouldThrowIllegalArgumentException_WhenMultipleWildcardsOnEncryptionPaths() throws Exception {
expectedException.expect(IllegalArgumentException.class);
Expand Down
Loading