Data encryption in financial systems is a regulatory and architectural baseline. With .NET 8 LTS (November 2023), the System.Security.Cryptography namespace has changed significantly: several APIs widely used in earlier .NET versions are now obsolete or removed, and the recommended patterns for symmetric encryption, key derivation, and asymmetric key transport have all been updated. This post covers the current .NET 8 approach to AES encryption (both CBC and GCM modes), PBKDF2 key derivation, RSA key exchange, and practical hybrid encryption — with guidance on what changed and why it matters for enterprise financial applications.
What Changed from Earlier .NET Versions
If you worked with .NET cryptography before .NET 6, several classes you may have relied on are now obsolete or removed in .NET 8:
RijndaelManaged— removed in .NET 8. UseAes.Create()instead.RijndaelManagedthrowsPlatformNotSupportedExceptionon .NET 8.MD5CryptoServiceProvider,SHA1CryptoServiceProvider— deprecated. Use the staticMD5.HashData()andSHA256.HashData()methods introduced in .NET 7.PasswordDeriveBytes— obsolete. UseRfc2898DeriveBytes(PBKDF2) with SHA-256 or SHA-512, or the new staticRfc2898DeriveBytes.Pbkdf2()method in .NET 6+.RSACryptoServiceProvider— works but is a legacy Windows CAPI wrapper. PreferRSA.Create()for cross-platform behaviour and modern padding support.
Symmetric Encryption — AES in .NET 8
AES is the standard for symmetric encryption. .NET 8 supports two modes that matter architecturally: AES-CBC (cipher-block chaining — confidentiality only) and AES-GCM (Galois/Counter Mode — authenticated encryption providing both confidentiality and integrity). For any new system, prefer AES-GCM: it detects ciphertext tampering before decryption, which AES-CBC cannot. AES-CBC without a separate MAC is vulnerable to padding oracle attacks.
AES-GCM — Authenticated Encryption (Recommended)
using System.Security.Cryptography;
public static class AesGcmEncryption
{
public static (byte[] ciphertext, byte[] nonce, byte[] tag) Encrypt(
byte[] plaintext, byte[] key)
{
var nonce = RandomNumberGenerator.GetBytes(AesGcm.NonceByteSizes.MaxSize);
var ciphertext = new byte[plaintext.Length];
var tag = new byte[AesGcm.TagByteSizes.MaxSize];
using var aes = new AesGcm(key, AesGcm.TagByteSizes.MaxSize);
aes.Encrypt(nonce, plaintext, ciphertext, tag);
return (ciphertext, nonce, tag);
}
public static byte[] Decrypt(byte[] ciphertext, byte[] key, byte[] nonce, byte[] tag)
{
var plaintext = new byte[ciphertext.Length];
using var aes = new AesGcm(key, AesGcm.TagByteSizes.MaxSize);
aes.Decrypt(nonce, ciphertext, tag, plaintext);
return plaintext;
}
}
AES-CBC (where backward compatibility requires it)
using System.Security.Cryptography;
public static class AesCbcEncryption
{
public static (byte[] ciphertext, byte[] iv) Encrypt(byte[] plaintext, byte[] key)
{
using var aes = Aes.Create();
aes.Key = key;
aes.GenerateIV();
using var ms = new MemoryStream();
using var cs = new CryptoStream(ms, aes.CreateEncryptor(), CryptoStreamMode.Write);
cs.Write(plaintext);
cs.FlushFinalBlock();
return (ms.ToArray(), aes.IV);
}
public static byte[] Decrypt(byte[] ciphertext, byte[] key, byte[] iv)
{
using var aes = Aes.Create();
aes.Key = key;
aes.IV = iv;
using var ms = new MemoryStream(ciphertext);
using var cs = new CryptoStream(ms, aes.CreateDecryptor(), CryptoStreamMode.Read);
using var output = new MemoryStream();
cs.CopyTo(output);
return output.ToArray();
}
}
Key Derivation — PBKDF2 with SHA-256
Never use a raw password as an encryption key. Password-based keys must be derived using a key derivation function that applies a computationally expensive hash to slow down brute-force attacks. PBKDF2 (Password-Based Key Derivation Function 2) is the NIST-recommended approach. In .NET 8, use the static Rfc2898DeriveBytes.Pbkdf2() method with SHA-256 and a minimum of 600,000 iterations — the 2023 OWASP recommendation for PBKDF2-HMAC-SHA256.
using System.Security.Cryptography;
public static class KeyDerivation
{
public static byte[] DeriveKey(string password, byte[] salt, int keyLengthBytes = 32)
{
return Rfc2898DeriveBytes.Pbkdf2(
password: password,
salt: salt,
iterations: 600_000,
hashAlgorithm: HashAlgorithmName.SHA256,
outputLength: keyLengthBytes);
}
public static byte[] GenerateSalt(int length = 16)
=> RandomNumberGenerator.GetBytes(length);
}
For machine-to-machine encryption in production financial services, avoid password-derived keys entirely. Generate cryptographically random AES keys and store them in a managed key store — Azure Key Vault, AWS Secrets Manager, or an HSM. Key rotation, access control, and audit logs come for free from the platform rather than being built from scratch.
Asymmetric Encryption — RSA in .NET 8
RSA is used for key transport and digital signatures, not for bulk data encryption — it is orders of magnitude slower than AES and limited by key size in the amount of data it can encrypt directly. The standard architectural pattern is hybrid encryption: encrypt the data payload with AES-GCM using a freshly generated key, then encrypt that AES key with the recipient’s RSA public key.
using System.Security.Cryptography;
public static class RsaEncryption
{
public static RSA CreateKeyPair() => RSA.Create(4096);
public static byte[] Encrypt(byte[] data, RSA publicKey)
=> publicKey.Encrypt(data, RSAEncryptionPadding.OaepSHA256);
public static byte[] Decrypt(byte[] ciphertext, RSA privateKey)
=> privateKey.Decrypt(ciphertext, RSAEncryptionPadding.OaepSHA256);
public static byte[] Sign(byte[] data, RSA privateKey)
=> privateKey.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
public static bool Verify(byte[] data, byte[] signature, RSA publicKey)
=> publicKey.VerifyData(data, signature,
HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
}
Hybrid Encryption Pattern
The practical pattern for encrypting arbitrarily large payloads in financial systems combines both approaches — RSA for secure key transport, AES-GCM for the actual data — and uses CryptographicOperations.ZeroMemory to scrub key material from managed memory after use.
using System.Security.Cryptography;
public static class HybridEncryption
{
public static (byte[] encryptedKey, byte[] nonce, byte[] tag, byte[] ciphertext)
Encrypt(byte[] plaintext, RSA recipientPublicKey)
{
var aesKey = RandomNumberGenerator.GetBytes(32);
try
{
var (ciphertext, nonce, tag) = AesGcmEncryption.Encrypt(plaintext, aesKey);
var encryptedKey = RsaEncryption.Encrypt(aesKey, recipientPublicKey);
return (encryptedKey, nonce, tag, ciphertext);
}
finally
{
CryptographicOperations.ZeroMemory(aesKey);
}
}
public static byte[] Decrypt(
byte[] encryptedKey, byte[] nonce, byte[] tag,
byte[] ciphertext, RSA recipientPrivateKey)
{
var aesKey = RsaEncryption.Decrypt(encryptedKey, recipientPrivateKey);
try
{
return AesGcmEncryption.Decrypt(ciphertext, aesKey, nonce, tag);
}
finally
{
CryptographicOperations.ZeroMemory(aesKey);
}
}
}
Architectural Guidance
- Prefer AES-GCM over AES-CBC for new systems. Authenticated encryption prevents bit-flipping and padding oracle attacks that AES-CBC alone cannot stop.
- Never reuse a nonce with the same AES-GCM key. Nonce reuse in GCM completely breaks confidentiality — both the keystream and the plaintext become recoverable. Generate a fresh random nonce per message.
- Use RSA-OAEP-SHA256 padding. PKCS#1 v1.5 padding is vulnerable to the Bleichenbacher adaptive chosen-ciphertext attack. OAEP is the current standard.
- Use PBKDF2 with SHA-256 and ≥ 600,000 iterations for any password-derived key. For non-interactive M2M keys, use
RandomNumberGenerator.GetBytes(32)and store in a managed secrets store. - Zero key material after use with
CryptographicOperations.ZeroMemory. In long-running services, uncleared key bytes can persist in managed heap memory until the next GC cycle — and potentially be readable via memory dumps. - Store private keys outside the application. In production financial systems, RSA private keys belong in HSMs or platform key vaults (Azure Key Vault, AWS KMS). Never commit key material to configuration files or source control.
- Use certificate-backed RSA where available. X.509 certificates provide key identity, expiry, and chain-of-trust that raw key bytes do not — essential for regulated environments such as capital markets platforms.
