Лучшая практика для генерации случайного токена для забытого пароля

96

Я хочу сгенерировать идентификатор забытого пароля. Я читал, что могу сделать это, используя метку времени с mt_rand (), но некоторые люди говорят, что метка времени может не быть уникальной каждый раз. Так что я здесь немного запутался. Могу ли я сделать это с использованием отметки времени?

Вопрос
Как лучше всего генерировать случайные / уникальные токены произвольной длины?

Я знаю, что здесь задают много вопросов, но я все больше запутываюсь, читая разные мнения разных людей.

увлеченный
источник
@AlmaDoMundo: Компьютер не может делить время неограниченно.
juergen d
@juergend - извини, не понял.
Alma Do
Вы получите такую ​​же метку времени, если назовете ее, например, с интервалом в наносекунду. Некоторые функции времени, например, могут возвращать время только с шагом 100 нс, некоторые - только с шагом секунд.
juergen d
@juergend ах, это. Да. Я упомянул «классическую» метку времени только с секундами. Но если действовать так, как вы сказали - да (это оставляет нам только возможность с машиной времени получить неуникальную временную метку)
Alma Do
1
Поднимите голову, принятый ответ не использует CSPRNG .
Скотт Арчишевски

Ответы:

151

В PHP используйте random_bytes() . Причина: вы ищете способ получить токен напоминания пароля, и, если это одноразовые учетные данные для входа, у вас действительно есть данные для защиты (то есть - вся учетная запись пользователя)

Итак, код будет таким:

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

Обновление : предыдущие версии этого ответа ссылались на, uniqid()и это неверно, если есть вопрос безопасности, а не только уникальность. uniqid()по сути, просто microtime()с некоторой кодировкой. Есть простые способы получить точные прогнозы microtime()на вашем сервере. Злоумышленник может отправить запрос на сброс пароля, а затем попробовать использовать пару вероятных токенов. Это также возможно, если используется more_entropy, так как дополнительная энтропия также мала. Спасибо @NikiC и @ScottArciszewski за указание на это.

Подробнее см.

Алма До
источник
21
Обратите внимание, что random_bytes()это доступно только с PHP7. Для более старых версий ответ от @yesitsme кажется лучшим вариантом.
Джеральд Шнайдер
3
@GeraldSchneider или random_compat , который является полифилом для этих функций, получившим наибольшее количество рецензий;)
Скотт Аркишевски,
Я создал поле varchar (64) в своей базе данных sql для хранения этого токена. Я установил $ length равным 64, но возвращаемая строка имеет длину 128 символов. Как мне получить строку фиксированного размера (тогда 64)?
Горди
2
@gordie Установите длину 32, каждый байт состоит из 2 шестнадцатеричных символов
JohnHoulderUK
Что должно быть $length? Идентификатор пользователя? Или что?
stack
72

Это отвечает на "лучший случайный" запрос:

В ответе Ади 1 от Security.StackExchange есть решение для этого:

Убедитесь, что у вас есть поддержка OpenSSL, и вы никогда не ошибетесь с этим однострочным

$token = bin2hex(openssl_random_pseudo_bytes(16));

1. Ади, понедельник, 12 ноября 2018 г., Celeritas, «Создание неопознаваемого токена для электронных писем с подтверждением», 20 сентября 2013 г., 7:06, https://security.stackexchange.com/a/40314/

Да, это я
источник
25
openssl_random_pseudo_bytes($length)- поддержка: PHP 5> = 5.3.0, ....................................... ................... (Для PHP 7 и выше используйте random_bytes($length)) ...................... .................... (Для PHP ниже 5.3 - не используйте PHP ниже 5.3)
jave.web 03
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)).

Отделение поиска от валидатора

Исходя из моей предыдущей работы над безопасными файлами 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.
}

Эти фрагменты кода не являются законченными решениями (я отказался от проверки ввода и интеграции фреймворка), но они должны служить примером того, что делать.

Скотт Арцишевски
источник
Когда вы проверяете предоставленный пользователем токен сброса, почему вы используете двоичное представление случайного токена? Как вы думаете, будет ли возможно (и безопасно?): 1) хранить в БД хешированное шестнадцатеричное значение токена с помощью hash('sha256', bin2hex($token)), 2) проверять с помощью if (hash_equals(hash('sha256', $validator), $results[0]['token'])) {...? Благодарность!
Guicara
Да, сравнение шестнадцатеричных строк тоже безопасно. Это действительно вопрос предпочтений. Я предпочитаю выполнять все криптографические операции с необработанным двоичным кодом и конвертировать только в hex / base64 для передачи или хранения.
Скотт Аркишевски
Привет, Скотт, это, по сути, вопрос не только к твоему ответу, но и ко всей статье о функции «Помни меня». Почему бы не использовать уникальный idкак селектор? Я имею в виду первичный ключ account_recoveryтаблицы. Нам не нужен дополнительный уровень безопасности для селектора, не так ли? Благодарность!
Андре Поликанин
id:secretв порядке. selector:secretв порядке. secretсам по себе нет. Цель состоит в том, чтобы отделить запрос к базе данных (который требует времени) от протокола аутентификации (который должен быть постоянным по времени).
Скотт
Есть ли вред от использования openssl_random_pseudo_bytesвместо этого, random_bytesесли работает PHP 5.6? Кроме того, не следует ли добавлять в строку запроса ссылки только селектор, а не валидатор?
greg
7

Вы также можете использовать DEV_RANDOM, где 128 = 1/2 длины сгенерированного токена. Код ниже генерирует 256 токенов.

$token = bin2hex(mcrypt_create_iv(128, MCRYPT_DEV_RANDOM));
Грэм Т
источник
4
Я бы предложил MCRYPT_DEV_URANDOMбольше MCRYPT_DEV_RANDOM.
Скотт Арчишевски
2

Это может быть полезно, когда вам нужен очень случайный токен.

<?php
   echo mb_strtoupper(strval(bin2hex(openssl_random_pseudo_bytes(16))));
?>
Ir Calif
источник