ASP。NET Identity's默认密码散列器-它是如何工作的,它是安全的?

我想知道在MVC 5和ASP附带的UserManager中默认实现的密码散列器是否。NET身份框架,是否足够安全?如果是的话,你能给我解释一下它是怎么运作的吗?

IPasswordHasher接口是这样的:

public interface IPasswordHasher
{
string HashPassword(string password);
PasswordVerificationResult VerifyHashedPassword(string hashedPassword,
string providedPassword);
}
如你所见,它不需要一个盐,但它在这个线程中被提到:"Asp.net身份密码哈希" 它确实在幕后给它加盐。我想知道它是怎么做到的?这些盐是从哪里来的呢?< / p >

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

170985 次浏览

下面是默认实现(ASP。微软网络框架ASP。网络核心)的工作方式。它使用带有随机盐的键推导函数来生成哈希。盐是KDF输出的一部分。因此,每次你“哈希”相同的密码,你将得到不同的哈希。为了验证散列,输出将被分割回盐和其他部分,并且使用指定的盐再次对密码运行KDF。如果结果与初始输出的其余部分匹配,则验证散列。

散列:

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

因为这些天ASP。NET是开源的,你可以在GitHub上找到它: AspNet。3.0身份AspNet。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.)
*/

对于像我这样的新手来说,这里是带有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属性设置为包含上述代码的类的名称。

我理解公认的答案,并对其进行了投票,但我认为我应该在这里抛弃我的外行答案…

创建散列

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

根据散列检查密码

检查用户输入的密码。

  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次意味着 攻击者也需要这样做。我的意思是除非他们有 在超级计算机上,他们没有足够的资源来计算时间 强制密码从哈希。这将大大降低为给定盐生成哈希表的时间。李< / >
我基于.net6 PasswordHasher文档的最新版本(V3)编写我的类PasswordHasher https://github.com/dotnet/aspnetcore/blob/b56bb17db3ae73ce5a8664a2023a9b9af89499dd/src/Identity/Extensions.Core/src/PasswordHasher.cs < / p >
namespace Utilities;


public class PasswordHasher
{
public const int Pbkdf2Iterations = 1000;




public static string HashPasswordV3(string password)
{
return Convert.ToBase64String(HashPasswordV3(password, RandomNumberGenerator.Create()
, prf: KeyDerivationPrf.HMACSHA512, iterCount: Pbkdf2Iterations, saltSize: 128 / 8
, numBytesRequested: 256 / 8));
}




public static bool VerifyHashedPasswordV3(string hashedPasswordStr, string password)
{
byte[] hashedPassword = Convert.FromBase64String(hashedPasswordStr);
var iterCount = default(int);
var prf = default(KeyDerivationPrf);


try
{
// Read header information
prf = (KeyDerivationPrf)ReadNetworkByteOrder(hashedPassword, 1);
iterCount = (int)ReadNetworkByteOrder(hashedPassword, 5);
int saltLength = (int)ReadNetworkByteOrder(hashedPassword, 9);


// Read the salt: must be >= 128 bits
if (saltLength < 128 / 8)
{
return false;
}
byte[] salt = new byte[saltLength];
Buffer.BlockCopy(hashedPassword, 13, salt, 0, salt.Length);


// Read the subkey (the rest of the payload): must be >= 128 bits
int subkeyLength = hashedPassword.Length - 13 - salt.Length;
if (subkeyLength < 128 / 8)
{
return false;
}
byte[] expectedSubkey = new byte[subkeyLength];
Buffer.BlockCopy(hashedPassword, 13 + salt.Length, expectedSubkey, 0, expectedSubkey.Length);


// Hash the incoming password and verify it
byte[] actualSubkey = KeyDerivation.Pbkdf2(password, salt, prf, iterCount, subkeyLength);
#if NETSTANDARD2_0 || NETFRAMEWORK
return ByteArraysEqual(actualSubkey, expectedSubkey);
#elif NETCOREAPP
return CryptographicOperations.FixedTimeEquals(actualSubkey, expectedSubkey);
#else
#error Update target frameworks
#endif
}
catch
{
// This should never occur except in the case of a malformed payload, where
// we might go off the end of the array. Regardless, a malformed payload
// implies verification failed.
return false;
}
}




// privates
private static byte[] HashPasswordV3(string password, RandomNumberGenerator rng, KeyDerivationPrf prf, int iterCount, int saltSize, int numBytesRequested)
{
byte[] salt = new byte[saltSize];
rng.GetBytes(salt);
byte[] subkey = KeyDerivation.Pbkdf2(password, salt, prf, iterCount, numBytesRequested);
var outputBytes = new byte[13 + salt.Length + subkey.Length];
outputBytes[0] = 0x01; // format marker
WriteNetworkByteOrder(outputBytes, 1, (uint)prf);
WriteNetworkByteOrder(outputBytes, 5, (uint)iterCount);
WriteNetworkByteOrder(outputBytes, 9, (uint)saltSize);
Buffer.BlockCopy(salt, 0, outputBytes, 13, salt.Length);
Buffer.BlockCopy(subkey, 0, outputBytes, 13 + saltSize, subkey.Length);
return outputBytes;
}


private static void WriteNetworkByteOrder(byte[] buffer, int offset, uint value)
{
buffer[offset + 0] = (byte)(value >> 24);
buffer[offset + 1] = (byte)(value >> 16);
buffer[offset + 2] = (byte)(value >> 8);
buffer[offset + 3] = (byte)(value >> 0);
}


private static uint ReadNetworkByteOrder(byte[] buffer, int offset)
{
return ((uint)(buffer[offset + 0]) << 24)
| ((uint)(buffer[offset + 1]) << 16)
| ((uint)(buffer[offset + 2]) << 8)
| ((uint)(buffer[offset + 3]));
}


}

在UserController中使用:

namespace WebApi.Controllers.UserController;


[Route("api/[controller]/[action]")]
[ApiController]
public class UserController : ControllerBase
{
private readonly IUserService _userService;
public UserController(IUserService userService)
{
_userService = userService;
}




[HttpPost]
public async Task<IActionResult> Register(VmRegister model)
{
var user = new User
{
UserName = model.UserName,
PasswordHash = PasswordHasher.HashPasswordV3(model.Password),
FirstName = model.FirstName,
LastName = model.LastName,
Mobile = model.Mobile,
Email = model.Email,
};
await _userService.Add(user);
return StatusCode(201, user.Id);
}




[HttpPost]
public async Task<IActionResult> Login(VmLogin model)
{
var user = await _userService.GetByUserName(model.UserName);


if (user is null || !PasswordHasher.VerifyHashedPasswordV3(user.PasswordHash, model.Password))
throw new Exception("The UserName or Password is wrong.");
// generate token
return Ok();
}

https://github.com/mammadkoma/WebApi/tree/master/WebApi

下面是Andrew Savinykh对的回答 我做了以下更改。 我正在使用Dapper与一个已配置了AspNet身份的现有DB

请注意PasswordHasherCompatibilityMode.IdentityV2工作 如果你正在使用AspNet身份认证,那就太好了。尚未测试AspNetCore 身份。< / p >

下面是完整类的GitHub要点