为忘记密码生成随机令牌的最佳实践

Posted

技术标签:

【中文标题】为忘记密码生成随机令牌的最佳实践【英文标题】:best practice to generate random token for forgot password 【发布时间】:2013-09-25 11:45:59 【问题描述】:

我想生成忘记密码的标识符。我读到我可以通过使用带有 mt_rand() 的时间戳来做到这一点,但有些人说时间戳可能不是每次都是唯一的。所以我在这里有点困惑。我可以通过使用时间戳来做到这一点吗?

问题 生成自定义长度的随机/唯一令牌的最佳做法是什么?

我知道这里有很多问题,但在阅读了不同人的不同意见后,我变得更加困惑。

【问题讨论】:

@AlmaDoMundo:计算机不能无限地划分时间。 @juergend - 对不起,不明白。 如果您调用它,例如相隔一纳秒,您将获得相同的时间戳。例如,一些时间函数只能以 100ns 的步长返回时间,有些只能以秒的步长返回。 @juergend 啊,那个。是的。我提到了只有几秒钟的“经典”时间戳。但是如果像你说的那样行事 - 是的(这只会让我们选择使用时间机器来获取非唯一时间戳) 请注意,接受的答案不利用CSPRNG。 【参考方案1】:

当您需要一个非常随机的令牌时,这可能会有所帮助

<?php
   echo mb_strtoupper(strval(bin2hex(openssl_random_pseudo_bytes(16))));
?>

【讨论】:

【参考方案2】:

这回答了“最佳随机”请求:

Adi's answer1 来自 Security.StackExchange 有一个解决方案:

确保你有 OpenSSL 支持,并且你永远不会出错这个单线

$token = bin2hex(openssl_random_pseudo_bytes(16));

1. Adi,2018 年 11 月 12 日星期一,Celeritas,“为确认电子邮件生成不可猜测的令牌”,2013 年 9 月 20 日 7:06,https://security.stackexchange.com/a/40314/

【讨论】:

openssl_random_pseudo_bytes($length) - 支持:PHP 5 >= 5.3.0 , ................... .............................(对于 PHP 7 及更高版本,请使用random_bytes($length))............ ..................................(对于低于 5.3 的 PHP - 不要使用低于 5.3 的 PHP)跨度> 【参考方案3】:

已接受答案的早期版本 (md5(uniqid(mt_rand(), true))) 不安全,仅提供大约 2^60 种可能的输出——对于低预算的攻击者来说,在大约一周的时间内进行暴力搜索的范围内:

mt_rand() is predictable(最多只能加 31 位熵) uniqid() only adds up to 29 bits of entropy md5() 不添加熵,它只是确定性地混合它

由于56-bit DES key can be brute-forced in about 24 hours,平均情况下大约有 59 位熵,我们可以计算 2^59 / 2^56 = 大约 8 天。取决于此令牌验证的实现方式,it might be possible to practically leak timing information and infer the first N bytes of a valid reset token。

由于问题是关于“最佳实践”的,并且以...开头。

我想生成忘记密码的标识符

...我们可以推断此令牌具有隐含的安全要求。当您向随机数生成器添加安全要求时,最佳做法是始终使用加密安全的伪随机数生成器(缩写为 CSPRNG)。


使用 CSPRNG

在 PHP 7 中,您可以使用 bin2hex(random_bytes($n))(其中 $n 是大于 15 的整数)。

在 PHP 5 中,您可以使用 random_compat 来公开相同的 API。

或者,bin2hex(mcrypt_create_iv($n, MCRYPT_DEV_URANDOM)) 如果您安装了ext/mcrypt。另一个不错的单线是bin2hex(openssl_random_pseudo_bytes($n))

从验证器中分离查找

从我之前在 secure "remember me" cookies in PHP 上的工作来看,减轻上述时间泄漏(通常由数据库查询引入)的唯一有效方法是将查找与验证分开。

如果你的表看起来像这样(mysql)...

CREATE TABLE account_recovery (
    id INTEGER(11) UNSIGNED NOT NULL AUTO_INCREMENT 
    userid INTEGER(11) UNSIGNED NOT NULL,
    token CHAR(64),
    expires DATETIME,
    PRIMARY KEY(id)
);

...您需要再添加一列selector,如下所示:

CREATE TABLE account_recovery (
    id INTEGER(11) UNSIGNED NOT NULL AUTO_INCREMENT 
    userid INTEGER(11) UNSIGNED NOT NULL,
    selector CHAR(16),
    token CHAR(64),
    expires DATETIME,
    PRIMARY KEY(id),
    KEY(selector)
);

使用 CSPRNG 发出密码重置令牌时,将两个值都发送给用户,将选择器和随机令牌的 SHA-256 哈希值存储在数据库中。使用选择器获取哈希值和用户 ID,计算用户提供的令牌的 SHA-256 哈希值与使用 hash_equals() 存储在数据库中的令牌。

示例代码

在 PHP 7(或带有 random_compat 的 5.6)中使用 PDO 生成重置令牌:

$selector = bin2hex(random_bytes(8));
$token = random_bytes(32);

$urlToEmail = 'http://example.com/reset.php?'.http_build_query([
    'selector' => $selector,
    'validator' => bin2hex($token)
]);

$expires = new DateTime('NOW');
$expires->add(new DateInterval('PT01H')); // 1 hour

$stmt = $pdo->prepare("INSERT INTO account_recovery (userid, selector, token, expires) VALUES (:userid, :selector, :token, :expires);");
$stmt->execute([
    'userid' => $userId, // define this elsewhere!
    'selector' => $selector,
    'token' => hash('sha256', $token),
    'expires' => $expires->format('Y-m-d\TH:i:s')
]);

验证用户提供的重置令牌:

$stmt = $pdo->prepare("SELECT * FROM account_recovery WHERE selector = ? AND expires >= NOW()");
$stmt->execute([$selector]);
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (!empty($results)) 
    $calc = hash('sha256', hex2bin($validator));
    if (hash_equals($calc, $results[0]['token'])) 
        // The reset token is valid. Authenticate the user.
    
    // Remove the token from the DB regardless of success or failure.

这些代码 sn-ps 不是完整的解决方案(我避开了输入验证和框架集成),但它们应该作为如何做的示例。

【讨论】:

在验证用户提供的重置令牌时,为什么要使用随机令牌的二进制表示?你认为有可能(并且安全吗?):1)用hash('sha256', bin2hex($token))将令牌的散列十六进制值存储在数据库中,2)用if (hash_equals(hash('sha256', $validator), $results[0]['token'])) ...验证?谢谢! 是的,比较十六进制字符串也是安全的。这真的是一个偏好问题。我更喜欢对原始二进制文件进行所有加密操作,并且只转换为 hex/base64 进行传输或存储。 嗨,Scott,这基本上是一个问题,不仅是为了你的答案,而且是关于“记住我”功能的整篇文章。为什么不使用唯一的id 作为选择器呢?我的意思是,account_recovery 表的主键。我们不需要为选择器增加额外的安全层,对吗?谢谢! id:secret 可以。 selector:secret 没问题。 secret 本身不是。目标是将数据库查询(时序泄漏)与身份验证协议(应该是恒定时间)分开。 如果运行 PHP 5.6,使用 openssl_random_pseudo_bytes 代替 random_bytes 有什么危害吗?另外,你不应该只在链接的查询字符串中附加选择器而不是验证器吗?【参考方案4】:

在 PHP 中,使用random_bytes()。原因:您正在寻找获取密码提醒令牌的方法,如果它是一次性登录凭据,那么您实际上有一个要保护的数据(即 - 整个用户帐户)

所以,代码如下:

//$length = 78 etc
$token = bin2hex(random_bytes($length));

更新此答案的previous versions 指的是uniqid(),如果存在安全问题而不仅仅是唯一性,这是不正确的。 uniqid() 本质上只是带有一些编码的microtime()。有一些简单的方法可以准确预测服务器上的microtime()。攻击者可以发出密码重置请求,然后尝试通过几个可能的令牌。如果使用 more_entropy,这也是可能的,因为额外的熵同样很弱。感谢@NikiC 和@ScottArciszewski 指出这一点。

更多详情见

http://phpsecurity.readthedocs.org/en/latest/Insufficient-Entropy-For-Random-Values.html

【讨论】:

请注意,random_bytes() 仅适用于 PHP7。对于旧版本,@yesitsme 的回答似乎是最好的选择。 @GeraldSchneider 或 random_compat,这是获得最多同行评审的这些功能的 polyfill ;) 我在我的 sql 数据库中创建了一个 varchar(64) 字段来存储这个令牌。我将 $length 设置为 64,但返回的字符串长度为 128 个字符。如何获得固定大小的字符串(此处为 64)? @gordie 设置长度为32,每个字节为2个十六进制字符 $length 应该是什么?用户id?还是什么?【参考方案5】:

您也可以使用 DEV_RANDOM,其中 128 = 1/2 生成的令牌长度。下面的代码生成 256 个令牌。

$token = bin2hex(mcrypt_create_iv(128, MCRYPT_DEV_RANDOM));

【讨论】:

我会建议 MCRYPT_DEV_URANDOM 而不是 MCRYPT_DEV_RANDOM

以上是关于为忘记密码生成随机令牌的最佳实践的主要内容,如果未能解决你的问题,请参考以下文章

生成用于创建密码检索令牌的随机“站点盐”的好方法是啥?

Web应用程序中安全问题的最佳实践

生成加密安全的身份验证令牌

从Mysql数据库中自动删除

忘记密码:实现忘记密码功能的最佳方法是什么?

如何在 Node.js 中正确实现“忘记/重置密码”功能? (使用一次性令牌)