2024年PHP大会日本站

password_hash

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

password_hash创建密码哈希值

描述

password_hash(#[\SensitiveParameter] 字符串 $password, 字符串|整数|空值 $algo, 数组 $options = []): 字符串

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 (字符串) - 手动提供用于哈希密码的salt。请注意,这将覆盖并阻止自动生成salt。

    如果省略,password_hash()将为每个哈希密码生成一个随机salt。这是预期的操作模式。

    警告

    salt选项已弃用。现在最好只使用默认生成的salt。从PHP 8.0.0开始,显式给定的salt将被忽略。

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

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

PASSWORD_ARGON2IPASSWORD_ARGON2ID支持的选项:

参数

password

用户的密码。

警告

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

algo

一个密码算法常量,表示在哈希密码时使用的算法。

options

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

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

返回值

返回哈希后的密码。

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

变更日志

版本 描述
8.3.0 当由于salt生成失败而抛出ValueError时,password_hash()现在将底层的Random\RandomException设置为Exception::$previous异常。
8.0.0 password_hash()不再在失败时返回false,如果密码哈希算法无效,则将抛出ValueError,如果密码哈希由于未知错误而失败,则抛出Error
8.0.0 algo参数现在可以为空。
7.4.0 algo参数现在期望一个字符串,但为了向后兼容性,仍然接受整数
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 使用 Argon2i 的 password_hash() 示例

<?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 中已删除手动提供盐值的支持。

注意:

建议您在服务器上测试此函数,并调整 cost 参数,以便该函数的执行时间在交互式系统上少于 350 毫秒。上面示例中的脚本将帮助您为您的硬件选择一个合适的 cost 值。

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

  • 任何新算法必须在成为默认算法之前至少在 PHP 的一个完整版本中存在。例如,如果在 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
5 年前
自 2017 年以来,NIST 建议在对密码等记忆型密钥进行哈希处理时使用密钥输入。通过混合使用密钥输入(通常称为“胡椒粉”),即使攻击者拥有哈希值和盐值,也可以防止攻击者完全暴力破解密码哈希值。例如,SQL 注入通常只影响数据库,而不影响磁盘上的文件,因此存储在配置文件中的“胡椒粉”仍然在攻击者无法触及的范围。 “胡椒粉”必须随机生成一次,并且对所有用户都可以相同。如果网站所有者这样做,许多密码泄露本可以完全避免。

由于 `password_hash` 没有“胡椒粉”参数(即使 Argon2 有一个“secret”参数,PHP 也不允许设置它),混合使用“胡椒粉”的正确方法是使用 `hash_hmac()`。“添加注释”的 php.net 规则说我不能链接外部网站,所以我无法用 NIST、维基百科、安全 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
1 年前
如果您要使用 bcrypt,则应使用随机的大型字符串来为密码添加“胡椒粉”,因为商品硬件可以在一小时内破解 bcrypt 8 个字符的密码;https://www.tomshardware.com/news/eight-rtx-4090s-can-break-passwords-in-under-an-hour
nicoSWD
11 年前
我同意 martinstoeckli 的观点,

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

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

在 Windows 上,它使用 CryptGenRandom()。

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

不要试图通过创建不安全的来超越这些默认值。任何基于 rand()、mt_rand()、uniqid() 或这些变体的都不是好方法。
Lyo Mi
8 年前
请注意,password_hash 会在第一个空字节处***截断***密码。

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

如果您使用任何可以生成空字节的输入(例如,将 sha1 的 raw 参数设置为 true,或者空字节可能自然地出现在用户的密码中),您的应用程序的安全性可能会远低于您的预期。

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

当然,解决方法是确保您永远不会将空字节传递给 password_hash。
fullstadev at gmail dot com
6个月前
类似于这里关于在 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 个字符的限制内,以确保您的所有“随机信息”都被哈希,而不仅仅是一部分。
martinstoeckli
11 年前
在大多数情况下,最好省略 salt 参数。没有此参数,该函数将从操作系统的随机源生成密码学安全的盐。
ms1 at rdrecs dot com
5 年前
简单来说,定时攻击是一种可以根据执行速度计算密码字符的攻击。

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

根据 phpnetcomment201908 at lucb1e dot com 的建议,我已经向代码中添加了代码,以使用 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
10年前
对于密码,您通常希望哈希计算时间在 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
?>
To Top