Stocker des mots de passe utilisateur de manière sécurisée

Cet article vous explique comment stocker des mots de passe utilisateur en base de données de manière sécurisée dans le cadre d'une application web. Sécuriser ces données est crucial, car une base de données fait partie de ces choses qui peuvent être compromises notamment s'il y a une faille de sécurité quelque part sur votre site.

L'article reprend principalement la structure et les idées décrites sur cette page. Il commence par expliquer ce qu'il ne faut pas faire et pourquoi, pour finir sur la bonne manière de stocker des mots de passe. Quand c'est utile, vous trouverez un petit bout de code en Python expliquant comment un hacker devrait procéder pour retrouver un ou des mots de passe une fois qu'il est en possession de votre base de données (Python a été choisi pour sa simplicité et sa clarté quasi-immédiate).

Table des matières

  1. La pire solution : stocker les mots de passe en clair
  2. Un peu mieux, mais toujours mauvais : MD5/SHA1
  3. Encore un peu mieux, mais toujours mauvais : SHA1, avec un sel fixe
  4. Déjà mieux, mais perfectible : SHA1, avec un sel par utilisateur
  5. Une bonne solution : Bcrypt
  6. Exemple concret en PHP
  7. L'importance d'un bon mot de passe

La pire solution : stocker les mots de passe en clair

Les mots de passe font partie de ces choses qu'il ne faut jamais stocker en clair. En effet, dans l'éventualité où vous vous feriez pirater votre base de données, le hacker aura accès à tous les identifiants et mots de passe de vos utilisateurs.

Utilisateur Mot de passe
[email protected] motdepasse
[email protected] motdepasse123

Cela compromet non seulement la sécurité des comptes utilisateur de votre site, mais probablement aussi les comptes que vos utilisateurs ont sur d'autres sites s'ils partagent les mêmes identifiants.

Le fait de stocker des mots de passe en clair compromet la sécurité des comptes que vos utilisateurs ont sur d'autres sites s'ils ont utilisé les mêmes identifiants.

Bref, c'est une très très mauvaise idée. Pourtant, certains le font toujours et l'ont fait, comme Sony en 2011 (magnifique, non ?).

Un peu mieux, mais toujours mauvais : MD5/SHA1

Une meilleure solution consiste à hacher le mot de passe à l'aide d'une fonction de hachage à sens unique, telle que md5() ou sha1(). Même si le serveur ne stocke pas le mot de passe brut, il sait encore authentifier l'utilisateur : il suffit en effet de re-hacher le mot de passe avant de le comparer à celui stocké dans la base de données, comme suit :

def mot_de_passe_valide(utilisateur, mot_de_passe):
    return sha1(mot_de_passe) == utilisateur['mot_de_passe'];
Utilisateur sha1(Mot de passe)
[email protected] 940c0f26fd5a30775bb1cbd1f6840398d39bb813
[email protected] 2451f04d62956cf5e910cce1f0e17d29e315d879

Le fait qu'il soit impossible de « dé-hacher » un mot de passe donne la fausse impression que cette méthode est sécurisée. Pourtant, les experts en sécurité suggèrent fortement de ne plus utiliser de telles fonctions pour hacher des mots de passe.

Pourquoi ? Parce que, ce que font les hackers, ce n'est pas tenter de retrouver un mot de passe à partir d'un hash. Ils vont tout simplement hacher une liste de mots de passe possibles et comparer les hash de ces mots de passe aux hash trouvés dans la base de données. S'il y en a un qui correspond, c'est qu'ils ont trouvé un mot de passe. Petit exemple :

database = {
    "940c0f26fd5a30775bb1cbd1f6840398d39bb813": "[email protected]",
    "2451f04d62956cf5e910cce1f0e17d29e315d879": "[email protected]"
}

for password in COMMON_PASSWORDS:
    if sha1(password) in database:
        print "Password of user " + database[sha1(password)] + ": " + password

Remarquez que le code ci-dessus attaque tous les mots de passe en même temps. Qu'il y ait 10 ou un million d'utilisateurs dans votre base de données, le hacker ne prendra pas plus de temps à trouver un mot de passe valide. Dans l'absolu, le fait qu'il y ait plus d'utilisateurs aide même le hacker, car il est plus probable qu'un utilisateur ait un mot de passe facile à deviner.

Vous vous dites sûrement qu'il y a trop de mots de passe possibles pour que cette technique soit faisable, mais il y a bien moins de mots de passe que ce que vous pensez. Trop de personnes utilisent des mots de passe basés sur des mots du dictionnaire, avec éventuellement de temps à autres quelques chiffres et lettres en plus par-ci par-là. Voici par exemple une liste des 10 mots de passe les plus utilisés, constatés dans une étude faite par Imperva sur 32 millions de hash de mots de passe du site RockYou.com leakés il y a trois ans :

  1. 123456
  2. 12345
  3. 123456789
  4. Password
  5. iloveyou
  6. princess
  7. rockyou
  8. 1234567
  9. 12345678
  10. abc123

Si vous ajoutez à la simplicité générale des mots de passe le fait que la plupart des fonctions de hachage comme sha1() et md5() sont extrèmement rapides à exécuter, vous vous rendez probablement compte de la faible sécurité de cette méthode de stockage.

Par rapide, j'entends qu'un ordinateur moderne peut en calculer plusieurs milliards par seconde (oui oui, vous avez bien lu). À titre d'exemple, une ATI Radeon 6990 peut calculer 10,3 milliards de MD5 ou 2,6 milliards de SHA1 par seconde (source) ! Si je vous parle d'une carte graphique, c'est parce que les processeurs des ces dernières sont largement plus rapides que les processeurs pour calculer des hash.

Il fut un temps où les hackers utilisaient des rainbow tables. Il s'agit d'une liste gigantesque qui contient des hash pré-calculés pour tous les mots de passe fréquemment utilisés. Pour les fonctions rapides, comme celles dont il est question ici, elles ne sont plus ou très peu utilisées.

Notons que selon une étude publiée par Zone Alarm il y a quelques années, environ deux tiers des mots de passe ont une taille inférieure ou égale à 8 caractères (8 caractères : 20 %, 7 caractères : 19 %, 6 caractères : 26 %). Couplé au fait que selon l'étude d'Imperva, 60 % des utilisateurs n'utilisent que des caractères alphanumériques dans leurs mots de passe, cela veut dire qu'un hacker est susceptible de récupérer 60 % des mots de passe présents dans votre base de données en moins de 24 heures s'ils sont hachés à l'aide de sha1(). Ajoutons aussi que pour tous les utilisateurs qui ont un mot de passe trop commun (et donc dans le haut de la liste des mots de passe fréquents), ce dernier sera probablement deviné en moins d'un heure.

À partir d'une base de données contenant des mots de passe hachés avec SHA1, un hacker est susceptible de récupérer 60 % des mots de passe en moins de 24 heures.

Cela dit, pour les mots de passe plus complexes (comprenez minimum 12 caractères avec des lettres majuscules et minuscules, des chiffres et des caractères spéciaux), ce genre d'attaque prend tout de suite plus de temps. À titre d'exemple, il est possible de trouver tous les mots de passe de 8 caractères sans caractères spéciaux (donc minuscules + majuscules + chiffres) en seulement 5 heures 30 avec l'ATI Radeon 6990. Pour la même taille de mot de passe avec caractères spéciaux (lettres accentuées + tous les autres caractères, comme $, ^, ', etc.), cela prendrait environ trois mois et une semaine.

Par contre, n'oublions pas qu'on parle ici d'un seul système avec un seul processeur graphique. Si on part dans des systèmes avec plusieurs cartes graphiques, plusieurs ordinateurs, etc., le temps est largement réduit. Il suffit d'un bon botnet pour craquer n'importe quel mot de passe rapidement. De plus, si on couple tout ceci à la loi de Moore, on se rend vite compte que plus les années passent, moins les fonctions comme sha1() et md5() sont recommandées pour stocker des mots de passe.

Au fil des années, les fonctions de hachage comme SHA1 et MD5 deviennent de plus en plus rapides à exécuter et deviennent donc de moins en moins recommandées pour stocker des mots de passe.

Cette méthode de stockage de mots de passe était utilisée par LinkedIn, jusqu'au moment où, en 2012, un grand nombre des hash des mots de passe de leurs utilisateurs ont atterri sur la toile. Les hackers ont pu deviner la plupart des mots de passe correspondants aux hash.

Bref, stocker un « bête » hash de mot de passe n'est pas une bonne idée. Si un hacker accède à votre base de données, il récupèrera la majorité des mots de passe de vos utilisateurs.

Encore un peu mieux, mais toujours mauvais : SHA1, avec un sel fixe

Une méthode qui est déjà un peu mieux consiste à « saler » le mot de passe avant de le hacher. Le fait de saler un mot de passe consiste à relever son goût en lui ajoutant une épice bien utile lui préfixer une longue chaîne de caractères aléatoires. Cette chaîne ne doit évidemment pas être stockée au même endroit que les mots de passe, sinon tout ça ne sert à rien.

Utilisateur sha1('SeL123456789' + Mot de passe)
[email protected] 6a98b39b7f8e979bb1fd7e2af04d613d48085755
[email protected] d4f1f513cd444de6cea353473944ffd42faafccb

Avec ce système, si un petit malin accède à votre base de données, il sera plus compliqué pour lui de deviner les mots de passe, car il ne connaîtra pas le sel. Cela dit, si le hacker a accès à votre base de données, il aura probablement également accès à votre code-source et donc au sel. C'est pourquoi il ne faut pas considérer le sel comme quelque chose de secret.

Un sel ne doit jamais être considéré comme étant secret.

Cependant, même si le sel n'est pas secret, cela rend les vieilles rainbow tables impossibles à utiliser, étant donné qu'elles partent du principe qu'il n'y a pas de sel. Enfin bon, étant donné que ce type d'attaque n'est plus utilisé, vous comprendrez qu'un sel global n'aide pas vraiment. Le hacker peut encore toujours utiliser la même boucle for vue auparavant :

database = {
    "6a98b39b7f8e979bb1fd7e2af04d613d48085755": "[email protected]",
    "d4f1f513cd444de6cea353473944ffd42faafccb": "[email protected]"
}

for password in COMMON_PASSWORDS:
    if sha1(SEL + password) in database:
        print "Password of user " + database[sha1(SEL + password)] + ": " + password

Bref, ajouter un sel fixe, c'est déjà mieux, mais ça ne change rien par rapport à la méthode précédente, si ce n'est qu'il rend impossible l'utilisation des rainbow tables.

Déjà mieux, mais perfectible : SHA1, avec un sel par utilisateur

La solution suivante consiste à générer un sel unique par utilisateur. Ce que ça change, c'est que plutôt que de tenter de trouver les mots de passe de tous vos utilisateurs en même temps, le hacker sera obligé de partir en quête du mot de passe de chaque utilisateur indépendamment. Il aura donc largement moins de chance de tomber rapidement sur un mot de passe valide.

Utilisateur Sel sha1(Sel + Mot de passe)
[email protected] 526b8ee06c8a9 105c47e907ca201a15b2faa82aa3e204cddadcc1
[email protected] 526b8eec23bb1 1f220291cd81763058bca17d217baa7c6adef706

Cette fois, pour authentifier l'utilisateur, c'est tout petit peu plus compliqué, mais toujours relativement simple :

def valid_password(user, password):
    return sha1(user['salt'] + password) == user['password'];

Par contre, trouver un mot de passe devient alors beaucoup plus coûteux :

database = {
    "105c47e907ca201a15b2faa82aa3e204cddadcc1": "[email protected]",
    "1f220291cd81763058bca17d217baa7c6adef706": "[email protected]"
}

for user in users:
    USER_SALT = user['salt']

    for password in COMMON_PASSWORDS:
        if sha1(USER_SALT + password) in database:
           print "Password of user " + database[sha1(USER_SALT + password)] + ": " + password

Si vous avez un million d'utilisateurs, cela veut dire qu'un hacker mettra un million de fois plus de temps à trouver les mots de passe de tous vos utilisateurs. Cependant, ce n'est toujours pas impossible. Au lieu d'une heure processeur, il lui en faudra 1 million, ce qui peut être loué chez Amazon pour la modique somme de $40,000 (soit 28.970 €). Certes, c'est une grosse somme, mais n'oublions pas le nombre de mots de passe que le hacker obtient pour cette somme, ainsi que la somme que ça va lui rapporter quand il revendra ces mots de passe ou les utilisera pour entrer dans des comptes PayPal, Amazon, etc. Ça revient à seulement 3 centimes par mot de passe !

Au fond, le véritable problème est que les fonctions de hachage peuvent être exécutées à une vitesse bien trop rapide. Malgré le fait que ces fonctions aient été conçues en pensant à la sécurité, elles ont également été conçues pour rester rapides lorsqu'elles sont exécutées sur des données plus conséquentes, comme des fichiers entiers par exemple. En résumé, ces fonctions n'ont pas été conçues pour stocker des mots de passe.

Les fonctions comme MD5 et SHA1 ne doivent, de par leur conception, ne pas être utilisées pour stocker des mots de passe.

Une bonne solution : Bcrypt

Plutôt donc que d'utiliser une fonction de hachage qui n'a pas été conçue pour chiffrer des mots de passe, on peut en utiliser une qui l'est. Ces fonctions, en plus d'être des fonctions de hachage unidirectionnelles sécurisées — comme sha1() —, ont été conçues pour être lentes (ironique, non ?).

Les bonnes fonctions de hachage cryptographique doivent avoir été conçues pour être lentes.

Un exemple d'une telle fonction est Bcrypt. Cette fonction, basée sur l'algorithme Blowfish, met approximativement 100 millisecondes à s'exécuter, ce qui est déjà 10.000 fois plus lent que sha1(). C'est assez rapide pour que l'utilisateur ne le remarque pas lorsqu'il se connecte, tout en restant assez lent pour qu'il soit extrêmement coûteux de l'exécuter pour hacher une grosse liste de mots de passe fréquents.

Par exemple, si un hacker veut comparer un mot de passe chiffré à l'aide de Bcrypt avec un milliard de mots de passe fréquents, cela prendra environ 30.000 heures processeur (soit 870 €). Tout ça, pour un seul et unique mot de passe. Cela revient à 30.000 fois plus cher que si les mots de passe étaient chiffrés via sha1() avec un sel par utilisateur. Ce n'est pas impossible à faire, mais c'est déjà bien plus que les moyens que la plupart des hackers sont prêts à mettre en oeuvre.

Bcrypt fonctionne en exécutant une fonction de chiffrage interne plusieurs fois d'affilée, ce qui ralentit son exécution (PBKDF2 utilise le même principe). Le nombre de fois qu'il boucle est configurable via le paramètre log_rounds (qui, comme son nom l'indique, est logarithmique). Ça veut dire que si un jour on sort des processeurs ou des cartes graphiques 1.000 fois plus puissantes que ce qu'on a aujourd'hui, il suffit de reconfigurer le système et de modifier ce fameux log_rounds en ajoutant simplement 10 à la valeur actuelle. Cela aura pour effet d'annuler l'avantage du nouveau processeur.

La lenteur de Bcrypt est due au fait qu'il exécute une fonction de chiffrage interne plusieurs fois d'affilée.

Vous allez me dire que puisque Bcrypt est lent, il suffit de réutiliser ces bonnes vieilles rainbow tables. Sauf que justement, Bcrypt intègre un système de sel par utilisateur. Par ailleurs, de nombreuses implémentations de Bcrypt, dont celle de PHP (disponible à partir de PHP 5.5.0, mais portée sur les versions antérieures à partir de la 5.3.7, voir ici), intègrent directement le sel dans la même chaîne de caractères que le hash. Il n'est donc pas nécessaire de créer une colonne à part pour le sel dans la base de données (ce qui en plus au passage rend les choses un poil moins claires pour le hacker).

Bcrypt intègre directement le système de sel unique par utilisateur.

Voilà à quoi ressemblerait la table avec les mots de passe chiffrés à l'aide de Bcrypt :

Utilisateur bcrypt(Mot de passe)
[email protected] $2a$10$YCZuPKAvSTKNbdwyw0d.rOtrg/HecepnblK6fTFKtBzuuDbZFNvQC
[email protected] $2a$10$ClILMkEsm7T/TSCqAbko0uITgYg2OvVt5.23uo9nM0xIXwroFdpOm

La structure de la chaîne de caractères est la suivante :

Structure Bcrypt

Comme vous pouvez le voir, toutes les données sont stockées dans la même chaîne de caractères : le paramètre log_rounds qui contrôle la lenteur, le sel et le mot de passe haché.

Exemple concret en PHP

Ci-dessous, vous pouvez trouver un exemple qui montre comment stocker un mot de passe en base de données et vérifier la validité de ce dernier en PHP (≥ 5.5.0 ou ≥ 5.3.7 avec le portage installé et importé via la fonction require).

function create_user($user, $password) {
    $dbh = connect();

    # Via les options, on peut définir le coût, qui équivaut au fameux log_rounds expliqué ci-dessus
    $options = array('cost' => 11);
    $hashed_password = password_hash($password, PASSWORD_BCRYPT, $options);

    $response = $dbh->prepare("INSERT INTO users
                               SET user = :user,
                               hashed_password = :hashed_password");
    $response->execute(array(
        'user' => $user,
        'hashed_password' => $hashed_password
    ));
}

function is_valid_user($user, $password) {
    require_once('includes/blowfish.inc.php');

    $dbh = connect();

    $response = $dbh->prepare("SELECT hashed_password
                               FROM users
                               WHERE user = :user");

    $response->execute(array(
        'user' => $user
    ));

    if ($data = $response->fetch()) {
        return password_verify($password, $data->hashed_password);
    }

    return false;
}

Comme vous pouvez le voir, la fonction password_verify est intelligente et repèrera toutes les données dont elle a besoin (le log_rounds, le sel et le mot de passe haché) directement dans la chaîne de caractères. Simple, rapide (mais pas trop :p) et efficace.

L'importance d'un bon mot de passe

Quelle que soit la méthode de chiffrage utilisée, si un utilisateur a le mot de passe « motdepasse », vous ne pouvez rien pour lui. Les mots de passe les plus simples sont évidemment les premiers à être testés par un hacker, donc si votre mot de passe est dans le haut de la liste, il sera probablement deviné très rapidement.

Vous l'aurez compris : un bon mot de passe est un mot de passe qui se trouve le plus bas possible dans la liste des mots de passe fréquents, voire qui ne s'y trouve pas. Autrement dit : pas de mots de passe basés sur des mots du dictionnaire, même avec des petites modifications comme des lettres et/ou chiffres supplémentaires. Tous ces mots de passe seront dans les quelques premiers millions de mots de passe testés.

Un bon mot de passe doit idéalement faire au minimum 12 caractères, comporter des majuscules, des minuscules, des chiffres et des caractères spéciaux (minimum deux de chaque type). N'hésitez jamais à tester vos mots de passe sur un utilitaire en ligne comme Password Meter.

Un bon mot de passe comporte au minimum 12 caractères et doit être composé de lettres minuscules, majuscules, de chiffres et de caractères spéciaux.

Une méthode consiste à créer une phrase, la raccourcir en ne prenant que les premières lettres de chaque mot, mettre par exemple uniquement les lettres de la première moitié de l'alphabet en majuscules, y insérer quelques chiffres et caractères spéciaux, et le tour est joué. Exemple :

  • Choisir un bon mot de passe est très important
  • cubmdpeti
  • CuBMDpEtI
  • CuB3MD!pE6tI

Certaines personnes suggèrent d'utiliser des « phrases de passe » (passphrase) plutôt que des mots de passe, comme par exemple « J'aime bien partir en vacances dans les Ardennes ! ». Si le système autorise les longs mots de passe, c'est un mot de passe relativement puissant et plus facile à retenir qu'un mot de passe complexe. En effet, il comporte des majuscules, des minuscules et des caractères spéciaux. Cet exemple manque juste de chiffres, mais en soi, vu la longueur, ça rattrape la chose... Enfin bon, vu le nombre de systèmes qui restreignent arbitrairement la longueur et les caractères utilisables dans les mots de passe, ce n'est malheureusement pas fort utilisable...

Je n'ai personnellement jamais compris les sites qui empêchent d'utiliser certains caractères spéciaux ou qui restreignent le mot de passe à par exemple 10 caractères (ça existe...). Sachant qu'en théorie, le mot de passe est de toute façon haché par après, la chaîne de caractères à stocker en base de données est de taille fixe. Bref, restreindre le choix des caractères pour un mot de passe n'est pas justifiable et est une grave erreur. Je suspecte certains des sites qui font cela de stocker les mots de passe en clair dans leur base de données. Ce qui les rend alors peureux des caractères spéciaux sont les problèmes d'encodage qui pourraient survenir avec les caractères accentués... Enfin bon, j'espère que j'ai tort...

Voici que cet article touche à sa fin, j'espère que vous avez appris des choses :-) N'hésitez surtout pas à le partager pour sensibiliser les gens à l'importance cruciale d'une bonne méthode de stockage de mots de passe (et à l'importance de choisir un bon mot de passe !).

Merci d'avoir pris le temps de lire cet article. Si vous l'appréciez, n'hésitez pas à le partager autour de vous.

Si vous le souhaitez, vous pouvez également soutenir l'écriture de ces articles via le bouton Flattr ci-dessous ou en m'offrant une bière.

Vous pouvez également vous abonner ou me suivre sur les réseaux sociaux à l'aide des boutons ci-dessous.

Commentaires

comments powered by Disqus
S'abonner

Si vous souhaitez recevoir un e-mail lors de mes prochaines publications, laissez-moi votre adresse ci-dessous. Elle ne sera jamais divulguée à des tiers.