如何在C#中使用PBKDF2 HMAC SHA-256或SHA-512进行密码哈希及加盐迭代处理?

11

我希望找到一种解决方案或方法,可以允许我添加盐并控制迭代次数。原生的Rfc2898DeriveBytes基于HMACSHA1算法。理想情况下,使用SHA-256或SHA-512将使系统具备未来兼容性。

我目前发现的最佳示例是:http://jmedved.com/2012/04/pbkdf2-with-sha-256-and-others/, 但当我使用SHA-256运行它时,速度实际上比使用SHA-512要慢。我使用了64k次迭代、一个GUID作为盐以及相同长度的不同密码进行比较。

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

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


2
SHA对于密码哈希来说太快了。 - SLaks
4
.Net框架中包括了一个PBKDF2实现,可在Rfc2898DeriveBytes中使用。请使用该实现。 - SLaks
1
这个问题没有包括PDKDF2 HMAC、Scrypt或SHA3。不确定为什么你标记为重复。 - jpshook
1
@SLaks 这个问题只在 PBKDF2 的上下文中提到了 SHA,所以没问题。称 3DES 不安全也是夸大其词。正确使用的 3DES 是无法被破解的。@.Developr SHA1 中的碰撞弱点不适用于密码哈希。因此,PBKDF2-HMAC-SHA1 没有实际问题。最大的问题是 .net 的实现性能较差,迫使您使用比您本来可以使用的更低的迭代次数。 - CodesInChaos
3
我认为人们在快速的SHA方面错失了重点。SHA是快速的,这就是为什么你要使用像PBKDF2这样的密钥拉伸算法进行哈希处理。随着GPU速度的提高,你可以增加迭代次数并在用户登录时重新哈希密码。OP并没有询问这是否是一个好的解决方案(它是好的!)只是需要一个使用比SHA1更好的伪随机函数的.NET实现。 - Oli
显示剩余16条评论
6个回答

4

PWDTK.NET库(http://sourceforge.net/projects/pwdtknet/)似乎是我能找到的唯一实现PBKDF2 HMAC SHA-512并允许盐和迭代的库。我还没有找到用于测试的PBKDF2 HMAC SHA-512测试向量。

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

虽然不太喜欢自己回答问题,但既然评论已经降为了关于速度的讨论,而且还没有人回答,那我就来说一下吧。

感谢所有留言的人。


3
你好,开发者。我是 PWDTK.NET 的作者 HDizzle。你提到测试向量的稀缺可用性是正确的,因此我编写了框架,使得函数、属性等与规范(http://www.ietf.org/rfc/rfc2898.txt)中的相匹配。有一些数学知识的人可以将代码映射到数学公式。我意识到测试向量可能是理想的解决方案,但我只能利用手头的资源。到目前为止,还没有人找到 PWDTK 的错误,源代码也被许多人下载,这是一个好的迹象!希望你喜欢 PWDTK! - thashiznets
2
您可以通过NuGet安装PWDTK.NET软件包;-) - juFo
PBKDF2-HMAC-SHA-512的测试向量可在StackOverflow问题PBKDF2-HMAC-SHA-512 test vectors中获取,该问题是在您提出此问题仅几周后发布的! - Anti-weakpasswords

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();
}

持久化盐值和哈希密码。请注意,您可以将它们持久化为二进制,也可以使用辅助函数将它们存储为字符串。请注意,盐值是随机生成的。

验证用户的登录:

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 上的源代码

2

我的CryptSharp库可以使用任意的HMAC进行PBKDF2。可以控制盐和迭代次数。请查看CryptSharp.Utility命名空间,其中还包括C# Scrypt实现和其他一些内容。


老兄,我看了一下你的演示:string cryptedPassword = Crypter.Blowfish.Crypt(password); 或者 string cryptedPassword = Crypter.Blowfish.Crypt(password, Crypter.Blowfish.GenerateSalt(6));你并没有以任何方式推广存储盐.....需要保留盐!所以我建议你添加String salt = Crypter.Blowfish.GenerateSalt(64); <- 更大的盐然后使用Crypter.Blowfish.Crypt(password, salt); 使其对用户更加友好 ;) - thashiznets
什么?Crypt密码格式将算法参数嵌入到盐字符串中。首先,Blowfish加密的轮数参数是log2。其次,它与盐的长度无关。如果您没有指定盐,则Crypt会自动使用合理的默认参数进行调用。最后,对于Blowfish加密,有效值为4到31,对应于2^4到2^31轮。 - James
我应该补充一下,Crypt格式将盐存储在最终密码字符串中。您不必单独存储盐 - 这就是它用户友好的原因。 :) 格式为[算法][盐][密码]。GenerateSalt会给您[算法][盐],Crypt会填写最后一部分,然后您将其存储在数据库中。 - James
如果GenerateSalt返回[algorithm][salt],那么你需要存储[algorithm][salt],否则你怎么进行比较呢? - thashiznets
不,你已经存储了[算法][盐][密码](即“哈希密码”),因为这是Crypt的输出。它包含[算法][盐]。这比你想象的要容易得多。有关更多详细信息,请参见http://en.wikipedia.org/wiki/Crypt_(C)。 - James
显示剩余2条评论

1

另一种实现方式 - 在我发现像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()进行了简单测试。

几项初步性能测试表明,在将"pass"/"saltSALT"转换为ASCII编码的字节并使用GetBytes(200)进行1000次迭代的情况下,它与.NET实现的SHA-1大致相同。有时比内置实现快一点,有时慢一点 - 在我的古老电脑上大约是84秒对83秒。然而,所有这些都是在调试构建的PBKDF2DeriveBytes中完成的(由于大部分工作显然是在HMAC中完成的,我们需要更多的迭代或运行来测量实际差异)。 免责声明:

我不是密码学天才。如上所示,这还没有经过严格测试。我不做任何保证。但也许,除了其他答案和实现之外,它可以帮助理解方法论。


1

我的开源C# 密码工具库Google Code上目前支持HMAC SHA1-160和HMAC SHA2-256,以及盐值和迭代(PKDBF2)。密码和哈希生成的时间已经内置在该库中,并通过附带的Windows Forms GUI进行演示。

我的代码在我自己的机器上使用65,536次迭代进行SHA2-256哈希所需的时间为0.80秒。由于我还没有对其进行性能测试,因此它肯定可以更加高效。

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


1

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接