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
165 changes: 159 additions & 6 deletions Mastercard.Developer.ClientEncryption.Core/Encryption/AES/AesCbc.cs
Original file line number Diff line number Diff line change
@@ -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))
Expand All @@ -43,5 +81,120 @@
}
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

Check warning on line 170 in Mastercard.Developer.ClientEncryption.Core/Encryption/AES/AesCbc.cs

View workflow job for this annotation

GitHub Actions / build

Remove this commented out code. (https://rules.sonarsource.com/csharp/RSPEC-125)

Check warning on line 170 in Mastercard.Developer.ClientEncryption.Core/Encryption/AES/AesCbc.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this commented out code.

See more on https://sonarcloud.io/project/issues?id=Mastercard_client-encryption-csharp2&issues=AZp36XtNHSqpJbhBENG3&open=AZp36XtNHSqpJbhBENG3&pullRequest=51
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());
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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");
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ namespace Mastercard.Developer.ClientEncryption.Core.Encryption
{
public class JweConfig : EncryptionConfig
{
/// <summary>
/// Enable HMAC verification for CBC mode encryption algorithms (A128CBC-HS256, A192CBC-HS384, A256CBC-HS512).
/// Default is false for backward compatibility.
/// </summary>
public bool EnableCbcHmacVerification { get; internal set; }

internal JweConfig() { }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ public JweConfigBuilder WithEncryptionCertificate(X509Certificate2 encryptionCer
{
_encryptionCertificate = encryptionCertificate;
return this;
}

}
/// <summary>
/// See: <see cref="EncryptionConfig.EncryptionCertificate"/>
/// </summary>
Expand Down Expand Up @@ -71,6 +71,19 @@ public JweConfigBuilder WithEncryptedValueFieldName(string encryptedValueFieldNa
return this;
}

private bool _enableCbcHmacVerification = false;

/// <summary>
/// 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.
/// </summary>
public JweConfigBuilder WithCbcHmacVerification(bool enable = true)
{
_enableCbcHmacVerification = enable;
return this;
}

/// <summary>
/// Build a <see cref="JweConfig"/>
/// </summary>
Expand All @@ -88,7 +101,8 @@ public JweConfig Build()
EncryptionPaths = _encryptionPaths.Count == 0 ? new Dictionary<string, string> { { "$", "$" } } : _encryptionPaths,
DecryptionPaths = _decryptionPaths.Count == 0 ? new Dictionary<string, string> { { "$.encryptedData", "$" } } : _decryptionPaths,
EncryptedValueFieldName = _encryptedValueFieldName ?? "encryptedData",
Scheme = EncryptionConfig.EncryptionScheme.Jwe
Scheme = EncryptionConfig.EncryptionScheme.Jwe,
EnableCbcHmacVerification = _enableCbcHmacVerification
};
}

Expand Down
Loading
Loading