Я хочу сгенерировать идентификатор забытого пароля. Я читал, что могу сделать это, используя метку времени с mt_rand (), но некоторые люди говорят, что метка времени может не быть уникальной каждый раз. Так что я здесь немного запутался. Могу ли я сделать это с использованием отметки времени?
Вопрос
Как лучше всего генерировать случайные / уникальные токены произвольной длины?
Я знаю, что здесь задают много вопросов, но я все больше запутываюсь, читая разные мнения разных людей.
Ответы:
В PHP используйте
random_bytes()
. Причина: вы ищете способ получить токен напоминания пароля, и, если это одноразовые учетные данные для входа, у вас действительно есть данные для защиты (то есть - вся учетная запись пользователя)Итак, код будет таким:
//$length = 78 etc $token = bin2hex(random_bytes($length));
Обновление : предыдущие версии этого ответа ссылались на,
uniqid()
и это неверно, если есть вопрос безопасности, а не только уникальность.uniqid()
по сути, простоmicrotime()
с некоторой кодировкой. Есть простые способы получить точные прогнозыmicrotime()
на вашем сервере. Злоумышленник может отправить запрос на сброс пароля, а затем попробовать использовать пару вероятных токенов. Это также возможно, если используется more_entropy, так как дополнительная энтропия также мала. Спасибо @NikiC и @ScottArciszewski за указание на это.Подробнее см.
источник
random_bytes()
это доступно только с PHP7. Для более старых версий ответ от @yesitsme кажется лучшим вариантом.$length
? Идентификатор пользователя? Или что?Это отвечает на "лучший случайный" запрос:
В ответе Ади 1 от Security.StackExchange есть решение для этого:
1. Ади, понедельник, 12 ноября 2018 г., Celeritas, «Создание неопознаваемого токена для электронных писем с подтверждением», 20 сентября 2013 г., 7:06, https://security.stackexchange.com/a/40314/
источник
openssl_random_pseudo_bytes($length)
- поддержка: PHP 5> = 5.3.0, ....................................... ................... (Для PHP 7 и выше используйтеrandom_bytes($length)
) ...................... .................... (Для PHP ниже 5.3 - не используйте PHP ниже 5.3)Более ранняя версия принятого ответа (
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. }
Эти фрагменты кода не являются законченными решениями (я отказался от проверки ввода и интеграции фреймворка), но они должны служить примером того, что делать.
источник
hash('sha256', bin2hex($token))
, 2) проверять с помощьюif (hash_equals(hash('sha256', $validator), $results[0]['token'])) {...
? Благодарность!id
как селектор? Я имею в виду первичный ключaccount_recovery
таблицы. Нам не нужен дополнительный уровень безопасности для селектора, не так ли? Благодарность!id:secret
в порядке.selector:secret
в порядке.secret
сам по себе нет. Цель состоит в том, чтобы отделить запрос к базе данных (который требует времени) от протокола аутентификации (который должен быть постоянным по времени).openssl_random_pseudo_bytes
вместо этого,random_bytes
если работает PHP 5.6? Кроме того, не следует ли добавлять в строку запроса ссылки только селектор, а не валидатор?Вы также можете использовать DEV_RANDOM, где 128 = 1/2 длины сгенерированного токена. Код ниже генерирует 256 токенов.
$token = bin2hex(mcrypt_create_iv(128, MCRYPT_DEV_RANDOM));
источник
MCRYPT_DEV_URANDOM
большеMCRYPT_DEV_RANDOM
.Это может быть полезно, когда вам нужен очень случайный токен.
<?php echo mb_strtoupper(strval(bin2hex(openssl_random_pseudo_bytes(16)))); ?>
источник