C#: password storage with a salt and PBKDF2

Derive a slow salted hash from a password and verify a login attempt in constant time.

C#
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)));
  }
}
Output
hash length: 32 bytes
right password: True
wrong password: False

Explanation

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.

Explore the dry run
Guided walkthroughReady
1using System;
2using System.Security.Cryptography;
3using System.Text;
4
5class Program {
6 static byte[] Derive(string password, byte[] salt) {
7 return Rfc2898DeriveBytes.Pbkdf2(Encoding.UTF8.GetBytes(password), salt, 600000, HashAlgorithmName.SHA256, 32);
8 }
9
10 static void Main() {
11 byte[] salt = RandomNumberGenerator.GetBytes(16);
12 byte[] stored = Derive("correct horse", salt);
13
14 Console.WriteLine("hash length: " + stored.Length + " bytes");
15 Console.WriteLine("right password: " + CryptographicOperations.FixedTimeEquals(stored, Derive("correct horse", salt)));
16 Console.WriteLine("wrong password: " + CryptographicOperations.FixedTimeEquals(stored, Derive("wrong guess", salt)));
17 }
18}

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
Prepared trace of the published program. Some rows group repeated iterations.
Step / lineWhat happensState after stepOutput 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

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.

Further reading