Compréhension des fonctions de hachage et sécurisation des mots de passe

De temps en temps, des serveurs et des bases de données sont volés ou compromis. Dans cet esprit, il est important de s'assurer que certaines données utilisateur cruciales, telles que les mots de passe, ne peuvent pas être récupérées. Aujourd'hui, nous allons apprendre les bases du hachage et ce qu'il faut faire pour protéger les mots de passe dans vos applications Web..

Tutoriel republié

Toutes les quelques semaines, nous revoyons certains des articles préférés de nos lecteurs tout au long de l'histoire du site. Ce tutoriel a été publié pour la première fois en janvier 2011.


1. Déni de responsabilité

La cryptologie est un sujet suffisamment complexe et je ne suis en aucun cas un expert. Des recherches sont en cours dans ce domaine dans de nombreuses universités et agences de sécurité..

Dans cet article, je vais essayer de garder les choses aussi simples que possible, tout en vous présentant une méthode relativement sûre de stockage des mots de passe dans une application Web..


2. Que fait le "hachage"??

Le hachage convertit une donnée (petite ou grande) en une donnée relativement courte telle qu'une chaîne ou un entier.

Ceci est accompli en utilisant une fonction de hachage à sens unique. "À sens unique" signifie qu'il est très difficile (ou pratiquement impossible) de l'inverser.

Un exemple courant de fonction de hachage est md5 (), très populaire dans de nombreux langages et systèmes..

$ data = "Hello World"; $ hash = md5 ($ data); echo $ hash; // b10a8db164e0754105b7a99be72e3fe5

Avec md5 (), le résultat sera toujours une chaîne de 32 caractères. Mais, il ne contient que des caractères hexadécimaux; techniquement, il peut également être représenté par un entier de 128 bits (16 octets). Tu peux md5 () des chaînes et des données beaucoup plus longues, et vous aurez toujours un hachage de cette longueur. Ce seul fait pourrait vous donner une idée de la raison pour laquelle cela est considéré comme une fonction "à sens unique".


3. Utilisation d'une fonction de hachage pour stocker les mots de passe

Le processus habituel lors de l'enregistrement d'un utilisateur:

  • L'utilisateur remplit le formulaire d'inscription, y compris le champ du mot de passe.
  • Le script Web stocke toutes les informations dans une base de données.
  • Cependant, le mot de passe est exécuté via une fonction de hachage, avant d'être stocké.
  • La version originale du mot de passe n'a été stockée nulle part, elle est donc techniquement supprimée..

Et le processus de connexion:

  • L'utilisateur entre son nom d'utilisateur (ou adresse électronique) et son mot de passe.
  • Le script exécute le mot de passe via la même fonction de hachage.
  • Le script trouve l'enregistrement utilisateur dans la base de données et lit le mot de passe haché stocké..
  • Ces deux valeurs sont comparées et l'accès est accordé si elles correspondent..

Une fois que nous avons choisi une méthode décente pour le hachage du mot de passe, nous allons implémenter ce processus ultérieurement dans cet article..

Notez que le mot de passe d'origine n'a jamais été stocké nulle part. Si la base de données est volée, les identifiants des utilisateurs ne peuvent pas être compromis, non? Eh bien, la réponse est "ça dépend". Regardons quelques problèmes potentiels.


4. Problème n ° 1: collision de hachage

Une "collision" de hachage se produit lorsque deux entrées de données différentes génèrent le même hachage résultant. La probabilité que cela se produise dépend de la fonction que vous utilisez..

Comment cela peut-il être exploité?

A titre d'exemple, j'ai vu d'anciens scripts utilisant crc32 () pour hacher les mots de passe. Cette fonction génère un entier de 32 bits comme résultat. Cela signifie qu’il n’ya que 2 ^ 32 (4 294 967 296) résultats possibles.

Disons un mot de passe:

echo crc32 ('supersecretpassword'); // sorties: 323322056

Maintenant, assumons le rôle d'une personne qui a volé une base de données et qui a la valeur de hachage. Il se peut que nous ne puissions pas convertir 323322056 en "supersecretpassword". Cependant, nous pouvons trouver un autre mot de passe qui sera converti dans la même valeur de hachage, avec un script simple:

set_time_limit (0); $ i = 0; while (true) if (crc32 (base64_encode ($ i)) == 323322056) echo base64_encode ($ i); sortie;  $ i ++; 

Cela peut durer un certain temps, mais il devrait éventuellement renvoyer une chaîne. Nous pouvons utiliser cette chaîne renvoyée - au lieu de 'supersecretpassword' - et cela nous permettra de nous connecter au compte de cette personne..

Par exemple, après avoir exécuté ce script pendant quelques instants sur mon ordinateur, on m'a donné 'MTIxMjY5MTAwNg =='. Testons le:

echo crc32 ('supersecretpassword'); // sorties: 323322056 echo crc32 ('MTIxMjY5MTAwNg =='); // sorties: 323322056

Comment cela peut-il être évité?

De nos jours, un puissant PC domestique peut être utilisé pour exécuter une fonction de hachage presque un milliard de fois par seconde. Nous avons donc besoin d'une fonction de hachage qui a très grande gamme.

Par exemple, md5 () pourrait convenir, car il génère des hachages de 128 bits. Cela se traduit par 340,282,366,920,938,463,463,374,607,431,768,211,456 résultats possibles. Il est impossible de parcourir autant d'itérations pour trouver des collisions. Cependant, certaines personnes ont encore trouvé le moyen de le faire (voir ici).

Sha1

Sha1 () est une meilleure alternative et génère une valeur de hachage de 160 bits encore plus longue..


5. Problème n ° 2: les tables arc-en-ciel

Même si nous réglons le problème de la collision, nous ne sommes toujours pas en sécurité.

Une table arc-en-ciel est construite en calculant les valeurs de hachage des mots couramment utilisés et leurs combinaisons.

Ces tables peuvent avoir des millions, voire des milliards de lignes.

Par exemple, vous pouvez utiliser un dictionnaire et générer des valeurs de hachage pour chaque mot. Vous pouvez également commencer à combiner des mots et générer des hachages pour ceux-là aussi. Ce n'est pas tout; vous pouvez même commencer à ajouter des chiffres avant / après / entre les mots et les stocker également dans la table.

Considérant le coût peu élevé du stockage, on peut produire et utiliser de gigantesques tables Rainbow..

Comment cela peut-il être exploité?

Imaginons qu'une grande base de données soit volée, avec 10 millions de mots de passe hachés. Il est assez facile de rechercher la table arc-en-ciel pour chacune d’elles. On ne les trouvera pas tous, certes, mais néanmoins… certains le seront.!

Comment cela peut-il être évité?

Nous pouvons essayer d'ajouter un "sel". Voici un exemple:

$ password = "easypassword"; // cela peut être trouvé dans une table arc-en-ciel // parce que le mot de passe contient 2 mots communs echo sha1 ($ password); // 6c94d3b42518febd4ad747801d50a8972022f956 // utilise un tas de caractères aléatoires, et il peut être plus long que cela $ salt = "f # @ V) Hu ^% Hgfds"; // ceci ne sera pas trouvé dans les tables arc-en-ciel pré-construites echo sha1 ($ salt. $ password); // cd56a16759623378628c0d9336af69b74d9d71a5

En gros, nous concaténons la chaîne "salt" avec les mots de passe avant de les hacher. La chaîne résultante ne sera évidemment pas sur une table arc-en-ciel pré-construite. Mais nous ne sommes toujours pas en sécurité!


6. Problème n ° 3: Rainbow Tables (encore)

Rappelez-vous qu'une table Rainbow peut être créée à partir de zéro, après que la base de données ait été volée.

Comment cela peut-il être exploité?

Même si un sel a été utilisé, celui-ci peut avoir été volé avec la base de données. Tout ce qu'ils ont à faire est de générer une nouvelle table Rainbow à partir de rien, mais cette fois, ils concaténent le sel à chaque mot qu'ils mettent dans la table..

Par exemple, dans une Rainbow Table générique, "easypassword"peut exister. Mais dans cette nouvelle table Rainbow, ils ont"f # @ V) Hu ^% Hgfdseasypassword"aussi. Quand ils utiliseront les 10 millions de hashes salés volés contre cette table, ils pourront à nouveau trouver des allumettes.

Comment cela peut-il être évité?

Nous pouvons utiliser un "sel unique" à la place, qui change pour chaque utilisateur.

Un candidat pour ce type de sel est la valeur de l'ID utilisateur de la base de données:

$ hash = sha1 ($ user_id. $ password);

Cela suppose que le numéro d'identification d'un utilisateur ne change jamais, ce qui est généralement le cas.

Nous pouvons également générer une chaîne aléatoire pour chaque utilisateur et l’utiliser comme sel unique. Mais nous devrions nous assurer que nous stockons cela dans la fiche de l'utilisateur quelque part.

// génère une fonction de chaîne aléatoire de 22 caractères unique unique_salt () return substr (sha1 (mt_rand ()), 0,22);  $ unique_salt = unique_salt (); $ hash = sha1 ($ unique_salt. $ password); // et enregistrez le $ unique_salt avec la fiche utilisateur //… 

Cette méthode nous protège contre Rainbow Tables, car maintenant chaque mot de passe a été traité avec une valeur différente. L’attaquant devrait générer 10 millions de tables Rainbow distinctes, ce qui serait tout à fait irréaliste..


7. Problème n ° 4: la vitesse de hachage

La plupart des fonctions de hachage ont été conçues pour être rapides, car elles sont souvent utilisées pour calculer des valeurs de somme de contrôle pour des fichiers et des fichiers volumineux, afin de vérifier l'intégrité des données..

Comment cela peut-il être exploité?

Comme je l'ai déjà mentionné, un PC moderne doté de puissants GPU (oui, de cartes vidéo) peut être programmé pour calculer environ un milliard de hachages par seconde. De cette façon, ils peuvent utiliser une attaque par force brute pour essayer tous les mots de passe possibles..

Vous pensez peut-être qu'exiger un mot de passe de 8 caractères au moins le gardera d'une attaque par force brute, mais déterminons s'il en est bien ainsi:

  • Si le mot de passe peut contenir des lettres minuscules, majuscules et des chiffres, cela signifie 62 (26 + 26 + 10) caractères possibles.
  • Une chaîne de 8 caractères contient 62 ^ 8 versions possibles. C'est un peu plus de 218 milliards de dollars.
  • À un taux de 1 milliard de hachages par seconde, cela peut être résolu en environ 60 heures.

Et pour les mots de passe de 6 caractères, ce qui est également assez commun, cela prendrait moins de 1 minute.

N'hésitez pas à exiger des mots de passe de 9 ou 10 caractères, mais vous pourriez commencer à agacer certains de vos utilisateurs..

Comment cela peut-il être évité?

Utilisez une fonction de hachage plus lente.

Imaginez que vous utilisiez une fonction de hachage qui ne peut s'exécuter que 1 million de fois par seconde sur le même matériel, au lieu de 1 milliard de fois par seconde. Il faudrait alors 1000 fois plus de temps à l'attaquant pour forcer brutalement un hash. 60 heures deviendraient presque 7 ans!

Une façon de le faire serait de le mettre en œuvre vous-même:

function myhash ($ mot de passe, $ unique_salt) $ salt = "f # @ V) Hu ^% Hgfds"; $ hash = sha1 ($ unique_salt. $ password); // le fait prendre 1000 fois plus longtemps pour ($ i = 0; $ i < 1000; $i++)  $hash = sha1($hash);  return $hash; 

Vous pouvez également utiliser un algorithme prenant en charge un "paramètre de coût", tel que BLOWFISH. En PHP, cela peut être fait en utilisant le crypte() une fonction.

function myhash ($ mot de passe, $ unique_salt) // le sel pour Blowfish devrait être long de 22 caractères. crypt ($ mot de passe, '$ 2a $ 10 $'. $ unique_salt); 

Le deuxième paramètre à la crypte() fonction contient des valeurs séparées par le signe dollar ($).

La première valeur est '$ 2a', ce qui indique que nous allons utiliser l'algorithme BLOWFISH.

La deuxième valeur, "$ 10" dans ce cas, est le "paramètre de coût". Il s’agit du logarithme en base 2 du nombre d’itérations qu’il exécutera (10 => 2 ^ 10 = 1024 itérations). Ce nombre peut aller de 04 à 31..

Courons un exemple:

function myhash ($ mot de passe, $ unique_salt) return crypt ($ mot de passe, '$ 2a $ 10 $'. $ unique_salt);  function unique_salt () return substr (sha1 (mt_rand ()), 0,22);  $ password = "verysecret"; echo myhash ($ password, unique_salt ()); // résultat: $ 2a $ 10 $ dfda807d832b094184faeu1elwhtR2Xhtuvs3R9J1nfRGBCudCCzC

Le hachage résultant contient l'algorithme ($ 2a), le paramètre de coût (10 $) et le sel de 22 caractères utilisé. Le reste est le hachage calculé. Faisons un test:

// suppose que cela a été extrait de la base de données $ hash = '$ 2a $ 10 $ dfda807d832b094184faeu1elwhtR2Xhtuvs3R9J1nfRGBCudCCzC'; // supposons qu'il s'agit du mot de passe que l'utilisateur a saisi pour se reconnecter dans $ password = "verysecret"; if (check_password ($ hash, $ password)) echo "Accès autorisé!";  else echo "Accès refusé!";  function check_password ($ hash, $ password) // les 29 premiers caractères incluent algorithme, coût et sel // appelons-le $ full_salt $ full_salt = substr ($ hash, 0, 29); // lance la fonction de hachage sur $ password $ new_hash = crypt ($ password, $ full_salt); // renvoie true ou false return ($ hash == $ new_hash); 

Lorsque nous exécutons ceci, nous voyons "Accès accordé!"


8. Assembler

Avec tout ce qui précède à l’esprit, écrivons une classe d’utilitaires basée sur ce que nous avons appris jusqu’à présent:

class PassHash // blowfish private statique $ algo = '$ 2a'; // paramètre de coût privé statique $ cost = '$ 10'; // principalement à usage interne public static function unique_salt () return substr (sha1 (mt_rand ()), 0,22);  // ceci sera utilisé pour générer une fonction statique publique hash hash ($ password) return crypt ($ password, self :: $ algo. self :: $ cost. '$'. self :: unique_salt ());  // ceci sera utilisé pour comparer un mot de passe à une fonction de hachage publique statique check_password ($ hash, $ password) $ full_salt = substr ($ hash, 0, 29); $ new_hash = crypt ($ password, $ full_salt); return ($ hash == $ new_hash); 

Voici l'utilisation lors de l'enregistrement de l'utilisateur:

// inclure la classe require ("PassHash.php"); // lit toutes les entrées de formulaire à partir de $ _POST //… // réalisez vos opérations habituelles de validation de formulaire //… // hash le mot de passe $ pass_hash = PassHash :: hash ($ _ POST ['password']); // stocke toutes les informations utilisateur dans la base de données, à l'exception de $ _POST ['password'] // store $ pass_hash à la place //… 

Et voici l'utilisation lors du processus de connexion d'un utilisateur:

// inclure la classe require ("PassHash.php"); // lit toutes les entrées de formulaire à partir de $ _POST //… // récupère l'enregistrement utilisateur en fonction de $ _POST ['nom d'utilisateur'] ou similaire //… // vérifie le mot de passe avec lequel l'utilisateur a essayé de se connecter avec if (PassHash :: check_password ( $ user ['pass_hash'], $ _POST ['password']) // accorder l'accès //… sinon // refuser l'accès //…

9. Remarque sur la disponibilité de Blowfish

L'algorithme Blowfish n'est peut-être pas implémenté dans tous les systèmes, même s'il est maintenant très populaire. Vous pouvez vérifier votre système avec ce code:

if (CRYPT_BLOWFISH == 1) echo "Oui";  else echo "Non"; 

Cependant, depuis PHP 5.3, vous n'avez pas à vous inquiéter; PHP est livré avec cette implémentation intégrée.


Conclusion

Cette méthode de hachage des mots de passe doit être suffisamment solide pour la plupart des applications Web. Cela dit, n'oubliez pas: vous pouvez également demander à vos membres d'utiliser des mots de passe plus forts, en imposant des longueurs minimales, des caractères mélangés, des chiffres et des caractères spéciaux..

Une question pour vous, lecteur: comment hachez-vous vos mots de passe? Pouvez-vous recommander des améliorations par rapport à cette implémentation??