From 349bd4aa098eb19745a89c237439ab68a2bb55c1 Mon Sep 17 00:00:00 2001 From: karen-avetisyan-mc Date: Wed, 12 Nov 2025 11:47:11 +0000 Subject: [PATCH 1/3] feat: Add configurable CBC-HMAC support with backward compatibility - Implement HMAC verification for A128CBC-HS256, A192CBC-HS384, A256CBC-HS512 - Add EnableCbcHmacVerification config flag (default: false) - Follow RFC 7516 specification for authenticated encryption - Add 14 comprehensive unit tests (all 186 tests pass) - Update README with configuration examples - Maintain full backward compatibility --- .../Encryption/AES/AesCbc.cs | 165 +++++++++- .../Encryption/JWE/JweObject.cs | 68 +++- .../Encryption/JweConfig.cs | 6 + .../Encryption/JweConfigBuilder.cs | 20 +- .../Encryption/JWE/CbcHmacJweObjectTest.cs | 305 ++++++++++++++++++ README.md | 44 +++ 6 files changed, 594 insertions(+), 14 deletions(-) create mode 100644 Mastercard.Developer.ClientEncryption.Tests/Tests/Encryption/JWE/CbcHmacJweObjectTest.cs 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..a8a2a3e 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,50 @@ var config = JweConfigBuilder.AJweEncryptionConfig() .Build(); ``` +###### Supported Encryption Algorithms + +The library supports the following JWE encryption algorithms: + +**AES-GCM (Galois/Counter Mode):** + +- `A128GCM` - AES-128 with GCM mode +- `A192GCM` - AES-192 with GCM mode +- `A256GCM` - AES-256 with GCM mode (default) + +**AES-CBC with HMAC (Cipher Block Chaining with HMAC authentication):** + +- `A128CBC-HS256` - AES-128-CBC with HMAC-SHA256 +- `A192CBC-HS384` - AES-192-CBC with HMAC-SHA384 +- `A256CBC-HS512` - AES-256-CBC with HMAC-SHA512 + +The encryption algorithm is determined by the `enc` header parameter in the JWE header. + +###### 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 From ad2b1198b3865974a07bf958c00547cdc1557043 Mon Sep 17 00:00:00 2001 From: karen-avetisyan-mc Date: Wed, 26 Nov 2025 13:37:29 +0000 Subject: [PATCH 2/3] updating the README.md --- README.md | 41 +++++++++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index a8a2a3e..04d90f6 100644 --- a/README.md +++ b/README.md @@ -152,21 +152,42 @@ var config = JweConfigBuilder.AJweEncryptionConfig() ###### Supported Encryption Algorithms -The library supports the following JWE encryption algorithms: +The library supports the following JWE encryption algorithms according to [RFC 7516](https://datatracker.ietf.org/doc/html/rfc7516): -**AES-GCM (Galois/Counter Mode):** +**Key Encryption Algorithms (`alg` header):** -- `A128GCM` - AES-128 with GCM mode -- `A192GCM` - AES-192 with GCM mode -- `A256GCM` - AES-256 with GCM mode (default) +| 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 | -**AES-CBC with HMAC (Cipher Block Chaining with HMAC authentication):** +**Content Encryption Algorithms (`enc` header):** -- `A128CBC-HS256` - AES-128-CBC with HMAC-SHA256 -- `A192CBC-HS384` - AES-192-CBC with HMAC-SHA384 -- `A256CBC-HS512` - AES-256-CBC with HMAC-SHA512 +| 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 | -The encryption algorithm is determined by the `enc` header parameter in the JWE header. +**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 (Recommended):** 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 From 9a6a3f8c7476800f5436c8c94606e1df67de161c Mon Sep 17 00:00:00 2001 From: karen-avetisyan-mc <117922723+karen-avetisyan-mc@users.noreply.github.com> Date: Wed, 26 Nov 2025 13:38:45 +0000 Subject: [PATCH 3/3] Fix formatting in README for AES-GCM description --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 04d90f6..c545274 100644 --- a/README.md +++ b/README.md @@ -186,7 +186,7 @@ The encryption algorithm is determined by the `enc` parameter in the JWE header. **GCM vs CBC-HMAC:** -- **AES-GCM (Recommended):** Provides both encryption and authentication in a single operation. Default choice for new implementations. +- **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