Choosing a password hashing algorithm for C#: fast isn't the best

Posted: (EET/GMT+2)

 

If you need to store passwords, you might have heard the wisdom: do not store the password in plain texts, and do not hash just once. For example, the latest OWASP guidance talks about this in detail.

But when it comes selecting the hashing algorithm itself, the wisdom continues: "do not use a normal fast hash function". Somewhat counterintuitively, this includes otherwise strong cryptographic algorithms such as SHA-256 or HMAC-SHA256.

The problem is not that HMAC-SHA256 is weak or that the implementation in .NET would be buggy. The problem is that it is (too) fast for the purposes of password hashing.

For many cryptographic operations, speed is a good thing. For password verification, we actually want each attempt to take a small but noticeable amount of computational work. If an attacker obtains your password database, they can try password guesses offline. A fast hash lets them try huge numbers of guesses very quickly.

A password hashing algorithm deliberately makes that work more expensive.

One option available directly in .NET is PBKDF2 (Password-Based Key Derivation Function 2). Here's an example:

using System.Security.Cryptography;
...
Rfc2898DeriveBytes.Pbkdf2(...)

PBKDF2 repeatedly applies a pseudorandom function such as HMAC-SHA256. The iteration count controls how much work is required. Here is an improved example:

using System.Security.Cryptography;

// set parameters
const int SaltSize = 16;
const int HashSize = 32;
const int Iterations = 600_000;
string password = "correct 1 horse 2 battery 3 staple";

// calculate the hash
byte[] salt = RandomNumberGenerator.GetBytes(SaltSize);
byte[] hash = Rfc2898DeriveBytes.Pbkdf2(password, salt,
        Iterations, HashAlgorithmName.SHA256, HashSize);

// write the output
Console.WriteLine(Convert.ToBase64String(salt));
Console.WriteLine(Convert.ToBase64String(hash));

The salt should be random and unique for every password. Remember that the salt is not a secret. You can store it together with the password hash.

You should also store the algorithm and iteration count so that the password can be verified later and upgraded when recommendations change. For example, a stored value might conceptually look like this:

pbkdf2-sha256/600000/<salt>$<hash>

To verify a password, run PBKDF2 again using the same salt and parameters:

byte[] candidateHash = Rfc2898DeriveBytes.Pbkdf2(enteredPassword,
    storedSalt, storedIterations, HashAlgorithmName.SHA256,
    storedHash.Length);

bool valid = CryptographicOperations.FixedTimeEqual(
    candidateHash, storedHash);

Use CryptographicOperations.FixedTimeEquals instead of a normal byte-by-byte comparison. It avoids leaking useful timing differences when comparing cryptographic values.

So why not just use HMACSHA256 directly?

You certainly can calculate an HMAC:

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

byte[] passwordBytes = Encoding.UTF8.GetBytes(password);
using HMACSHA256 hmac = new HMACSHA256(secretKey);
byte[] hash = hmac.ComputeHash(passwordBytes);

But this is not a good password storage function by itself.

HMAC-SHA256 is designed to be fast. PBKDF2-HMAC-SHA256 intentionally performs the underlying operation many times.

The difference can be summarized like this:

  • HMACSHA256: fast keyed message authentication
  • PBKDF2-HMAC-SHA256: deliberately expensive password derivation.

As of this writing, OWASP recommends 600,000 iterations for PBKDF2-HMAC-SHA256.

Do not treat that number as permanent. Hardware gets faster and password hashing recommendations change over time. Also remember that PBKDF2 is not the only password hashing algorithm. Modern guidance often prefers memory-hard algorithms such as Argon2id when an appropriate implementation is available.

For applications using ASP.NET Core Identity, do not normally build your own password storage format at all. Use the framework's PasswordHasher, which manages the hashing format and versioning for you (more info here).

The low-level PBKDF2 API is more appropriate when you are implementing an existing protocol, a custom storage format, or another scenario where you explicitly need control over the parameters.

Good rules:

  • never store passwords in plaintext!
  • do not use SHA-256 or HMAC-SHA256 alone for password storage
  • use a unique cryptographically random salt for every password
  • use a deliberately slow password hashing or derivation algorithm
  • store the algorithm and work factor together with the hash
  • review the work factor periodically
  • use framework password hashing when the framework already provides it.

With password hashing, slower is not an unfortunate side effect. It is part of the security design.

Keep it safe!