Menu

PHP password_hash() et password_verify() : mots de passe

Hachez un mot de passe avec password_hash($password, PASSWORD_DEFAULT), stockez le résultat de 60 caractères, et vérifiez une connexion avec password_verify($password, $hash). Pourquoi le hash change à chaque fois, le coût, password_needs_rehash, Argon2id, et pourquoi md5 et sha1 sont inadaptés aux mots de passe.

Cette page contient des éditeurs exécutables - modifiez, exécutez et voyez la sortie instantanément.

Stockez les mots de passe avec password_hash($password, PASSWORD_DEFAULT) et vérifiez une connexion avec password_verify($password, $hash), qui renvoie true ou false. Ne stockez jamais le mot de passe lui-même, et n'utilisez jamais md5() ou sha1() pour cela.

Exécutez-le deux fois : le hash est différent à chaque fois, et les deux versions sont validées. C'est pareil pour chaque hash de cette page, donc votre sortie ne correspondra à celle de personne d'autre.

Se connecter avec password_verify

Une connexion compare le mot de passe envoyé au hash enregistré à la création du compte. Ce bloc « inscrit » un utilisateur puis affiche un formulaire de connexion ; essayez le bon mot de passe (hunter2), puis un mauvais.

Le message d'erreur est le même pour un email inconnu et un mauvais mot de passe. Dire lequel était faux indique à un attaquant quels emails ont un compte. Après une connexion réussie, un vrai site stocke l'identifiant de l'utilisateur dans la session, comme le montre la page sur les sessions.

Ce que contient le hash

Un hash bcrypt est une chaîne qui contient tout ce dont password_verify() a besoin : l'algorithme, le coût et le sel, suivis du hash lui-même. password_get_info() relit les premières parties.

Comme le sel est stocké dans le hash, une seule colonne de base de données suffit, et deux utilisateurs avec le même mot de passe obtiennent quand même des hashs différents. Faites de cette colonne un VARCHAR(255) : bcrypt fait 60 caractères, mais PASSWORD_DEFAULT a le droit de passer à un algorithme dont la sortie est plus longue.

Choisir le coût

Le coût est la lenteur volontaire de chaque hash, et chaque cran supplémentaire double le travail. PHP 8.3 utilise le coût 10 pour bcrypt (PHP 8.4 a relevé la valeur par défaut à 12). Une connexion devrait prendre environ 100 à 300 ms de hachage ; mesurez-le sur votre propre serveur :

Les chiffres dépendent de la machine, mais chaque ligne devrait valoir environ le double de la précédente. Plus haut est plus sûr contre le cassage et plus lent pour chaque connexion, choisissez donc le coût le plus élevé que votre page de connexion peut se permettre.

Mettre à jour les anciens hashs avec password_needs_rehash

Quand vous relevez le coût ou que PHP change PASSWORD_DEFAULT, les hashs existants continuent de fonctionner, et password_needs_rehash() vous dit lesquels sont dépassés. Le seul moment où vous avez le mot de passe en clair est la connexion, c'est donc là que vous faites la mise à jour :

Le préfixe $2y$10$ devient $2y$12$, et à la connexion suivante password_needs_rehash() renvoie false.

N'utilisez jamais md5 ni sha1 pour les mots de passe

md5() et sha1() sont des fonctions de hachage génériques rapides et sans sel. La rapidité est exactement ce qu'il ne faut pas pour les mots de passe : un attaquant qui a récupéré une table peut essayer des milliards de tentatives par seconde sur un GPU, et le hash sans sel d'un mot de passe courant se trouve déjà dans des tables de correspondance publiques. Comparez la vitesse :

Dans le temps où password_hash() produit un hash, md5 en produit des centaines de milliers, donc chaque tentative d'un casseur coûte autant de fois moins cher. Cherchez 5f4dcc3b5aa765d61d8327deb882cf99 et vous trouverez immédiatement le mot « password ».

Si vous héritez d'une table de hashs md5, vous ne pouvez pas les convertir directement. Enveloppez-les plutôt : stockez password_hash($oldMd5) et vérifiez avec password_verify(md5($input), $stored), puis remplacez chacun par un hash normal à la prochaine connexion de cet utilisateur.

Argon2id et la limite de 72 octets

PASSWORD_ARGON2ID utilise plus de mémoire par hash, ce qui rend le cassage par GPU bien plus coûteux. Il est disponible quand PHP est compilé avec libargon2 (c'est le cas de la plupart des paquets Linux) ; vérifiez avec defined('PASSWORD_ARGON2ID').

La dernière ligne affiche bool(true) : deux mots de passe différents correspondent, car bcrypt ignore tout ce qui suit l'octet 72. Les vrais mots de passe atteignent rarement cette taille, mais des phrases de passe en japonais ou dans d'autres écritures multioctets atteignent 72 octets vers 24 caractères. Si vous autorisez des mots de passe très longs, utilisez Argon2id, qui n'a pas cette limite. password_verify() fonctionne avec les deux algorithmes, vous pouvez donc changer avec password_needs_rehash() comme ci-dessus.

Questions fréquentes

Pourquoi password_hash donne-t-il un résultat différent à chaque fois ?

Chaque appel génère un nouveau sel aléatoire et le stocke dans le hash, donc le même mot de passe donne une chaîne différente à chaque fois. C'est voulu : password_verify() relit le sel dans le hash stocké, elle renvoie donc toujours true pour le bon mot de passe.

Quelle longueur donner à la colonne du mot de passe pour password_hash ?

Un hash bcrypt produit par PASSWORD_DEFAULT fait aujourd'hui 60 caractères, mais l'algorithme par défaut peut changer dans une future version de PHP. Utilisez VARCHAR(255) pour qu'un hash plus long tienne sans migration.

Pourquoi password_verify renvoie-t-il toujours false ?

Les causes habituelles sont un hash tronqué (une colonne VARCHAR(50) coupe le hash de 60 caractères), le passage en second argument d'un nouveau password_hash() au lieu du hash stocké, la comparaison avec un hash fait par md5(), ou des espaces en trop dans la valeur stockée. Vérifiez que strlen($hash) vaut 60 et passez le mot de passe en clair en premier argument.

Peut-on décrypter une valeur de password_hash en PHP ?

Non. Un hash est à sens unique : aucune fonction ne le retransforme en mot de passe. Pour vérifier un mot de passe, on hache la tentative et on compare, ce que fait password_verify(). Pour un mot de passe oublié, envoyez un lien de réinitialisation au lieu de l'ancien mot de passe.

Faut-il utiliser PASSWORD_DEFAULT, PASSWORD_BCRYPT ou PASSWORD_ARGON2ID ?

PASSWORD_DEFAULT est le choix habituel : c'est bcrypt aujourd'hui et PHP pourra le faire évoluer vers un algorithme plus fort, que password_needs_rehash() vous permet d'adopter progressivement. PASSWORD_ARGON2ID résiste mieux au cassage par GPU et convient très bien quand votre version de PHP l'inclut.

Illustration des langages de programmation de Coddy

Apprendre à coder avec Coddy

COMMENCER