在C#中如何哈希密码?Bcrypt/PBKDF2

43

我查阅了msdn和其他关于如何实现此功能的资源,但是没有找到明确的解决方案。这是我找到的最好的资料: http://blogs.msdn.com/b/shawnfa/archive/2004/04/14/generating-a-key-from-a-password.aspx?Redirected=true

我想使用bcrypt或PBKDF2(似乎与bcrypt相关)在C#中对密码进行哈希处理。我想试验一下计算机需要多少轮才能对密码进行哈希处理。然而,所有的东西似乎都是关于加密的,而且大家都在谈论哈希处理。我搞不清楚怎么做。我该如何对密码进行哈希处理?看起来PBKDF2(Rfc2898?)是一个随机数生成器,我可以使用GetBytes(amount)来选择我的哈希大小。

我很困惑。我应该如何使用bcrypt/PBKDF对密码进行哈希处理?


那篇文章讨论的是从密码生成密钥以用于加密某些内容。这与哈希密码是非常不同的话题。 - hatchet - done with SOverflow
1
查看https://dev59.com/n3RB5IYBdhLWcg3w4bAo获取一些资源。如果你在谷歌上搜索“hash passwords salt .net”,你会得到很多适当的结果。 - hatchet - done with SOverflow
5
他为什么要这样做呢?这样他就需要自己实现减速和加盐方案,而不是使用内置的PBKDF2实现。 - CodesInChaos
1
@hatchet:是的,但它们都没有解释如何在C#中实现。他们只是说使用它....而我正在尝试以正确的方式做到这一点。 - user34537
我也写了一点关于如何处理这个问题的内容,可以在这里查看:http://davismj.me/blog/bcrypt/ - Matthew James Davis
显示剩余5条评论
9个回答

39

PBKDF2

您实际上很接近了。您提供的链接显示了如何调用Rfc2898DeriveBytes函数来获取PBKDF2哈希结果。但是,您被示例使用派生密钥进行加密的事实所迷惑了(PBKDF1和2的原始动机是创建适合用作加密密钥的“密钥”导出函数)。当然,我们不想将输出用于加密,而是要作为哈希值本身使用。

如果您想使用PBKDF2,可以尝试为此目的编写的SimpleCrypto.Net库。如果您查看实现,就会发现这实际上只是一个细包装器(您猜对了),它围绕着Rfc2898DeriveBytes

BCrypt

如果您想尝试这个变体,可以尝试名为(还有啥)BCrypt.NET的C#实现。

免责声明:我没有使用或测试我链接到的任何库... YMMV


看起来他们的 calculateHash 只是 .GetBytes(64)。所以,我要选择我的哈希位大小并使用 GetBytes 吗? - user34537
由于我不熟悉PBKDF2算法的细节,所以我不想猜测。然而,正如你所说,似乎你可以只选择一个足够安全的哈希长度,并从该方法中获取相应数量的字节。 - paracycle
有些人似乎仍然推荐使用SHA1和SHA256而不是PBKDF2。那么,PBKDF2更好吗? - James
@James:其实它们并不完全相同。PBKDF2是一种基于密码的密钥派生函数,它接受一个密码并从中派生出一个密钥。这个密钥旨在用于加密操作。另一方面,SHA用于哈希输入字节。虽然您可以将PBKDF2用于类似的目的,但我认为使用生成的密钥作为哈希值不是很好的选择。 - paracycle
2
SimpleCrypto.NET似乎由于其实现方式存在一些安全问题。在将这些类型的项目集成到自己的项目中之前,一定要查看GitHub Issues。 - NathanAldenSr
1
我创建了一个简单的.NET库,它使用Microsoft实现的PBKDF2(Rfc2898DerivedBytes),但在其之上提供了一个极其简单的接口,类似于BCrypt.NET,因此通常只需要调用Compute(),将结果保存到数据库中,稍后通过调用Verify进行验证。它被称为SimpleHashing.Net:https://github.com/ilya-git/SimpleHashing.Net,也有一个同名的Nuget包。再次强调,这不是自己实现的,而是在MS实现之上提供了一个薄的包装,以便于使用。 - Ilya Chernomordik

19
首先,我敦促每个人使用平台自带的经过密码学验证的参考算法。
不要使用第三方软件包和未经验证的OSS组件或从互联网上复制粘贴的任何其他代码。
对于.NET,请使用PBKDF2而不是bCrypt,因为.NET没有bCrypt的认证实现。
我并不是不尊重任何高尚的开源开发者(我也是其中之一),但您永远无法确定他们的网站在10年内不会被黑客攻击,从Nuget / npm或其他包管理器获取恶意软件包。
有关验证的更多信息,请参见此SO答案。
现在,回到PBKDF2,这里是简单的代码:
public static byte[] PBKDF2Hash(string input, byte[] salt)
{
    // Generate the hash
    Rfc2898DeriveBytes pbkdf2 = new Rfc2898DeriveBytes(input, salt, iterations: 5000);
    return pbkdf2.GetBytes(20); //20 bytes length is 160 bits
}
如果你需要哈希值的字符串表示(而不是字节数组) - 你可以使用这个回答中的超快转换类。 https://dev59.com/Y3VC5IYBdhLWcg3wYQEp#624379

如果需要哈希值的字符串表示(而不是字节数组),您可以使用此回答中的超快速转换类。

https://dev59.com/Y3VC5IYBdhLWcg3wYQEp#624379


你可以使用 base-64 转换将字节数组转换为字符串,例如 Convert.ToBase64String(bytes),它生成的字符串比十六进制数字字符串更短。 - Maximvs

11

这里有一个方便的代码片段,可以帮助你使用哈希密码,但是在使用之前,你需要阅读文档理论1 理论2等相关内容,否则可能会存在安全漏洞。安全是一个非常重要的话题,请谨慎对待!

将NuGet包BCrypt.Net添加到解决方案中。

我花了很长时间(数天之久)才找到实际编码的方法,以使哈希密码正常工作!因此,我在这里提供了这个方便的代码片段。

const int WorkFactor = 14;
var HashedPassword = BCrypt.Net.BCrypt.HashPassword(Password, WorkFactor); 

你应该根据需要调整WorkFactor,请查看讨论。这是一个log2函数
"数字是log2,因此每当计算机速度加倍时,默认数字加1。"
然后,将哈希密码存储在您的数据库中,如passwordFromLocalDB,并测试传入的password,如下所示:
if (BCrypt.Net.BCrypt.Verify(password, passwordFromLocalDB) == true)

祝你好运!


1
你应该添加盐!非常重要! - Luca Ziegler

8

编辑:正如@MikeT指出的那样,我发布的原始链接和代码已不再被认为是将密码哈希化以存储在数据存储区中的最佳实践。

这篇文章由Scott Brady撰写,介绍了如何使用PasswordHasher,以及如何使用接口开发更强大的哈希实现。

因此,自己应用字节级哈希函数不再是必需的,但如果你想看看它是如何完成的,本文探讨了PasswordHasher<T>的实现:

探索ASP.NET Core Identity PasswordHasher

该文章中的相关代码片段如下所示:

private static byte[] HashPasswordV2(string password, RandomNumberGenerator rng)
{
    const KeyDerivationPrf Pbkdf2Prf = KeyDerivationPrf.HMACSHA1; // default for Rfc2898DeriveBytes
    const int Pbkdf2IterCount = 1000; // default for Rfc2898DeriveBytes
    const int Pbkdf2SubkeyLength = 256 / 8; // 256 bits
    const int SaltSize = 128 / 8; // 128 bits

    // Produce a version 2 text hash.
    byte[] salt = new byte[SaltSize];
    rng.GetBytes(salt);
    byte[] subkey = KeyDerivation.Pbkdf2(password, salt, Pbkdf2Prf, Pbkdf2IterCount, Pbkdf2SubkeyLength);

    var outputBytes = new byte[1 + SaltSize + Pbkdf2SubkeyLength];
    outputBytes[0] = 0x00; // format marker
    Buffer.BlockCopy(salt, 0, outputBytes, 1, SaltSize);
    Buffer.BlockCopy(subkey, 0, outputBytes, 1 + SaltSize, Pbkdf2SubkeyLength);
    return outputBytes;
}

原始回答:

微软为使用 .Net Core 的任何人提供了一个使用 PBKDF2 的示例代码页面:

在ASP.NET Core中哈希密码

从文章中可以看到:

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=
 */

您所引用的页面/代码存在警告:以下代码展示了如何使用KeyDerivation.Pbkdf2生成共享密钥。它不应该用于将密码哈希存储在数据存储中。 - MikeT
谢谢您指出这一点。这是我回答时不存在的一个相对较新的变化。我会更新它的。 - pcdev

3

今年早些时候,我在为我们的ASP.NET Web Forms项目创建哈希值时研究了同样的事情,我想按照MVC项目开箱即用的方式进行操作。

我偶然发现了这个问题 => ASP.NET Identity默认密码哈希程序是如何工作的,它是否安全? 然后我在这里找到了带有ByteArraysEqual方法的源代码 => http://www.symbolsource.org/MyGet/Metadata/aspnetwebstacknightly/Project/Microsoft.AspNet.Identity.Core/2.0.0-rtm-140327/Release/Default/Microsoft.AspNet.Identity.Core/Microsoft.AspNet.Identity.Core/Crypto.cs?ImageName=Microsoft.AspNet.Identity.Core


2
对于其他后来者,上面链接中的代码是有用的,因为它是原始代码而不是解构实现。(不确定编辑/重新发布SO代码或其他代码是否有用或符合政策,所以...)这3个函数是:HashPasswordVerifyHashedPassword链接1,然后是ByteArraysEqual链接2备用 - zanlok
非常有帮助。另一个链接是mono源代码(应该是相同的和稳定的链接):https://github.com/mono/aspnetwebstack/blob/master/src/System.Web.Helpers/Crypto.cs - Monkey Code

2
我对不涉及任何库的答案感兴趣。
我阅读了这篇文章,其中链接了不同语言的实现,包括C#,我也会在这里提供链接。https://crackstation.net/hashing-security.htm

https://github.com/defuse/password-hashing/blob/master/PasswordStorage.cs

有趣的是,正如在这里提到的几次一样,它使用了Rfc2898DeriveBytes。
private static byte[] PBKDF2(string password, byte[] salt, int iterations, int outputBytes){
    using (var pbkdf2 = new Rfc2898DeriveBytes(password, salt)) {
        pbkdf2.IterationCount = iterations;
        return pbkdf2.GetBytes(outputBytes);
    }
}

1
这对我来说实际上是最好的答案,没有额外的库,它在System.Security.Cryptography中,甚至哈希算法是可选择的。非常好,第一次尝试就成功了!(顺便说一句,当问题被提出时,大概这个类还没有出现在.net中) - beatcoder
@beatcoder Rfc2898DeriveBytes自.NET Framework 2.0以来就存在了,该版本于2005年10月发布 - https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.rfc2898derivebytes?redirectedfrom=MSDN&view=netframework-2.0 - Dave Black

1

1
我有这个,但它没有显示如何哈希密码,只显示了如何加密。 - user34537
1
抱歉,你是对的,这主要是关于加密的,但我希望你能利用这个类来获取你需要的内容。Paracycle做得很好,找到了它的包装器! - Jay S

1

PBKDF2使用HMACSHA1,如果您想要一种更现代和可定制的解决方案,您应该查看此API,它使用HMACSHA256或512进行密钥拉伸,就像PBKDF2一样。

https://sourceforge.net/projects/pwdtknet/

源代码中包含的示例GUI演示了如何从密码获取哈希值,包括创建加密随机盐......尽情享受 :)


1

PBKDF2

http://msdn.microsoft.com/en-us/library/system.security.cryptography.rfc2898derivebytes.aspx的示例中,当你到达"Rfc2898DeriveBytes k1 = new Rfc2898DeriveBytes(pwd1, salt1, myIterations);"这行代码时,k1就是哈希值。该示例是为了加密而设计的,因为Rfc2898DeriveBytes最初是用于创建加密密钥的。

如果您不提供盐值,Rfc2898DeriveBytes将自动生成一个盐值,但我不知道RNGCryptoServiceProvider是否更好地实现了密码学随机性。

根据OWASP(https://www.owasp.org/index.php/Using_Rfc2898DeriveBytes_for_PBKDF2)的说法,Rfc2898DeriveBytes使用SHA1,因此只适用于长度不超过160位的哈希。如果您创建了一个更长的哈希,攻击者仍然只需要担心前160位,但是您使密码哈希/身份验证变得更加昂贵,却没有任何收益。
以下是用于Rfc2898DeriveBytes密码哈希的示例代码(在数据库中存储哈希、盐和迭代次数):
public class Rfc2898PasswordEncoder
{
    private int _byteLength = 160 / 8; // 160 bit hash length

    public class EncodedPassword
    {
        public byte[] Hash { get; set; }
        public byte[] Salt { get; set; }
        public int Iterations { get; set; }
    }

    public EncodedPassword EncodePassword(string password, int iterations)
    {
        var populatedPassword = new EncodedPassword
        {
            Salt = CreateSalt(),
            Iterations = iterations
        };

        // Add Hash
        populatedPassword.Hash = CreateHash(password, populatedPassword.Salt, iterations);

        return populatedPassword;
    }

    public bool ValidatePassword(string password, EncodedPassword encodedPassword)
    {
        // Create Hash
        var testHash = CreateHash(password, encodedPassword.Salt, encodedPassword.Iterations);

        return testHash == encodedPassword.Hash;
    }

    public byte[] CreateSalt()
    {
        var salt = new byte[_byteLength]; // Salt should be same length as hash

        using (var saltGenerator = new RNGCryptoServiceProvider())
        {
            saltGenerator.GetBytes(salt);
        }

        return salt;
    }

    private byte[] CreateHash(string password, byte[] salt, long iterations)
    {
        byte[] hash;
        using (var hashGenerator = new Rfc2898DeriveBytes(password, salt, (int)iterations))
        {
            hash = hashGenerator.GetBytes(_byteLength);
        }

        return hash;
    }
} 

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