
Password hashing is a valuable security feature that helps to ensure users’ passwords are encrypted at rest when saved in user data stores, such as databases. Hashing works on the principle of reliable/repeatable one-way encrypting of data.
Here is a simple class for implementing password hashing. It adopts best practice, and avoids some of the pitfalls of simpler schemes.
NOTE (1): For this to work across calls and application lifecycles, the hashing parameters (maximum password length, salt length, and salt insertion offset) must be retained.
NOTE (2): This was written a long time ago, and the RNGCryptoServiceProvider, Rfc2898DeriveBytes, etc. class’ instance methods have been made obsolete as they are no longer secure.
using System;
using System.Security.Cryptography;
namespace My.Security
{
/// <summary>
/// Provides methods that support password-hashing so passwords aren't stored
/// as plain text for security reasons.
/// </summary>
public class HashMyPassword
{
private readonly int _maxLength;
private readonly int _saltLength;
private readonly int _saltOffset;
/// <summary>
/// Creates a new instance of the <see cref="HashMyPassword"/> class,
/// using default configuration values.
/// (Max password length: 20, salt-length: 16, salt-offset: 7.)
/// </summary>
public HashMyPassword()
: this(20, 16, 7)
{
}
/// <summary>
/// Creates a new instance of the <see cref="HashMyPassword"/> class,
/// explicitly configuring the hashing function.</summary>
/// <param name="maxLength">The maximum length a password can be.</param>
/// <param name="saltLength">
/// The length of the salt used by the hashing function.
/// </param>
/// <param name="saltOffset">The offset to insert the salt in the hash.</param>
public HashMyPassword(int maxLength, int saltLength, int saltOffset)
{
if (maxLength < 8)
{
throw new ArgumentException(
"Passwords of less than 8 characters are not " +
"supported for security reasons.");
}
if (saltLength < 8)
{
throw new ArgumentException(
"Short salt lengths are not supported for security " +
"reasons. 8 is the minimum.");
}
if (saltOffset < 0 || saltOffset > maxLength - 1)
{
throw new ArgumentException(
"The salt offset is invalid - it must fall within " +
"the password length.");
}
this._maxLength = maxLength;
this._saltLength = saltLength;
this._saltOffset = saltOffset;
}
/// <summary>
/// Returns a hash-encoded value for the supplied password.
/// </summary>
/// <param name="password">The password to one-way encode.</param>
/// <returns>The hashed value for the password.</returns>
public string Hash(string password)
{
// Generate a new 'salt' value to seed the hashing algorithm with
// every time to make it harder to crack the password.
byte[] salt = new byte[this._saltLength];
new RNGCryptoServiceProvider().GetBytes(salt);
// Compute the hash code for the password using the PBKDF2
// pseudo-random number generator.
Rfc2898DeriveBytes encoder = new Rfc2898DeriveBytes(password, salt, 10000);
byte[] hash = encoder.GetBytes(this._maxLength);
// Combine the salt and password, with the salt insert in the
// indicated position in the password.
byte[] combined = new byte[this._saltLength + this._maxLength];
Array.Copy(hash, 0, combined, 0, this._saltOffset);
Array.Copy(salt, 0, combined, this._saltOffset, this._saltLength);
Array.Copy(hash, this._saltOffset, combined,
this._saltOffset + this._saltLength,
this._maxLength - this._saltOffset);
// Return the encoded password as a base-64 string so it can be saved
// as a normal text value.
return Convert.ToBase64String(combined);
}
/// <summary>
/// Returns a value indicating if the supplied password is correct
/// (based on the expected hash value.)
/// </summary>
/// <param name="password">The plain-text password to check.</param>
/// <param name="hashedPassword">The expected hashed value.</param>
/// <returns>True if the password is correct, and False otherwise.</returns>
public bool Validate(string password, string hashedPassword)
{
byte[] hashBytes = Convert.FromBase64String(hashedPassword);
byte[] salt = new byte[this._saltLength];
Array.Copy(hashBytes, this._saltOffset, salt, 0, this._saltLength);
// Extract the encoded password from the combined hashed value.
byte[] expected = new byte[this._maxLength];
Array.Copy(hashBytes, 0, expected, 0, this._saltOffset);
Array.Copy(hashBytes, this._saltOffset + this._saltLength, expected,
this._saltOffset, this._maxLength - this._saltOffset);
// Compute the hash code for the password using the PBKDF2
// pseudo-random number generator.
Rfc2898DeriveBytes encryptor =
new Rfc2898DeriveBytes(password, salt, 10000);
byte[] hash = encryptor.GetBytes(this._maxLength);
// Compare the newly encoded hash to the extracted value.
for (int i = 0; i < this._maxLength; i++)
{
if (expected[i] != hash[i])
{
return false;
}
}
return true;
}
}
}
Code language: C# (cs)