~bohwaz/blog/

Avec de vrais morceaux de 2.0 !

Comment stocker les mots de passe

Une seule réponse possible : avec bcrypt !

Pourquoi bcrypt ? Je ne m'étendrais pas sur le sujet, d'autres l'ont déjà fait avant moi, et mieux.

Comme dis et répété depuis longtemps par mes petits camarades (et notamment mat), c'est la seule méthode qu'on devrait utiliser. Et je suis également fautif, jusqu'à quelques années j'utilisais toujours une méthode perso à base de sha1($password . $salt) mais c'est pas pareil ! Alors pourquoi ne pas déjà le faire ? Pourquoi ne pas l'avoir déjà fait depuis longtemps ? Et bien en partie car PHP ne donne pas d'accès simple à bcrypt, qu'il faut passer par la syntaxe un peu ésotérique de crypt pour y avoir accès. Mais pas de panique, c'est quand même simple à faire. Démonstration.

Premièrement nous allons hasher notre mot de passe. Pour cela nous devons générer le salt nous-même, PHP n'ayant pas de méthode native pour le faire, donc on va se baser sur un sha1 de uniqid() qui est nourri par rand(). rand() n'est pas réputé pour être un bon générateur de hasard, il vaudrait mieux utiliser directement /dev/urandom ou openssl, mais pour un cas simple rand() devrait suffire amplement, surtout avec Suhosin qui donne à rand() une vrai capacité de générer du vrai hasard.

$salt = substr(sha1(uniqid(mt_rand(), true)), 0, 22);

Ensuite nous allons hasher le mot de passe avec le salt :

$password = crypt($password, '$2a$08$' . $salt);

Nous avons donc désormais un mot de passe hashé, que nous pouvons stocker tel quel dans la base de données par exemple. Maintenant imaginons que nous voulions vérifier ce mot de passe, par exemple après que l'utilisateur l'ait entré dans un formulaire de connexion à un site ? Et bien c'est très simple, on reprends notre $password que nous avons stocké et on le donne à crypt en second argument, qui va se charger tout seul d'en extraire le salt et renvoyer un hash. Si le hash diffère du hash stocké, c'est que le mot de passe entré est erroné.

if ($password == crypt($_POST['password'], $password))
{
    echo "Connexion réussie !";
}

Et voilà, maintenant vous n'avez plus d'excuse !

Écrire un commentaire
(facultatif)
(facultatif)
(obligatoire)
 _              _       
| |_ __ _ _ __ | |_ ___ 
| __/ _` | '_ \| __/ _ \
| || (_| | | | | ||  __/
 \__\__,_|_| |_|\__\___|
                        
(obligatoire)

Les adresses internet seront converties automatiquement.
Tags autorisés : <blockquote> <cite> <pre> <code> <var> <strong> <em> <del> <ins> <kbd> <samp> <abbr>

Eric

Ca fait quand même un peu "accumulation de plein de trucs pour créer de la sécurité" cette génération du salt.

Tout d'abord si on en est à faire très attention au salage et à son aléas, le minimum c'est de ne remplacer toutes les références à rand() et les remplacer par mt_rand().

Ensuite Je ne vois pas l'utilité de nourrir uniqid() par rand() ou mt_rand(). Le premier paramètre de uniqid() n'est pas une graine d'aléas mais un simple préfixe. En opérant ainsi on s'assure juste que les 1 à 10 premiers caractères résultants sont des chiffres donnés par rand().

Plus gênant, uniqid() est surtout là pour générer des jetons uniques et éventuellement peu prédictibles. Ils sont basés sur l'heure et si on connait à peu près l'heure on connait aussi la forme des premiers caractères résultats.

Au final on a entre 1 et 10 caractères venant de rand() (le plus souvent 9 ou 10), uniquement des chiffres puis les caractères venant de uniqid(), dont on peut très facilement connaitre les 7 ou 8 premiers, et dont le 14 est toujours un point.

Si vraiment il fallait jouer, il aurait au moins fallu faire d'abord un str_reverse sur uniqid, avec ou sans le paramètre d'entropie (vu qu'il est totalement redondant avec mt_rand). Ce sont les caractères vraiments difficiles à prévoir de uniqid qui se seraient retrouvés utilisés.

Le passage par sha1 me gêne lui aussi un peu. Il faudrait savoir exactement le pourquoi de son usage. J'éviterai de me prononcer mais j'ai peur que ce soit plus de l'impression de sécurité que de la sécurité (vu que ceux qui sont à même de prédire le salage avec mt_rand() et uniqid() pourront eux aussi faire le même sha1() pour obtenir le même résultat, on ne brouille donc les choses qu'en apparence).

*

Sinon, si vraiment on souhaite quelque chose de plus complet, comme tu le dis il y a openssl_random_pseudo_bytes() :

substr(str_replace('+', '.', base64_encode(openssl_random_pseudo_bytes(132))), 0,22)

Au moins c'est clair et on voit ce qu'on fait : 132 bits aléatoires sous forme base64.

Comme tu dis (mt_)rand() peut suffire, mais pas besoin de passer par l'accumulation proposée.
http://www.php.net/manual/en/function.crypt.php#102278 donne une bonne possibilité. Même s'il note que quelques bits sur les 128 ne sont pas aléatoires mais fixes, ça sera toujours mieux que le sha1+uniqid+rand :

substr(str_replace('+', '.', base64_encode(pack('N4', mt_rand(), mt_rand(), mt_rand(), mt_rand()))), 0, 22)

Ceci dit au final le salt est là quasi uniquement pour éviter les rainbow table. Il faut qu'il soit raisonnablement unique, qu'un tiers ne puisse pas injecter ou prédire sa valeur trop facilement, mais c'est à peu près tout. Ca devrait suffire. Si on s'en tient à ça, il n'y avait pas besoin d'aller chercher trop loin. N'importe quoi peut presque faire l'affaire.

BohwaZ

Hello,

Effectivement j'ai pas fait gaffe à rand() car avec Suhosin ça renvoie un vrai nombre aléatoire, qu'on utilise rand() ou mt_rand(). Mais je viens de corriger l'article, mt_rand() est logiquement mieux placé en général.

Comme tu l'as dit le salt ne sert qu'à éviter les rainbow tables, c'est pour ça qu'à moins d'être particulièrement parano, le seul fait d'utiliser uniqid par exemple suffirait, on ne cherche pas ici à créer quelque chose d'impossible à deviner.

Je ne connaissais pas l'idée à base de pack, c'est pas mal aussi.

Mais comme j'ai dis, uniqid() seul suffirait, mais je suis un peu parano alors je rajoute un peu de mt_rand() pour la forme. Mais si on est vraiment parano utiliser openssl est plus sûr.