password_hash

(PHP 5 >= 5.5.0, PHP 7, PHP 8)

password_hash创建密码哈希

描述

password_hash(#[\SensitiveParameter] string $password, string|int|null $algo, array $options = []): string

password_hash() 使用强单向哈希算法创建新的密码哈希。

当前支持以下算法

  • PASSWORD_DEFAULT - 使用 bcrypt 算法(从 PHP 5.5.0 开始默认使用)。请注意,此常量旨在随着时间推移而更改,因为 PHP 中会添加新的更强大的算法。因此,使用此标识符的结果长度可能会随着时间推移而改变。因此,建议将结果存储在可以扩展到 60 个字符以上的数据库列中(255 个字符将是一个不错的选择)。
  • PASSWORD_BCRYPT - 使用 CRYPT_BLOWFISH 算法来创建哈希。这将使用“$2y$”标识符生成标准 crypt() 兼容哈希。结果始终为 60 个字符的字符串,或在失败时为 false
  • PASSWORD_ARGON2I - 使用 Argon2i 哈希算法来创建哈希。此算法仅在 PHP 使用 Argon2 支持编译时可用。
  • PASSWORD_ARGON2ID - 使用 Argon2id 哈希算法来创建哈希。此算法仅在 PHP 使用 Argon2 支持编译时可用。

PASSWORD_BCRYPT 支持的选项

  • salt (string) - 手动提供在对密码进行哈希时使用的盐。请注意,这将覆盖并阻止自动生成盐。

    如果省略,password_hash() 将为每个经过哈希处理的密码自动生成一个随机盐。这是预期的操作模式。

    警告

    salt 选项已弃用。现在建议使用默认情况下生成的盐。从 PHP 8.0.0 开始,显式给出的盐将被忽略。

  • cost (int) - 表示应使用的算法成本。这些值的示例可以在 crypt() 页面上找到。

    如果省略,将使用默认值 10。这是一个很好的基线成本,但您可能需要根据您的硬件考虑增加它。

PASSWORD_ARGON2IPASSWORD_ARGON2ID 支持的选项

参数

password

用户的密码。

注意

使用 PASSWORD_BCRYPT 作为算法,将导致 password 参数被截断为最大 72 字节的长度。

algo

一个 密码算法常量,表示对密码进行哈希时要使用的算法。

options

包含选项的关联数组。有关每个算法支持选项的文档,请参阅 密码算法常量

如果省略,将创建随机盐并使用默认成本。

返回值

返回经过哈希处理的密码。

使用的算法、成本和盐作为哈希的一部分返回。因此,验证哈希所需的所有信息都包含在其中。这允许 password_verify() 函数验证哈希,而无需为盐或算法信息单独存储。

变更日志

版本 描述
8.0.0 password_hash() 现在不再在失败时返回 false,如果密码哈希算法无效,则会抛出 ValueError,如果密码哈希处理因未知错误而失败,则会抛出 Error
8.0.0 algo 参数现在可以为空。
7.4.0 algo 参数现在期望一个 string,但为了向后兼容,仍然接受 int
7.4.0 sodium 扩展为 Argon2 密码提供了一种替代实现。
7.3.0 使用 PASSWORD_ARGON2ID 对 Argon2id 密码的支持已添加。
7.2.0 使用 PASSWORD_ARGON2I 对 Argon2i 密码的支持已添加。

示例

示例 #1 password_hash() 示例

<?php
/**
* 我们只想使用当前的 DEFAULT 算法对密码进行哈希处理。
* 这目前是 BCRYPT,并将产生一个 60 个字符的结果。
*
* 注意,DEFAULT 可能会随着时间推移而更改,因此您需要准备
* 通过允许您的存储扩展到 60 个字符以外(255 个字符会很好)
*/
echo password_hash("rasmuslerdorf", PASSWORD_DEFAULT);
?>

上面的示例将输出类似于以下内容

$2y$10$.vGA1O9wmRjrwAVXD98HNOgsNpDczlqm3Jq7KnEd1rVAGv3Fykk1a

示例 #2 password_hash() 示例手动设置成本

<?php
/**
* 在这种情况下,我们想将 BCRYPT 的默认成本提高到 12。
* 注意,我们也切换到了 BCRYPT,它将始终是 60 个字符。
*/
$options = [
'cost' => 12,
];
echo
password_hash("rasmuslerdorf", PASSWORD_BCRYPT, $options);
?>

上面的示例将输出类似于以下内容

$2y$12$QjSH496pcT5CEbzjD/vtVeH03tfHKFy36d4J0Ltp3lRtee9HDxY3K

示例 #3 password_hash() 示例查找合适的成本

<?php
/**
* 此代码将对您的服务器进行基准测试,以确定您可以承受的最高成本。
* 您希望设置最高的成本,而不会使您的服务器速度过慢。
* 10 是一个好的基线,如果您的服务器足够快,更多更好。下面的代码的目标是 ≤ 350 毫秒的拉伸时间,
* 这是处理交互式登录的系统适当的延迟。
*/
$timeTarget = 0.350; // 350 毫秒

$cost = 10;
do {
$cost++;
$start = microtime(true);
password_hash("test", PASSWORD_BCRYPT, ["cost" => $cost]);
$end = microtime(true);
} while ((
$end - $start) < $timeTarget);

echo
"找到的合适成本: " . $cost;
?>

上面的示例将输出类似于以下内容

Appropriate Cost Found: 12

示例 #4 password_hash() 使用 Argon2i 的示例

<?php
echo 'Argon2i 散列: ' . password_hash('rasmuslerdorf', PASSWORD_ARGON2I);
?>

上面的示例将输出类似于以下内容

Argon2i hash: $argon2i$v=19$m=1024,t=2,p=2$YzJBSzV4TUhkMzc3d3laeg$zqU/1IN0/AogfP4cmSJI1vc8lpXRW9/S0sYY2i2jHT0

注释

注意

强烈建议您不要为此函数生成自己的盐。如果您没有指定盐,它将自动为您创建一个安全的盐。

如上所述,在 PHP 7.0 中提供 salt 选项将生成弃用警告。在 PHP 8.0 中已删除对手动提供盐的支持。

注意:

建议您在服务器上测试此函数,并调整成本参数,以便该函数的执行在交互式系统上花费不到 350 毫秒。上面的示例中的脚本将帮助您为您的硬件选择一个好的成本值。

注意 此函数支持的算法的更新(或对默认算法的更改)必须遵循以下规则:

  • 任何新的算法都必须在成为默认算法之前,至少在 PHP 的完整发行版中存在 1 个完整版本。因此,例如,如果在 7.5.5 中添加了一个新的算法,它在 7.7 之前(因为 7.6 将是第一个完整版本)将没有资格成为默认算法。但是,如果在 7.6.0 中添加了一个不同的算法,它在 7.7.0 时也有资格成为默认算法。
  • 默认值应该只在完整版本(7.3.0、8.0.0 等)中更改,而不是在修订版本中更改。对此的唯一例外是在当前默认值中发现严重的安全漏洞时。

另请参阅

添加注释

用户贡献的注释 8 个注释

phpnetcomment201908 at lucb1e dot com
4 年前
自 2017 年以来,NIST 建议在散列记忆秘密(如密码)时使用秘密输入。通过混合秘密输入(通常称为“胡椒粉”),可以防止攻击者完全暴力破解密码散列,即使他们拥有散列和盐。例如,SQL 注入通常只影响数据库,而不影响磁盘上的文件,因此存储在配置文件中的胡椒粉对于攻击者来说仍然是无法访问的。胡椒粉必须随机生成一次,并且可以对所有用户相同。如果网站所有者做到了这一点,许多密码泄露可能已经变得毫无用处。

由于 password_hash 没有胡椒粉参数(即使 Argon2 具有“secret”参数,PHP 也不允许设置它),混合胡椒粉的正确方法是使用 hash_hmac()。php.net 的“添加注释”规则说我不能链接外部网站,所以我不能用任何链接来支持这些内容,例如 NIST、维基百科、来自 security stackexchange 网站的解释原因的帖子,或者任何内容... 您将不得不手动验证这一点。代码

// config.conf
pepper=c1isvFdxMDdmjOlvxpecFw

<?php
// register.php
$pepper = getConfigVariable("pepper");
$pwd = $_POST['password'];
$pwd_peppered = hash_hmac("sha256", $pwd, $pepper);
$pwd_hashed = password_hash($pwd_peppered, PASSWORD_ARGON2ID);
add_user_to_database($username, $pwd_hashed);
?>

<?php
// login.php
$pepper = getConfigVariable("pepper");
$pwd = $_POST['password'];
$pwd_peppered = hash_hmac("sha256", $pwd, $pepper);
$pwd_hashed = get_pwd_from_db($username);
if (
password_verify($pwd_peppered, $pwd_hashed)) {
echo
"密码匹配.";
}
else {
echo
"密码不正确.";
}
?>

请注意,此代码包含一个计时攻击,它会泄露用户名是否存在。但我的注释超出了长度限制,所以我不得不删除这一段。

还要注意,如果胡椒粉泄露或可以破解,它将毫无用处。考虑它可能如何暴露,例如传递到 docker 容器的不同方法。针对破解,使用一个很长的随机生成的数值(如上面的示例所示),并在您使用干净的用户数据库进行新安装时更改胡椒粉。为现有数据库更改胡椒粉与更改其他散列参数相同:您可以将旧值包装在新值中并分层散列(更复杂),或者在有人登录时计算新的密码散列(使旧用户面临风险,因此这可能没问题取决于您升级的原因)。

为什么这有效?因为攻击者在窃取数据库后会执行以下操作

password_verify("a", $stolen_hash)
password_verify("b", $stolen_hash)
...
password_verify("z", $stolen_hash)
password_verify("aa", $stolen_hash)
等等。

(更现实地说,他们使用破解字典,但原则上,破解密码散列的方法是猜测。这就是我们使用特殊算法的原因:它们速度更慢,因此每次 verify() 操作都将更慢,因此他们每小时可以尝试的密码数量要少得多。)

现在,如果您使用胡椒粉会怎么样?现在他们需要这样做

password_verify(hmac_sha256("a", $secret), $stolen_hash)

如果没有该 $secret(胡椒粉),他们就无法进行此计算。他们将不得不这样做

password_verify(hmac_sha256("a", "a"), $stolen_hash)
password_verify(hmac_sha256("a", "b"), $stolen_hash)
...
等等,直到他们找到正确的胡椒粉。

如果您的胡椒粉包含 128 位的熵,只要 hmac-sha256 保持安全(即使 MD5 在技术上是安全的,可以在 hmac 中使用:只有它的抗碰撞性被破坏,但当然没有人会使用 MD5,因为越来越多的缺陷被发现),这将比太阳输出的能量更多。换句话说,即使给定已知的密码和盐,目前也不可能破解强度如此高的胡椒粉。
bhare at duck dot com
11 个月前
如果您要使用 bcrypt,那么您应该用随机的大字符串给密码加胡椒粉,因为商用硬件可以在一个小时内破解 bcrypt 8 个字符的密码;https://www.tomshardware.com/news/eight-rtx-4090s-can-break-passwords-in-under-an-hour
nicoSWD
10 年前
我同意 martinstoeckli 的观点,

不要创建您自己的盐,除非您真的知道自己在做什么。

默认情况下,它将使用 /dev/urandom 来创建盐,该盐基于来自设备驱动程序的噪声。

而在 Windows 上,它使用 CryptGenRandom()。

这两者都存在了多年,并且被认为是加密安全的(前者可能比后者更安全,尽管如此)。

不要试图通过创建更不安全的东西来胜过这些默认值。任何基于 rand()、mt_rand()、uniqid() 或这些变体的東西 *都不* 好。
Lyo Mi
8 年前
请注意,password_hash 将 ***截断*** 第一个 NULL 字节处的密码。

http://blog.ircmaxell.com/2015/03/security-issue-combining-bcrypt-with.html

如果您使用任何可以生成 NULL 字节的东西作为输入(具有原始值为 true 的 sha1,或者如果 NULL 字节可以自然地出现在人们的密码中),您可能会使您的应用程序比您预期的不安全得多。

密码
$a = "\01234567";
对于 bcrypt 来说,它是零字节长(空密码)。

当然,解决方法是确保您永远不会将 NULL 字节传递给 password_hash。
martinstoeckli
11 年前
在大多数情况下,最好省略 salt 参数。如果没有此参数,该函数将从操作系统的随机源生成一个密码安全的 salt。
ms1 at rdrecs dot com
4 年前
简单来说,时序攻击是可以根据执行速度来计算密码的字符的攻击。

更多内容请访问...
https://paragonie.com/blog/2015/11/preventing-timing-attacks-on-string-comparison-with-double-hmac-strategy

我根据 lucb1e dot com 的 phpnetcomment201908 的建议添加了代码,使用 phpnetcomment201908 at lucb1e dot com 发布的代码使这种“时序攻击”更难。

$pph_strt = microtime(true);

//...
/*他为 login.php 发布的代码*/
//...

$end = (microtime(true) - $pph_strt);

$wait = bcmul((1 - $end), 1000000); // usleep(250000) 1/4 of a second

usleep ( $wait );

echo "<br>Execution time:".(microtime(true) - $pph_strt)."; ";

注意,我建议更改等待时间以满足您的需要,但请确保它大于脚本在您的服务器上执行的最高时间。

此外,这是我为了混淆执行时间以抵消时序攻击而采用的变通方法。您可以在我发布的链接中找到更深入的讨论和来自比我更胜任密码学的人员的更多信息。我认为这并不存在,但还有其他人。我是在那里了解到什么是时序攻击的,因为我是新手,但我希望拥有牢固的安全性。
Mike Robinson
9 年前
对于密码,您通常希望哈希计算时间在 250 到 500 毫秒之间(管理员帐户可能更长)。由于计算时间取决于服务器的能力,在两台不同的服务器上使用相同的成本参数可能会导致执行时间有很大差异。这里有一个简短的小函数可以帮助您确定应该为您的服务器使用什么成本参数以确保您在该范围内(注意,我提供了一个盐来消除由创建伪随机盐引起的任何延迟,但这在对密码进行哈希时不应这样做)

<?php
/**
* @Param int $min_ms 哈希计算所需的最短时间(以毫秒为单位)
*/
function getOptimalBcryptCostParameter($min_ms = 250) {
for (
$i = 4; $i < 31; $i++) {
$options = [ 'cost' => $i, 'salt' => 'usesomesillystringforsalt' ];
$time_start = microtime(true);
password_hash("rasmuslerdorf", PASSWORD_BCRYPT, $options);
$time_end = microtime(true);
if ((
$time_end - $time_start) * 1000 > $min_ms) {
return
$i;
}
}
}
echo
getOptimalBcryptCostParameter(); // 在我的情况下打印 12
?>
fullstadev at gmail dot com
3 个月前
与这里关于在 password_hash() 中使用包含空字节的字符串的另一篇文章类似,我想更准确一点,因为我们现在遇到了一些问题。

我有一个应用程序生成随机哈希(CSPRN)的项目。他们所做的是使用 random_bytes(32),并将 password_hash() 应用于获得的字符串,并使用 bcrypt 算法。

这方面导致有时 random_bytes() 生成的字符串包含空字节,实际上导致他们的 password_hash() 调用出错(PHP v 8.2.18)。由于此问题(“Bcrypt 密码不能包含空字符”),我修改了生成随机哈希的函数,以使用 bin2hex()(或 base64 或其他)对使用 random_bytes() 获得的二进制随机字符串进行编码,以确保要哈希的字符串不包含空字节。

然后我想补充一点,当您使用 bcrypt 算法时,请务必记住 bcrypt 会将您的密码截断为 72 个字符。当您对随机字符串进行编码(例如,使用 random_bytes() 生成)时,这会将您的字符串从二进制转换为十六进制表示,例如将长度加倍。您通常希望您的整个密码仍然包含在 72 个字符限制内,以确保您的所有“随机信息”都被哈希,而不仅仅是部分信息。
To Top