najlepsza praktyka generowania losowego tokena dla zapomnianego hasła


95

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.


@AlmaDoMundo: Komputer nie może dzielić czasu bez ograniczeń.
juergen d

@juergend - przepraszam, nie rozumiem.
Alma Do

Otrzymasz ten sam znacznik czasu, jeśli nazwiesz go na przykład w odstępie nano sekundy. Na przykład niektóre funkcje czasu mogą zwracać czas tylko w krokach 100 ns, a niektóre tylko w krokach sekund.
juergen d

@juergend ah, that. Tak. Wspomniałem o „klasycznym” sygnaturze czasowej z samymi sekundami. Ale jeśli zachowujesz się tak, jak powiedziałeś - tak (to pozostawia nam tylko opcję z wehikułem czasu, aby uzyskać nieunikalny znacznik czasu)
Alma Do

1
Uwaga , zaakceptowana odpowiedź nie wykorzystuje CSPRNG .
Scott Arciszewski

Odpowiedzi:


150

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 tylko microtime()z pewnym kodowaniem. Istnieją proste sposoby uzyskania dokładnych prognoz microtime()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


21
Zauważ, że random_bytes()jest dostępny tylko od PHP7. W przypadku starszych wersji odpowiedź @yesitsme wydaje się być najlepszą opcją.
Gerald Schneider

3
@GeraldSchneider lub random_compat , czyli polyfill dla tych funkcji, który otrzymał najwięcej recenzji;)
Scott Arciszewski

Zrobiłem pole varchar (64) w mojej bazie danych sql do przechowywania tego tokenu. Ustawiłem $ length na 64, ale zwracany ciąg ma długość 128 znaków. Jak mogę uzyskać ciąg o stałym rozmiarze (tutaj 64)?
gordie

2
@gordie Ustaw długość na 32, każdy bajt to 2 znaki szesnastkowe
JohnHoulderUK

Co powinno być $length? Identyfikator użytkownika? Albo co?
stos

72

To odpowiada na żądanie „najlepszego losowego”:

Odpowiedź Adi 1 z Security.StackExchange ma na to rozwiązanie:

Upewnij się, że masz obsługę OpenSSL, a nigdy nie pomylisz się z tym jednowierszowym

$token = bin2hex(openssl_random_pseudo_bytes(16));

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/


25
openssl_random_pseudo_bytes($length)- obsługa: PHP 5> = 5.3.0, ....................................... ................... (Dla PHP 7 i nowszych użyj random_bytes($length)) ...................... .................... (Dla PHP poniżej 5.3 - nie używaj PHP poniżej 5.3)
jave.web

54

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:

Ponieważ 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ę…

Chcę wygenerować identyfikator dla zapomnianego hasła

... 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 $njest liczbą całkowitą większą niż 15).

W PHP 5 możesz użyć random_compattego samego API.

Alternatywnie, bin2hex(mcrypt_create_iv($n, MCRYPT_DEV_URANDOM))jeśli ext/mcryptzainstalowałeś. Kolejny dobry jednolinijkowy jest bin2hex(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ć.


Dlaczego podczas weryfikacji tokenu resetowania podanego przez użytkownika używasz binarnej reprezentacji losowego tokenu? Czy myślisz, że będzie możliwe (i bezpieczne?): 1) przechowywanie w DB zaszyfrowanej wartości szesnastkowej tokena za pomocą hash('sha256', bin2hex($token)), 2) weryfikowanie za pomocą if (hash_equals(hash('sha256', $validator), $results[0]['token'])) {...? Dzięki!
Guicara,

Tak, porównywanie ciągów szesnastkowych też jest bezpieczne. To naprawdę kwestia preferencji. Wolę wykonywać wszystkie operacje kryptograficzne na surowym pliku binarnym i zawsze konwertować na hex / base64 tylko w celu transmisji lub przechowywania.
Scott Arciszewski

Cześć Scott, jest to w zasadzie pytanie nie tylko do odpowiedzi, ale do całego artykułu o funkcji „Zapamiętaj mnie”. Dlaczego nie użyć unikatu idjako selektora? Mam na myśli klucz główny account_recoverytabeli. Nie potrzebujemy dodatkowej warstwy zabezpieczeń selektora, prawda? Dzięki!
Andre Polykanine

id:secretjest OK. selector:secretjest OK. secretsama 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).
Scott Arciszewski

Czy używanie openssl_random_pseudo_byteszamiast tego random_bytesjest szkodliwe, jeśli działa PHP 5.6? Ponadto, czy w zapytaniu o łącze nie należy dołączać tylko selektora, a nie walidatora?
greg,

7

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));

4
Sugerowałbym MCRYPT_DEV_URANDOMwięcej MCRYPT_DEV_RANDOM.
Scott Arciszewski

2

Może to być pomocne, gdy potrzebujesz bardzo losowego tokena

<?php
   echo mb_strtoupper(strval(bin2hex(openssl_random_pseudo_bytes(16))));
?>
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.