Migrating ASP.NET projects between different versions can be time-consuming: the database schema and the providers library changes overtime. I have been doing migrations from ASP.NET SqlServerMembership to ASP.NET Universal Membership (System.Web.Providers.DefaultMembershipProvider), and to the latest ASP.NET Identity 2.1.0. I wasted more than half day on the migration to ASP.NET Identity 2.1.0 and I finally realized the default hash algorithms are totally different in different .NET frameworks.

What’s the issue?

As described in my previous post (the following link), I could not use the previous hashed password to do verifications even I tried to implement the same logic with SHA1.

http://kosmisch.net/Blog/DotNetEssential/Archive/2015/1/31/migrate-from-universal-membership-provider-to-aspnet-identity-210-passwordharsher.html

I have also been trying to use SHA256 and HMACSHA1 to resolve the issues but all failed.

Then I began to look into the source code of different versions.

Big Finding

The default hash algorithm in .NET 4.0 changed to SHA256, more specifically, HMACSHA256. In previous versions, the default is SHA1.

That’s why different hashed passwords are generated using the same salt and clear password.

static void Main(string[] args)
        {
            string salt = "Zan8iwiiQQbWDRId/KpX4g==";
            string passwordText = "password";


            Console.WriteLine(EncryptPassword(passwordText, 1, salt, HashAlgorithm.Create("SHA1")));
            Console.WriteLine(EncryptPassword(passwordText, 1, salt, HashAlgorithm.Create("SHA256")));
            Console.WriteLine(EncryptPassword(passwordText, 1, salt, new HMACSHA1()));
            Console.WriteLine(EncryptPassword(passwordText, 1, salt, new HMACSHA256()));

            Console.ReadKey();
        }


        //This is copied from the existing SQL providers and is provided only for back-compat.
        private static string EncryptPassword(string pass, int passwordFormat, string salt, HashAlgorithm algorithm)
        {
            if (passwordFormat == 0) // MembershipPasswordFormat.Clear
                return pass;

            byte[] bIn = Encoding.Unicode.GetBytes(pass);
            byte[] bSalt = Convert.FromBase64String(salt);
            byte[] bRet = null;

            if (passwordFormat == 1)
            { // MembershipPasswordFormat.Hashed 
                HashAlgorithm hm = algorithm;
                if (hm is KeyedHashAlgorithm)
                {
                    KeyedHashAlgorithm kha = (KeyedHashAlgorithm)hm;
                    if (kha.Key.Length == bSalt.Length)
                    {
                        kha.Key = bSalt;
                    }
                    else if (kha.Key.Length < bSalt.Length)
                    {
                        byte[] bKey = new byte[kha.Key.Length];
                        Buffer.BlockCopy(bSalt, 0, bKey, 0, bKey.Length);
                        kha.Key = bKey;
                    }
                    else
                    {
                        byte[] bKey = new byte[kha.Key.Length];
                        for (int iter = 0; iter < bKey.Length;)
                        {
                            int len = Math.Min(bSalt.Length, bKey.Length - iter);
                            Buffer.BlockCopy(bSalt, 0, bKey, iter, len);
                            iter += len;
                        }
                        kha.Key = bKey;
                    }
                    bRet = kha.ComputeHash(bIn);
                }
                else
                {
                    byte[] bAll = new byte[bSalt.Length + bIn.Length];
                    Buffer.BlockCopy(bSalt, 0, bAll, 0, bSalt.Length);
                    Buffer.BlockCopy(bIn, 0, bAll, bSalt.Length, bIn.Length);
                    bRet = hm.ComputeHash(bAll);
                }
            }

            return Convert.ToBase64String(bRet);
        }

The above sample code generates the following output:

image

The New SqlPasswordHasher class

The new application user manager class is revised to resolve this issue. Remember to specify your own hash algorithm used in previous versions.

using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using System;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;

namespace KosmischStudio.Website.Models
{
    public class ApplicationUserManager : UserManager<ApplicationUser>
    {
        public ApplicationUserManager()
            : base(new UserStore<ApplicationUser>(new ApplicationDbContext()))
        {
            //if you didn't specify default hash algorithm and if you are migrating from .NET 4.0, use HMACSHA256;
            // or if you are migrating from earlier versions, use SHA1 or any other algorithms you speficied
            this.PasswordHasher = new SqlPasswordHasher("HMACSHA256", true, 1);
        }

        protected async override Task<bool> VerifyPasswordAsync(IUserPasswordStore<ApplicationUser, string> store, ApplicationUser user, string password)
        {
            var hash = await store.GetPasswordHashAsync(user).ConfigureAwait(false);

            if (this.PasswordHasher.VerifyHashedPassword(hash, password) == PasswordVerificationResult.SuccessRehashNeeded)
            {
                // Make our new hash
                hash = PasswordHasher.HashPassword(password);

                // Save it to the DB
                await store.SetPasswordHashAsync(user, hash).ConfigureAwait(false);

                // Invoke internal method to upgrade the security stamp
                BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.NonPublic;
                MethodInfo minfo = typeof(UserManager<ApplicationUser>).GetMethod("UpdateSecurityStampInternal", bindingFlags);
                var updateSecurityStampInternalTask = (Task)minfo.Invoke(this, new[] { user });
                await updateSecurityStampInternalTask.ConfigureAwait(false);

                // Update user
                await UpdateAsync(user).ConfigureAwait(false);
            }

            return PasswordHasher.VerifyHashedPassword(hash, password) != PasswordVerificationResult.Failed;
        }
    }

    public class SqlPasswordHasher : PasswordHasher
    {
        /// <summary>
        /// Whether to keep the legacy hash format: {$HashedPassword}|{$Format}|{$Salt}
        /// </summary>
        /// <returns></returns>
        public bool KeepLegacyHashFormat { get; set; }

        /// <summary>
        /// Format of the password: 1 : Hashed, 0 Clear, 2 Encrypt
        /// </summary>
        /// <returns></returns>
        public int PasswordFormat { get; set; }

        /// <summary>
        /// Algorithm used by your previous membership provider 
        /// </summary>
        /// <returns></returns>
        public string LegacyAlgorithm { get; set; }

        public SqlPasswordHasher(string legacyAlgorithm, bool keepLegacyHashFormat, int passwordFormat)
        {
            LegacyAlgorithm = legacyAlgorithm;
            KeepLegacyHashFormat = keepLegacyHashFormat;
            PasswordFormat = passwordFormat;
        }

        public override string HashPassword(string password)
        {

            if (KeepLegacyHashFormat)
            {
                StringBuilder passwordNew = new StringBuilder();
                string salt = GenerateSalt();
                string hashedPassword = EncryptPassword(password, PasswordFormat, salt);
                StringBuilder newPassword = new StringBuilder();
                newPassword.AppendFormat("{0}|{1}|{2}", hashedPassword, PasswordFormat, salt);
                return newPassword.ToString();
            }
            else
                return base.HashPassword(password);
        }

        private static string GenerateSalt()
        {
            string base64String;
            using (RNGCryptoServiceProvider rNGCryptoServiceProvider = new RNGCryptoServiceProvider())
            {
                byte[] numArray = new byte[16];
                rNGCryptoServiceProvider.GetBytes(numArray);
                base64String = Convert.ToBase64String(numArray);
            }
            return base64String;
        }

        public override PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword)
        {
            string[] passwordProperties = hashedPassword.Split('|');
            if (passwordProperties.Length != 3)
            {
                return base.VerifyHashedPassword(hashedPassword, providedPassword);
            }
            else
            {
                string passwordHash = passwordProperties[0];
                int passwordformat = 1;
                string salt = passwordProperties[2];
                if (String.Equals(EncryptPassword(providedPassword, passwordformat, salt), passwordHash, StringComparison.CurrentCultureIgnoreCase))
                {
                    if (!this.KeepLegacyHashFormat)
                        return PasswordVerificationResult.SuccessRehashNeeded;
                    else
                        return PasswordVerificationResult.Success;
                }
                else
                {
                    return PasswordVerificationResult.Failed;
                }
            }
        }



        //This is copied from the existing SQL providers and is provided only for back-compat.
        private string EncryptPassword(string pass, int passwordFormat, string salt)
        {
            if (passwordFormat == 0) // MembershipPasswordFormat.Clear
                return pass;

            byte[] bIn = Encoding.Unicode.GetBytes(pass);
            byte[] bSalt = Convert.FromBase64String(salt);
            byte[] bRet = null;

            if (passwordFormat == 1)
            { // MembershipPasswordFormat.Hashed 
                HashAlgorithm hm = HashAlgorithm.Create(this.LegacyAlgorithm);
                if (hm is KeyedHashAlgorithm)
                {
                    KeyedHashAlgorithm kha = (KeyedHashAlgorithm)hm;
                    if (kha.Key.Length == bSalt.Length)
                    {
                        kha.Key = bSalt;
                    }
                    else if (kha.Key.Length < bSalt.Length)
                    {
                        byte[] bKey = new byte[kha.Key.Length];
                        Buffer.BlockCopy(bSalt, 0, bKey, 0, bKey.Length);
                        kha.Key = bKey;
                    }
                    else
                    {
                        byte[] bKey = new byte[kha.Key.Length];
                        for (int iter = 0; iter < bKey.Length;)
                        {
                            int len = Math.Min(bSalt.Length, bKey.Length - iter);
                            Buffer.BlockCopy(bSalt, 0, bKey, iter, len);
                            iter += len;
                        }
                        kha.Key = bKey;
                    }
                    bRet = kha.ComputeHash(bIn);
                }
                else
                {
                    byte[] bAll = new byte[bSalt.Length + bIn.Length];
                    Buffer.BlockCopy(bSalt, 0, bAll, 0, bSalt.Length);
                    Buffer.BlockCopy(bIn, 0, bAll, bSalt.Length, bIn.Length);
                    bRet = hm.ComputeHash(bAll);
                }
            }

            return Convert.ToBase64String(bRet);
        }
    }
}
About author
Comments

Re:ASP.NET Membership Default Password Hash Algorithms in .NET 4.x and Previous Versions

SHA-1 has been proved to be not secure enough in 2005, http://en.wikipedia.org/wiki/SHA-1 Thus, it is reasonable for Microsoft to change the default to a safer one. Of course, if no documentation mentioning that it is obviously a problem of Microsoft.

Author: Lex Li @ 2/1/2015 7:21:01 PM | [Reply]

Re:ASP.NET Membership Default Password Hash Algorithms in .NET 4.x and Previous Versions

@Lex Li Yeah, it definitely better to change the algorithm to a more secured one. I didn't notice any documentation about this; maybe I missed it...

Author: Raymond Tang @ 2/1/2015 8:38:05 PM | [Reply]

Add comment
Title
Title is required.
Name
Name is required.
Email
Please input your personal email with valid format.
Comments
Please input comment content.
Captcha Refresh
Input captcha:

Subscription

Statistics

Locations of visitors to this page