best practice per generare token casuali per password dimenticata


94

Voglio generare l'identificatore per la password dimenticata. Ho letto che posso farlo usando il timestamp con mt_rand (), ma alcune persone dicono che il timestamp potrebbe non essere unico ogni volta. Quindi sono un po 'confuso qui. Posso farlo usando il timestamp con questo?

Domanda
Qual è la migliore pratica per generare gettoni casuali / unici di lunghezza personalizzata?

So che ci sono molte domande qui in giro, ma sono sempre più confuso dopo aver letto opinioni diverse da persone diverse.


@AlmaDoMundo: un computer non può dividere il tempo illimitato.
juergen d

@juergend - scusa, non capisco.
Alma Do

Otterrai lo stesso timestamp se lo chiami, ad esempio, un nano secondo a parte. Alcune funzioni temporali, ad esempio, possono restituire il tempo solo in passi di 100 ns, alcune solo in passi di secondi.
juergen d

@juergend ah, quello. Sì. Ho menzionato il timestamp "classico" con solo i secondi. Ma se agisci come hai detto - sì (questo ci lascia solo un'opzione con la macchina del tempo per ottenere un timestamp non univoco)
Alma Do

1
Attenzione , la risposta accettata non fa leva su un CSPRNG .
Scott Arciszewski

Risposte:


147

In PHP, usa random_bytes() . Motivo: stai cercando il modo per ottenere un token di promemoria della password e, se si tratta di credenziali di accesso una tantum, in realtà hai un dato da proteggere (ovvero l'intero account utente)

Quindi, il codice sarà il seguente:

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

Aggiornamento : le versioni precedenti di questa risposta si riferivano uniqid()e questo non è corretto se c'è una questione di sicurezza e non solo di unicità. uniqid()è essenzialmente solo microtime()con qualche codifica. Esistono semplici modi per ottenere previsioni accurate del microtime()sul tuo server. Un utente malintenzionato può inviare una richiesta di reimpostazione della password e quindi provare con un paio di probabili token. Ciò è possibile anche se si utilizza more_entropy, poiché l'entropia aggiuntiva è altrettanto debole. Grazie a @NikiC e @ScottArciszewski per averlo segnalato.

Per maggiori dettagli vedere


21
Nota che random_bytes()è disponibile solo da PHP7. Per le versioni precedenti, la risposta di @yesitsme sembra essere l'opzione migliore.
Gerald Schneider

3
@GeraldSchneider o random_compat , che è il polyfill per queste caratteristiche che ha ricevuto il maggior numero di peer review;)
Scott Arciszewski

Ho creato un campo varchar (64) nel mio database sql per memorizzare questo token. Ho impostato $ length su 64, ma la stringa restituita è lunga 128 caratteri. Come posso ottenere una stringa con una dimensione fissa (qui, 64 quindi)?
Gordie

2
@gordie Imposta la lunghezza su 32, ogni byte è di 2 caratteri esadecimali
JohnHoulderUK

Cosa dovrebbe essere $length? L'id dell'utente? O cosa?
stack

71

Questo risponde alla richiesta "migliore casuale":

La risposta 1 di Adi da Security.StackExchange ha una soluzione per questo:

Assicurati di avere il supporto OpenSSL e non sbaglierai mai con questa riga

$token = bin2hex(openssl_random_pseudo_bytes(16));

1. Adi, lunedì 12 novembre 2018, Celeritas, "Generating an un indovessable token for conferma e-mail", 20 settembre 13 alle 7:06, https://security.stackexchange.com/a/40314/


24
openssl_random_pseudo_bytes($length)- supporto: PHP 5> = 5.3.0, ....................................... ................... (Per PHP 7 e versioni successive, utilizzare random_bytes($length)) ...................... .................... (Per PHP inferiore a 5.3 - non utilizzare PHP inferiore a 5.3)
jave.web

54

La versione precedente della risposta accettata ( md5(uniqid(mt_rand(), true))) è insicura e offre solo circa 2 ^ 60 possibili output, ben all'interno dell'intervallo di una ricerca di forza bruta in circa una settimana per un attaccante a basso budget:

Poiché una chiave DES a 56 bit può essere forzata in circa 24 ore e un caso medio avrebbe circa 59 bit di entropia, possiamo calcolare 2 ^ 59/2 ^ 56 = circa 8 giorni. A seconda di come viene implementata questa verifica del token, potrebbe essere possibile praticamente perdere informazioni sulla temporizzazione e dedurre i primi N byte di un token di ripristino valido .

Poiché la domanda riguarda le "migliori pratiche" e si apre con ...

Voglio generare l'identificatore per la password dimenticata

... possiamo dedurre che questo token ha requisiti di sicurezza impliciti. E quando si aggiungono requisiti di sicurezza a un generatore di numeri casuali, la migliore pratica è quella di utilizzare sempre un generatore di numeri pseudocasuali crittograficamente sicuro (abbreviato CSPRNG).


Utilizzando un CSPRNG

In PHP 7, puoi usare bin2hex(random_bytes($n))(dove $nè un numero intero maggiore di 15).

In PHP 5, puoi utilizzare random_compatper esporre la stessa API.

In alternativa, bin2hex(mcrypt_create_iv($n, MCRYPT_DEV_URANDOM))se hai ext/mcryptinstallato. Un'altra buona battuta è bin2hex(openssl_random_pseudo_bytes($n)).

Separazione della ricerca dal validatore

Prendendo spunto dal mio lavoro precedente sui cookie sicuri "ricordami" in PHP , l'unico modo efficace per mitigare la suddetta perdita di tempo (tipicamente introdotta dalla query del database) è separare la ricerca dalla convalida.

Se la tua tabella ha questo aspetto (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)
);

... devi aggiungere un'altra colonna, in questo selectormodo:

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

Utilizzare un CSPRNG Quando viene emesso un token di reimpostazione della password, inviare entrambi i valori all'utente, memorizzare il selettore e un hash SHA-256 del token casuale nel database. Usa il selettore per prendere l'hash e l'ID utente, calcola l'hash SHA-256 del token che l'utente fornisce con quello memorizzato nel database utilizzando hash_equals().

Codice di esempio

Generazione di un token di ripristino in PHP 7 (o 5.6 con random_compat) con 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')
]);

Verifica del token di ripristino fornito dall'utente:

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

Questi frammenti di codice non sono soluzioni complete (ho evitato la convalida dell'input e le integrazioni del framework), ma dovrebbero servire come esempio di cosa fare.


Quando verifichi il token di ripristino fornito dall'utente, perché utilizzi la rappresentazione binaria del token casuale? Pensi che sarà possibile (e sicuro?): 1) memorizzare in DB il valore esadecimale hash del token con hash('sha256', bin2hex($token)), 2) verificare con if (hash_equals(hash('sha256', $validator), $results[0]['token'])) {...? Grazie!
Guicara

Sì, anche il confronto di stringhe esadecimali è sicuro. È davvero una questione di preferenza. Preferisco eseguire tutte le operazioni crittografiche su binario grezzo e convertirle solo in hex / base64 per la trasmissione o l'archiviazione.
Scott Arciszewski

Ciao Scott, è fondamentalmente una domanda non solo per la tua risposta, ma per l'intero articolo sulla funzione "Ricordami". Perché non utilizzare l'unicità idcome selettore? Voglio dire, la chiave primaria del account_recoverytavolo. Non abbiamo bisogno di un ulteriore livello di sicurezza per il selettore, vero? Grazie!
Andre Polykanine

id:secretè OK. selector:secretè OK. secretdi per sé non lo è. L'obiettivo è separare la query del database (che perde tempo) dal protocollo di autenticazione (che dovrebbe essere tempo costante).
Scott Arciszewski

C'è qualche danno nell'usare openssl_random_pseudo_bytesinvece random_bytesse è in esecuzione PHP 5.6? Inoltre, non dovresti aggiungere solo il selettore e non il validatore nella querystring del collegamento?
greg

7

Puoi anche usare DEV_RANDOM, dove 128 = 1/2 della lunghezza del token generato. Il codice seguente genera 256 token.

$token = bin2hex(mcrypt_create_iv(128, MCRYPT_DEV_RANDOM));

4
Suggerirei MCRYPT_DEV_URANDOMoltre MCRYPT_DEV_RANDOM.
Scott Arciszewski

2

Questo può essere utile ogni volta che hai bisogno di un token molto molto casuale

<?php
   echo mb_strtoupper(strval(bin2hex(openssl_random_pseudo_bytes(16))));
?>
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.