Explorons l’évolution des techniques pour sécuriser les mots de passe web, soulignant l’importance d’une protection solide pour les données utilisateurs. Découvrez comment rendre chaque mot de passe unique et résistant aux attaques.
En début d’année, Camille Fauchier publiait son guide d’implémentation d’authentification JWT avec Passport dans Nest.js. Et je remarquais l’utilisation des méthodes bcrypt.hash
et bcrypt.compare
pour respectivement créer un hash du mot de passe et comparer la saisie de l’utilisateur à ce hash lors d’une authentification.
Je voulais comprendre précisément comment fonctionne le hachage de mot de passe de bcrypt et sa gestion du salt. Cela aurait pu être l’unique sujet de cet article. Mais avant d’en arriver là, j’aimerais revenir sur les différentes méthodes de transformation des mots de passe dans un usage web au fil du temps.
Cet article n’est pas tout à fait tech, plutôt un aller vers le passé pour mieux revenir dans le présent.
Au sein d’un site web, quand on veut permettre à un utilisateur de se connecter à un service via un couple formé d’un identifiant et d’un mot de passe, on peut stocker ces informations dans une base de données.
Seulement voilà, si la table qui contient les identifiants et mots de passe se retrouve entre de mauvaises mains, alors celui qui les détient peut, sur cette application au moins, se connecter avec les comptes dont il connaît le couple identifiant/mot de passe.
Mais qui pourrait avoir accès à cette table ?
Aussi, la bonne pratique est devenue de transformer un mot de passe avec des méthodes de hachage (citons MD5
, SHA-1
désormais abandonnés mais aussi Eksblowfish
qui est utilisé par bcrypt
). Une méthode de hachage prend en entrée un mot de passe et fournit en retour une chaîne de caractères de manière à ce qu’on ne puisse pas “revenir en arrière”, c’est-à-dire qu’on ne puisse pas retrouver le mot de passe à partir de la chaîne de caractère engendrée.
Prenons en exemple MD5
pour comprendre comment fonctionne une fonction de hachage.
Le but d’une fonction de hachage est de fournir une empreinte de la valeur qu’elle représente. Cette empreinte est d’une longueur fixe et qui potentiellement peut être moins grande que la valeur initiale. Une fonction de hachage n’est donc pas injective. Il n’est ainsi pas possible de retrouver la valeur initiale à partir de la valeur du hash. Une fonction de hachage est à distinguer d’une méthode de chiffrage, laquelle ne perd aucune information et assure que son résultat est déchiffrable.
En appliquant une méthode de hachage simplement sur un mot de passe, on obtient toujours la même chaîne de caractères.
Cela a un impact immédiat : si l’on connaît la méthode de hachage (par exemple SHA-1
) et qu’on dispose d’une liste des mots de passe hachés d’une application, alors on peut - en utilisant un générateur aléatoire de mot de passe - reconstituer les mots de passe hachés auxquels correspondent ces entrées aléatoires. Pour cela on peut utiliser une rainbow table qui associe le mot de passe à une chaîne de caractères résultante.
Une rainbow table est construite à partir d’une liste de mots de passe potentiels auxquels est appliquée la méthode de hachage et dont le résultat subit une méthode de réduction, puis la méthode de hachage et une autre méthode de réduction, et éventuellement encore plusieurs fois le couple méthode de hachage et méthode de réduction. Seuls les mots de passe d’entrée et les réductions finales sont stockés dans la rainbow table, ce qui permet de limiter la mémoire nécessaire.
Voici une ligne d’une rainbow table exemple pour le mot de passe initial mypassword
, la méthode de hachage MD5
. Les méthodes de réduction conservent la longueur du mot de passe. Par souci de lisibilité, on applique uniquement 3 fois la méthode de hachage suivie à chaque fois par une méthode de réduction pour réduire le hash à une chaîne de caractère de même nature que le mot de passe (ici une chaîne de caractères composée uniquement de lettres minuscules). Pour cette ligne, seuls sont stockés mypassword
en entrée et jmtexivpnb
en sortie.
A partir d’un hash, on applique la dernière réduction et on cherche si le résultat existe dans la rainbow table, si c’est le cas alors c’est reduction2 qui devrait être le mot de passe initial :
jmtexivpnb
, alors on part du mot de passe initial mypassword
, on calcule le 1er hash, la 1ère réduction, le 2ème hash et enfin la 2ème réduction et alors on sait que le mot de passe initial est possiblement lywzdjkaqn
Si cette réduction n’est pas dans la rainbow table, alors on applique au hash l’avant dernière réduction, la méthode de hachage et enfin la dernière réduction. On cherche alors le résultat dans la rainbow table et si elle est présente alors le mot de passe peut être la reduction1 :
jmtexivpnb
, alors on applique au mot de passe initial le 1er hash et la 1ère réduction et tfsnyrwexo
est possiblement le mot de passeEnfin si cette réduction n’était pas non plus présente, on applique toute la chaîne et si la réduction est présente dans la rainbow table, le mot de passe est potentiellement le mot de passe initial :
jmtexivpnb
alors le mot de passe candidat est mypassword
Les réductions intermédiaires, qui ont le même nombre de caractères que le mot de passe testé et sont formées à partir du même ensemble de caractères, assurent que pour un même mot de passe on en teste finalement plusieurs. C’est pourquoi plus il y a d’étapes hachage-réduction, moins il est nécessaire d’avoir d’entrées dans la rainbow table mais plus il y aura d’étapes de calcul pour retrouver le mot de passe correspondant à un hash.
On peut alors retrouver les mots de passe originaux en reconnaissant les mots de passe hachés. Ce n’est donc pas suffisant en cas de fuite de données : les mots de passe restent retrouvables.
Alors on a eu recours à du “salt”. Si on veut utiliser la méthode SHA-1
(uniquement pour l’exemple, SHA-1 n’étant plus recommandé), on peut modifier le mot de passe en le concaténant avec du sel (”salt” en anglais), avant le mot de passe et après le mot de passe. Par exemple on peut toujours ajouter “saltBefore-” avant le mot de passe et “-saltAfter” après. La chaîne de caractère stockée en base de donnée sera alors le résultat de sha1("saltBefore-" + password + "-saltAfter")
Si les chaînes de caractères “saltBefore-” et “-saltAfter” sont dans le code, alors pour construire une rainbow table il faut connaître à la fois :
L’attaque pas rainbow table devient plus compliquée parce que la rainbow table doit être construite spécifiquement pour cette méthode de hachage (et donc le salt utilisé).
Cependant, deux utilisateurs qui ont le même mot de passe ont la même chaîne de caractères stockée en base de données. Si l’un est connu, l’autre aussi.
Pour éviter que deux mots de passe identiques soient représentés par la même chaîne de caractères une fois hachés, il suffit de changer de salt pour chaque mot de passe. De la sorte, si deux utilisateurs ont le même mot de passe, alors le hash stocké sera différent.
C’est ce que propose la méthode bcrypt.hash
qui permet par défaut d’ajouter un salt aléatoire lors de chaque génération de mot de passe. Ensuite, pour comparer le mot de passe saisi par l’utilisateur avec le hash sauvegardé en base de données la méthode bcrypt.compare
applique le hash à la saisie utilisateur et compare le résultat à la sauvegarde.
Mais comment la méthode bcrypt.compare
connaît-elle le salt utilisé lors du hash initial ? Pour cela voyons comment fonctionne bcrypt.hash
La méthode bcrypt.hash
prend deux paramètres d’entrée :
bcrypt.genSalt
dont l’unique paramètre est un nombre entier représentant le coût numérique)Dans le premier cas le salt est engendré automatiquement, dans le second il est fourni à la méthode.
Utilisons la 2ème façon de faire. Et créons un hash à partir d’un salt engendré explicitement.
La méthode bcrypt.hash
fournit donc en sortie une chaîne de caractères qui contient en préfixe le salt utilisé. Cette chaîne de caractère appelée hash est en fait la concaténation du salt et du hash généré.
Ainsi lorsque, pour vérifier si la saisie utilisateur correspond bien au hash stocké, on fournit à la méthode bcrypt.compare
le mot de passe en clair saisi par l’utilisateur ainsi que le hash avec lequel le comparer, le salt utilisé est fourni directement dans le hash.
Puisqu’aucun salt n’est identique, aucune génération de rainbow table unique n’est possible. L’utilisation de rainbow table impliquerait de créer une rainbow table par hash/salt.
Cependant créer et utiliser une rainbow table serait très lent puisqu’on l’a vu, cela consiste à appliquer de nombreuses fois la méthode de hachage, ce qui pour bcrypt
est très coûteux en temps et même réglable via le paramètre entier représentant le coût numérique.
En revanche, en connaissant la chaîne de caractère résultante de la méthode bcrypt.hash
, on connaît le code associé à l’algorithme de hash utilisé (dans l’exemple ci-dessus $2b$
) et le salt utilisé. Plutôt que de construire pour ce hash une rainbow table très coûteuse en temps à la création comme à l’utilisation, c’est chaque mot de passe candidat qui pourrait être haché pour vérifier une concordance.
La bonne pratique actuelle est donc d’utiliser une méthode de hachage avec salt aléatoire pour hacher chaque mot de passe, par exemple en utilisant bcrypt
.
Le salt aléatoire différent pour chaque mot de passe est stocké dans le hash. Le temps nécessaire pour hacher un mot de passe avec bcrypt
protège de la création et utilisation d’une rainbow table spécifique à un salt donné.
Pour annihiler le risque d’une recherche d’un mot de passe associé à un hash, il est possible de séparer le stockage du salt et du hash de chaque mot de passe dans des bases de données différentes afin de rendre plus difficile la connaissance de la méthode de hachage utilisée.
Les Echos (2018) - mot de passe en clair