使用 MD5 或 sha-256 C# 散列密码

Posted

技术标签:

【中文标题】使用 MD5 或 sha-256 C# 散列密码【英文标题】:Hashing passwords with MD5 or sha-256 C# 【发布时间】:2011-05-18 19:29:04 【问题描述】:

我正在为一个应用程序编写一个注册表单,但对于 C# 的新手仍然有问题。

我希望将密码加密/散列到 md5 或 sha-256,最好是 sha-256。

有什么好的例子吗?我希望它能够从“字符串密码”中获取信息;然后对其进行散列并存储在变量“string hPassword;”中。有什么想法吗?

【问题讨论】:

你打算用散列密码做什么?将其存储在数据库中?然后简单地散列它是不够的(Rainbow table)。使用盐。 我会将其存储在数据库中。你有什么建议。 请大家注意,下面的答案已经过时了。请记住,您永远不应该再使用 MD5 来散列密码。它是旧的、破碎的和过时的。对于大多数人来说,最简单、最新且安全的解决方案是bcrypt。至少在撰写此评论时 :) Argon2 可能很快就会成为标准推荐。 【参考方案1】:

不要使用简单的哈希,甚至是加盐的哈希。使用某种加强键的技术,例如bcrypt(带有.NET implementation here)或PBKDF2(带有built-in implementation)。

这是一个使用 PBKDF2 的示例。

从您的密码生成密钥...

string password = GetPasswordFromUserInput();

// specify that we want to randomly generate a 20-byte salt
using (var deriveBytes = new Rfc2898DeriveBytes(password, 20))

    byte[] salt = deriveBytes.Salt;
    byte[] key = deriveBytes.GetBytes(20);  // derive a 20-byte key

    // save salt and key to database

然后去测试密码是否有效……

string password = GetPasswordFromUserInput();

byte[] salt, key;
// load salt and key from database

using (var deriveBytes = new Rfc2898DeriveBytes(password, salt))

    byte[] newKey = deriveBytes.GetBytes(20);  // derive a 20-byte key

    if (!newKey.SequenceEqual(key))
        throw new InvalidOperationException("Password is invalid!");

【讨论】:

我假设在您的帖子中您说 of PBKDF2,您打算说 OR。我不建议在 .NET 中使用 bcrypt,*** 放弃了 bcrypt 以使用 PBKDF2。请参阅 *** 博客上 Kevin Montrose 的评论 blog.***.com/2011/05/… @Chris:是的,我的意思是“或 PBKDF2”;更新。我个人也会选择 PBKDF2,尽管我不知道有什么理由不使用 bcrypt。 (我只听说过关于它的好话。) bcrypt 的问题是没有经过验证的 .NET 实现,这就是 *** 选择放弃使用 bcrypt 的原因。他们不希望花费资源来验证实现,而 .NET 中包含的 PBKDF2 本机实现已经针对 Microsoft 进行了验证。 这里是 *** 详细信息更改为使用 PBKDF2 的直接链接:blog.***.com/2011/05/… @NH。 - 你说得对,那个链接已经不好用了……我用 Wayback Machine 去找到那个页面的旧版本,结果发现它在页面的 cmets 中,从那以后就不见了他们将域移至.blog 域。所以这里是a link to where *** details changing to use PBKDF2。【参考方案2】:

您将要使用System.Security.Cryptography 命名空间;具体来说,MD5 class 或 SHA256 class。

从this page 上的代码中提取一些信息,并且知道两个类具有相同的基类 (HashAlgorithm),您可以使用如下函数:

public string ComputeHash(string input, HashAlgorithm algorithm)

   Byte[] inputBytes = Encoding.UTF8.GetBytes(input);

   Byte[] hashedBytes = algorithm.ComputeHash(inputBytes);

   return BitConverter.ToString(hashedBytes);

那么你可以这样称呼它(对于 MD5):

string hPassword = ComputeHash(password, new MD5CryptoServiceProvider());

或者对于 SHA256:

string hPassword = ComputeHash(password, new SHA256CryptoServiceProvider());

编辑:添加盐支持 正如 dtb 在 cmets 中指出的那样,如果包含添加 salt 的功能,此代码会更强大。如果您不熟悉它,salt 是一组随机位,作为散列函数的输入包含在内,这在很大程度上可以阻止字典对散列密码的攻击(例如,使用 rainbow table)。这是支持 salt 的 ComputeHash 函数的修改版本:

public static string ComputeHash(string input, HashAlgorithm algorithm, Byte[] salt)

   Byte[] inputBytes = Encoding.UTF8.GetBytes(input);

   // Combine salt and input bytes
   Byte[] saltedInput = new Byte[salt.Length + inputBytes.Length];
   salt.CopyTo(saltedInput, 0);
   inputBytes.CopyTo(saltedInput, salt.Length);

   Byte[] hashedBytes = algorithm.ComputeHash(saltedInput);

   return BitConverter.ToString(hashedBytes);

希望这对您有所帮助!

【讨论】:

OP 有一个字符串,并希望将该字符串的哈希值作为一个字符串。加密类只处理字节数组。 ASCIIEncoding.Default 令人困惑地返回 UTF-16 编码。我建议Encoding.UTF8(ASCII 太有限了)。 如果您现在还向您的ComputeHash 方法添加一个byte[] salt 参数并将这些字节添加到inputBytes,那么我正式喜欢您的回答:-) 使用 PBKDF2(通过 Rfc2898DeriveBytes)更安全,实际上需要 less 代码。 MD5CryptoServiceProviderSHA256CryptoServiceProvider(以及相关的HashAlgorithms)实现了IDisposable 接口,因此它们的创建和生命周期应该以最佳方式包装在using 块中而不是新对象中只是作为参数传递。【参考方案3】:

在将密码存储在数据库中时,您应该始终在散列之前对密码进行加盐。

推荐的数据库列:

密码盐:整数 密码哈希:二进制(20)

您在网上找到的大多数帖子都会讨论 ASCII 编码盐和哈希,但这不是必需的,只会增加不必要的计算。此外,如果您使用 SHA-1,则输出将只有 20 个字节,因此您在数据库中的哈希字段长度只需 20 个字节。我理解您对 SHA-256 的询问,但除非您有令人信服的理由,否则在大多数业务实践中使用带有盐值的 SHA-1 就足够了。如果你坚持使用SHA-256,那么数据库中的hash字段长度需要为32字节。

以下是一些函数,它们将生成盐、计算哈希并根据密码验证哈希。

下面的 salt 函数从 4 个以加密方式创建的随机字节生成一个加密强的 salt 作为整数。

private int GenerateSaltForPassword()

    RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
    byte[] saltBytes = new byte[4];
    rng.GetNonZeroBytes(saltBytes);
    return (((int)saltBytes[0]) << 24) + (((int)saltBytes[1]) << 16) + (((int)saltBytes[2]) << 8) + ((int)saltBytes[3]);

然后可以使用带有以下功能的盐对密码进行哈希处理。盐与密码连接,然后计算哈希。


private byte[] ComputePasswordHash(string password, int salt)

    byte[] saltBytes = new byte[4];
    saltBytes[0] = (byte)(salt >> 24);
    saltBytes[1] = (byte)(salt >> 16);
    saltBytes[2] = (byte)(salt >> 8);
    saltBytes[3] = (byte)(salt);

    byte[] passwordBytes = UTF8Encoding.UTF8.GetBytes(password);

    byte[] preHashed = new byte[saltBytes.Length + passwordBytes.Length];
    System.Buffer.BlockCopy(passwordBytes, 0, preHashed, 0, passwordBytes.Length);
    System.Buffer.BlockCopy(saltBytes, 0, preHashed, passwordBytes.Length, saltBytes.Length);

    SHA1 sha1 = SHA1.Create();
    return sha1.ComputeHash(preHashed);


只需计算哈希值,然后将其与预期的哈希值进行比较,即可检查密码。


private bool IsPasswordValid(string passwordToValidate, int salt, byte[] correctPasswordHash)

    byte[] hashedPassword = ComputePasswordHash(passwordToValidate, salt);

    return hashedPassword.SequenceEqual(correctPasswordHash);


【讨论】:

-1 用于普通 SHA-1。使用慢散列,例如 PBKDF2、bcrypt 或 scrypt。还建议使用 64 位盐,但这是一个小问题。 SHA1 并非 100% 可靠,尽管 Google 团队花了 2 年时间来证明这一点:security.googleblog.com/2017/02/… 也可以使用 SHA-256 或 SHA3【参考方案4】:

TL;DR 使用 Microsoft.AspNetCore.Cryptography.KeyDerivation,使用 SHA-512 实现 PBKDF2。

开始使用密码散列的好主意是查看OWASP guidelines 所说的内容。推荐算法列表包括 Argon2、PBKDF2、scrypt 和 bcrypt。所有这些算法都可以调整以调整散列密码所需的时间,以及相应地通过蛮力破解密码的时间。所有这些算法都利用盐来防止彩虹表攻击。

这些算法都不是很弱,但有一些区别:

bcrypt 已存在近 20 年,已被广泛使用和 经受住了时间的考验。它对GPU有很强的抵抗力 攻击,但不针对 FPGA Argon2 是最新加入的,是 2015 年密码哈希竞赛的获胜者。它对 GPU 和 FPGA 攻击有更好的保护,但对我来说有点太新了 我对 scrypt 了解不多。它旨在阻止 GPU 和 FPGA 加速攻击,但我听说它没有最初声称的那么强大 PBKDF2 是由不同哈希参数化的一系列算法 职能。它不提供针对 GPU 或 ASIC 攻击的特定保护,尤其是在使用 SHA-1 等较弱的散列函数时,但是,如果它对您很重要,它已通过 FIPS 认证,并且如果迭代次数为足够大。

仅基于算法,​​我可能会选择 bcrypt,PBKDF2 是最不受欢迎的。

但是,这还不是全部,因为即使是最好的算法也可能因实施不当而变得不安全。让我们看看 .NET 平台有什么可用的:

Bcrypt 可通过bcrypt.net 获得。他们说这个实现是基于 Java jBCrypt 的。目前在 github 上有 6 个贡献者和 8 个问题(全部关闭)。总的来说,看起来不错,但是不知道有没有人对代码进行过审核,如果发现漏洞,也很难说是否会尽快提供更新版本。由于这些原因,我听说 Stack Overflow 不再使用 bcrypt 使用 Argon2 的最佳方式可能是通过绑定到 著名的 libsodium 库,例如 https://github.com/adamcaudill/libsodium-net。这个想法是 大多数加密是通过 libsodium 实现的,它有相当大的 支持,并且“未经测试”的部分非常有限。然而,在 密码学细节意义重大,因此结合 Argon2 相对较新,我会将其视为实验性选项 长期以来,.NET 都有一个内置的 PBKDF2 实现,通过 Rfc2898DeriveBytes 班级。但是,该实现只能使用 SHA-1 哈希函数,这在当今被认为太快不安全 最后,最新的解决方案是 Microsoft.AspNetCore.Cryptography.KeyDerivation包 可通过 NuGet 获得。它提供带有 SHA-1、SHA-256 或 SHA-512 哈希函数的 PBKDF2 算法,比Rfc2898DeriveBytes 好很多。这里最大的优势是实现是由微软提供的,虽然我无法正确评估微软开发人员与 BCrypt.net 或 libsodium 开发人员的加密勤奋,但信任它是有意义的,因为如果你正在运行一个 .NET 应用程序,你已经严重依赖微软。如果发现安全问题,我们可能还期望 Microsoft 发布更新。希望。

总结到目前为止的研究,虽然 PBKDF2 可能是四个算法中最不受欢迎的算法,但 Microsoft 提供的实现的可用性胜过这一点,因此合理的决定是使用 Microsoft.AspNetCore.Cryptography.KeyDerivation

目前最新的包以 .NET Standard 2.0 为目标,因此可用于 .NET Core 2.0 或 .NET Framework 4.6.1 或更高版本。如果您使用较早的框架版本,则可以使用以前版本的包 1.1.3,它针对 .NET Framework 4.5.1 或 .NET Core 1.0。不幸的是,它甚至无法在更早的 .NET 版本中使用。

文档和工作示例可在docs.microsoft.com 获得。但是,不要照原样复制粘贴,开发人员仍然需要做出决定。

第一个决定是使用什么哈希函数。可用选项包括 SHA-1、SHA-256 和 SHA-512。其中,SHA-1 绝对是安全的太快了,SHA-256 是不错的,但我会推荐 SHA-512,因为据说它的 64 位操作使用使得它更难从基于 GPU 的攻击中受益。

然后,您需要选择密码哈希输出长度和盐长度。输出长于哈希函数输出(例如 SHA-512 的 512 位)是没有意义的,并且完全像这样可能是最安全的。对于盐的长度,意见不一。 128 位应该足够了,但无论如何,长于哈希输出长度的长度肯定不会带来任何好处。

接下来,有一个迭代计数。它越大,密码哈希越难破解,但用户登录所需的时间越长。我建议选择它,这样在典型的生产系统上哈希需要 0.25 - 1 秒,无论如何,它不应小于 10000。

通常,您会得到字节数组作为盐和哈希值。使用 Base64 将它们转换为字符串。您可以选择在数据库中使用两个不同的列,或者使用 Base64 中没有的分隔符将盐和密码组合在一个列中。

不要忘记设计一种密码哈希存储方式,以便将来无缝迁移到更好的哈希算法。

【讨论】:

【参考方案5】:

如果您要存储散列密码,请使用 bcrypt 而不是 SHA-256。问题是 SHA-256 已针对速度进行了优化,如果有人可以访问您的数据库,则更容易对密码进行暴力攻击。

阅读这篇文章:Enough With The Rainbow Tables: What You Need To Know About Secure Password Schemes 和这个 answer 到之前的 SO 问题。

文章中的一些引用:

问题在于 MD5 速度很快。它的现代竞争对手也是如此,例如 SHA1 和 SHA256。速度是现代安全散列的设计目标,因为散列是几乎每个密码系统的构建块,并且通常在每个数据包或每个消息的基础上按需执行。

速度正是您在密码哈希函数中所不想要的。


最后,我们了解到,如果我们想安全地存储密码,我们有三个合理的选择:PHK 的 MD5 方案、Provos-Maziere 的 Bcrypt 方案和 SRP。我们了解到正确的选择是 Bcrypt。

【讨论】:

密码经过哈希处理后将存储在数据库中。根本不存储在本地。 好的,那么每个密码都应该使用唯一的盐进行哈希处理,并且您应该使用 bcrypt 或类似的东西。使用盐进行散列可以阻止使用彩虹表的攻击,并且使用较慢的算法会增加对散列密码进行暴力攻击的工作量。 我不建议在 .NET 中使用 bcrypt,*** 放弃 bcrypt 使用 PBKDF2。请参阅 *** 博客上 Kevin Montrose 的评论blog.***.com/2011/05/… @ChrisMarisic 注意为什么 *** 不使用bcrypt 很有用。当你说“*** 放弃了 bcrypt” 时,你可能会觉得bcrypt 有问题。【参考方案6】:

PBKDF2 正在使用 HMACSHA1.......如果您想要更现代的 HMACSHA256 或 HMACSHA512 实现并且仍然想要密钥拉伸以使算法变慢,我建议使用此 API:https://sourceforge.net/projects/pwdtknet/

【讨论】:

【参考方案7】:

这是一个持久性不知道 SecuredPassword 类的完整实现

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;


    public class SecuredPassword
    
        private const int saltSize = 256;
        private readonly byte[] hash;
        private readonly byte[] salt;

        public byte[] Hash
        
        get  return hash; 
    

    public byte[] Salt
    
        get  return salt; 
    

    public SecuredPassword(string plainPassword)
    
        if (string.IsNullOrWhiteSpace(plainPassword))
            return; 

        using (var deriveBytes = new Rfc2898DeriveBytes(plainPassword, saltSize))
        
            salt = deriveBytes.Salt;
            hash = deriveBytes.GetBytes(saltSize);
        
    

    public SecuredPassword(byte[] hash, byte[] salt)
    
        this.hash = hash;
        this.salt = salt;
    

    public bool Verify(string password)
    
        if (string.IsNullOrWhiteSpace(password))
            return false; 

        using (var deriveBytes = new Rfc2898DeriveBytes(password, salt))
        
            byte[] newKey = deriveBytes.GetBytes(saltSize);

            return newKey.SequenceEqual(hash);
        
    

和测试:

 public class SecuredPasswordTests

    [Test]
    public void IsHashed_AsExpected()
    
        var securedPassword = new SecuredPassword("password");

        Assert.That(securedPassword.Hash, Is.Not.EqualTo("password"));
        Assert.That(securedPassword.Hash.Length, Is.EqualTo(256));
    

    [Test]
    public void Generates_Unique_Salt()
    
        var securedPassword = new SecuredPassword("password");
        var securedPassword2 = new SecuredPassword("password");

        Assert.That(securedPassword.Salt, Is.Not.Null);
        Assert.That(securedPassword2.Salt, Is.Not.Null);
        Assert.That(securedPassword.Salt, Is.Not.EqualTo(securedPassword2.Salt));
    

    [Test]
    public void Generates_Unique_Hash()
    
        var securedPassword = new SecuredPassword("password");
        var securedPassword2 = new SecuredPassword("password");

        Assert.That(securedPassword.Hash, Is.Not.Null);
        Assert.That(securedPassword2.Hash, Is.Not.Null);
        Assert.That(securedPassword.Hash, Is.Not.EqualTo(securedPassword2.Hash));
    

    [Test]
    public void Verify_WhenMatching_ReturnsTrue()
    
        var securedPassword = new SecuredPassword("password");
        var result = securedPassword.Verify("password");
        Assert.That(result, Is.True);
    

    [Test]
    public void Verify_WhenDifferent_ReturnsFalse()
    
        var securedPassword = new SecuredPassword("password");
        var result = securedPassword.Verify("Password");
        Assert.That(result, Is.False);
    

    [Test]
    public void Verify_WhenRehydrated_AndMatching_ReturnsTrue()
    
        var securedPassword = new SecuredPassword("password123");

        var rehydrated = new SecuredPassword(securedPassword.Hash, securedPassword.Salt);

        var result = rehydrated.Verify("password123");
        Assert.That(result, Is.True);
    

    [Test]
    public void Constructor_Handles_Null_Password()
    
        Assert.DoesNotThrow(() => new SecuredPassword(null));
    

    [Test]
    public void Constructor_Handles_Empty_Password()
    
        Assert.DoesNotThrow(() => new SecuredPassword(string.Empty));
    

    [Test]
    public void Verify_Handles_Null_Password()
    
        Assert.DoesNotThrow(() => new SecuredPassword("password").Verify(null));
    

    [Test]
    public void Verify_Handles_Empty_Password()
    
        Assert.DoesNotThrow(() => new SecuredPassword("password").Verify(string.Empty));
    

    [Test]
    public void Verify_When_Null_Password_ReturnsFalse()
    
        Assert.That(new SecuredPassword("password").Verify(null), Is.False);
    

【讨论】:

【参考方案8】:

System.Security.Cryptography.SHA256 类应该可以解决问题:

http://msdn.microsoft.com/en-us/library/system.security.cryptography.sha256.aspx

【讨论】:

【参考方案9】:

请使用这个,因为我之前也有同样的问题,但可以通过小代码 sn-p 解决它

    public static string ComputeHash(string input, HashAlgorithm algorithm, Byte[] salt)
    
        Byte[] inputBytes = Encoding.UTF8.GetBytes(input);

        // Combine salt and input bytes
        Byte[] saltedInput = new Byte[salt.Length + inputBytes.Length];
        salt.CopyTo(saltedInput, 0);
        inputBytes.CopyTo(saltedInput, salt.Length);

        Byte[] hashedBytes = algorithm.ComputeHash(saltedInput);


        StringBuilder hex = new StringBuilder(hashedBytes.Length * 2);
        foreach (byte b in hashedBytes)
            hex.AppendFormat("0:X2", b);

        return hex.ToString();

    

【讨论】:

你应该添加一个解释,见How do I ask a good question?

以上是关于使用 MD5 或 sha-256 C# 散列密码的主要内容,如果未能解决你的问题,请参考以下文章

MD5,sha1,sha256分别输出多少位啊?

散列密码的最佳实践 - SHA256 还是 SHA512?

Mac终端快捷生成md5/sha

SHA-256 或 MD5 用于文件完整性

PHP 之sha256 sha512封装

PHP 之sha256 sha512封装