这是一个好的PHP密码哈希函数吗?如果不是,为什么?

22

我在想这个函数(部分源于大约2年前的phpBB版本)是否足够好。

如果不好,为什么?
你会怎样改变它(使现有用户过渡无缝)?

hash_pwd()的结果将保存在数据库中。

function hash_pwd($password)
{
    $itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';

    $random_state = $this->unique_id();
    $random = '';
    $count = 6;

    if (($fh = @fopen('/dev/urandom', 'rb')))
    {
        $random = fread($fh, $count);
        fclose($fh);
    }

    if (strlen($random) < $count)
    {
        $random = '';

        for ($i = 0; $i < $count; $i += 16)
        {
            $random_state = md5($this->unique_id() . $random_state);
            $random .= pack('H*', md5($random_state));
        }
        $random = substr($random, 0, $count);
    }

    $hash = $this->_hash_crypt_private($password, $this->_hash_gensalt_private($random, $itoa64), $itoa64);

    if (strlen($hash) == 34)
    {
        return $hash;
    }

    return false;
}


function unique_id()
{
    $val = microtime();
    $val = md5($val);

    return substr($val, 4, 16);
}

function _hash_crypt_private($password, $setting, &$itoa64)
{
    $output = '*';

    // Check for correct hash
    if (substr($setting, 0, 3) != '$H$')
    {
        return $output;
    }

    $count_log2 = strpos($itoa64, $setting[3]);

    if ($count_log2 < 7 || $count_log2 > 30)
    {
        return $output;
    }

    $count = 1 << $count_log2;
    $salt = substr($setting, 4, 8);

    if (strlen($salt) != 8)
    {
        return $output;
    }

    /**
    * We're kind of forced to use MD5 here since it's the only
    * cryptographic primitive available in all versions of PHP
    * currently in use.  To implement our own low-level crypto
    * in PHP would result in much worse performance and
    * consequently in lower iteration counts and hashes that are
    * quicker to crack (by non-PHP code).
    */
    if (PHP_VERSION >= 5)
    {
        $hash = md5($salt . $password, true);
        do
        {
            $hash = md5($hash . $password, true);
        }
        while (--$count);
    }
    else
    {
        $hash = pack('H*', md5($salt . $password));
        do
        {
            $hash = pack('H*', md5($hash . $password));
        }
        while (--$count);
    }

    $output = substr($setting, 0, 12);
    $output .= $this->_hash_encode64($hash, 16, $itoa64);

    return $output;
}

function _hash_gensalt_private($input, &$itoa64, $iteration_count_log2 = 6)
{
    if ($iteration_count_log2 < 4 || $iteration_count_log2 > 31)
    {
        $iteration_count_log2 = 8;
    }

    $output = '$H$';
    $output .= $itoa64[min($iteration_count_log2 + ((PHP_VERSION >= 5) ? 5 : 3), 30)];
    $output .= $this->_hash_encode64($input, 6, $itoa64);

    return $output;
}

function _hash_encode64($input, $count, &$itoa64)
{
    $output = '';
    $i = 0;

    do
    {
        $value = ord($input[$i++]);
        $output .= $itoa64[$value & 0x3f];

        if ($i < $count)
        {
            $value |= ord($input[$i]) << 8;
        }

        $output .= $itoa64[($value >> 6) & 0x3f];

        if ($i++ >= $count)
        {
            break;
        }

        if ($i < $count)
        {
            $value |= ord($input[$i]) << 16;
        }

        $output .= $itoa64[($value >> 12) & 0x3f];

        if ($i++ >= $count)
        {
            break;
        }

        $output .= $itoa64[($value >> 18) & 0x3f];
    }
    while ($i < $count);

    return $output;
}

11
这个实现过于复杂和定制化了。使用一个简单、广为人知且经过充分测试的实现,比如bcrypt - ThiefMaster
2
所有关于bcrypt的内容。慢才是好的,顺便说一句。 - j_mcnally
http://www.php.net/manual/zh/function.crypt.php 4.3+ 不够好吗?如果是这样,你可能有更大的问题。 - j_mcnally
7
如果你遇到需要依赖已经编写、测试和调试过的代码而不是自己编写的编程问题,那么加密就是其中之一。 - Andy Lester
1
另外,还可以查看Openwall的PHP密码哈希框架(PHPass)。它是可移植的,并且针对用户密码的许多常见攻击进行了加固。编写该框架的人(SolarDesigner)也是编写John The Ripper并担任密码哈希竞赛的评委的人。因此,他对密码攻击有一定的了解。 - jww
显示剩余5条评论
4个回答

52
你提供的代码是PHPASS的一个移植版本,具体来说是“可移植”算法。请注意portable的限定条件。只有在将第二个构造函数参数设置为true时,才会应用于phpass库。从这里开始,在本答案中,phpass仅指可移植算法,而不是库本身。如果您没有明确指定portable,则库将默认使用bcrypt...
PHPBB团队并没有自己开发这个(这是一件好事),而是直接从phpass移植过来的(有争议)。
我们应该问几个问题:

它是不是不好的?

简短的回答是否定的,它提供了相当好的安全性。如果您现在已经在使用此代码,则不必急于更改。对于大多数用途而言,它已经足够了。但是话虽如此,如果您要开始一个新项目,我不会选择它,因为有更好的替代方案。

有哪些弱点?

相对于pbkdf2:算法phpass使用hash(),而pbkdf2()使用hash_hmac()。现在,每次调用时HMAC内部运行2个哈希,但PHP实现仅需要执行单个调用的hash()大约1.6倍的时间(C真是太棒了)。因此,我们从hash_hmac获得2个哈希,其时间为执行2个哈希所需时间的62%。

这意味着什么?对于给定的运行时pbkdf2将运行大约37.5% 更多哈希值比phpass算法。在给定时间内进行更多哈希==好,因为它会导致执行更多计算。

因此,当使用相同的基元(在这种情况下为md5)时,pbkdf2phpass强大约37.5%。但是pbkdf2也可以采用更强的基元。因此,我们可以使用sha512pbkdf2来获得比phpass算法更大的优势(主要是因为sha512是一个比md5更困难的算法,需要更多的计算)。

这意味着pbkdf2不仅能够在相同的时间内生成更多的计算,而且还能够生成更难的计算。

话虽如此,差异并不过分显着。它非常可测,pbkdf2绝对比phpass“更强大”。

  • 相对于bcrypt:这是一个更难的比较。但是让我们看看它的表面。 phpass使用md5和PHP中的循环。 pbkdf2使用任何基元(在C中)和PHP中的循环。 bcrypt使用所有C中的自定义算法(这意味着它是任何可用哈希的不同算法)。因此,bcrypt具有显着优势,仅因为算法全部在C中。这允许每个单位时间进行更多的“计算”。从而使其成为更有效的慢算法(在给定的运行时中进行更多计算)。

    但与它执行的计算数量一样重要的是计算质量。这可能是一篇完整的研究论文,但简而言之,它归结为bcrypt内部使用的计算比普通哈希函数难得多。

    bcrypt强大性的一个例子是bcrypt使用的远比普通哈希函数更大的内部状态。 SHA512使用512位内部状态对抗1024位块。相反,bcrypt使用约32kb的内部状态来计算单个576位块。 bcrypt的内部状态比SHA512(和md5phpass)大得多,部分说明了<

    是否应该避免使用bcrypt

    对于新项目,绝对需要避免使用bcrypt。不是因为它不好,实际上它并不差。而是因为有明显更强的算法存在(数量级别的)。那么为什么不使用它们呢?

    如果需要进一步证明bcrypt的强度,请查看来自Password13(PDF)的幻灯片,该演示启动了一个包含25个GPU集群的系统用于破解密码哈希。以下是相关结果:

    • md5($password)
      • 每秒180亿次猜测
      • 9.4小时 - 所有可能的8位密码
    • sha1($password)
      • 每秒61亿次猜测
      • 27小时 - 所有可能的8位密码
    • md5crypt (与成本为10的phpass非常相似):
      • 每秒7700万次猜测
      • 2.5年 - 所有可能的8位密码
    • bcrypt 成本为5
      • 每秒71000次猜测
      • 2700年 - 所有可能的8位密码

    注意: 所有可能的8位密码都使用94个字符集:

    a-zA-Z0-9~`!@#$%^&*()_+-={}|[]\:";'<>,.?/
    

    结论

    如果你正在编写新代码,毫无疑问应该使用 bcrypt。如果你现在的生产环境中使用的是 phpasspbkdf2,你可能需要升级,但这并不是一个明确的“你存在显著的漏洞”。


  • 线性预测是误导性的,计算机的能力不会停滞不前!考虑到摩尔定律,通过每次切换到更强大的机器并从上一台机器离开的地方继续破解,8位字符密码在约20年内可以被破解,只要有足够的投入。密码经不起时间的考验! - CSᵠ
    1
    @user93353:cost是bcrypt和phpass算法都接受的一个参数,用于“调整”操作的计算成本。这样,随着硬件变得更快,可以逐渐增加成本,以使其保持同样的安全性。今天,使用bcrypt的成本为10只需要不到100毫秒的运行时间。但是成本为12则需要近半秒钟。因此,通过调整成本,我们可以保持执行时间合理,同时提供额外的防御措施来抵御攻击者的暴力破解。 - ircmaxell
    @ircmaxell 嘿,安东尼,那个PDF报告的链接失效了(访问被禁止),你有副本吗?- 我需要它为大学报告。任何帮助都将不胜感激! - Rixhers Ajazi
    @RixhersAjazi:很抱歉,我不知道。我建议你去搜索一下PPT,它们在互联网上应该能找到...这是Jeremi Gosney在Passwords12上的演讲。 - ircmaxell
    找到了一个视频(甚至更好):D - Rixhers Ajazi
    显示剩余3条评论

    19

    快速回答:

    使用即将推出的bcrypt或来自ircmaxell的password_compat库 - 这是一个bcrypt库。

    长篇回答:

    对于现代技术来说,此方案过于复杂且已经过时。应避免使用Md5,仅用盐不够安全。使用bcrypt可以避免麻烦。你可能会问为什么是这个特定的库?因为相同的函数在php 5.5中也可用,所以您无需更改任何编码。祝好运,保持简单高效。另外,较慢的登录和密码处理速度也有好处。

    更新1

    @Gumbo -> MD5并没有被破解,但是像MD5这样的哈希算法的主要目的现在和过去都是用于文件检查(如你所知,用于检查文件内容是否可信,而不是用于密码存储), 因为哈希值很容易解密,所以你不会使用Bcrypt来检查文件的内容, 因为你需要等待30-45秒...... 所以这意味着哈希值专门设计成可以快速读取。即使与bcrypt相比,SHA512仍然完全不足。这就是为什么我们必须推进在PHP中使用强大的密码算法,如Blowfish/Bcrypt。我们作为用户和程序员都必须扩展知识,明确纯文本密码存储或低级哈希算法并不是答案 - 绝不能用于此类敏感信息。

    现在针对OP的问题,你需要向所有用户发送一条通知,说明“为了您的安全,密码加密系统已经更新......”,然后要求他们进行一次性密码更新,完成密码更新后,您可以使用名为password_verify的函数,并且如果您想最大限度地控制成本比率,可以使用password_needs_rehash,以便在某些情况下更改与密码相关联的成本。

    不需要大块的代码实现这个,因为密码保护的收益超过了“重新编码”新密码系统的负面影响。

    希望这可以回答大部分问题,但是IRCmaxell的回答更加出色,进一步详细介绍了不同算法!祝OP好运,并感谢ircmaxell!

    更新2

    此外,像这样存储的密码是否确实可以被破解?如何破解?(我现在很好奇)

    我的网络安全教授告诉我,任何事情都可能被破解..我当时笑了。现在我以他的眼光看事情...呃,是的..那绝对是真的!

    Bcrypt目前是存储密码的最佳方法!但是,如果您查看Scrypt,它似乎很有前途,但PHP不支持它。尽管如此,任何事情都可能被破解,只是一个“极客”在地下室破解Bcrypt的时间问题。但是现在我们是安全的..就像IPv4永远不会用完一样....等等?... ;)

    希望这个回答解决了您提出的问题。此外,为了让您更好地理解,我的系统花费17美元,登录需要约20-30秒,但是当我向我的教授和客户展示系统以及为什么应该强制使用它时,他们喜欢它并感到安心,因为他们受到了保护。


    2
    @koichirose 所有密码都是“可破解的”。真正的问题是有多容易?即使PHPBB已经混淆,但它仍然依赖于设计为运行快速的md5,而且这种实现可以很容易地并行化。即使是强密码也可以在几个小时内被一个有动力的黑客用几千美元的商品硬件暴力破解。bcrypt之所以经常被推荐,是因为它很慢,并且很难[如果不是不可能]并行化。请参见:http://security.stackexchange.com/questions/211/how-to-securely-hash-passwords - Sammitch
    实际上,MD5已经被破解了。而且已经有一段时间了。@RixhersAjazi - PeeHaa
    谢谢 @ircmaxell ,你的话对我来说意义非凡。 - Rixhers Ajazi
    是的,@RixhersAjazi,但这并不意味着它没有损坏。 - PeeHaa
    2
    @RixhersAjazi,我认为PeeHaa所指的是MD5在签名或完整性检查角色中容易受到预像和碰撞攻击的弱点。你刚提到的情况正是不应该使用md5的情况(因为它在密码学上已经破解)。 - ircmaxell
    显示剩余15条评论

    1
    我会选择bcrypt。它很好。 除了加入盐以防止彩虹表攻击外,bcrypt还是一种自适应函数:随着时间的推移,迭代计数可以增加以使其变慢,因此即使计算能力增加,它仍然具有抵抗暴力搜索攻击的能力。
    实现:如何在PHP中使用bcrypt进行哈希密码?

    -5

    首先,抱歉我的英语不太好。

    在回答问题之前,所有评论者都必须提出以下几个问题:

    这段代码将在哪里使用?

    它将保护什么类型的数据?

    它将用于关键级别系统吗?

    好的,我们有一些用例。我正在为我们的信息系统使用简化密码生成器,是的,它确实不是“第三次世界大战”准备就绪,但用户会告诉别人密码的机会,或者它将被某些恶意软件从他的计算机泄漏的可能性仍然更大。

    • md5已经不安全了,使用最新的指纹生成器(sha128、sha256、sha512)
    • 使用随机长度的盐,例如:

      private function generateSalt() {
        $salt = '';
        $i = rand(20, 40); //最小和最大长度
        for($length = 0; $length < $i; $length++) {
          $salt .= chr(rand(33, 126));
        }        
        return $salt;
      }
      
    • 然后生成一些密码:

      private function generatePass() {
        $vocabulary = 'abcdefghijklmnopqrstuvwxyz0123456789';
        $password = '';
        $i = rand(7,10);
        for($length = 0; $length < $i; $length++) {
            $password .= substr($vocabulary,rand(0, 35),1);
        }        
        return $password.'-'; // 避免复制粘贴 :)
      }
      
    • 是的,密码应该更强大,但用户永远不会记住它,它将保存在浏览器中或写在某个地方。期望和安全性与现实之间的平衡。

      $newSalt = $this->generateSalt();
      $newPass = $this->generatePass();
      $newHash = hash('sha512', $newPass.':'.$newSalt); 
      // 现在将哈希和盐存储到数据库中
      
    • 有了新的哈希,但这还不够,真正的工作才刚刚开始:

      1. 日志记录
      2. 会话保护
      3. 用户、用户+IP、IP临时/永久封禁
      4. 等等

    登录+注销+重新/生成密码:1或2个SQL表和几KB的代码

    关于安全的其他事项:许多表格,真的比几KB的代码更多


    2
    你没有看到上面的两篇文章吗? - PeeHaa
    是的,但还有另一个问题:最薄弱的环节仍然是用户,哈希是我们的保护措施,以避免整个数据被泄露。 - Michal Taneček
    1
    如果你把这称作密码安全/存储,那么我就会想知道互联网上/我使用的网站是如何存储我的密码的。(我是认真的...)看看ircmaxell的回答,然后再看看我的,不过他的在前面 :P - Rixhers Ajazi
    好的,那么当您知道前8到15个字符是密码,然后是“:”,接着是20到40个未知字符时,请计算SHA512指纹的碰撞可能性和暴力破解时间。 - Michal Taneček

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接