C#中的哈希和盐密码

ACP*_*ACP 174 c# passwords hash salt

我刚刚浏览了DavidHayden关于哈希用户密码的文章之一.

真的,我无法得到他想要实现的目标.

这是他的代码:

private static string CreateSalt(int size)
{
    //Generate a cryptographic random number.
    RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
    byte[] buff = new byte[size];
    rng.GetBytes(buff);

    // Return a Base64 string representation of the random number.
    return Convert.ToBase64String(buff);
}

private static string CreatePasswordHash(string pwd, string salt)
{
    string saltAndPwd = String.Concat(pwd, salt);
    string hashedPwd =
        FormsAuthentication.HashPasswordForStoringInConfigFile(
        saltAndPwd, "sha1");
    return hashedPwd;
}
Run Code Online (Sandbox Code Playgroud)

有没有其他C#方法来散列密码并添加盐?

blo*_*art 240

实际上,这有点奇怪,使用字符串转换 - 成员资格提供程序将它们放入配置文件中.哈希和盐是二进制blob,除非要将它们放入文本文件中,否则不需要将它们转换为字符串.

在我的书" 开始ASP.NET安全"中,(哦,最后,借口皮条客这本书)我做了以下几点

static byte[] GenerateSaltedHash(byte[] plainText, byte[] salt)
{
  HashAlgorithm algorithm = new SHA256Managed();

  byte[] plainTextWithSaltBytes = 
    new byte[plainText.Length + salt.Length];

  for (int i = 0; i < plainText.Length; i++)
  {
    plainTextWithSaltBytes[i] = plainText[i];
  }
  for (int i = 0; i < salt.Length; i++)
  {
    plainTextWithSaltBytes[plainText.Length + i] = salt[i];
  }

  return algorithm.ComputeHash(plainTextWithSaltBytes);            
}
Run Code Online (Sandbox Code Playgroud)

盐生成就是问题中的例子.您可以使用将文本转换为字节数组Encoding.UTF8.GetBytes(string).如果必须将哈希值转换为其字符串表示形式,则可以使用Convert.ToBase64String并将Convert.FromBase64String其转换回来.

您应该注意,您不能在字节数组上使用相等运算符,它会检查引用,因此您应该简单地遍历两个数组,从而检查每个字节

public static bool CompareByteArrays(byte[] array1, byte[] array2)
{
  if (array1.Length != array2.Length)
  {
    return false;
  }

  for (int i = 0; i < array1.Length; i++)
  {
    if (array1[i] != array2[i])
    {
      return false;
    }
  }

  return true;
}
Run Code Online (Sandbox Code Playgroud)

每个密码始终使用新盐.盐不必保密,可以与散列本身一起存放.

  • CompareByteArrays的漂亮LINQ语句重构`return array1.Length == array2.Length &&!array1.Where((t,i)=> t!= array2 [i]).Any();` (18认同)
  • -1表示使用快速哈希函数.使用像PBKDF2,bcrypt或scrypt这样的慢速构造. (16认同)
  • @Brettski从技术上讲,是的,但是为每个用户提供_**唯一**_盐会使Rainbow Tables(通常被认为是破解哈希密码的最有效方法)实际上毫无用处.[这是一个快速的概述](http://crackstation.net/hashing-security.htm)给出了一个深入但不是压倒性的概述,如何安全地存储密码,以及它为什么/如何工作. (6认同)
  • 感谢您的建议 - 真的帮助我开始了.我也发现了这个链接<http://www.dijksterhuis.org/creating-salted-hash-values-in-c/>,我发现这是一个很好的实用建议,并反映了这篇文章中的内容. (3认同)
  • @hunter:你应该添加一个.ToList()来使它保持恒定时间.例如:return array1.Length == array2.Length &&!array1.Where((t,i)=> t!= array2 [i]).ToList().Any(); 否则,LINQ会在找到一个不相等的字节后立即返回. (3认同)
  • 使用恒定时间比较也被认为是一种好习惯,但密码哈希对此并不重要. (2认同)

Ada*_*ton 44

什么吹笛说,但代码少了一点.使用Linq或CopyTo连接数组.

public static byte[] Hash(string value, byte[] salt)
{
    return Hash(Encoding.UTF8.GetBytes(value), salt);
}

public static byte[] Hash(byte[] value, byte[] salt)
{
    byte[] saltedValue = value.Concat(salt).ToArray();
    // Alternatively use CopyTo.
    //var saltedValue = new byte[value.Length + salt.Length];
    //value.CopyTo(saltedValue, 0);
    //salt.CopyTo(saltedValue, value.Length);

    return new SHA256Managed().ComputeHash(saltedValue);
}
Run Code Online (Sandbox Code Playgroud)

Linq也有一种比较字节数组的简单方法.

public bool ConfirmPassword(string password)
{
    byte[] passwordHash = Hash(password, _passwordSalt);

    return _passwordHash.SequenceEqual(passwordHash);
}
Run Code Online (Sandbox Code Playgroud)

然而,在实施任何此操作之前,请查看此帖子.对于密码哈希,您可能需要慢速哈希算法,而不是快速哈希算法.

为此,有一个Rfc2898DeriveBytes类很慢(并且可以变得更慢),并且可以回答原始问题的第二部分,因为它可以获取密码和salt并返回哈希.有关更多信息,请参阅此问题.注意,Stack ExchangeRfc2898DeriveBytes用于密码散列(此处为源代码).

  • @MushinNoShin SHA256是一个快速哈希.密码散列需要慢速散列,如PBKDF2,bcrypt或scrypt.有关详细信息,请参阅security.se上的[如何安全地哈希密码?](http://security.stackexchange.com/questions/211/how-to-securely-hash-passwords). (6认同)

Mic*_*ael 32

我一直在阅读像SHA256这样的散列函数并不真正用于存储密码:https: //patrickmn.com/security/storing-passwords-securely/#notpasswordhashes

而是自适应密钥派生函数,如PBKDF2,bcrypt或scrypt.以下是Microsoft 在Microsoft.AspNet.Identity库中为PasswordHasher编写的基于PBKDF2的文件:

/* =======================
 * HASHED PASSWORD FORMATS
 * =======================
 * 
 * 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.)
 */

public string HashPassword(string password)
{
    var prf = KeyDerivationPrf.HMACSHA256;
    var rng = RandomNumberGenerator.Create();
    const int iterCount = 10000;
    const int saltSize = 128 / 8;
    const int numBytesRequested = 256 / 8;

    // Produce a version 3 (see comment above) text hash.
    var salt = new byte[saltSize];
    rng.GetBytes(salt);
    var 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, iterCount);
    WriteNetworkByteOrder(outputBytes, 9, saltSize);
    Buffer.BlockCopy(salt, 0, outputBytes, 13, salt.Length);
    Buffer.BlockCopy(subkey, 0, outputBytes, 13 + saltSize, subkey.Length);
    return Convert.ToBase64String(outputBytes);
}

public bool VerifyHashedPassword(string hashedPassword, string providedPassword)
{
    var decodedHashedPassword = Convert.FromBase64String(hashedPassword);

    // Wrong version
    if (decodedHashedPassword[0] != 0x01)
        return false;

    // Read header information
    var prf = (KeyDerivationPrf)ReadNetworkByteOrder(decodedHashedPassword, 1);
    var iterCount = (int)ReadNetworkByteOrder(decodedHashedPassword, 5);
    var saltLength = (int)ReadNetworkByteOrder(decodedHashedPassword, 9);

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

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

    // Hash the incoming password and verify it
    var actualSubkey = KeyDerivation.Pbkdf2(providedPassword, salt, prf, iterCount, subkeyLength);
    return actualSubkey.SequenceEqual(expectedSubkey);
}

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]));
}
Run Code Online (Sandbox Code Playgroud)

请注意,这需要安装Microsoft.AspNetCore.Cryptography.KeyDerivation nuget包,它需要.NET Standard 2.0(.NET 4.6.1或更高版本).对于早期版本的.NET,请参阅Microsoft的System.Web.Helpers库中的Crypto类.

更新2015年11月
更新了使用来自不同Microsoft库的实现的答案,该库使用PBKDF2-HMAC-SHA256哈希而不是PBKDF2-HMAC-SHA1(注意如果iterCount足够高,PBKDF2-HMAC-SHA1 仍然安全的).您可以查看复制简化代码的代码,因为它实际上处理了从上一个答案实现的验证和升级哈希值,如果您将来需要增加iterCount,则非常有用.

  • 1)将`PBKDF2SubkeyLength`减少到20个字节.这是自然尺寸f SHA1并且增加它超过了减慢防御者的速度而不会减慢攻击者的速度.2)我建议增加迭代次数.我推荐10k到100k,具体取决于您的性能预算.3)恒定的时间比较也不会造成伤害,但没有太大的实际影响. (2认同)

Seb*_*son 24

Salt用于为哈希添加额外的复杂性,使其更难以暴力破解.

来自Sitepoint上文章:

黑客仍然可以执行所谓的字典攻击.恶意方可以通过获取他们知道人们经常使用的100,000个密码(例如城市名称,运动队等),对它们进行散列,然后将字典中的每个条目与数据库中的每一行进行比较来进行字典攻击.表.如果黑客找到了匹配,宾果游戏!他们有你的密码.然而,要解决这个问题,我们只需要哈希哈希值.

为了对哈希进行加密,我们只需提供一个随机查找的文本字符串,将其与用户提供的密码连接起来,然后将随机生成的字符串和密码一起散列为一个值.然后,我们将哈希值和salt保存为Users表中的单独字段.

在这种情况下,黑客不仅需要猜测密码,他们还必须猜测盐.在明文中添加salt可以提高安全性:现在,如果黑客尝试字典攻击,他必须使用每个用户行的盐来哈希他的100,000个条目.尽管仍有可能,但黑客攻击成功的可能性会大幅减少.

在.NET中没有自动执行此操作的方法,因此您将使用上面的解决方案.


Ami*_*adi 20

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

\n

这里的大多数其他答案(包括接受的答案)都使用 SHA-256 哈希算法,即使您使用盐,该算法也不再适合存储用户密码。为此,您应该选择较慢的哈希函数,例如 Bcrypt、Argon2、Scrypt 或 PBKDF2;后者是 .NET 中唯一可用的。

\n

您可以在另一个问题中找到主要创建 PBKDF2 哈希的辅助方法等,但我在下面提供的方法比该问题甚至此处提供的方法(例如本问题)具有以下优点。

\n

优点:

\n
    \n
  • 使用 .NET 6 中引入的新静态Rfc2898DeriveBytes.Pbkdf2()方法,无需每次都实例化和处置对象。

    \n
  • \n
  • 使用.NET 6 \xe2\x80\x94 中引入的新RandomNumberGenerator类及其静态方法 \xe2\x80\x94 来生成盐。GetBytes原始问题中使用的类RNGCryptoServiceProvider和此处的许多答案已过时

    \n
  • \n
  • 使用CryptographicOperations.FixedTimeEquals方法(在 .NET Core 2.1 中引入)来比较方法中的关键字节Verify,而不是像已接受的答案那样手动进行比较 \xe2\x80\x94 。这除了消除大量嘈杂的样板之外,还消除了定时攻击

    \n
  • \n
  • 使用 SHA-256 而不是默认的 SHA-1 作为底层算法,只是为了安全起见,因为后者是一种更强大、更可靠的算法。

    \n
  • \n
  • 该方法返回的字符串Hash(以及扩展后该方法接收的字符串Verify)具有以下结构:

    \n

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

    \n

    这是该特定解决方案最重要的优点;这意味着我们基本上包含有关用于在最终字符串中创建哈希的配置的元数据。这有效地允许我们将来更改哈希器类中的设置(例如迭代次数、salt/key 大小等),而不会破坏以前使用旧设置创建的哈希值。这是我遇到的大多数其他解决方案(使用 PBKDF2)实际上忽略并没有考虑到的问题,尽管它很重要。相反,它们通常依赖当前配置值来验证哈希值,这意味着一旦您决定更改任何配置值,任何先前创建的哈希值将不再得到正确验证。

    \n
  • \n
\n

其他要点:

\n
    \n
  • 我在返回的哈希字符串中使用密钥和盐的十六进制表示形式。如果您愿意,也可以使用 base64,只需将每次出现的Convert.ToHexString和分别更改Convert.FromHexStringConvert.ToBase64Convert.FromBase64即可。其余逻辑保持完全相同。
  • \n
  • 通常推荐的盐大小为 64 位或更高。我已将其设置为 128 位。
  • \n
  • 密钥大小通常应与所选算法的自然输出大小相同 \xe2\x80\x94 请参阅此评论。在我们的例子中,正如我之前提到的,底层算法是 SHA-256,其输出大小为 256 位,这正是我们设置密钥大小的值。
  • \n
  • 如果您打算使用它来存储用户密码,通常建议使用至少10,000 次迭代或更多。我已将默认值设置为 50,000,您当然可以根据需要进行更改。
  • \n
\n

代码:

\n
public static class SecretHasher\n{\n    private const int _saltSize = 16; // 128 bits\n    private const int _keySize = 32; // 256 bits\n    private const int _iterations = 50000;\n    private static readonly HashAlgorithmName _algorithm = HashAlgorithmName.SHA256;\n\n    private const char segmentDelimiter = \':\';\n\n    public static string Hash(string input)\n    {\n        byte[] salt = RandomNumberGenerator.GetBytes(_saltSize);\n        byte[] hash = Rfc2898DeriveBytes.Pbkdf2(\n            input,\n            salt,\n            _iterations,\n            _algorithm,\n            _keySize\n        );\n        return string.Join(\n            segmentDelimiter,\n            Convert.ToHexString(hash),\n            Convert.ToHexString(salt),\n            _iterations,\n            _algorithm\n        );\n    }\n\n    public static bool Verify(string input, string hashString)\n    {\n        string[] segments = hashString.Split(segmentDelimiter);\n        byte[] hash = Convert.FromHexString(segments[0]);\n        byte[] salt = Convert.FromHexString(segments[1]);\n        int iterations = int.Parse(segments[2]);\n        HashAlgorithmName algorithm = new HashAlgorithmName(segments[3]);\n        byte[] inputHash = Rfc2898DeriveBytes.Pbkdf2(\n            input,\n            salt,\n            iterations,\n            algorithm,\n            hash.Length\n        );\n        return CryptographicOperations.FixedTimeEquals(inputHash, hash);\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

用法:

\n
// Hash:\nstring password = "...";\nstring hashed = SecretHasher.Hash(password);\n\n// Verify:\nstring enteredPassword = "...";\nbool isPasswordCorrect = SecretHasher.Verify(enteredPassword, hashed);\n
Run Code Online (Sandbox Code Playgroud)\n


Kai*_*ann 11

使用System.Web.Helpers.CryptoMicrosoft 的 NuGet 包。它会自动向哈希值添加盐。

您对密码进行哈希处理,如下所示:var hash = Crypto.HashPassword("foo");

您可以像这样验证密码:var verified = Crypto.VerifyHashedPassword(hash, "foo");


Bam*_*gbe 8

我创建了一个具有以下方法的类:

  1. 创造盐
  2. 哈希输入
  3. 验证输入

    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); 
          }
     }
    
    Run Code Online (Sandbox Code Playgroud)

    `


Ily*_*dik 7

我创建了一个库SimpleHashing.Net,使用 Microsoft 提供的基本类使哈希过程变得简单。普通 SHA 确实不足以安全地存储密码。

该库使用 Bcrypt 的哈希格式的想法,但由于没有官方的 MS 实现,我更喜欢使用框架中可用的内容(即 PBKDF2),但开箱即用有点太难了。

这是如何使用该库的快速示例:

ISimpleHash simpleHash = new SimpleHash();

// Creating a user hash, hashedPassword can be stored in a database
// hashedPassword contains the number of iterations and salt inside it similar to bcrypt format
string hashedPassword = simpleHash.Compute("Password123");

// Validating user's password by first loading it from database by username
string storedHash = _repository.GetUserPasswordHash(username);
isPasswordValid = simpleHash.Verify("Password123", storedHash);
Run Code Online (Sandbox Code Playgroud)


tha*_*ets 5

呸,这样更好!http://sourceforge.net/projects/pwdtknet/它更好,因为.....它执行密钥拉伸并使用HMACSHA512 :)