在 C# 中重现 JS PBKDF2 哈希
Reproducing JS PBKDF2 Hash in C#
我必须为现有数据库实施一些新的安全功能。他们过去常常使用来自数据库的盐在客户端对密码进行哈希处理。在将密码发送到服务器之前,他们使用此代码对密码进行哈希处理:
var hash = CryptoJS.PBKDF2(password, USER_SALT, {
keySize: 4,
iterations: 1000
});
现在您已经可以猜到,这是非常不安全的,如果用户从服务器获取 Salt 并且在客户端完成 hasing,攻击者就会获取 PW,就好像它是以纯文本发送的一样。这就是为什么我需要在服务器端进行。
现在该数据库有数千个条目,其中包含使用该数据库的用户的散列密码。我尝试在 C# 中使用许多来自 Internet 的引用来实现类似的方法,但我无法获得与存储在数据库中相同的哈希值。
public static string HashPassword(string password, string salt)
{
byte[] saltBytes = Encoding.UTF8.GetBytes(salt);
using (var rfc2898 = new Rfc2898DeriveBytes(password, saltBytes, 1000))
{
byte[] hash = rfc2898.GetBytes(16);
string hashString = string.Empty;
foreach (byte x in hash)
{
hashString += String.Format("{0:x2}", x);
}
return hashString;
}
}
这是我用的方法。我想重现 CryptoJS 的密钥大小 4,所以我会得到 4x = 32 位长密码哈希。我确实得到了它们,但它们与我在 JS 和 CryptoJS 中得到的不同。
有谁知道如何获得与 CryptoJS 版本相同的结果?因为数据库中的所有密码都以这种方式存储,所以现在仅在服务器端再次以相同的方式生成它们至关重要。 (密码现在已针对攻击进行了加密)。
使用JSFiddle for CryptoJS/PBKDF2(虽然我不得不更正CDN链接),其相关代码是
var password = $("[name='password']").val();
var iterations = 1000;
// sizes must be a multiple of 32
var keySize = 128;
var ivSize = 128;
var salt = CryptoJS.lib.WordArray.random(128/8);
$("#salt").html(salt.toString(CryptoJS.enc.Base64));
$("#iter").html(iterations);
$("#keysize").html(keySize);
$("#ivsize").html(ivSize);
var output = CryptoJS.PBKDF2(password, salt, {
keySize: (keySize+ivSize)/32,
iterations: iterations
});
// the underlying words arrays might have more content than was asked: remove insignificant words
output.clamp();
var key = CryptoJS.lib.WordArray.create(output.words.slice(0, keySize/32));
var iv = CryptoJS.lib.WordArray.create(output.words.slice(keySize/32));
$("#hasher").html("SHA1");
$("#key").html(key.toString(CryptoJS.enc.Base64));
$("#iv").html(iv.toString(CryptoJS.enc.Base64));
我得到了一个样本 input/output "hello"
带有 0CD1HGFdkclqcWG5aV+rvw==
的(base64)盐(和默认哈希算法,具有指定的 1000 次迭代和 32+32 字节输出);产生了 tRczLRRuFy/zFiPn1PBKmQ==
/ dhyeE+0Dd9avSJbM/4TcNw==
.
随后使用了以下 C# 发现代码:
string password = "hello";
byte[] salt = Convert.FromBase64String("0CD1HGFdkclqcWG5aV+rvw==");
int iterations = 1000;
using (var pbkdf2 = new Rfc2898DeriveBytes(password, salt, iterations, HashAlgorithmName.SHA1))
{
Console.WriteLine("UTF-8 / SHA-1");
Console.Write(Convert.ToBase64String(pbkdf2.GetBytes(16)));
Console.Write(' ');
Console.WriteLine(Convert.ToBase64String(pbkdf2.GetBytes(16)));
}
using (var pbkdf2 = new Rfc2898DeriveBytes(password, salt, iterations, HashAlgorithmName.SHA256))
{
Console.WriteLine("UTF-8 / SHA-2-256");
Console.Write(Convert.ToBase64String(pbkdf2.GetBytes(16)));
Console.Write(' ');
Console.WriteLine(Convert.ToBase64String(pbkdf2.GetBytes(16)));
}
byte[] utf16 = Encoding.Unicode.GetBytes(password);
using (var pbkdf2 = new Rfc2898DeriveBytes(utf16, salt, iterations, HashAlgorithmName.SHA1))
{
Console.WriteLine("UTF-16LE / SHA-2-256");
Console.Write(Convert.ToBase64String(pbkdf2.GetBytes(16)));
Console.Write(' ');
Console.WriteLine(Convert.ToBase64String(pbkdf2.GetBytes(16)));
}
using (var pbkdf2 = new Rfc2898DeriveBytes(utf16, salt, iterations, HashAlgorithmName.SHA256))
{
Console.WriteLine("UTF-16LE / SHA-2-256");
Console.Write(Convert.ToBase64String(pbkdf2.GetBytes(16)));
Console.Write(' ');
Console.WriteLine(Convert.ToBase64String(pbkdf2.GetBytes(16)));
}
输出为
UTF-8 / SHA-1
tRczLRRuFy/zFiPn1PBKmQ== dhyeE+0Dd9avSJbM/4TcNw==
UTF-8 / SHA-2-256
lkBtILt+xDNEQrX0aWUk3Q== ouOiijCw5sjfMcJo9YZ4Ug==
UTF-16LE / SHA-2-256
1T2gJFFECc5AnpvoiFrBwg== rmHsTuOQdM5YDsmzklMEUQ==
UTF-16LE / SHA-2-256
G4/Ik5vZAd2l8kwq45BKaw== Iqy61Eaf8jmoxO2TpA+rkg==
结论:CryptoJS.PBKDF2 使用的是 SHA-1 和 UTF-8,这意味着最有可能的问题是,如果您没有得到相同的答案,那么您加载的盐分有误。如果它不是 UTF-8 字符串,它可能是十六进制数据或 base64(但这完全取决于您的用法,这里没有 "the right answer is",因为 salt 是 "just bytes")。
我必须为现有数据库实施一些新的安全功能。他们过去常常使用来自数据库的盐在客户端对密码进行哈希处理。在将密码发送到服务器之前,他们使用此代码对密码进行哈希处理:
var hash = CryptoJS.PBKDF2(password, USER_SALT, {
keySize: 4,
iterations: 1000
});
现在您已经可以猜到,这是非常不安全的,如果用户从服务器获取 Salt 并且在客户端完成 hasing,攻击者就会获取 PW,就好像它是以纯文本发送的一样。这就是为什么我需要在服务器端进行。
现在该数据库有数千个条目,其中包含使用该数据库的用户的散列密码。我尝试在 C# 中使用许多来自 Internet 的引用来实现类似的方法,但我无法获得与存储在数据库中相同的哈希值。
public static string HashPassword(string password, string salt)
{
byte[] saltBytes = Encoding.UTF8.GetBytes(salt);
using (var rfc2898 = new Rfc2898DeriveBytes(password, saltBytes, 1000))
{
byte[] hash = rfc2898.GetBytes(16);
string hashString = string.Empty;
foreach (byte x in hash)
{
hashString += String.Format("{0:x2}", x);
}
return hashString;
}
}
这是我用的方法。我想重现 CryptoJS 的密钥大小 4,所以我会得到 4x = 32 位长密码哈希。我确实得到了它们,但它们与我在 JS 和 CryptoJS 中得到的不同。
有谁知道如何获得与 CryptoJS 版本相同的结果?因为数据库中的所有密码都以这种方式存储,所以现在仅在服务器端再次以相同的方式生成它们至关重要。 (密码现在已针对攻击进行了加密)。
使用JSFiddle for CryptoJS/PBKDF2(虽然我不得不更正CDN链接),其相关代码是
var password = $("[name='password']").val();
var iterations = 1000;
// sizes must be a multiple of 32
var keySize = 128;
var ivSize = 128;
var salt = CryptoJS.lib.WordArray.random(128/8);
$("#salt").html(salt.toString(CryptoJS.enc.Base64));
$("#iter").html(iterations);
$("#keysize").html(keySize);
$("#ivsize").html(ivSize);
var output = CryptoJS.PBKDF2(password, salt, {
keySize: (keySize+ivSize)/32,
iterations: iterations
});
// the underlying words arrays might have more content than was asked: remove insignificant words
output.clamp();
var key = CryptoJS.lib.WordArray.create(output.words.slice(0, keySize/32));
var iv = CryptoJS.lib.WordArray.create(output.words.slice(keySize/32));
$("#hasher").html("SHA1");
$("#key").html(key.toString(CryptoJS.enc.Base64));
$("#iv").html(iv.toString(CryptoJS.enc.Base64));
我得到了一个样本 input/output "hello"
带有 0CD1HGFdkclqcWG5aV+rvw==
的(base64)盐(和默认哈希算法,具有指定的 1000 次迭代和 32+32 字节输出);产生了 tRczLRRuFy/zFiPn1PBKmQ==
/ dhyeE+0Dd9avSJbM/4TcNw==
.
随后使用了以下 C# 发现代码:
string password = "hello";
byte[] salt = Convert.FromBase64String("0CD1HGFdkclqcWG5aV+rvw==");
int iterations = 1000;
using (var pbkdf2 = new Rfc2898DeriveBytes(password, salt, iterations, HashAlgorithmName.SHA1))
{
Console.WriteLine("UTF-8 / SHA-1");
Console.Write(Convert.ToBase64String(pbkdf2.GetBytes(16)));
Console.Write(' ');
Console.WriteLine(Convert.ToBase64String(pbkdf2.GetBytes(16)));
}
using (var pbkdf2 = new Rfc2898DeriveBytes(password, salt, iterations, HashAlgorithmName.SHA256))
{
Console.WriteLine("UTF-8 / SHA-2-256");
Console.Write(Convert.ToBase64String(pbkdf2.GetBytes(16)));
Console.Write(' ');
Console.WriteLine(Convert.ToBase64String(pbkdf2.GetBytes(16)));
}
byte[] utf16 = Encoding.Unicode.GetBytes(password);
using (var pbkdf2 = new Rfc2898DeriveBytes(utf16, salt, iterations, HashAlgorithmName.SHA1))
{
Console.WriteLine("UTF-16LE / SHA-2-256");
Console.Write(Convert.ToBase64String(pbkdf2.GetBytes(16)));
Console.Write(' ');
Console.WriteLine(Convert.ToBase64String(pbkdf2.GetBytes(16)));
}
using (var pbkdf2 = new Rfc2898DeriveBytes(utf16, salt, iterations, HashAlgorithmName.SHA256))
{
Console.WriteLine("UTF-16LE / SHA-2-256");
Console.Write(Convert.ToBase64String(pbkdf2.GetBytes(16)));
Console.Write(' ');
Console.WriteLine(Convert.ToBase64String(pbkdf2.GetBytes(16)));
}
输出为
UTF-8 / SHA-1
tRczLRRuFy/zFiPn1PBKmQ== dhyeE+0Dd9avSJbM/4TcNw==
UTF-8 / SHA-2-256
lkBtILt+xDNEQrX0aWUk3Q== ouOiijCw5sjfMcJo9YZ4Ug==
UTF-16LE / SHA-2-256
1T2gJFFECc5AnpvoiFrBwg== rmHsTuOQdM5YDsmzklMEUQ==
UTF-16LE / SHA-2-256
G4/Ik5vZAd2l8kwq45BKaw== Iqy61Eaf8jmoxO2TpA+rkg==
结论:CryptoJS.PBKDF2 使用的是 SHA-1 和 UTF-8,这意味着最有可能的问题是,如果您没有得到相同的答案,那么您加载的盐分有误。如果它不是 UTF-8 字符串,它可能是十六进制数据或 base64(但这完全取决于您的用法,这里没有 "the right answer is",因为 salt 是 "just bytes")。