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.
Let’s start.
VIDEO: Hashing and Salting Passwords in .NET.
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.
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.
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 CryptographicOperations.FixedTimeEquals(hashToCompare, Convert.FromHexString(hash)); }
Here, we define the VerifyPassword()
method 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.
In opposition to other comparison methods, CryptographicOperations.FixedTimeEquals()
will take the same amount of time to do the hash comparison regardless of the input hash correctness. This is necessary because hackers can infer information about the internal state of our system based on execution time variations allowing them to potentially guess the correct password. These are called timing side-channel exploits.
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.
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.