I. Introduction

I-A. L'importance de hasher les mots de passe

Stocker un mot de passe ne se fait pas de n'importe quelle façon et surtout pas en clair. C'est certes très pratique, car on peut redonner facilement son mot de passe à un utilisateur mais ce n'est pas sécurisé ! Si une personne malveillante accède à vos bases de données, il accède à toutes les informations des utilisateurs. Et comme un utilisateur utilise très souvent le même mot de passe partout (grosse erreur), vous offrez accès à bien plus que votre propre site.

Hasher les mots de passe est donc indispensable !

Note : on ne confondra pas cryptage et hashage, le premier est réversible, pas le second (en théorie).

I-B. Les solutions historiques

Quand on parcourt le web à la recherche de tutoriel, on vous conseille généralement l'utilisation de MD5 pour hasher les mots de passe.

Hash MD5
Sélectionnez
$hash = md5('Mot de passe';

C'était une solution intelligente il y a 10 ou 15 ans, aujourd'hui il existe des tonnes de tables de comparaison (rainbow table) pour MD5 ce qui le rend quasi inutile pour tous les mots de passe issus du dictionnaire. Sans compter que les performances actuelles des CPU et GPU rendent le brute force relativement rapideBenchlark d'outil de bruteforce.

La mode de MD5 passée, c'est SHA qui a pris la suite. Bien meilleur que MD5, on peut encore l'utiliser aujourd'hui notamment si on lui associe un sel pour rendre inutile les rainbow tables.

Hash sha1
Sélectionnez
$hash = sha1('Mot de passe';

I-C. Bonnes pratiques

Vous l'aurez compris, stocker un mot de passe en clair est une faute grave et ne se justifie jamais. Votre mot de passe est une donnée ultrasensible et ne doit être connu que de vous.

I-C-1. Algorithme

Aujourd'hui la référence pour la hashage est Bcrypt basé sur blowfish, cet algorithme possède deux avantages :

  • il utilise un sel aléatoire pour créer le hash ;
  • il est adaptatif, c'est-à-dire que l'on peut augmenter le nombre d'itérations pour le rendre plus lent et donc s'adapter à la montée en puissance du matériel.

L'inconvénient majeur c'est que les anciennes versions de PHP ne proposent pas de fonction simple pour générer un hash.

Il faut alors passer par la fonction crypt()Documentation et la génération de grains de sel aléatoires, ce qui peut se révéler être assez complexe finalement. Pour les plus curieux vous pouvez trouver un exemple d'implémentation de Bcrypt dans Pry, mon framework PHP5.

I-C-2. Comment récupérer un mot de passe ?

Pour faire simple, on ne peut pas et on ne doit pas pouvoir !

Si un utilisateur perd son mot de passe on lui demande de le réinitialiser avec un nouveau mot de passe. On s'assure en général de l'identité de l'utilisateur via l'envoi d'un lien par mail permettant d'activer la réinitialisation du mot de passe.

Un site qui vous permet de retrouver votre mot de passe est un site non sécurisé.

Soit le site ne protège absolument pas votre mot de passe, il est donc visible sans le moindre effort à toute personne ayant accès à la base.

Soit le site a mis en place un cryptage (réversible). Pour les mots de passe, le cryptage est une fausse bonne idée puisque l'administrateur du site qui possède la clé de décryptage peut avoir accès à vos informations. De plus la clé de décryptage est en général sur le serveur, un hacker pourrait la trouver, et donc décrypter toutes les données cryptées.

I-C-3. Résumé

Pour être sécurisé il faut donc respecter ces points :

  • pas de mot de passe en clair ;
  • pas de mot de passe crypté ;
  • mot de passe hashé avec Bcrypt.

La nouvelle API pour la gestion des mots de passe de PHP intègre Bcrypt, et nous permet d'appliquer simplement le principe le plus important dans la gestion des mots de passe : le hashage !

II. Hasher un mot de passe

PHP met à disposition la fonction password_hash()Documentation pour hasher les chaînes de caractères. L'utilisation la plus simple mais non recommandée est :

Hasher via password_hash
Sélectionnez
$pass = 'Mot de passe' ;
$hash = password_hash($pass, PASSWORD_DEFAULT) ;

Cette utilisation est non recommandée, car non pérenne. La constante PASSWORD_DEFAULT fait référence à l'algorithme par défaut défini dans PHP, algorithme qui est susceptible de changer d'une version à l'autre. On préférera donc spécifier l'algorithme à utiliser :

Hash avec algorithme spécifique
Sélectionnez
$pass = 'Mot de passe' ;
$hash = password_hash($pass,PASSWORD_BCRYPT) ;

À l'heure actuelle seul PASSWORD_BCRYPT est disponible, mais d'autres pourraient faire leur apparition et être listés sur la page dédiéeListe des contantes.

Le troisième paramètre de la fonction de hash permet de spécifier des options pour l'algorithme, comme le sel ou la complexité :

Option de hash
Sélectionnez
$pass = 'Mot de passe' ;
$hash = password_hash($pass,PASSWORD_BCRYPT,['cost' => 13];

Dans cet exemple on augmente le coût de l'algorithme le rendant par la même occasion plus lent et donc plus dur à « brute-forcer ».

Il existe également l'option « salt », qui permet de générer ses propres grains de sel. Je vous déconseille de l'utiliser, PHP génère déjà tout seul des sels aléatoires. À moins d'être un expert dans le domaine vous ne feriez que réduire la sécurité du hash.

II-A. Comment déterminer le bon coût

Nous l'avons évoqué juste avant, il est possible de spécifier un coût à l'algorithme. Trop faible, le hash s'expose au brute force, trop fort, il mettra trop de temps à être généré.

Pour déterminer le coût idéal en fonction du matériel disponible, on peut utiliser ce code :

Déterminer le coût idéal
Sélectionnez
function getOptimalCost($timeTarget)
{ 
    $cost = 9;
    do {
        $cost++;
        $start = microtime(true);
        password_hash("test", PASSWORD_BCRYPT, ["cost" => $cost]);
        $end = microtime(true);
    } while (($end - $start) < $timeTarget);
    
    return $cost;
}

echo getOptimalCost(0.3);

On choisira en général un temps d'exécution entre 0,1 s et 0,5 s ; c'est le bon compromis entre sécurité et attente de l'utilisateur.

III. Comparer un mot de passe et un hash

Vous l'aurez peut-être remarqué, le hash d'une même chaîne ne donne jamais le même résultat, dans ce cas comment le comparer ?

PHP met à disposition la fonction password_verify()Documentation, pour cela :

Comparer hash et mot de passe
Sélectionnez
if(password_verify('ADMIN', '$2a$10$GlvaE1qXuYE6O/ICVtPTeOf3QwE6QNB2quHgqpbK2JKzDYCNnyAL6')) {
    echo 'OK';
} else {
    echo 'ERREUR';
}

IV. Vérifier un hash

Comme il est possible de passer des options au hash, il se peut qu'au fil du temps un hash ne soit plus compatible car, par exemple, on a souhaité augmenter le coût de l'algorithme.

Il est possible pour commencer d'avoir les informations sur un hash via la fonction password_get_info()Documentation.

Info Hash
Sélectionnez
print_r(password_get_info($hash));

Cela affichera le type d'algorithme ainsi que les options utilisées :

Résultat
Sélectionnez

Array
(
   [algo] => 1
   [algoName] => bcrypt
   [options] => Array
   (
	 [cost] => 12
   )
)

Pratique, mais il est plus simple d'utiliser directement la fonction password_needs_rehash()Documentation qui va vérifier la validité d'un hash avec les options fournies :

Test de hash
Sélectionnez
if (password_verify($pass, $hash)) {
    if (password_needs_rehash($hash, $algorithm, $options)) {
        $hash = password_hash($pass, $algorithm, $options);
    }
}

On peut utiliser ce test après avoir changé le coût de l'algorithme pour permettre aux utilisateurs de réinitialiser leur mot de passe de manière progressive, au fur et à mesure qu'ils accèdent au site.

V. Alternative pour PHP < 5.5

Si vous ne pouvez pas encore utiliser PHP 5.5 ou que vous devez supporter plusieurs versions de PHP, tout n'est pas perdu.

Il existe de nombreuses alternatives, la plus connue étant très probablement password_compat qui implémente exactement la même API pour des versions de PHP à partir de 5.3.7

https://github.com/ircmaxell/password_compat

VI. Conclusion

Plus d'excuse désormais, vous avez tous les outils nécessaires à la sécurisation de vos mots de passe et ce, même avec des versions de PHP plus anciennes.

N'oubliez pas de traiter les données sensibles de vos utilisateurs comme si c'était les vôtres.

VII. Remerciement

Merci à christele_r et Bovino pour leur relecture technique ainsi qu'à f-leb pour les corrections.

26 commentaires Donner une note à l'article (5)