C#: password storage with a salt and PBKDF2
Derive a slow salted hash from a password and verify a login attempt in constant time.
On this page
C# bytes and the System.Security.Cryptography namespace.
- Derive a slow salted hash from a password and verify a login attempt in constant time.
- Predict the sample output and explain the state changes.
- Complete the practice task and check the boundary cases.
The concept
Passwords must be stored so that a database leak does not reveal them. The recommended approach is a slow, salted password hash. The salt is a random value stored beside the hash. It makes equal passwords produce different hashes and defeats precomputed tables.
Rfc2898DeriveBytes.Pbkdf2 applies HMAC many times, and the iteration count sets the cost for an attacker. Current guidance, such as the OWASP Password Storage Cheat Sheet, recommends hundreds of thousands of iterations for PBKDF2 with HMAC-SHA-256. To log in, derive the hash again from the typed password and the stored salt, then compare in constant time.
Code example
using System;
using System.Security.Cryptography;
using System.Text;
class Program {
static byte[] Derive(string password, byte[] salt) {
return Rfc2898DeriveBytes.Pbkdf2(Encoding.UTF8.GetBytes(password), salt, 600000, HashAlgorithmName.SHA256, 32);
}
static void Main() {
byte[] salt = RandomNumberGenerator.GetBytes(16);
byte[] stored = Derive("correct horse", salt);
Console.WriteLine("hash length: " + stored.Length + " bytes");
Console.WriteLine("right password: " + CryptographicOperations.FixedTimeEquals(stored, Derive("correct horse", salt)));
Console.WriteLine("wrong password: " + CryptographicOperations.FixedTimeEquals(stored, Derive("wrong guess", salt)));
}
}hash length: 32 bytes right password: True wrong password: False
Follow the code
Open the walkthrough to inspect the example one step at a time.
Show guided walkthrough
using System;using System.Security.Cryptography;using System.Text; class Program { static byte[] Derive(string password, byte[] salt) { return Rfc2898DeriveBytes.Pbkdf2(Encoding.UTF8.GetBytes(password), salt, 600000, HashAlgorithmName.SHA256, 32); } static void Main() { byte[] salt = RandomNumberGenerator.GetBytes(16); byte[] stored = Derive("correct horse", salt); Console.WriteLine("hash length: " + stored.Length + " bytes"); Console.WriteLine("right password: " + CryptographicOperations.FixedTimeEquals(stored, Derive("correct horse", salt))); Console.WriteLine("wrong password: " + CryptographicOperations.FixedTimeEquals(stored, Derive("wrong guess", salt))); }}Follow the example
Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.
View the complete dry-run table
| Step / line | What happens | State after step | Output so far |
|---|---|---|---|
| A cryptographically secure random 16-byte salt. | salt = 16 random bytes | — | |
| Store the salt, the iteration count, and this 32-byte hash, never the password. | stored = 32 bytes | — | |
| Deriving again with the same salt reproduces the same hash. | stored = 32 bytes | hash length: 32 bytes right password: True | |
| A different password gives a different hash. | stored = 32 bytes | hash length: 32 bytes right password: True wrong password: False |
Behind the scenes
How C# processes this code
C# compiles to code for the .NET runtime, which invokes the program entry point.
Cost and common mistakes
O(iterations) per hash, deliberately expensive.
- Use RandomNumberGenerator, not System.Random, for security values such as salts and tokens.
- A global salt shared by all users is much weaker than a unique salt per user.
- Store the algorithm and iteration count next to the hash so you can raise the cost later. Argon2id or scrypt are also good choices when available.
Try it yourself
Why compare with FixedTimeEquals rather than ==?
Reveal answer and reasoning
A normal comparison can stop at the first difference, and the time it takes can leak information about the stored hash.