Calendrier de l'Avent - Comment stocker des mots de passe ?
Mise en contexte
Tout au long de cet article, nous allons jouer à un petit jeu. Vous êtes un serveur de confiance, qui souhaite offrir à ses clients une expérience sécurisée et authentifiée : chez vous, chacun a son propre compte, sécurisé par un mot de passe. La connexion à un espace client est validée lorsque le mot de passe rentré est le bon.
Malheureusement, vous n’y connaissez rien en cryptographie, et notamment, votre serveur n’est pas très sécurisé, ayant tendance à faire fuiter quelques informations stockées. En revanche, votre liaison internet est solide, et vos échanges avec vos clients resteront bien secrets et imperméables. Pour ma part, je jouerai le rôle du pirate éthique, qui cherchera à casser votre schéma de sécurité, mais vous expliquera aussi gentiment ses problèmes et comment l’améliorer. Voyons comment vérifier les mots de passe de nos clients sans trop exposer leur compte.
Rassurez-vous, aucune base de cryptographie ou de sécurité informatique n’est nécessaire ici : mon objectif sera de vous initier progressivement aux concepts d’aujourd’hui.
La méthode (vraiment) trop naïve
Il y a une solution extrêmement simple à ce problème : on pourrait demander littéralement à l’utilisateur son mot de passe, vu que la liaison est sûre, et juste vérifier si ce que l’on a stocké sur le serveur est le même mot de passe.
Bon, si ça n’était pas assez évident, il s’agit bien sûr d’une très mauvaise idée : on a dit que le serveur était peu fiable, si les mots de passe fuitent, ils se retrouvent “en clair” à la vue de tous, c’est-à-dire que la liste des mots de passe est disponible telle que ces mots de passe devront être tapés par l’utilisateur. En d’autres termes, c’est nul. Même en sécurisant le serveur, une personne mal intentionnée de votre équipe technique pourrait aller lire le fichier et récupérer tous les mots de passe de vos clients. Mauvaise idée… De manière générale, personne ne devrait avoir un avantage sur la récupération d’informations sécurisées, à part le détenteur du mot de passe : ici, on viole ce principe car un accès au fichier donne des privilèges immenses. C’est aussi pour ça que les entreprises préfèrent vous donner des liens de réinitialisation de mot de passe plutôt que de vous le communiquer : à part dans le cas d’un mot de passe temporaire, si quelqu’un peut vous le fournir, c’est une arnaque ! Soit vous êtes victime d’un piratage, soit l’entreprise a une politique de stockage vraiment très bancale, donc, à fuir de toute urgence !
Une première solution de masquage : le hachage
Mais alors, comment permettre une vérification du mot de passe sans stocker le mot de passe lui-même ? La solution est une idée mathématique très ingénieuse : stockons un résultat calculatoire sur le mot de passe, suffisamment puissant pour discriminer le bon mot de passe d’un mauvais. On appelle l’opération qui produit ce résultat une fonction de hachage. Evidemment, on s’attend à ce que cette fonction ait quelques propriétés pratiques. D’abord, elle doit être déterministe : en effet, si je compte vérifier le mot de passe proposé en calculant son hash et en le comparant à mon hash stocké, j’ai intérêt à toujours suivre le même comportement lors du calcul.
Ensuite, elle doit être difficile à inverser : cela veut dire que sachant le résultat, il est compliqué de produire une entrée qui renvoie vers ce même résultat. Par exemple, la fonction h
qui à tout mot de passe renvoie celui-ci concaténé devant “abc” n’est pas la meilleure des candidates. Exercice, h(mon_super_mot_de_passe) = coucouabc
, trouvez mon_super_mot_de_passe
.
Une autre propriété souhaitable est la difficulté de produire une collision : comme nous ne stockons que le résultat du calcul h(mot_de_passe)
, nous souhaitons que ce calcul soit suffisamment varié en résultats possibles pour que sa vérification soit suffisante. Ainsi, pour la fonction h
qui envoie blabla
pour tout mot de passe de départ, il est compliqué de retrouver le mot de passe initial choisi par l’utilisateur : mais ce n’est pas grave, car notre serveur acceptera tout mot de passe comme valide. Le rêve de tout pirate ! Cela veut aussi dire qu’une bonne fonction de hachage a des valeurs drastiquement différentes pour abc
, bac
, abcd
, ab
, etc.
Concrètement, les fonctions utilisées aujourd’hui (la famille de fonctions SHA-2, ou MD5, par exemple) sont des fonctions de “compression”, qui compactent l’information de manière à ce que chaque changement mineur soit répercuté sur toute la longueur du hash, le résultat du calcul. Par exemple :
sha256('abc') = ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
sha256('bac') = 268a6cb0fda1c6f872af9fada6e289f31abeb8a9c2e18104ef29a65f0898448d
sha256('abcd') = 88d4266fd4e6338d13b845fcf289579d209c897823b9217da3e161936f031589
sha256('ab') = fb8e20fc2e4c3f248c60c39bd652f3c1347298bb977b8b4d5903b85055620603
mais, les collisions existent toujours vu la taille fixe (même si très grande) de la sortie :
MD5(d131dd02c5e6eec4693d9a0698aff95c2fcab58712467eab4004583eb8fb7f8955ad340609f4b30283e488832571415a085125e8f7cdc99fd91dbdf280373c5b
d8823e3156348f5bae6dacd436c919c6dd53e2b487da03fd02396306d248cda0
e99f33420f577ee8ce54b67080a80d1ec69821bcb6a8839396f9652b6ff72a70)
=MD5(d131dd02c5e6eec4693d9a0698aff95c2fcab50712467eab4004583eb8fb7f89
55ad340609f4b30283e4888325f1415a085125e8f7cdc99fd91dbd7280373c5b
d8823e3156348f5bae6dacd436c919c6dd53e23487da03fd02396306d248cda0
e99f33420f577ee8ce54b67080280d1ec69821bcb6a8839396f965ab6ff72a70)
=79054025255fb1a26e4bc422aef54eb4
(si, si, regardez bien, il y a quelques petites différences dans les entrées)
Le déplorable “faible” nombre de mots de passe
A ce moment-là, ayant entre mes mains une nouvelle fuite de mots de passe de vos clients, je remarque quelque chose d’assez frappant. Rappelons qu’un mot de passe, ça peut être long. C’est une chaîne de caractères, disons alphanumériques pour simplifier, qui va faire entre, disons, 6 et 12 caractères pour votre site. Un caractère alphanumérique, c’est 26+26+10 = 62
caractères possibles, donc pour un mot de passe de 6 à 12 caractères, ça fait 62^6 + 62^7 + ... + 62^12 = 3 279 156 381 452 671 945 408
possibles très exactement. C’est… beaucoup. Alors pourquoi, pourquoi, sur des milliers de clients, est-ce que je retrouve toujours plus ou moins les mêmes hachés (et donc, si vous avez suivi, les mêmes mots de passe) ???
Les humains sont prévisibles, et n’aiment pas l’aléatoire complet. Les mots de passe, on en a partout, il faut essayer d’en utiliser des différents, des assez sûrs, des qui satisferont les diverses conditions des sites. Bref, il est totalement naturel d’utiliser des leitmotives pour aider à s’en rappeler. Le problème, c’est que la sécurité apparente du mot de passe s’en retrouve fortement affectée. Ainsi, même en considérant des mots de passe “longs” qui sembleront sûrs et à forte entropie (pensez l’entropie comme un “nombre de possibles”, une mesure fiable de la sécurité d’un mot de passe), l’espace dans lequel il est raisonnable de chercher un mot de passe inconnu pour un pirate est beaucoup, beaucoup plus restreint que l’espace tout entier. Pour votre culture personnelle, la liste rockyou
répertorie les mots de passe les plus fréquents utilisés aux US, et malgré son âge de plusieurs décennies, elle continue d’être très pertinente pour des attaques exhaustives sur des mots de passe inconnus. On peut en tirer plusieurs leçons : d’abord, la cybersécurité a encore de beaux jours devant elle et reste un domaine peu ancré chez le citoyen lambda ; ensuite, les personnes n’apprennent jamais de leurs erreurs, même après un piratage de compte majeur ; enfin, pour nous deux, duo de défenseur débutant et d’attaquant pédagogue, il va être urgent de réhausser le niveau de protection.
Car la fonction de hachage n’est qu’un masquage, comme on l’a déjà dit, d’un mot de passe déjà existant, certes non-inversible, mais tout de même déterministe : et ainsi, si je hache avec la même fonction de hachage (standardisée, donc publique et universelle) plusieurs mots de passe très communs, il est fort probable que je retrouve quelques valeurs de notre liste fuitée, passant alors outre notre première couche de sécurité… Ainsi, je récupère quelques mots de passe pour mon usage personnel. Avec un peu d’ingéniosité, je peux même précalculer des hashs de mots de passe communs, et chercher ces motifs dans les tables ayant fuité publiquement. De tels dictionnaires liant hashs à mots de passe sont appelés rainbow tables, assez utiles et populaires il y a quelques décennies mais tombés en désuétude depuis (la section suivante expliquera pourquoi). Le plus terrible dans tout ça est qu’une fois un mot de passe récupéré depuis un serveur vulnérable, il peut être testé sur des serveurs beaucoup plus sûrs : ainsi, il est impératif de ne pas réutiliser le même mot de passe (ou même un similaire, car créer des variations est très facile) sur différents sites, et de changer ses mots de passe suite à un piratage avéré ! Mais ça, vous le saviez (et le faisiez) déjà, n’est-ce pas ?
Le sel à la rescousse de l’entropie
Le problème, comme souvent, relève de l’interface chaise-clavier (pour les L1 qui ne l’auraient pas, je parle de l’utilisateur). On l’a vu, la sécurité d’un mot de passe repose sur la forte entropie qu’on lui impose, qui rend la recherche exhaustive (tester des mots de passe possibles à la main, successivement) beaucoup trop fastidieuse et irréalisable. Malheureusement, le hachage n’est qu’un masquage de l’information, une transformation déterministe qui envoie donc un ensemble de taille t
sur.. oui, sur un autre ensemble de taille t
, oui (modulo quelques collisions, dont on a espéré qu’elles étaient très rares). On semble donc être dans une impasse : l’humain n’est déjà pas doué pour ajouter de l’incertitude, de l’imprévisibilité dans sa vie, alors dans ses mots de passe… Si seulement il existait un moyen d’introduire une dimension d’aléatoire dans le hachage, de manière à artificellement agrandir l’espace des hachés…
Eh bien, quand l’humain n’est pas efficace pour introduire de l’aléatoire, on lui impose. Arrive alors le salage.
Un sel, c’est un vecteur complètement aléatoire, généré par le serveur ou la machine de l’utilisateur, mais qui n’est par définition pas choisi. Ainsi, le sel a une entropie maximale, et comme la fonction de hachage a cette puissante propriété de propagation de l’entropie et des changements tout au long de son résultat, c’est un outil très efficace pour anéantir tout l’intérêt d’une rainbow table lorsque le sel est différent pour chaque utilisateur. Le sel peut fuiter, ou même être publiquement révélé, ce n’est pas un problème, car il s’agit juste d’un facteur d’aléatoire supplémentaire, dont le comportement est extrêmement flou même s’il est connu.
En effet, soit h
une fonction de hachage quelconque, si je stocke par avance h('abc')
dans le but d’identifier un mot de passe ‘abc’ haché dans une base ayant fuité, je n’aurai aucun avantage pour identifier le hash de ce mot de passe précédé d’un sel, par exemple h('18rY03iT4nADpabc')
: tout mon travail est à refaire, et en plus, les résultats ne sont même pas réutilisables, car le sel varie pour chaque utilisateur.
Parenthèse un peu plus technique, attention tout de même à un léger piège : les fonctions de hachage opèrent généralement en découpant leur entrée en blocs. Un pirate peut gagner du temps en précalculant les premiers blocs, et en reprenant le calcul à ce même point tout au long de son attaque.
Par exemple, si notre fonction h
travaille sur des blocs de 4 caractères de long, et maintient un état interne en mémoire, au lieu de calculer plein de h('18rY03iT4nADp???')
pour diverses valeurs, il est plus rentable de calculer h(p????)
à chaque fois, avec en état interne h(18rY03iT4nAD)
que l’on ne calcule qu’une seule fois. Voilà pourquoi un bon salage met toujours le sel en fin de mot de passe et non l’inverse (même si l’avantage est léger dans le cas d’une attaque de hachés, il existe d’autres problèmes où c’est une faille significative, comme la proof of knowledge de la part d’un serveur)
Performances concrètes et nécessité de l’itération
Maintenant qu’on a un modèle théorique “sûr”, utilisons des outils concrets pour tester sa sécurité, et estimer si nous pouvons résister à une recherche exhaustive sur un grand espace de mots de passe probables, disons quelques millions. Pour cela, je me suis emparé de la liste rockyou
déjà mentionnée, j’ai obtenu une liste de hachés salés (et des sels correspondants, publics), et j’ai récupéré l’outil de référence pour des attaques ultra-rapides de hachés, hashcat
. C’est un outil open-source disponible sur Github, je me dois donc de faire un rappel préventif : évidemment, récupérer des hashs, et/ou tenter de les briser en les attaquant, que ce soit à l’aide de hashcat
ou pas, c’est illégal, même avec l’accord du propriétaire des données car souvent cela implique d’attaquer le serveur qui détient les données. Mentionnons aussi que hashcat
cherche des égalités sur des hashs connus : il n’est donc d’aucune utilité pour attaquer des comptes en ligne, où le hash est gardé secret par le serveur. Vérifiez bien que vous êtes dans les cas particuliers où vous êtes embauché pour, ou dans un cadre privé, éducatif, avec vos propres données, et où vous ne causez aucun dégât à personne, avant de faire joujou avec. Et comme ça utilise vos cartes graphiques pour effectuer un maximum de calculs rapides en parallèle, évitez de faire tourner des attaques sur des PC peu performants ou des PC portables : le mauvais refroidissement pourrait faire fondre le plastique ou des composants internes.
Bref. On a mentionné la famille SHA-2 (SHA256, SHA512...)
ou MD5
précédemment. Quelles sont les performances raisonnables pour les attaquer avec, disons, un super-parc graphique optimisé pour l’attaque ? J’ai eu la chance d’en avoir un à disposition pendant une certaine période professionnelle de ma vie, et d’utiliser hashcat
dessus : essentiellement, sur un alignement de quatre RTX 4070 (des cartes très haut de gamme et très performantes), on peut espérer en attaquer plusieurs milliers de milliards par seconde.
Ah. Comment faire alors ? Faut-il utiliser des sels démesurément grands ? Toujours pas, car au final, l’espace qui varie est celui des mots de passe que l’utilisateur peut prendre, dont on a déjà dit qu’il était plutôt petit : la solution, c’est de construire de nouvelles fonctions de hachage arbitrairement lentes, pour contrecarrer les attaques exhaustives sur GPU (l’acronyme anglais derrière les unités de calcul des cartes graphiques). Concrètement, on va demander l’itération chaînée d’une fonction de hachage spécifique, toujours avec salage, des milliers voire millions de fois, afin de forcer artificiellement une lenteur de calcul et de freiner la progression de tout attaquant potentiel. Quelques millisecondes supplémentaires, pour un utilisateur légitime, c’est peu, et indétectable. Mais pour un attaquant, payer ce coût temporel à chaque mot de passe testé, c’est extrêmement dissuasif. Sans rentrer dans les détails techniques, c’est le fonctionnement des constructions PBKDF et PBKDF2, qui ont pour vocation de dériver tout mot de passe salé en une clé assez forte en termes d’entropie pour être utilisée dans des applications cryptographiques (par exemple, comme clé de chiffrement AES…). PBKDF signifie d’ailleurs PuBlic Key Derivation Function.
Ceci conclut cette introduction aux fonctions de hachage et à leur usage pour stocker de manière fiable des mots de passe,
et joyeux Noël à vous.