如何正确存储PBKDF2密码哈希


How to properly store a PBKDF2 password hash

我一直在研究哈希/加密密码并将其存储在数据库中的正确方法。我知道Salt和Hashing,所以我环顾四周,PBKDF2似乎是一个不错的选择。因此,我发现这个网站提供了一个很好的教程,并将PBKDF2改编为PHP(这就是我的网站所使用的)。

因此,我已经设置了我的网站,使用这些功能来生成/创建密码,但正如您在以下代码中看到的:

function create_hash($password) {
// format: algorithm:iterations:salt:hash
$salt = base64_encode(mcrypt_create_iv(PBKDF2_SALT_BYTES, MCRYPT_DEV_URANDOM));
return PBKDF2_HASH_ALGORITHM . ":" . PBKDF2_ITERATIONS . ":" .  $salt . ":" .
    base64_encode(pbkdf2(
        PBKDF2_HASH_ALGORITHM,
        $password,
        $salt,
        PBKDF2_ITERATIONS,
        PBKDF2_HASH_BYTES,
        true
    )); }

salt在create_hash函数中生成,并存储在生成的哈希中,最终看起来像sha256:1000:salt:hashed_password。这是我必须存储在数据库中的内容,由于salt包含在生成的哈希中,所以我不需要将其添加到数据库中。然而,在用这个生成了一些测试用户之后,我想知道在数据库中的散列密码中包含PBKDF2设置是否真的是一件好事。他们就像我的新手自己认为的那样,是一个黑客,在破解我的数据库后,会看到这些sha256:1000:salt:密码的东西,并弄清楚每个部分代表什么,这将对他的尝试有很大帮助,不是吗?

因此,我对它进行了一些修改,使其具有一个外部salt,我生成该salt并将其存储在数据库中,并在通过PBKDF2运行它之前将其包含在密码中。然后,我做同样的事情,将给定的密码与我数据库中的登录密码进行比较,它就起作用了。我唯一担心的是,一个有128位salt的密码散列只有50个字符长,这对我来说似乎不对

这是我当前的代码:

define("PBKDF2_HASH_ALGORITHM", "sha256");
define("PBKDF2_ITERATIONS", 10000);
define("PBKDF2_SALT_BYTES", 128);
define("PBKDF2_HASH_BYTES", 24);
define("HASH_SECTIONS", 4);
define("HASH_ALGORITHM_INDEX", 0);
define("HASH_ITERATION_INDEX", 1);
define("HASH_SALT_INDEX", 2);
define("HASH_PBKDF2_INDEX", 3);
function create_hash($password, $salt)
{
    // format: salthash
    return  
        base64_encode(pbkdf2(
            PBKDF2_HASH_ALGORITHM,
            $password,
            $salt,
            PBKDF2_ITERATIONS,
            PBKDF2_HASH_BYTES,
            true
        ));
}
function validate_password($password, $salt, $good_hash)
{
    $pbkdf2 = base64_decode($good_hash);
    return slow_equals(
        $pbkdf2,
        pbkdf2(
            PBKDF2_HASH_ALGORITHM,
            $password,
            $salt,
            PBKDF2_ITERATIONS,
            PBKDF2_HASH_BYTES,
            true
        )
    );
}
// Compares two strings $a and $b in length-constant time.
function slow_equals($a, $b)
{
    $diff = strlen($a) ^ strlen($b);
    for($i = 0; $i < strlen($a) && $i < strlen($b); $i++)
    {
        $diff |= ord($a[$i]) ^ ord($b[$i]);
    }
    return $diff === 0; 
}
function pbkdf2($algorithm, $password, $salt, $count, $key_length, $raw_output = false)
{
    $algorithm = strtolower($algorithm);
    if(!in_array($algorithm, hash_algos(), true))
        die('PBKDF2 ERROR: Invalid hash algorithm.');
    if($count <= 0 || $key_length <= 0)
        die('PBKDF2 ERROR: Invalid parameters.');
    $hash_length = strlen(hash($algorithm, "", true));
    $block_count = ceil($key_length / $hash_length);
    $output = "";
    for($i = 1; $i <= $block_count; $i++) {
        // $i encoded as 4 bytes, big endian.
        $last = $salt . pack("N", $i);
        // first iteration
        $last = $xorsum = hash_hmac($algorithm, $last, $password, true);
        // perform the other $count - 1 iterations
        for ($j = 1; $j < $count; $j++) {
            $xorsum ^= ($last = hash_hmac($algorithm, $last, $password, true));
        }
        $output .= $xorsum;
    }
    if($raw_output)
        return substr($output, 0, $key_length);
    else
        return bin2hex(substr($output, 0, $key_length));
}

我对密码保存格式的担忧有意义吗?或者,不管怎样,这也会是一样的吗?因为只要我的数据库里有我的盐,一旦它被破解,黑客仍然可以强行通过?

谢谢,很抱歉问了这么长的问题。

我唯一担心的是一个带有128位salt的密码hash只有50个字符长,这对我来说似乎不对。

结果散列的大小与salt的大小、密码和迭代次数完全无关。现代安全哈希算法(如sha256)的输出总是相同的长度,无论输入如何。零长度输入具有与25TB输入相同的长度输出。

还是无论如何都会是一样的,因为我数据库中的salt,一旦被破解,黑客仍然可以用蛮力闯过去?

通过将salt分成两部分,您会增加代码的复杂性(通常是一件坏事)。根据你储存盐块的方式,在某些情况下你可能会获得一点好处。例如,如果静态salt片段存储在数据库之外,那么数据库的转储将不会为攻击者提供足够的信息来对数据库中的密码哈希进行离线攻击。

如果盐碎片彼此分开储存,则会获得少量的纵深防御。它是否超过复杂性成本是一个判断问题,但我想说,很有可能会更好地花时间寻找XSS和SQL注入漏洞(攻击者经常获取上述数据库转储的方式),并使用SSL和证书或强密码保护系统各组件之间的连接。

用@Romain的评论回答我自己的问题。

请参阅以下答案,以更好地理解隐藏salt在这里,使用bcrypt(因为在您的链接中没有实现给出),以及如何使用哈希来确保安全的最佳实践密码在这里。最后,永远不要低估你的对手!

此外,(因为这有点旧),bcrypt确实比pbkdf2"更好",但scrypt甚至更好!

有两件事,一是你应该使用慢哈希函数,比如bcrypt,而不是SHA256;二是你可能根本不应该存储密码(而是使用openId或类似的东西)。

也就是说,你需要为每个用户提供不同的salt,你可以将它们存储在同一行,甚至(正如你正在做的那样)同一个字段,或者完全不同的数据库中。你愿意付出多少努力实际上取决于你和你的绩效要求。

除了每个用户/密码的salt之外,您还可以考虑每个应用程序的salt,它不在任何数据库中。