【问题标题】:How can I hash passwords with salt and iterations using PBKDF2 HMAC SHA-256 or SHA-512 in C#?如何在 C# 中使用 PBKDF2 HMAC SHA-256 或 SHA-512 使用盐和迭代对密码进行哈希处理?
【发布时间】:2013-04-26 20:09:15
【问题描述】:

我想找到一种解决方案或方法,让我可以添加盐并控制迭代次数。本机 Rfc2898DeriveBytes 基于 HMACSHA1。理想情况下,使用 SHA-256 或 SHA-512 将使系统面向未来。

这是迄今为止我找到的最好的例子:http://jmedved.com/2012/04/pbkdf2-with-sha-256-and-others/,但是当我使用 SHA-256 运行它时,它实际上比使用 SHA-512 慢。我使用了 64k 次迭代、盐指南和不同长度的密码进行比较。

我还找到了这个解决方案:http://sourceforge.net/projects/pwdtknet/,它提供了完整的源代码。它似乎更健壮。

到目前为止,我无法从每个人那里获得相同的输出。

【问题讨论】:

  • SHA 对于密码哈希来说太快了。
  • .Net 框架在Rfc2898DeriveBytes class 中包含一个 PBKDF2 实现。使用它。
  • 该问题不包括 PDKDF2 HMAC、Scrypt 或 SHA3。不知道为什么你标记为重复。
  • @Developr:PBKDFv2、bcrypt 或 scrypt。 daemonology.net/blog/…
  • 我认为人们错过了关于 SHA 速度快的观点。SHA 速度很快,这就是为什么您使用 PBKDF2 等关键拉伸算法进行哈希处理的原因。随着 GPU 速度的提高,您可以加快迭代并将密码重新哈希为用户登录。 OP 并没有问这是否是一个好的解决方案(它是!!!)只是一个 .net 实现,它使用比 SHA1 更好的东西来实现伪随机函数。

标签: c# asp.net encryption


【解决方案1】:

最近的替代方案是Microsoft.AspNetCore.Cryptography.KeyDerivation NuGet 包,它允许将 PBKDF2 与 SHA-256 和 SHA-512 哈希函数一起使用,这比 Rfc2898DeriveBytes 中内置的 SHA-1 更强。与其他答案中提到的第三方库相比,它的优势在于它是由 Microsoft 实现的,因此一旦您已经依赖 .NET 平台,就无需对其执行安全审核。可通过docs.microsoft.com 获取文档。

【讨论】:

    【解决方案2】:

    这是由 SecurityDriven.NET 的 Inferno 库提供的。

    安装包 Inferno

    Inferno 推广 SHA-384,因为 NSA Suite B 使用它来保护绝密信息,并且“其截断设计可有效防御长度扩展攻击”(1)

    using SecurityDriven.Inferno;
    using SecurityDriven.Inferno.Extensions;
    using static SecurityDriven.Inferno.SuiteB;
    using static SecurityDriven.Inferno.Utils;
    using PBKDF2 = SecurityDriven.Inferno.Kdf.PBKDF2;
    

    存储用户密码:

    var sha384Factory = HmacFactory;
    var random = new CryptoRandom();
    
    byte[] derivedKey
    string hashedPassword = null;
    string passwordText = "foo";
    
    byte[] passwordBytes = SafeUTF8.GetBytes(passwordText);
    var salt = random.NextBytes(384/8);
    
    using (var pbkdf2 = new PBKDF2(sha384Factory, passwordBytes, salt, 256*1000))
        derivedKey=  pbkdf2.GetBytes(384/8);
    
    
    using (var hmac = sha384Factory()) 
    {
        hmac.Key = derivedKey;
        hashedPassword = hmac.ComputeHash(passwordBytes).ToBase16();
    }
    

    同时保留 salt 和 hashedPassword。请注意,您可以将它们保存为二进制文件,也可以使用帮助程序将它们存储为字符串。请注意,盐是随机创建的。

    验证用户的登录:

    var user = GetUserByUserName("bob")
    
    var sha384Factory = HmacFactory;
    
    byte[] derivedKey
    string hashedPassword = null;
    string suppliedPassword = "foo";
    
    byte[] passwordBytes = SafeUTF8.GetBytes(suppliedPassword);
    
    using (var pbkdf2 = new PBKDF2(sha384Factory, passwordBytes, user.UserSalt, 256*1000))
        derivedKey=  pbkdf2.GetBytes(384/8);
    
    
    using (var hmac = sha384Factory()) 
    {
        hmac.Key = derivedKey;
        hashedPassword = hmac.ComputeHash(passwordBytes).ToBase16();
    }
    
    isAuthenticated = hashedPassword == user.UserHashedPassword; //true for bob
    

    正如您在此处看到的,该过程几乎相同。主要区别在于没有使用CryptoRandom,我们在创建PBKDF2 实例时使用持久化的UserSalt。

    Source on GitHub

    【讨论】:

      【解决方案3】:

      另一种实现 - 在我发现 RoadWarrior、Zer 和 thasiznets 等其他公司之前已经完成了。

      这就像Rfc2898DeriveBytes 一样源自.NET 的System.Cryptography.DeriveBytes。换句话说,用法是一样的——尽管我只实现了我使用的一个构造函数。

      除了那个沿袭,它根本不是基于 Microsoft 的实现。这也需要免责声明 - 请参阅此答案的底部。

      它允许任意伪随机函数,这意味着我们可以插入 HMAC SHA256 或 HMAC SHA512 - 或者比我更有密码洞察力和勇气的人可以插入他们想要的任何东西 - 就像 RFC 允许的那样。它还使用 long 而不是 int 来计算迭代次数 - 仅用于疯狂的迭代次数。

      /// <summary>
      /// More generic version of the built-in Rfc2898DeriveBytes class. This one
      /// allows an arbitrary Pseudo Random Function, meaning we can use e.g. 
      /// HMAC SHA256 or HMAC SHA512 rather than the hardcoded HMAC SHA-1 of the 
      /// built-in version.
      /// </summary>
      public class PBKDF2DeriveBytes : DeriveBytes
      {
          // Initialization:
      
          private readonly IPseudoRandomFunction prf;
          private readonly byte[] salt;
          private readonly long iterationCount;
      
          private readonly byte[] saltAndBlockNumber;
      
          // State:
      
          // Last result of prf.Transform - also used as buffer
          // between GetBytes() calls:
          private byte[] buffer;
      
          private int bufferIndex;
          private int nextBlock;
      
          /// <param name="prf">
          ///    The Pseudo Random Function to use for calculating the derived key
          /// </param>
          /// <param name="salt">
          ///    The initial salt to use in calculating the derived key
          /// </param>
          /// <param name="iterationCount">
          ///    Number of iterations. RFC 2898 recommends a minimum of 1000
          ///    iterations (in the year 2000) ideally with number of iterations
          ///    adjusted on a regular basis (e.g. each year).
          /// </param>
          public PBKDF2DeriveBytes(
             IPseudoRandomFunction prf, byte[] salt, long iterationCount)
          {
              if (prf == null)
              {
                  throw new ArgumentNullException("prf");
              }
      
              if (salt == null)
              {
                  throw new ArgumentNullException("salt");
              }
      
              this.prf = prf;
              this.salt = salt;
              this.iterationCount = iterationCount;
      
              // Prepare combined salt = concat(original salt, block number)
              saltAndBlockNumber = new byte[salt.Length + 4];
              Buffer.BlockCopy(salt, 0, saltAndBlockNumber, 0, salt.Length);
      
              Reset();
          }
      
          /// <summary>
          ///    Retrieves a derived key of the length specified.
          ///    Successive calls to GetBytes will return different results -
          ///    calling GetBytes(20) twice is equivalent to calling
          ///    GetBytes(40) once. Use Reset method to clear state.
          /// </summary>
          /// <param name="keyLength">
          ///    The number of bytes required. Note that for password hashing, a
          ///    key length greater than the output length of the underlying Pseudo
          ///    Random Function is redundant and does not increase security.
          /// </param>
          /// <returns>The derived key</returns>
          public override byte[] GetBytes(int keyLength)
          {
              var result = new byte[keyLength];
      
              int resultIndex = 0;
      
              // If we have bytes in buffer from previous run, use those first:
              if (buffer != null && bufferIndex > 0)
              {
                  int bufferRemaining = prf.HashSize - bufferIndex;
      
                  // Take at most keyLength bytes from the buffer:
                  int bytesFromBuffer = Math.Min(bufferRemaining, keyLength);
      
                  if (bytesFromBuffer > 0)
                  {
                      Buffer.BlockCopy(buffer, bufferIndex, result, 0,
                         bytesFromBuffer);
                      bufferIndex += bytesFromBuffer;
                      resultIndex += bytesFromBuffer;
                  }
              }
      
              // If, after filling from buffer, we need more bytes to fill
              // the result, they need to be computed:
              if (resultIndex < keyLength)
              {
                  ComputeBlocks(result, resultIndex);
      
                  // If we used the entire buffer, reset index:
                  if (bufferIndex == prf.HashSize)
                  {
                      bufferIndex = 0;
                  }
              }
      
              return result;
          }
      
          /// <summary>
          ///    Resets state. The next call to GetBytes will return the same
          ///    result as an initial call to GetBytes.
          ///    Sealed since it's called from constructor.
          /// </summary>
          public sealed override void Reset()
          {
              buffer = null;
              bufferIndex = 0;
              nextBlock = 1;
          }
      
          private void ComputeBlocks(byte[] result, int resultIndex)
          {
              int currentBlock = nextBlock;
      
              // Keep computing blocks until we've filled the result array:
              while (resultIndex < result.Length)
              {
                  // Run iterations for block:
                  F(currentBlock);
      
                  // Populate result array with the block, but only as many bytes
                  // as are needed - keep the rest in buffer:
                  int bytesFromBuffer = Math.Min(
                         prf.HashSize,
                         result.Length - resultIndex
                  );
                  Buffer.BlockCopy(buffer, 0, result, resultIndex, bytesFromBuffer);
      
                  bufferIndex = bytesFromBuffer;
                  resultIndex += bytesFromBuffer;
                  currentBlock++;
              }
              nextBlock = currentBlock;
          }
      
          private void F(int currentBlock)
          {
              // First iteration:
              // Populate initial salt with the current block index:
              Buffer.BlockCopy(
                 BlockNumberToBytes(currentBlock), 0, 
                 saltAndBlockNumber, salt.Length, 4
              );
      
              buffer = prf.Transform(saltAndBlockNumber);
      
              // Remaining iterations:
              byte[] result = buffer;
              for (long iteration = 2; iteration <= iterationCount; iteration++)
              {
                  // Note that the PRF transform takes the immediate result of the
                  // last iteration, not the combined result (in buffer):
                  result = prf.Transform(result);
      
                  for (int byteIndex = 0; byteIndex < buffer.Length; byteIndex++)
                  {
                      buffer[byteIndex] ^= result[byteIndex];
                  }
              }
          }
      
          private static byte[] BlockNumberToBytes(int blockNumber)
          {
              byte[] result = BitConverter.GetBytes(blockNumber);
      
              // Make sure the result is big endian:
              if (BitConverter.IsLittleEndian)
              {
                  Array.Reverse(result);
              }
      
              return result;
          }
      }
      

      IPseudoRandomFunction 声明为:

      public interface IPseudoRandomFunction : IDisposable
      {
          int HashSize { get; }
          byte[] Transform(byte[] input);
      }
      

      HMAC-SHA512 IPseudoRandomFunction 示例(为简洁起见 - 我使用允许任何 .NET 的 HMAC 类的泛型类):

      public class HMACSHA512PseudoRandomFunction : IPseudoRandomFunction
      {
          private HMAC hmac;
          private bool disposed;
      
          public HmacPseudoRandomFunction(byte[] input)
          {
              hmac = new HMACSHA512(input);
          }
      
          public int HashSize
          {
              // Might as well return a constant 64
              get { return hmac.HashSize / 8; }
          }
      
          public byte[] Transform(byte[] input)
          {
              return hmac.ComputeHash(input);
          }
      
          public void Dispose()
          {
              if (!disposed)
              {
                  hmac.Dispose();
                  hmac = null;
                  disposed = true;
              }
          }
      }
      

      结果...这个:

      using (var prf = new HMACSHA512PseudoRandomFunction(input))
      {
          using (var hash = new PBKDF2DeriveBytes(prf, salt, 1000))
          {
              hash.GetBytes(32);
          }
      }
      

      ... 是 HMAC-SHA512 的等效项:

      using (var hash = new Rfc2898DeriveBytes(input, salt, 1000))
      {
          hash.GetBytes(32);
      }
      

      测试

      PBKDF2DeriveBytes 类已经过测试

      它还通过Reset() 的简单测试和对GetBytes() 的多次调用运行。

      一些初步的性能测试表明,它与 SHA-1 的 .NET 实现相当,在“pass”/“saltSALT”上运行 1000 次,迭代 1000 次,转换为 ASCII 编码的字节,GetBytes(200)。有时比内置实现快一点,有时慢一点——我们在我的古老计算机上谈论的是 84 秒对 83 秒。不过,所有这些都是通过 PBKDF2DeriveBytes 的调试版本完成的(由于大部分工作显然是在 HMAC 中完成的,我们需要更多的迭代或运行来衡量实际差异)。

      免责声明

      我不是密码学天才。如上所述,这还没有经过大量测试。我不做任何保证。但也许,连同其他答案和实现,它可以帮助理解该方法。

      【讨论】:

        【解决方案4】:

        我的CryptSharp 库可以使用任意 HMAC 执行 PBKDF2。可以控制盐和迭代。查看 CryptSharp.Utility 命名空间。它与 C# Scrypt 实现和其他一些东西一起存在。

        【讨论】:

        • 老兄,我看了这个并利用了你的演示:string cryptedPassword = Crypter.Blowfish.Crypt(password);或字符串 cryptedPassword = Crypter.Blowfish.Crypt(password, Crypter.Blowfish.GenerateSalt(6));无论如何,您都不是在提倡储存盐……需要保留盐!所以我建议你添加 String salt = Crypter.Blowfish.GenerateSalt(64);
        • 什么? Crypt 密码格式将算法参数嵌入到 salt 字符串中。首先,Blowfish crypt 的 rounds 参数是 log2。其次,它与盐的长度无关。如果您不指定盐,Crypt 会使用合理的默认参数自动调用它。最后,对于 Blowfish crypt,有效值为 4 到 31,对应 2^4 到 2^31 轮。
        • 我应该补充一点,Crypt 格式将盐存储在最终的密码字符串中。您不必单独存储盐 - 这就是使它对用户友好的原因。 :) 格式是 [算法] [盐] [密码]。 GenerateSalt 为您提供[算法][salt],Crypt 填充最后一位,然后您将其存储在数据库中。
        • 所以如果 GenerateSalt 给你 [algorithm][salt] 那么你需要存储 [algorithm][salt] 否则你如何进行比较?
        • 不,您已经存储了 [algorithm][salt][password](“散列密码”),因为那是 Crypt 的输出。包含[算法] [盐]。这比你想象的要容易得多。有关详细信息,请参阅en.wikipedia.org/wiki/Crypt_(C)
        【解决方案5】:

        我在 Google Code 上的开源 C# password utilities library 目前使用 HMAC SHA1-160 和 HMAC SHA2-256,以及盐和迭代 (PKDBF2)。如随附的 Windows 窗体 gui 所示,库中内置了密码和哈希生成时间。

        我的代码目前在我的机器上需要 0.80 秒来执行 SHA2-256 哈希和 65,536 次迭代。它肯定会更有效,因为我还没有对其进行分析。

        我的 SHA2-256 代码产生与 here 所示相同的测试结果。

        【讨论】:

          【解决方案6】:

          PWDTK.NET 库 (http://sourceforge.net/projects/pwdtknet/) 似乎是我能找到的唯一实现 PBKDF2 HMAC SHA-512 并允许加盐和迭代的实现。我无法找到 PBKDF2 HMAC SHA-512 的测试向量来进行测试。

          我很惊讶没有更多的开发者使用它。

          不是很喜欢回答我自己的问题,但由于 cmets 退化为关于速度的讨论并且还没有其他人回答,我不妨。

          感谢所有评论的人。

          【讨论】:

          • 您好开发者,我是 HDizzle PWDTK.NET 的作者。关于测试向量的稀缺性,您是正确的,因此我对框架进行了编码,以使功能、属性等与此处的规范中的内容相匹配:ietf.org/rfc/rfc2898.txt,因此欢迎那些有一点数学知识的人将代码映射到数学。我意识到测试向量本来是理想的解决方案,但我不得不尽我所能。我还没有有人在 PWDTK 中发现错误,而且源代码已被许多人下载,所以这是一个好兆头!希望你喜欢 PWDTK!
          • 你可以通过 NuGet 安装 PWDTK.NET 包;-)
          • PBKDF2-HMAC-SHA-512 测试向量可在 StackOverflow 问题 PBKDF2-HMAC-SHA-512 test vectors 在您提出这个问题几周后发布!
          猜你喜欢
          • 1970-01-01
          • 2016-01-10
          • 2014-05-13
          • 2013-01-09
          • 2015-10-23
          • 2014-11-11
          • 2013-03-13
          • 2020-02-02
          • 2016-08-01
          相关资源
          最近更新 更多