在插入数据库之前散列密码

信息安全 密码学 哈希 。网
2021-09-08 15:01:42

在过去的两周里,我阅读了许多关于网站安全和散列密码的博客。

许多网站都提到了不同方法的优缺点,这让我对我的代码的安全性有点困惑。

如果可能的话,任何人都可以看看下面的代码,让我知道他们的想法。我已经在https://codereview.stackexchange.com/questions/30814/hashing-passwords-for-website-is-this-secure上发布了这个问题,但到目前为止,还没有人回复。

我采取的步骤如下:

  1. 创建随机盐
  2. 将随机盐和电子邮件一起添加以创建密码盐(电子邮件将在数据库中加密)
  3. 哈希密码和盐并存储在数据库中

    public const int HashBytes = 128;
    public const int DefaultIterations = 10000;
    
        //Create random bytes for salt
        public static string SaltSHA256()
        {
            const int minSaltSize = 8;
            const int maxSaltSize = 16;
            var random = new Random();
            int saltSize = random.Next(minSaltSize, maxSaltSize);
            byte[] saltBytes = new byte[saltSize];
            var rng = new RNGCryptoServiceProvider();
            rng.GetNonZeroBytes(saltBytes);
            HashAlgorithm hash = new SHA256Managed();
            byte[] bytes = hash.ComputeHash(saltBytes);
            return Convert.ToBase64String(bytes);
        }
    
        //Create salt using email and public static string SaltSHA256()
        //Store email and public static string SaltSHA256() in database
        public static string SaltRfc2898(string email,string hashedSalt)
        {
            var salt = new Rfc2898DeriveBytes(email, Encoding.UTF8.GetBytes(hashedSalt), DefaultIterations);
            return Convert.ToBase64String(salt.GetBytes(HashBytes));
        }
    
        //Hash password and salt
        public static string PasswordHashRfc2898(string password,string salt)
        {
            var hashedPassword = new Rfc2898DeriveBytes(password, Encoding.UTF8.GetBytes(salt), DefaultIterations);
            return Convert.ToBase64String(hashedPassword.GetBytes(HashBytes));
        }
        //Get salt and password from database based on username
        //Salt in from data created by public static string SaltSHA256()
    
        public static bool DoPasswordsMatch(string password,string salt)
        {
            //Password would be pulled from db based on username
            byte[] testPassword = Convert.FromBase64String("basestring");
    
            //Salt would be pulled from database based on username
            var saltFromDatabase = salt;
    
            //Hash password and salt
            var hashUserInputAndSalt = PasswordHashRfc2898(password, saltFromDatabase);
    
            //Convert to byte[] ready for comparison
            byte[] convertUserInputFromBase64 = Convert.FromBase64String(hashUserInputAndSalt);
    
            //Compare and return true or false
            return convertUserInputFromBase64.SequenceEqual(testPassword);
    
        }
    
1个回答

您正在使用Rfc2898DeriveBytes,它实现了PBKDF2:这很好。

您正在产生一个 128字节的输出,这对于密码验证来说完全是多余的。128,即 16 字节,就足够了。128 字节并没有什么坏处,只是它们会使更大的字符串存储在您的数据库中。

您正在使用 10000 次迭代,这可能有点低。越高越好。10000 还不错,但很有可能您的服务器可以处理十倍的数量,这将使攻击难度增加十倍。我建议您尝试几个值,并将计数设置为与预期工作负载(即每秒连接用户数)相关的系统可容忍的最高值。

编码问题最好在存储级别单独保存。salt、P​​BKDF2 输出和密码都是字节序列。Rfc2898DeriveBytes可以为你编码一个String密码,但期望盐是“一些字节”,并产生一个“一些字节”的输出。为了存储到数据库中,要么使用该数据库的能力来处理二进制数据,要么应用一些编码,比如Base64所以,分别在写之前和读之后使用Convert.FromBase64String()和。Convert.ToBase64String()您的 Base64 和 UTF-8 的混合令人困惑,使阅读(审计)您的代码成为一项更难的任务。

你的盐生成很奇怪:

  • 使盐长度可变是没有意义的。只需 16 个字节。
  • 避免值为 0 的字节是没有意义的。盐是字节序列,而不是字符序列,并且 0 并不特殊。
  • 散列盐没有意义。

PBKDF2 的适当盐是至少 8 个字节的序列,并且是唯一的(尽可能)。用强 PRNG的输出填充 16 个字节是一个很好的方法。GUID也可以工作(盐不必是随机的,它们只需要被正常系统尽可能少地重用,GUID 就是这样做的)。在 .NET 中,如果您想生成好的盐,只需执行以下操作:

byte[] salt = Guid.NewGuid().ToByteArray();

或者,Rfc2898DeriveBytes可以为您生成盐。用户注册或密码更改后,请执行以下操作:

Rfc2898DeriveBytes h = new Rfc2898DeriveBytes(password, saltSize, iterations);
byte[] hashedPassword = h.GetBytes(hashSize);
byte[] salt = h.Salt;
// then store salt and hashedPassword in the database

并且,经核实:

byte[] salt = ... // read from database
Rfc2898DeriveBytes h = new Rfc2898DeriveBytes(password, salt, iterations);
byte[] hashedPassword = h.GetBytes(hashSize);
// compare hashedPassword with the stored value

可以调用一个单独的 PRNGRNGCryptoServiceProvider()并自己做事;这就是你所做的,虽然它很奇怪,但它并不但是,使用系统已经提供的内容更简单。更简单的代码通常意味着更少的错误。