如何哈希密码


Answers:


62

更新该答案严重过时。请改用https://stackoverflow.com/a/10402129/251311的建议。

您可以使用

var md5 = new MD5CryptoServiceProvider();
var md5data = md5.ComputeHash(data);

要么

var sha1 = new SHA1CryptoServiceProvider();
var sha1data = sha1.ComputeHash(data);

要获取data字节数组,可以使用

var data = Encoding.ASCII.GetBytes(password);

并从md5data或获取字符串sha1data

var hashedPassword = ASCIIEncoding.GetString(md5data);

11
我真的建议使用SHA1。除非您要保持与现有系统的向后兼容性,否则MD5是禁忌。另外,请确保将其放入using语句中或Clear()在使用完完成后调用它。
vcsjones 2010年

3
@vcsjones:我不想在这里进行圣战,但md5足以应付几乎所有类型的任务。它的漏洞还涉及非常特殊的情况,并且攻击者几乎需要了解很多有关加密的知识。
zerkms 2010年

4
@zerkms点,但是如果没有向后兼容性的理由,则没有理由使用MD5。“安全胜于遗憾”。
vcsjones

4
此时没有理由使用MD5。鉴于计算时间微不足道,因此没有理由使用MD5,因为它与现有系统兼容。即使MD5“足够好”,使用安全得多的SHA也不会给用户带来任何成本。我敢肯定zerkms知道这对于发问者来说更多。
Gerald Davis 2010年

11
三个大错误:1)ASCII默默地降低具有不寻常字符的密码2)普通MD5 / SHA-1 / SHA-2速度很快。3)你需要盐。| 请改用PBKDF2,bcrypt或scrypt。PBKDF2是Rfc2898DeriveBytes类中最简单的(不确定WP7中是否存在)
CodesInChaos 2012年

298

这里的大多数其他答案都与当今的最佳做法有些过时。因此,这里是使用PBKDF2 / Rfc2898DeriveBytes存储和验证密码的应用程序。下面的代码在本文的独立类中:如何存储带盐的密码哈希的另一个示例。基础非常简单,因此这里进行了细分:

步骤1使用加密的PRNG创建盐值:

byte[] salt;
new RNGCryptoServiceProvider().GetBytes(salt = new byte[16]);

步骤2创建Rfc2898DeriveBytes并获取哈希值:

var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 100000);
byte[] hash = pbkdf2.GetBytes(20);

步骤3组合盐和密码字节以供以后使用:

byte[] hashBytes = new byte[36];
Array.Copy(salt, 0, hashBytes, 0, 16);
Array.Copy(hash, 0, hashBytes, 16, 20);

步骤4将合并的Salt + hash转换为字符串进行存储

string savedPasswordHash = Convert.ToBase64String(hashBytes);
DBContext.AddUser(new User { ..., Password = savedPasswordHash });

步骤5对照存储的密码验证用户输入的密码

/* Fetch the stored value */
string savedPasswordHash = DBContext.GetUser(u => u.UserName == user).Password;
/* Extract the bytes */
byte[] hashBytes = Convert.FromBase64String(savedPasswordHash);
/* Get the salt */
byte[] salt = new byte[16];
Array.Copy(hashBytes, 0, salt, 0, 16);
/* Compute the hash on the password the user entered */
var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 100000);
byte[] hash = pbkdf2.GetBytes(20);
/* Compare the results */
for (int i=0; i < 20; i++)
    if (hashBytes[i+16] != hash[i])
        throw new UnauthorizedAccessException();

注意:根据特定应用程序的性能要求,100000可以降低该值。最小值应在左右10000


8
@Daniel基本上,该帖子是关于使用比单独的哈希更安全的东西。如果您只是简单地对密码进行哈希处理(即使添加了盐),您的用户密码就会被泄露(并且很可能已出售/发布),甚至没有机会告诉他们更改密码。使用上面的代码会使攻击者感到困难,而对开发人员则不容易。
csharptest.net 2014年

2
@DatVM不,每次存储哈希值时都要添加新盐。这就是为什么它与哈希一起存储的原因,以便您可以验证密码。
csharptest.net 2015年

9
@CiprianJijie重点是您不应该能够做到的。
csharptest.net '16

9
万一有人在做一个VerifyPassword方法,如果您想使用Linq和一个较短的布尔值调用,可以这样做:return hash.SequenceEqual(hashBytes.Skip(_saltSize));
JESU卡斯蒂略

2
@ csharptest.net您推荐什么样的阵列大小?数组的大小是否会在很大程度上影响安全性?我不知道很多关于散列/加密
lennyy

71

基于csharptest.net很好的答案,我为此编写了一个Class:

public static class SecurePasswordHasher
{
    /// <summary>
    /// Size of salt.
    /// </summary>
    private const int SaltSize = 16;

    /// <summary>
    /// Size of hash.
    /// </summary>
    private const int HashSize = 20;

    /// <summary>
    /// Creates a hash from a password.
    /// </summary>
    /// <param name="password">The password.</param>
    /// <param name="iterations">Number of iterations.</param>
    /// <returns>The hash.</returns>
    public static string Hash(string password, int iterations)
    {
        // Create salt
        byte[] salt;
        new RNGCryptoServiceProvider().GetBytes(salt = new byte[SaltSize]);

        // Create hash
        var pbkdf2 = new Rfc2898DeriveBytes(password, salt, iterations);
        var hash = pbkdf2.GetBytes(HashSize);

        // Combine salt and hash
        var hashBytes = new byte[SaltSize + HashSize];
        Array.Copy(salt, 0, hashBytes, 0, SaltSize);
        Array.Copy(hash, 0, hashBytes, SaltSize, HashSize);

        // Convert to base64
        var base64Hash = Convert.ToBase64String(hashBytes);

        // Format hash with extra information
        return string.Format("$MYHASH$V1${0}${1}", iterations, base64Hash);
    }

    /// <summary>
    /// Creates a hash from a password with 10000 iterations
    /// </summary>
    /// <param name="password">The password.</param>
    /// <returns>The hash.</returns>
    public static string Hash(string password)
    {
        return Hash(password, 10000);
    }

    /// <summary>
    /// Checks if hash is supported.
    /// </summary>
    /// <param name="hashString">The hash.</param>
    /// <returns>Is supported?</returns>
    public static bool IsHashSupported(string hashString)
    {
        return hashString.Contains("$MYHASH$V1$");
    }

    /// <summary>
    /// Verifies a password against a hash.
    /// </summary>
    /// <param name="password">The password.</param>
    /// <param name="hashedPassword">The hash.</param>
    /// <returns>Could be verified?</returns>
    public static bool Verify(string password, string hashedPassword)
    {
        // Check hash
        if (!IsHashSupported(hashedPassword))
        {
            throw new NotSupportedException("The hashtype is not supported");
        }

        // Extract iteration and Base64 string
        var splittedHashString = hashedPassword.Replace("$MYHASH$V1$", "").Split('$');
        var iterations = int.Parse(splittedHashString[0]);
        var base64Hash = splittedHashString[1];

        // Get hash bytes
        var hashBytes = Convert.FromBase64String(base64Hash);

        // Get salt
        var salt = new byte[SaltSize];
        Array.Copy(hashBytes, 0, salt, 0, SaltSize);

        // Create hash with given salt
        var pbkdf2 = new Rfc2898DeriveBytes(password, salt, iterations);
        byte[] hash = pbkdf2.GetBytes(HashSize);

        // Get result
        for (var i = 0; i < HashSize; i++)
        {
            if (hashBytes[i + SaltSize] != hash[i])
            {
                return false;
            }
        }
        return true;
    }
}

用法:

// Hash
var hash = SecurePasswordHasher.Hash("mypassword");

// Verify
var result = SecurePasswordHasher.Verify("mypassword", hash);

示例哈希可能是这样的:

$MYHASH$V1$10000$Qhxzi6GNu/Lpy3iUqkeqR/J1hh8y/h5KPDjrv89KzfCVrubn

如您所见,为了方便使用,我还将迭代包含在哈希中,如果需要升级,也可以对其进行升级。


如果您对.net core感兴趣,我在Code Review上也有一个.net core版本。


1
只是为了验证,如果您确实升级了哈希引擎,您会增加哈希的V1部分并从中删除吗?
迈克·科尔

1
是的,那是计划。然后,您将根据所需的验证方法V1以及V2所需的验证方法进行决策。
Christian Gollhardt's

感谢您的答复和课程。我在讲话时正在实施它。
Mike Cole

2
是的@NelsonSilva。那是因为
Christian Gollhardt

1
使用此代码的所有副本/粘贴内容(包括我),我希望有人发声并在发现问题后对其进行修订!:)
小资

14

我使用哈希和盐加密密码(这与Asp.Net成员身份使用的哈希相同):

private string PasswordSalt
{
   get
   {
      var rng = new RNGCryptoServiceProvider();
      var buff = new byte[32];
      rng.GetBytes(buff);
      return Convert.ToBase64String(buff);
   }
}

private string EncodePassword(string password, string salt)
{
   byte[] bytes = Encoding.Unicode.GetBytes(password);
   byte[] src = Encoding.Unicode.GetBytes(salt);
   byte[] dst = new byte[src.Length + bytes.Length];
   Buffer.BlockCopy(src, 0, dst, 0, src.Length);
   Buffer.BlockCopy(bytes, 0, dst, src.Length, bytes.Length);
   HashAlgorithm algorithm = HashAlgorithm.Create("SHA1");
   byte[] inarray = algorithm.ComputeHash(dst);
   return Convert.ToBase64String(inarray);
}

16
-1表示使用普通的SHA-1,这很快。使用慢键派生功能,例如PBKDF2,bcrypt或scrypt。
CodesInChaos 2012年

1
  1. 撒盐
  2. 用salt创建哈希密码
  3. 保存哈希和盐
  4. 使用密码和密码解密...因此开发人员无法解密密码
public class CryptographyProcessor
{
    public string CreateSalt(int size)
    {
        //Generate a cryptographic random number.
          RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
         byte[] buff = new byte[size];
         rng.GetBytes(buff);
         return Convert.ToBase64String(buff);
    }


      public string GenerateHash(string input, string salt)
      { 
         byte[] bytes = Encoding.UTF8.GetBytes(input + salt);
         SHA256Managed sHA256ManagedString = new SHA256Managed();
         byte[] hash = sHA256ManagedString.ComputeHash(bytes);
         return Convert.ToBase64String(hash);
      }

      public bool AreEqual(string plainTextInput, string hashedInput, string salt)
      {
           string newHashedPin = GenerateHash(plainTextInput, salt);
           return newHashedPin.Equals(hashedInput); 
      }
 }

1

@ csharptest.netChristian Gollhardt的答案都很好,非常感谢。但是,在具有数百万条记录的生产环境中运行此代码后,我发现内存泄漏。RNGCryptoServiceProviderRfc2898DeriveBytes类是从IDisposable派生的,但我们不对其进行处理。如果有人需要废弃版本,我将写我的解决方案作为答案。

public static class SecurePasswordHasher
{
    /// <summary>
    /// Size of salt.
    /// </summary>
    private const int SaltSize = 16;

    /// <summary>
    /// Size of hash.
    /// </summary>
    private const int HashSize = 20;

    /// <summary>
    /// Creates a hash from a password.
    /// </summary>
    /// <param name="password">The password.</param>
    /// <param name="iterations">Number of iterations.</param>
    /// <returns>The hash.</returns>
    public static string Hash(string password, int iterations)
    {
        // Create salt
        using (var rng = new RNGCryptoServiceProvider())
        {
            byte[] salt;
            rng.GetBytes(salt = new byte[SaltSize]);
            using (var pbkdf2 = new Rfc2898DeriveBytes(password, salt, iterations))
            {
                var hash = pbkdf2.GetBytes(HashSize);
                // Combine salt and hash
                var hashBytes = new byte[SaltSize + HashSize];
                Array.Copy(salt, 0, hashBytes, 0, SaltSize);
                Array.Copy(hash, 0, hashBytes, SaltSize, HashSize);
                // Convert to base64
                var base64Hash = Convert.ToBase64String(hashBytes);

                // Format hash with extra information
                return $"$HASH|V1${iterations}${base64Hash}";
            }
        }

    }

    /// <summary>
    /// Creates a hash from a password with 10000 iterations
    /// </summary>
    /// <param name="password">The password.</param>
    /// <returns>The hash.</returns>
    public static string Hash(string password)
    {
        return Hash(password, 10000);
    }

    /// <summary>
    /// Checks if hash is supported.
    /// </summary>
    /// <param name="hashString">The hash.</param>
    /// <returns>Is supported?</returns>
    public static bool IsHashSupported(string hashString)
    {
        return hashString.Contains("HASH|V1$");
    }

    /// <summary>
    /// Verifies a password against a hash.
    /// </summary>
    /// <param name="password">The password.</param>
    /// <param name="hashedPassword">The hash.</param>
    /// <returns>Could be verified?</returns>
    public static bool Verify(string password, string hashedPassword)
    {
        // Check hash
        if (!IsHashSupported(hashedPassword))
        {
            throw new NotSupportedException("The hashtype is not supported");
        }

        // Extract iteration and Base64 string
        var splittedHashString = hashedPassword.Replace("$HASH|V1$", "").Split('$');
        var iterations = int.Parse(splittedHashString[0]);
        var base64Hash = splittedHashString[1];

        // Get hash bytes
        var hashBytes = Convert.FromBase64String(base64Hash);

        // Get salt
        var salt = new byte[SaltSize];
        Array.Copy(hashBytes, 0, salt, 0, SaltSize);

        // Create hash with given salt
        using (var pbkdf2 = new Rfc2898DeriveBytes(password, salt, iterations))
        {
            byte[] hash = pbkdf2.GetBytes(HashSize);

            // Get result
            for (var i = 0; i < HashSize; i++)
            {
                if (hashBytes[i + SaltSize] != hash[i])
                {
                    return false;
                }
            }

            return true;
        }

    }
}

用法:

// Hash
var hash = SecurePasswordHasher.Hash("mypassword");

// Verify
var result = SecurePasswordHasher.Verify("mypassword", hash);

0

我认为使用KeyDerivation.Pbkdf2比Rfc2898DeriveBytes更好。

示例和说明: 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=
 */

这是本文的示例代码。这是最低安全级别。为了增加它,我将代替KeyDerivationPrf.HMACSHA1参数

KeyDerivationPrf.HMACSHA256或KeyDerivationPrf.HMACSHA512。

不要妥协密码哈希。有许多数学上合理的方法可以优化密码哈希黑客。后果可能是灾难性的。一旦恶意分子可以使用您用户的密码哈希表,由于算法较弱或实现不正确,他就很容易破解密码。他有很多时间(时间x计算机能力)来破解密码。密码哈希在密码学上应该很强大,可以将“很多时间”转换为“ 不合理的时间 ”。

还有一点要添加

哈希验证需要时间(这很好)。当用户输入错误的用户名时,无需花费任何时间即可检查用户名是否正确。如果用户名正确,我们将开始密码验证-这是一个相对较长的过程。

对于黑客来说,很容易理解用户是否存在。

确保当用户名错误时不返回立即答复。

不用说:永远不要给出答案是什么问题。只是一般的“凭据错误”。


1
顺便说一句,先前的答案stackoverflow.com/a/57508528/11603057是不正确且有害的。那是散列而不是密码散列的示例。在密钥派生过程中,必须是伪随机函数的迭代。没有 我无法发表评论或投票(我的声誉低下)。请不要错过不正确的答案!
艾伯特·柳巴斯基
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.