J'ai besoin de hacher les mots de passe pour le stockage dans une base de données. Comment puis-je faire cela en Java?
J'espérais prendre le mot de passe en texte brut, ajouter un sel aléatoire, puis stocker le sel et le mot de passe haché dans la base de données.
Ensuite, lorsqu'un utilisateur voulait se connecter, je pouvais prendre son mot de passe soumis, ajouter le sel aléatoire de ses informations de compte, le hacher et voir s'il correspond au mot de passe de hachage stocké avec ses informations de compte.
Pour ce faire, vous pouvez utiliser une fonction intégrée au runtime Java. Le SunJCE dans Java 6 prend en charge PBKDF2, ce qui est un bon algorithme à utiliser pour le hachage du mot de passe.
byte[] salt = new byte[16];
random.nextBytes(salt);
KeySpec spec = new PBEKeySpec("password".toCharArray(), salt, 65536, 128);
SecretKeyFactory f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
byte[] hash = f.generateSecret(spec).getEncoded();
Base64.Encoder enc = Base64.getEncoder();
System.out.printf("salt: %s%n", enc.encodeToString(salt));
System.out.printf("hash: %s%n", enc.encodeToString(hash));
Voici une classe d’utilitaire que vous pouvez utiliser pour l’authentification de mot de passe PBKDF2:
import Java.security.NoSuchAlgorithmException;
import Java.security.SecureRandom;
import Java.security.spec.InvalidKeySpecException;
import Java.security.spec.KeySpec;
import Java.util.Arrays;
import Java.util.Base64;
import Java.util.regex.Matcher;
import Java.util.regex.Pattern;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
/**
* Hash passwords for storage, and test passwords against password tokens.
*
* Instances of this class can be used concurrently by multiple threads.
*
* @author erickson
* @see <a href="http://stackoverflow.com/a/2861125/3474">StackOverflow</a>
*/
public final class PasswordAuthentication
{
/**
* Each token produced by this class uses this identifier as a prefix.
*/
public static final String ID = "$31$";
/**
* The minimum recommended cost, used by default
*/
public static final int DEFAULT_COST = 16;
private static final String ALGORITHM = "PBKDF2WithHmacSHA1";
private static final int SIZE = 128;
private static final Pattern layout = Pattern.compile("\\$31\\$(\\d\\d?)\\$(.{43})");
private final SecureRandom random;
private final int cost;
public PasswordAuthentication()
{
this(DEFAULT_COST);
}
/**
* Create a password manager with a specified cost
*
* @param cost the exponential computational cost of hashing a password, 0 to 30
*/
public PasswordAuthentication(int cost)
{
iterations(cost); /* Validate cost */
this.cost = cost;
this.random = new SecureRandom();
}
private static int iterations(int cost)
{
if ((cost < 0) || (cost > 30))
throw new IllegalArgumentException("cost: " + cost);
return 1 << cost;
}
/**
* Hash a password for storage.
*
* @return a secure authentication token to be stored for later authentication
*/
public String hash(char[] password)
{
byte[] salt = new byte[SIZE / 8];
random.nextBytes(salt);
byte[] dk = pbkdf2(password, salt, 1 << cost);
byte[] hash = new byte[salt.length + dk.length];
System.arraycopy(salt, 0, hash, 0, salt.length);
System.arraycopy(dk, 0, hash, salt.length, dk.length);
Base64.Encoder enc = Base64.getUrlEncoder().withoutPadding();
return ID + cost + '$' + enc.encodeToString(hash);
}
/**
* Authenticate with a password and a stored password token.
*
* @return true if the password and token match
*/
public boolean authenticate(char[] password, String token)
{
Matcher m = layout.matcher(token);
if (!m.matches())
throw new IllegalArgumentException("Invalid token format");
int iterations = iterations(Integer.parseInt(m.group(1)));
byte[] hash = Base64.getUrlDecoder().decode(m.group(2));
byte[] salt = Arrays.copyOfRange(hash, 0, SIZE / 8);
byte[] check = pbkdf2(password, salt, iterations);
int zero = 0;
for (int idx = 0; idx < check.length; ++idx)
zero |= hash[salt.length + idx] ^ check[idx];
return zero == 0;
}
private static byte[] pbkdf2(char[] password, byte[] salt, int iterations)
{
KeySpec spec = new PBEKeySpec(password, salt, iterations, SIZE);
try {
SecretKeyFactory f = SecretKeyFactory.getInstance(ALGORITHM);
return f.generateSecret(spec).getEncoded();
}
catch (NoSuchAlgorithmException ex) {
throw new IllegalStateException("Missing algorithm: " + ALGORITHM, ex);
}
catch (InvalidKeySpecException ex) {
throw new IllegalStateException("Invalid SecretKeyFactory", ex);
}
}
/**
* Hash a password in an immutable {@code String}.
*
* <p>Passwords should be stored in a {@code char[]} so that it can be filled
* with zeros after use instead of lingering on the heap and elsewhere.
*
* @deprecated Use {@link #hash(char[])} instead
*/
@Deprecated
public String hash(String password)
{
return hash(password.toCharArray());
}
/**
* Authenticate with a password in an immutable {@code String} and a stored
* password token.
*
* @deprecated Use {@link #authenticate(char[],String)} instead.
* @see #hash(String)
*/
@Deprecated
public boolean authenticate(String password, String token)
{
return authenticate(password.toCharArray(), token);
}
}
Voici un implémentation complète avec deux méthodes faisant exactement ce que vous voulez:
String getSaltedHash(String password)
boolean checkPassword(String password, String stored)
Le fait est que même si un attaquant a accès à la fois à votre base de données et à votre code source, les mots de passe sont toujours sûrs.
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import Java.security.SecureRandom;
import org.Apache.commons.codec.binary.Base64;
public class Password {
// The higher the number of iterations the more
// expensive computing the hash is for us and
// also for an attacker.
private static final int iterations = 20*1000;
private static final int saltLen = 32;
private static final int desiredKeyLen = 256;
/** Computes a salted PBKDF2 hash of given plaintext password
suitable for storing in a database.
Empty passwords are not supported. */
public static String getSaltedHash(String password) throws Exception {
byte[] salt = SecureRandom.getInstance("SHA1PRNG").generateSeed(saltLen);
// store the salt with the password
return Base64.encodeBase64String(salt) + "$" + hash(password, salt);
}
/** Checks whether given plaintext password corresponds
to a stored salted hash of the password. */
public static boolean check(String password, String stored) throws Exception{
String[] saltAndHash = stored.split("\\$");
if (saltAndHash.length != 2) {
throw new IllegalStateException(
"The stored password must have the form 'salt$hash'");
}
String hashOfInput = hash(password, Base64.decodeBase64(saltAndHash[0]));
return hashOfInput.equals(saltAndHash[1]);
}
// using PBKDF2 from Sun, an alternative is https://github.com/wg/scrypt
// cf. http://www.unlimitednovelty.com/2012/03/dont-use-bcrypt.html
private static String hash(String password, byte[] salt) throws Exception {
if (password == null || password.length() == 0)
throw new IllegalArgumentException("Empty passwords are not supported.");
SecretKeyFactory f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
SecretKey key = f.generateSecret(new PBEKeySpec(
password.toCharArray(), salt, iterations, desiredKeyLen));
return Base64.encodeBase64String(key.getEncoded());
}
}
Nous stockons 'salt$iterated_hash(password, salt)'
. Le sel est composé de 32 octets aléatoires et son objectif est que, si deux personnes différentes choisissent le même mot de passe, les mots de passe stockés seront toujours différents.
Le iterated_hash
, qui est fondamentalement hash(hash(hash(... hash(password, salt) ...)))
rend très coûteux pour un attaquant potentiel qui a accès à votre base de données de deviner les mots de passe, de les hacher et de rechercher des hachages dans la base de données. Vous devez calculer ce iterated_hash
à chaque fois qu'un utilisateur se connecte, mais cela ne vous coûte pas très cher comparé à l'attaquant qui passe près de 100% de son temps à calculer des hachages.
BCrypt est une très bonne bibliothèque, et il y a un port Java .
Vous pouvez calculer les hachages en utilisant MessageDigest
, mais ceci est faux en termes de sécurité. Les hachages ne doivent pas être utilisés pour stocker les mots de passe, car ils sont facilement cassables.
Vous devez utiliser un autre algorithme tel que bcrypt, PBKDF2 et scrypt pour stocker vos mots de passe. Voir ici .
Vous pouvez utiliser la bibliothèque Shiro (anciennement JSecurity ) implémentation de ce qui est décrit par OWASP .
Il semble également que la bibliothèque JASYPT possède un tilitaire similaire .
En plus de bcrypt et PBKDF2 mentionnés dans d'autres réponses, je recommanderais de regarder scrypt
MD5 et SHA-1 ne sont pas recommandés car ils sont relativement rapides. Par conséquent, en utilisant un calcul distribué "rent rent" (exemple: EC2) ou un processeur graphique haut de gamme moderne, on peut "déchiffrer" les mots de passe en utilisant des attaques par force brute/dictionnaire avec des coûts relativement bas et raisonnables. temps.
Si vous devez les utiliser, répétez au moins l'algorithme une quantité de fois significative et prédéfinie (plus de 1000).
Voir ici pour plus d'informations: https://security.stackexchange.com/questions/211/how-to-securely-hash-passwords
Et ici: http://codahale.com/how-to-safely-store-a-password/ (critique la famille SHA, MD5, etc. à des fins de hachage du mot de passe)
Entièrement d'accord avec Erickson sur le fait que PBKDF2 est la réponse.
Si vous n'avez pas cette option, ou si vous n'avez besoin que d'un hachage, Apache Commons DigestUtils est beaucoup plus facile que d'obtenir le code JCE correctement: https://commons.Apache.org/proper/commons-codec/apidocs /org/Apache/commons/codec/digest/DigestUtils.html
Si vous utilisez un hash, utilisez sha256 ou sha512. Cette page contient de bonnes recommandations sur la gestion du mot de passe et le hachage (notez qu'elle ne recommande pas le hachage pour la gestion du mot de passe): http://www.daemonology.net/blog/2009-06-11-cryptographic-right-answers .html
Bien que la recommandation du NIST PBKDF2 ait déjà été mentionnée, j'aimerais souligner qu'il y avait un public concours de hachage de mots de passe qui s'est déroulé de 2013 à 2015. Finalement, Argon2 a été choisi comme fonction de hachage de mot de passe recommandée.
Il y a un assez bien adopté liaison Java pour la bibliothèque d'origine (C native) que vous pouvez utiliser.
Dans le cas d'utilisation moyenne, je ne pense pas que le choix de PBKDF2 sur Argon2 ou inversement ait une importance en termes de sécurité. Si vous avez de fortes exigences en matière de sécurité, je vous recommande d’envisager Argon2 dans votre évaluation.
Pour plus d'informations sur la sécurité des fonctions de hachage de mot de passe, voir security.se .
Ici vous avez deux liens pour le hachage MD5 et d'autres méthodes de hachage:
API Javadoc: http://Java.Sun.com/j2se/1.4.2/docs/api/Java/security/MessageDigest.html
Vous pouvez utiliser Spring SecurityCrypto (a seulement 2 dépendances de compilation optionnelles ), qui prend en charge PBKDF2 , BCrypt et SCrypt cryptage du mot de passe.
SCryptPasswordEncoder sCryptPasswordEncoder = new SCryptPasswordEncoder();
String sCryptedPassword = sCryptPasswordEncoder.encode("password");
boolean passwordIsValid = sCryptPasswordEncoder.matches("password", sCryptedPassword);
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String bCryptedPassword = bCryptPasswordEncoder.encode("password");
boolean passwordIsValid = bCryptPasswordEncoder.matches("password", bCryptedPassword);
Pbkdf2PasswordEncoder pbkdf2PasswordEncoder = new Pbkdf2PasswordEncoder();
String pbkdf2CryptedPassword = pbkdf2PasswordEncoder.encode("password");
boolean passwordIsValid = pbkdf2PasswordEncoder.matches("password", pbkdf2CryptedPassword);
Parmi tous les schémas de hachage standard, LDAP ssha est le plus sûr à utiliser,
http://www.openldap.org/faq/data/cache/347.html
Je voudrais simplement suivre les algorithmes spécifiés ici et utiliser MessageDigest pour faire le hachage.
Vous devez stocker le sel dans votre base de données comme vous l'avez suggéré.