我正在为一个应用程序编写一个注册表单,但仍然遇到 C# 新手的问题。
我正在寻找加密/散列密码到 md5 或 sha-256,最好是 sha-256。
有什么好的例子吗?我希望它能够从“字符串密码”中获取信息;然后对其进行哈希处理并存储在变量“string hPassword;”中。有任何想法吗?
不要使用简单的散列,甚至是加盐的散列。使用某种密钥强化技术,例如bcrypt(此处使用 .NET 实现)或PBKDF2(使用内置实现)。
这是一个使用 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!");
}
您将要使用System.Security.Cryptography
命名空间;具体来说,MD5
class或SHA256
class。
从这个页面上的代码中提取一点,并且知道两个类具有相同的基类 ( 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 在评论中指出的那样,如果它包含添加盐的能力,这段代码会更强大。如果您不熟悉它,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);
}
希望这对您有所帮助!
在将密码存储在数据库中时,您应该始终在散列之前对密码进行加盐。
推荐的数据库列:
您在网上找到的大多数帖子都会讨论 ASCII 编码盐和哈希,但这不是必需的,只会添加不需要的计算。此外,如果您使用SHA-1,那么输出将只有 20 个字节,因此您在数据库中的哈希字段长度只需 20 个字节。我理解您对 SHA-256 的询问,但除非您有令人信服的理由,否则在大多数业务实践中使用带有盐值的 SHA-1 就足够了。如果你坚持使用 SHA-256,那么数据库中的 hash 字段长度需要为 32 个字节。
下面是一些函数,它们将生成盐、计算散列并根据密码验证散列。
下面的盐函数从 4 个加密创建的随机字节生成一个加密强盐作为整数。
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);
}
TL;DR 使用Microsoft.AspNetCore.Cryptography.KeyDerivation,使用 SHA-512 实现 PBKDF2。
开始使用密码散列的好主意是查看OWASP 指南所说的内容。推荐算法列表包括 Argon2、PBKDF2、scrypt 和 bcrypt。所有这些算法都可以调整以调整散列密码所需的时间,以及相应地通过蛮力破解密码的时间。所有这些算法都使用盐来防止彩虹表攻击。
这些算法都不是非常弱,但有一些区别:
仅基于算法,我可能会选择 bcrypt,PBKDF2 是最不受欢迎的。
然而,这还不是全部,因为即使是最好的算法也会因为糟糕的实现而变得不安全。让我们看看 .NET 平台有什么可用的:
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 中没有的分隔符将盐和密码组合在一列中。
不要忘记设计一种密码哈希存储方式,以便将来无缝地迁移到更好的哈希算法。
如果要存储散列密码,请使用bcrypt而不是 SHA-256。问题是 SHA-256 已针对速度进行了优化,如果有人可以访问您的数据库,则更容易对密码进行暴力攻击。
阅读这篇文章:彩虹表就够了:您需要了解的有关安全密码方案的知识以及对上一个 SO 问题的回答。
文章中的一些引用:
问题是 MD5 速度很快。它的现代竞争对手也是如此,例如 SHA1 和 SHA256。速度是现代安全散列的设计目标,因为散列是几乎每个密码系统的构建块,并且通常在每个数据包或每个消息的基础上按需执行。
速度正是您在密码哈希函数中不想要的。
最后,我们了解到,如果我们想安全地存储密码,我们有三个合理的选择:PHK 的 MD5 方案、Provos-Maziere 的 Bcrypt 方案和 SRP。我们了解到正确的选择是 Bcrypt。
这是持久性不知道 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);
}
}
PBKDF2 正在使用 HMACSHA1.......如果你想要更现代的 HMACSHA256 或 HMACSHA512 实现并且仍然想要密钥拉伸以使算法变慢,我建议使用这个 API:https ://sourceforge.net/projects/pwdtknet/
System.Security.Cryptography.SHA256 类应该可以解决问题:
http://msdn.microsoft.com/en-us/library/system.security.cryptography.sha256.aspx
请使用它,因为我之前有同样的问题,但可以通过小代码片段解决它
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();
}