Come posso eseguire l'hashing di una password in Java?


176

Ho bisogno di hash password per l'archiviazione in un database. Come posso farlo in Java?

Speravo di prendere la password in chiaro, aggiungere un salt casuale, quindi archiviare il salt e la password con hash nel database.

Quindi, quando un utente voleva accedere, potevo prendere la sua password inviata, aggiungere il salt casuale dalle informazioni del suo account, l'hash e vedere se corrisponde alla password hash memorizzata con le informazioni del suo account.


11
@YGL questo in realtà non è una ricombinazione al giorno d'oggi con gli attacchi GPU che sono così economici, la famiglia SHA è in realtà una pessima scelta per l'hash della password (troppo veloce) anche con il sale. Usa bcrypt, scrypt o PBKDF2
Eran Medan il

11
Perché questa domanda è stata chiusa? Questa è una domanda per un vero problema di ingegneria e le risposte sono preziose. L'OP non sta chiedendo una biblioteca, sta chiedendo come risolvere il problema di ingegneria.
stackoverflowuser2010

12
Semplicemente stupefacente. Questa domanda ha 52 voti e qualcuno decide di chiuderla come "fuori tema".
stackoverflowuser2010

1
Sì, ho già pubblicato su Meta su questo numero di chiusure prima, ma sono stato picchiato piuttosto male.
Chris Dutrow,

8
Questa domanda dovrebbe essere riaperta. È una domanda su come scrivere un programma per risolvere il problema descritto (autenticazione password), con una soluzione di codice breve. Vedere la parola chiave "libreria" non giustifica la chiusura riflessiva di una domanda; non sta chiedendo una raccomandazione per la biblioteca, sta chiedendo come eseguire l'hash delle password. Modifica: lì, risolto.
Erickson,

Risposte:


157

Per fare ciò, puoi effettivamente utilizzare una funzione integrata nel runtime Java. SunJCE in Java 6 supporta PBKDF2, che è un buon algoritmo da utilizzare per l'hash della password.

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));

Ecco una classe di utilità che è possibile utilizzare per l'autenticazione della password 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);
  }

}

11
Potresti essere un po 'diffidente nei confronti delle conversioni da byte a esadecimali con BigInteger: gli zeri iniziali vengono rimossi. Va bene per il debug rapido, ma ho visto bug nel codice di produzione a causa di questo effetto.
Thomas Pornin,

24
I punti salienti di @ thomas-pornin perché abbiamo bisogno di una libreria , non di un blocco di codice che è quasi lì. Spaventoso che la risposta accettata non risponda alla domanda su un argomento così importante.
Nilzor,

9
Usa l'algoritmo PBKDF2WithHmacSHA512 a partire da Java 8. È un po 'più forte.
iwan.z,

1
Note, ALGs esistenti non vengono eliminati nelle versioni successive: java_4: PBEWithMD5AndDES, DESede, DES java_5 / 6/7: PBKDF2WithHmacSHA1, PBE (solo in Java 5), PBEWithSHA1AndRC2_40, PBEWithSHA1And, PBEWithMD5AndTriple java_8: PBEWithHmacSHA224AndAES_128, PBEWithHmacSHA384AndAES_128, PBEWithHmacSHA512AndAES_128, RC4_40, PBKDF2WithHmacSHA256 , PBEWithHmacSHA1AndAES_128, RC4_128, PBKDF2WithHmacSHA224, PBEWithHmacSHA256AndAES_256, RC2_128, PBEWithHmacSHA224AndAES_256, PBEWithHmacSHA384AndAES_256, PBEWithHmacSHA512AndAES_256, PBKDF2WithHmacSHA512, PBEWithHmacSHA256AndAES_128, PBKDF2WithHmacSHA384, PBEWithHmacSHA1AndAES_256
iwan.z

4
@TheTosters Sì, il tempo di esecuzione sarà più lungo per password errate ; più specificamente, le password errate richiederanno lo stesso tempo delle password corrette. Previene gli attacchi temporali, anche se confesso che non riesco a pensare a un modo pratico per sfruttare una tale vulnerabilità in questo caso. Ma non tagli gli angoli. Solo perché non riesco a vederlo, non significa che una mente più subdola non lo farà.
Erickson,

97

Ecco un'implementazione completa con due metodi che fanno esattamente quello che vuoi:

String getSaltedHash(String password)
boolean checkPassword(String password, String stored)

Il punto è che anche se un utente malintenzionato ha accesso sia al database che al codice sorgente, le password sono comunque sicure.

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());
    }
}

Stiamo memorizzando 'salt$iterated_hash(password, salt)'. Il sale è composto da 32 byte casuali e lo scopo è che se due persone diverse scelgono la stessa password, le password memorizzate appariranno comunque diverse.

Il iterated_hashche è fondamentalmente hash(hash(hash(... hash(password, salt) ...)))molto costoso per un potenziale attaccante che ha accesso al tuo database per indovinare le password, eseguirne l'hashing e cercare hash nel database. Devi calcolare questo iterated_hashogni volta che un utente accede, ma non ti costa molto rispetto all'attaccante che spende quasi il 100% del suo tempo nel calcolo degli hash.


14
Mi dispiace, ma perché dovrei scegliere questo su una libreria esistente? Una biblioteca ha probabilmente maggiori probabilità di essere esaminata a fondo. Dubito che ognuno dei 14 voti positivi abbia analizzato il codice per eventuali problemi.
Joachim Sauer,

2
@JoachimSauer In pratica si tratta solo di utilizzare una libreria (javax.crypto) ma hai ragione: le password vuote non sono supportate. Aggiunta un'eccezione per renderla esplicita. Grazie!
Martin Konicek,

3
Probabilmente dovresti cambiare le firme dei metodi in char[] passwordinvece di String password.
Assylias,

2
Anche se sembra che il riferimento non riceva un accordo unanime. Vedi anche questo: security.stackexchange.com/a/20369/12614
assylias

3
Sei sicuro che .equals () sulle stringhe non cortocircuita (ovvero: interrompe il loop quando trova due byte che non sono uguali)? In questo caso c'è il rischio di un attacco di temporizzazione che perde informazioni sull'hash della password.
bobpoekert,


7

È possibile utilizzare l' implementazione della libreria Shiro (precedentemente JSecurity ) di ciò che è descritto da OWASP .

Sembra anche che la libreria JASYPT abbia un'utilità simile .


Questo è quello che stavo usando. Ma dal momento che abbiamo deciso di non usare Shiro, c'erano delle preoccupazioni sull'inefficienza di dover includere l'intera libreria di Shiro solo per quel pacchetto.
Chris Dutrow,

Non conosco una libreria composta solo da un'utilità di hashing della password. Probabilmente starai meglio se vuoi creare dipendenze. La risposta di Erickson mi sembra abbastanza buona. O semplicemente copia il codice da quel link OWASP a cui ho fatto riferimento se preferisci utilizzare SHA in modo sicuro.
Laz

7

Puoi calcolare gli hash usando MessageDigest, ma questo è sbagliato in termini di sicurezza. Gli hash non devono essere utilizzati per archiviare le password, in quanto sono facilmente fragili.

Dovresti usare un altro algoritmo come bcrypt, PBKDF2 e scrypt per memorizzare le tue password. Vedi qui .


3
Come si cancellerebbe la password all'accesso senza salvare Salt nel database?
ZZ Coder,

9
L'uso del nome utente come sale non è un difetto fatale, ma non è affatto buono come l'utilizzo di un sale da un RNG crittografico. E non c'è assolutamente alcun problema nell'archiviare il sale nel database. Il sale non è segreto.
Erickson,

1
Il nome utente e l'e-mail non verrebbero memorizzati anche nel database?
Chris Dutrow,

@ZZ Coder, corretto @erickson, in qualche modo ho pensato che sarebbe stato un punto fermo per tutte le password, il che avrebbe portato a una tabella arcobaleno facilmente calcolabile.
Bozho,

13
Un problema con l'utilizzo del nome utente (o di un altro ID come e-mail) come salt è che non è possibile modificare l'ID senza che l'utente abbia impostato anche una nuova password.
Lawrence Dol,

6

Oltre a bcrypt e PBKDF2 menzionati in altre risposte, consiglierei di guardare scrypt

MD5 e SHA-1 non sono raccomandati in quanto sono relativamente veloci, quindi utilizzando il calcolo distribuito "affitto all'ora" (ad es. EC2) o una moderna GPU di fascia alta si possono "crackare" le password usando attacchi di forza bruta / dizionario a costi relativamente bassi e ragionevoli tempo.

Se è necessario utilizzarli, almeno iterare l'algoritmo un numero di volte significativo predefinito (1000+).


6

Concordo pienamente con Erickson che PBKDF2 è la risposta.

Se non hai questa opzione o hai solo bisogno di usare un hash, Apache Commons DigestUtils è molto più facile che ottenere il codice JCE giusto: https://commons.apache.org/proper/commons-codec/apidocs/org/apache /commons/codec/digest/DigestUtils.html

Se usi un hash, vai con sha256 o sha512. Questa pagina contiene buoni consigli sulla gestione delle password e sull'hash (si noti che non raccomanda l'hashing per la gestione delle password): http://www.daemonology.net/blog/2009-06-11-cryptographic-right-answers.html


vale la pena notare che SHA512 non è migliore di SHA256 (a questo scopo) solo perché il numero è più grande.
Azsgy,

5

È possibile utilizzare Spring Security Crypto (ha solo 2 dipendenze di compilazione opzionali ), che supporta la crittografia password PBKDF2 , BCrypt , SCrypt e Argon2 .

Argon2PasswordEncoder argon2PasswordEncoder = new Argon2PasswordEncoder();
String aCryptedPassword = argon2PasswordEncoder.encode("password");
boolean passwordIsValid = argon2PasswordEncoder.matches("password", aCryptedPassword);
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);

4

Mentre la raccomandazione NIST PBKDF2 è già stata menzionata, vorrei sottolineare che c'era una competizione pubblica di hashing delle password che si è svolta dal 2013 al 2015. Alla fine, Argon2 è stato scelto come funzione di hashing della password consigliata.

Esiste un binding Java abbastanza ben adottato per la libreria originale (C nativa) che è possibile utilizzare.

Nel caso d'uso medio, non credo sia importante dal punto di vista della sicurezza se si sceglie PBKDF2 su Argon2 o viceversa. Se hai forti requisiti di sicurezza, ti consiglio di prendere in considerazione Argon2 nella tua valutazione.

Per ulteriori informazioni sulla sicurezza delle funzioni di hashing delle password, vedere security.se .


@zaph Ho modificato la risposta per essere più obiettivo. Si prega di essere consapevoli del fatto che la raccomandazione NIST potrebbe non essere sempre la scelta migliore (vedere qui per un esempio) - ovviamente questo vale per tutto ciò che è raccomandato anche altrove. Pertanto penso che questa risposta offra un valore a questa domanda.
Qw3ry,

2

Qui hai due collegamenti per l'hash MD5 e altri metodi hash:

API Javadoc: https://docs.oracle.com/javase/1.5.0/docs/api/java/security/MessageDigest.html

Tutorial: http://www.twmacinta.com/myjava/fast_md5.php


3
Basta tenere presente che per l'hash della password, è meglio rallentare. Dovresti usare migliaia di iterazioni della funzione hash come tecnica di "rafforzamento chiave". Inoltre, il sale è un imperativo.
Erickson,

Avevo l'impressione che più iterazioni di un algoritmo di hashing di qualità avrebbero prodotto la stessa sicurezza di una iterazione poiché la lunghezza dei byte sarebbe sempre la stessa?
Chris Dutrow,

@erickson Sarebbe meglio rallentare esplicitamente gli attaccanti.
Deamon,

6
Informazioni sul rafforzamento chiave: i sali esistono per rendere inutilizzabili gli hash precalcolati. Ma gli attaccanti non devono pre-calcolare. Gli aggressori possono semplicemente hash string + salt "al volo" fino a quando non trovano quello giusto. Ma se ripeterai migliaia di volte per i tuoi hash, dovranno fare lo stesso. Il tuo server non sarà influenzato molto dalle iterazioni 10k poiché non succede così spesso. Gli aggressori avranno bisogno di 10k volte la potenza di calcolo.
Zockman,

2
@Simon oggi MD5 è considerato inutile per l'hash della password in quanto può essere decifrato in pochi secondi utilizzando gli attacchi di forza bruta / dizionario della GPU. Vedi qui: codahale.com/how-to-safely-store-a-password
Eran Medan

1

Tra tutti gli schemi hash standard, LDAP ssha è il più sicuro da usare,

http://www.openldap.org/faq/data/cache/347.html

Vorrei solo seguire gli algoritmi specificati lì e utilizzare MessageDigest per eseguire l'hash.

È necessario archiviare il sale nel database come suggerito.


1
Poiché SSHA non esegue l'iterazione della funzione hash, è troppo veloce. Ciò consente agli aggressori di provare le password più rapidamente. Algoritmi migliori come Bcrypt, PBBKDF1 e PBKDF2 utilizzano tecniche di "rafforzamento delle chiavi" per rallentare gli aggressori al punto in cui una password dovrebbe scadere prima che possano forzare lo spazio di password di 8 lettere.
Erickson,

Il problema con tutti questi meccanismi è che non si ottiene il supporto client. Il problema con la password con hash è che non puoi supportare l'hash con password con un altro algoritmo. Con ssha, almeno tutti i client LDAP lo supportano.
ZZ Coder,

Non è "il più sicuro", è semplicemente "piuttosto compatibile". bcrypt / scrypt sono molto più intensivi.
Controlla il

1

A partire dal 2020, l'algoritmo più credibile e flessibile in uso,

quello che molto probabilmente ottimizzerà la sua forza dato qualsiasi hardware,

è Argon2id o Argon2i .

Fornisce lo strumento di calibrazione necessario per trovare parametri di resistenza ottimizzati dati un tempo di hashing target e l'hardware utilizzato.

  • Argon2i è specializzato in hashing avidi di memoria
  • Argon2d è specializzato nell'hashing avido di CPU
  • Argon2id usa entrambi i metodi.

L'hashish di memoria avido aiuterebbe contro l'uso della GPU per cracking.

La sicurezza primaverile / L'implementazione di Bouncy Castle non è ottimizzata e relativamente settimana dato ciò che un utente malintenzionato potrebbe utilizzare. cf: documentazione di primavera

L'attuale implementazione utilizza Bouncy castle che non sfrutta il parallelismo / ottimizzazioni che i cracker di password faranno, quindi c'è un'asimmetria non necessaria tra attaccante e difensore.

L'implementazione più credibile in uso per java è quella di mkammerer ,

un vaso / libreria wrapper dell'implementazione nativa ufficiale scritta in Rust.

È ben scritto e semplice da usare.

La versione integrata fornisce build native per Linux, Windows e OSX.

Ad esempio, viene utilizzato da jpmorganchase nel suo progetto di sicurezza a tessera utilizzato per proteggere Quorum , l'implementazione della criptovaluta di Ethereum.

Ecco il codice di esempio da tessera.

La calibrazione può essere eseguita utilizzando de.mkammerer.argon2.Argon2Helper # findIterations

Gli algoritmi SCRYPT e Pbkdf2 potrebbero anche essere calibrati scrivendo alcuni semplici benchmark, ma gli attuali valori di iterazioni minime sicure richiederanno tempi di hashing più elevati.


0

lo ho appreso da un video su udemy e modificato per essere una password casuale più forte

}

private String pass() {
        String passswet="1234567890zxcvbbnmasdfghjklop[iuytrtewq@#$%^&*" ;

        char icon1;
        char[] t=new char[20];

         int rand1=(int)(Math.random()*6)+38;//to make a random within the range of special characters

            icon1=passswet.charAt(rand1);//will produce char with a special character

        int i=0;
        while( i <11) {

             int rand=(int)(Math.random()*passswet.length());
             //notice (int) as the original value of Math>random() is double

             t[i] =passswet.charAt(rand);

             i++;
                t[10]=icon1;
//to replace the specified item with icon1
         }
        return new String(t);
}






}

Sono aperto alla correzione, ma penso che non dovresti usare numeri casuali durante l'hash. Questo è così che la tua funzione hash rimane deterministica; cioè se si hash una stringa più volte si otterrà sempre lo stesso valore di hash per quella stringa.
Duldi
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.