In a system that stores user credentials, we must pay careful attention to how we store passwords. We must never store passwords in plain text. In this article, we will learn how to use hashing and salting techniques in C# to safely store user passwords.

To download the source code for this article, you can visit our GitHub repository.

Let’s start.

Password Hashing

Hashing a password means applying a one-way encryption algorithm that will produce a cryptographic string. One-way encryption can’t be decrypted, making it impossible to infer the original password from the hashed string.

Hashing algorithms are deterministic, meaning that the same input string will always produce the same hash.

By storing a hashed version of passwords, we ensure that user credentials will still be safe in case of a data breach. Attackers won’t be able to decrypt hashes to obtain original passwords.

Choosing a Hashing Algorithm

An attacker that gains access to password hashes can still try to use brute force on them. That is, obtaining the original passwords by hashing every potential password and comparing the resulting hash to what’s in the breached database.

The probability of such a procedure succeeding depends on how fast the attacker can hash a collection of passwords. Consequently, we consider password-hashing algorithms more secure the more costly in terms of time and memory consumption they are.

However, as more and more computational power becomes available to the general public, hashing algorithms considered secure enough for password storage in the past became insecure. This is due to hackers being able to breach them via brute force. Some examples are the old MD5 and SHA algorithms. We must never use these for password hashing.

On the other hand, several hashing algorithms are safe:

  • PBKDF2
  • BCrypt/SCrypt
  • Argon2

PBKDF2 is a popular key-derivation password hashing algorithm that is available natively in the .NET framework. Unlike MD5 or SHA, PBKDF2 enhances password protection to brute-force by adding extra complexity.

The BCrypt/SCrypt family of algorithms is designed to be CPU and memory intensive, especially the newer SCrypt, which is very resistant to brute-force attacks.

Argon2 and its variants are often regarded as the best password hashing algorithm in the market since they are designed to prevent some of the newer and most advanced hacking techniques.

Hashing Isn’t Enough: Password Salting

Unfortunately, hashing is not enough to keep passwords safe from bad actors. One downside of hashing algorithms is that they always produce the same hash given the same password.

Meaning that once a single password hash has been compromised it might very well be that more than one user is affected because they decided to use the same password. In addition to that, there are advanced techniques that help for easier brute-force password cracking like precomputed hash tables, rainbow tables, or the abuse of hash collisions.

To avoid this, we must salt our passwords before storing them. Salting is the process in which we add a random piece of information to the password before hashing, making every password hash unique. This will render brute-force attacks much more difficult.

Wanna join Code Maze Team, help us produce more awesome .NET/C# content and get paid? >> JOIN US! <<

Best Practices for Password Salting

We must use a different salt for each password. We discourage using a system-wide salt since it makes this technique much less effective. If possible, it is a good idea to store password salts separately.

For maximum effect, our password salt must be hard to guess for an attacker. The recommendation is to use a cryptographic random number generator such as RandomNumberGenerator in the System.Security.Cryptography package to generate our password salt.

Finally, we must ensure that password salts are long enough to be effective. A good rule of thumb is to make them the same size as our output hash.

Hashing and Salting Passwords in C# With PBKDF2

PBKDF2 is a key-derivation function that we can use to generate secure password hashes. It has been part of the framework since .NET 6.

We will use the Rfc2898DeriveBytes.Pbkdf2() static method. The method takes several parameters including a salt. To generate a proper random salt, let’s use the RandomNumberGenerator.GetBytes() static method:

const int keySize = 64;
const int iterations = 350000;
HashAlgorithmName hashAlgorithm = HashAlgorithmName.SHA512;

string HashPasword(string password, out byte[] salt)
{
    salt = RandomNumberGenerator.GetBytes(keySize);

    var hash = Rfc2898DeriveBytes.Pbkdf2(
        Encoding.UTF8.GetBytes(password),
        salt,
        iterations,
        hashAlgorithm,
        keySize);

     return Convert.ToHexString(hash);
}

Here, we define the HashPassword() function that takes a password in clear text and returns its hashed version. We obtain the random salt generated along the process via the salt output parameter. It is important to get and store this piece of data since it is necessary for later password verification.

To start with, we define the keySize constant as the desired size in bytes of the resulting hash and the size of the random salt. We should align this value with the hash size that the underlying hashing algorithm produces.

PBKDF2 can be applied multiple times to a given input value to strengthen the resulting hash, so we define the iterations constant. Reportedly, safe values for production environments are in the hundreds of thousands.

Wanna join Code Maze Team, help us produce more awesome .NET/C# content and get paid? >> JOIN US! <<

Finally, in the hashAlgorithm variable, we define the underlying hashing method PBKDF2 will use for derivation, SHA512 in this case.

Let’s try and use our hashing functionality to produce some results:

var hash = HashPasword("clear_password", out var salt);

Console.WriteLine($"Password hash: {hash}");
Console.WriteLine($"Generated salt: {Convert.ToHexString(salt)}");

Here, we use our HashPassword() function to obtain the hash associated with the clear text password as a first parameter and the random salt in the salt output parameter, which we display on the console:

Password hash: 22C5D20222F190966E71921E074D6EF254616C3536ABC192CA111D25E707B46592717C70BE06592997A73F13ED2884FF63775C78A2FA73CFDD6F7A3DCF296086
Generated salt: 9003A697CA6F038B5140A9A86D000899E1521C4B29BE5996E452882E2103D2404AEB3F2EB89DECB63310D8F6B3B02FF15323CE8DE4F9F7547641D5A2FFB1F698

Verifying Hashed Passwords

Once our user password is securely stored, the other important flow to consider is the verification of user passwords when they attempt to log in to the system. Since we can’t decrypt hash algorithms, we must hash the incoming password again and compare the result with the hashed version we originally stored for the user. The correct password will always have the same hash:

bool VerifyPassword(string password, string hash, byte[] salt)
{
    var hashToCompare = Rfc2898DeriveBytes.Pbkdf2(password, salt, iterations, hashAlgorithm, keySize);

    return hashToCompare.SequenceEqual(Convert.FromHexString(hash));
}

Here, we define the VerifyPassword() function that takes the clear password, the stored password hash for the user, and the associated salt. It will return true if the provided clear password generates the same hash.

Very importantly, we need to provide value for iterations, hashAlgorithm, and keySize that match the values used in the initial hashing.

Hashing Helper Classes and Libraries

BCrypt.Net-Next is a password-hashing third-party library based on the BCrypt algorithm.

PasswordHasher<TUser> class is part of the Microsoft.AspNetCore.Identity package that implements password hashing and verification based on PBKDF2 with random salts and iterations.

Wanna join Code Maze Team, help us produce more awesome .NET/C# content and get paid? >> JOIN US! <<

Conclusion

In this article, we have learned what password hashing is and why we must never store passwords in plain text. We also learned what makes a good hashing algorithm.

We have learned what’s password salting and how to use it to strengthen our password security.

Finally, we reviewed an example of how to use PBKDF2 in .NET for hashing and salting passwords, along with the verification.