Si vous êtes développeur web, vous avez probablement déjà créé un système de connexion. Et si c'est le cas, vous avez dû utiliser une certaine forme de hachage pour protéger les mots de passe des utilisateurs en cas d'une violation de sécurité de votre application/plateforme.
Il y a beaucoup d'idées contradictoires sur la façon de créer correctement le hash d'un mot de passe. Le hachage d'un mot de passe est l'une des choses les plus simples mais la plupart des développeurs le font mal. Et c'est tout le fait de ce tutoriel dédié aux développeurs où je vais vous expliquer comment stocker en toute sécurité les mots de passes dans une base de données et pourquoi il doit être fait d'une certaine façon.
Bon bien sûr sur votre application/site vous devez "forcer" l'utilisateur à saisir un mot de passe avec un minimum de caractères et des caractères spéciaux car aujourd'hui un mot de passe trop simple peut être cracké/sniffé en un rien de temps.
Voici les différentes parties de ce tutoriel :
- Qu'est-ce que le hash d'un mot de passe ?
- Comment le hash d'un mot de passe est-il cracké ?
- Méthodes de hachage efficaces
- Quel algorithme de hachage utiliser ?
- Comment renforcer encore plus sa méthode de hachage ?
- Code PHP : Méthode de base
- Code PHP : Méthode level up
- Code PHP : Méthode shield
- Conclusion
Les algorithmes de hachage sont des fonctions à "sens unique". Cela signifie qu'il traduit n'importe quelle quantité de données en un checksum de taille fixe qui ne peut être inversé. Et si un infime changement est opéré dans ces données, le hachage qui en résulte sera complètement différent.
Maintenant vous pouvez penser que le stockage du hash d'un mot de passe utilisateur en base de données est suffisant si il advenait à être dérobé par un pirate. Alors bien que le hash normal est nettement préférable que de stocker le mot de passe en clair, il y a beaucoup de façons de retranscrire rapidement les mots de passe hachés. Environ 40% des hashs classiques peuvent être retranscris via des dictionnaires, des lookup tables, des rainbow tables ou bien en brute force.
Comme je vous le disais dans le chapitre ci-dessus, il y a plusieurs façons de cracker un mot de passe haché.
La plus simple étant l'attaque par dictionnaires et brute force. Là il s'agit de deviner le mot de passe en utilisant une liste de mots ou des dictionnaires de mots de passe pré-existants. Des logiciels comme Hash Code Cracker le font.
Les attaques par brute force sont les mêmes sauf qu'ici le mot de passe est généré aléatoirement et ne provient pas d'un dictionnaire. Cette technique a évoluée ces dernières années en utilisant maintenant les processeurs graphiques qui peuvent accélérer jusqu'à plus de 100 fois le processus. Des logiciels gratuits comme oclHashcat le font très bien.
Ces deux méthodes peuvent prendre plus ou moins beaucoup de temps suivant l'algorithme de hash employé mais aussi (et surtout) de la méthode de hachage.
Passons aux attaques via Lookup Tables (ou table de correspondance). Imaginons que vous avez une base de données avec 1 million de hashs. Si vous souhaitez effectuer une attaque par dictionnaire, vous devrez effectuer 1 million de fois l'opération. Ici avec une table de correspondance associant un hash au mot de passe vous irez beaucoup plus vite à le décoder mais encore faut-il avoir une table de très grosse taille.
Des dumps de ce genre de tables ayant plus de 20 milliards d'occurrences existent sur le net. Le plus souvent celles-ci utilise un serveur de base de données de type NoSQL (big data).
Les Rainbow Tables (ou table arc-en-ciel) sont un hybride des Lookup Tables et de brute force. Pour plus d'infos il y a un article complet sur Wikipedia.
Pour lutter contre les Lookup Tables ou les Rainbow Tables, tout ce que nous avons à faire est de donner à chaque mot de passe un salt long et unique. Un salt c'est une chaîne de caractère aléatoire qui sera concaténée au mot de passe avant son hachage. Ainsi les attaques via Lookup Tables ou Rainbow Tables deviendront inutiles.
Afin de garantir l'authenticité du salt, il est préférable d'utiliser une chaîne de caractère générée de façon aléatoire qui soit au moins aussi longue que le hash lui-même. C'est important que cette chaîne soit aussi longue, c'est un peu le même principe que le chiffre de Vernam. En gros si votre hash fait 256 bits de longs, utilisez un salt de 256 bits.
Pour moi l'une des meilleures façon de générer une chaîne hexadécimale aléatoire est d'utiliser un CSPRNG (Cryptographically Secure Pseudo-Random Number Generator). N'utilisez pas les bibliothèques de maths comme rand() en PHP. Tiens d'ailleurs si vous êtes en PHP, vous pouvez utiliser la fonction mcrypt_create_iv() comme bon CSPRNG. Et comme vous voulez que chaque mot de passe ait son propre salt, il est important de changer celui-ci à chaque fois que le mot de passe est changé.
L'algorithme pour stocker le mot de passe en base de données serait alors le suivant :
- Générer un salt en utilisant un CSPRNG.
- Calculer le hash via la fonction $hash = hash("sha256", $password . $salt);
- Sauvegarder le hash et le salt en base de données.
Pour avoir un exemple de fonction, reportez-vous à la partie où je fournis le code PHP pour la méthode de base.
Ci-dessus je vous ai mis en exemple l'algorithme SHA256.
De la même manière vous pouvez utiliser les suivants :
- Famille SHA2 : SHA256 ou SHA512
- RipeMD160
- WHIRLPOOL
Par contre je vous déconseille fortement :
- MD5
- SHA0 ou SHA1
- crypt (à moins qu'il utilise SHA 256 ou SHA512)
- Aucune algorithme qui n'a subi d'audit poussé comme lors de la NIST hash function competition qui est une compétition ayant pour objectif de trouver une nouvelle fonction de hachage.
Pour renforcer un peu plus votre hash, vous pouvez faire une fonction récursive qui retraitera le hash généré x fois :
$hash = hash("sha256", $password . $salt); for( $i=0; $i<1000; $i++ ) $hash = hash("sha256", $hash);
La fonction ci-dessus va hacher jusqu'à 1.000 fois le hash généré de façon récursive. Cette procédure va bien entendue ralentir de 1.000 fois processus de hash mais il sera aussi 1000 fois plus difficile à casser par brute force. Cette procédure s'appelle une key stretching. Dans le code PHP level up, je vous donne un exemple de key stretching en utilisant l'algorithme PBKDF2.
Alors maintenant vous allez me demander le nombre d'itérations recommandées pour cette procédure. Voici la chronologie que j'ai pu en tirer en épluchant le net :
- Septembre 2000, L'IETF indique dans sa RFC 2898 qu'il faut itérer jusqu'à 1.000 fois.
- Février 2005, le protocole d'authentification réseau Kerberos 5 utilise 4.096 itérations par défaut (RFC 3962)
- Septembre 2010, ElcomSoft affirme que iOS 3.x utilise 2.000 itérations, iOS 4.x utilise 10.000 itérations et que Blackberry n'en utilise qu'un (sans préciser lequel).
- Mai 2011, LastPass indique utiliser jusqu'à 100.000 itérations de SHA256.
Vous pouvez voir avec ces quelques exemples qu'au fil des années le nombre d'itérations augmentent grandement. Alors pourquoi se limiter et ne pas mettre jusqu'à 1 million d'itérations ?
Tout simplement parce que le nombre d'itérations consomme pas mal de temps CPU. Mais cela dépend encore plus du nombre d'authentifications des utilisateurs par seconde. C'est donc à vous de voir
Voici le code PHP pour la méthode expliquée dans le chapitre de méthode de hachage efficace. Cette implémentation est sécurisée et peut être utilisée sur un grand nombre d'applications/sites web.
La première méthode hash_password() va vous permettre de stocker le mot de passe et le salt dans une seule colonne de votre base de données en concaténant le salt et le hash.
function hash_password($password){ // 256 bits random string $salt = bin2hex(mcrypt_create_iv(32, MCRYPT_DEV_URANDOM)); // prepend salt then hash $hash = hash("sha256", $password . $salt); // return salt and hash in the same string return $salt . $hash; }
La deuxième fonction va vous permettre de vérifier le mot de passe saisi par l'utilisateur.
function check_password($password, $dbhash) { // get salt from dbhash $salt = substr($dbhash, 0, 64); // get the SHA256 hash $valid_hash = substr($dbhash, 64, 64); // hash the password $test_hash = hash("sha256", $password . $salt); // test return $test_hash === $valid_hash; }
Voici la méthode level up dont je vous parlais dans le chapitre pour renforcer encore plus le hash d'un mot de passe. Ici nous allons utiliser l'algorithme PBKDF2 (Password Based Key Derivation Function). Le code ci-dessous est issu du domaine public et peut être utilisé dans n'importe quel but. Il est également conforme à de nombreux vecteurs de test (RFC 6070). Le nombre minimum d'itérations conseillée est de 1.024. Utilisez un des algorithmes que je recommande dans le chapitre des algorithmes de hachage recommandés.
/* * PBKDF2 key derivation function as defined by RSA's PKCS #5: https://www.ietf.org/rfc/rfc2898.txt * $algorithm - The hash algorithm to use. Recommended: SHA256 * $password - The password. * $salt - A salt that is unique to the password. * $count - Iteration count. Higher = better. Recommended: At least 1024. * $key_length - The length of the derived key in BYTES. * Returns: A $key_length-byte key derived from the password and salt (in binary). * * Test vectors can be found here: https://www.ietf.org/rfc/rfc6070.txt */ function pbkdf2($algorithm, $password, $salt, $count, $key_length){ $algorithm = strtolower($algorithm); if(!in_array($algorithm, hash_algos(), true)) die('PBKDF2 ERROR: Invalid hash algorithm.'); if($count < 0 || $key_length < 0) die('PBKDF2 ERROR: Invalid parameters.'); if($key_length > 4294967295) die('PBKDF2 ERROR: Derived key too long.'); $hLen = strlen(hash($algorithm, "", true)); $numBlocks = (int)ceil((double)$key_length / $hLen); $output = ""; for($i = 1; $i <= $numBlocks; $i++) { $output .= pbkdf2_f($password, $salt, $count, $i, $algorithm, $hLen); } return substr($output, 0, $key_length); } /* * The pseudorandom function used by PBKDF2. * Definition: https://www.ietf.org/rfc/rfc2898.txt */ function pbkdf2_f($password, $salt, $count, $i, $algorithm, $hLen){ //$i encoded as 4 bytes, big endian. $last = $salt . chr(($i >> 24) % 256) . chr(($i >> 16) % 256) . chr(($i >> 8) % 256) . chr($i % 256); $xorsum = ""; for($r = 0; $r < $count; $r++) { $u = hash_hmac($algorithm, $last, $password, true); $last = $u; if(empty($xorsum)) $xorsum = $u; else { for($c = 0; $c < $hLen; $c++) { $xorsum[$c] = chr(ord(substr($xorsum, $c, 1)) ^ ord(substr($u, $c, 1))); } } } return $xorsum; }
Aujourd'hui, bcrypt est sans doute la meilleure façon de hacher un mot de passe. C'est un algorithme de hachage qui s'adapte en fonction de la puissance de votre machine. Il utilise Eksblowfish qui se rapproche beaucoup de Blowfish. La phase de hachage est la même que Blowfish mais la phase de planification de la clé de Eksblowfish assure que tout état ultérieur dépend à la fois du salt et du mot de passe utilisateur et aucun état ne peut être précalculé à l'insu des deux.
En PHP, vous pouvez utiliser la fonction crypt() pour générer un hash bcrypt. La classe ci-dessous vous permettra de faire sans problème et elle génèrera automatiquement un salt.
class Bcrypt { private $rounds; public function __construct($rounds = 12) { if(CRYPT_BLOWFISH != 1) { throw new Exception("bcrypt not supported. See http://php.net/crypt"); } $this->rounds = $rounds; } public function hash($input) { $hash = crypt($input, $this->getSalt()); if(strlen($hash) > 13) return $hash; return false; } public function verify($input, $existingHash) { $hash = crypt($input, $existingHash); return $hash === $existingHash; } private function getSalt() { $salt = sprintf('$2a$%02d$', $this->rounds); $bytes = $this->getRandomBytes(16); $salt .= $this->encodeBytes($bytes); return $salt; } private $randomState; private function getRandomBytes($count) { $bytes = ''; if(function_exists('openssl_random_pseudo_bytes') && (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN')) { // OpenSSL slow on Win $bytes = openssl_random_pseudo_bytes($count); } if($bytes === '' && is_readable('/dev/urandom') && ($hRand = @fopen('/dev/urandom', 'rb')) !== FALSE) { $bytes = fread($hRand, $count); fclose($hRand); } if(strlen($bytes) < $count) { $bytes = ''; if($this->randomState === null) { $this->randomState = microtime(); if(function_exists('getmypid')) { $this->randomState .= getmypid(); } } for($i = 0; $i < $count; $i += 16) { $this->randomState = md5(microtime() . $this->randomState); if (PHP_VERSION >= '5') { $bytes .= md5($this->randomState, true); } else { $bytes .= pack('H*', md5($this->randomState)); } } $bytes = substr($bytes, 0, $count); } return $bytes; } private function encodeBytes($input) { // The following is code from the PHP Password Hashing Framework $itoa64 = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; $output = ''; $i = 0; do { $c1 = ord($input[$i++]); $output .= $itoa64[$c1 >> 2]; $c1 = ($c1 & 0x03) << 4; if ($i >= 16) { $output .= $itoa64[$c1]; break; } $c2 = ord($input[$i++]); $c1 |= $c2 >> 4; $output .= $itoa64[$c1]; $c1 = ($c2 & 0x0f) << 2; $c2 = ord($input[$i++]); $c1 |= $c2 >> 6; $output .= $itoa64[$c1]; $output .= $itoa64[$c2 & 0x3f]; } while (1); return $output; } }
Et pour l'utiliser :
$bcrypt = new Bcrypt(15); $hash = $bcrypt->hash('password'); $isGood = $bcrypt->verify('password', $hash);
Cette classe provient de Stack Overflow et je la recommande fortement! Si cette classe ne fonctionne pas chez vous, rabattez-vous sur la méthode level up qui est aussi très bien!
Je vous conseille également de vous pencher sur PHPASS qui est un framework de hachage largement utilisé sur le web, notamment dans les systèmes phpbb, WordPress, Vanilla, Drupal, bbPress ou encore intégré a des modules tels que mod_auth_mysql pour apache.
Voilà j'espère ne pas avoir trop fait compliqué. J'ai essayé de faire le plus court possible tout en vous expliquant plutôt que de cracher du code tout fait. Ces méthodes sont abordables par n'importe quel développeur mais doivent s'adapter en fonction du traffic de votre application/site et des capacités de votre serveur.
MISES A JOUR DE L'ARTICLE |
bon article, merci.
Je ne comprends pas bien le mécanisme si j'utilise un salt aléatoire pour le calcul d'un hash d'un mot de passe d'une application. Au moment où je vais comparer ce hash au mot de passe envoyé par l'utilisateur + fonction de hash comment aurais-je connaissance du salt qui avait été utilisé ?
Ok, tout compris (j'ai trouvé la réponse à ma question précédente en lisant le code de hash_password et check_password).
désolé pour le bruit !
Article sympa.. Je reconnais ne pas avoir forcément besoin d'une telle 'artillerie' mais c'est bien d'avoir des articles à la fois pointus et abordables..
Je testerai quand même sur PHP pour ma culture perso.
S.
Salut, article niquel,
cependant je met en place le système de base et je me rend compte que en voulant récupérer le hash valid dans la fonction check_password. Celui ci n'existe pas puisque l'on essaye de récupérer la valeur a partir du 64eme caractères.... alors que le hash du password et du salt concaténé fait 64 caractères.
Une idée ?
merci !
Merci énormément pour ce code !
Très utile
Le chiffrement est la méthode que l'on utiliser pour crypter un password et le stocker en base.
Mais quelque soit la méthode (crypter ou pas), si quelqu'un trouve mon mots de passe il accédera a mes données.
D’où ma question certainement stupide, en fin de compte a quoi cela sert 'il de chiffrer les password.
Alors certain dirons c'est au cas ou on pirate ta DB. Dans ce cas il n'y a toujours pas d’intérêt a crypter le password car le pirate a accès a tous les contenus stocké en DB.
Décidément bien que l'article soit super intéressant sur la méthode, je ne voie pas l’intérêt d'une tel chose.
Qq peut il m'éclairer ?
@Topheur
Si le pirate a accès à ta DB et que les mots de passe sont en clair, non seulement il pourra lire les contenus de la DB mais aussi se connecter sur le site sous l'identité qu'il veut.
L'intérêt de chiffrer les mots de passe est de limiter le préjudice à la divulgation des infos de la DB.
@Topheur Et comme souvent les clients utilisent les mêmes mot de passe d'un site à l'autre, ça permet de limiter les dégâts...
Bonsoir
très bon mini tutoriel !
Des améliorations à faire dans ta classe
afin de permettre notamment à ton algo de supporter
les versions de PHP relativement récente par exemple à partir de la version 4 et aussi les CPU
car tout le monde ne s'y connait forcement pas en serveurs( utilisateur connecté par secondes ou par minutes).
moi je l'ai amélioré et c'est un peu plus flexible et robuste en ligne de code mais sa vaut le coup.
Encore merci pour le partage .
Hello !
Je ne comprends pas la fonction de vérif de mot de passe de la première méthode.
Si :
salt = azerty
mdp = wxcvbn
salt_crypté = iiiiiiiiii
mdp_crypté = pppppppppp
mdp à tester = wxcvbX
On a donc deux arguments, le mot de passe à tester (mdp à tester = wxcvbX) et le hash du mot de passe concaténé au salt (dbhash = ppppppppppiiiiiiiiii)
On récupère le salt_crypté grâce à la fonction substr($dbhash, 0, 64).
On récupère le mdp_crypté avec substr($dbhash, 64, 64).
On hash le mdp à tester pour pouvoir le comparer hash("sha256", $mdp à tester . $salt_crypté).
Et c'est la où ça plante dans mon cerveau, on hash donc "wxcvbXiiiiiiiiii" et non pas "wxcvbXazerty", la comparaison suivante ne peut donc pas fonctionner...
Comment récupère-t-on le salt non crypté en fait ?
Moi je n'ai pas compris
)
Un petit éclaircicement serait fort apprécié (et je pense que Shakealot ne serait pas contre également