Skip to content

CWE-326: Inadequate Encryption Strength - C#

Overview

Inadequate Encryption Strength in C# applications occurs when weak algorithms, short keys, or deprecated primitives are used for sensitive data. The .NET crypto stack is powerful, but legacy defaults (CBC without authentication, SHA-1, small RSA keys) and copy-pasted examples can quietly introduce weak cryptography.

Modern .NET (6+) includes AesGcm, RSA, HMACSHA256, and RandomNumberGenerator, which provide strong building blocks. However, older APIs like DES, TripleDES, MD5, SHA1, or low-iteration PBKDF2 are still available and easy to misuse.

In real systems this shows up in API key storage, token encryption, webhook verification, and password hashing. ASP.NET Core provides Data Protection for safe key management, while cloud deployments should push key storage into managed services like Azure Key Vault.

Common Vulnerable Patterns

Using DES with ECB

using System;
using System.Security.Cryptography;
using System.Text;

public static class WeakDesEncryption
{
    // VULNERABLE - DES with ECB is weak and leaks patterns.
    public static byte[] Encrypt(string plaintext)
    {
        using var des = DES.Create();
        des.Key = Encoding.UTF8.GetBytes("weakkey1");
        des.Mode = CipherMode.ECB;

        using var encryptor = des.CreateEncryptor();
        return encryptor.TransformFinalBlock(
            Encoding.UTF8.GetBytes(plaintext), 0, plaintext.Length);
    }

    // Attack example:
    // Input: repeated blocks "AAAAAA..."
    // Result: identical ciphertext blocks reveal structure.
}

Why this is vulnerable: DES has only 56-bit effective key strength and ECB mode exposes repeated plaintext patterns. The ciphertext can be brute-forced or pattern-analyzed with commodity hardware.

RSA-1024 with PKCS#1 v1.5

using System;
using System.Security.Cryptography;
using System.Text;

public static class WeakRsaEncryption
{
    // VULNERABLE - 1024-bit RSA is deprecated.
    public static byte[] EncryptApiKey(string apiKey)
    {
        using var rsa = RSA.Create(1024);
        return rsa.Encrypt(Encoding.UTF8.GetBytes(apiKey), RSAEncryptionPadding.Pkcs1);
    }

    // Attack example:
    // Input: captured ciphertext
    // Result: feasible factorization or padding-oracle exploitation.
}

Why this is vulnerable: 1024-bit RSA can be factored by well-funded attackers and PKCS#1 v1.5 padding is more brittle than OAEP.

MD5 for Password Hashing

using System;
using System.Security.Cryptography;
using System.Text;

public static class WeakPasswordHashing
{
    // VULNERABLE - MD5 is broken and too fast.
    public static string HashPassword(string password)
    {
        using var md5 = MD5.Create();
        byte[] hash = md5.ComputeHash(Encoding.UTF8.GetBytes(password));
        return Convert.ToHexString(hash);
    }

    // Attack example:
    // Input: leaked hash
    // Result: GPU cracks millions of guesses per second.
}

Why this is vulnerable: MD5 is cryptographically broken, unsalted, and extremely fast, enabling large-scale brute-force and rainbow table attacks.

Low-Iteration PBKDF2 with Fixed Salt

using System;
using System.Security.Cryptography;

public static class WeakPbkdf2
{
    // VULNERABLE - low iterations and fixed salt.
    public static byte[] DeriveKey(string password)
    {
        return Rfc2898DeriveBytes.Pbkdf2(
            password,
            "fixed_salt"u8.ToArray(),
            1000,
            HashAlgorithmName.SHA1,
            16);
    }

    // Attack example:
    // Input: stolen DB
    // Result: identical passwords yield identical keys.
}

Why this is vulnerable: Low iteration counts allow fast guessing, fixed salts remove uniqueness, and SHA-1 is deprecated.

System.Random for Keys

using System;

public static class WeakRandomKey
{
    // VULNERABLE - System.Random is predictable.
    public static byte[] GenerateKey()
    {
        var rnd = new Random(42);
        var key = new byte[32];
        rnd.NextBytes(key);
        return key;
    }

    // Attack example:
    // Input: ciphertext
    // Result: attacker reproduces key from RNG state.
}

Why this is vulnerable: System.Random is deterministic and predictable, making generated keys guessable.

Secure Patterns

AES-256-GCM Encryption

using System;
using System.Security.Cryptography;
using System.Text;

public static class AesGcmEncryption
{
    // SECURE - AES-256-GCM with random nonce and AAD.
    public static string Encrypt(string plaintext, byte[] key)
    {
        byte[] nonce = RandomNumberGenerator.GetBytes(12);
        byte[] plaintextBytes = Encoding.UTF8.GetBytes(plaintext);
        byte[] ciphertext = new byte[plaintextBytes.Length];
        byte[] tag = new byte[16];

        using var aes = new AesGcm(key);
        aes.Encrypt(nonce, plaintextBytes, ciphertext, tag, Encoding.UTF8.GetBytes("CTX_V1"));

        byte[] payload = new byte[nonce.Length + tag.Length + ciphertext.Length];
        Buffer.BlockCopy(nonce, 0, payload, 0, nonce.Length);
        Buffer.BlockCopy(tag, 0, payload, nonce.Length, tag.Length);
        Buffer.BlockCopy(ciphertext, 0, payload, nonce.Length + tag.Length, ciphertext.Length);

        return Convert.ToBase64String(payload);
    }

    public static string Decrypt(string token, byte[] key)
    {
        byte[] payload = Convert.FromBase64String(token);
        byte[] nonce = payload[..12];
        byte[] tag = payload[12..28];
        byte[] ciphertext = payload[28..];
        byte[] plaintext = new byte[ciphertext.Length];

        using var aes = new AesGcm(key);
        aes.Decrypt(nonce, ciphertext, tag, plaintext, Encoding.UTF8.GetBytes("CTX_V1"));

        return Encoding.UTF8.GetString(plaintext);
    }
}

// Short usage
byte[] key = RandomNumberGenerator.GetBytes(32);
string encrypted = AesGcmEncryption.Encrypt("123-45-6789", key);
string decrypted = AesGcmEncryption.Decrypt(encrypted, key);

Why this works:

  • AES-256 provides strong key strength against brute-force attacks.
  • GCM mode provides confidentiality and integrity in a single operation.
  • Random 96-bit nonces ensure ciphertexts are non-deterministic.
  • The authentication tag detects tampering before plaintext is returned.
  • AAD binds context to ciphertext and prevents substitution attacks.

ChaCha20-Poly1305 Encryption

using System;
using System.Security.Cryptography;
using System.Text;

public static class ChaCha20Poly1305Encryption
{
    // SECURE - ChaCha20-Poly1305 authenticated encryption.
    public static string Encrypt(string plaintext, byte[] key)
    {
        byte[] nonce = RandomNumberGenerator.GetBytes(12);
        byte[] plaintextBytes = Encoding.UTF8.GetBytes(plaintext);
        byte[] ciphertext = new byte[plaintextBytes.Length];
        byte[] tag = new byte[16];

        using var chacha = new ChaCha20Poly1305(key);
        chacha.Encrypt(nonce, plaintextBytes, ciphertext, tag, Encoding.UTF8.GetBytes("CTX_V1"));

        byte[] payload = new byte[nonce.Length + tag.Length + ciphertext.Length];
        Buffer.BlockCopy(nonce, 0, payload, 0, nonce.Length);
        Buffer.BlockCopy(tag, 0, payload, nonce.Length, tag.Length);
        Buffer.BlockCopy(ciphertext, 0, payload, nonce.Length + tag.Length, ciphertext.Length);

        return Convert.ToBase64String(payload);
    }

    public static string Decrypt(string token, byte[] key)
    {
        byte[] payload = Convert.FromBase64String(token);
        byte[] nonce = payload[..12];
        byte[] tag = payload[12..28];
        byte[] ciphertext = payload[28..];
        byte[] plaintext = new byte[ciphertext.Length];

        using var chacha = new ChaCha20Poly1305(key);
        chacha.Decrypt(nonce, ciphertext, tag, plaintext, Encoding.UTF8.GetBytes("CTX_V1"));

        return Encoding.UTF8.GetString(plaintext);
    }
}

// Short usage
byte[] key = RandomNumberGenerator.GetBytes(32);
string encrypted = ChaCha20Poly1305Encryption.Encrypt("sensitive-data", key);
string decrypted = ChaCha20Poly1305Encryption.Decrypt(encrypted, key);

Why this works:

  • ChaCha20-Poly1305 provides authenticated encryption without hardware dependencies.
  • Better performance than AES on systems without AES-NI acceleration.
  • 256-bit key provides strong security against brute-force.
  • Random 96-bit nonces prevent ciphertext reuse.
  • Poly1305 MAC ensures integrity and authenticity.
  • Ideal for mobile, IoT, and cloud environments with diverse hardware.

RSA-3072 with OAEP-SHA256

using System;
using System.Security.Cryptography;
using System.Text;

public static class RsaOaepEncryption
{
    // SECURE - RSA-3072 with OAEP-SHA256 for small secrets.
    public static (RSA privateKey, RSA publicKey) GenerateKeys()
    {
        var privateKey = RSA.Create(3072);
        var publicKey = RSA.Create();
        publicKey.ImportParameters(privateKey.ExportParameters(false));
        return (privateKey, publicKey);
    }

    public static byte[] Encrypt(RSA publicKey, string plaintext)
    {
        return publicKey.Encrypt(
            Encoding.UTF8.GetBytes(plaintext),
            RSAEncryptionPadding.OaepSHA256);
    }

    public static string Decrypt(RSA privateKey, byte[] ciphertext)
    {
        return Encoding.UTF8.GetString(
            privateKey.Decrypt(ciphertext, RSAEncryptionPadding.OaepSHA256));
    }
}

// Short usage
var (priv, pub) = RsaOaepEncryption.GenerateKeys();
byte[] enc = RsaOaepEncryption.Encrypt(pub, "api-key");
string dec = RsaOaepEncryption.Decrypt(priv, enc);

Why this works:

  • 3072-bit RSA meets long-term security guidance.
  • OAEP padding prevents padding-oracle style attacks.
  • SHA-256 avoids deprecated hash functions.
  • Correctly limits RSA usage to small secrets or key wrapping.

BCrypt for Password Hashing

using BCrypt.Net;

public static class BcryptPasswords
{
    // SECURE - bcrypt with work factor 12.
    public static string Hash(string password)
    {
        return BCrypt.Net.BCrypt.HashPassword(password, workFactor: 12);
    }

    public static bool Verify(string password, string storedHash)
    {
        return BCrypt.Net.BCrypt.Verify(password, storedHash);
    }
}

// Short usage
string hash = BcryptPasswords.Hash("Str0ngPassw0rd!");
bool ok = BcryptPasswords.Verify("Str0ngPassw0rd!", hash);

Why this works:

  • Adaptive work factor slows brute-force attacks as hardware improves.
  • Per-password salts prevent rainbow table reuse.
  • Hash output encodes salt and cost, reducing storage mistakes.
  • Verification uses timing-safe comparisons internally.

Argon2 for Password Hashing

using Konscious.Security.Cryptography;
using System;
using System.Security.Cryptography;
using System.Text;

public static class Argon2Passwords
{
    // SECURE - Argon2id with OWASP recommended parameters.
    public static string Hash(string password)
    {
        byte[] salt = RandomNumberGenerator.GetBytes(16);

        using var argon2 = new Argon2id(Encoding.UTF8.GetBytes(password))
        {
            Salt = salt,
            DegreeOfParallelism = 1,
            MemorySize = 19456,  // 19 MiB
            Iterations = 2
        };

        byte[] hash = argon2.GetBytes(32);

        return $"argon2id$v=19$m=19456,t=2,p=1${Convert.ToBase64String(salt)}${Convert.ToBase64String(hash)}";
    }

    public static bool Verify(string password, string stored)
    {
        var parts = stored.Split('$');
        if (parts.Length != 5 || parts[0] != "argon2id") return false;

        byte[] salt = Convert.FromBase64String(parts[3]);
        byte[] expected = Convert.FromBase64String(parts[4]);

        using var argon2 = new Argon2id(Encoding.UTF8.GetBytes(password))
        {
            Salt = salt,
            DegreeOfParallelism = 1,
            MemorySize = 19456,
            Iterations = 2
        };

        byte[] actual = argon2.GetBytes(expected.Length);

        return CryptographicOperations.FixedTimeEquals(actual, expected);
    }
}

// Short usage
string hash = Argon2Passwords.Hash("Str0ngPassw0rd!");
bool ok = Argon2Passwords.Verify("Str0ngPassw0rd!", hash);

Why this works:

  • Argon2id combines memory-hardness with side-channel resistance.
  • OWASP's top recommendation for password hashing (PHC winner 2015).
  • Memory cost (19 MiB) makes GPU/ASIC attacks economically infeasible.
  • Configurable parameters allow tuning for future hardware improvements.
  • Fixed-time comparison prevents timing attacks during verification.
  • Superior to bcrypt for resisting parallel cracking attempts.

PBKDF2 with High Iteration Count

using System;
using System.Security.Cryptography;

public static class Pbkdf2Passwords
{
    // SECURE - PBKDF2-SHA256 with high iterations and random salt.
    public static string Hash(string password)
    {
        byte[] salt = RandomNumberGenerator.GetBytes(32);
        byte[] hash = Rfc2898DeriveBytes.Pbkdf2(
            password,
            salt,
            600_000,
            HashAlgorithmName.SHA256,
            32);

        return $"pbkdf2$600000${Convert.ToBase64String(salt)}${Convert.ToBase64String(hash)}";
    }

    public static bool Verify(string password, string stored)
    {
        var parts = stored.Split('$');
        int iterations = int.Parse(parts[2]);
        byte[] salt = Convert.FromBase64String(parts[3]);
        byte[] expected = Convert.FromBase64String(parts[4]);

        byte[] actual = Rfc2898DeriveBytes.Pbkdf2(
            password,
            salt,
            iterations,
            HashAlgorithmName.SHA256,
            expected.Length);

        return CryptographicOperations.FixedTimeEquals(actual, expected);
    }
}

// Short usage
string stored = Pbkdf2Passwords.Hash("Str0ngPassw0rd!");
bool ok = Pbkdf2Passwords.Verify("Str0ngPassw0rd!", stored);

Why this works:

  • High iteration count raises the cost of each password guess.
  • SHA-256 avoids deprecated SHA-1.
  • Random salts ensure per-user uniqueness.
  • Fixed-time comparison prevents timing leaks during verification.

HMAC-SHA256 for Message Authentication

using System;
using System.Security.Cryptography;
using System.Text;

public static class HmacSigning
{
    // SECURE - HMAC-SHA256 with fixed-time verification.
    public static string Sign(string data, byte[] key)
    {
        using var hmac = new HMACSHA256(key);
        return Convert.ToHexString(hmac.ComputeHash(Encoding.UTF8.GetBytes(data)));
    }

    public static bool Verify(string data, string signature, byte[] key)
    {
        string expected = Sign(data, key);
        return CryptographicOperations.FixedTimeEquals(
            Convert.FromHexString(expected),
            Convert.FromHexString(signature));
    }
}

// Short usage
byte[] key = RandomNumberGenerator.GetBytes(32);
string sig = HmacSigning.Sign("payload", key);
bool ok = HmacSigning.Verify("payload", sig, key);

Why this works:

  • HMAC provides integrity and authenticity of messages.
  • 256-bit keys match SHA-256 security strength.
  • HMAC construction prevents length-extension attacks.
  • Fixed-time comparison reduces timing side channels.

ASP.NET Core Data Protection

// SECURE - ASP.NET Core Data Protection for managed key storage.
using System.IO;
using Microsoft.AspNetCore.DataProtection;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDataProtection()
    .SetApplicationName("MyApp")
    .PersistKeysToFileSystem(new DirectoryInfo(@"C:\keys"));

var app = builder.Build();

app.MapPost("/encrypt", (IDataProtectionProvider provider, string data) =>
{
    var protector = provider.CreateProtector("payments-v1");
    string protectedData = protector.Protect(data);
    return Results.Ok(protectedData);
});

app.MapPost("/decrypt", (IDataProtectionProvider provider, string token) =>
{
    var protector = provider.CreateProtector("payments-v1");
    string plaintext = protector.Unprotect(token);
    return Results.Ok(plaintext);
});

app.Run();

Why this works:

  • Data Protection uses strong algorithms with authenticated encryption.
  • Key ring storage and rotation are managed by the framework.
  • Protectors isolate contexts to prevent cross-use of ciphertext.
  • APIs enforce correct usage and reduce custom crypto errors.

Framework-Specific Guidance (Optional)

ASP.NET Core

ASP.NET Core Data Protection provides built-in authenticated encryption and key management. Use it for protecting data at rest and in cookies, and store keys outside the app container.

// SECURE - ASP.NET Core with Key Vault backed keys.
using System;
using Azure.Identity;
using Microsoft.AspNetCore.DataProtection;

var builder = WebApplication.CreateBuilder(args);

string keyVaultUri = builder.Configuration["KeyVault:Uri"] ?? "";
string keyName = builder.Configuration["KeyVault:DataProtectionKey"] ?? "dataprotection";

builder.Services.AddDataProtection()
    .SetApplicationName("MyApp")
    .PersistKeysToAzureKeyVault(
        new Uri($"{keyVaultUri}keys/{keyName}"),
        new DefaultAzureCredential());

var app = builder.Build();
app.MapGet("/token", (IDataProtectionProvider provider) =>
{
    var protector = provider.CreateProtector("tokens-v1");
    return protector.Protect("token-value");
});
app.Run();
// appsettings.json
{
  "KeyVault": {
    "Uri": "https://my-vault.vault.azure.net/",
    "DataProtectionKey": "dataprotection"
  }
}

.NET Framework (DPAPI)

For legacy .NET Framework apps, DPAPI protects data using machine/user credentials without manual key handling.

// SECURE - DPAPI for legacy .NET Framework.
using System;
using System.Security.Cryptography;
using System.Text;

public static class DpapiProtector
{
    public static string Protect(string plaintext)
    {
        byte[] data = Encoding.UTF8.GetBytes(plaintext);
        byte[] protectedData = ProtectedData.Protect(data, null, DataProtectionScope.CurrentUser);
        return Convert.ToBase64String(protectedData);
    }

    public static string Unprotect(string token)
    {
        byte[] data = Convert.FromBase64String(token);
        byte[] plain = ProtectedData.Unprotect(data, null, DataProtectionScope.CurrentUser);
        return Encoding.UTF8.GetString(plain);
    }
}

Azure Functions + Key Vault

Use managed identity with Key Vault for key retrieval and avoid hardcoding secrets.

// SECURE - Azure Function key retrieval.
using System;
using Azure.Identity;
using Azure.Security.KeyVault.Secrets;

var client = new SecretClient(new Uri("https://my-vault.vault.azure.net/"), new DefaultAzureCredential());
KeyVaultSecret secret = client.GetSecret("encryption-key");
byte[] key = Convert.FromBase64String(secret.Value);

Common Pitfalls

Using AES-CBC without authentication

// WRONG - CBC without MAC is malleable.
using var aes = Aes.Create();
aes.Mode = CipherMode.CBC;
// CORRECT - use AES-GCM for AEAD.
using var aes = new AesGcm(key);

Reusing nonces in GCM

// WRONG - reusing nonce breaks GCM security.
byte[] nonce = new byte[12];
// CORRECT - random nonce per encryption.
byte[] nonce = RandomNumberGenerator.GetBytes(12);

Using SHA-1 for signatures or HMAC

// WRONG - SHA-1 is deprecated.
using var hmac = new HMACSHA1(key);
// CORRECT - SHA-256 or stronger.
using var hmac = new HMACSHA256(key);

Low PBKDF2 iteration counts

// WRONG - 1000 iterations is far too low.
Rfc2898DeriveBytes.Pbkdf2(password, salt, 1000, HashAlgorithmName.SHA1, 16);
// CORRECT - 600,000+ with SHA-256.
Rfc2898DeriveBytes.Pbkdf2(password, salt, 600_000, HashAlgorithmName.SHA256, 32);

Hardcoding keys in source code

// WRONG - hardcoded key.
byte[] key = Encoding.UTF8.GetBytes("hardcoded_key_1234567890123456");
// CORRECT - load from Key Vault or environment.
byte[] key = Convert.FromBase64String(Environment.GetEnvironmentVariable("ENCRYPTION_KEY"));

Remediation Steps

Locate the Finding

  • Identify where encryption or hashing is performed.
  • Search for weak algorithms: DES, TripleDES, MD5, SHA1, RC2, RSA.Create(1024).
  • Find key generation sources: new Random() or hardcoded constants.

Understand the Data Flow

  • Trace how plaintext enters encryption methods.
  • Identify where keys are loaded and stored.
  • Confirm whether integrity/authentication is applied.

Identify the Pattern

  • Match code against vulnerable patterns above.
  • Verify cipher modes (ECB, CBC) and key sizes.
  • Check PBKDF2 iteration counts and salt usage.

Apply the Fix

PRIORITY 1: Use AES-256-GCM for symmetric encryption

byte[] key = RandomNumberGenerator.GetBytes(32);
string token = AesGcmEncryption.Encrypt("data", key);

PRIORITY 2: Use RSA-3072 with OAEP for small secrets

using var rsa = RSA.Create(3072);
byte[] enc = rsa.Encrypt(Encoding.UTF8.GetBytes("api-key"), RSAEncryptionPadding.OaepSHA256);

PRIORITY 3: Use bcrypt or Argon2 for passwords

string hash = BCrypt.Net.BCrypt.HashPassword("password", workFactor: 12);

PRIORITY 4: If constrained, use PBKDF2-SHA256 with high iterations

Verify the Fix

  • Run unit tests that confirm tampering fails.
  • Test with invalid ciphertext, wrong keys, and mismatched tags.
  • Re-run SAST scanners to confirm removal of weak algorithms.

Check for Similar Issues

  • Search for DES, TripleDES, MD5, SHA1, RC4, RSA.Create(1024).
  • Review shared crypto helpers used across services.
  • Inspect configuration for weak cipher suites or legacy flags.

Migration Considerations

Note: Changing algorithms or key sizes will make existing encrypted data unreadable.

What Breaks

  • Encrypted data: Data encrypted with DES/AES-CBC cannot be decrypted with AES-GCM.
  • Password hashes: MD5/SHA-1 hashes will not verify with bcrypt or Argon2.
  • API keys: RSA-1024 ciphertext must be re-encrypted with stronger keys.

Migration Approach

// SECURE - dual-read migration for encrypted fields.
using System;
using System.Security.Cryptography;
using System.Text;

public record EncryptedRecord(Guid Id, string Token, string Version);

public static class MigrationCrypto
{
    public static string DecryptLegacyDes(string token, byte[] key)
    {
        using var des = DES.Create();
        des.Key = key;
        des.Mode = CipherMode.ECB;
        using var decryptor = des.CreateDecryptor();
        byte[] data = Convert.FromBase64String(token);
        byte[] plain = decryptor.TransformFinalBlock(data, 0, data.Length);
        return Encoding.UTF8.GetString(plain).TrimEnd('\0', ' ');
    }

    public static string EncryptLegacyDes(string plaintext, byte[] key)
    {
        using var des = DES.Create();
        des.Key = key;
        des.Mode = CipherMode.ECB;
        using var encryptor = des.CreateEncryptor();
        int blockSize = des.BlockSize / 8;
        string padded = plaintext.PadRight(((plaintext.Length + blockSize - 1) / blockSize) * blockSize);
        byte[] plain = Encoding.UTF8.GetBytes(padded);
        byte[] cipher = encryptor.TransformFinalBlock(plain, 0, plain.Length);
        return Convert.ToBase64String(cipher);
    }

    public static string EncryptAesGcm(string plaintext, byte[] key)
    {
        byte[] nonce = RandomNumberGenerator.GetBytes(12);
        byte[] plain = Encoding.UTF8.GetBytes(plaintext);
        byte[] cipher = new byte[plain.Length];
        byte[] tag = new byte[16];
        using var aes = new AesGcm(key);
        aes.Encrypt(nonce, plain, cipher, tag);

        byte[] payload = new byte[nonce.Length + tag.Length + cipher.Length];
        Buffer.BlockCopy(nonce, 0, payload, 0, nonce.Length);
        Buffer.BlockCopy(tag, 0, payload, nonce.Length, tag.Length);
        Buffer.BlockCopy(cipher, 0, payload, nonce.Length + tag.Length, cipher.Length);
        return Convert.ToBase64String(payload);
    }

    public static string DecryptAesGcm(string token, byte[] key)
    {
        byte[] payload = Convert.FromBase64String(token);
        byte[] nonce = payload[..12];
        byte[] tag = payload[12..28];
        byte[] cipher = payload[28..];
        byte[] plain = new byte[cipher.Length];
        using var aes = new AesGcm(key);
        aes.Decrypt(nonce, cipher, tag, plain);
        return Encoding.UTF8.GetString(plain);
    }

    public static string DecryptWithMigration(
        EncryptedRecord record,
        byte[] oldKey,
        byte[] newKey,
        Action<Guid, string, string> saveUpgraded)
    {
        if (record.Version == "v1_des")
        {
            string legacy = DecryptLegacyDes(record.Token, oldKey);
            string upgraded = EncryptAesGcm(legacy, newKey);
            saveUpgraded(record.Id, upgraded, "v2_aesgcm");
            return legacy;
        }

        return DecryptAesGcm(record.Token, newKey);
    }
}

Implementation Steps:

  1. Add new AES-GCM implementation alongside legacy decrypt.
  2. Deploy dual-read support in production.
  3. Monitor migration progress and upgrade on access.
  4. Force remaining upgrades after threshold is reached.
  5. Remove legacy decrypt after completion.

Big-Bang Migration

// SECURE - one-time migration script.
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;

byte[] oldKey = Encoding.UTF8.GetBytes("legacyky"); // 8 bytes for DES
byte[] newKey = RandomNumberGenerator.GetBytes(32);

string legacyToken = MigrationCrypto.EncryptLegacyDes("secret", oldKey);
List<EncryptedRecord> records = new()
{
    new EncryptedRecord(Guid.NewGuid(), legacyToken, "v1_des"),
};

foreach (var record in records)
{
    string legacy = MigrationCrypto.DecryptLegacyDes(record.Token, oldKey);
    string upgraded = MigrationCrypto.EncryptAesGcm(legacy, newKey);
    UpdateRecord(record.Id, upgraded, "v2_aesgcm");
}

static void UpdateRecord(Guid id, string token, string version)
{
    // Persist to database in your storage layer.
}

Rollback Procedures

// ROLLBACK - restore from backup if migration fails (SQL Server example).
using Microsoft.Data.SqlClient;

static void RestoreDatabaseBackup(string connectionString, string dbName, string backupPath)
{
    using var connection = new SqlConnection(connectionString);
    connection.Open();

    string sql = $@"
ALTER DATABASE [{dbName}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
RESTORE DATABASE [{dbName}] FROM DISK = '{backupPath}' WITH REPLACE;
ALTER DATABASE [{dbName}] SET MULTI_USER;";

    using var command = new SqlCommand(sql, connection);
    command.ExecuteNonQuery();
}

RestoreDatabaseBackup("Server=.;Database=master;Trusted_Connection=True;", "MyApp", "C:\\backup\\MyApp.bak");

Monitoring and Metrics

  • Track percentage of records upgraded.
  • Monitor decryption error rates and latency.
  • Alert on unexpected legacy decrypt usage spikes.

Performance Considerations

  • AES-256-GCM is fast with hardware acceleration in modern CPUs.
  • Higher PBKDF2 iteration counts increase CPU cost; tune based on load tests.
  • Argon2 memory cost impacts server RAM usage; size parameters carefully.

Additional Resources