【问题标题】:How to hash a password如何散列密码
【发布时间】:2011-05-10 00:48:38
【问题描述】:

我想在手机上存储密码的哈希值,但我不知道该怎么做。我似乎只能找到加密方法。密码应该如何正确散列?

【问题讨论】:

    标签: c# security hash passwords windows-phone-7


    【解决方案1】:

    这里的大多数其他答案都与当今的最佳实践有些过时。因此,这里是使用 PBKDF2/Rfc2898DeriveBytes 存储和验证密码的应用程序。以下代码在这篇文章的一个独立类中:Another example of how to store a salted password hash。基础真的很简单,所以在这里分解一下:

    步骤 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 步 合并 salt 和密码字节以备后用:

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

    STEP 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 左右。

    【讨论】:

    • @Daniel 基本上这篇文章是关于使用比单独的哈希更安全的东西。如果您只是简单地对密码进行哈希处理,即使使用盐,您的用户密码也会在您有机会告诉他们更改之前就被泄露(并且可能会被出售/发布)。使用上面的代码,让攻击者难上加难,开发者不易。
    • @DatVM 不,每次存储哈希时都会使用新盐。这就是为什么它与哈希相结合进行存储,以便您可以验证密码。
    • @CiprianJijie 整点是你不认为能够。
    • 如果有人正在使用 VerifyPassword 方法,如果您想使用 Linq 和更短的布尔值调用,可以这样做:return hash.SequenceEqual(hashBytes.Skip(_saltSize));
    • @csharptest.net 你推荐什么样的数组大小?数组的大小是否对安全性有很大影响?我不太了解散列/密码学
    【解决方案2】:

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

    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 版本。

    【讨论】:

    • 只是为了验证一下,如果您升级哈希引擎,您会增加哈希的 V1 部分并关闭它吗?
    • 是的,这就是计划。然后,您将根据V1V2 决定您需要哪种验证方法。
    • 你为什么不用SecureString
    • 是的@NelsonSilva。那是因为salt
    • 这段代码的所有复制/粘贴(包括我),我希望有人说出来,如果发现问题,帖子会得到修改! :)
    【解决方案3】:

    更新此答案严重过时。请改用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);
    

    并从md5datasha1data取回字符串

    var hashedPassword = ASCIIEncoding.GetString(md5data);
    

    【讨论】:

    • 我真的会推荐使用 SHA1。除非您要保持与现有系统的向后兼容性,否则 MD5 是禁忌。此外,请确保在使用完实现后将其放入 using 语句或在其上调用 Clear()
    • @vcsjones:我不想在这里进行圣战,但md5 足以胜任几乎所有类型的任务。它的漏洞也涉及非常具体的情况,几乎要求攻击者对密码学有很多了解。
    • @zerkms 点了,但是如果没有向后兼容的理由,就没有理由使用 MD5。 “比后悔更安全”。
    • 此时没有理由使用 MD5。鉴于计算时间微不足道,除了与现有系统兼容外,没有理由使用 MD5。即使 MD5 “足够好”,用户使用更安全的 SHA 也是免费的。我相信 zerkms 知道这一点,评论更适合提问者。
    • 三大错误:1) ASCII 会默默地降低具有异常字符的密码 2) 普通 MD5/SHA-1/SHA-2 速度很快。 3)你需要盐。 |请改用 PBKDF2、bcrypt 或 scrypt。 PBKDF2 在 Rfc2898DeriveBytes 类中最简单(不确定是否存在于 WP7)
    【解决方案4】:

    我使用哈希和盐来加密我的密码(这与 Asp.Net Membership 使用的哈希相同):

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

    【讨论】:

    • -1 用于使用普通的 SHA-1,速度很快。使用慢速密钥派生函数,例如 PBKDF2、bcrypt 或 scrypt。
    【解决方案5】:

    @csharptest.netChristian Gollhardt's 的答案很棒,非常感谢。但是在拥有数百万条记录的生产环境中运行此代码后,我发现存在内存泄漏。 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);
    

    【讨论】:

      【解决方案6】:

      在 ASP.NET Core 中,使用PasswordHasher&lt;TUser&gt;
      • 命名空间:Microsoft.AspNetCore.Identity
      • 组装:Microsoft.Extensions.Identity.Core.dll (NuGet | Source)


      要散列密码,请使用HashPassword()

      var hashedPassword = new PasswordHasher<object?>().HashPassword(null, password);
      

      要验证密码,请使用VerifyHashedPassword()

      var passwordVerificationResult = new PasswordHasher<object?>().VerifyHashedPassword(null, hashedPassword, password);
      switch (passwordVerificationResult)
      {
          case PasswordVerificationResult.Failed:
              Console.WriteLine("Password incorrect.");
              break;
          
          case PasswordVerificationResult.Success:
              Console.WriteLine("Password ok.");
              break;
          
          case PasswordVerificationResult.SuccessRehashNeeded:
              Console.WriteLine("Password ok but should be rehashed and updated.");
              break;
          
          default:
              throw new ArgumentOutOfRangeException();
      }
      
      

      优点:

      • .NET 平台的一部分。比构建自己的加密算法更安全、更值得信赖。
      • 可配置的迭代次数和未来的兼容性(请参阅PasswordHasherOptions)。
      • 在验证密码 (source) 时将 Timing Attack 考虑在内,就像 PHPGo 所做的那样。

      缺点:

      【讨论】:

      • 还有一个缺点:&lt;TUser&gt; 作为假参数。它是 ASP.NET Core Identity 的一部分,这意味着将来它可能会依赖于 Identity 框架。
      【解决方案7】:
      1. 创建盐,
      2. 使用盐创建哈​​希密码
      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); 
            }
       }
      

      【讨论】:

        【解决方案8】:

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

        示例及说明: Hash passwords in 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 计算机能力)来破解密码。密码散列应该在密码学上很强大以“很多时间” 到“不合理的时间量”。

        再补充一点

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

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

        当用户名错误时,请确保不要立即返回答案。

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

        【讨论】:

        • 顺便说一句,以前的答案stackoverflow.com/a/57508528/11603057 不正确且有害。这是散列的一个例子,而不是密码散列。必须是密钥推导过程中伪随机函数的迭代。没有。我不能评论它或投反对票(我的声誉很低)。请不要错过不正确的答案!
        • 您似乎很了解加密货币。因此,同时提供加密和验证例程会非常好 - 这样加密技能较少的人(比如我)就可以使用您的安全提议。我已经查看了您指向 MS 的链接 - 不幸的是,他们也没有提供验证方法。
        • @John Ranger。 1. 注册您的用户。 1.1 获取他的用户名(或电子邮件)、密码和重复密码字段。 1.2 如果密码 == 重复密码,则从密码中进行哈希处理。 1.3.保留散列值和用户名。 2. 验证。 2.1 获取用户密码和用户名。 2.2 对其进行哈希处理。 2.3 检索由他的用户名存储的散列。 2.4 如果存储散列 == 当前散列 - 如果不是 OK,则 OK。就是这样。
        • 如果用户名不存在并且没有可检索的内容会在返回 NOT OK 之前稍有延迟。并且永远不要告诉用户出了什么问题:用户名不存在或存储散列!=当前散列,只是一般错误。 N次重试后考虑锁定用户K分钟以防止暴力攻击。
        【解决方案9】:

        首先使用下面的类来生成盐。每个用户需要有不同的盐,我们可以将它与其他用户属性一起保存在数据库中。 rounds 值决定了密码被散列的次数。

        更多详情:https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.rfc2898derivebytes.-ctor?view=netcore-3.1#System_Security_Cryptography_Rfc2898DeriveBytes__ctor_System_Byte___System_Byte___System_Int32_

        public class HashSaltWithRounds
        {
            int saltLength = 32;
            public byte[] GenerateSalt()
            {
                using (var randomNumberGenerator = new RNGCryptoServiceProvider())
                {
                    var randomNumber = new byte[saltLength];
                    randomNumberGenerator.GetBytes(randomNumber);
                    return randomNumber;
                }
            }
        
            public string HashDataWithRounds(byte[] password, byte[] salt, int rounds)
            {
                using(var rfc2898= new Rfc2898DeriveBytes(password, salt, rounds))
                {
                    return Convert.ToBase64String(rfc2898.GetBytes(32));
                }
            }
        }
        

        我们可以从控制台应用程序中调用它,如下所示。我使用相同的盐对密码进行了两次哈希处理。

        public class Program
        {
            public static void Main(string[] args)
            {
                int numberOfIterations = 99;
                var hashFunction = new HashSaltWithRounds();
        
                string password = "Your Password Here";
                byte[] salt = hashFunction.GenerateSalt();
        
                var hashedPassword1 = hashFunction.HashDataWithRounds(Encoding.UTF8.GetBytes(password), salt, numberOfIterations);
                var hashedPassword2 = hashFunction.HashDataWithRounds(Encoding.UTF8.GetBytes(password), salt, numberOfIterations);
        
                Console.WriteLine($"hashedPassword1 :{hashedPassword1}");
                Console.WriteLine($"hashedPassword2 :{hashedPassword2}");
                Console.WriteLine(hashedPassword1.Equals(hashedPassword2));
        
                Console.ReadLine();
        
            }
        }
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2021-09-26
          • 2017-02-09
          • 2015-12-02
          • 2019-03-19
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2019-02-15
          相关资源
          最近更新 更多