En route vers PHP 5.5 : Sécurité : API simple pour le hachage de mots de passe

18 octobre 2012php, php-5.5
 Cet article a été rédigé il y a plusieurs années et peut ne plus être tout à fait à jour…

Ces derniers mois, plusieurs gros sites ont fait la une des titres d’actualités informatiques, suite à des fuites de listes de logins ou emails + mots de passe ; ceux-ci, trop souvent, étaient stockés non-hachés, ou hachés avec une fonction peu sécurisée, ou non salés, …

Plus ou moins en parallèle, et depuis des années, les applications développées en PHP (et les développeurs ayant travaillé dessus), n’ont pas vraiment la réputation d’utiliser un mécanisme de hachage de mots de passe réellement optimal — une hypothèse étant que hasher1 un mot de passe de manière sécurisée — correcte, si j’ose dire — est suffisamment peu instinctif pour être implémenté de manière appropriée à tous les coups.

Pour essayer d’améliorer les choses, la version courante2 de la branche master de PHP (et donc, probablement, PHP 5.5) apporte une API simple pour le hachage de mots de passe : l’idée est que si PHP fournit nativement quelques fonctions, simples à utiliser et faisant correctement le travail, il y a des chances qu’elles soient petit à petit utilisées, que les mots de passe soient stockés de manière plus sécuritée… et qu’au final, tout le monde y gagne.

Hacher un mot de passe avec la nouvelle API

La nouvelle API, dite “simplifiée”, a pour objectif de rendre le hachage de mots de passe plus simple : elle doit pouvoir être utilisée facilement et permettre un hachage de mot de passe sans avoir à se poser de question comme “quel algorithme utiliser ?”, “dois-je utiliser un salt ?”, “comment doit-il être généré ?”, “combien de champs en base de données dois-je utiliser ?”, …

En fait, à partir du moment où vous avez un mot de passer à hacher, il suffit d’un seul appel de fonction :

$password = '@zErt|123';
$hash = password_hash($password, PASSWORD_DEFAULT);
var_dump($hash);

En quelques mots, il suffit de passer à password_hash() le mot de passe à hacher, ainsi que l’algorithme à utiliser — considérant que l’algorithme par défaut est censé être tout à fait adapté.

Si vous exécutez cette portion de code, vous obtiendrez en sortie quelque chose de ce type (qui change à chaque appel, puisque le salt est généré aléatoirement et que le hash en dépend) :

string(60) "$2y$10$0XPjx7G0kX1OxopwGQbsIehW/D1X95vajb45APWz5EZx.aJpXEdli"

La sortie de password_hash() contient tout ce qui est requis pour, ultérieurement, vérifier le hash : salt, hash, algorithme utilisé.

De la sorte, une seule chaine de caractères est à stocker : un seul champ “mot de passe haché” dans votre base de données — et pas besoin d’un second champ pour le salt, ni de garder trace de l’algorithme utilisé (en particulier pour le cas où un autre algorithme serait supporté par cette API, à l’avenir : cette fonction continuera de fonctionner, tout comme la vérification, sans que vous n’ayez à modifier quoi que ce soit à votre code).

En fait, la sortie de password_hash() contient quatre informations :

  • L’algorithme utilisé ; pour l’exemple pris ici, 2y correspond à l’algorithme bcrypt, qui est actuellement le seul supporté par cette nouvelle API,
  • Le paramètre de coût utilisé ; ici, 10
  • Le couple salt utilisé (qui, pour cet exemple, a été généré automatiquement par la fonction) + hash généré : 0XPjx7G0kX1OxopwGQbsIehW/D1X95vajb45APWz5EZx.aJpXEdli

Spécifier l’algorithme utilisé

En fonction de vos besoins ou préférences, il est possible de personnaliser le comportement de la fonction password_hash() ; par exemple, en lui spécifiant l’agorithme que vous souhaitez utiliser :

$password = '@zErt|123';
$hash = password_hash($password, PASSWORD_BCRYPT);
var_dump($hash);

La sortie ressemblera à ce que nous avons obtenu précédemment :

string(60) "$2y$10$1xpgT3cLt86T1guNcG0UW.pd6WFKmqqjp08ZgxR.ztpJ0TmdQMh9C"

Note : aujourd’hui, un seul algorithme est implémenté : bcrypt.

Utiliser PASSWORD_BCRYPT ou PASSWORD_DEFAULT revient donc aujourd’hui au même, puisque le premier est celui par défaut ; mais autant spécifier PASSWORD_DEFAULT, en prévision d’une éventuelle future version de PHP, où un nouvel algorithme, plus performant, deviendrait celui sélectionné par défaut.

Spécifier le salt manuellement

Si vous en éprouvez le besoin, vous pouvez spécifier manuellement le salt qui sera utilisé par la fonction password_hash(), en tirant parti du troisième paramètre, optionnel, qu’elle accepte — et en y faisant figurer une entrée nommée salt :

$password = '@zErt|123';
$salt = mcrypt_create_iv(32, MCRYPT_DEV_URANDOM);
$hash = password_hash($password, PASSWORD_DEFAULT, array(
    'salt' => $salt, 
));
var_dump($salt, $hash);

Notez qu’utiliser un salt trop court lévera un avertissement :

Warning:  password_hash(): Provided salt is too short: 5 expecting 22 
    in /.../php-5.5/tests/security/password-api-5.php on line 28

Aussi, n’oubliez pas qu’il est important d’utiliser, pour chaque mot de passe distinct, un salt unique.

Spécifier un coût

Le troisième paramètre, optionnel, que nous avons utilisé juste au-dessus pour spécifier manuellement un salt peut aussi être utilisé pour spécifier le coût — dans le but de contrôler la quantité de temps CPU qui sera investie pour la création du hash :

$password = '@zErt|123';
$hash = password_hash($password, PASSWORD_DEFAULT, array(
    'cost' => 14, 
));
var_dump($hash);

Et la sortie correspondante (qui, encore une fois, changera à chaque appel) :

string(60) "$2y$14$rUR3Pv2qSLth7inydeNxQ.n6YF/AUP1/4UTOk1dAZ/6c/pKL2NRVi"

Le cost par défaut est censé être adapté, que ce soit niveau temps ou sécurité, pour des machines telles que celles à notre disposition de nos jours ; augmenter la valeur de cost améliorera la solidité du hash, en échange de plus de temps CPU consommé.

Notez qu’un intérêt de l’algorithme bcrypt, par rapport à d’autres comme md5 ou sha1, est qu’il est conçu pour être lent, de manière à rendre extêmement difficiles les attaques par brute-force : idéalement, le coût spécifié devrait entrainer un temps de calcul de l’ordre du quart ou de la demie seconde.

A vous de mesurer quel coût est le plus adapté, en fonction de la capacité de votre matériel d’une part, et du temps CPU que vous pouvez investir, d’autre part.

Vérifier si un mot de passe est correct

Savoir hacher un mot de passe, c’est bien ; mais il faut aussi savoir vérifier si un mot de passe saisi est valide — autrement dit, s’il correspond au hash que nous avons stocké précédemmet (typiquement, lorsqu’un utilisateur essaye de s’identifier, nous voulons vérifier que le mot de passe qu’il vient de saisir est le même que celui qu’il avait saisi lors de son inscription).

Pour cela, ici encore, une seule fonction à appeler, qui se charge de tout le travail : password_verify().

Cette fonction attend en paramètres le mot de passe, et le hash que nous avions stocké précédemment :

$password = '@zErt|123';
$hash = '$2y$10$0XPjx7G0kX1OxopwGQbsIehW/D1X95vajb45APWz5EZx.aJpXEdli';

if (password_verify($password, $hash)) {
    echo "OK";
}
else {
    echo "KO";
}

En sortie, un simple true ou false, selon si le mot de passe correspond ou non au hash :

OK

Du fait que toutes les informations requises sont stockées directement dans le hash (algorithme, coût, salt, et hash en lui-même), cette fonction n’a besoin d’aucun autre paramètre.

Attention : lorsque vous vérifiez si un mot de passe correspond à un hash stocké, pour lutter contre les timing attacks (attaques temporelles), veillez à systématiquement utiliser cette fonction, plutôt qu’à re-hasher le mot de passe avec password_hash() et comparer le résultat avec le hash stocké : password_verify() vérifiera systématiquement l’ensemble des octets des deux hashes, de manière à fournir une comparaison à temps constant.

Quelques fonctions utilitaires

En plus des fonctions password_hash() et password_verify() qui permettent respectivement de hacher un mot de passe et d’en vérifier un, la nouvelle API de hachage de mots de passe fournit deux fonctions utilitaires.

La première, password_get_info(), permet d’obtenir des informations quant au hash qu’on lui passe en paramètre :

$hash = '$2y$10$0XPjx7G0kX1OxopwGQbsIehW/D1X95vajb45APWz5EZx.aJpXEdli';
$info = password_get_info($hash);
var_dump($info);

Avec cet exemple de code, la sortie obtenue est la suivante :

array(3) {
  ["algo"]=>
  int(1)
  ["algoName"]=>
  string(6) "bcrypt"
  ["options"]=>
  array(1) {
    ["cost"]=>
    int(10)
  }
}

En somme, password_get_info() permet d’extraire du hash les informations qui correspondent aux paramètres utilisés pour le créer — sauf le salt.


La seconde fonction, password_needs_rehash(), permet de savoir s’il est nécessaire de re-hasher un mot de passe :

$options = array(
    'cost' => 16, 
);
$hash = '$2y$10$0XPjx7G0kX1OxopwGQbsIehW/D1X95vajb45APWz5EZx.aJpXEdli';

if (password_needs_rehash($hash, PASSWORD_DEFAULT, $options)) {
    echo "Re-hachage requis";
    // TODO : re-hacher le mot de passe, stocker le nouveau hash
}
else {
    echo "Re-hachage non requis";
}

Pour faire bref, cette fonction permet de déterminer si un mot de passe doit être re-haché — ce qui serait le cas, par exemple, après un changement d’algorithme ou de coût.

Voir aussi

Pour cet article, je vais poster quelques liens en plus de celui vers la RFC correspondant à la fonctionnalité…

RFC, Slides, Vidéo

Tout d’abord, comme pour toute fonctionnalité ajoutée à PHP, celle-ci est passée par une RFC et un vote : RFC: Adding simple password hashing API.

La semaine dernière, Anthony Ferrara (@ircmaxell sur Twitter), l’auteur de la RFC et développeur de cette nouvelle API, l’a présentée à la conférence PHP North West ; il a depuis publié ses slides, ainsi que la vidéo de sa présentation : Password Hashing in PHP Talk.

L’enregistrement est en anglais, mais la présentation va plus loin que juste passer sur les nouvelles fonctions et leur utilisation comme je l’ai fait dans cet article3 : elle explique comment des mots de passe peuvent être attaqués, à quel point il peut être rapide de cracker des mots de passe hachés de manière inefficace, pourquoi il est important de les hacher efficacement avant de les stocker, … bref, 23 minutes intéressantes, et une vidéo que je vous encourage à regarder !

Compatibilité PHP 5.3 et 5.4

Si vous souhaitez commencer à utiliser ces nouvelles fonctions de (probablement) PHP 5.5 dans votre code écrit en PHP 5.3 ou 5.4, Anthony a mis à disposition une bibliothèque nommée password_compat, écrite en PHP, qui fournit les mêmes fonctionnalités et qui est compatible avec l’implémentation native intégrée à PHP 5.5.
Vous pouvez donc dès maintenant utiliser cette API simplifiée, tout en bénéficiant de l’implémentation native le jour où vous passerez à PHP 5.5.

@ljouanneau me faisait d’ailleurs remarquer hier qu’il propose un fork permettant d’utiliser cette bibliothèque sur des version de PHP >= 5.3.3 et <= 5.3.7, ce qui permet de travailler avec sous Debian Squeeze.

J'ai tendance à utiliser de manière quelque peu interchangeable le terme anglais de [*"hash"*](http://en.wikipedia.org/wiki/Hash_function), quitte à le franciser ici et là en le conjuguant *(désolé pour ceux dont ça rayerait les yeux ^^)*, et le terme français de [*"hachage"*](http://fr.wikipedia.org/wiki/Fonction_de_hachage) *(qui rayera certainement les yeux de quelques autres d'entre vous ^^)* ; désolé, habitude.
Cette fonctionnalité a été implémentée par [@ircmaxell](https://twitter.com/ircmaxell) sur une branche de [son clone de PHP](https://github.com/ircmaxell/php-src), et a été [mergée sur la branche `master` de PHP](https://github.com/php/php-src/commit/9aacdf6e892fe46526e1e60a3b3fea1b1c350699) mardi 16 octobre 2012 -- il était donc possible de commencer à expérimenter avec cette fonctionnalité depuis quelques temps *(en compilant PHP depuis la branche / le clone en question)*, même si elle n'a réellement été intégrée que récemment au repository officiel.

  1. “` Mon but dans cet article n’était pas de parler de l’importance du hachage de mots de passe, ou de réellement entrer dans le sujet de la sécurité — mais “juste” de présenter les quelques fonctions fournies par cette nouvelle API simplifiée. 

Vous avez apprécié cet article ? Faites-le savoir !

Ce blog a récemment été migré vers un générateur de sites statiques et je n'ai pas encore eu le temps de remettre un mécanisme de commentaires en place.

Avec un peu de chance, je parviendrai à m'en occuper d'ici quelques semaines ;-)