Cryptography is one of those areas where "works correctly" and "is secure" are completely different things. .NET provides robust cryptographic primitives, but using them incorrectly is easy and the consequences are severe. This guide covers the practical patterns you'll actually use: password hashing, symmetric encryption, and data integrity verification.
Password Hashing
Passwords must be hashed with a slow, salted algorithm. SHA-256 is fast — that's the problem. An attacker with a GPU can compute billions of SHA-256 hashes per second. Use a password hashing function designed to be computationally expensive.
Using ASP.NET Core Identity's Hasher
If you're using Identity, PasswordHasher<T> handles everything:
var hasher = new PasswordHasher<User>();
// Hash a password
var hash = hasher.HashPassword(user, "my-secure-password");
// Verify a password
var result = hasher.VerifyHashedPassword(user, hash, "my-secure-password");
if (result == PasswordVerificationResult.Success)
{
// Password is correct
}
else if (result == PasswordVerificationResult.SuccessRehashNeeded)
{
// Password is correct but was hashed with older settings
// Rehash with current settings
var newHash = hasher.HashPassword(user, "my-secure-password");
}
Under the hood, this uses PBKDF2 with HMAC-SHA512, 128-bit salt, and 100,000 iterations (as of .NET 8). The SuccessRehashNeeded result lets you transparently upgrade hashes when security parameters increase.
Using Bcrypt Directly
For non-Identity applications, the BCrypt.Net-Next package is a solid choice:
using BCrypt.Net;
// Hash with a work factor of 12
var hash = BCrypt.Net.BCrypt.EnhancedHashPassword("my-password", 12);
// Verify
var isValid = BCrypt.Net.BCrypt.EnhancedVerify("my-password", hash);
The work factor controls computational cost. Each increment doubles the time. Start at 12 and increase as hardware improves.
Symmetric Encryption with AES
For encrypting data (not passwords — passwords are hashed, never encrypted), use AES with authenticated encryption:
public static class AesEncryption
{
public static (byte[] ciphertext, byte[] nonce, byte[] tag) Encrypt(
byte[] plaintext, byte[] key)
{
var nonce = new byte[AesGcm.NonceByteSizes.MaxSize]; // 12 bytes
RandomNumberGenerator.Fill(nonce);
var tag = new byte[AesGcm.TagByteSizes.MaxSize]; // 16 bytes
var ciphertext = new byte[plaintext.Length];
using var aes = new AesGcm(key, tag.Length);
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, tag.Length);
aes.Decrypt(nonce, ciphertext, tag, plaintext);
return plaintext;
}
}
AES-GCM provides both confidentiality and authenticity — if anyone tampers with the ciphertext, decryption fails. Never use AES-CBC without a separate MAC; it's vulnerable to padding oracle attacks.
Generating Keys
Use cryptographically secure random number generation for keys:
// Generate a 256-bit AES key
var key = RandomNumberGenerator.GetBytes(32);
// Store it securely (Key Vault, DPAPI, etc.)
var base64Key = Convert.ToBase64String(key);
Never derive encryption keys from passwords directly. If you must use a password, use a key derivation function:
public static byte[] DeriveKeyFromPassword(string password, byte[] salt)
{
return Rfc2898DeriveBytes.Pbkdf2(
password,
salt,
iterations: 100_000,
hashAlgorithm: HashAlgorithmName.SHA256,
outputLength: 32);
}
Hashing for Integrity
When you need to verify data hasn't been tampered with (not passwords), use SHA-256 or SHA-512:
// Hash a file
await using var stream = File.OpenRead("document.pdf");
var hash = await SHA256.HashDataAsync(stream);
var hashString = Convert.ToHexStringLower(hash);
For message authentication (proving data came from someone who holds a shared secret), use HMAC:
public static byte[] ComputeHmac(byte[] data, byte[] key)
{
return HMACSHA256.HashData(key, data);
}
public static bool VerifyHmac(byte[] data, byte[] key, byte[] expectedMac)
{
var computedMac = HMACSHA256.HashData(key, data);
return CryptographicOperations.FixedTimeEquals(computedMac, expectedMac);
}
FixedTimeEquals is critical here. Regular byte comparison short-circuits on the first difference, leaking timing information that attackers can exploit.
Webhook Signature Verification
A common real-world use of HMAC — verifying webhook signatures:
app.MapPost("/webhooks/stripe", async (HttpContext context) =>
{
context.Request.EnableBuffering();
using var reader = new StreamReader(context.Request.Body);
var body = await reader.ReadToEndAsync();
var signature = context.Request.Headers["Stripe-Signature"].ToString();
var secret = Encoding.UTF8.GetBytes(configuration["Stripe:WebhookSecret"]!);
var payload = Encoding.UTF8.GetBytes(body);
var expectedSignature = Convert.ToHexStringLower(
HMACSHA256.HashData(secret, payload));
if (!CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(expectedSignature),
Encoding.UTF8.GetBytes(signature)))
{
return Results.Unauthorized();
}
// Process the webhook
return Results.Ok();
});
What Not to Do
Don't use MD5 or SHA-1 for security purposes. Both have known collision vulnerabilities. Use SHA-256 or SHA-512.
Don't encrypt passwords. Encryption is reversible. If your database is breached, the attacker gets the key and every password. Hash passwords so they can't be reversed.
Don't reuse nonces or IVs. Every encryption operation needs a unique nonce. Reusing one with the same key can completely break AES-GCM security.
Don't implement your own cryptographic algorithms. Use the primitives provided by .NET's System.Security.Cryptography namespace. They've been reviewed, tested, and hardened.
Wrapping Up
The rules are straightforward: hash passwords with bcrypt or PBKDF2, encrypt data with AES-GCM, verify integrity with HMAC-SHA256, and use CryptographicOperations.FixedTimeEquals for comparisons. .NET's cryptography APIs handle the hard parts — your job is to choose the right tool for the job and avoid the well-documented pitfalls.