найкраща практика для генерації випадкового маркера для забутого пароля


94

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

Запитання
Яка найкраща практика для створення випадкових / унікальних токенів нестандартної довжини?

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


@AlmaDoMundo: Комп’ютер не може ділити час необмежено.
juergen d

@juergend - вибачте, не розумію.
Альма До

Ви отримаєте однакову позначку часу, якщо назвете її, наприклад, наносекундною відстанью. Наприклад, деякі функції часу можуть повертати час лише з кроком 100 нс, деякі - лише з кроком секунд.
juergen d

@juergend ах, це. Так. Я згадав "класичну" мітку часу лише із секундами. Але якщо поводитись так, як ти сказав - так (це залишає нам лише можливість з машиною часу отримати не унікальну мітку часу)
Алма До

1
Голова вгору, прийнята відповідь не використовує CSPRNG .
Скотт Арчішевскі

Відповіді:


148

У PHP використовуйте random_bytes(). Причина: ви шукаєте спосіб отримати маркер нагадування про пароль, і якщо це одноразові облікові дані для входу, то ви насправді маєте дані для захисту (а це - весь обліковий запис користувача)

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

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

Оновлення : посилання на попередні версії цієї відповіді було uniqid()неправильним, якщо мова йде про безпеку, а не лише про унікальність. uniqid()по суті просто microtime()з деяким кодуванням. Існують прості способи отримання точних прогнозів microtime()на вашому сервері. Зловмисник може надіслати запит на скидання пароля, а потім спробувати пройти кілька ймовірних токенів. Це також можливо, якщо використовується більше_ентропії, оскільки додаткова ентропія настільки ж слабка. Завдяки @NikiC та @ScottArciszewski за те, що вони на це вказують.

Детальніше див


21
Зауважте, що random_bytes()доступний лише з PHP7. У старих версіях найкращим варіантом є відповідь @yesitsme.
Джеральд Шнайдер

3
@GeraldSchneider або random_compat , що є поліфілом для цих функцій, який отримав найбільшу рецензію;)
Скотт Арчішевський

Я створив поле varchar (64) у своїй базі даних sql для зберігання цього маркера. Я встановив $ length для 64, але рядок, що повертається, має довжину 128 символів. Як я можу отримати рядок із фіксованим розміром (тут 64 тоді)?
gordie

2
@gordie Встановіть довжину 32, кожен байт складає 2 шістнадцяткові символи
JohnHoulderUK

Що повинно бути $length? Ідентифікатор користувача? Або що?
стек

71

Це відповідає "найкращому випадковому" запиту:

Відповідь Аді 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/


24
openssl_random_pseudo_bytes($length)- підтримка: PHP 5> = 5.3.0, ....................................... ................... (для PHP 7 і новіших версій, використовуйте random_bytes($length)) ...................... .................... (Для PHP нижче 5,3 - не використовуйте PHP нижче 5,3)
jave.web

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'])) {...? Дякую!
Гікара

Так, порівняння шістнадцяткових рядків теж безпечно. Це справді питання переваг. Я вважаю за краще робити всі крипто-операції на необробленому двійковому файлі і лише коли-небудь конвертувати в 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))));
?>
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.