C#
Password Hashing
Bcrypt
PBKDF2
Security

`Hash` Password in C? Bcrypt/PBKDF2

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

For password storage in C#, the real goal is not “hash a string” but “store passwords with a slow, salted password hashing algorithm and verify them safely later”. Between bcrypt and PBKDF2, both can be acceptable, but PBKDF2 has strong built-in support in .NET while bcrypt typically comes from a third-party library.

Choosing Between bcrypt and PBKDF2

The first practical rule is that passwords should not be stored with a fast hash such as SHA-256 alone. Password hashing needs a per-password salt and a cost setting that makes offline guessing expensive.

In current guidance, Argon2id is generally preferred when available, bcrypt remains acceptable in many legacy systems, and PBKDF2 is especially common in .NET because it is built in and widely approved in regulated environments.

For a C# application, PBKDF2 is often the easiest defensible choice because you can implement it with standard library APIs and avoid inventing your own format.

A Runnable PBKDF2 Example in C#

This example uses a random salt, Rfc2898DeriveBytes.Pbkdf2, and constant-time verification.

csharp
1using System;
2using System.Security.Cryptography;
3
4public static class PasswordStore
5{
6    private const int SaltSize = 16;
7    private const int KeySize = 32;
8    private const int Iterations = 600_000;
9
10    public static string HashPassword(string password)
11    {
12        byte[] salt = RandomNumberGenerator.GetBytes(SaltSize);
13        byte[] key = Rfc2898DeriveBytes.Pbkdf2(
14            password,
15            salt,
16            Iterations,
17            HashAlgorithmName.SHA256,
18            KeySize);
19
20        return string.Join(
21            ":",
22            "PBKDF2-SHA256",
23            Iterations,
24            Convert.ToBase64String(salt),
25            Convert.ToBase64String(key));
26    }
27
28    public static bool VerifyPassword(string password, string stored)
29    {
30        string[] parts = stored.Split(':');
31        if (parts.Length != 4 || parts[0] != "PBKDF2-SHA256")
32            return false;
33
34        int iterations = int.Parse(parts[1]);
35        byte[] salt = Convert.FromBase64String(parts[2]);
36        byte[] expected = Convert.FromBase64String(parts[3]);
37
38        byte[] actual = Rfc2898DeriveBytes.Pbkdf2(
39            password,
40            salt,
41            iterations,
42            HashAlgorithmName.SHA256,
43            expected.Length);
44
45        return CryptographicOperations.FixedTimeEquals(actual, expected);
46    }
47}

That format stores the algorithm label, iteration count, salt, and derived key. Storing the parameters with the hash makes future upgrades easier.

Where bcrypt Fits

bcrypt is still widely used and can be a reasonable choice if your team already depends on a mature bcrypt library such as BCrypt.Net-Next. Its main strengths are simplicity and an adaptive work factor.

However, bcrypt has an important input limit around 72 bytes in many implementations. That matters if you allow long passphrases or arbitrary Unicode-heavy input. It is one reason teams often prefer Argon2id or PBKDF2 in newer designs.

A typical bcrypt usage in C# looks like this:

csharp
string hash = BCrypt.Net.BCrypt.HashPassword(password, workFactor: 12);
bool ok = BCrypt.Net.BCrypt.Verify(password, hash);

That is concise, but it depends on an external package and the library’s behavior should still be reviewed before adoption.

Operational Guidance

Whatever algorithm you choose, store enough metadata to support migration later. A good stored format includes:

  • algorithm name
  • work factor or iteration count
  • salt
  • derived hash

Then, when your policy changes, you can verify old hashes and transparently rehash on the next successful login.

Also remember that password hashing is only one part of the authentication story. Rate limiting, MFA, breached-password screening, and secure reset flows still matter.

What Not to Do

Do not write your own crypto primitives. Do not store raw SHA-256 of the password. Do not reuse the same salt across users. Do not compare hashes with ordinary string equality when a constant-time comparison API is available.

Also avoid outdated .NET examples that use old instance patterns when newer one-shot APIs are available. In current .NET guidance, the one-shot Pbkdf2 method is the cleaner approach.

Common Pitfalls

The biggest pitfall is choosing a fast general-purpose hash instead of a password hashing scheme. That makes brute-force attacks dramatically easier.

Another mistake is omitting the salt or hard-coding it globally. Each password needs its own random salt.

A third issue is forgetting the algorithm’s limits. bcrypt’s password-length behavior is not the same as PBKDF2’s, so the choice affects input handling and migration planning.

Summary

  • In C#, PBKDF2 is often the easiest secure built-in option for password storage.
  • bcrypt is still acceptable in many systems but commonly relies on a third-party package and has input-length limits.
  • Always use a unique random salt and store algorithm parameters with the hash.
  • Verify using constant-time comparison.
  • Do not use plain SHA-256 or any other fast hash by itself for password storage.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.