【问题标题】:Migration from SHA1 to SHA2 ASP.net 4.5, C#从 SHA1 迁移到 SHA2 ASP.net 4.5,C#
【发布时间】:2017-06-22 22:10:39
【问题描述】:

我们有一个内置于 .NET Framework 4.5 版本的 ASP.NET Web 应用程序。目前该应用程序正在生产中使用 SHA1 加密算法。该算法在应用程序的 web.config 文件的“MachineKey”标签中设置。此应用程序使用 ASP.Net Membership 概念来维护登录凭据。

由于 SHA1 算法即将退化,因此我们希望将应用程序从 SHA1 更新到 SHA2。为此,我们在应用程序的 web.config 文件的“MachineKey”标签中设置了“HMACSHA256”。

使用上述设置将我们的应用程序升级到 SHA2 后,我们预计旧用户的密码(使用 SHA1 加密并已存在于会员数据库中)将无法使用 SHA2 算法。但它允许老用户登录,而无需修改之前加密的密码。

问题 1:在应用程序的 web.config 文件的“MachineKey”标签中所做的更改是否足够/推荐用于此迁移?

问题 2:由于我们仍然可以使用以前加密的密码登录应用程序,会员数据库真的使用 web.config 文件中设置的 SHA2 加密吗?或者我们需要添加一些额外的设置来启用会员数据库级别的 SHA2 加密?请指教。

请建议是否有在会员数据库级别启用 SHA2 加密的最佳方法。

【问题讨论】:

    标签: c# asp.net .net sha


    【解决方案1】:

    我不知道是否可以通过 Membership 处理此类迁移,而无需强制用户进行密码重置过程。

    但是您可以通过同时将 Membership 迁移到 Asp.Net Identity 来做到这一点:Asp.Net Identity 具有扩展点,允许您处理“备用”密码签名匹配以支持旧签名。此时,您的登录名和密码仍然在内存中未散列,然后您可以顺便将签名转换为新格式。

    blog 后面的所有详细信息和代码,包括 SQL 迁移,我只是在下面代码的附加注释中稍微解释一下。

    这是实现此目的的主要类:

    public class BackCompatPasswordHasher : PasswordHasher
    {
        public override string HashPassword(string password)
        {
            return base.HashPassword(password);
        }
    
        public override PasswordVerificationResult VerifyHashedPassword(
            string hashedPassword, string providedPassword)
        {
            // Relies on SQL migration having formatted old hashes as
            // (aspnet_Membership.Password + '|' + 
            // CAST(aspnet_Membership.PasswordFormat as varchar) + '|'
            // + aspnet_Membership.PasswordSalt)
            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))
                {
                    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);
                }
            }
    
            return Convert.ToBase64String(bRet);
        }
    }
    

    然后,在您的用户管理器中:

    public class IdentityUserManager : UserManager<IdentityUser>
    {
        public IdentityUserManager(IUserStore<IdentityUser> store)
            : base(store)
        {
            PasswordHasher = new BackCompatPasswordHasher();
        }
    }
    

    在我的实际代码库中,我添加了一些用于处理重新散列的附加功能,但不幸的是,没有 cmets 说明原因。也许那是原始实现者的一些多余代码,或者确实需要。我没有调查它,所以这里是 IdentityUserManager 中的附加代码:

        private ConcurrentDictionary<string, string> UserRehashed = 
            new ConcurrentDictionary<string, string>();
    
        private bool CanRehash(IdentityUser user)
        {
            return UserRehashed.TryAdd(user.Id, user.Id);
        }
    
        protected async override Task<bool> VerifyPasswordAsync(
            IUserPasswordStore<IdentityUser, string> store, IdentityUser user,
            string password)
        {
            var hash = await store.GetPasswordHashAsync(user).ConfigureAwait(false);
            var verifPassRes = PasswordHasher.VerifyHashedPassword(hash, password);
            if (verifPassRes == PasswordVerificationResult.SuccessRehashNeeded &&
                // avoid rehash loop.
                CanRehash(user))
            {
                var chPassRes = await this.ChangePasswordAsync(user.Id,
                    password, password).ConfigureAwait(false);
                if (!chPassRes.Succeeded)
                {
                    // throw or log, whatever.
                }
            }
    
            return verifPassRes != PasswordVerificationResult.Failed;
        }
    

    【讨论】:

      【解决方案2】:

      通过我的other answer 技术细节,我才明白为什么您可以更改散列算法而不会导致旧密码“丢失”。

      Membership 确实存储了它与每个密码一起使用的散列算法,因此允许它在散列算法更改后仍然验证旧密码。

      这解释了您所看到的行为。要仔细检查(并确认您的问题 1 和 2 问题),请检查 db 中的数据,检查 PasswordFormat 列,该列应根据使用的哈希算法而变化。

      您还应该检查仅使用旧帐户登录是否足以让会员资格将其重新散列到 SHA-2。如果是这种情况,所有普通用户将在完成更改后快速重新散列。

      我不会删除我之前的答案,因为如果考虑迁移身份验证框架,它仍然可能会有所帮助。

      【讨论】:

        【解决方案3】:

        SHA1 升级到 SHA2 的全部意义在于实际上减少了多年前被黑客入侵的 SHA1 的已知安全问题。因此,尝试拥有一个混合系统几乎毫无意义。

        短期:99% 的用户使用 SHA1 .. 只有新用户 1% 的用户使用 SHA2 ?? :(

        您只需要强制更改密码即可让 100% 的人使用 SHA2

        【讨论】:

        • SHA1 漏洞是关于产生“冲突”的可能性:从不同的内容,获得相同的签名。据我所知,在登录密码内部签名上利用 SHA1 冲突尚未完成,而且很可能永远不会完成,因为冲突生成技术需要冗长的输入,而登录/密码输入应该禁止这些输入。
        • @Frédéric 我正在阅读有关这些“碰撞”的信息,但我不确定我是否真的很好地理解了这个问题。
        • 冲突原理是改变一些内容(例如文件下载,添加一些恶意软件或证书,...),但其 SHA-1 哈希值将保持不变,引诱依赖 SHA-1 签名的安全系统。这在 MD5 散列中进行了无数次,SHA-1 也有同样的漏洞(越来越容易访问,几年前它仍然需要巨大的计算能力)。因此 SHA-1 签名证书的终结发生在 2017 年。关于私人持有的 SHA-1 签名,例如数据库中的哈希密码,利用该漏洞是另一回事。
        猜你喜欢
        • 2015-08-20
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-08-27
        • 2021-02-06
        • 1970-01-01
        • 2016-06-30
        • 1970-01-01
        相关资源
        最近更新 更多