ASP.NET Identity 的默认密码哈希器 - 它是如何工作的,它是不是安全?

Posted

技术标签:

【中文标题】ASP.NET Identity 的默认密码哈希器 - 它是如何工作的,它是不是安全?【英文标题】:ASP.NET Identity's default Password Hasher - How does it work and is it secure?ASP.NET Identity 的默认密码哈希器 - 它是如何工作的,它是否安全? 【发布时间】:2014-01-04 11:52:33 【问题描述】:

我想知道在 MVC 5 和 ASP.NET 身份框架附带的UserManager 中默认实现的密码哈希是否足够安全?如果是这样,您能否向我解释一下它是如何工作的?

IPPasswordHasher 界面如下所示:

public interface IPasswordHasher

    string HashPassword(string password);
    PasswordVerificationResult VerifyHashedPassword(string hashedPassword, 
                                                       string providedPassword);

如您所见,它不需要盐,但在此线程中提到:“Asp.net Identity password hashing” 它确实在幕后加盐。所以我想知道它是如何做到的?这种盐是从哪里来的?

我担心的是盐是静态的,因此非常不安全。

【问题讨论】:

我不认为这直接回答了你的问题,但是 Brock Allen 在这里写了你的一些担忧 => brockallen.com/2013/10/20/… 并且还写了一个开源的用户身份管理和身份验证库,它有各种样板功能,如密码重置、散列等。github.com/brockallen/BrockAllen.MembershipReboot @Shiva 谢谢,我会查看图书馆和页面上的视频。但我宁愿不必处理外部库。如果我能避免,就不会。 仅供参考:*** 等效的安全性。所以虽然你经常会在这里得到一个好的/正确的答案。专家们在security.stackexchange.com 特别是评论“它安全吗”我问了一个类似的问题,答案的深度和质量令人惊叹。 @philsoady 谢谢,这当然有道理,我已经在其他一些“子论坛”上,如果我没有得到答案,我可以使用,我会转到 @ 987654327@。并感谢您的提示! 【参考方案1】:

这是默认实现(ASP.NET Framework 或 ASP.NET Core)的工作方式。它使用带有随机盐的Key Derivation Function 来生成散列。盐包含在 KDF 的输出中。因此,每次你“散列”相同的密码时,你都会得到不同的散列。为了验证散列,输出被拆分回 salt 和其余部分,然后 KDF 再次在具有指定 salt 的密码上运行。如果结果与初始输出的其余部分匹配,则验证哈希。

散列:

public static string HashPassword(string password)

    byte[] salt;
    byte[] buffer2;
    if (password == null)
    
        throw new ArgumentNullException("password");
    
    using (Rfc2898DeriveBytes bytes = new Rfc2898DeriveBytes(password, 0x10, 0x3e8))
    
        salt = bytes.Salt;
        buffer2 = bytes.GetBytes(0x20);
    
    byte[] dst = new byte[0x31];
    Buffer.BlockCopy(salt, 0, dst, 1, 0x10);
    Buffer.BlockCopy(buffer2, 0, dst, 0x11, 0x20);
    return Convert.ToBase64String(dst);

验证:

public static bool VerifyHashedPassword(string hashedPassword, string password)

    byte[] buffer4;
    if (hashedPassword == null)
    
        return false;
    
    if (password == null)
    
        throw new ArgumentNullException("password");
    
    byte[] src = Convert.FromBase64String(hashedPassword);
    if ((src.Length != 0x31) || (src[0] != 0))
    
        return false;
    
    byte[] dst = new byte[0x10];
    Buffer.BlockCopy(src, 1, dst, 0, 0x10);
    byte[] buffer3 = new byte[0x20];
    Buffer.BlockCopy(src, 0x11, buffer3, 0, 0x20);
    using (Rfc2898DeriveBytes bytes = new Rfc2898DeriveBytes(password, dst, 0x3e8))
    
        buffer4 = bytes.GetBytes(0x20);
    
    return ByteArraysEqual(buffer3, buffer4);

【讨论】:

所以如果我理解正确,HashPassword 函数会在同一个字符串中返回两者?当您验证它时,它会再次将其拆分,并使用拆分的盐对传入的明文密码进行哈希处理,并将其与原始哈希进行比较? @AndréSnedeHansen,没错。我也建议您询问安全性或密码学 SE。在这些各自的上下文中,“它是否安全”部分可能会得到更好的解决。 @shajeerpuzhakkal 如上述答案所述。 @AndrewSavinykh 我知道,这就是我要问的原因 - 有什么意义?让代码看起来更智能? ;) 因为对我来说,使用十进制数字计算东西更直观(毕竟我们有 10 个手指 - 至少我们大多数人是这样),所以使用十六进制声明一些东西似乎是不必要的代码混淆。 @MihaiAlexandru-Ionut var hashedPassword = HashPassword(password); var result = VerifyHashedPassword(hashedPassword, password); - 是您需要做的。之后 result 包含 true。【参考方案2】:

因为现在 ASP.NET 是开源的,你可以在 GitHub 上找到它: AspNet.Identity 3.0 和 AspNet.Identity 2.0。

来自cmets:

/* =======================
 * 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.)
 */

【讨论】:

是的,值得注意的是,zespri 显示的算法还有一些附加内容。 GitHub 上的源代码是 Asp.Net.Identity 3.0,它仍处于预发布阶段。 2.0哈希函数的来源在CodePlex 最新的实现可以在github.com/dotnet/aspnetcore/blob/master/src/Identity/…下找到。他们归档了另一个存储库;)【参考方案3】:

我理解已接受的答案,并对其投了赞成票,但我想我会把外行的答案放在这里...

创建哈希

    盐是使用函数随机生成的 Rfc2898DeriveBytes 生成哈希和盐。 Rfc2898DeriveBytes 的输入是密码、要生成的盐的大小以及要执行的散列迭代次数。 https://msdn.microsoft.com/en-us/library/h83s4e12(v=vs.110).aspx 然后将盐和哈希混合在一起(先是盐,然后是 通过哈希)并编码为字符串(因此盐被编码在 哈希)。然后这个编码的哈希(包含盐和哈希)是 针对用户存储(通常)在数据库中。

根据哈希检查密码

检查用户输入的密码。

    盐是从存储的散列密码中提取的。 salt 用于使用 Rfc2898DeriveBytes 的重载对用户输入密码进行哈希处理,该重载采用 salt 而不是生成一个。 https://msdn.microsoft.com/en-us/library/yx129kfs(v=vs.110).aspx 然后比较存储的散列和测试散列。

哈希

实际上,哈希是使用 SHA1 哈希函数 (https://en.wikipedia.org/wiki/SHA-1) 生成的。 该函数被迭代调用1000次(在默认的Identity实现中)

为什么这样安全

随机盐意味着攻击者无法使用预先生成的表 尝试破解密码的哈希值。他们需要生成一个 每种盐的哈希表。 (这里假设黑客也破坏了你的盐) 如果 2 个密码相同,它们将 有不同的哈希值。 (意味着攻击者无法推断出“普通” 密码) 迭代调用 SHA1 1000 次意味着 攻击者也需要这样做。这个想法是,除非他们有 在超级计算机上的时间,他们将没有足够的资源来蛮力 强制从哈希中输入密码。它会大大减慢为给定盐生成哈希表的时间。

【讨论】:

感谢您的解释。在“创建哈希 2”中。你提到盐和哈希是混合在一起的,你知道这是否存储在 AspNetUsers 表的 PasswordHash 中。盐是否存放在任何地方供我查看? @unicorn2 如果你看一下 Andrew Savinykh 的回答...数据库。您将能够在 PasswordHash 表中看到这个 Base64 编码的字符串。关于 Base64 字符串,你只能说它的前三分之一是盐。有意义的 salt 是存储在 PasswordHash 表中的完整字符串的 Base64 解码版本的前 16 个字节 @Nattrass,我对哈希和盐的理解相当初级,但是如果盐很容易从哈希密码中提取出来,那么盐的意义何在。我认为加盐是哈希算法的额外输入,不容易被猜到。 @NSouth 唯一的盐使得哈希对于给定的密码是唯一的。所以两个相同的密码会有不同的哈希值。访问您的哈希和盐仍然无法让攻击者记住您的密码。哈希是不可逆的。他们仍然需要暴力破解所有可能的密码。唯一的盐只是意味着如果黑客设法掌握了您的整个用户表,他们就无法通过对特定哈希的频率分析来推断通用密码。【参考方案4】:

对于像我这样的新手,这里是带有 const 的代码和比较 byte[] 的实际方法。我从 *** 获得了所有这些代码,但定义了 consts,因此可以更改值,也可以更改

// 24 = 192 bits
    private const int SaltByteSize = 24;
    private const int HashByteSize = 24;
    private const int HasingIterationsCount = 10101;


    public static string HashPassword(string password)
    
        // http://***.com/questions/19957176/asp-net-identity-password-hashing

        byte[] salt;
        byte[] buffer2;
        if (password == null)
        
            throw new ArgumentNullException("password");
        
        using (Rfc2898DeriveBytes bytes = new Rfc2898DeriveBytes(password, SaltByteSize, HasingIterationsCount))
        
            salt = bytes.Salt;
            buffer2 = bytes.GetBytes(HashByteSize);
        
        byte[] dst = new byte[(SaltByteSize + HashByteSize) + 1];
        Buffer.BlockCopy(salt, 0, dst, 1, SaltByteSize);
        Buffer.BlockCopy(buffer2, 0, dst, SaltByteSize + 1, HashByteSize);
        return Convert.ToBase64String(dst);
    

    public static bool VerifyHashedPassword(string hashedPassword, string password)
    
        byte[] _passwordHashBytes;

        int _arrayLen = (SaltByteSize + HashByteSize) + 1;

        if (hashedPassword == null)
        
            return false;
        

        if (password == null)
        
            throw new ArgumentNullException("password");
        

        byte[] src = Convert.FromBase64String(hashedPassword);

        if ((src.Length != _arrayLen) || (src[0] != 0))
        
            return false;
        

        byte[] _currentSaltBytes = new byte[SaltByteSize];
        Buffer.BlockCopy(src, 1, _currentSaltBytes, 0, SaltByteSize);

        byte[] _currentHashBytes = new byte[HashByteSize];
        Buffer.BlockCopy(src, SaltByteSize + 1, _currentHashBytes, 0, HashByteSize);

        using (Rfc2898DeriveBytes bytes = new Rfc2898DeriveBytes(password, _currentSaltBytes, HasingIterationsCount))
        
            _passwordHashBytes = bytes.GetBytes(SaltByteSize);
        

        return AreHashesEqual(_currentHashBytes, _passwordHashBytes);

    

    private static bool AreHashesEqual(byte[] firstHash, byte[] secondHash)
    
        int _minHashLength = firstHash.Length <= secondHash.Length ? firstHash.Length : secondHash.Length;
        var xor = firstHash.Length ^ secondHash.Length;
        for (int i = 0; i < _minHashLength; i++)
            xor |= firstHash[i] ^ secondHash[i];
        return 0 == xor;
    

在您的自定义 ApplicationUserManager 中,您将 PasswordHasher 属性设置为包含上述代码的类的名称。

【讨论】:

对于这个.. _passwordHashBytes = bytes.GetBytes(SaltByteSize); 我猜你的意思是这个_passwordHashBytes = bytes.GetBytes(HashByteSize);.. 在你的场景中没关系,因为它们的大小相同但一般......

以上是关于ASP.NET Identity 的默认密码哈希器 - 它是如何工作的,它是不是安全?的主要内容,如果未能解决你的问题,请参考以下文章

ASP.NET Core Identity - 扩展密码哈希

如何在 Asp.Net identity 2 中手动检查密码?

ASP.NET Core Identity 系列之三

如何在 ASP.NET 5 MVC 6 (vNext) 中定义 Identity 的密码规则?

如何覆盖 ASP.NET Core Identity 的密码策略

如何在 ASP.Net MVC Identity 2 中更改密码验证?