En route vers PHP 5.5 : Sécurité : ajout de la fonction hash_pbkdf2()

30 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…

La version courante de la branche master de PHP (et donc, probablement, PHP 5.5) apporte une implémentation native de l’algorithme PBKDF2.

Cet algorithme est décrit par la RFC 2898 ; son idée est d’appliquer une fonction pseudo-aléatoire à une entrée, en l’associant à un salage, et de répéter ce processus un grand nombre de fois afin de produire une clef dérivée, qui pourra alors être utilisée comme clef cryptographique ; la répétition du processus rend le craquage de clef plus difficile.

PBKDF2 peut par exemple être utilisée pour dériver un mot de passer à utiliser pour encrypter / décrypter un fichier.
Aussi, PBKDF2 est la solution recommandée par le NIST pour le stockage de mots de passe (dans le cas où se conformer aux recommandations du NIST n’est pas requis, vous devriez plutôt utiliser BCrypt – à ce sujet, vous pouvez lire l’article « En route vers PHP 5.5 : Sécurité : API simple pour le hachage de mots de passe », que j’ai publié il y a quelques jours).

Nouvelle fonction hash_pbkdf2()

La nouvelle fonction hash_pbkdf2() accepte plusieurs paramètres :

  • Algorithme de hachage à utiliser
    • 'sha512' est actuellement un des algorithme de hachage les plus puissant inclu avec PHP, et constitue une bonne primitive pour cette fonction,
    • 'sha256' peut aussi être considéré comme suffisamment solide,
    • 'sha1' et 'md5' peuvent aussi être utilisés avec hash_pbkdf2()
  • Sel : le second paramètre passé à hash_pbkdf2() devrait être une chaine de caractères aléatoire d’au moins 64 bits d’entropie
    • Au moins 8 octets de long si générée depuis une fonction comme mcrypt_create_iv(),
    • Ou au moins 11 caractères si ceux-ci ne contiennent que des lettres et chiffres.
    • Note : le sel devrait être généré aléatoirement pour chaque mot de passe haché (et non commun à tous les mots de passe d’une base d’utilisateurs, par exemple), et stocké avec la clef générée.
  • Nombre d’itérations à utiliser
    • Pour un usage web, un minimum de 1000 itérations est recommandé,
    • Mais puisque la puissance matérielle des serveurs varie, vous devriez considérer que le nombre d’itérations idéal devrait mener à une fonction durant entre un et cinq dizièmes de seconde : utiliser un grand nombre d’itérations augmente la résitance aux attaques par brute-force.
  • Longueur attendue pour la clef générée
    • Par défaut, la clef générée sera de la même longueur que la sortie de l’algorithme de hachage utilisé,
    • Mais, en fonction de vos besoins (par exemple, certains algorithmes d’encryption attendent une clef de longueur spécifique), une autre longueur peut être spécifiée.
  • Sortie brute
    • Si ce dernier paramètre vaut false (ce qui est le cas par défaut), la clef générée sera retournée sous forme d’une chaine de caractères hexadécimale,
    • Si ce dernier paramètre vaut true, la clef générée sera retournée sous la forme d’une chaine biraire.

Quelques exemples

Les exemples que je reproduis ci-dessous sont fortement inspirés de ceux fournis dans la RFC ; merci à son auteur, Anthony Ferrara ;-)

Pour encrypter un fichier

Dans le cas où vous souhaitez encrypter un fichier ou une donnée quelconque, vous ne devriez pas utiliser directement le mot de passe souhaité ; à la place, vous devriez le dériver à l’aide d’une fonction comme hash_pbkdf2().

Par exemple, pour encrypter une donnée, vous pourriez utiliser une portion de code semblable à celle-ci :

$texte = "Bonjour. Ceci est un texte à encrypter !";

$password = "azerty123";
$salt = mcrypt_create_iv(16, MCRYPT_DEV_URANDOM);
$key = hash_pbkdf2("sha512", $password, $salt, 10000, 16, true);

$iv = mcrypt_create_iv(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC), MCRYPT_DEV_URANDOM);

$encrypte = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $texte, MCRYPT_MODE_CBC, $iv);
var_dump($encrypte);

Et vous obtiendriez en sortie une chaine binaire, correspondant à votre texte encrypté :

string(48) "˒��5]u�'}���ɑ]����(4�'A�����XҢH��e\�s%"

Et pour décrypter, dans l’autre sens :

$decrypte = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, $encrypte, MCRYPT_MODE_CBC, $iv);
var_dump($decrypte);

Ce qui vous renverrait votre chaine de départ :

string(48) "Bonjour. Ceci est un texte à encrypter !"

En somme, une seule différence par rapport à ce que vous auriez probablement instinctivement fait : rajouter une passe de dérivation du mot de passe, qui rendra plus difficile une éventuelle attaque par brute-force.

Pour du stockage de mots de passe

hash_pbkdf2() n’est pas « la » fonction recommandée pour du stockage de mot de passe, mais est de suffisamment bonne qualité pour pouvoir être utilisée dans ce but : elle implémente tout de même l’algorithme recommandé par le NIST.

Pour faire simple, à partir du moment où vous avez un mot de passe, vous générez un sel aléatoire qui ne sera utilisé que pour ce mot de passe là (un salt différent par mot de passe, donc), et il ne vous reste plus qu’à invoquer hash_pbkdf2() :

$password = "azerty123";
$salt = mcrypt_create_iv(16, MCRYPT_DEV_URANDOM);
$key = hash_pbkdf2("sha512", $password, $salt, 5000, 16, true);

var_dump($password);
var_dump($salt);
var_dump($key);

En fonction du salt, différent à chaque appel, vous obtiendrez quelque chose de ce type, avec un sortie binaire, puisque true a été passé en dernier paramètre à hash_pbkdf2() :

string(9) "azerty123"
string(16) "èn¹ «K“BÎеƒýÖ÷¶"
string(16) "@+u5IÇ«É1ßOäÆç"

Alternativement, si vous souhaitez obtenir une représentation textuelle hexadécimale, en passant false en sixième paramètre à hash_pbkdf2(), comme ceci :

$password = "azerty123";
$salt = mcrypt_create_iv(16, MCRYPT_DEV_URANDOM);
$key = hash_pbkdf2("sha512", $password, $salt, 5000, 16, false);

var_dump($password);
var_dump($salt);
var_dump($key);

Vous obtiendrez alors une sortie ressemblant à celle-ci :

string(9) "azerty123"
string(16) "lÒCmld“|ý¯ÙãwÆw"
string(16) "0a84597f010ed505"


Note : de manière générale, pour hacher un mot de passe avec PHP 5.5 (il existe une bibliothèque de compatibilité pour PHP 5.3 et 5.4), vous devriez utiliser la nouvelle API simple pour le hachage de mots de passe, et pas hash_pbkdf2().

Une fonction lente ?

Le quatrième paramètre accepté par hash_pbkdf2() est le nombre d’itérations que la fonction va utiliser en interne.

Plus ce nombre est élevé, plus le résultat généré sera résistant à une attaque par brute-force – mais, en même temps, plus la fonction sera lente.

Pour illustrer ceci, j’ai joué la portion de code suivante :

$password = "azerty123";
$salt = mcrypt_create_iv(16, MCRYPT_DEV_URANDOM);

foreach ([100, 1000, 2500, 5000, 10000, 15000, 20000, 30000, 40000, 50000, 75000, 100000] as $nbIterations) {
    $before = microtime(true);
    for ($i=0 ; $i<500 ; $i++) {
        $key = hash_pbkdf2("sha512", $password, $salt, $nbIterations, 16);
    }
    echo "hash_pbkdf2 $nbIterations : " . ((microtime(true)-$before)*1000)/500 . " ms\n";
}

Sur ma machine (un core 2 à 2.8 GHz ; pas quelque chose de « tout récent », donc), j’ai obtenu les résultats suivants :

hash_pbkdf2 100 : 0.33216190338135 ms
hash_pbkdf2 1000 : 3.2877559661865 ms
hash_pbkdf2 2500 : 8.2311177253723 ms
hash_pbkdf2 5000 : 16.46826171875 ms
hash_pbkdf2 10000 : 32.885704040527 ms
hash_pbkdf2 15000 : 49.359501838684 ms
hash_pbkdf2 20000 : 65.565310001373 ms
hash_pbkdf2 30000 : 98.355558395386 ms
hash_pbkdf2 40000 : 131.08198404312 ms
hash_pbkdf2 50000 : 164.11205816269 ms
hash_pbkdf2 75000 : 245.84366607666 ms
hash_pbkdf2 100000 : 327.56755208969 ms

Avec 1000 itérations (le minimum recommandé), il faut environ 3 ms pour que la fonction s’exécute ; alors qu’on monte à 328 ms pour 100,000 itérations.

Notez que, selon toute probabilité, vous n’utiliserez pas une fonction de ce type très souvent : si on considère que vous l’utilisez pour stocker un mot de passe, vous l’utiliserez une fois à l’inscription d’un utilisateur, et une autre fois à chaque fois qu’il essaye de s’identifier sur votre site – ça fait quoi ? Au maximum, quelque chose comme une fois par utilisateur et par jour ?
Autrement dit, n’hésitez pas à investir un peu de temps CPU pour la sécurité ;-)

Voir aussi