Chcę wygenerować identyfikator dla zapomnianego hasła. Czytałem, że mogę to zrobić, używając timestamp z mt_rand (), ale niektórzy mówią, że znacznik czasu może nie być unikalny za każdym razem. Więc jestem tu trochę zdezorientowany. Czy mogę to zrobić za pomocą znacznika czasu?
Pytanie
Jaka jest najlepsza praktyka generowania losowych / unikalnych tokenów o niestandardowej długości?
Wiem, że zadaje się tu wiele pytań, ale po przeczytaniu różnych opinii różnych ludzi jestem bardziej zdezorientowany.
Odpowiedzi:
W PHP użyj
random_bytes()
. Powód: szukasz sposobu na uzyskanie tokena przypominającego hasło, a jeśli są to jednorazowe dane logowania, to faktycznie masz dane do ochrony (czyli całe konto użytkownika)Tak więc kod będzie wyglądał następująco:
//$length = 78 etc $token = bin2hex(random_bytes($length));
Aktualizacja : poprzednie wersje tej odpowiedzi dotyczyły
uniqid()
i jest to niepoprawne, jeśli chodzi o bezpieczeństwo, a nie tylko o wyjątkowość.uniqid()
jest w zasadzie tylkomicrotime()
z pewnym kodowaniem. Istnieją proste sposoby uzyskania dokładnych prognozmicrotime()
dotyczących serwera. Osoba atakująca może wysłać żądanie zresetowania hasła, a następnie wypróbować kilka prawdopodobnych tokenów. Jest to również możliwe, jeśli używa się more_entropy, ponieważ dodatkowa entropia jest podobnie słaba. Dziękuję @NikiC i @ScottArciszewski za wskazanie tego.Aby uzyskać więcej informacji, zobacz
źródło
random_bytes()
jest dostępny tylko od PHP7. W przypadku starszych wersji odpowiedź @yesitsme wydaje się być najlepszą opcją.$length
? Identyfikator użytkownika? Albo co?To odpowiada na żądanie „najlepszego losowego”:
Odpowiedź Adi 1 z Security.StackExchange ma na to rozwiązanie:
1. Adi, pon. 12 listopada 2018 r., Celeritas, „Generating an unguessable token for potwierdzenie e-mails”, 20 września 2013 o 7:06, https://security.stackexchange.com/a/40314/
źródło
openssl_random_pseudo_bytes($length)
- obsługa: PHP 5> = 5.3.0, ....................................... ................... (Dla PHP 7 i nowszych użyjrandom_bytes($length)
) ...................... .................... (Dla PHP poniżej 5.3 - nie używaj PHP poniżej 5.3)Wcześniejsza wersja zaakceptowanej odpowiedzi (
md5(uniqid(mt_rand(), true))
) jest niepewna i oferuje tylko około 2 ^ 60 możliwych wyników - dobrze w zakresie przeszukiwania siłą w ciągu około tygodnia dla napastnika o niskim budżecie:mt_rand()
jest przewidywalna (i sumuje się tylko do 31 bitów entropii)uniqid()
sumuje się tylko do 29 bitów entropiimd5()
nie dodaje entropii, po prostu miesza ją deterministyczniePonieważ 56-bitowy klucz DES można wymusić brutalnie w ciągu około 24 godzin , a przeciętny przypadek miałby około 59 bitów entropii, możemy obliczyć 2 ^ 59/2 ^ 56 = około 8 dni. W zależności od tego, jak zaimplementowano tę weryfikację tokenu, może być możliwe praktycznie wyciek informacji o czasie i wywnioskowanie pierwszych N bajtów ważnego tokenu resetowania .
Ponieważ pytanie dotyczy „sprawdzonych metod” i otwiera się…
... możemy wywnioskować, że ten token ma niejawne wymagania dotyczące bezpieczeństwa. A kiedy dodajesz wymagania bezpieczeństwa do generatora liczb losowych, najlepszą praktyką jest zawsze używanie generatora liczb pseudolosowych zabezpieczonego kryptograficznie (w skrócie CSPRNG).
Korzystanie z CSPRNG
W PHP 7 możesz użyć
bin2hex(random_bytes($n))
(gdzie$n
jest liczbą całkowitą większą niż 15).W PHP 5 możesz użyć
random_compat
tego samego API.Alternatywnie,
bin2hex(mcrypt_create_iv($n, MCRYPT_DEV_URANDOM))
jeśliext/mcrypt
zainstalowałeś. Kolejny dobry jednolinijkowy jestbin2hex(openssl_random_pseudo_bytes($n))
.Oddzielenie wyszukiwania od walidatora
Opierając się na mojej poprzedniej pracy nad bezpiecznymi plikami cookie „zapamiętaj mnie” w PHP , jedynym skutecznym sposobem złagodzenia wspomnianego wycieku czasu (zwykle wprowadzanego przez zapytanie do bazy danych) jest oddzielenie wyszukiwania od weryfikacji.
Jeśli twoja tabela wygląda tak (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) );
... musisz dodać jeszcze jedną kolumnę
selector
, na przykład: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) );
Użyj CSPRNG Po wystawieniu tokena resetowania hasła wyślij obie wartości do użytkownika, zapisz selektor i skrót SHA-256 losowego tokenu w bazie danych. Użyj selektora, aby pobrać hash i identyfikator użytkownika, obliczyć skrót SHA-256 tokenu, który podaje użytkownik, z tym, który jest przechowywany w bazie danych
hash_equals()
.Przykładowy kod
Generowanie tokena resetowania w PHP 7 (lub 5.6 z random_compat) z 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') ]);
Weryfikacja tokena resetowania podanego przez użytkownika:
$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. }
Te fragmenty kodu nie są kompletnymi rozwiązaniami (zrezygnowałem z walidacji danych wejściowych i integracji ram), ale powinny służyć jako przykład tego, co robić.
źródło
hash('sha256', bin2hex($token))
, 2) weryfikowanie za pomocąif (hash_equals(hash('sha256', $validator), $results[0]['token'])) {...
? Dzięki!id
jako selektora? Mam na myśli klucz głównyaccount_recovery
tabeli. Nie potrzebujemy dodatkowej warstwy zabezpieczeń selektora, prawda? Dzięki!id:secret
jest OK.selector:secret
jest OK.secret
sama nie jest. Celem jest oddzielenie zapytania do bazy danych (które jest nieszczelne w czasie) od protokołu uwierzytelniania (który powinien być stały).openssl_random_pseudo_bytes
zamiast tegorandom_bytes
jest szkodliwe, jeśli działa PHP 5.6? Ponadto, czy w zapytaniu o łącze nie należy dołączać tylko selektora, a nie walidatora?Możesz również użyć DEV_RANDOM, gdzie 128 = 1/2 długości wygenerowanego tokena. Poniższy kod generuje 256 tokenów.
$token = bin2hex(mcrypt_create_iv(128, MCRYPT_DEV_RANDOM));
źródło
MCRYPT_DEV_URANDOM
więcejMCRYPT_DEV_RANDOM
.Może to być pomocne, gdy potrzebujesz bardzo losowego tokena
<?php echo mb_strtoupper(strval(bin2hex(openssl_random_pseudo_bytes(16)))); ?>
źródło