diff --git a/Mastercard.Developer.ClientEncryption.Core/Encryption/AES/AesCbc.cs b/Mastercard.Developer.ClientEncryption.Core/Encryption/AES/AesCbc.cs
index 6351420..3cb498a 100644
--- a/Mastercard.Developer.ClientEncryption.Core/Encryption/AES/AesCbc.cs
+++ b/Mastercard.Developer.ClientEncryption.Core/Encryption/AES/AesCbc.cs
@@ -1,28 +1,66 @@
using System;
using System.IO;
+using System.Linq;
using System.Security.Cryptography;
+using System.Text;
using Mastercard.Developer.ClientEncryption.Core.Utils;
using Mastercard.Developer.ClientEncryption.Core.Encryption.JWE;
namespace Mastercard.Developer.ClientEncryption.Core.Encryption.AES
{
+ internal class AesCbcAuthenticated
+ {
+ public byte[] Ciphertext { get; private set; }
+ public byte[] AuthTag { get; private set; }
+
+ internal AesCbcAuthenticated(byte[] ciphertext, byte[] authTag)
+ {
+ Ciphertext = ciphertext;
+ AuthTag = authTag;
+ }
+ }
+
internal static class AesCbc
{
- public static byte[] Decrypt(byte[] secretKeyBytes, JweObject jweObject)
+ public static byte[] Decrypt(byte[] secretKeyBytes, JweObject jweObject, bool enableHmacVerification)
{
- // Extract the encryption key
- byte[] aesKey = new byte[16];
- Array.Copy(secretKeyBytes, 16, aesKey, 0, aesKey.Length);
+ // Determine key sizes based on the total secret key length
+ // A128CBC-HS256: 32 bytes (16 for HMAC, 16 for AES)
+ // A192CBC-HS384: 48 bytes (24 for HMAC, 24 for AES)
+ // A256CBC-HS512: 64 bytes (32 for HMAC, 32 for AES)
+ int keyLength = secretKeyBytes.Length / 2;
+
+ // Extract HMAC key (first half) and encryption key (second half)
+ byte[] hmacKey = new byte[keyLength];
+ byte[] aesKey = new byte[keyLength];
+ Array.Copy(secretKeyBytes, 0, hmacKey, 0, keyLength);
+ Array.Copy(secretKeyBytes, keyLength, aesKey, 0, keyLength);
+ // Decode values needed for both HMAC and decryption
+ byte[] authTag = Base64Utils.URLDecode(jweObject.AuthTag);
+ byte[] iv = Base64Utils.URLDecode(jweObject.Iv);
+ byte[] ciphertext = Base64Utils.URLDecode(jweObject.CipherText);
+
+ // Verify HMAC only if enabled
+ if (enableHmacVerification)
+ {
+ byte[] aad = Encoding.ASCII.GetBytes(jweObject.RawHeader);
+
+ if (!VerifyHmac(hmacKey, aad, iv, ciphertext, authTag))
+ {
+ throw new EncryptionException("HMAC verification failed");
+ }
+ }
+
+ // Decrypt
byte[] plaintext;
using (var aes = Aes.Create())
{
aes.Key = aesKey;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
- aes.IV = Base64Utils.URLDecode(jweObject.Iv);
+ aes.IV = iv;
- byte[] ciphertext = Base64Utils.URLDecode(jweObject.CipherText);
using (var decryptor = aes.CreateDecryptor())
{
using (var memoryStream = new MemoryStream(ciphertext))
@@ -43,5 +81,120 @@ public static byte[] Decrypt(byte[] secretKeyBytes, JweObject jweObject)
}
return plaintext;
}
+
+ internal static AesCbcAuthenticated Encrypt(byte[] secretKeyBytes, byte[] iv, byte[] plaintext, byte[] aad, bool enableHmacGeneration)
+ {
+ // Determine key sizes based on the total secret key length
+ int keyLength = secretKeyBytes.Length / 2;
+
+ // Extract HMAC key (first half) and encryption key (second half)
+ byte[] hmacKey = new byte[keyLength];
+ byte[] aesKey = new byte[keyLength];
+ Array.Copy(secretKeyBytes, 0, hmacKey, 0, keyLength);
+ Array.Copy(secretKeyBytes, keyLength, aesKey, 0, keyLength);
+
+ // Encrypt
+ byte[] ciphertext;
+ using (var aes = Aes.Create())
+ {
+ aes.Key = aesKey;
+ aes.Mode = CipherMode.CBC;
+ aes.Padding = PaddingMode.PKCS7;
+ aes.IV = iv;
+
+ using (var encryptor = aes.CreateEncryptor())
+ {
+ using (var memoryStream = new MemoryStream())
+ {
+ using (var cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write))
+ {
+ cryptoStream.Write(plaintext, 0, plaintext.Length);
+ cryptoStream.FlushFinalBlock();
+ ciphertext = memoryStream.ToArray();
+ }
+ }
+ }
+ }
+
+ // Compute HMAC only if enabled
+ byte[] authTag;
+ if (enableHmacGeneration)
+ {
+ byte[] fullHmac = ComputeHmac(hmacKey, aad, iv, ciphertext);
+ // Truncate to half the length for the authentication tag (same as keyLength)
+ authTag = new byte[keyLength];
+ Array.Copy(fullHmac, 0, authTag, 0, keyLength);
+ }
+ else
+ {
+ authTag = new byte[0]; // Empty auth tag when HMAC is disabled
+ }
+
+ return new AesCbcAuthenticated(ciphertext, authTag);
+ }
+
+ private static bool VerifyHmac(byte[] hmacKey, byte[] aad, byte[] iv, byte[] ciphertext, byte[] authTag)
+ {
+ byte[] expectedTag = ComputeHmac(hmacKey, aad, iv, ciphertext);
+
+ // Truncate to half the length for the authentication tag
+ int tagLength = hmacKey.Length;
+ byte[] truncatedExpectedTag = new byte[tagLength];
+ Array.Copy(expectedTag, 0, truncatedExpectedTag, 0, tagLength);
+
+ // Constant-time comparison
+ if (authTag.Length != truncatedExpectedTag.Length)
+ {
+ return false;
+ }
+
+ int result = 0;
+ for (int i = 0; i < authTag.Length; i++)
+ {
+ result |= authTag[i] ^ truncatedExpectedTag[i];
+ }
+
+ return result == 0;
+ }
+
+ private static byte[] ComputeHmac(byte[] hmacKey, byte[] aad, byte[] iv, byte[] ciphertext)
+ {
+ // Construct Additional Authenticated Data (AAD) length in bits as 64-bit big-endian
+ long aadLengthBits = (long)aad.Length * 8;
+ byte[] aadLength = BitConverter.GetBytes(aadLengthBits);
+ if (BitConverter.IsLittleEndian)
+ {
+ Array.Reverse(aadLength);
+ }
+
+ // Concatenate: AAD || IV || Ciphertext || AAD Length
+ var hmacInput = new MemoryStream();
+ hmacInput.Write(aad, 0, aad.Length);
+ hmacInput.Write(iv, 0, iv.Length);
+ hmacInput.Write(ciphertext, 0, ciphertext.Length);
+ hmacInput.Write(aadLength, 0, aadLength.Length);
+
+ // Determine HMAC algorithm based on key length
+ HMAC hmac;
+ switch (hmacKey.Length)
+ {
+ case 16: // HS256
+ hmac = new HMACSHA256(hmacKey);
+ break;
+ case 24: // HS384
+ hmac = new HMACSHA384(hmacKey);
+ break;
+ case 32: // HS512
+ hmac = new HMACSHA512(hmacKey);
+ break;
+ default:
+ throw new EncryptionException($"Unsupported HMAC key length: {hmacKey.Length}");
+ }
+
+ using (hmac)
+ {
+ return hmac.ComputeHash(hmacInput.ToArray());
+ }
+ }
}
}
diff --git a/Mastercard.Developer.ClientEncryption.Core/Encryption/JWE/JweObject.cs b/Mastercard.Developer.ClientEncryption.Core/Encryption/JWE/JweObject.cs
index 1480fea..e2892ca 100644
--- a/Mastercard.Developer.ClientEncryption.Core/Encryption/JWE/JweObject.cs
+++ b/Mastercard.Developer.ClientEncryption.Core/Encryption/JWE/JweObject.cs
@@ -10,6 +10,8 @@ namespace Mastercard.Developer.ClientEncryption.Core.Encryption.JWE
internal class JweObject
{
private const string A128CBC_HS256 = "A128CBC-HS256";
+ private const string A192CBC_HS384 = "A192CBC-HS384";
+ private const string A256CBC_HS512 = "A256CBC-HS512";
private const string A256GCM = "A256GCM";
private const string A128GCM = "A128GCM";
private const string A192GCM = "A192GCM";
@@ -44,7 +46,13 @@ public string Decrypt(JweConfig config)
plaintext = AesGcm.Decrypt(unwrappedKey, this);
break;
case A128CBC_HS256:
- plaintext = AesCbc.Decrypt(unwrappedKey, this);
+ plaintext = AesCbc.Decrypt(unwrappedKey, this, config.EnableCbcHmacVerification);
+ break;
+ case A192CBC_HS384:
+ plaintext = AesCbc.Decrypt(unwrappedKey, this, config.EnableCbcHmacVerification);
+ break;
+ case A256CBC_HS512:
+ plaintext = AesCbc.Decrypt(unwrappedKey, this, config.EnableCbcHmacVerification);
break;
default:
throw new EncryptionException($"Encryption method {encryptionMethod} is not supported");
@@ -54,19 +62,69 @@ public string Decrypt(JweConfig config)
public static string Encrypt(JweConfig config, string payload, JweHeader header)
{
- var cek = AesEncryption.GenerateCek(256);
+ var encryptionMethod = header.Enc;
+
+ // Determine CEK size based on encryption method
+ int cekSize;
+ bool isCbcHmac = false;
+ switch (encryptionMethod)
+ {
+ case A128CBC_HS256:
+ cekSize = 256; // 128-bit AES + 128-bit HMAC
+ isCbcHmac = true;
+ break;
+ case A192CBC_HS384:
+ cekSize = 384; // 192-bit AES + 192-bit HMAC
+ isCbcHmac = true;
+ break;
+ case A256CBC_HS512:
+ cekSize = 512; // 256-bit AES + 256-bit HMAC
+ isCbcHmac = true;
+ break;
+ case A128GCM:
+ cekSize = 128;
+ break;
+ case A192GCM:
+ cekSize = 192;
+ break;
+ case A256GCM:
+ cekSize = 256;
+ break;
+ default:
+ // Default to A256GCM for backward compatibility
+ cekSize = 256;
+ break;
+ }
+
+ var cek = AesEncryption.GenerateCek(cekSize);
var encryptedSecretKeyBytes = RsaEncryption.WrapSecretKey(config.EncryptionCertificate.GetRSAPublicKey(), cek, "SHA-256");
var encryptedKey = Base64Utils.URLEncode(encryptedSecretKeyBytes);
- var iv = AesEncryption.GenerateIV();
var payloadBytes = Encoding.UTF8.GetBytes(payload);
var headerString = header.Json.ToString();
var encodedHeader = Base64Utils.URLEncode(Encoding.UTF8.GetBytes(headerString));
var aad = Encoding.ASCII.GetBytes(encodedHeader);
- var encrypted = AesGcm.Encrypt(cek, iv, payloadBytes, aad);
- return Serialize(encodedHeader, encryptedKey, Base64Utils.URLEncode(iv), Base64Utils.URLEncode(encrypted.Ciphertext), Base64Utils.URLEncode(encrypted.AuthTag));
+ if (isCbcHmac)
+ {
+ // Use 16-byte IV for CBC mode
+ var iv = new byte[16];
+ using (var rng = System.Security.Cryptography.RandomNumberGenerator.Create())
+ {
+ rng.GetBytes(iv);
+ }
+
+ var encrypted = AesCbc.Encrypt(cek, iv, payloadBytes, aad, config.EnableCbcHmacVerification);
+ return Serialize(encodedHeader, encryptedKey, Base64Utils.URLEncode(iv), Base64Utils.URLEncode(encrypted.Ciphertext), Base64Utils.URLEncode(encrypted.AuthTag));
+ }
+ else
+ {
+ // Use 12-byte IV for GCM mode
+ var iv = AesEncryption.GenerateIV();
+ var encrypted = AesGcm.Encrypt(cek, iv, payloadBytes, aad);
+ return Serialize(encodedHeader, encryptedKey, Base64Utils.URLEncode(iv), Base64Utils.URLEncode(encrypted.Ciphertext), Base64Utils.URLEncode(encrypted.AuthTag));
+ }
}
public static JweObject Parse(string encryptedPayload)
diff --git a/Mastercard.Developer.ClientEncryption.Core/Encryption/JweConfig.cs b/Mastercard.Developer.ClientEncryption.Core/Encryption/JweConfig.cs
index b4c4c12..5b97ed5 100644
--- a/Mastercard.Developer.ClientEncryption.Core/Encryption/JweConfig.cs
+++ b/Mastercard.Developer.ClientEncryption.Core/Encryption/JweConfig.cs
@@ -4,6 +4,12 @@ namespace Mastercard.Developer.ClientEncryption.Core.Encryption
{
public class JweConfig : EncryptionConfig
{
+ ///
+ /// Enable HMAC verification for CBC mode encryption algorithms (A128CBC-HS256, A192CBC-HS384, A256CBC-HS512).
+ /// Default is false for backward compatibility.
+ ///
+ public bool EnableCbcHmacVerification { get; internal set; }
+
internal JweConfig() { }
}
}
diff --git a/Mastercard.Developer.ClientEncryption.Core/Encryption/JweConfigBuilder.cs b/Mastercard.Developer.ClientEncryption.Core/Encryption/JweConfigBuilder.cs
index 1019287..368e234 100644
--- a/Mastercard.Developer.ClientEncryption.Core/Encryption/JweConfigBuilder.cs
+++ b/Mastercard.Developer.ClientEncryption.Core/Encryption/JweConfigBuilder.cs
@@ -24,8 +24,8 @@ public JweConfigBuilder WithEncryptionCertificate(X509Certificate2 encryptionCer
{
_encryptionCertificate = encryptionCertificate;
return this;
- }
-
+ }
+
///
/// See:
///
@@ -71,6 +71,19 @@ public JweConfigBuilder WithEncryptedValueFieldName(string encryptedValueFieldNa
return this;
}
+ private bool _enableCbcHmacVerification = false;
+
+ ///
+ /// Enable HMAC verification for CBC mode encryption algorithms (A128CBC-HS256, A192CBC-HS384, A256CBC-HS512).
+ /// When enabled, HMAC authentication tags will be verified during decryption and generated during encryption.
+ /// Default is false for backward compatibility.
+ ///
+ public JweConfigBuilder WithCbcHmacVerification(bool enable = true)
+ {
+ _enableCbcHmacVerification = enable;
+ return this;
+ }
+
///
/// Build a
///
@@ -88,7 +101,8 @@ public JweConfig Build()
EncryptionPaths = _encryptionPaths.Count == 0 ? new Dictionary { { "$", "$" } } : _encryptionPaths,
DecryptionPaths = _decryptionPaths.Count == 0 ? new Dictionary { { "$.encryptedData", "$" } } : _decryptionPaths,
EncryptedValueFieldName = _encryptedValueFieldName ?? "encryptedData",
- Scheme = EncryptionConfig.EncryptionScheme.Jwe
+ Scheme = EncryptionConfig.EncryptionScheme.Jwe,
+ EnableCbcHmacVerification = _enableCbcHmacVerification
};
}
diff --git a/Mastercard.Developer.ClientEncryption.Tests/Tests/Encryption/JWE/CbcHmacJweObjectTest.cs b/Mastercard.Developer.ClientEncryption.Tests/Tests/Encryption/JWE/CbcHmacJweObjectTest.cs
new file mode 100644
index 0000000..0ec9892
--- /dev/null
+++ b/Mastercard.Developer.ClientEncryption.Tests/Tests/Encryption/JWE/CbcHmacJweObjectTest.cs
@@ -0,0 +1,305 @@
+using System;
+using Mastercard.Developer.ClientEncryption.Core.Encryption;
+using Mastercard.Developer.ClientEncryption.Core.Encryption.JWE;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Mastercard.Developer.ClientEncryption.Tests.NetCore.Test;
+
+namespace Mastercard.Developer.ClientEncryption.Tests.NetCore.Encryption.JWE
+{
+ [TestClass]
+ public class CbcHmacJweObjectTest
+ {
+ [TestMethod]
+ public void TestDecrypt_ShouldWorkWithoutHmacVerification_WhenHmacDisabledByDefault()
+ {
+ // GIVEN - Default config without HMAC enabled
+ JweObject jweObject = TestUtils.GetTestCbcJweObject();
+ var config = TestUtils.GetTestJweConfigBuilder().Build();
+
+ // WHEN
+ string decryptedPayload = jweObject.Decrypt(config);
+
+ // THEN
+ Assert.AreEqual("bar", decryptedPayload);
+ Assert.IsFalse(config.EnableCbcHmacVerification, "HMAC verification should be disabled by default");
+ }
+
+ [TestMethod]
+ public void TestDecrypt_ShouldWorkWithoutHmacVerification_WhenExplicitlyDisabled()
+ {
+ // GIVEN
+ JweObject jweObject = TestUtils.GetTestCbcJweObject();
+ var config = TestUtils.GetTestJweConfigBuilder()
+ .WithCbcHmacVerification(false)
+ .Build();
+
+ // WHEN
+ string decryptedPayload = jweObject.Decrypt(config);
+
+ // THEN
+ Assert.AreEqual("bar", decryptedPayload);
+ Assert.IsFalse(config.EnableCbcHmacVerification);
+ }
+
+ [TestMethod]
+ public void TestEncryptDecrypt_A128CBC_HS256_WithHmacEnabled()
+ {
+ // GIVEN
+ var config = TestUtils.GetTestJweConfigBuilder()
+ .WithCbcHmacVerification(true)
+ .Build();
+
+ var header = new JweHeader("RSA-OAEP-256", "A128CBC-HS256", config.EncryptionKeyFingerprint, "application/json");
+
+ string payload = "{\"test\":\"data\"}";
+
+ // WHEN - Encrypt with HMAC
+ string encrypted = JweObject.Encrypt(config, payload, header);
+
+ // THEN - Decrypt with HMAC verification
+ var jweObject = JweObject.Parse(encrypted);
+ string decrypted = jweObject.Decrypt(config);
+
+ Assert.AreEqual(payload, decrypted);
+ Assert.IsTrue(config.EnableCbcHmacVerification);
+ }
+
+ [TestMethod]
+ public void TestEncryptDecrypt_A192CBC_HS384_WithHmacEnabled()
+ {
+ // GIVEN
+ var config = TestUtils.GetTestJweConfigBuilder()
+ .WithCbcHmacVerification(true)
+ .Build();
+
+ var header = new JweHeader("RSA-OAEP-256", "A192CBC-HS384", config.EncryptionKeyFingerprint, "application/json");
+
+ string payload = "{\"accountNumber\":\"1234567890\"}";
+
+ // WHEN - Encrypt with HMAC
+ string encrypted = JweObject.Encrypt(config, payload, header);
+
+ // THEN - Decrypt with HMAC verification
+ var jweObject = JweObject.Parse(encrypted);
+ string decrypted = jweObject.Decrypt(config);
+
+ Assert.AreEqual(payload, decrypted);
+ }
+
+ [TestMethod]
+ public void TestEncryptDecrypt_A256CBC_HS512_WithHmacEnabled()
+ {
+ // GIVEN
+ var config = TestUtils.GetTestJweConfigBuilder()
+ .WithCbcHmacVerification(true)
+ .Build();
+
+ var header = new JweHeader("RSA-OAEP-256", "A256CBC-HS512", config.EncryptionKeyFingerprint, "application/json");
+
+ string payload = "{\"sensitiveData\":\"secret\"}";
+
+ // WHEN - Encrypt with HMAC
+ string encrypted = JweObject.Encrypt(config, payload, header);
+
+ // THEN - Decrypt with HMAC verification
+ var jweObject = JweObject.Parse(encrypted);
+ string decrypted = jweObject.Decrypt(config);
+
+ Assert.AreEqual(payload, decrypted);
+ }
+
+ [TestMethod]
+ public void TestEncryptDecrypt_WithHmacDisabled_ShouldStillWork()
+ {
+ // GIVEN - HMAC explicitly disabled
+ var config = TestUtils.GetTestJweConfigBuilder()
+ .WithCbcHmacVerification(false)
+ .Build();
+
+ var header = new JweHeader("RSA-OAEP-256", "A128CBC-HS256", config.EncryptionKeyFingerprint, null);
+
+ string payload = "{\"test\":\"backward-compatible\"}";
+
+ // WHEN
+ string encrypted = JweObject.Encrypt(config, payload, header);
+ var jweObject = JweObject.Parse(encrypted);
+ string decrypted = jweObject.Decrypt(config);
+
+ // THEN
+ Assert.AreEqual(payload, decrypted);
+ }
+
+ [TestMethod]
+ public void TestDecrypt_ShouldThrowException_WhenHmacVerificationFailsDueToCiphertextTampering()
+ {
+ // GIVEN - Encrypt with HMAC enabled
+ var config = TestUtils.GetTestJweConfigBuilder()
+ .WithCbcHmacVerification(true)
+ .Build();
+
+ var header = new JweHeader("RSA-OAEP-256", "A128CBC-HS256", config.EncryptionKeyFingerprint, null);
+
+ string payload = "{\"test\":\"data\"}";
+ string encrypted = JweObject.Encrypt(config, payload, header);
+
+ // Tamper with the ciphertext
+ var parts = encrypted.Split('.');
+ // Corrupt one character in the ciphertext (part 3)
+ var tamperedCiphertext = parts[3].Substring(0, parts[3].Length - 1) + "X";
+ var tamperedJwe = $"{parts[0]}.{parts[1]}.{parts[2]}.{tamperedCiphertext}.{parts[4]}";
+
+ var jweObject = JweObject.Parse(tamperedJwe);
+
+ // WHEN/THEN - Should throw exception due to HMAC verification failure
+ try
+ {
+ jweObject.Decrypt(config);
+ Assert.Fail("Expected EncryptionException to be thrown");
+ }
+ catch (EncryptionException ex)
+ {
+ Assert.IsTrue(ex.Message.Contains("HMAC verification failed"),
+ $"Expected HMAC verification failure, but got: {ex.Message}");
+ }
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(EncryptionException), "HMAC verification failed")]
+ public void TestDecrypt_ShouldThrowException_WhenHmacVerificationFailsDueToAuthTagTampering()
+ {
+ // GIVEN - Encrypt with HMAC enabled
+ var config = TestUtils.GetTestJweConfigBuilder()
+ .WithCbcHmacVerification(true)
+ .Build();
+
+ var header = new JweHeader("RSA-OAEP-256", "A256CBC-HS512", config.EncryptionKeyFingerprint, null);
+
+ string payload = "{\"test\":\"data\"}";
+ string encrypted = JweObject.Encrypt(config, payload, header);
+
+ // Tamper with the authentication tag
+ var parts = encrypted.Split('.');
+ // Corrupt one character in the auth tag (part 4)
+ var tamperedAuthTag = parts[4].Substring(0, parts[4].Length - 1) + "Y";
+ var tamperedJwe = $"{parts[0]}.{parts[1]}.{parts[2]}.{parts[3]}.{tamperedAuthTag}";
+
+ var jweObject = JweObject.Parse(tamperedJwe);
+
+ // WHEN/THEN - Should throw exception due to HMAC verification failure
+ jweObject.Decrypt(config);
+ }
+
+ [TestMethod]
+ public void TestDecrypt_ShouldNotThrowHmacException_WhenHmacDisabledAndCiphertextTampered()
+ {
+ // GIVEN - Encrypt with HMAC disabled
+ var config = TestUtils.GetTestJweConfigBuilder()
+ .WithCbcHmacVerification(false)
+ .Build();
+
+ var header = new JweHeader("RSA-OAEP-256", "A128CBC-HS256", config.EncryptionKeyFingerprint, null);
+
+ string payload = "{\"test\":\"data\"}";
+ string encrypted = JweObject.Encrypt(config, payload, header);
+
+ // Tamper with the ciphertext
+ var parts = encrypted.Split('.');
+ var tamperedCiphertext = parts[3].Substring(0, parts[3].Length - 1) + "X";
+ var tamperedJwe = $"{parts[0]}.{parts[1]}.{parts[2]}.{tamperedCiphertext}.{parts[4]}";
+
+ var jweObject = JweObject.Parse(tamperedJwe);
+
+ // WHEN/THEN - Should not throw HMAC exception (but will fail with padding/decryption error)
+ bool caughtException = false;
+ try
+ {
+ jweObject.Decrypt(config);
+ }
+ catch (Exception ex)
+ {
+ caughtException = true;
+ // Should NOT be HMAC verification failure (but could be padding or other crypto exception)
+ Assert.IsFalse(ex.Message.Contains("HMAC verification failed"),
+ "Should not fail due to HMAC when HMAC is disabled. Got: " + ex.Message);
+ }
+
+ // Tampering should cause some kind of failure (padding, decryption, etc.) but not HMAC
+ Assert.IsTrue(caughtException, "Expected some exception due to tampering");
+ }
+
+ [TestMethod]
+ public void TestConfigBuilder_ShouldDefaultToHmacDisabled()
+ {
+ // GIVEN/WHEN
+ var config = TestUtils.GetTestJweConfigBuilder().Build();
+
+ // THEN
+ Assert.IsFalse(config.EnableCbcHmacVerification,
+ "HMAC verification should be disabled by default for backward compatibility");
+ }
+
+ [TestMethod]
+ public void TestConfigBuilder_WithCbcHmacVerification_ShouldEnableHmac()
+ {
+ // GIVEN/WHEN
+ var config = TestUtils.GetTestJweConfigBuilder()
+ .WithCbcHmacVerification()
+ .Build();
+
+ // THEN
+ Assert.IsTrue(config.EnableCbcHmacVerification);
+ }
+
+ [TestMethod]
+ public void TestConfigBuilder_WithCbcHmacVerificationTrue_ShouldEnableHmac()
+ {
+ // GIVEN/WHEN
+ var config = TestUtils.GetTestJweConfigBuilder()
+ .WithCbcHmacVerification(true)
+ .Build();
+
+ // THEN
+ Assert.IsTrue(config.EnableCbcHmacVerification);
+ }
+
+ [TestMethod]
+ public void TestConfigBuilder_WithCbcHmacVerificationFalse_ShouldDisableHmac()
+ {
+ // GIVEN/WHEN
+ var config = TestUtils.GetTestJweConfigBuilder()
+ .WithCbcHmacVerification(false)
+ .Build();
+
+ // THEN
+ Assert.IsFalse(config.EnableCbcHmacVerification);
+ }
+
+ [TestMethod]
+ public void TestEncrypt_MultipleTimes_WithHmacEnabled_ShouldProduceDifferentCiphertext()
+ {
+ // GIVEN
+ var config = TestUtils.GetTestJweConfigBuilder()
+ .WithCbcHmacVerification(true)
+ .Build();
+
+ var header = new JweHeader("RSA-OAEP-256", "A128CBC-HS256", config.EncryptionKeyFingerprint, null);
+
+ string payload = "{\"test\":\"data\"}";
+
+ // WHEN - Encrypt the same payload twice
+ string encrypted1 = JweObject.Encrypt(config, payload, header);
+ string encrypted2 = JweObject.Encrypt(config, payload, header);
+
+ // THEN - Should be different due to random IV
+ Assert.AreNotEqual(encrypted1, encrypted2,
+ "Multiple encryptions should produce different ciphertext due to random IV");
+
+ // But both should decrypt to the same value
+ var jweObject1 = JweObject.Parse(encrypted1);
+ var jweObject2 = JweObject.Parse(encrypted2);
+
+ Assert.AreEqual(payload, jweObject1.Decrypt(config));
+ Assert.AreEqual(payload, jweObject2.Decrypt(config));
+ }
+ }
+}
diff --git a/README.md b/README.md
index d7391a7..c545274 100644
--- a/README.md
+++ b/README.md
@@ -150,6 +150,71 @@ var config = JweConfigBuilder.AJweEncryptionConfig()
.Build();
```
+###### Supported Encryption Algorithms
+
+The library supports the following JWE encryption algorithms according to [RFC 7516](https://datatracker.ietf.org/doc/html/rfc7516):
+
+**Key Encryption Algorithms (`alg` header):**
+
+| Algorithm | Description | Key Size |
+|-----------|-------------|----------|
+| `RSA-OAEP` | RSAES using Optimal Asymmetric Encryption Padding (OAEP) with SHA-1 and MGF1 | 2048+ bits |
+| `RSA-OAEP-256` | RSAES-OAEP using SHA-256 and MGF1 with SHA-256 | 2048+ bits |
+
+**Content Encryption Algorithms (`enc` header):**
+
+| Algorithm | Description | Key Size | Authentication |
+|-----------|-------------|----------|----------------|
+| `A128GCM` | AES-128 with Galois/Counter Mode | 128 bits | Built-in |
+| `A192GCM` | AES-192 with Galois/Counter Mode | 192 bits | Built-in |
+| `A256GCM` | AES-256 with Galois/Counter Mode (default) | 256 bits | Built-in |
+| `A128CBC-HS256` | AES-128-CBC with HMAC-SHA256 | 256 bits (128+128) | HMAC-SHA256 |
+| `A192CBC-HS384` | AES-192-CBC with HMAC-SHA384 | 384 bits (192+192) | HMAC-SHA384 |
+| `A256CBC-HS512` | AES-256-CBC with HMAC-SHA512 | 512 bits (256+256) | HMAC-SHA512 |
+
+**Algorithm Selection:**
+
+The encryption algorithm is determined by the `enc` parameter in the JWE header. For example:
+
+```json
+{
+ "alg": "RSA-OAEP-256",
+ "enc": "A256GCM",
+ "kid": "761b003c1eade3a5490e5000d37887baa5e6ec0e226c07706e599451fc032a79"
+}
+```
+
+**GCM vs CBC-HMAC:**
+
+- **AES-GCM:** Provides both encryption and authentication in a single operation. Default choice for new implementations.
+- **AES-CBC-HMAC:** Provides encryption via CBC mode and authentication via HMAC. Requires two separate operations and proper HMAC verification configuration.
+
+###### Configuring CBC-HMAC Verification
+
+For CBC-HMAC algorithms (`A128CBC-HS256`, `A192CBC-HS384`, `A256CBC-HS512`), HMAC verification is **disabled by default** for backward compatibility. You can enable HMAC authentication and verification using the `WithCbcHmacVerification()` method:
+
+```cs
+var config = JweConfigBuilder.AJweEncryptionConfig()
+ .WithEncryptionCertificate(encryptionCertificate)
+ .WithDecryptionKey(decryptionKey)
+ .WithCbcHmacVerification(true) // Enable HMAC verification
+ .Build();
+```
+
+**When HMAC verification is enabled:**
+
+- During encryption: HMAC authentication tags are generated according to RFC 7516
+- During decryption: HMAC tags are verified before decryption, providing authenticated encryption
+- Tampering with ciphertext or authentication tags will cause decryption to fail with an `EncryptionException`
+
+**When HMAC verification is disabled (default):**
+
+- Maintains backward compatibility with existing implementations
+- HMAC verification is skipped during decryption
+- Empty authentication tags are generated during encryption
+
+**Security Recommendation:** Enable HMAC verification for new integrations using CBC-HMAC algorithms to ensure data integrity and authenticity.
+
##### • Performing JWE Encryption