Stocker les mots de passes de vos utilisateurs

tbowan

23 Décembre 2019

Dès qu’on a besoin de contrôler les accès des utilisateurs, il faut pouvoir les authentifier. Même si des alternatives existent, le couple identifiant/mot de passe reste une méthode simple et relativement efficace. Aujourd’hui, nous allons voir pourquoi et comment sécuriser le stockage de ce sésame pour que les données de vos utilisateurs soient protégées.

Aujourd’hui, vous avez décidé de développer votre propre application et avez besoin de vérifier l’identité de vos utilisateurs… On est tous passé par là. Et parmi les solutions possibles, vous avez opté pour l’authentification par un mot de passe (personne ne vous juge).

Pour éviter qu’un espion ne lise les mots de passes, vous avez donc créé des certificats et mis en place une connexion sécurisée via HTTPS. Pour rendre les mots de passes difficile à deviner, vous avez même mis en place une politique de mots de passes (taille, caractères nécessaires, …) et des protections anti brute force.

Et maintenant, vous vous demandez quoi faire de plus… Vous êtes au bon endroit 😉.

Divulgâchage : pour protéger en profondeur, stockez les mots de passes dans une version hachée et salée. Pour ça, ne réinventez pas la roue, elle a toute les chances de casser, préférez-donc les fonctions fait pour comme password_hash() en PHP.

Pourquoi faire plus ?

Effectivement, votre périmètre défensif est protégé. Un attaquant à l’extérieur ne peut ni intercepter ni deviner les mots de passes.

Citadelle de Lille Wikipedia

Le problème, de manière générale, c’est que vous ne pouvez pas être sûr à 100% qu’aucune vulnérabilité n’existe ailleurs dans votre système. Les failles potentielles sont nombreuses et même si vous y mettez toute votre énergie, il y a toujours une mince probabilité qu’il reste quelque chose. Ne serait-ce qu’un insider, un salarié ayant un accès légitime qui va faire fuiter les données.

L’actualité ne manque pas d’exemples de ces fuites de données dans les entreprises de toutes tailles. Qu’importe les moyens mis en œuvres, une (petite) porte a été trouvée et a permis d’exporter la base. Si vos mots de passes sont en clair, vous exposez vos utilisateurs à des usurpations de leurs identités. Je ne suis pas sûr que la CNIL appréciera.

Vous devez donc partir du principe qu’un attaquant peut obtenir un accès à l’intérieur du périmètre et donc, in fine, lire le contenu de votre base de donnée, et donc, les mots de passes.

Pour s’en prémunir, on doit donc faire de la défense en profondeur. C’est à dire ajouter une deuxième ligne de défense à l’intérieur du périmètre. Ne pas faire entièrement confiance à l’environnement et aux autres lignes de défenses pour pouvoir se défendre soi-même en cas de besoin.

Comment faire plus

L’idée est donc de rendre les mots de passes illisibles et si vous pensez à la cryptographie, vous êtes sur la bonne voie.

Contre-intuitivement, ce n’est pas le chiffrement qui va nous aider ici. Une fois chiffrés, les mots de passes seront bien sûr illisibles, mais qui dit chiffrement dit clé de déchiffrement qu’il faut donc stocker quelque part. Ce qui ajoute une nouvelle donnée sensible qu’il faut aussi protéger. Si vous la chiffrez, il vous faut alors une deuxième clé, qu’il faudra aussi protéger… Le système est de plus en plus complexe et donc fragile.

On retrouve ce genre de construction dans des applications qui ont besoin de lire les données après les avoir stocké, dont certaines applications de paiement par carte bancaire (cf. conditions 3.4 à 3.6 de la norme PCI-DSS). Dans ce cas, on parle alors de DEK et de KEK :

  • DEK pour Data Encryption Key, une clé pour chiffrer les données,
  • KEK pour Key Encryption Key, une clé pour chiffrer les clés.

Et si vous partez sur ce genre de système, il va vous falloir protéger la KEK, soit avec une autre KEK (et retour à la case départ), soit sur d’autres supports (i.e. des HSM). C’est de plus en plus complexe et vous en viendrez à célébrer des Cérémonie de Clés, une partie du budget devant alors être consacrée aux bougies, encens et autres chapeaux pointus.

Heureusement, comme nous n’avons pas besoin de relire les mots de passes, une autre méthode cryptographique existe, les fonction de hachage. Mais comme on va le voir, il faut bien les choisir et ajouter un sel.

Comment hacher ?

Le premier écueil est dans le choix de la fonction de hachage. Celle-ci doit être robuste à une attaque de pré-image, autrement dit, un attaquant disposant de l’empreinte du mot de passe ne devrait pas pouvoir le retrouver plus facilement qu’en testant exhaustivement tous les mots de passes (on dit aussi par brute force).

Oubliez définitivement md5 et sha1 ! Ces fonctions sont cassées, et depuis bien trop longtemps.

Illustration de stevepb

Lors de l’enregistrement de vos utilisateurs (ou lorsqu’ils changent leur mot de passe), vous allez calculer une empreinte cryptographique et c’est cette empreinte qui sera stockée.

function store($password) {
    return hash("sha512", $password) ;
}

Lors de l’authentification de vos utilisateurs, lorsqu’ils vous fournissent leur mot de passe, vous allez en calculer l’empreinte et la comparer avec celle dans la base de donnée.

Deuxième écueil ici lors de la comparaison. D’habitude, lorsqu’on compare deux chaines, on retourne faux dès qu’on voit qu’elles diffèrent et on évite des comparaisons inutiles pour gagner du temps. Dans notre cas, c’est problématique car en mesurant le temps mis pour répondre à sa requête, un attaquant aura une information sur la longueur du préfixe commun entre l’empreinte stockée en base, et celle du mot de passe qu’il soumet. Par essais et erreurs, il pourrait alors découvrir l’empreinte stockée en base (sans avoir accès à la base et c’est ça le problème).

Pour faire les choses bien, on va donc utiliser la fonction hash_equals pour comparer les empreintes car elle est conçue spécifiquement pour prendre le même temps, qu’importe quand les chaînes diffèrent.

function verify($password, $stored) {
    return hash_equals(store($password), $stored) ;
}

Pourquoi saler ?

Si on en reste là, en utilisant une fonction de hachage seule, vos mots de passes seront tous transformés sous la même forme. Deux mots de passes identiques mais d’utilisateurs différents auront la même empreinte. Et ça va poser problème.

« Parce que c’est plein d’électrolytes » Idiocracy

Plutôt que de casser les mots de passes en tentant exhaustivement toutes les possibilités (ça peut prendre un temps fou), on pourrait utiliser un dictionnaire. En stockant les mots de passes possibles avec leur empreinte, il vous suffira de trouver l’empreinte dans le dictionnaire et ensuite lire le mot de passe correspondant.

Par exemple. Si on construit toutes les empreintes SHA256 des mots de passes de 6 caractères alphanumériques, notre dictionnaire nécessitera 62662^6 (nombre de mots de passes possibles) fois 32 octets (taille d’une empreinte), soit 1,8 To ce qui est largement acceptable.

Si la recherche exhaustive de tous les mots de passes a une complexité en O(n)O(n), la recherche dans un dictionnaire bien rangé tombe à O(log(n))O(log(n)).

Si vous n’avez qu’une seule empreinte à cracker, ça ne sert bien évidement à rien puisque le temps de création du dictionnaire sera aussi long que pour tenter tous les essais. En fait, il sera même un peu plus long puisqu’il faudra stocker tout ça sur disque pour plus tard.

Par contre, dès que vous avez une deuxième empreinte, le dictionnaire étant déjà créé, vous n’avez plus qu’à faire une recherche, ce qui rembourse largement l’investissement. Plus vous en avez, plus c’est rentable.

Créer un dictionnaire contenant tous les mots de passe, ça peut prendre trop de place. Plus les mots de passes possibles sont longs et complexes, plus les dictionnaires sont grands. Au point qu’à un moment, ils ne sont plus stockable… Mais là encore, il y a des solutions.

Mots de passes fréquents. La plupart des utilisateurs manquent d’originalité et finissent par utiliser, encore et encore, les mêmes mots de passes. Vous pourriez alors créer un dictionnaire avec ces mots de passes et gagner en espace disque. Bien sûr, vous ne casserez pas les trucs vraiment compliqué mais vous devriez avoir un bon taux de réussite sur une base complète.

D’ailleurs, vous n’avez parfois même pas à créer le dictionnaire ni même à faire la recherche vous même si les dictionnaires sont déjà créés et publiquement indexés (i.e. merci google).

Les tables arc-en-ciel. Sans rentrer dans les détails, il s’agit d’un truc pour compresser un dictionnaire. Plutôt que de stocker les empreintes de tous les mots de passes, vous pouvez en fait en éliminez une bonne proportion que vous pouvez en fait recalculer.

C’est un exemple de compromis temps/mémoire. Par rapport aux dictionnaires, les tables prennent moins de place mais nécessite plus de temps pour trouver un mot de passe. Par rapport à une attaque exhaustive, les tables prennent plus de mémoire mais restent plus rapides.

Comment saler ?

Pour contrer ces attaques par dictionnaires, on va augmenter artificiellement la taille des mots de passes en leur ajoutant un sel ; des caractères aléatoires. Un dictionnaire construit pour casser ces mots de passes devrait alors traiter également les sels possibles, explosant la taille mémoire nécessaire et rendant donc l’attaque matériellement impossible.

Illustration de Daria-Yakovleva

Comme on part du principe qu’il faut protéger une base de donnée entière, contenant beaucoup de mots de passes, il vaut mieux utiliser un sel aléatoire pour chaque mot de passe. Si le sel était commun à toute la base, ou s’il était dérivé à partir du mot de passe, un attaquant pourrait créer un dictionnaire spécifique, rendant le sel inutile.

Techniquement, vous pourriez utiliser des données unique par utilisateur (e.g. son login ou son adresse mail). Mais d’expérience, lorsqu’on s’embarque dans ce genre de « techniques maison », ça marche peut être au début, mais on finit toujours par faire une erreur à un moment et l’édifice s’écroule.

Lors de l’enregistrement de vos utilisateurs (ou lorsqu’ils changent leur mot de passe), vous allez donc générer une chaîne aléatoire (le sel) et l’ajouter au mot de passe puis calculer une empreinte cryptographique de l’ensemble. C’est le sel et l’empreinte qui seront stockés.

function store($password) {
    $salt = bin2hex(random_bytes(16)) ; // 128 bits d'aléa
    $hash = hash("sha512", $password . $salt) ;
    return "$salt.$hash" ;
}

Lors de l’authentification de vos utilisateurs, lorsqu’ils vous fournissent leur mot de passe, vous allez extraire le sel, le joindre au mot de passe et hacher l’ensemble pour comparer leur empreinte à celle stockée.

function verify($password, $stored) {
    list($salt, $hash) = explode(".", $stored) ;
    return hash_equals(hash("sha512", $password . $salt), $hash) ;
}

Comment ralentir ?

Jusqu’ici, nous avons garanti qu’un attaquant ne puisse que tenter une attaque par tentatives exhaustives. C’est très bien mais un dernier problème se pose : la vitesse de calcul d’une empreinte.

Illustration de andrewhatton123

Dans notre cas, cette rapidité se retourne contre nous puisque si nous pouvons allons vite pour calculer une empreinte (via un processeur), un attaquant ira encore plus vite (via ses cartes graphiques)…

Pour donner un ordre d’idée, sur notre carte graphique GTX 1050 TI sortie en octobre 2016, hashcat calcule 130 millions de sha512 par seconde. Pour un mot de passe de 6 caractères alphanumériques, il ne lui faudra que 7 minutes pour les tester tous…

En fait, après avoir « protégé » le mot de passe aze123 avec la fonction store() précédente, il n’a fallu que 2 secondes pour que hashcat ne casse l’empreinte1.

Après 2 secondes, Hashcat a cassé le mot de passe

Il faut donc ralentir ces attaques en utilisant des algorithmes de hachage plus lents. Si c’est plus lent pour nous, ça n’est pas très grave puisque ça n’est pas une opération que nous faisons souvent. Par contre, ça pénalisera l’attaquant puisque lui, ne fait que des hachages.

Plutôt que SHA512, il est alors plus pertinent d’utiliser d’autres fonctions comme bcrypt, scrypt, argon2 ou encore PBKDF2 qui sont conçues pour prendre leur temps et fournissent un paramètre pour configurer le coût du calcul. Notez que, tout comme le sel, le coût doit aussi être inscrit à côté de l’empreinte pour qu’on puisse la recalculer ensuite.

En PHP, on pourrait vouloir définir une fonction de hachage similaire à bcrypt en utilisant la fonction crypt comme suit :

function myHash($password, $salt, $cost) {
    $options = sprintf('$2a$%\'.02d$%\'.22s$', $cost, $salt) ;
    return crypt($password, $options) ;
}

Lors de l’enregistrement la méthode ne change pas, on calcul un sel puis une empreinte.

function store($password) {
    $salt = bin2hex(random_bytes(16))    ;
    $hash = myHash($password, $salt, 10) ;
    return $hash ;
}

Lors de l’authentification de vos utilisateurs, vous pouvez utiliser la version stockée comme option pour que crypt() recalcule l’empreinte.

function verify($password, $stored) {
    $hash = crypt($password, $stored) ;
    return hash_equals($stored, $hash) ;
}

Et en vrai ?

Si vous développez une application, franchement, je vous déconseille de réinventer la roue comme je l’ai fait dans cet article. C’était juste pour vous montrer et en vrai, je ne m’embêterais pas avec toutes ces considérations manuellement.

Non seulement faire les choses soi-même, c’est un nid à erreurs potentielles lorsqu’on parle de cryptographie, mais aussi (surtout ?) parce que vous devriez aussi gérer la notion de mise à jours de l’algorithme (que feriez-vous si vous devez augmenter le coût du hachage ? la longueur du sel ?).

Parce que PHP est quand même un truc sérieux et pratique, on dispose des deux fonctions password_hash() et password_verify() qui font exactement de dont on a besoin :

  1. Elles vont hacher les mots de passes en utilisant les fonctions les plus adaptées du moment,
  2. Elles vont saler les empreintes, en utilisant une génération aléatoire sûre,
  3. Elles vont effectuer leurs opérations en temps constant pour éviter les attaques temporelles,
  4. Elles vont s’occuper des problèmes de formatage pour fonctionner sans accroc.

Je devrais en fait remplacer les deux fonctions store() et verify() par ces deux-ci :

function store($password) {
    return password_hash($password, PASSWORD_DEFAULT) ;
}

function verify($password, $stored) {
    return password_verify($password, $stored) ;
}

Pour les autres langages, vous n’aurez peut être pas, nativement, de fonctions similaires. Je vous dirais bien de passer à PHP mais je sais que ça n’est pas toujours possibles 😉. Alors voici quelques pistes…

Et après ?

Pour aller encore plus loin dans la sécurisation du mot de passe, vous pourriez remplacer votre hachage salé par un code d’authentification avec clé (HMAC en acronyme) mais à condition de déléguer cette tâche à un matériel spécifique (HSM en acronyme).

Si vous avez besoin de pouvoir lire les mots de passes en clair (keepass je te vois), il vous faudra chiffrer avec une clé, puis chiffrer la clé avec une autre clé. Et pour faire les choses bien, utiliser un HSM aussi.

En attendant, voici quelques articles pour rester dans l’ambiance du hachage et de l’authentification…

Fonctions de hachage : Les empreintes cryptographiques

28 Novembre 2019 Vérifier l’intégrité d’une donnée est une tâche complexe. Si, en plus, vous êtes en présence d’attaquants intelligents, elle devient carrément cryptographique. Dans cet article, on vous explique le pourquoi du comment.

Brute force de Nortel IP-8815 avec Hydra

9 Décembre 2019 Votre téléphone IP-8815 est bloqué par un mot de passe que vous ne connaissez pas… Il est peut être possible de le trouver en laissant cette tâche ingrate à un programme fidèle, Hydra. voici comment procéder.


  1. Pour les curieux, la ligne de commande à taper est un peu technique…

    hashcat64.exe -a 3 -w 3 -m 1710 -p . -1 ?l?u?d <hash>.<sel> ?1?1?1?1?1?1
    • -a 3 pour demander un brute force (et non une attaque par dictionnaire),
    • -w 3 pour lui demander d’aller plus vite (au détriment de la consommation électrique),
    • -m 1710 pour lui dire que l’empreinte se calcule en concaténant le mot de passe au sel (dans ce sens, si on les inverse, c’est 1720),
    • -p . pour lui dire que nous séparons le sel de l’empreinte par un . (notez que hashcat s’attend à voir l’empreinte puis le sel, j’ai donc du adapter la sortie de ma fonction store),
    • -1 ?l?u?d pour lui dire que je crée un ensemble spécifique contenant des minuscules (?l), des majuscules (?u) et des chiffres (?d)
    • <hash>.<sel> doit être remplacé par le haché et le sel,
    • ?1?1?1?1?1?1 pour lui dire que le mot de passe fait 6 alpha numériques.
    ↩︎