zoukankan      html  css  js  c++  java
  • .NET Core验证ASP.NET密码

    .NET Core验证ASP.NET密码

    随着.NET Core的持续更新和完善,越来越多的机构已经选择或者升级为.NET Core。但由于技术不完全相同,不可能所有应用/数据库都能无缝迁移,因此ASP.NET Core和传统ASP.NET之间多少会存在一些挑战,需要更多的渐进升级方法和交互。

    其中,密码共享就是升级到ASP.NET Core一个很容易想到的渐进升级方式,也是一个需要解决的问题。

    啥?什么都不用做?

    其实如果坚持走ASP.NET Core Identity这一套,代码是写了一个if/else,能兼容老ASP.NET Identity生成的密码的,Github链接:https://github.com/dotnet/aspnetcore/blob/8b7f6621695d93b2a55fb8a5b1be99c4af867ae4/src/Identity/Extensions.Core/src/PasswordHasher.cs#L185-L207,核心代码如下:

    /// <summary>
    /// Returns a <see cref="PasswordVerificationResult"/> indicating the result of a password hash comparison.
    /// </summary>
    /// <param name="user">The user whose password should be verified.</param>
    /// <param name="hashedPassword">The hash value for a user's stored password.</param>
    /// <param name="providedPassword">The password supplied for comparison.</param>
    /// <returns>A <see cref="PasswordVerificationResult"/> indicating the result of a password hash comparison.</returns>
    /// <remarks>Implementations of this method should be time consistent.</remarks>
    public virtual PasswordVerificationResult VerifyHashedPassword(TUser user, string hashedPassword, string providedPassword)
    {
        if (hashedPassword == null)
        {
            throw new ArgumentNullException(nameof(hashedPassword));
        }
        if (providedPassword == null)
        {
            throw new ArgumentNullException(nameof(providedPassword));
        }
    
        byte[] decodedHashedPassword = Convert.FromBase64String(hashedPassword);
    
        // read the format marker from the hashed password
        if (decodedHashedPassword.Length == 0)
        {
            return PasswordVerificationResult.Failed;
        }
        switch (decodedHashedPassword[0])
        {
            case 0x00:
                if (VerifyHashedPasswordV2(decodedHashedPassword, providedPassword))
                {
                    // This is an old password hash format - the caller needs to rehash if we're not running in an older compat mode.
                    return (_compatibilityMode == PasswordHasherCompatibilityMode.IdentityV3)
                        ? PasswordVerificationResult.SuccessRehashNeeded
                        : PasswordVerificationResult.Success;
                }
                else
                {
                    return PasswordVerificationResult.Failed;
                }
    
            case 0x01:
                int embeddedIterCount;
                if (VerifyHashedPasswordV3(decodedHashedPassword, providedPassword, out embeddedIterCount))
                {
                    // If this hasher was configured with a higher iteration count, change the entry now.
                    return (embeddedIterCount < _iterCount)
                        ? PasswordVerificationResult.SuccessRehashNeeded
                        : PasswordVerificationResult.Success;
                }
                else
                {
                    return PasswordVerificationResult.Failed;
                }
    
            default:
                return PasswordVerificationResult.Failed; // unknown format marker
        }
    }
    

    它根据Base64解码后的第一个字节,来判断是老版本还是新版本,然后调用各自不同的验证函数。

    但时代在变化,很多人已经不用官方提供的这一套Identity验证密码,那么有什么“骚”操作,可以解密吗?

    和密码刚正面

    传统的ASP.NET MVC模板项目是通过ASP.NET Identity管理的密码,它由Rfc2898DeriveBytes实现,该算法进行了1000次循环SHA1哈希、并加盐,以确保难以通过传统的哈希碰撞来确保密码的安全性,其核心代码如下(Github链接:https://github.com/aspnet/AspNetIdentity/blob/9c48993a446288032f9824633e6dae81257da06e/src/Microsoft.AspNet.Identity.Core/Crypto.cs#L26-L46):

    private const int PBKDF2IterCount = 1000; // default for Rfc2898DeriveBytes
    private const int PBKDF2SubkeyLength = 256/8; // 256 bits
    private const int SaltSize = 128/8; // 128 bits
    
    /* =======================
    *  HASHED PASSWORD FORMATS
    *  =======================
    *  
    *  Version 0:
    *  PBKDF2 with HMAC-SHA1, 128-bit salt, 256-bit subkey, 1000 iterations.
    *  (See also: SDL crypto guidelines v5.1, Part III)
    *  Format: { 0x00, salt, subkey }
    */
    
    public static string HashPassword(string password)
    {
        if (password == null)
        {
            throw new ArgumentNullException("password");
        }
    
        // Produce a version 0 (see comment above) text hash.
        byte[] salt;
        byte[] subkey;
        using (var deriveBytes = new Rfc2898DeriveBytes(password, SaltSize, PBKDF2IterCount))
        {
            salt = deriveBytes.Salt;
            subkey = deriveBytes.GetBytes(PBKDF2SubkeyLength);
        }
    
        var outputBytes = new byte[1 + SaltSize + PBKDF2SubkeyLength];
        Buffer.BlockCopy(salt, 0, outputBytes, 1, SaltSize);
        Buffer.BlockCopy(subkey, 0, outputBytes, 1 + SaltSize, PBKDF2SubkeyLength);
        return Convert.ToBase64String(outputBytes);
    }
    

    值得一提的是,代码用到了Rfc2898DeriveBytes,经常读我的博客的知道,这个类是老朋友了,通过传入明文密码、盐、迭代次数和算法,可以在不依赖于哈希算法安全性的前提下做单向加密,确保了密码的不可(难以)破解性。

    可见,密码的原始字节由{0, salt, subkey}三部分组成,其中盐为128位,即16字节,subkey256位,即32字节,总共1+16+32=49字节,密文需要转换为Base64,根据Base64的信息量计算公式

    Math.Log(64, 256)=0.75
    

    可知,Base64编码相对原始字节的比例为0.75:1,因此计算可得转换为Base64之后,其字符串长度为

    Math.Ceiling(49 / 0.75 / 4) * 4 = 68
    

    我们来从数据库随便查询一个由ASP.NET Identity创建的密码:

    SELECT TOP 1 
    	PasswordHash, 
    	LEN(PasswordHash) AS Len 
    FROM [User]
    

    结果如下,可见结果长度真的为68

    所以,说了这么多,怎么把密码迁移到ASP.NET Core呢?

    注意看,上面的代码没什么特别,就是依赖于Rfc2898DeriveBytes这个类。

    .NET Core内置了Rfc2898DeriveBytes这个类,可以直接使用,不需要安装任何NuGet包,因此……直接复制粘贴上文中的【核心代码】即可。

    ASP.NET Core中的Identity有什么区别?

    Github核心代码在此:https://github.com/dotnet/aspnetcore/blob/8b7f6621695d93b2a55fb8a5b1be99c4af867ae4/src/Identity/Extensions.Core/src/PasswordHasher.cs#L113-L156

    我就从简引用一下关键注释:

    /* =======================
    *  HASHED PASSWORD FORMATS
    *  =======================
    * 
    *  Version 2:
    *  PBKDF2 with HMAC-SHA1, 128-bit salt, 256-bit subkey, 1000 iterations.
    *  (See also: SDL crypto guidelines v5.1, Part III)
    *  Format: { 0x00, salt, subkey }
    * 
    *  Version 3:
    *  PBKDF2 with HMAC-SHA256, 128-bit salt, 256-bit subkey, 10000 iterations.
    *  Format: { 0x01, prf (UInt32), iter count (UInt32), salt length (UInt32), salt, subkey }
    *  (All UInt32s are stored big-endian.)
    */
    

    原来,老ASP.NET Identity中的密码版本为V2,当前ASP.NET Core Identity中的密码版本为V3,首字节(版本号)从0x00改成了0x01,算法从HMACSHA1升级为了HMACSHA256,另外V3版本还将迭代次数从1000升级为10000,另外还将算法名、迭代次数、盐的长度信息保存在了密码中。

    另外需要注意的是,新ASP.NET Core Identity的实现很有弹性,它通过依赖注入IOptions<T>的形式,使得所有选项都是可以配置的。

    解密示例

    假如我有一个密码:myF&TB9vhTx7,我使用传统的ASP.NET MVC创建项目,然后用这个密码注册一个帐号:

    然后在数据库中将哈希值取出来:

    SELECT PasswordHash
    FROM [AspNetUsers]
    WHERE [Email] = N'sdflysha@qq.com'
    

    查得结果如下,结果为AJRvdJ1/Ii+58zU6yBrPJH4hCkMagxqK/W6oejAuG1hIrNPEQMAAyYynsXWwat9Huw==

    我将上述代码作简化,用最简单的代码表达验证密码的过程,除去两行注释代码(用于做断言),整个过程只需7行代码,代码如下:

    string pwdHashText = "AJRvdJ1/Ii+58zU6yBrPJH4hCkMagxqK/W6oejAuG1hIrNPEQMAAyYynsXWwat9Huw==";
    string pwd = "myF&TB9vhTx7";
    byte[] pwdHash = Convert.FromBase64String(pwdHashText);
    // Debug.Assert(pwdHash.Length == 49);
    // Debug.Assert(pwdHash[0] == 0);
    byte[] salt = pwdHash[1..17];
    byte[] hash = pwdHash[17..49];
    using var r = new Rfc2898DeriveBytes(pwd, salt, 1000);
    Console.WriteLine(r.GetBytes(32).SequenceEqual(hash));
    

    此时,运行结果如下,运行显示True,表示验证解码成功:

    总结

    我总结一个表格如下所示:

    ASP.NET Identity ASP.NET Core Identity
    版本 V2 V3
    首字节 0x00 0x01
    默认算法 HMACSHA1 HMACSHA256
    迭代次数 1000 10000
    密码信息 版本+盐+哈希值 版本+算法+迭代次数+盐长度+盐+哈希值
    原始长度 1+16+32=49 1+4+4+4+16+32=61
    Base64长度 68 84
    可配置性 不可配置 可自由配置
    安全性 非常好

    密码解密其实是前往ASP.NET Core上较为轻松的一步,核心代码也就7行。

    其实更有挑战、也更有意思的是如何解传统ASP.NET Identity中的Cookie,我将在下一篇中详情分析这个主题,敬请期待!

    喜欢的朋友请关注我的微信公众号:【DotNet骚操作】

    DotNet骚操作

    新年快到了,祝大家阖家欢乐,鼠年大吉!

  • 相关阅读:
    自定义实现wcf的用户名密码验证
    EF6:编写你自己的code first 数据迁移操作(睡前来一篇,翻译的)
    .net 连接Redis
    idea external libraries 只有jdk问题
    MAC vim安装gruvbox主题
    Mac gitk安装与优化
    spring security 4 filter 顺序及作用
    mysql 采样查询 / 间隔查询 / 跳跃查询的两种实现思路
    nginx warn an upstream response is buffered to a temporary file /var/cache/nginx/proxy_temp/ while reading upstream
    nginx日志 logrotate配置
  • 原文地址:https://www.cnblogs.com/sdflysha/p/20200122-validate-identity-password-using-dotnet-core.html
Copyright © 2011-2022 走看看