1
\$\begingroup\$

I share below my password hashing implementation with salt and pepper in C#, for your review and feedback. In my implementation methods like ValidatePassword(), ChangePassword() are in the user class.

I recommend random.org's free random bytes generator coded in hexadecimal to get a secure Pepper for each installation. I prefer 128 bytes length. It separates each byte (every 2 digits in hexadecimal) with space. Copy-paste the pepper generated by random.org to the config file with the with the key "Pepper" like so:

<add key="Pepper" value="25 4d 58 42 05 ba 0a f0 8a 72 .. .. e3"/>

The simplified source code:

using System;
using System.Text;
using System.Configuration;
using System.Globalization;
using System.Security.Cryptography;

namespace CryptoPassword
{
    public static class Password
    {
        private const int SaltLength = 32;
        private const int KeyLength = 32;
        private const int IterationCount = 100000;

        private static readonly byte[] Pepper = ConvertSpacedHexToByteArray(ConfigurationManager.AppSettings["Pepper"]);
        //I recommend random.org's free 128 byte random bytes generator coded in hexadecimal
        //to get a secure pepper for each installation.
        //Copy-paste the pepper generated by the random.org to the config file with the key "Pepper"
        //like so: "<add key="Pepper" value="25 4d 58 42 05 ba 0a f0 8a 72 .. .. e3"/>"


        /// <summary>
        /// hexString should have its double hex digits corresponding to each byte separated by ' '
        /// like so: "A1 23 F3 14".
        /// </summary>
        /// <param name="hexString"></param>
        /// <returns>byte[] form</returns>
        public static byte[] ConvertSpacedHexToByteArray(string hexString)
        {   
            string[] hexValuesSplit = hexString.Split(' ');
            byte[] data = new byte[hexValuesSplit.Length];

            for (var index = 0; index < hexValuesSplit.Length; index++)
            {
                data[index] = byte.Parse(hexValuesSplit[index], NumberStyles.HexNumber, CultureInfo.InvariantCulture);
            }

            return data;
        }

        // I dont recommend playing with salt length a lot. Make it a const,
        // and use the non-parametrized GenerateSalt() unless you know what you are doing.
        // Change the const SaltLength at major releases.
        public static byte[] GenerateSalt()
        {
            return GenerateSalt(SaltLength);
        }
        //this parameterized version is for the flexibility at knowledgable hands.
        public static byte[] GenerateSalt(int saltLength)
        {
            using (RNGCryptoServiceProvider saltCellar = new RNGCryptoServiceProvider())
            {
                byte[] salt = new byte[saltLength];
                saltCellar.GetBytes(salt);
                return salt;
            }
        }

        public static string GenerateHash(string password)
        {
            byte[] salt = GenerateSalt();
            byte[] hash = GenerateHash(password, salt);

            return Convert.ToBase64String(salt) + ":" + Convert.ToBase64String(hash);
            //store salt and hash together with ':' as the separator, coded in Base64.
        }

        public static byte[] GenerateHash(string password, byte [] salt)
        {
            //create an hmac hash of the password using the pepper value as the key
            using (var hmac = new HMACSHA512(Pepper))
            {
                byte[] initialHash = hmac.ComputeHash(Encoding.UTF8.GetBytes(password));

                //generate a key value using pbkdf2 that will serve as the password hash
                using (var pbkdf2 = new Rfc2898DeriveBytes(initialHash, salt, IterationCount)) //int.Parse(ConfigurationManager.AppSettings["IterationCount"])))
                {
                    return pbkdf2.GetBytes(KeyLength);
                }
            }
        }

        public static bool ValidatePassword(string password, string testPassword)
        {
            string[] hashParts = password.Split(':');
            byte[] salt = Convert.FromBase64String(hashParts[0]);
            byte[] hash = Convert.FromBase64String(hashParts[1]);
            byte[] testHash = GenerateHash(testPassword, salt);

            //IMPORTANT!!! The following is required to defend against timing attacks.
            //We dont want to exit at the first byte mismatch but test every byte no matter what
            //so that timing is the same for valid and invalid passwords and
            //we dont want to leak information to the attacker about upto which byte his guess is correct.
            uint differences = (uint)hash.Length ^ (uint)testHash.Length;
            for (int position = 0; position < Math.Min(hash.Length, testHash.Length); position++)
                differences |= (uint)(hash[position] ^ testHash[position]);
            return differences == 0;
        }
    }
}
\$\endgroup\$
  • 1
    \$\begingroup\$ Why not use something like Argon2/Bcrypt, but reinvent the wheel. If it is intentional, you might want to tag it as such. \$\endgroup\$ – Ludisposed Sep 1 '18 at 13:33
  • \$\begingroup\$ Unfortunately, pbkdf2 tag is not present. I think they are all good enough as of 2018. I chose pbkdf2 for compliance. It is good enough for most contexts, it has standard/compliance advantage and been tested long enough, i understand the mechanism in detail and feel confident. I am waiting for Argon2 to get wider adoption/testing and plan to use it in late 2019 or 2020. \$\endgroup\$ – Ersin TASKIN Sep 2 '18 at 12:50

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.