Лучшая практика для генерации случайного токена для забытого пароля
Я хочу сгенерировать идентификатор забытого пароля. Я прочитал, что могу сделать это, используя метку времени с mt_rand(), но некоторые люди говорят, что метка времени может быть не всегда уникальной. Так что я немного запутался здесь. Могу ли я сделать это с помощью отметки времени с этим?
Вопрос
Как лучше всего генерировать случайные / уникальные токены произвольной длины?
Я знаю, что здесь задают много вопросов, но я все больше путаюсь после прочтения разных мнений разных людей.
6 ответов
В PHP используйте random_bytes()
, Причина: вы ищете способ получить токен для напоминания пароля, и, если это одноразовые учетные данные, у вас действительно есть данные для защиты (то есть - вся учетная запись пользователя)
Итак, код будет следующим:
//$length = 78 etc
$token = bin2hex(random_bytes($length));
Обновление: предыдущие версии этого ответа имели в виду uniqid()
и это неверно, если речь идет о безопасности, а не только об уникальности. uniqid()
по сути просто microtime()
с некоторой кодировкой. Есть простые способы получить точные прогнозы microtime()
на вашем сервере. Злоумышленник может отправить запрос на сброс пароля, а затем попробовать пару возможных токенов. Это также возможно, если используется more_entropy, так как дополнительная энтропия также слаба. Спасибо NikiC и Scott Arciszewski за указание на это.
Для более подробной информации смотрите
Это отвечает на "лучший случайный" запрос:
Ответ Ади1 от Security.StackExchange имеет решение для этого:
Убедитесь, что у вас есть поддержка OpenSSL, и вы никогда не ошибетесь с этим однострочником
$token = bin2hex(openssl_random_pseudo_bytes(16));
1. Ади, понедельник, 12 ноября 2018 г., Celeritas, "Генерирование неопровержимого токена для писем с подтверждением", 20 сентября 13 г. в 7:06, https://security.stackexchange.com/a/40314/
Более ранняя версия принятого ответа (md5(uniqid(mt_rand(), true))
) небезопасен и предлагает только около 2^60 возможных выходных данных - вполне в пределах диапазона поиска грубой силы примерно за неделю для малобюджетного злоумышленника:
mt_rand()
предсказуемо (и только добавляет до 31 бита энтропии)uniqid()
только добавляет до 29 бит энтропииmd5()
не добавляет энтропии, просто смешивает
Поскольку 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))
,
Отделение поиска от валидатора
Исходя из моей предыдущей работы над безопасными cookie-файлами "помни меня" в 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 случайного токена в базе данных. Используйте селектор, чтобы получить хеш и идентификатор пользователя, вычислить хеш SHA-256 токена, который пользователь предоставляет с хешем, хранящимся в базе данных, используя hash_equals()
,
Пример кода
Генерация токена сброса в PHP 7 (или 5.6 с random_compat) с помощью 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.
}
Эти фрагменты кода не являются полными решениями (я отказался от проверки входных данных и интеграции фреймворка), но они должны служить примером того, что делать.
Вы также можете использовать DEV_RANDOM, где 128 = 1/2 сгенерированной длины токена. Код ниже генерирует 256 токенов.
$token = bin2hex(mcrypt_create_iv(128, MCRYPT_DEV_RANDOM));
Это может быть полезно, когда вам нужен очень очень случайный токен
<?php
echo mb_strtoupper(strval(bin2hex(openssl_random_pseudo_bytes(16))));
?>
Ты можешь использовать
echo str_shuffle('ASGDHFfdgfdre5475433fd');