PasswordHash的组成说明
(一) 概述
Asp.Net Core Identity中的IdentityUser表中,PasswordHash默认是一长串的Base64格式的字符串,目前版本称为V3,而Asp.Net Identity中的版本称为V2。
比如,在网页注册页面输入的密码Ss_123,可能生成的PasswordHash为:AQAAAAEAACcQAAAAEHfLUrXi8Zh9fMzc6PC4b0q1JzQYhMoVMlTUFtJnIuMhMKfuOqw+tVz/1pXg0jzHgg==
该string实际是代表61个字节的二进制数据的Base64格式,本质上是包括算法名称、Salt和Hash结果的字符串数据。解析如下:
(1)PasswordHash的数据格式分为V2版和V3版,其中V2版是Asp.net Identity中使用的,而V3版实在Asp.Net Core Identity中使用的,前者长度为49字节,后者长度为61字节。
(2)V3版本中,数据长度:61字节,即1+4+4+4+16+32=61,表示 “版本+算法类型+迭代次数+Salt长度+Salt数据+密码哈希结果 ”,其中版本的值为1,迭代次数的默认值为10000,Salt占用的字节长度默认为16,哈希值的默认长度为32。默认情况下,使用的算法为HMCASHA256。需要说明的是,算法类型、迭代次数、Salt长度均占据4个字节,按照Big-endian格式存放,与Intel CPU中的little-endian不同,因此需要使用这些数据的时候,需要进行转换。比如:Array.Reverse()方法可以对数组中存放的数据进行转换,另外Asp.Net Core Identity源码中使用的转换方式为:
//buffer的length应该等于4,然后通过移位与位或运算来将big-endian转换位little-endian
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]));
}
(3)V2版本中:数据长度:49字节,1+16+32=49,表示 “版本+Salt+哈希值 ”,其中版本的值为0,Salt占用的字节长度为16,哈希值占用32直接。默认情况下,该版本的迭代次数为1000,默认算法为HMCASHA1,相应的Base64的长度为68.
(二)Hash算法
接着需要说明的是算法,生成Hash值的算法来自KeyDerivation类中的Pbkdf2方法,输入参数包括:原始密码,盐,算法,迭代次数,生成的Hash值的字节数。
Console.Write("Enter a password: ");
//需要进行Hash运算的原始密码
string password = Console.ReadLine();
// 产生随机的128位的Salt
byte[] salt = new byte[128 / 8];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(salt);
}
// 生成256位的hash结果 (算法:HMACSHA256,迭代次数:10000,盐:salt数组)
//注:numBytesRequested表示生成的Hash值的字节数
string hashed = Convert.ToBase64String(KeyDerivation.Pbkdf2(
password: password,
salt: salt,
prf: KeyDerivationPrf.HMACSHA256,
iterationCount: 10000,
numBytesRequested: 256 / 8));
Console.WriteLine($"Hashed: {hashed}");
(三)测试
比如,当passwordhash值为“AQAAAAEAACcQAAAAEHfLUrXi8Zh9fMzc6PC4b0q1JzQYhMoVMlTUFtJnIuMhMKfuOqw+tVz/1pXg0jzHgg==”时(原始密码为Ss_123),则通过以下代码的计算,可以证实上面的说法。
//针对v3版本的测试
Console.Write("Enter a password: "); //此处输入Ss_123
string password = Console.ReadLine();
string passwordHashString= "AQAAAAEAACcQAAAAEHfLUrXi8Zh9fMzc6PC4b0q1JzQYhMoVMlTUFtJnIuMhMKfuOqw+tVz/1pXg0jzHgg==";
byte[] hashpwd = Convert.FromBase64String(passwordHashString);
byte version = hashpwd[0];
byte[] count =hashpwd[5..9]; //迭代次数
byte[] saltLen = hashpwd[9..13]; //盐长度,此处为big-endian存放的
byte[] saltData = hashpwd[13..29]; //盐
byte[] phash = hashpwd[29..61]; //hash值,又叫sub-key
Array.Reverse(count); //将big-endian转换为little-endian
Array.Reverse(saltLen); //将big-endian转换