pbkdf2-如何验证散列用户密码(C#)



当用户注册时,我运行他们通过CreateHash((输入的密码-见下文。

然后我收到了一本字典<字符串,字符串>其中有Hash和Salt,然后我将它们分别作为varchar(256(存储在SQL数据库中。

当用户尝试登录时,我会从数据库中检索salt,并将其和输入的密码传递到GetHash((中-请参阅下面的

我得到的散列与数据库中的不匹配。

我可能做错了什么?

public class EncryptionHelper
{
public const int SALT_SIZE = 128;
public const int HASH_SIZE = 128;
public const int ITERATIONS = 100000;
// On declaring a new password for example
public static Dictionary<string, string> CreateHash(string input)
{
Dictionary<string, string> hashAndSalt = new Dictionary<string, string>();
RNGCryptoServiceProvider provider = new RNGCryptoServiceProvider();
byte[] salt = new byte[SALT_SIZE];
provider.GetBytes(salt);
Rfc2898DeriveBytes pbkdf2 = new Rfc2898DeriveBytes(input, salt, ITERATIONS);
hashAndSalt.Add("Hash", Encoding.Default.GetString(pbkdf2.GetBytes(HASH_SIZE)));
hashAndSalt.Add("Salt", Encoding.Default.GetString(salt));
return hashAndSalt;
}
// To check if Users password input is correct for example
public static string GetHash(string saltString, string passwordString)
{
byte[] salt = Encoding.ASCII.GetBytes(saltString);
Rfc2898DeriveBytes pbkdf2 = new Rfc2898DeriveBytes(passwordString, salt, ITERATIONS);
return Encoding.Default.GetString(pbkdf2.GetBytes(HASH_SIZE));
}
}

我认为您有两个问题。

  1. 正如Encoding.Default的文档所述,它返回的编码可能因计算机而异,甚至在单个计算机上也是如此。这个可能不是你的问题,但在选择编码时应该慎重
  2. 大多数文本编码不是我所说的"编码";圆形可跳闸";对于除文本之外的任何内容,它们被设计为进行编码

第二个选项可能是您的问题。让我们假设正在使用Encoding.UTF8

假设provider.GetBytes(salt)生成以下字节(表示为十六进制(:

78 73 AB 7A 4F 61 E9 3E 8A 96

我们可以使用以下代码将其转换为字符串并返回:

byte[] salt = new byte[10];
provider.GetBytes(salt);
string saltText = Encoding.UTF8.GetString(salt);
salt = Encoding.UTF8.GetBytes(saltText);

现在让我们看一下十六进制的输出:

78 73 EF BF BD 7A 4F 61 EF BF BD 3E EF BF BD EF BF BD

发生了什么事?为什么我们没有拿出和放进去一样的东西?

好吧,我们已经尝试将字节解释为UTF8,但其中一些字节不能正确解码为字符串(因为它不是UTF8编码的文本(。这意味着我们得到的字符串并不代表二进制数据。然后,我们尝试将字符串转换回二进制数据,得到完全不同的结果。

为了解决这个问题,您需要使用旨在准确表示二进制数据的编码。有几个常见的选项:

  1. 十六进制(如上所述(
  2. Base64

由于base64选项是内置的,所以让我们使用它。

public static Dictionary<string, string> CreateHash(string input)
{
Dictionary<string, string> hashAndSalt = new Dictionary<string, string>();
RNGCryptoServiceProvider provider = new RNGCryptoServiceProvider();
byte[] salt = new byte[SALT_SIZE];
provider.GetBytes(salt);
Rfc2898DeriveBytes pbkdf2 = new Rfc2898DeriveBytes(input, salt, ITERATIONS);
hashAndSalt.Add("Hash", Convert.ToBase64String(pbkdf2.GetBytes(HASH_SIZE)));
hashAndSalt.Add("Salt", Convert.ToBase64String(salt));
return hashAndSalt;
}
// To check if Users password input is correct for example
public static string GetHash(string saltString, string passwordString)
{
byte[] salt = Convert.FromBase64String(saltString);
Rfc2898DeriveBytes pbkdf2 = new Rfc2898DeriveBytes(passwordString, salt, ITERATIONS);
return Convert.ToBase64String(pbkdf2.GetBytes(HASH_SIZE));
}

现在,因为我们使用的是base64,而不是不能处理二进制数据的字符串编码,所以我们得到了相同的散列!

如果你想要十六进制,你可以:

  • 使用Convert.ToHexStringConvert.FromHexString,假设您正在使用。NET 5或更新版本
  • 如果您使用的是旧版本,请参阅这些答案来实现转换为十六进制和从十六进制转换的方法

我的建议是将这些值作为二进制数据而不是字符串存储在数据库中。您不需要将它们转换为字符串,将它们保持为二进制更有意义。

Encoding.Default.GetString()将把字节数组解释为它已经包含字符串数据。这是不对的;数组包含随机字节。相反,您希望获得字节的Base64编码版本。使用base-64编码版本将允许您为salt返回相同的字节数组。

我还建议使用Tuple而不是Dictionary,这将节省几个分配:

public static (string salt, string hash) CreateHash(string input)
{
var provider = new RNGCryptoServiceProvider();
byte[] salt = new byte[SALT_SIZE];
provider.GetBytes(salt);
var pbkdf2 = new Rfc2898DeriveBytes(input, salt, ITERATIONS);
var hash = pbkdf2.GetBytes(HASH_SIZE);
var saltString = Convert.ToBase64String(salt);
var hashString = Convert.ToBase64String(hash);
return (saltString, hashString);
}
public static string GetHash(string saltString, string passwordString)
{
byte[] salt = Convert.FromBase64String(saltString);       
var pbkdf2 = new Rfc2898DeriveBytes(passwordString, salt, ITERATIONS);
return Convert.ToBase64String(pbkdf2.GetBytes(HASH_SIZE));
}
public static bool ValidateLogin(string username, string password)
{ 
//made up the GetUserCredential() method
(string saltString, string hashString) cred = GetUserCredential(username);
//add code here to return false if credential lookup fails
return GetHash(cred.saltString, password) == cred.hashString;
}

在这里看到它的工作:

https://dotnetfiddle.net/aiJnS2

编码。默认值可能是ANSI编码,因为它基于ANSI代码页,所以很容易丢失数据。我会使用Base64作为散列的文本表示。

public static string GetHash(string saltString, string passwordString)
{
var salt = Encoding.ASCII.GetBytes(saltString);
var pbkdf2 = new Rfc2898DeriveBytes(passwordString, salt, ITERATIONS);
return Convert.ToBase64String(pbkdf2.GetBytes(HASH_SIZE));
}

这是因为您为两个Rfc2898DeriveBytes实例提供了两种不同的盐。

CreateHash()中,Encoding.Default.GetString(salt)尝试将字节编码为字符串。应该避免它,因为它可能会失败或以不可逆的方式转换字节。(在GetHash()中,Encoding.ASCII.GetBytes(saltString)将与您之前在CreateHash()生成的盐不同。(

使用诸如base16或base64之类的编码。请先看这个问题。

  • 如何将字节数组转换为十六进制字符串,反之亦然

最新更新