C#中的哈希和盐密码


178

我刚刚浏览了DavidHayden的有关散列用户密码的文章之一。

真的我无法理解他想要达到的目标。

这是他的代码:

private static string CreateSalt(int size)
{
    //Generate a cryptographic random number.
    RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
    byte[] buff = new byte[size];
    rng.GetBytes(buff);

    // Return a Base64 string representation of the random number.
    return Convert.ToBase64String(buff);
}

private static string CreatePasswordHash(string pwd, string salt)
{
    string saltAndPwd = String.Concat(pwd, salt);
    string hashedPwd =
        FormsAuthentication.HashPasswordForStoringInConfigFile(
        saltAndPwd, "sha1");
    return hashedPwd;
}

还有其他C#方法用于哈希密码并为其添加盐吗?


这里要说的是不与盐散列库encrypto.codeplex.com
雄武

6
在第一种生成盐的方法中,您应该传递什么尺寸?
Shane LeBlanc

6
链接断开。
osmanraifgunes 2014年

@ShaneLeBlanc您至少应具有has函数输出的位数。SHA1不是加密级别的,因此您至少应使用SHA256输出256位或32字节的。但是,256位不容易转换为base 64,因为每个base64 char编码为6位,并且256不能被6整除。因此,您需要一个公共分母6(对于base64)和8(对于一个字节中的位)超过256位,即264位或33个字节。TLDR:使用33
VSO

Answers:


248

实际上,对于字符串转换来说,这有点奇怪-成员资格提供程序所做的就是将其放入配置文件中。哈希和盐是二进制Blob,除非要将它们放入文本文件,否则无需将它们转换为字符串。

在我的《Beginning ASP.NET Security》一书中,(最后,借口是这本书的借口),我执行以下操作

static byte[] GenerateSaltedHash(byte[] plainText, byte[] salt)
{
  HashAlgorithm algorithm = new SHA256Managed();

  byte[] plainTextWithSaltBytes = 
    new byte[plainText.Length + salt.Length];

  for (int i = 0; i < plainText.Length; i++)
  {
    plainTextWithSaltBytes[i] = plainText[i];
  }
  for (int i = 0; i < salt.Length; i++)
  {
    plainTextWithSaltBytes[plainText.Length + i] = salt[i];
  }

  return algorithm.ComputeHash(plainTextWithSaltBytes);            
}

盐的生成就是这个问题的例子。您可以使用将文本转换为字节数组Encoding.UTF8.GetBytes(string)。如果必须将哈希转换为其字符串表示形式,则可以使用Convert.ToBase64String并将Convert.FromBase64String其转换回。

您应该注意,不能在字节数组上使用相等运算符,它会检查引用,因此您应该简单地遍历两个数组,检查每个字节,从而

public static bool CompareByteArrays(byte[] array1, byte[] array2)
{
  if (array1.Length != array2.Length)
  {
    return false;
  }

  for (int i = 0; i < array1.Length; i++)
  {
    if (array1[i] != array2[i])
    {
      return false;
    }
  }

  return true;
}

始终为每个密码使用新的盐。盐不必保密,可以与哈希表本身一起存储。


3
感谢您的建议-确实帮助我入门。我还遇到了这个链接< dijksterhuis.org/creating-salted-hash-values-in-c >,我发现这是一个很好的实用建议,并反映了本文中的大部分内容
Alex P 2010年

18
漂亮的LINQ语句重构CompareByteArrays return array1.Length == array2.Length && !array1.Where((t, i) => t != array2[i]).Any();
猎人

6
@Brettski从技术上讲,是的,但是为每个用户设置一个唯一的盐会使Rainbow Tables(通常被认为是破解哈希密码的最有效方法)几乎没有用。快速浏览概述了如何安全地存储密码以及为什么/如何正常工作的深入但全面的概述。
游侠2012年

3
@hunter:您应该添加一个.ToList()以使其保持恒定时间。例如:return array1.Length == array2.Length &&!array1.Where((t,i)=> t!= array2 [i])。ToList()。Any(); 否则,LINQ将在发现一个不相等的字节后立即返回。
Alex Rouillard

17
-1用于使用快速哈希函数。使用慢速构造,例如PBKDF2,bcrypt或scrypt。
CodesInChaos

48

布拉德特所说的话,但代码少了一点。使用Linq或CopyTo串联数组。

public static byte[] Hash(string value, byte[] salt)
{
    return Hash(Encoding.UTF8.GetBytes(value), salt);
}

public static byte[] Hash(byte[] value, byte[] salt)
{
    byte[] saltedValue = value.Concat(salt).ToArray();
    // Alternatively use CopyTo.
    //var saltedValue = new byte[value.Length + salt.Length];
    //value.CopyTo(saltedValue, 0);
    //salt.CopyTo(saltedValue, value.Length);

    return new SHA256Managed().ComputeHash(saltedValue);
}

Linq也有一种比较字节数组的简单方法。

public bool ConfirmPassword(string password)
{
    byte[] passwordHash = Hash(password, _passwordSalt);

    return _passwordHash.SequenceEqual(passwordHash);
}

但是,在执行任何此操作之前,请查看这篇文章。对于密码哈希,您可能需要慢速哈希算法,而不是快速算法。

为此目的,有一Rfc2898DeriveBytes类很慢的类(可以变得更慢),并且可以回答原始问题的第二部分,因为它可以使用密码和密码并返回哈希。有关更多信息,请参见此问题。注意,Stack Exchange正在使用Rfc2898DeriveBytes密码哈希(此处为源代码)。


6
@MushinNoShin SHA256是一个快速哈希。密码哈希需要慢速哈希,例如PBKDF2,bcrypt或scrypt。请参阅如何安全地对密码进行哈希处理?在security.se上了解详细信息。
CodesInChaos

32

我一直在阅读,诸如SHA256之类的哈希函数并不是真正用于存储密码:https : //patrickmn.com/security/storing-passwords-securely/#notpasswordhashes

取而代之的是自适应密钥派生功能,例如PBKDF2,bcrypt或scrypt。这是基于PBKDF2的,Microsoft 在其Microsoft.AspNet.Identity库中为PasswordHasher编写的:

/* =======================
 * HASHED PASSWORD FORMATS
 * =======================
 * 
 * Version 3:
 * PBKDF2 with HMAC-SHA256, 128-bit salt, 256-bit subkey, 10000 iterations.
 * Format: { 0x01, prf (UInt32), iter count (UInt32), salt length (UInt32), salt, subkey }
 * (All UInt32s are stored big-endian.)
 */

public string HashPassword(string password)
{
    var prf = KeyDerivationPrf.HMACSHA256;
    var rng = RandomNumberGenerator.Create();
    const int iterCount = 10000;
    const int saltSize = 128 / 8;
    const int numBytesRequested = 256 / 8;

    // Produce a version 3 (see comment above) text hash.
    var salt = new byte[saltSize];
    rng.GetBytes(salt);
    var subkey = KeyDerivation.Pbkdf2(password, salt, prf, iterCount, numBytesRequested);

    var outputBytes = new byte[13 + salt.Length + subkey.Length];
    outputBytes[0] = 0x01; // format marker
    WriteNetworkByteOrder(outputBytes, 1, (uint)prf);
    WriteNetworkByteOrder(outputBytes, 5, iterCount);
    WriteNetworkByteOrder(outputBytes, 9, saltSize);
    Buffer.BlockCopy(salt, 0, outputBytes, 13, salt.Length);
    Buffer.BlockCopy(subkey, 0, outputBytes, 13 + saltSize, subkey.Length);
    return Convert.ToBase64String(outputBytes);
}

public bool VerifyHashedPassword(string hashedPassword, string providedPassword)
{
    var decodedHashedPassword = Convert.FromBase64String(hashedPassword);

    // Wrong version
    if (decodedHashedPassword[0] != 0x01)
        return false;

    // Read header information
    var prf = (KeyDerivationPrf)ReadNetworkByteOrder(decodedHashedPassword, 1);
    var iterCount = (int)ReadNetworkByteOrder(decodedHashedPassword, 5);
    var saltLength = (int)ReadNetworkByteOrder(decodedHashedPassword, 9);

    // Read the salt: must be >= 128 bits
    if (saltLength < 128 / 8)
    {
        return false;
    }
    var salt = new byte[saltLength];
    Buffer.BlockCopy(decodedHashedPassword, 13, salt, 0, salt.Length);

    // Read the subkey (the rest of the payload): must be >= 128 bits
    var subkeyLength = decodedHashedPassword.Length - 13 - salt.Length;
    if (subkeyLength < 128 / 8)
    {
        return false;
    }
    var expectedSubkey = new byte[subkeyLength];
    Buffer.BlockCopy(decodedHashedPassword, 13 + salt.Length, expectedSubkey, 0, expectedSubkey.Length);

    // Hash the incoming password and verify it
    var actualSubkey = KeyDerivation.Pbkdf2(providedPassword, salt, prf, iterCount, subkeyLength);
    return actualSubkey.SequenceEqual(expectedSubkey);
}

private static void WriteNetworkByteOrder(byte[] buffer, int offset, uint value)
{
    buffer[offset + 0] = (byte)(value >> 24);
    buffer[offset + 1] = (byte)(value >> 16);
    buffer[offset + 2] = (byte)(value >> 8);
    buffer[offset + 3] = (byte)(value >> 0);
}

private static uint ReadNetworkByteOrder(byte[] buffer, int offset)
{
    return ((uint)(buffer[offset + 0]) << 24)
        | ((uint)(buffer[offset + 1]) << 16)
        | ((uint)(buffer[offset + 2]) << 8)
        | ((uint)(buffer[offset + 3]));
}

请注意,这需要安装Microsoft.AspNetCore.Cryptography.KeyDerivation nuget软件包,该软件包需要.NET Standard 2.0(.NET 4.6.1或更高版本)。有关.NET的早期版本,请参阅Microsoft的System.Web.Helpers库中的Crypto类。

2015年11月
更新,更新了答案,以使用来自另一个Microsoft库的实现,该库使用PBKDF2-HMAC-SHA256哈希代替PBKDF2-HMAC-SHA1(请注意,如果iterCount足够高,则PBKDF2-HMAC-SHA1 仍然安全的)。您可以检查出从中复制了简化代码的,因为它实际上处理了根据先前答案实现的验证和升级哈希,如果将来需要增加iterCount,则很有用。


1
请注意,可能值得将PBKDF2IterCount增加到一个更大的数字,有关更多信息,请参见security.stackexchange.com/q/3959
迈克尔

2
1)减少PBKDF2SubkeyLength到20个字节。这就是SHA1的自然大小,将其增加到SHA1的自然大小会减慢防御者的速度,而不会减慢攻击者的速度。2)我建议增加迭代次数。我建议根据性能预算为1万到10万。3)持续进行时间比较也不会有任何伤害,但不会产生太大的实际影响。
CodesInChaos

KeyDerivationPrf,KeyDerivation和BlockCopy未定义,它们的类是什么?
mrbengi '16

@mrbengi是否已安装提到的Microsoft.AspNet.Cryptography.KeyDerivation nuget软件包?如果这是不适合在这里是不需要NuGet包版本。Buffer.BlockCopy应该存在,它是系统的一部分。
迈克尔

1
nuget包现在是Microsoft.AspNetCore.Cryptography.KeyDerivation。
詹姆斯·布雷克

25

盐用于为哈希增加额外的复杂性,以使其更难以进行暴力破解。

有关Sitepoint文章中

黑客仍然可以执行所谓的字典攻击。恶意方可以通过以下方式来进行字典攻击:例如,使用他们知道人们经常使用的100,000个密码(例如城市名称,运动队等),对它们进行哈希处理,然后将字典中的每个条目与数据库中的每一行进行比较表。如果黑客找到匹配的物品,请宾果游戏!他们有您的密码。但是,要解决此问题,我们只需要对哈希加盐即可。

为了给哈希加盐,我们只需简单地找到一个看起来随机的文本字符串,将其与用户提供的密码连接起来,然后将随机生成的字符串和密码都哈希为一个值。然后,我们将哈希表和盐值另存为Users表中的单独字段。

在这种情况下,黑客不仅需要猜测密码,还必须猜测密码。在明文中添加盐可以提高安全性:现在,如果黑客尝试进行字典攻击,则他必须用每个用户行的盐对100,000个条目进行哈希处理。尽管仍然有可能,但成功破解黑客的机会却从根本上减少了。

.NET中没有自动执行此操作的方法,因此您将拥有上述解决方案。


盐用于防御彩虹桌子等东西。为了防御字典攻击,像任何好的KDF一样,都需要一个工作因素(也称为密钥扩展):en.wikipedia.org/wiki/Key_stretching
Erwan Legrand

11

我创建了一个具有以下方法的类:

  1. 创造盐
  2. 哈希输入
  3. 验证输入

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

    `



3

我制作了一个库SimpleHashing.Net,以使Microsoft提供的基本类的哈希处理过程变得容易。普通的SHA不足以安全地存储密码。

该库使用来自Bcrypt的哈希格式的想法,但是由于没有正式的MS实现,我更喜欢使用框架中可用的东西(即PBKDF2),但是开箱即用有点困难。

这是一个如何使用库的快速示例:

ISimpleHash simpleHash = new SimpleHash();

// Creating a user hash, hashedPassword can be stored in a database
// hashedPassword contains the number of iterations and salt inside it similar to bcrypt format
string hashedPassword = simpleHash.Compute("Password123");

// Validating user's password by first loading it from database by username
string storedHash = _repository.GetUserPasswordHash(username);
isPasswordValid = simpleHash.Verify("Password123", storedHash);

2

这就是我的方法。.我创建哈希并使用ProtectedDataapi 存储它:

    public static string GenerateKeyHash(string Password)
    {
        if (string.IsNullOrEmpty(Password)) return null;
        if (Password.Length < 1) return null;

        byte[] salt = new byte[20];
        byte[] key = new byte[20];
        byte[] ret = new byte[40];

        try
        {
            using (RNGCryptoServiceProvider randomBytes = new RNGCryptoServiceProvider())
            {
                randomBytes.GetBytes(salt);

                using (var hashBytes = new Rfc2898DeriveBytes(Password, salt, 10000))
                {
                    key = hashBytes.GetBytes(20);
                    Buffer.BlockCopy(salt, 0, ret, 0, 20);
                    Buffer.BlockCopy(key, 0, ret, 20, 20);
                }
            }
            // returns salt/key pair
            return Convert.ToBase64String(ret);
        }
        finally
        {
            if (salt != null)
                Array.Clear(salt, 0, salt.Length);
            if (key != null)
                Array.Clear(key, 0, key.Length);
            if (ret != null)
                Array.Clear(ret, 0, ret.Length);
        } 
    }

    public static bool ComparePasswords(string PasswordHash, string Password)
    {
        if (string.IsNullOrEmpty(PasswordHash) || string.IsNullOrEmpty(Password)) return false;
        if (PasswordHash.Length < 40 || Password.Length < 1) return false;

        byte[] salt = new byte[20];
        byte[] key = new byte[20];
        byte[] hash = Convert.FromBase64String(PasswordHash);

        try
        {
            Buffer.BlockCopy(hash, 0, salt, 0, 20);
            Buffer.BlockCopy(hash, 20, key, 0, 20);

            using (var hashBytes = new Rfc2898DeriveBytes(Password, salt, 10000))
            {
                byte[] newKey = hashBytes.GetBytes(20);

                if (newKey != null)
                    if (newKey.SequenceEqual(key))
                        return true;
            }
            return false;
        }
        finally
        {
            if (salt != null)
                Array.Clear(salt, 0, salt.Length);
            if (key != null)
                Array.Clear(key, 0, key.Length);
            if (hash != null)
                Array.Clear(hash, 0, hash.Length);
        }
    }

    public static byte[] DecryptData(string Data, byte[] Salt)
    {
        if (string.IsNullOrEmpty(Data)) return null;

        byte[] btData = Convert.FromBase64String(Data);

        try
        {
            return ProtectedData.Unprotect(btData, Salt, DataProtectionScope.CurrentUser);
        }
        finally
        {
            if (btData != null)
                Array.Clear(btData, 0, btData.Length);
        }
    }

    public static string EncryptData(byte[] Data, byte[] Salt)
    {
        if (Data == null) return null;
        if (Data.Length < 1) return null;

        byte[] buffer = new byte[Data.Length];

        try
        {
            Buffer.BlockCopy(Data, 0, buffer, 0, Data.Length);
            return System.Convert.ToBase64String(ProtectedData.Protect(buffer, Salt, DataProtectionScope.CurrentUser));
        }
        finally
        {
            if (buffer != null)
                Array.Clear(buffer, 0, buffer.Length);
        }
    }

保存时以及以后进行比较时如何称呼它?
SearchForKnowledge,2015年

2

我阅读了所有答案,我认为这些答案就足够了,特别是@Michael的哈希缓慢的文章和@CodesInChaos好的评论,但是我决定分享我的代码片段以进行哈希/验证,这可能是有用的,并且不需要[ Microsoft.AspNet.Cryptography [.KeyDerivation ]。

    private static bool SlowEquals(byte[] a, byte[] b)
            {
                uint diff = (uint)a.Length ^ (uint)b.Length;
                for (int i = 0; i < a.Length && i < b.Length; i++)
                    diff |= (uint)(a[i] ^ b[i]);
                return diff == 0;
            }

    private static byte[] PBKDF2(string password, byte[] salt, int iterations, int outputBytes)
            {
                Rfc2898DeriveBytes pbkdf2 = new Rfc2898DeriveBytes(password, salt);
                pbkdf2.IterationCount = iterations;
                return pbkdf2.GetBytes(outputBytes);
            }

    private static string CreateHash(string value, int salt_bytes, int hash_bytes, int pbkdf2_iterations)
            {
                // Generate a random salt
                RNGCryptoServiceProvider csprng = new RNGCryptoServiceProvider();
                byte[] salt = new byte[salt_bytes];
                csprng.GetBytes(salt);

                // Hash the value and encode the parameters
                byte[] hash = PBKDF2(value, salt, pbkdf2_iterations, hash_bytes);

                //You need to return the salt value too for the validation process
                return Convert.ToBase64String(hash) + ":" + 
                       Convert.ToBase64String(hash);
            }

    private static bool ValidateHash(string pureVal, string saltVal, string hashVal, int pbkdf2_iterations)
            {
                try
                {
                    byte[] salt = Convert.FromBase64String(saltVal);
                    byte[] hash = Convert.FromBase64String(hashVal);

                    byte[] testHash = PBKDF2(pureVal, salt, pbkdf2_iterations, hash.Length);
                    return SlowEquals(hash, testHash);
                }
                catch (Exception ex)
                {
                    return false;
                }
            }

请注意非常重要的SlowEquals函数,最后,希望对您有所帮助,请不要犹豫,向我建议更好的方法。


为什么不设置人为的非繁忙延迟,而不是造成繁忙循环。例如使用Task.Delay。这将延迟蛮力尝试,但不会阻塞活动线程。
gburton

@gburton感谢您的建议。我会检查它。
QMaster

CreateHash中有一个错字:您正在将Convert.ToBase64String(hash)与其本身(而不是盐)并发。除此之外,这是一个不错的答案,几乎解决了其他答案的评论中提出的每个问题。
ZeRemz

2

使用System.Web.Helpers.CryptoMicrosoft 的NuGet软件包。它会自动将盐添加到哈希中。

您可以像这样对密码进行哈希处理: var hash = Crypto.HashPassword("foo");

您可以像这样验证密码: var verified = Crypto.VerifyHashedPassword(hash, "foo");


1

如果您不使用asp.net或.net core,在> = .Net Standard 2.0项目中也有一种简便的方法。

首先,您可以设置所需的散列,盐和迭代数的大小,该大小与散列生成的持续时间有关:

private const int SaltSize = 32;
private const int HashSize = 32;
private const int IterationCount = 10000;

要产生密码哈希值和盐值,您可以使用以下代码:

public static string GeneratePasswordHash(string password, out string salt)
{
    using (Rfc2898DeriveBytes rfc2898DeriveBytes = new Rfc2898DeriveBytes(password, SaltSize))
    {
        rfc2898DeriveBytes.IterationCount = IterationCount;
        byte[] hashData = rfc2898DeriveBytes.GetBytes(HashSize);
        byte[] saltData = rfc2898DeriveBytes.Salt;
        salt = Convert.ToBase64String(saltData);
        return Convert.ToBase64String(hashData);
    }
}

要验证用户输入的密码是否有效,可以检查数据库中的值:

public static bool VerifyPassword(string password, string passwordHash, string salt)
{
    using (Rfc2898DeriveBytes rfc2898DeriveBytes = new Rfc2898DeriveBytes(password, SaltSize))
    {
        rfc2898DeriveBytes.IterationCount = IterationCount;
        rfc2898DeriveBytes.Salt = Convert.FromBase64String(salt);
        byte[] hashData = rfc2898DeriveBytes.GetBytes(HashSize);
        return Convert.ToBase64String(hashData) == passwordHash;
    }
}

以下单元测试显示了用法:

string password = "MySecret";

string passwordHash = PasswordHasher.GeneratePasswordHash(password, out string salt);

Assert.True(PasswordHasher.VerifyPassword(password, passwordHash, salt));
Assert.False(PasswordHasher.VerifyPassword(password.ToUpper(), passwordHash, salt));

Microsoft Rfc2898DeriveBytes源


-1

回答原始问题的这一部分“是否还有其他用于散列密码的C#方法”,您可以使用ASP.NET Identity v3.0 https://www.nuget.org/packages/Microsoft.AspNet.Identity来实现。 EntityFramework / 3.0.0-rc1-final

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNet.Identity;
using System.Security.Principal;

namespace HashTest{


    class Program
    {
        static void Main(string[] args)
        {

            WindowsIdentity wi = WindowsIdentity.GetCurrent();

            var ph = new PasswordHasher<WindowsIdentity>();

            Console.WriteLine(ph.HashPassword(wi,"test"));

            Console.WriteLine(ph.VerifyHashedPassword(wi,"AQAAAAEAACcQAAAAEA5S5X7dmbx/NzTk6ixCX+bi8zbKqBUjBhID3Dg1teh+TRZMkAy3CZC5yIfbLqwk2A==","test"));

        }
    }


}

-1
 protected void m_GenerateSHA256_Button1_Click(objectSender, EventArgs e)
{
string salt =createSalt(10);
string hashedPassword=GenerateSHA256Hash(m_UserInput_TextBox.Text,Salt);
m_SaltHash_TextBox.Text=Salt;
 m_SaltSHA256Hash_TextBox.Text=hashedPassword;

}
 public string createSalt(int size)
{
 var rng= new System.Security.Cyptography.RNGCyptoServiceProvider();
 var buff= new byte[size];
rng.GetBytes(buff);
 return Convert.ToBase64String(buff);
}


 public string GenerateSHA256Hash(string input,string salt)
{
 byte[]bytes=System.Text.Encoding.UTF8.GetBytes(input+salt);
 new System.Security.Cyptography.SHA256Managed();
 byte[]hash=sha256hashString.ComputedHash(bytes);
 return bytesArrayToHexString(hash);
  }

其他方法是字符串password = HashPasswordForStoringInConfigFile(TextBox1.Text,SHA1)
Ankush shukla

-6
create proc [dbo].[hash_pass] @family nvarchar(50), @username nvarchar(50), @pass nvarchar(Max),``` @semat nvarchar(50), @tell nvarchar(50)

as insert into tbl_karbar values (@family,@username,(select HASHBYTES('SHA1' ,@pass)),@semat,@tell)
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.