Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Migrating from Membership to Identity when passwordFormat = Encrypted and decryption = AES

Recently I began developing a new MVC app and needed to take an older existing asp.net membership database and convert it into the new(er) Identity system.

If you've found yourself in a similar situation, then likely you've come upon this helpful post from microsoft which gives you great guidance and scripts on converting the database over to the new schema, including the passwords.

To handle the differences in hashing/encryption of passwords between the two systems, they include a custom password hasher, SqlPasswordHasher, which parses the password field (which has been combined into Password|PasswordFormat|Salt) and attempts to duplicate the logic found inside SqlMembershipProvider to compare the incoming password with the stored version.

However, as I (and another commenter on that post) noticed, this handy hasher they provided doesn't handle Encrypted passwords (despite the confusing lingo they use in the post that seems to indicate that it does). It seems like it should, considering they do bring across the password format into the database, but then curiously the code doesn't use it, instead having

int passwordformat = 1;

which is for hashed passwords. What I needed was one that would handle my scenario which is encrypted passwords using the System.Web/MachineKey configuration element's decryptionKey.

If you also are in such a predicament, and are using the AES algorithm (as defined in the decryption property of the machineKey) then my answer below should come to your rescue.

like image 857
Erikest Avatar asked Jun 11 '16 21:06

Erikest


1 Answers

First, let's talk real quick about what the SqlMembershipProvider is doing under the hood. The provider combines the salt, converted to a byte[] with the password, encoded as a unicode byte array, into a single larger byte array, by concatenating the two together. Pretty straightforward. Then it passes this off, through an abstraction (MembershipAdapter) to the MachineKeySection where the real work is done.

The important part about that handoff is that it instructs the MachineKeySection to use an empty IV (intialization vector) and also to perform no signing. That empty IV is the real lynchpin, because the machineKey element has no IV property, so if you've scratched your head and wondered how the providers were handling this aspect, that's how. Once you know that (from digging around the source code) then you can distill down the encryption code in the MachineKeySection code and combine it with the membership provider's code to arrive at a more complete hasher. Full source:

public class SQLPasswordHasher : PasswordHasher
{
    public override string HashPassword(string password)
    {
        return base.HashPassword(password);
    }

    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 = int.Parse(passwordProperties[1]);
            string salt = passwordProperties[2];

            if (String.Equals(EncryptPassword(providedPassword, passwordformat, salt), passwordHash, StringComparison.CurrentCultureIgnoreCase))
            {
                return PasswordVerificationResult.SuccessRehashNeeded;
            }
            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("SHA1");
            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);
            }
        }
        else //MembershipPasswordFormat.Encrypted, aka 2
        {               
            byte[] bEncrypt = new byte[bSalt.Length + bIn.Length];
            Buffer.BlockCopy(bSalt, 0, bEncrypt, 0, bSalt.Length);
            Buffer.BlockCopy(bIn, 0, bEncrypt, bSalt.Length, bIn.Length);

            // Distilled from MachineKeyConfigSection EncryptOrDecryptData function, assuming AES algo and paswordCompatMode=Framework20 (the default)
            using (var stream = new MemoryStream())
            {
                var aes = new AesCryptoServiceProvider();
                aes.Key = HexStringToByteArray(MachineKey.DecryptionKey);
                aes.GenerateIV();
                aes.IV = new byte[aes.IV.Length];
                using (var transform = aes.CreateEncryptor())
                {
                    using (var stream2 = new CryptoStream(stream, transform, CryptoStreamMode.Write))
                    {
                        stream2.Write(bEncrypt, 0, bEncrypt.Length);
                        stream2.FlushFinalBlock();
                        bRet = stream.ToArray();
                    }
                }
            }               
        }

        return Convert.ToBase64String(bRet);
    }

    public static byte[] HexStringToByteArray(String hex)
    {
        int NumberChars = hex.Length;
        byte[] bytes = new byte[NumberChars / 2];
        for (int i = 0; i < NumberChars; i += 2)
            bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
        return bytes;
    }

    private static MachineKeySection MachineKey
    {
        get
        {
            //Get encryption and decryption key information from the configuration.
            System.Configuration.Configuration cfg = WebConfigurationManager.OpenWebConfiguration(System.Web.Hosting.HostingEnvironment.ApplicationVirtualPath);
            return cfg.GetSection("system.web/machineKey") as MachineKeySection;
        }
    }


}

If you have a different algorithm, then the steps will be very close to the same, but you may want to first dive into the source for MachineKeySection and carefully walkthrough how they're initializing things. Happy Coding!

like image 60
Erikest Avatar answered Nov 15 '22 04:11

Erikest