Testing

webTiger Logo Wide

Password Hashing

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)