Cryptography and Hashing in .NET: SHA, BCrypt, HMAC, and Password Security
Master cryptography in .NET. Covers hashing (SHA-256, SHA-512), password hashing (BCrypt, PBKDF2), HMAC, digital signatures, salting, peppering, rainbow table attacks, and production password storage best practices.
What is Hashing?
Hashing converts input data of any length into a fixed-length output (digest). It is:
- One-way — you cannot reverse a hash to get the original input
- Deterministic — the same input always produces the same hash
- Avalanche effect — a tiny input change produces a completely different hash
- Fixed-length output — SHA-256 always outputs 256 bits, regardless of input size
"password123" → SHA256 → "ef92b778bafe771e89245b89ecbc08a44a4e166c06659911881f383d4473e94f"
"Password123" → SHA256 → "7b1f9a14c6d7ea6c9f0e8b3a4d2c1f5e0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d"
(one char different — completely different hash)SHA: Secure Hash Algorithms
Built into .NET — no NuGet package needed.
// SHA-256 (recommended — 256-bit output)
public static string ComputeSha256(string input)
{
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(input));
return Convert.ToHexString(bytes).ToLower();
}
// SHA-512 (stronger — 512-bit output)
public static string ComputeSha512(string input)
{
var bytes = SHA512.HashData(Encoding.UTF8.GetBytes(input));
return Convert.ToHexString(bytes).ToLower();
}
// Usage
string hash = ComputeSha256("Hello, World!");
// → "dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986d"
// Verify a hash
bool isMatch = ComputeSha256(input) == storedHash;SHA Family
| Algorithm | Output Size | Security | Use Case | |---|---|---|---| | MD5 | 128 bits | Broken | Legacy only — never for passwords | | SHA-1 | 160 bits | Deprecated | Legacy only | | SHA-256 | 256 bits | Strong | File integrity, token signing | | SHA-384 | 384 bits | Very strong | High-security contexts | | SHA-512 | 512 bits | Very strong | Maximum security |
Never use MD5 or SHA-1 for security purposes — both have known collision attacks.
The Password Storage Problem
SHA alone is not suitable for passwords. Here's why:
// ❌ Never store passwords as plain SHA hashes
string hash = ComputeSha256("password123");
// → always the same hash — vulnerable to precomputed rainbow tables
// Anyone who has ever computed SHA256("password123") knows this hash maps to "password123"Rainbow table attack: Attackers precompute hashes for millions of common passwords. They look up your stored hash in their table and instantly find the original password.
Salting
A salt is a random value added to the password before hashing. Even if two users have the same password, their salted hashes will differ.
public class PasswordHasher
{
public (string Hash, string Salt) HashPassword(string password)
{
// Generate a random salt
var salt = RandomNumberGenerator.GetBytes(32);
var saltHex = Convert.ToHexString(salt);
// Combine password + salt, then hash
var combined = Encoding.UTF8.GetBytes(password + saltHex);
var hash = SHA256.HashData(combined);
var hashHex = Convert.ToHexString(hash);
return (hashHex, saltHex);
}
public bool VerifyPassword(string password, string storedHash, string storedSalt)
{
var combined = Encoding.UTF8.GetBytes(password + storedSalt);
var hash = Convert.ToHexString(SHA256.HashData(combined));
return hash == storedHash;
}
}But salted SHA is still too fast — a modern GPU can compute billions of SHA-256 hashes per second. You need a slow hash function for passwords.
BCrypt (Recommended for Passwords)
BCrypt is specifically designed for password hashing:
- Slow by design — configurable work factor (cost)
- Built-in salt — automatically generates and embeds the salt
- Future-proof — increase the work factor as hardware improves
dotnet add package BCrypt.Net-Next// Hash a password
string hashedPassword = BCrypt.Net.BCrypt.HashPassword("MyPassword123!", workFactor: 12);
// → "$2a$12$SomeRandomSaltEmbeddedHereLongHashValueHere..."
// Verify
bool isValid = BCrypt.Net.BCrypt.Verify("MyPassword123!", hashedPassword);
// User registration
public async Task<User> RegisterAsync(RegisterRequest request, CancellationToken ct)
{
if (await _users.ExistsAsync(request.Email, ct))
throw new ConflictException("Email already registered.");
var user = new User
{
Email = request.Email,
PasswordHash = BCrypt.Net.BCrypt.HashPassword(request.Password, workFactor: 12),
CreatedAt = DateTime.UtcNow
};
await _users.AddAsync(user, ct);
return user;
}
// User login
public async Task<string> LoginAsync(LoginRequest request, CancellationToken ct)
{
var user = await _users.GetByEmailAsync(request.Email, ct)
?? throw new UnauthorizedException("Invalid email or password.");
if (!BCrypt.Net.BCrypt.Verify(request.Password, user.PasswordHash))
throw new UnauthorizedException("Invalid email or password.");
return GenerateJwtToken(user);
}Work factor guide: 12 takes ~250ms on modern hardware. Increase as hardware improves. Never go below 10.
PBKDF2 (Built-in .NET)
PBKDF2 is built into .NET — no extra package needed. Used by ASP.NET Core Identity by default.
public class Pbkdf2Hasher
{
private const int Iterations = 600_000; // NIST 2023 recommendation
private const int HashSize = 32; // SHA-256 output
private const int SaltSize = 16;
public string HashPassword(string password)
{
var salt = RandomNumberGenerator.GetBytes(SaltSize);
var hash = Rfc2898DeriveBytes.Pbkdf2(
password,
salt,
Iterations,
HashAlgorithmName.SHA256,
HashSize);
// Store: iterations:salt:hash (all base64)
return $"{Iterations}:{Convert.ToBase64String(salt)}:{Convert.ToBase64String(hash)}";
}
public bool VerifyPassword(string password, string storedHash)
{
var parts = storedHash.Split(':');
var iterations = int.Parse(parts[0]);
var salt = Convert.FromBase64String(parts[1]);
var hash = Convert.FromBase64String(parts[2]);
var computed = Rfc2898DeriveBytes.Pbkdf2(
password, salt, iterations, HashAlgorithmName.SHA256, HashSize);
return CryptographicOperations.FixedTimeEquals(computed, hash);
}
}HMAC (Hash-based Message Authentication Code)
HMAC uses a secret key to produce a hash. Used for API request signing, webhook verification, and JWT signing.
// Create HMAC signature
public static string ComputeHmacSha256(string message, string secretKey)
{
var keyBytes = Encoding.UTF8.GetBytes(secretKey);
var messageBytes = Encoding.UTF8.GetBytes(message);
var hashBytes = HMACSHA256.HashData(keyBytes, messageBytes);
return Convert.ToHexString(hashBytes).ToLower();
}
// Verify a webhook signature (e.g., from Stripe, GitHub)
public bool VerifyWebhookSignature(
string payload,
string receivedSignature,
string webhookSecret)
{
var expectedSig = ComputeHmacSha256(payload, webhookSecret);
// Use FixedTimeEquals to prevent timing attacks
return CryptographicOperations.FixedTimeEquals(
Convert.FromHexString(expectedSig),
Convert.FromHexString(receivedSignature));
}Peppering
A pepper is a secret value added to ALL passwords before hashing — stored in config, not the database. Even if the database is stolen, hashes are useless without the pepper.
public class PepperedHasher
{
private readonly string _pepper;
public PepperedHasher(IConfiguration config)
=> _pepper = config["Security:PasswordPepper"]!;
public string HashPassword(string password)
{
// pepper + password, THEN bcrypt (bcrypt handles the salt)
var pepperedPassword = _pepper + password;
return BCrypt.Net.BCrypt.HashPassword(pepperedPassword, workFactor: 12);
}
public bool Verify(string password, string storedHash)
{
var pepperedPassword = _pepper + password;
return BCrypt.Net.BCrypt.Verify(pepperedPassword, storedHash);
}
}Attack Types and Defences
| Attack | Description | Defence |
|---|---|---|
| Rainbow table | Precomputed hash-to-password table | Salting (each password has unique hash) |
| Brute force | Try all possible passwords | Slow hashing (BCrypt/PBKDF2), rate limiting |
| Dictionary attack | Common password wordlist | Slow hashing, password strength requirements |
| Timing attack | Guess via response time | CryptographicOperations.FixedTimeEquals |
| Database breach | Stolen hashed passwords | BCrypt + pepper — can't crack without pepper |
| Collision attack | Two inputs produce same hash | Use SHA-256+ (SHA-1/MD5 are vulnerable) |
ASP.NET Core Identity (Built-in)
ASP.NET Core Identity uses PBKDF2 with SHA-256, 600,000 iterations by default:
builder.Services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<AppDbContext>()
.AddDefaultTokenProviders();
// Increase password hashing iterations (optional — defaults are safe)
builder.Services.Configure<PasswordHasherOptions>(options =>
{
options.IterationCount = 600_000;
});File Integrity Verification
SHA is appropriate for verifying file integrity (not passwords):
public static async Task<string> ComputeFileHashAsync(string filePath, CancellationToken ct)
{
await using var stream = File.OpenRead(filePath);
var hashBytes = await SHA256.HashDataAsync(stream, ct);
return Convert.ToHexString(hashBytes).ToLower();
}
// Verify downloaded file
var downloadedHash = await ComputeFileHashAsync("installer.exe", ct);
var expectedHash = "a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e";
if (downloadedHash != expectedHash)
throw new SecurityException("File integrity check failed — file may be corrupted or tampered.");Interview Questions
Q: Why can't you use SHA-256 alone for password storage? SHA-256 is designed to be fast — a modern GPU can compute billions of hashes per second. Fast hashing makes brute-force attacks feasible. Additionally, identical passwords produce identical hashes (without salting), enabling rainbow table lookups. Use BCrypt or PBKDF2 — they're intentionally slow and include salting.
Q: What is the difference between a salt and a pepper? A salt is random, unique per password, stored alongside the hash. It prevents rainbow table attacks. A pepper is a secret value shared across all passwords, stored in configuration (not the database). A stolen database with salted+peppered hashes is useless without the pepper.
Q: What is a timing attack and how do you prevent it?
Comparing strings with == short-circuits at the first mismatch — the response time leaks information about how many characters are correct. CryptographicOperations.FixedTimeEquals always takes the same time regardless of where the mismatch is, preventing timing-based inference.
Q: When would you use HMAC instead of plain SHA? HMAC uses a secret key — it proves the message came from someone who knows the key. Use it for API request signing, webhook verification (only the legitimate sender knows the secret), JWT HS256 signing, and anywhere you need both integrity and authenticity.
Enjoyed this article?
Explore the Backend Systems learning path for more.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.