meilleure pratique pour générer un jeton aléatoire pour un mot de passe oublié

94

Je souhaite générer un identifiant pour le mot de passe oublié. J'ai lu que je peux le faire en utilisant horodatage avec mt_rand (), mais certaines personnes disent que l'horodatage n'est peut-être pas unique à chaque fois. Donc je suis un peu confus ici. Puis-je le faire en utilisant l'horodatage avec ceci?

Question
Quelle est la meilleure pratique pour générer des jetons aléatoires / uniques de longueur personnalisée?

Je sais qu'il y a beaucoup de questions posées ici, mais je suis de plus en plus confus après avoir lu les opinions différentes des différentes personnes.

enthousiaste
la source
@AlmaDoMundo: Un ordinateur ne peut pas diviser le temps de manière illimitée.
juergen d
@juergend - désolé, ne comprenez pas cela.
Alma Do
Vous obtiendrez le même horodatage si vous l'appelez par exemple à une nano seconde d'intervalle. Certaines fonctions de temps, par exemple, ne peuvent renvoyer le temps que par pas de 100ns, d'autres seulement par pas de secondes.
juergen d
@juergend ah, ça. Oui. J'ai mentionné l'horodatage «classique» avec des secondes seulement. Mais si agissez comme vous l'avez dit - oui (cela ne nous laisse qu'une option avec Time Machine pour obtenir un horodatage non unique)
Alma Do
1
Attention , la réponse acceptée ne tire pas parti d'un CSPRNG .
Scott Arciszewski

Réponses:

148

En PHP, utilisez random_bytes(). Raison: vous cherchez le moyen d'obtenir un jeton de rappel de mot de passe, et, s'il s'agit d'informations d'identification de connexion uniques, vous avez en fait une donnée à protéger (qui est - tout le compte d'utilisateur)

Ainsi, le code sera le suivant:

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

Mise à jour : les versions précédentes de cette réponse faisaient référence uniqid()et c'est incorrect s'il y a une question de sécurité et pas seulement d'unicité. uniqid()est essentiellement juste microtime()avec un certain codage. Il existe des moyens simples d'obtenir des prévisions précises microtime()sur votre serveur. Un attaquant peut émettre une demande de réinitialisation de mot de passe, puis essayer quelques jetons probables. Ceci est également possible si more_entropy est utilisé, car l'entropie supplémentaire est également faible. Merci à @NikiC et @ScottArciszewski pour l'avoir signalé.

Pour plus de détails, voir

Alma Do
la source
21
Notez que ce random_bytes()n'est disponible qu'à partir de PHP7. Pour les anciennes versions, la réponse de @yesitsme semble être la meilleure option.
Gerald Schneider
3
@GeraldSchneider ou random_compat , qui est le polyfill pour ces fonctionnalités qui a reçu le plus de critiques par les pairs;)
Scott Arciszewski
J'ai créé un champ varchar (64) dans ma base de données SQL pour stocker ce jeton. J'ai mis $ length à 64, mais la chaîne renvoyée contient 128 caractères. Comment puis-je obtenir une chaîne de taille fixe (ici, 64 alors)?
gordie
2
@gordie Définit la longueur sur 32, chaque octet est
composé de
Que devrait être $length? L'identifiant de l'utilisateur? Ou quoi?
stack du
71

Cela répond à la requête `` meilleur aléatoire '':

La réponse 1 d' Adi de Security.StackExchange a une solution pour cela:

Assurez-vous d'avoir le support OpenSSL, et vous ne vous tromperez jamais avec ce one-liner

$token = bin2hex(openssl_random_pseudo_bytes(16));

1. Adi, Mon 12 novembre 2018, Celeritas, «Génération d'un jeton impossible à deviner pour les e-mails de confirmation», 20 septembre 2013 à 7 h 06, https://security.stackexchange.com/a/40314/

Oui c'est moi
la source
24
openssl_random_pseudo_bytes($length)- support: PHP 5> = 5.3.0, ....................................... ................... (Pour PHP 7 et plus, utilisez random_bytes($length)) ...................... .................... (Pour PHP inférieur à 5.3 - ne pas utiliser PHP inférieur à 5.3)
jave.web
54

La version antérieure de la réponse acceptée ( md5(uniqid(mt_rand(), true))) n'est pas sécurisée et n'offre qu'environ 2 ^ 60 sorties possibles - bien dans la portée d'une recherche par force brute dans environ une semaine pour un attaquant à petit budget:

Puisqu'une clé DES de 56 bits peut être forcée brutalement en environ 24 heures , et qu'un cas moyen aurait environ 59 bits d'entropie, nous pouvons calculer 2 ^ 59/2 ^ 56 = environ 8 jours. Selon la façon dont cette vérification de jeton est mise en œuvre, il peut être possible de pratiquement perdre des informations de synchronisation et de déduire les N premiers octets d'un jeton de réinitialisation valide .

Puisque la question porte sur les «bonnes pratiques» et s'ouvre sur ...

Je souhaite générer un identifiant pour le mot de passe oublié

... nous pouvons en déduire que ce jeton a des exigences de sécurité implicites. Et lorsque vous ajoutez des exigences de sécurité à un générateur de nombres aléatoires, la meilleure pratique consiste à toujours utiliser un générateur de nombres pseudo-aléatoires cryptographiquement sécurisé (abrégé CSPRNG).


Utilisation d'un CSPRNG

En PHP 7, vous pouvez utiliser bin2hex(random_bytes($n))(où $nest un entier supérieur à 15).

En PHP 5, vous pouvez utiliser random_compatpour exposer la même API.

Sinon, bin2hex(mcrypt_create_iv($n, MCRYPT_DEV_URANDOM))si vous avez ext/mcryptinstallé. Un autre bon one-liner est bin2hex(openssl_random_pseudo_bytes($n)).

Séparer la recherche du validateur

En tirant de mon travail précédent sur les cookies sécurisés «se souvenir de moi» en PHP , le seul moyen efficace d'atténuer la fuite de synchronisation susmentionnée (généralement introduite par la requête de base de données) est de séparer la recherche de la validation.

Si votre table ressemble à ceci (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)
);

... vous devez ajouter une colonne supplémentaire selector, comme ceci:

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

Utiliser un CSPRNG Lorsqu'un jeton de réinitialisation de mot de passe est émis, envoyez les deux valeurs à l'utilisateur, stockez le sélecteur et un hachage SHA-256 du jeton aléatoire dans la base de données. Utilisez le sélecteur pour récupérer le hachage et l'ID utilisateur, calculez le hachage SHA-256 du jeton fourni par l'utilisateur avec celui stocké dans la base de données hash_equals().

Exemple de code

Générer un jeton de réinitialisation en PHP 7 (ou 5.6 avec random_compat) avec 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')
]);

Vérification du jeton de réinitialisation fourni par l'utilisateur:

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

Ces extraits de code ne sont pas des solutions complètes (j'ai évité la validation d'entrée et les intégrations de framework), mais ils devraient servir d'exemple de ce qu'il faut faire.

Scott Arciszewski
la source
Lorsque vous vérifiez le jeton de réinitialisation fourni par l'utilisateur, pourquoi utilisez-vous la représentation binaire du jeton aléatoire? Pensez-vous qu'il sera possible (et sécurisé?) De: 1) stocker dans la base de données la valeur hexadécimale hachée du jeton avec hash('sha256', bin2hex($token)), 2) vérifier avec if (hash_equals(hash('sha256', $validator), $results[0]['token'])) {...? Merci!
Guicara
Oui, la comparaison des chaînes hexadécimales est également sécurisée. C'est vraiment une question de préférence. Je préfère effectuer toutes les opérations de cryptage sur du binaire brut et ne convertir que jamais en hex / base64 pour la transmission ou le stockage.
Scott Arciszewski
Salut Scott, c'est essentiellement une question non seulement pour votre réponse, mais pour tout l'article sur la fonction "Remember Me". Pourquoi ne pas utiliser l'unique idcomme sélecteur? Je veux dire, la clé primaire de la account_recoverytable. Nous n'avons pas besoin d'une couche de sécurité supplémentaire pour le sélecteur, n'est-ce pas? Merci!
Andre Polykanine
id:secretest OK. selector:secretest OK. secretlui-même ne l'est pas. Le but est de séparer la requête de la base de données (qui manque de temps) du protocole d'authentification (qui doit être à temps constant).
Scott Arciszewski
Y a-t-il un problème à utiliser à la openssl_random_pseudo_bytesplace random_bytessi utilise PHP 5.6? De plus, ne devriez-vous pas ajouter uniquement le sélecteur et non le validateur dans la chaîne de requête du lien?
greg du
7

Vous pouvez également utiliser DEV_RANDOM, où 128 = 1/2 de la longueur du jeton généré. Le code ci-dessous génère 256 jetons.

$token = bin2hex(mcrypt_create_iv(128, MCRYPT_DEV_RANDOM));
Graham T
la source
4
Je suggérerais MCRYPT_DEV_URANDOMplus MCRYPT_DEV_RANDOM.
Scott Arciszewski
2

Cela peut être utile chaque fois que vous avez besoin d'un jeton très aléatoire

<?php
   echo mb_strtoupper(strval(bin2hex(openssl_random_pseudo_bytes(16))));
?>
Ir Californie
la source