Best Practice zum Generieren eines zufälligen Tokens für ein vergessenes Passwort

94

Ich möchte eine Kennung für ein vergessenes Passwort generieren. Ich habe gelesen, dass ich es mit einem Zeitstempel mit mt_rand () tun kann, aber einige Leute sagen, dass der Zeitstempel möglicherweise nicht jedes Mal eindeutig ist. Also bin ich hier etwas verwirrt. Kann ich damit Zeitstempel verwenden?

Frage
Was ist die beste Vorgehensweise, um zufällige / eindeutige Token mit benutzerdefinierter Länge zu generieren?

Ich weiß, dass hier viele Fragen gestellt werden, aber ich werde verwirrter, nachdem ich unterschiedliche Meinungen von den verschiedenen Leuten gelesen habe.

daran interessiert
quelle
@AlmaDoMundo: Ein Computer kann die Zeit nicht unbegrenzt teilen.
Jürgen d
@juergend - sorry, verstehe das nicht.
Alma Do
Sie erhalten den gleichen Zeitstempel, wenn Sie ihn beispielsweise im Abstand von einer Nanosekunde nennen. Einige Zeitfunktionen können beispielsweise die Zeit nur in Schritten von 100 ns zurückgeben, andere nur in Schritten von Sekunden.
Jürgen d
@juergend ah, das. Ja. Ich erwähnte den "klassischen" Zeitstempel nur mit Sekunden. Aber wenn Sie sich so verhalten, wie Sie es gesagt haben - ja (das lässt uns nur eine Option mit Zeitmaschine, um einen nicht eindeutigen Zeitstempel zu erhalten)
Alma Do
1
Head's up, die akzeptierte Antwort nutzt kein CSPRNG .
Scott Arciszewski

Antworten:

147

Verwenden Sie in PHP random_bytes(). Grund: Sie suchen nach einer Möglichkeit, ein Kennworterinnerungstoken zu erhalten. Wenn es sich um einmalige Anmeldeinformationen handelt, müssen Sie tatsächlich Daten schützen (dh das gesamte Benutzerkonto).

Der Code lautet also wie folgt:

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

Update : Auf frühere Versionen dieser Antwort wurde verwiesenuniqid() und das ist falsch, wenn es um Sicherheit und nicht nur um Einzigartigkeit geht. uniqid()ist im Wesentlichen nur microtime()mit etwas Codierung. Es gibt einfache Möglichkeiten, genaue Vorhersagen über die microtime()auf Ihrem Server zu erhalten. Ein Angreifer kann eine Anforderung zum Zurücksetzen des Kennworts ausgeben und dann einige wahrscheinliche Token durchgehen. Dies ist auch möglich, wenn more_entropy verwendet wird, da die zusätzliche Entropie ähnlich schwach ist. Vielen Dank an @NikiC und @ScottArciszewski für diesen Hinweis.

Weitere Details finden Sie unter

Alma Do.
quelle
21
Beachten Sie, dass dies random_bytes()nur ab PHP7 verfügbar ist. Für ältere Versionen scheint die Antwort von @yesitsme die beste Option zu sein.
Gerald Schneider
3
@GeraldSchneider oder random_compat , das ist die Polyfüllung für diese Funktionen, die die meisten Peer-Review erhalten hat;)
Scott Arciszewski
Ich habe ein varchar (64) -Feld in meiner SQL-Datenbank erstellt, um dieses Token zu speichern. Ich habe $ length auf 64 gesetzt, aber die zurückgegebene Zeichenfolge ist 128 Zeichen lang. Wie kann ich eine Zeichenfolge mit einer festen Größe erhalten (hier dann 64)?
Gordon
2
@gordie Setze die Länge auf 32, jedes Byte besteht aus 2 Hex-Zeichen
JohnHoulderUK
Was soll sein $length? Die ID des Benutzers? Oder was?
Stapel
71

Dies beantwortet die 'beste zufällige' Anfrage:

Adis Antwort 1 von Security.StackExchange hat eine Lösung dafür:

Stellen Sie sicher, dass Sie OpenSSL-Unterstützung haben und dass Sie mit diesem Einzeiler nichts falsch machen

$token = bin2hex(openssl_random_pseudo_bytes(16));

1. Adi, Montag, 12. November 2018, Celeritas, "Generieren eines nicht bewertbaren Tokens für Bestätigungs-E-Mails", 20. September 13, 7:06 Uhr, https://security.stackexchange.com/a/40314/

Ja, ich bin es
quelle
24
openssl_random_pseudo_bytes($length)- Unterstützung: PHP 5> = 5.3.0, ....................................... ................... (Verwenden Sie für PHP 7 und random_bytes($length)höher) ...................... .................... (Für PHP unter 5.3 - verwenden Sie PHP nicht unter 5.3)
jave.web
54

Die frühere Version der akzeptierten Antwort ( md5(uniqid(mt_rand(), true))) ist unsicher und bietet nur etwa 2 ^ 60 mögliche Ausgaben - im Bereich einer Brute-Force-Suche in etwa einer Woche für einen Low-Budget-Angreifer:

Da ein 56-Bit-DES-Schlüssel in ungefähr 24 Stunden brutal erzwungen werden kann und ein durchschnittlicher Fall ungefähr 59 Bit Entropie haben würde, können wir 2 ^ 59/2 ^ 56 = ungefähr 8 Tage berechnen. Abhängig davon, wie diese Token-Überprüfung implementiert ist, kann es möglich sein, Zeitinformationen praktisch zu verlieren und auf die ersten N Bytes eines gültigen Reset-Tokens zu schließen .

Da es sich bei der Frage um "Best Practices" handelt und mit ...

Ich möchte eine Kennung für ein vergessenes Passwort generieren

... können wir daraus schließen, dass dieses Token implizite Sicherheitsanforderungen hat. Wenn Sie einem Zufallszahlengenerator Sicherheitsanforderungen hinzufügen, empfiehlt es sich, immer einen kryptografisch sicheren Pseudozufallszahlengenerator (abgekürzt CSPRNG) zu verwenden.


Verwenden eines CSPRNG

In PHP 7 können Sie verwenden bin2hex(random_bytes($n))(wobei $neine Ganzzahl größer als 15 ist).

In PHP 5 können Sie random_compatdieselbe API verfügbar machen.

Alternativ, bin2hex(mcrypt_create_iv($n, MCRYPT_DEV_URANDOM))wenn Sie ext/mcryptinstalliert haben. Ein weiterer guter Einzeiler ist bin2hex(openssl_random_pseudo_bytes($n)).

Trennen der Suche vom Validator

Aus meiner früheren Arbeit an sicheren "Remember Me" -Cookies in PHP besteht die einzige wirksame Möglichkeit, das oben erwähnte Timing-Leck (das normalerweise durch die Datenbankabfrage verursacht wird) zu verringern, darin, die Suche von der Validierung zu trennen.

Wenn Ihre Tabelle so aussieht (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)
);

... Sie müssen eine weitere Spalte hinzufügen selector, wie folgt:

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

Verwenden eines CSPRNG Wenn ein Token zum Zurücksetzen des Kennworts ausgegeben wird, senden Sie beide Werte an den Benutzer, speichern Sie den Selektor und einen SHA-256-Hash des zufälligen Tokens in der Datenbank. Verwenden Sie den Selektor, um den Hash und die Benutzer-ID abzurufen, und berechnen Sie den SHA-256-Hash des Tokens, den der Benutzer mit dem in der Datenbank gespeicherten Token bereitstellt hash_equals().

Beispielcode

Generieren eines Reset-Tokens in PHP 7 (oder 5.6 mit random_compat) mit 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')
]);

Überprüfen des vom Benutzer bereitgestellten Reset-Tokens:

$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.
}

Diese Codefragmente sind keine vollständigen Lösungen (ich habe die Eingabevalidierung und die Framework-Integration vermieden), aber sie sollten als Beispiel dafür dienen, was zu tun ist.

Scott Arciszewski
quelle
Warum verwenden Sie die binäre Darstellung des zufälligen Tokens, wenn Sie das vom Benutzer bereitgestellte Reset-Token überprüfen? Glauben Sie, dass es möglich (und sicher?) Ist, 1) den Hash-Hex-Wert des Tokens in der DB zu speichern hash('sha256', bin2hex($token)), 2) mit zu verifizieren if (hash_equals(hash('sha256', $validator), $results[0]['token'])) {...? Vielen Dank!
Guicara
Ja, der Vergleich von Hex-Strings ist ebenfalls sicher. Es ist wirklich eine Frage der Präferenz. Ich bevorzuge es, alle Kryptooperationen mit rohen Binärdateien durchzuführen und immer nur zur Übertragung oder Speicherung in hex / base64 zu konvertieren.
Scott Arciszewski
Hallo Scott, es ist im Grunde eine Frage nicht nur für Ihre Antwort, sondern für den gesamten Artikel über die Funktion "Remember Me". Warum nicht das Unique idals Selektor verwenden? Ich meine, der Primärschlüssel der account_recoveryTabelle. Wir brauchen keine zusätzliche Sicherheitsebene für den Selektor, oder? Vielen Dank!
Andre Polykanine
id:secretist in Ordnung. selector:secretist in Ordnung. secretselbst ist nicht. Das Ziel besteht darin, die Datenbankabfrage (die zeitlich undicht ist) vom Authentifizierungsprotokoll (das eine konstante Zeit sein sollte) zu trennen.
Scott Arciszewski
Gibt es einen Schaden bei der Verwendung openssl_random_pseudo_bytesstatt , random_byteswenn läuft PHP 5.6? Sollten Sie nicht auch nur den Selektor und nicht den Validator an den Querystring des Links anhängen?
Greg
7

Sie können auch DEV_RANDOM verwenden, wobei 128 = 1/2 der generierten Tokenlänge ist. Der folgende Code generiert 256 Token.

$token = bin2hex(mcrypt_create_iv(128, MCRYPT_DEV_RANDOM));
Graham T.
quelle
4
Ich würde MCRYPT_DEV_URANDOMüber vorschlagen MCRYPT_DEV_RANDOM.
Scott Arciszewski
2

Dies kann hilfreich sein, wenn Sie ein sehr, sehr zufälliges Token benötigen

<?php
   echo mb_strtoupper(strval(bin2hex(openssl_random_pseudo_bytes(16))));
?>
Ir Calif
quelle