保护 PHP 密码的哈希和盐值

当前据说 MD5 部分不安全。考虑到这一点,我想知道使用哪种机制进行密码保护。

这个问题, “双重哈希” 密码是否比仅哈希一次密码安全?建议多次散列可能是一个好主意,而如何对单个文件实施密码保护?建议使用盐。

我正在使用 PHP。我想要一个安全,快速的密码加密系统。将密码哈希一百万次可能更安全,但也更慢。如何在速度和安全性之间取得良好的平衡?另外,我希望结果具有恒定数量的字符。

  1. 哈希机制必须在 PHP 中可用
  2. 必须安全
  3. 它可以使用盐(在这种情况下,所有盐都一样好吗?是否有任何方法可以生成优质盐?)

另外,我是否应该在数据库中存储两个字段(例如,一个使用 MD5,另一个使用 SHA)?它会使它更安全或更不安全吗?

如果我不够清楚,我想知道要使用哪个哈希函数以及如何选择合适的盐,以便拥有安全,快速的密码保护机制。

尚未完全涵盖我的问题的相关问题:

PHP 中的 SHA 和 MD5 有什么区别
简单密码加密
安全的 asp.net 密钥,密码存储方法
您将如何在 Tomcat 5.5 中实现盐腌密码

答案

免责声明 :该答案写于 2008 年。

从那时起,PHP 就给了我们password_hashpassword_verify并且自从引入以来,它们就是推荐的密码哈希和检查方法。

答案的理论仍然是一个很好的阅读。

TL; DR

不要

  • 不要限制用户可以输入哪些字符作为密码。只有白痴这样做。
  • 不要限制密码的长度。如果您的用户想要一个句子,其中包含 supercalifragilisticexpialidocious,请不要阻止他们使用它。
  • 不要在密码中剥离或转义 HTML 和特殊字符。
  • 切勿以纯文本形式存储用户密码。
  • 除非用户忘记了密码, 否则不要通过电子邮件将密码发送给您的用户,而是您发送了一个临时密码
  • 永远不要以任何方式记录密码。
  • 切勿使用SHA1或 MD5 甚至 SHA256 哈希密码! 现代饼干可以分别超过 60 和 1800 亿个哈希 / 秒。
  • 请勿将bcrypt 与 hash()的原始输出混合使用,请使用十六进制输出或 base64_encode 对其进行编码。 (这适用于其中可能包含流氓\0任何输入,这会严重削弱安全性。)

多斯

  • 尽可能使用 scrypt;如果不能,请使用 bcrypt。
  • 如果不能使用带有 SHA2 散列的 bcrypt 或 scrypt,请使用 PBKDF2。
  • 数据库受到威胁时,请重置每个人的密码。
  • 实施合理的 8-10 个字符的最小长度,再加上至少 1 个大写字母,1 个小写字母,一个数字和一个符号。这将改善密码的熵,从而使其更难以破解。 (有关一些辩论,请参见 “什么才是好的密码?” 部分。)

为什么仍要哈希密码?

哈希密码的目的很简单:通过破坏数据库来防止对用户帐户的恶意访问。因此,密码哈希的目的是通过花费大量时间或金钱来计算纯文本密码,从而阻止黑客或黑客。时间 / 成本是您武器库中最好的威慑力量。

您想要对用户帐户进行良好而可靠的哈希处理的另一个原因是,给您足够的时间来更改系统中的所有密码。如果您的数据库遭到破坏,您将需要足够的时间至少锁定系统,如果不更改数据库中的每个密码。

Whitehat Security 的首席技术官 Jeremiah Grossman 在最近的密码恢复后要求在蛮力破解他的密码保护之后, 在 White Hat Security 博客上说。

有趣的是,在摆脱这场噩梦的过程中,我学到了很多我不知道的有关密码破解,存储和复杂性的信息。 我已经明白了为什么密码存储比密码复杂性重要得多。如果您不知道密码的存储方式,那么您真正可以依靠的仅仅是复杂性。这可能是密码和加密专家的常识,但是对于一般的 InfoSec 或 Web 安全专家而言,我对此表示高度怀疑。

(强调我的。)

无论如何,什么才是好的密码?

。 (并不是我完全赞同兰德尔的观点。)

简而言之,熵就是密码中的变化量。当密码仅为小写罗马字母时,则只有 26 个字符。差异不大。字母数字密码最好使用 36 个字符。但是允许带符号的大写和小写字母约为 96 个字符。这比仅字母要好得多。一个问题是,为了使我们的密码令人难忘,我们插入了模式,从而减少了熵。糟糕!

密码熵近似容易。使用全部范围的 ascii 字符(大约 96 个可键入字符),每个字符的熵为 6.6,而对于 8 个字符的密码,它的熵仍然太低(熵的 52.679 位),对于将来的安全性而言太低了。但好消息是:更长的密码以及带有 unicode 字符的密码,确实增加了密码的熵并使其难以破解。

Crypto StackExchange站点上,关于密码熵的讨论已经很长时间了。良好的 Google 搜索也会带来很多结果。

在我与 @popnoodles 交谈的评论中,他指出强制 X 长度为 X 的密码策略包含 X 个字母,数字,符号等,实际上可以通过使密码方案更可预测来减少熵。我同意。随机性,尽可能真正地随机性,始终是最安全但最难忘的解决方案。

据我所知,制作世界上最好的密码是 Catch-22。它不是令人难忘,太可预测,太短,太多的 Unicode 字符(在 Windows / 移动设备上很难键入),太长的时间等等。没有密码能真正满足我们的目的,因此我们必须像保护它们一样保护它们在诺克斯堡。

最佳实践

Bcrypt 和scrypt是当前的最佳实践。 Scrypt在时间上会比 bcrypt 更好,但是它尚未被 Linux / Unix 或 Web 服务器视为采用标准,并且尚未对其算法进行深入审查。但是,该算法的未来看起来确实很有希望。如果您正在使用 Ruby,那么有一个scrypt gem可以为您提供帮助,而 Node.js 现在拥有自己的scrypt包。您可以通过Scrypt扩展或Libsodium扩展(在 PECL 中都可用)在 PHP 中使用 Scrypt。

我强烈建议您阅读crypt 函数的文档,如果您想了解如何使用 bcrypt,或者为自己找到一个好的 包装器,或者将类似PHPASS 的东西用于更传统的实现。我建议至少进行 12 轮 bcrypt,如果不是 15 到 18 轮。

当我了解到 bcrypt 仅使用河豚的密钥调度以及可变成本机制时,我改变了对使用 bcrypt 的想法。后者使您可以通过增加河豚已经昂贵的密钥计划来增加暴力破解密码的成本。

一般做法

我几乎无法想象这种情况了。 PHPASS支持 PHP 3.0.18 到 5.3,因此几乎可以在所有可以想到的安装中使用它 - 如果不确定您的环境是否支持 bcrypt,则应使用它。

但是,假设您根本无法使用 bcrypt 或 PHPASS。然后怎样呢?

尝试以您的环境 / 应用程序 / 用户感知可以容忍的最大回合数来实现PDKBF2 。我建议的最低数量是 2500 发。此外,请确保使用hash_hmac()(如果可用)以使操作难以重现。

未来实践

PHP 5.5 中引入了一个完整的密码保护库 ,该抽象了使用 bcrypt 的所有麻烦。尽管我们大多数人在最常见的环境(尤其是共享主机)中都坚持使用 PHP 5.2 和 5.3,但 @ircmaxell 为即将到来的 API 构建了一个兼容层 ,该向后兼容 PHP 5.3.7。

密码学回顾与免责声明

实际破解散列密码所需的计算能力不存在。计算机 “破解” 密码的唯一方法是重新创建密码并模拟用于保护密码的哈希算法。哈希的速度与其被强行使用的能力成线性关系。更糟糕的是,大多数哈希算法可以很容易地并行化以更快地执行。这就是为什么像 bcrypt 和 scrypt 这样的昂贵方案如此重要的原因。

你不可能预见攻击的所有威胁或途径,所以你必须保护用户前面你最大的努力。如果您不这样做,那么您甚至可能会错过被攻击的事实,直到为时已晚... 而您负有责任 。为了避免这种情况,首先要采取偏执的态度。 (内部)攻击您自己的软件,并尝试窃取用户凭据,或修改其他用户的帐户或访问其数据。如果您不测试系统的安全性,那么您就不能怪任何人。

最后:我不是密码学家。我所说的只是我的看法,但我碰巧认为这是基于良好的常识…… 以及大量的阅读资料。请记住,要尽可能地偏执,使事情难以侵入,然后,如果您仍然担心,请联系白帽黑客或密码专家,以了解他们对您的代码 / 系统的看法。

更简短,更安全的答案 - 根本不要编写自己的密码机制 ,而要使用经过考验的机制。

  • PHP 5.5 或更高版本: password_hash()是高质量的,并且是 PHP 核心的一部分。
  • 较旧的 PHP 版本:OpenWall 的phpass库比大多数自定义代码要好得多 - 在 WordPress,Drupal 等中使用。

大多数程序员只是不具备在不引入漏洞的情况下安全地编写与密码相关的代码的专业知识。

快速自我测试:什么是密码扩展?您应该使用多少次迭代?如果您不知道答案,则应使用password_hash() ,因为由于 CPU 的运行速度更快以及使用GPU 和 FPGA每秒数十亿次猜测的速度破解密码,所以密码扩展现在是密码机制的关键功能。 (使用 GPU)。

例如,您可以使用 5 台台式计算机中安装的 25 个 GPU 在 6 小时内破解所有 8 个字符的 Windows 密码 。这是强行手段,即枚举和检查每个 8 个字符的 Windows 密码 (包括特殊字符),而不是字典攻击。那是在 2012 年,到 2018 年,您可以使用更少的 GPU,或者使用 25 个 GPU 更快地破解。

在普通 CPU 上运行的 Windows 密码也有许多彩虹表攻击,而且攻击速度非常快。这是因为即使在 Windows 10 中 ,Windows 仍然 不会加盐或扩展其密码 - 不要犯与 Microsoft 相同的错误!

也可以看看:

  • 关于为何password_hash()phpass是最好的方式的更多信息,这是一个很好的答案
  • 一篇不错的博客文章 ,为 bcrypt,scrypt 和 PBKDF2 等主要算法提供了建议的 “工作因子”(迭代次数)。

我不会以两种不同的方式存储散列的密码,因为那样的话,系统至少与使用中最弱的散列算法一样弱。

从 PHP 5.5 开始,PHP 具有简单,安全的功能,用于哈希和验证密码, password_hash()password_verify()

$password = 'anna';
$hash = password_hash($password, PASSWORD_DEFAULT);
$expensiveHash = password_hash($password, PASSWORD_DEFAULT, array('cost' => 20));

password_verify('anna', $hash); //Returns true
password_verify('anna', $expensiveHash); //Also returns true
password_verify('elsa', $hash); //Returns false

使用password_hash() ,它将生成一个随机盐,并将其包含在输出的哈希中(连同所使用的成本和算法。) password_verify()然后读取该哈希,并确定所使用的盐和加密方法,并针对它进行验证提供的纯文本密码。

提供PASSWORD_DEFAULT指示 PHP 使用已安装版本的 PHP 的默认哈希算法。确切地讲,哪种算法打算在将来的版本中随着时间的推移而改变,因此它将始终是最强大的可用算法之一。

成本增加(默认为 10)会使散列变得更难以暴力破解,但也意味着生成散列并验证散列密码将为服务器的 CPU 带来更多工作。

请注意,即使默认的哈希算法可能会更改,但由于使用的算法存储在哈希中,并且password_verify()会继续使用,因此旧哈希仍会继续验证得很好。

尽管已经回答了问题,但我只想重申,用于哈希处理的盐应该是随机的,而不是像第一个答案中所建议的电子邮件地址那样。

有关更多说明,请访问 - http : //www.pivotalsecurity.com/blog/password-hashing-salt-should-it-be-random/

最近,我讨论了用随机位加盐的密码散列是否比用可猜测或已知盐加盐的密码散列更安全。让我们看看:如果存储密码的系统以及存储随机盐的系统遭到破坏,则攻击者将可以访问哈希和盐,因此盐是否是随机的都没有关系。攻击者可以生成预先计算的彩虹表来破解哈希。有趣的部分来了 - 生成预先计算的表并不是那么简单。让我们以 WPA 安全模型为例。实际上,您的 WPA 密码永远不会发送到 Wireless Access Point。而是使用您的 SSID(网络名称,如 Linksys,Dlink 等)进行哈希处理。这是一个很好的解释。为了从哈希中检索密码,您将需要知道密码以及盐(网络名称)。 Wifi 教堂已经预先计算了哈希表,该哈希表具有前 1000 个 SSID 和大约一百万个密码。所有表的大小约为 40 GB。在您的网站上可以看到,有人使用 15 个 FGPA 阵列 3 天来生成这些表。假设受害者使用的 SSID 为 “a387csf3”,密码为 “123456”,那么这些表会破解它吗?没有! .. 这不可以。即使密码很弱,表也没有针对 SSID a387csf3 的哈希。这是无盐的美丽。它将阻止在预计算表上蓬勃发展的饼干用户。它可以阻止坚定的黑客吗?可能不会。但是使用随机盐确实可以提供额外的防御层。当我们处于这个主题上时,让我们讨论将随机盐存储在单独系统上的其他优点。方案 1:密码哈希存储在系统 X 上,用于哈希的盐值存储在系统 Y。这些盐值是可猜测的或已知的(例如,用户名)方案 2:密码哈希存储在系统 X 上,并且盐值用于哈希存储在系统 Y 中。这些盐值是随机的。如您所料,如果系统 X 受到破坏,则在单独的系统上使用随机盐具有巨大优势(方案 2)。攻击者将需要猜测附加值才能破解哈希。如果使用 32 位盐,则每个猜测的密码将需要 2 ^ 32 = 4,294,967,296(约 42 亿)次迭代。

我只想指出,PHP 5.5 包含一个密码哈希 API ,该APIcrypt()提供了包装器。该 API 大大简化了哈希,验证和重新哈希密码哈希的任务。作者还发布了一个兼容包 (以您只require使用的单个 password.php 文件的形式),适用于使用 PHP 5.3.7 及更高版本并希望立即使用此功能的用户。

它目前仅支持 BCRYPT,但是它旨在易于扩展以包括其他密码哈希技术,并且由于该技术和成本是作为哈希的一部分存储的,因此,对您喜欢的哈希技术 / 成本的更改不会使当前哈希无效将自动验证,使用正确的技术 / 成本。如果您未明确定义自己的盐,它还会处理生成 “安全” 盐的问题。

该 API 公开了四个功能:

  • password_get_info() - 返回有关给定哈希的信息
  • password_hash() - 创建密码哈希
  • password_needs_rehash() - 检查给定的哈希值是否与给定的选项匹配。有用的检查哈希是否符合您当前的技术 / 成本方案,允许您在必要时重新哈希
  • password_verify() - 验证密码是否与哈希匹配

目前,这些函数接受 PASSWORD_BCRYPT 和 PASSWORD_DEFAULT 密码常量,它们现在是同义词,不同之处在于 PASSWORD_DEFAULT“如果支持更新的,更强大的哈希算法,则在更新的 PHP 版本中可能会更改”。在登录时使用 PASSWORD_DEFAULT 和 password_needs_rehash()(并在必要时进行重新哈希处理)应确保您的散列具有相当强的抵御暴力攻击的能力,而几乎不需要任何帮助。

编辑:我刚刚意识到,这是罗伯特 ·K 的答案中简要提到的。我将在这里留下这个答案,因为我认为它为不了解安全性的人提供了更多有关其工作方式和易用性的信息。

我正在使用Phpass ,这是一个简单的单文件 PHP 类,几乎可以在每个 PHP 项目中轻松实现。另请参见H。

默认情况下,它使用 Phpass 中实现的最强大的可用加密,该加密是bcrypt并回退到 MD5 等其他加密,以向与 Wordpress 之类的框架提供向后兼容性。

返回的哈希可以原样存储在数据库中。生成哈希的示例用法是:

$t_hasher = new PasswordHash(8, FALSE);
$hash = $t_hasher->HashPassword($password);

要验证密码,可以使用:

$t_hasher = new PasswordHash(8, FALSE);
$check = $t_hasher->CheckPassword($password, $hash);

谨记

关于 PHP 的密码加密,已经有很多说法,其中大部分是非常好的建议,但是在您甚至开始使用 PHP 进行密码加密的过程之前,请确保已实现或准备实施以下内容。

服务器

港口

如果您没有正确保护运行 PHP 和 DB 的服务器的安全性,那么加密的效果如何,都将毫无用处。大多数服务器以相对相同的方式运行,它们分配有端口,以使您可以通过 ftp 或 shell 远程访问它们。确保更改了活动的远程连接的默认端口。通过不这样做,您实际上使攻击者在访问系统时少了一步。

用户名

对于世界上所有好的东西,请勿使用用户名 admin,root 或类似名称。另外,如果您使用的是基于 UNIX 的系统,请不要使 root 帐户登录名不可访问,它应该始终仅是 sudo。

密码

您告诉用户输入正确的密码以避免被黑,请执行相同的操作。后门完全打开时,进行所有锁定前门的努力的意义何在?

数据库

服务器

理想情况下,您要将数据库和应用程序放在单独的服务器上。由于成本原因,这并非总是可能的,但是它确实可以保证一定的安全性,因为攻击者必须经过两个步骤才能完全访问系统。

用户

始终让您的应用程序拥有其自己的帐户来访问数据库,并仅为其提供所需的特权。

然后为您提供一个单独的用户帐户,该帐户不会存储在服务器上的任何位置,甚至不会存储在应用程序中。

像往常一样,不要使这个根或类似的东西。

密码

请遵循与所有正确密码相同的准则。另外,请勿在同一系统上的任何 SERVER 或 DB 帐户上重复使用相同的密码。

的 PHP

密码

永远不要在数据库中存储密码,而要存储哈希和唯一的盐,稍后我将解释原因。

散列

单向哈希!!!!!!!,永远不要以可以反转的方式哈希密码,哈希应该是一种方式,这意味着您不要反转它们并将它们与密码进行比较,而是哈希输入的密码以相同的方式比较两个哈希值。这意味着,即使攻击者可以访问数据库,他也不知道实际的密码是什么,而只是密码所产生的哈希值。这意味着在最坏的情况下为用户提供更高的安全性。

那里有很多很好的哈希函数( password_hashhash等),但是您需要选择一个好的算法才能使哈希有效。 (bcrypt 及其类似的算法是不错的算法。)

当散列速度是关键时,速度越慢越能抵抗暴力攻击。

散列中最常见的错误之一是散列不是用户独有的。这主要是因为不是唯一地产生盐。

密码在散列前应先加盐。 Salting 为密码添加了一个随机字符串,因此相似的密码在数据库中不会显示相同的密码。但是,如果盐不是每个用户都唯一的盐(即:您使用硬编码盐),那么您几乎会使盐变得一文不值了。因为一旦攻击者找出了一个密码盐,他就对所有密码都加盐。

创建盐时,请确保它与盐盐密码唯一,然后将完整的哈希和盐盐都存储在数据库中。这样做是为了使攻击者必须先破解每种盐和哈希,然后才能获得访问权限。对于攻击者来说,这意味着更多的工作和时间。

用户创建密码

如果用户正在通过前端创建密码,则意味着必须将其发送到服务器。这带来了一个安全问题,因为这意味着未加密的密码将发送到服务器,并且如果攻击者能够侦听和访问,则您在 PHP 中的所有安全性都毫无用处。总是安全地传输数据,这是通过 SSL 完成的,但是即使 SSL 并非完美无缺,也要感到厌倦(OpenSSL 的 Heartbleed 缺陷就是一个例子)。

还要使用户创建一个安全的密码,这很简单,应该始终这样做,最终用户将不胜感激。

最后,无论您采取什么安全措施都是 100%安全,防护技术越先进,攻击就变得越先进。但是,按照以下步骤操作将使您的站点更安全,而攻击者则更不希望这样做。

这是一个 PHP 类,可轻松为密码创建哈希和盐

http://git.io/mSJqpw

Google 说 SHA256 可用于 PHP。

您绝对应该使用盐。我建议使用随机字节(不要将自己局限于字符和数字)。通常,您选择的时间越长,它越安全,越慢。我想 64 字节应该没问题。

我在这里找到了关于此问题的完美话题: https : //crackstation.net/hashing-security.htm ,我希望您能从中受益,这里的源代码也提供了针对基于时间的攻击的防护。

<?php
/*
 * Password hashing with PBKDF2.
 * Author: havoc AT defuse.ca
 * www: https://defuse.ca/php-pbkdf2.htm
 */

// These constants may be changed without breaking existing hashes.
define("PBKDF2_HASH_ALGORITHM", "sha256");
define("PBKDF2_ITERATIONS", 1000);
define("PBKDF2_SALT_BYTES", 24);
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)
{
    // 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
        ));
}

function validate_password($password, $good_hash)
{
    $params = explode(":", $good_hash);
    if(count($params) < HASH_SECTIONS)
       return false; 
    $pbkdf2 = base64_decode($params[HASH_PBKDF2_INDEX]);
    return slow_equals(
        $pbkdf2,
        pbkdf2(
            $params[HASH_ALGORITHM_INDEX],
            $password,
            $params[HASH_SALT_INDEX],
            (int)$params[HASH_ITERATION_INDEX],
            strlen($pbkdf2),
            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; 
}

/*
 * PBKDF2 key derivation function as defined by RSA's PKCS #5: https://www.ietf.org/rfc/rfc2898.txt
 * $algorithm - The hash algorithm to use. Recommended: SHA256
 * $password - The password.
 * $salt - A salt that is unique to the password.
 * $count - Iteration count. Higher is better, but slower. Recommended: At least 1000.
 * $key_length - The length of the derived key in bytes.
 * $raw_output - If true, the key is returned in raw binary format. Hex encoded otherwise.
 * Returns: A $key_length-byte key derived from the password and salt.
 *
 * Test vectors can be found here: https://www.ietf.org/rfc/rfc6070.txt
 *
 * This implementation of PBKDF2 was originally created by https://defuse.ca
 * With improvements by http://www.variations-of-shadow.com
 */
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));
}
?>