如何散列密码

我想在手机上存储密码的哈希表,但我不知道如何做到这一点。我似乎只能找到加密方法。如何正确散列密码?

238667 次浏览

更新 : 这个答案已经严重过时了。请使用来自 https://stackoverflow.com/a/10402129/251311的建议。

你可以选择

var md5 = new MD5CryptoServiceProvider();
var md5data = md5.ComputeHash(data);

或者

var sha1 = new SHA1CryptoServiceProvider();
var sha1data = sha1.ComputeHash(data);

要获取 data作为字节数组,可以使用

var data = Encoding.ASCII.GetBytes(password);

以及从 md5datasha1data获得反向字符串

var hashedPassword = ASCIIEncoding.GetString(md5data);

我使用一个 hash 和一个 salt 进行密码加密(Asp.Net Membership 使用的是同一个 hash) :

private string PasswordSalt
{
get
{
var rng = new RNGCryptoServiceProvider();
var buff = new byte[32];
rng.GetBytes(buff);
return Convert.ToBase64String(buff);
}
}


private string EncodePassword(string password, string salt)
{
byte[] bytes = Encoding.Unicode.GetBytes(password);
byte[] src = Encoding.Unicode.GetBytes(salt);
byte[] dst = new byte[src.Length + bytes.Length];
Buffer.BlockCopy(src, 0, dst, 0, src.Length);
Buffer.BlockCopy(bytes, 0, dst, src.Length, bytes.Length);
HashAlgorithm algorithm = HashAlgorithm.Create("SHA1");
byte[] inarray = algorithm.ComputeHash(dst);
return Convert.ToBase64String(inarray);
}

考虑到今天的最佳实践,这里的大多数其他答案都有些过时了。

NET 中本地可用的最健壮的密码哈希算法是 PBKDF2,由 Rfc2898DeriveBytes类表示。 下面的代码在本文的独立类中: 另一个如何存储加盐密码散列的示例。这些基础知识非常简单,所以下面是一些细节:

STEP 1 使用加密 PRNG 创建 salt 值:

byte[] salt;
new RNGCryptoServiceProvider().GetBytes(salt = new byte[16]);

STEP 2 创建 Rfc2898DeriveBytes 并获取散列值:

var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 100000);
byte[] hash = pbkdf2.GetBytes(20);

STEP 3 组合 salt 和 password 字节以供以后使用:

byte[] hashBytes = new byte[36];
Array.Copy(salt, 0, hashBytes, 0, 16);
Array.Copy(hash, 0, hashBytes, 16, 20);

STEP 4 将组合的 salt + hash 转换为字符串以进行存储

string savedPasswordHash = Convert.ToBase64String(hashBytes);
DBContext.AddUser(new User { ..., Password = savedPasswordHash });

STEP 5 根据存储的密码验证用户输入的密码

/* Fetch the stored value */
string savedPasswordHash = DBContext.GetUser(u => u.UserName == user).Password;
/* Extract the bytes */
byte[] hashBytes = Convert.FromBase64String(savedPasswordHash);
/* Get the salt */
byte[] salt = new byte[16];
Array.Copy(hashBytes, 0, salt, 0, 16);
/* Compute the hash on the password the user entered */
var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 100000);
byte[] hash = pbkdf2.GetBytes(20);
/* Compare the results */
for (int i=0; i < 20; i++)
if (hashBytes[i+16] != hash[i])
throw new UnauthorizedAccessException();

注意: 根据特定应用程序的性能要求,值 100000可以降低。最小值应该在 10000左右。

基于 网站的伟大答案,我为此编写了一个类:

public static class SecurePasswordHasher
{
/// <summary>
/// Size of salt.
/// </summary>
private const int SaltSize = 16;


/// <summary>
/// Size of hash.
/// </summary>
private const int HashSize = 20;


/// <summary>
/// Creates a hash from a password.
/// </summary>
/// <param name="password">The password.</param>
/// <param name="iterations">Number of iterations.</param>
/// <returns>The hash.</returns>
public static string Hash(string password, int iterations)
{
// Create salt
byte[] salt;
new RNGCryptoServiceProvider().GetBytes(salt = new byte[SaltSize]);


// Create hash
var pbkdf2 = new Rfc2898DeriveBytes(password, salt, iterations);
var hash = pbkdf2.GetBytes(HashSize);


// Combine salt and hash
var hashBytes = new byte[SaltSize + HashSize];
Array.Copy(salt, 0, hashBytes, 0, SaltSize);
Array.Copy(hash, 0, hashBytes, SaltSize, HashSize);


// Convert to base64
var base64Hash = Convert.ToBase64String(hashBytes);


// Format hash with extra information
return string.Format("$MYHASH$V1${0}${1}", iterations, base64Hash);
}


/// <summary>
/// Creates a hash from a password with 10000 iterations
/// </summary>
/// <param name="password">The password.</param>
/// <returns>The hash.</returns>
public static string Hash(string password)
{
return Hash(password, 10000);
}


/// <summary>
/// Checks if hash is supported.
/// </summary>
/// <param name="hashString">The hash.</param>
/// <returns>Is supported?</returns>
public static bool IsHashSupported(string hashString)
{
return hashString.Contains("$MYHASH$V1$");
}


/// <summary>
/// Verifies a password against a hash.
/// </summary>
/// <param name="password">The password.</param>
/// <param name="hashedPassword">The hash.</param>
/// <returns>Could be verified?</returns>
public static bool Verify(string password, string hashedPassword)
{
// Check hash
if (!IsHashSupported(hashedPassword))
{
throw new NotSupportedException("The hashtype is not supported");
}


// Extract iteration and Base64 string
var splittedHashString = hashedPassword.Replace("$MYHASH$V1$", "").Split('$');
var iterations = int.Parse(splittedHashString[0]);
var base64Hash = splittedHashString[1];


// Get hash bytes
var hashBytes = Convert.FromBase64String(base64Hash);


// Get salt
var salt = new byte[SaltSize];
Array.Copy(hashBytes, 0, salt, 0, SaltSize);


// Create hash with given salt
var pbkdf2 = new Rfc2898DeriveBytes(password, salt, iterations);
byte[] hash = pbkdf2.GetBytes(HashSize);


// Get result
for (var i = 0; i < HashSize; i++)
{
if (hashBytes[i + SaltSize] != hash[i])
{
return false;
}
}
return true;
}
}

用法:

// Hash
var hash = SecurePasswordHasher.Hash("mypassword");


// Verify
var result = SecurePasswordHasher.Verify("mypassword", hash);

一个示例散列可以是这样的:

$MYHASH$V1$10000$Qhxzi6GNu/Lpy3iUqkeqR/J1hh8y/h5KPDjrv89KzfCVrubn

正如您可以看到的,我还在散列中包含了迭代,以便于使用,并且如果需要升级的话,还可以升级它。


如果你对.net core 感兴趣,我在 守则检讨上也有一个.net core 版本。

  1. 制作一种盐,
  2. 使用 salt 创建哈希密码
  3. 同时保存哈希和盐
  4. 用密码和盐来解密... 所以开发者不能解密密码
public class CryptographyProcessor
{
public string CreateSalt(int size)
{
//Generate a cryptographic random number.
RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
byte[] buff = new byte[size];
rng.GetBytes(buff);
return Convert.ToBase64String(buff);
}




public string GenerateHash(string input, string salt)
{
byte[] bytes = Encoding.UTF8.GetBytes(input + salt);
SHA256Managed sHA256ManagedString = new SHA256Managed();
byte[] hash = sHA256ManagedString.ComputeHash(bytes);
return Convert.ToBase64String(hash);
}


public bool AreEqual(string plainTextInput, string hashedInput, string salt)
{
string newHashedPin = GenerateHash(plainTextInput, salt);
return newHashedPin.Equals(hashedInput);
}
}

我认为使用 KeyDerivation.Pbkdf2比 Rfc2898DeriveBytes 更好。

例子和解释: ASP.NET 核心中的哈希密码

using System;
using System.Security.Cryptography;
using Microsoft.AspNetCore.Cryptography.KeyDerivation;
 
public class Program
{
public static void Main(string[] args)
{
Console.Write("Enter a password: ");
string password = Console.ReadLine();
 
// generate a 128-bit salt using a secure PRNG
byte[] salt = new byte[128 / 8];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(salt);
}
Console.WriteLine($"Salt: {Convert.ToBase64String(salt)}");
 
// derive a 256-bit subkey (use HMACSHA1 with 10,000 iterations)
string hashed = Convert.ToBase64String(KeyDerivation.Pbkdf2(
password: password,
salt: salt,
prf: KeyDerivationPrf.HMACSHA1,
iterationCount: 10000,
numBytesRequested: 256 / 8));
Console.WriteLine($"Hashed: {hashed}");
}
}
 
/*
* SAMPLE OUTPUT
*
* Enter a password: Xtw9NMgx
* Salt: NZsP6NnmfBuYeJrrAKNuVQ==
* Hashed: /OOoOer10+tGwTRDTrQSoeCxVTFr6dtYly7d0cPxIak=
*/

这是文章中的一个示例代码,是最低安全级别。 为了增加它,我将使用 KeyRD ationPrf.HMACSHA1参数来代替

HMACSHA256,或者 KeyDerationPrf.HMACSHA512。

不要在密码哈希问题上妥协。有许多数学上可靠的方法可以优化密码哈希破解。后果可能是灾难性的。 一旦一个恶意分子可以得到您的用户的密码哈希表,它将是相对的 给定的算法较弱或实现不正确的密码很容易被他破解。 他有很多时间(时间 x 计算机能力)来破解密码。密码散列应该具有很强的加密能力,以使“大量时间” 到“ 不合理的时间”。

还有一点要补充

散列验证需要时间(这很好)。 当用户输入错误的用户名时,很快就可以检查用户名是否不正确。 当用户名是正确的,我们开始密码验证-这是一个相对较长的过程。

对于黑客来说,很容易理解用户是否存在。

当用户名错误时,确保不会立即返回答案。

不用说: 永远不要回答什么是错误的。只是一般的“证书是错误的”。

非常感谢您的回答。但是在生产环境中运行了数百万条记录后,我发现存在内存泄漏。加密服务提供商Rfc2898DeriveBytes类是从 IDisposable 派生的,但是我们不释放它们。我会写我的解决方案作为一个答案,如果有人需要与处置版本。

public static class SecurePasswordHasher
{
/// <summary>
/// Size of salt.
/// </summary>
private const int SaltSize = 16;


/// <summary>
/// Size of hash.
/// </summary>
private const int HashSize = 20;


/// <summary>
/// Creates a hash from a password.
/// </summary>
/// <param name="password">The password.</param>
/// <param name="iterations">Number of iterations.</param>
/// <returns>The hash.</returns>
public static string Hash(string password, int iterations)
{
// Create salt
using (var rng = new RNGCryptoServiceProvider())
{
byte[] salt;
rng.GetBytes(salt = new byte[SaltSize]);
using (var pbkdf2 = new Rfc2898DeriveBytes(password, salt, iterations))
{
var hash = pbkdf2.GetBytes(HashSize);
// Combine salt and hash
var hashBytes = new byte[SaltSize + HashSize];
Array.Copy(salt, 0, hashBytes, 0, SaltSize);
Array.Copy(hash, 0, hashBytes, SaltSize, HashSize);
// Convert to base64
var base64Hash = Convert.ToBase64String(hashBytes);


// Format hash with extra information
return $"$HASH|V1${iterations}${base64Hash}";
}
}


}


/// <summary>
/// Creates a hash from a password with 10000 iterations
/// </summary>
/// <param name="password">The password.</param>
/// <returns>The hash.</returns>
public static string Hash(string password)
{
return Hash(password, 10000);
}


/// <summary>
/// Checks if hash is supported.
/// </summary>
/// <param name="hashString">The hash.</param>
/// <returns>Is supported?</returns>
public static bool IsHashSupported(string hashString)
{
return hashString.Contains("HASH|V1$");
}


/// <summary>
/// Verifies a password against a hash.
/// </summary>
/// <param name="password">The password.</param>
/// <param name="hashedPassword">The hash.</param>
/// <returns>Could be verified?</returns>
public static bool Verify(string password, string hashedPassword)
{
// Check hash
if (!IsHashSupported(hashedPassword))
{
throw new NotSupportedException("The hashtype is not supported");
}


// Extract iteration and Base64 string
var splittedHashString = hashedPassword.Replace("$HASH|V1$", "").Split('$');
var iterations = int.Parse(splittedHashString[0]);
var base64Hash = splittedHashString[1];


// Get hash bytes
var hashBytes = Convert.FromBase64String(base64Hash);


// Get salt
var salt = new byte[SaltSize];
Array.Copy(hashBytes, 0, salt, 0, SaltSize);


// Create hash with given salt
using (var pbkdf2 = new Rfc2898DeriveBytes(password, salt, iterations))
{
byte[] hash = pbkdf2.GetBytes(HashSize);


// Get result
for (var i = 0; i < HashSize; i++)
{
if (hashBytes[i + SaltSize] != hash[i])
{
return false;
}
}


return true;
}


}
}

用法:

// Hash
var hash = SecurePasswordHasher.Hash("mypassword");


// Verify
var result = SecurePasswordHasher.Verify("mypassword", hash);

使用下面的类首先生成一个 Salt。每个用户需要一个不同的 salt,我们可以将它与其他用户属性一起保存在数据库中。舍入值决定密码被散列的次数。

详细信息: https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.rfc2898derivebytes.-ctor?view=netcore-3.1#System_Security_Cryptography_Rfc2898DeriveBytes__ctor_System_Byte___System_Byte___System_Int32 _

public class HashSaltWithRounds
{
int saltLength = 32;
public byte[] GenerateSalt()
{
using (var randomNumberGenerator = new RNGCryptoServiceProvider())
{
var randomNumber = new byte[saltLength];
randomNumberGenerator.GetBytes(randomNumber);
return randomNumber;
}
}


public string HashDataWithRounds(byte[] password, byte[] salt, int rounds)
{
using(var rfc2898= new Rfc2898DeriveBytes(password, salt, rounds))
{
return Convert.ToBase64String(rfc2898.GetBytes(32));
}
}
}

我们可以像下面这样从一个控制台应用调用它。

public class Program
{
public static void Main(string[] args)
{
int numberOfIterations = 99;
var hashFunction = new HashSaltWithRounds();


string password = "Your Password Here";
byte[] salt = hashFunction.GenerateSalt();


var hashedPassword1 = hashFunction.HashDataWithRounds(Encoding.UTF8.GetBytes(password), salt, numberOfIterations);
var hashedPassword2 = hashFunction.HashDataWithRounds(Encoding.UTF8.GetBytes(password), salt, numberOfIterations);


Console.WriteLine($"hashedPassword1 :{hashedPassword1}");
Console.WriteLine($"hashedPassword2 :{hashedPassword2}");
Console.WriteLine(hashedPassword1.Equals(hashedPassword2));


Console.ReadLine();


}
}

Output

在 ASP.NET Core 中,使用 PasswordHasher<TUser>
•名称空间: Microsoft.AspNetCore.Identity
•汇编: Microsoft.Extensions.Identity.Core.dll(NuGet | 来源)


要散列密码,请使用 HashPassword():

var hashedPassword = new PasswordHasher<object?>().HashPassword(null, password);

要验证密码,请使用 VerifyHashedPassword():

var passwordVerificationResult = new PasswordHasher<object?>().VerifyHashedPassword(null, hashedPassword, password);
switch (passwordVerificationResult)
{
case PasswordVerificationResult.Failed:
Console.WriteLine("Password incorrect.");
break;
    

case PasswordVerificationResult.Success:
Console.WriteLine("Password ok.");
break;
    

case PasswordVerificationResult.SuccessRehashNeeded:
Console.WriteLine("Password ok but should be rehashed and updated.");
break;
    

default:
throw new ArgumentOutOfRangeException();
}



优点:

  • NET 平台的一部分。比构建自己的加密算法安全和可靠得多。
  • 可配置的迭代计数和未来兼容性(参见 PasswordHasherOptions)。
  • 在验证密码(来源)时考虑到了 定时攻击,就像 PHP去吧所做的那样。

缺点:

2022(. NET 6 +)解决方案:

这里的大多数其他答案都是多年前写的,因此没有利用在更新版本中引入的许多最新特性。NET.现在,同样的事情可以更加简单地实现,而且样板和噪音大大减少。我提出的解决方案还提供了额外的健壮性,允许您在将来修改设置(例如迭代次数等) ,而不必实际破坏旧的散列(例如,如果使用 接受答案提出的解决方案就会发生这种情况)。

优点:

  • 使用了本文介绍的新的静态 Rfc2898DeriveBytes.Pbkdf2()方法。NET 6,消除了每次实例化和释放对象的需要。

  • 使用新的 RandomNumberGenerator类及其静态 GetBytes方法ーー在。NET 6ー生成盐。公认答案 已经过时了中使用的 RNGCryptoServiceProvider类。

  • 使用 CryptographicOperations.FixedTimeEquals方法(在。NET Core 2.1) ,用于比较 Verify方法中的关键字节,而不是像已接受的答案那样手工进行比较。这,除了去除了大量的噪音样板,也抵消了 时间攻击

  • 使用 SHA-256而不是默认的 SHA-1作为底层算法,只是为了安全起见,因为后者是一个更加健壮和可靠的算法。

  • Hash方法返回的字符串具有以下结构:

    [key]:[salt]:[iterations]:[algorithm]

    这是这个解决方案最重要的优点,它意味着我们基本上是 包括关于用于在最终字符串中创建哈希的配置的元数据。这允许我们在将来更改哈希类中的设置(例如迭代次数、 salt/key 大小等) ,而无需破坏以前使用旧设置创建的哈希。。这是一个被接受的答案,例如,没有考虑到的东西,因为它依赖于“当前”配置值来验证散列。

其他要点:

  • 我正在使用键的十六进制表示和返回的散列字符串中的 salt。如果愿意,您可以使用 base64,只需将 Convert.ToHexStringConvert.FromHexString的每个匹配项分别更改为 Convert.ToBase64Convert.FromBase64。其余的逻辑完全相同。
  • 通常推荐的食盐大小是64位或更高。我已经将它设置为128位。
  • 密钥大小通常应该与所选算法的自然输出大小相同ーー参见 此评论。在我们的示例中,如前所述,底层算法是 SHA-256,其输出大小为256位,这正是我们设置密钥大小的位置。
  • 如果您计划使用它来存储用户密码,通常建议使用 至少10,000次或更多次迭代。我已经将默认值设置为50,000,您当然可以根据需要更改它。

密码:

public static class SecretHasher2
{
private const int _saltSize = 16; // 128 bits
private const int _keySize = 32; // 256 bits
private const int _iterations = 100000;
private static readonly HashAlgorithmName _algorithm = HashAlgorithmName.SHA256;


private const char segmentDelimiter = ':';


public static string Hash(string input)
{
byte[] salt = RandomNumberGenerator.GetBytes(_saltSize);
byte[] hash = Rfc2898DeriveBytes.Pbkdf2(
input,
salt,
_iterations,
_algorithm,
_keySize
);
return string.Join(
segmentDelimiter,
Convert.ToHexString(hash),
Convert.ToHexString(salt),
_iterations,
_algorithm
);
}


public static bool Verify(string input, string hashString)
{
string[] segments = hashString.Split(segmentDelimiter);
byte[] hash = Convert.FromHexString(segments[0]);
byte[] salt = Convert.FromHexString(segments[1]);
int iterations = int.Parse(segments[2]);
HashAlgorithmName algorithm = new HashAlgorithmName(segments[3]);
byte[] inputHash = Rfc2898DeriveBytes.Pbkdf2(
input,
salt,
iterations,
algorithm,
hash.Length
);
return CryptographicOperations.FixedTimeEquals(inputHash, hash);
}
}

用法:

// Hash:
string password = "...";
string hashed = SecretHasher.Hash(password);


// Verify:
string enteredPassword = "...";
bool isPasswordCorrect = SecretHasher.Verify(enteredPassword, hashed);