Skip to content

Commit 349bd4a

Browse files
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
1 parent c287264 commit 349bd4a

File tree

6 files changed

+594
-14
lines changed

6 files changed

+594
-14
lines changed

Mastercard.Developer.ClientEncryption.Core/Encryption/AES/AesCbc.cs

Lines changed: 159 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,66 @@
11
using System;
22
using System.IO;
3+
using System.Linq;
34
using System.Security.Cryptography;
5+
using System.Text;
46
using Mastercard.Developer.ClientEncryption.Core.Utils;
57
using Mastercard.Developer.ClientEncryption.Core.Encryption.JWE;
68

79
namespace Mastercard.Developer.ClientEncryption.Core.Encryption.AES
810
{
11+
internal class AesCbcAuthenticated
12+
{
13+
public byte[] Ciphertext { get; private set; }
14+
public byte[] AuthTag { get; private set; }
15+
16+
internal AesCbcAuthenticated(byte[] ciphertext, byte[] authTag)
17+
{
18+
Ciphertext = ciphertext;
19+
AuthTag = authTag;
20+
}
21+
}
22+
923
internal static class AesCbc
1024
{
11-
public static byte[] Decrypt(byte[] secretKeyBytes, JweObject jweObject)
25+
public static byte[] Decrypt(byte[] secretKeyBytes, JweObject jweObject, bool enableHmacVerification)
1226
{
13-
// Extract the encryption key
14-
byte[] aesKey = new byte[16];
15-
Array.Copy(secretKeyBytes, 16, aesKey, 0, aesKey.Length);
27+
// Determine key sizes based on the total secret key length
28+
// A128CBC-HS256: 32 bytes (16 for HMAC, 16 for AES)
29+
// A192CBC-HS384: 48 bytes (24 for HMAC, 24 for AES)
30+
// A256CBC-HS512: 64 bytes (32 for HMAC, 32 for AES)
31+
int keyLength = secretKeyBytes.Length / 2;
32+
33+
// Extract HMAC key (first half) and encryption key (second half)
34+
byte[] hmacKey = new byte[keyLength];
35+
byte[] aesKey = new byte[keyLength];
36+
Array.Copy(secretKeyBytes, 0, hmacKey, 0, keyLength);
37+
Array.Copy(secretKeyBytes, keyLength, aesKey, 0, keyLength);
1638

39+
// Decode values needed for both HMAC and decryption
40+
byte[] authTag = Base64Utils.URLDecode(jweObject.AuthTag);
41+
byte[] iv = Base64Utils.URLDecode(jweObject.Iv);
42+
byte[] ciphertext = Base64Utils.URLDecode(jweObject.CipherText);
43+
44+
// Verify HMAC only if enabled
45+
if (enableHmacVerification)
46+
{
47+
byte[] aad = Encoding.ASCII.GetBytes(jweObject.RawHeader);
48+
49+
if (!VerifyHmac(hmacKey, aad, iv, ciphertext, authTag))
50+
{
51+
throw new EncryptionException("HMAC verification failed");
52+
}
53+
}
54+
55+
// Decrypt
1756
byte[] plaintext;
1857
using (var aes = Aes.Create())
1958
{
2059
aes.Key = aesKey;
2160
aes.Mode = CipherMode.CBC;
2261
aes.Padding = PaddingMode.PKCS7;
23-
aes.IV = Base64Utils.URLDecode(jweObject.Iv);
62+
aes.IV = iv;
2463

25-
byte[] ciphertext = Base64Utils.URLDecode(jweObject.CipherText);
2664
using (var decryptor = aes.CreateDecryptor())
2765
{
2866
using (var memoryStream = new MemoryStream(ciphertext))
@@ -43,5 +81,120 @@ public static byte[] Decrypt(byte[] secretKeyBytes, JweObject jweObject)
4381
}
4482
return plaintext;
4583
}
84+
85+
internal static AesCbcAuthenticated Encrypt(byte[] secretKeyBytes, byte[] iv, byte[] plaintext, byte[] aad, bool enableHmacGeneration)
86+
{
87+
// Determine key sizes based on the total secret key length
88+
int keyLength = secretKeyBytes.Length / 2;
89+
90+
// Extract HMAC key (first half) and encryption key (second half)
91+
byte[] hmacKey = new byte[keyLength];
92+
byte[] aesKey = new byte[keyLength];
93+
Array.Copy(secretKeyBytes, 0, hmacKey, 0, keyLength);
94+
Array.Copy(secretKeyBytes, keyLength, aesKey, 0, keyLength);
95+
96+
// Encrypt
97+
byte[] ciphertext;
98+
using (var aes = Aes.Create())
99+
{
100+
aes.Key = aesKey;
101+
aes.Mode = CipherMode.CBC;
102+
aes.Padding = PaddingMode.PKCS7;
103+
aes.IV = iv;
104+
105+
using (var encryptor = aes.CreateEncryptor())
106+
{
107+
using (var memoryStream = new MemoryStream())
108+
{
109+
using (var cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write))
110+
{
111+
cryptoStream.Write(plaintext, 0, plaintext.Length);
112+
cryptoStream.FlushFinalBlock();
113+
ciphertext = memoryStream.ToArray();
114+
}
115+
}
116+
}
117+
}
118+
119+
// Compute HMAC only if enabled
120+
byte[] authTag;
121+
if (enableHmacGeneration)
122+
{
123+
byte[] fullHmac = ComputeHmac(hmacKey, aad, iv, ciphertext);
124+
// Truncate to half the length for the authentication tag (same as keyLength)
125+
authTag = new byte[keyLength];
126+
Array.Copy(fullHmac, 0, authTag, 0, keyLength);
127+
}
128+
else
129+
{
130+
authTag = new byte[0]; // Empty auth tag when HMAC is disabled
131+
}
132+
133+
return new AesCbcAuthenticated(ciphertext, authTag);
134+
}
135+
136+
private static bool VerifyHmac(byte[] hmacKey, byte[] aad, byte[] iv, byte[] ciphertext, byte[] authTag)
137+
{
138+
byte[] expectedTag = ComputeHmac(hmacKey, aad, iv, ciphertext);
139+
140+
// Truncate to half the length for the authentication tag
141+
int tagLength = hmacKey.Length;
142+
byte[] truncatedExpectedTag = new byte[tagLength];
143+
Array.Copy(expectedTag, 0, truncatedExpectedTag, 0, tagLength);
144+
145+
// Constant-time comparison
146+
if (authTag.Length != truncatedExpectedTag.Length)
147+
{
148+
return false;
149+
}
150+
151+
int result = 0;
152+
for (int i = 0; i < authTag.Length; i++)
153+
{
154+
result |= authTag[i] ^ truncatedExpectedTag[i];
155+
}
156+
157+
return result == 0;
158+
}
159+
160+
private static byte[] ComputeHmac(byte[] hmacKey, byte[] aad, byte[] iv, byte[] ciphertext)
161+
{
162+
// Construct Additional Authenticated Data (AAD) length in bits as 64-bit big-endian
163+
long aadLengthBits = (long)aad.Length * 8;
164+
byte[] aadLength = BitConverter.GetBytes(aadLengthBits);
165+
if (BitConverter.IsLittleEndian)
166+
{
167+
Array.Reverse(aadLength);
168+
}
169+
170+
// Concatenate: AAD || IV || Ciphertext || AAD Length
171+
var hmacInput = new MemoryStream();
172+
hmacInput.Write(aad, 0, aad.Length);
173+
hmacInput.Write(iv, 0, iv.Length);
174+
hmacInput.Write(ciphertext, 0, ciphertext.Length);
175+
hmacInput.Write(aadLength, 0, aadLength.Length);
176+
177+
// Determine HMAC algorithm based on key length
178+
HMAC hmac;
179+
switch (hmacKey.Length)
180+
{
181+
case 16: // HS256
182+
hmac = new HMACSHA256(hmacKey);
183+
break;
184+
case 24: // HS384
185+
hmac = new HMACSHA384(hmacKey);
186+
break;
187+
case 32: // HS512
188+
hmac = new HMACSHA512(hmacKey);
189+
break;
190+
default:
191+
throw new EncryptionException($"Unsupported HMAC key length: {hmacKey.Length}");
192+
}
193+
194+
using (hmac)
195+
{
196+
return hmac.ComputeHash(hmacInput.ToArray());
197+
}
198+
}
46199
}
47200
}

Mastercard.Developer.ClientEncryption.Core/Encryption/JWE/JweObject.cs

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ namespace Mastercard.Developer.ClientEncryption.Core.Encryption.JWE
1010
internal class JweObject
1111
{
1212
private const string A128CBC_HS256 = "A128CBC-HS256";
13+
private const string A192CBC_HS384 = "A192CBC-HS384";
14+
private const string A256CBC_HS512 = "A256CBC-HS512";
1315
private const string A256GCM = "A256GCM";
1416
private const string A128GCM = "A128GCM";
1517
private const string A192GCM = "A192GCM";
@@ -44,7 +46,13 @@ public string Decrypt(JweConfig config)
4446
plaintext = AesGcm.Decrypt(unwrappedKey, this);
4547
break;
4648
case A128CBC_HS256:
47-
plaintext = AesCbc.Decrypt(unwrappedKey, this);
49+
plaintext = AesCbc.Decrypt(unwrappedKey, this, config.EnableCbcHmacVerification);
50+
break;
51+
case A192CBC_HS384:
52+
plaintext = AesCbc.Decrypt(unwrappedKey, this, config.EnableCbcHmacVerification);
53+
break;
54+
case A256CBC_HS512:
55+
plaintext = AesCbc.Decrypt(unwrappedKey, this, config.EnableCbcHmacVerification);
4856
break;
4957
default:
5058
throw new EncryptionException($"Encryption method {encryptionMethod} is not supported");
@@ -54,19 +62,69 @@ public string Decrypt(JweConfig config)
5462

5563
public static string Encrypt(JweConfig config, string payload, JweHeader header)
5664
{
57-
var cek = AesEncryption.GenerateCek(256);
65+
var encryptionMethod = header.Enc;
66+
67+
// Determine CEK size based on encryption method
68+
int cekSize;
69+
bool isCbcHmac = false;
70+
switch (encryptionMethod)
71+
{
72+
case A128CBC_HS256:
73+
cekSize = 256; // 128-bit AES + 128-bit HMAC
74+
isCbcHmac = true;
75+
break;
76+
case A192CBC_HS384:
77+
cekSize = 384; // 192-bit AES + 192-bit HMAC
78+
isCbcHmac = true;
79+
break;
80+
case A256CBC_HS512:
81+
cekSize = 512; // 256-bit AES + 256-bit HMAC
82+
isCbcHmac = true;
83+
break;
84+
case A128GCM:
85+
cekSize = 128;
86+
break;
87+
case A192GCM:
88+
cekSize = 192;
89+
break;
90+
case A256GCM:
91+
cekSize = 256;
92+
break;
93+
default:
94+
// Default to A256GCM for backward compatibility
95+
cekSize = 256;
96+
break;
97+
}
98+
99+
var cek = AesEncryption.GenerateCek(cekSize);
58100
var encryptedSecretKeyBytes = RsaEncryption.WrapSecretKey(config.EncryptionCertificate.GetRSAPublicKey(), cek, "SHA-256");
59101
var encryptedKey = Base64Utils.URLEncode(encryptedSecretKeyBytes);
60102

61-
var iv = AesEncryption.GenerateIV();
62103
var payloadBytes = Encoding.UTF8.GetBytes(payload);
63104

64105
var headerString = header.Json.ToString();
65106
var encodedHeader = Base64Utils.URLEncode(Encoding.UTF8.GetBytes(headerString));
66107
var aad = Encoding.ASCII.GetBytes(encodedHeader);
67108

68-
var encrypted = AesGcm.Encrypt(cek, iv, payloadBytes, aad);
69-
return Serialize(encodedHeader, encryptedKey, Base64Utils.URLEncode(iv), Base64Utils.URLEncode(encrypted.Ciphertext), Base64Utils.URLEncode(encrypted.AuthTag));
109+
if (isCbcHmac)
110+
{
111+
// Use 16-byte IV for CBC mode
112+
var iv = new byte[16];
113+
using (var rng = System.Security.Cryptography.RandomNumberGenerator.Create())
114+
{
115+
rng.GetBytes(iv);
116+
}
117+
118+
var encrypted = AesCbc.Encrypt(cek, iv, payloadBytes, aad, config.EnableCbcHmacVerification);
119+
return Serialize(encodedHeader, encryptedKey, Base64Utils.URLEncode(iv), Base64Utils.URLEncode(encrypted.Ciphertext), Base64Utils.URLEncode(encrypted.AuthTag));
120+
}
121+
else
122+
{
123+
// Use 12-byte IV for GCM mode
124+
var iv = AesEncryption.GenerateIV();
125+
var encrypted = AesGcm.Encrypt(cek, iv, payloadBytes, aad);
126+
return Serialize(encodedHeader, encryptedKey, Base64Utils.URLEncode(iv), Base64Utils.URLEncode(encrypted.Ciphertext), Base64Utils.URLEncode(encrypted.AuthTag));
127+
}
70128
}
71129

72130
public static JweObject Parse(string encryptedPayload)

Mastercard.Developer.ClientEncryption.Core/Encryption/JweConfig.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ namespace Mastercard.Developer.ClientEncryption.Core.Encryption
44
{
55
public class JweConfig : EncryptionConfig
66
{
7+
/// <summary>
8+
/// Enable HMAC verification for CBC mode encryption algorithms (A128CBC-HS256, A192CBC-HS384, A256CBC-HS512).
9+
/// Default is false for backward compatibility.
10+
/// </summary>
11+
public bool EnableCbcHmacVerification { get; internal set; }
12+
713
internal JweConfig() { }
814
}
915
}

Mastercard.Developer.ClientEncryption.Core/Encryption/JweConfigBuilder.cs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ public JweConfigBuilder WithEncryptionCertificate(X509Certificate2 encryptionCer
2424
{
2525
_encryptionCertificate = encryptionCertificate;
2626
return this;
27-
}
28-
27+
}
28+
2929
/// <summary>
3030
/// See: <see cref="EncryptionConfig.EncryptionCertificate"/>
3131
/// </summary>
@@ -71,6 +71,19 @@ public JweConfigBuilder WithEncryptedValueFieldName(string encryptedValueFieldNa
7171
return this;
7272
}
7373

74+
private bool _enableCbcHmacVerification = false;
75+
76+
/// <summary>
77+
/// Enable HMAC verification for CBC mode encryption algorithms (A128CBC-HS256, A192CBC-HS384, A256CBC-HS512).
78+
/// When enabled, HMAC authentication tags will be verified during decryption and generated during encryption.
79+
/// Default is false for backward compatibility.
80+
/// </summary>
81+
public JweConfigBuilder WithCbcHmacVerification(bool enable = true)
82+
{
83+
_enableCbcHmacVerification = enable;
84+
return this;
85+
}
86+
7487
/// <summary>
7588
/// Build a <see cref="JweConfig"/>
7689
/// </summary>
@@ -88,7 +101,8 @@ public JweConfig Build()
88101
EncryptionPaths = _encryptionPaths.Count == 0 ? new Dictionary<string, string> { { "$", "$" } } : _encryptionPaths,
89102
DecryptionPaths = _decryptionPaths.Count == 0 ? new Dictionary<string, string> { { "$.encryptedData", "$" } } : _decryptionPaths,
90103
EncryptedValueFieldName = _encryptedValueFieldName ?? "encryptedData",
91-
Scheme = EncryptionConfig.EncryptionScheme.Jwe
104+
Scheme = EncryptionConfig.EncryptionScheme.Jwe,
105+
EnableCbcHmacVerification = _enableCbcHmacVerification
92106
};
93107
}
94108

0 commit comments

Comments
 (0)