ASP.NET Identity的默认密码哈希 – 它是如何工作的并且是否安全?

我想知道在MVC 5和ASP.NET Identity Framework附带的UserManager中默认实现的Password Hasher是否足够安全? 如果是这样,如果你可以向我解释它是如何工作的?

IPasswordHasher界面如下所示:

public interface IPasswordHasher { string HashPassword(string password); PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword); } 

正如你所看到的,它不需要盐,但是在这个post中提到:“ Asp.net身份密码哈希 ”,它确实在幕后加盐。 所以我想知道它是如何做到的? 这盐来自哪里?

我担心的是盐是静态的,使它非常不安全。

以下是默认实现的工作原理。 它使用随机盐的密钥派生函数来生成哈希。 盐被列为KDF产出的一部分。 因此,每次“哈希”相同的密码时,您将获得不同的哈希值。 为了validation散列,输出被分割回salt和其余部分,并且在具有指定salt的密码上再次运行KDF。 如果结果与初始输出的其余部分匹配,则validation散列。

哈希:

 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); } 

validation:

 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); } 

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

来自评论:

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

我理解接受的答案,并且已经投了票,但我认为我会在这里抛弃我的外行答案……

创建哈希

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

根据哈希检查密码

检查用户输入的密码。

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

哈希

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

为什么这是安全的

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

对于像我这样全新的人来说,这里是带有const的代码和比较byte []的实际方法。 我从stackoverflow获得了所有这些代码,但定义了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://stackoverflow.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属性设置为包含上述代码的类的名称。