如何在c#中使用PBKDF2 HMAC SHA-256或SHA-512使用盐和迭代散列密码

本文关键字:SHA-512 迭代 密码 HMAC PBKDF2 SHA-256 | 更新日期: 2023-09-27 18:11:55

我想找到一个解决方案或方法,将允许我添加盐和控制迭代的数量。原生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/有完整的源代码可用。它似乎更健壮。

到目前为止,我无法从它们每个中获得相同的输出。

如何在c#中使用PBKDF2 HMAC SHA-256或SHA-512使用盐和迭代散列密码

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

我很惊讶没有更多的开发者已经在使用这个了。

我不太喜欢回答自己的问题,但既然评论已经退化为关于速度的讨论,而且还没有其他人回答,我也可以这样做。

感谢所有评论的人。

我的CryptSharp库可以使用任何任意HMAC执行PBKDF2。盐和迭代可以被控制。看看密码器。实用程序的名称空间。它与c# script实现和其他一些东西一起在那里。

这是由SecurityDriven提供的。. NET的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和hashhedpassword。注意,您可以将它们作为二进制文件持久化,也可以使用帮助程序将它们存储为字符串。注意,salt是随机创建的。

验证用户的登录:

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。

GitHub上的源代码

我在Google Code上的开源c#密码实用程序库目前做HMAC SHA1-160和HMAC SHA2-256,以及盐和迭代(PKDBF2)。密码和哈希生成的计时是内置在库中的,如附带的Windows Forms gui所示。

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

我的SHA2-256代码产生与这里相同的测试结果。

另一个实现-从我发现其他像RoadWarrior, Zer和thasiznets已经做到了。

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

除此之外,它根本不是基于微软的实现。这也需要一个免责声明——参见这个答案的底部。

它允许任意的伪随机函数,这意味着我们可以插入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类已经对

进行了测试
  • 随机输入的HMAC-SHA1产生与微软实现相同的结果
  • HMAC-SHA1使用RFC 6070测试向量。
  • HMAC-SHA256使用来自https://stackoverflow.com/a/5136918/1169696的测试向量
  • 使用PBKDF2-HMAC-SHA-512测试载体的HMAC-SHA512测试载体

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

一些初步的性能测试表明,在使用GetBytes(200)将"pass"/"saltSALT"转换为ASCII编码的字节时,它与。net实现的SHA-1在1000次运行中进行1000次迭代时的性能相当。有时比内置实现快一点,有时慢一点——我们说的是在我的旧电脑上84秒对83秒。所有这些都是在PBKDF2DeriveBytes的调试构建中完成的(因为大部分工作显然是在HMAC中完成的,所以我们需要更多的迭代或运行来测量实际的差异)。

免责声明:

我不是密码天才。如上所示,这还没有经过大量测试。我不能保证。但是,与其他答案和实现一起,它可能有助于理解方法论。

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