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


94

我想生成忘记密码的标识符。我读到我可以通过将时间戳与mt_rand()一起使用来做到这一点,但有人说时间戳记并非每次都唯一。所以我有点困惑。我可以在此使用时间戳吗?

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

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


@AlmaDoMundo:计算机无法无限地划分时间。
juergen d

@juergend-对不起,不明白。
Alma Do

如果您将其命名为相隔十亿分之一秒,则将获得相同的时间戳。例如,某些时间函数只能以100ns的步长返回时间,而有些只能以秒为单位。
juergen d

@juergend啊,那个。是。我提到的“经典”时间戳只有几秒钟。但是,如果像您所说的那样做-是的(那只会让我们选择使用时间机器来获得非唯一的时间戳)
Alma Do

1
抬起头来,可接受的答案没有利用CSPRNG
Scott Arciszewski 2015年

Answers:


147

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

因此,代码如下:

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

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

有关更多详细信息,请参见


21
请注意,random_bytes()仅从PHP7开始可用。对于较旧的版本,@ yesitsme的答案似乎是最好的选择。
Gerald Schneider

3
@GeraldSchneider或random_compat,这是这些功能的补充,已获得同行最多的评论;)
Scott Arciszewski

我在sql数据库中创建了一个varchar(64)字段来存储此令牌。我将$ length设置为64,但是返回的字符串为128个字符长。如何获得固定大小的字符串(此处为64)?
gordie '16

2
@gordie将长度设置为32,每个字节为2个十六进制字符
JohnHoulderUK 2016年

应该是$length什么?用户的ID?或者是什么?
堆叠

71

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

Adi来自Security.StackExchange 的答案1为此提供了一个解决方案:

确保您具有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/


24
openssl_random_pseudo_bytes($length)-支持:PHP 5> = 5.3.0,....................................... ...................(对于PHP 7及更高版本,请使用random_bytes($length))............................... ....................(对于低于5.3的
PHP-

54

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

由于一个56位的DES密钥可以在大约24小时内被强行使用,并且平均情况下将具有大约59位的熵,因此我们可以计算2 ^ 59/2 ^ 56 =大约8天。取决于实现此令牌验证的方式,实际上可能泄漏时序信息并推断出有效重置令牌的前N个字节

由于问题是关于“最佳做法”的,所以开头是...

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

...我们可以推断此令牌具有隐式安全要求。并且,当您向随机数生成器添加安全性要求时,最佳实践是始终使用加密安全的伪随机数生成器(缩写为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))

将查找与验证器分开

从我以前在PHP中使用安全的“记住我” cookie的工作中汲取经验,减轻上述定时泄漏(通常由数据库查询引入)的唯一有效方法是将查找与验证分开。

如果您的表格如下所示(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()

范例程式码

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

$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.
}

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


验证用户提供的重置令牌时,为什么要使用随机令牌的二进制表示形式?您是否认为有可能(并且安全?):1)使用DB将令牌的哈希十六进制值存储在数据库中hash('sha256', bin2hex($token)); 2)使用进行验证if (hash_equals(hash('sha256', $validator), $results[0]['token'])) {...?谢谢!
Guicara 2015年

是的,比较十六进制字符串也是安全的。这实际上是一个偏好问题。我更喜欢对原始二进制文件执行所有加密操作,并且只能转换为十六进制/ base64进行传输或存储。
Scott Arciszewski

斯科特,您好,这基本上不仅是您的答案的问题,也是有关“记住我”功能的整篇文章的问题。为什么不使用唯一性id作为选择器?我的意思是account_recovery表的主键。选择器不需要额外的安全层,对吗?谢谢!
安德烈·波利卡宁

id:secret还可以 selector:secret还可以 secret本身不是。目的是将数据库查询(定时泄漏)与身份验证协议(应为固定时间)分开。
斯科特·阿西谢夫斯基

如果运行的是PHP 5.6 ,使用openssl_random_pseudo_bytes替代项会有什么危害random_bytes吗?另外,是否不应该在链接的查询字符串中仅附加选择器而不附加验证器?
greg

7

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

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

4
我建议MCRYPT_DEV_URANDOM结束MCRYPT_DEV_RANDOM
Scott Arciszewski 2015年

2

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

<?php
   echo mb_strtoupper(strval(bin2hex(openssl_random_pseudo_bytes(16))));
?>
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.