Como posso hash uma senha em Java?


176

Preciso fazer hash de senhas para armazenamento em um banco de dados. Como posso fazer isso em Java?

Eu esperava pegar a senha de texto sem formatação, adicionar um sal aleatório e armazenar o sal e a senha com hash no banco de dados.

Então, quando um usuário queria fazer login, eu podia pegar a senha enviada, adicionar o sal aleatório das informações da conta, fazer hash e ver se isso equivale à senha hash armazenada com as informações da conta.


11
@YGL atualmente não é uma recombinação atualmente, com ataques de GPU tão baratos, a família SHA é realmente uma péssima escolha para hash de senha (rápido demais), mesmo com sal. Use bcrypt, scrypt ou PBKDF2
Eran Medan

11
Por que essa pergunta foi encerrada? Esta é uma pergunta para um problema de engenharia real, e as respostas são inestimáveis. O OP não está pedindo uma biblioteca, ele está perguntando como resolver o problema de engenharia.
stackoverflowuser2010

12
Simplesmente incrível. Esta pergunta tem 52 votos positivos e alguém decide fechá-la como "fora do tópico".
stackoverflowuser2010

1
Sim, eu já publiquei no Meta sobre essa questão de fechamentos antes, mas fui bastante espancado.
Chris Dutrow

8
Esta questão deve ser reaberta. É uma pergunta sobre como escrever um programa para resolver o problema descrito (autenticação de senha), com uma solução de código curto. Ver a palavra-gatilho "biblioteca" não justifica o fechamento reflexivo de uma pergunta; ele não está pedindo uma recomendação da biblioteca, está perguntando como usar hash senhas. Edit: Lá, consertou.
30515

Respostas:


157

Você pode realmente usar um recurso incorporado ao tempo de execução Java para fazer isso. O SunJCE no Java 6 suporta PBKDF2, que é um bom algoritmo para usar no hash de senha.

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

Aqui está uma classe de utilitário que você pode usar para autenticação de senha 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
Você pode ter um pouco de cautela com as conversões de bytes em hexadecimal com BigInteger: zeros à esquerda são removidos. Não há problema em depurar rapidamente, mas vi bugs no código de produção devido a esse efeito.
Thomas Pornin

24
Os destaques de thomas-pornin: por que precisamos de uma biblioteca , não de um bloco de código que está quase lá. Assustador que a resposta aceita não responda à pergunta sobre um tópico tão importante.
Nilzor

9
Use o algoritmo PBKDF2WithHmacSHA512 começando com Java 8. É um pouco mais forte.
Iwan.z 03/11/19

1
Nota, algs existentes não são eliminadas em versões posteriores: java_4: PBEWithMD5AndDES, DESede, DES java_5 / 6/7: PBKDF2WithHmacSHA1, PBE (apenas em 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 Sim, o tempo de execução será mais longo para senhas incorretas ; mais especificamente, senhas erradas levarão o mesmo tempo que as senhas corretas. Isso evita ataques cronometrados, embora eu confesse que não consigo pensar em uma maneira prática de explorar essa vulnerabilidade nesse caso. Mas você não corta os cantos. Só porque eu não posso vê-lo, não significa que uma mente mais desonesta não.
Erickson

97

Aqui está uma implementação completa com dois métodos, fazendo exatamente o que você deseja:

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

O ponto é que, mesmo que um invasor obtenha acesso ao banco de dados e ao código-fonte, as senhas ainda estarão seguras.

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

Estamos armazenando 'salt$iterated_hash(password, salt)'. O salt tem 32 bytes aleatórios e seu objetivo é que, se duas pessoas diferentes escolherem a mesma senha, as senhas armazenadas ainda parecerão diferentes.

O iterated_hashbasicamente hash(hash(hash(... hash(password, salt) ...)))custa muito para um invasor em potencial que tenha acesso ao seu banco de dados adivinhar senhas, fazer hash e procurar hashes no banco de dados. Você precisa calcular isso iterated_hashtoda vez que um usuário faz login, mas não custa muito em comparação com o invasor que gasta quase 100% de seu tempo computando hashes.


14
Desculpe incomodar, mas por que devo escolher isso em uma biblioteca existente? Uma biblioteca provavelmente tem uma maior chance de ser completamente revisada. Duvido que cada um dos 14 votos analisados ​​tenha analisado o código quanto a problemas.
Joachim Sauer #

2
@JoachimSauer Isso é basicamente apenas usando uma biblioteca (javax.crypto), mas você está certo - senhas vazias não são suportadas. Adicionada uma exceção para torná-la explícita. Obrigado!
Martin Konicek

3
Você provavelmente deve alterar as assinaturas de métodos para em char[] passwordvez de String password.
assylias 28/03

2
Embora pareça que a referência não receba acordo unânime. Veja também: security.stackexchange.com/a/20369/12614
assylias

3
Você tem certeza de que .equals () nas strings não causa um curto-circuito (por exemplo: interrompe o loop quando encontra dois bytes que não são iguais)? Se isso ocorrer, existe o risco de um ataque de tempo vazar informações sobre o hash da senha.
precisa saber é o seguinte


7

Você pode usar a implementação da biblioteca Shiro (anteriormente JSecurity ) do que é descrito pelo OWASP .

Também parece que a biblioteca JASYPT tem um utilitário semelhante .


Na verdade, era isso que eu estava usando. Mas desde que decidimos não usar o Shiro, havia alguma preocupação com a ineficiência de ter que incluir toda a biblioteca do Shiro para apenas esse pacote.
Chris Dutrow 18/05/10

Não conheço uma biblioteca composta apenas por um utilitário de hash de senha. Provavelmente, é melhor você rolar sozinho se as dependências forem uma preocupação. A resposta de Erickson parece muito boa para mim. Ou simplesmente copie o código desse link do OWASP que eu referenciei se você preferir usar o SHA de maneira segura.
laz

7

Você pode calcular hashes usando MessageDigest, mas isso está errado em termos de segurança. Hashes não devem ser usados ​​para armazenar senhas, pois são facilmente quebráveis.

Você deve usar outro algoritmo como bcrypt, PBKDF2 e scrypt para armazenar suas senhas. Veja aqui .


3
Como você hash a senha no login sem armazenar sal no banco de dados?
ZZ Coder

9
Usar o nome de usuário como sal não é uma falha fatal, mas não é tão bom quanto usar sal de um RNG criptográfico. E não há absolutamente nenhum problema em armazenar o sal no banco de dados. O sal não é secreto.
Erickson

1
O nome de usuário e o email também não seriam armazenados no banco de dados?
Chris Dutrow

@ZZ Coder, @erickson correto, de alguma forma presumi que será um sal para todas as senhas, o que levaria a uma tabela rainbow facilmente computável.
Bozho 18/05/10

13
Um problema ao usar o nome de usuário (ou outro ID como email) como um problema é que você não pode alterar o ID sem que o usuário também defina uma nova senha.
Lawrence Dol

6

Além de bcrypt e PBKDF2 mencionados em outras respostas, eu recomendaria examinar scrypt

MD5 e SHA-1 não são recomendados, pois são relativamente rápidos, portanto, usando a computação distribuída "aluguel por hora" (por exemplo, EC2) ou uma GPU de ponta moderna, pode-se "quebrar" senhas usando ataques de força bruta / dicionário a custos relativamente baixos e razoáveis Tempo.

Se você precisar usá-los, pelo menos itere o algoritmo uma quantidade significativa predefinida de vezes (mais de 1000).


6

Concordo plenamente com Erickson que PBKDF2 é a resposta.

Se você não tiver essa opção ou apenas precisar usar um hash, o Apache Commons DigestUtils é muito mais fácil do que acertar o código JCE: https://commons.apache.org/proper/commons-codec/apidocs/org/apache /commons/codec/digest/DigestUtils.html

Se você usar um hash, use sha256 ou sha512. Esta página tem boas recomendações sobre manipulação e hash de senhas (observe que não recomenda hash para manipulação de senhas): http://www.daemonology.net/blog/2009-06-11-cryptographic-right-answers.html


vale a pena notar que o SHA512 não é melhor que o SHA256 (para essa finalidade) apenas porque o número é maior.
Azsgy 27/03/19

5

Você pode usar o Spring Security Crypto (possui apenas 2 dependências de compilação opcionais ), que suporta criptografia de senha 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

Embora a recomendação NIST PBKDF2 já tenha sido mencionada, gostaria de salientar que houve uma competição de hash de senha pública que foi realizada de 2013 a 2015. No final, o Argon2 foi escolhido como a função de hash de senha recomendada.

Há uma ligação Java bastante bem adotada para a biblioteca original (C nativa) que você pode usar.

No caso de uso médio, acho que não importa da perspectiva de segurança se você escolher PBKDF2 em vez de Argônio2 ou vice-versa. Se você possui fortes requisitos de segurança, recomendo considerar o Argônio2 em sua avaliação.

Para obter mais informações sobre a segurança das funções de hash de senha, consulte security.se .


@ zaph Eu editei a resposta para ser mais objetiva. Esteja ciente de que a recomendação do NIST nem sempre pode ser a melhor escolha (veja aqui um exemplo) - é claro que isso vale para qualquer coisa que seja recomendada em outro lugar também. Portanto, acho que essa resposta fornece um valor para essa pergunta.
Qw3ry

2

Aqui você tem dois links para o hash MD5 e outros métodos de 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
Lembre-se de que, para hash de senha, mais lento é melhor. Você deve usar milhares de iterações da função hash como uma técnica de "fortalecimento de chave". Além disso, o sal é imperativo.
Erickson

Fiquei com a impressão de que várias iterações de um algoritmo de hash de qualidade produziriam aproximadamente a mesma segurança que uma iteração, pois o comprimento dos bytes ainda seria o mesmo?
Chris Dutrow

@erickson Seria melhor desacelerar os invasores explicitamente.
deamon

6
Sobre o fortalecimento de chaves: Existem sais para tornar inutilizáveis ​​os hashes pré-computados. Mas os atacantes não precisam precomputar. Os atacantes podem apenas usar hash + sal "on the fly" até encontrar o caminho certo. Mas se você iterar milhares de vezes para seus hashes, eles terão que fazer o mesmo. Seu servidor não será afetado muito por 10 mil iterações, pois isso não acontece com muita frequência. Os invasores precisarão de 10k vezes o poder de computação.
Zockman 19/05/10

2
Hoje, o @Simon MD5 é considerado inútil para o hash de senha, pois pode ser quebrado em segundos usando ataques de força bruta / dicionário da GPU. Veja aqui: codahale.com/how-to-safely-store-a-password
Eran Medan

1

Entre todos os esquemas de hash padrão, o LDAP ssha é o mais seguro de usar,

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

Gostaria apenas de seguir os algoritmos especificados lá e usar o MessageDigest para fazer o hash.

Você precisa armazenar o sal no seu banco de dados, conforme sugerido.


1
Como o SSHA não itera a função hash, é muito rápido. Isso permite que os invasores tentem senhas mais rapidamente. Algoritmos melhores como Bcrypt, PBBKDF1 e PBKDF2 usam técnicas de "fortalecimento de chave" para retardar os invasores até o ponto em que uma senha deve expirar antes que eles possam fazer força bruta, mesmo com um espaço de senha de 8 letras.
Erickson

O problema com todos esses mecanismos é que você não recebe suporte ao cliente. O problema com a senha com hash é que você não pode suportar hash de senha com outros algoritmos. Com o ssha, pelo menos todos os clientes LDAP o suportam.
ZZ Coder

Não é "mais seguro", é apenas "bastante compatível". bcrypt / scrypt são muito mais intensivos em recursos.
Eckes

1

A partir de 2020, o algoritmo mais confiável e flexível em uso,

o mais provável de otimizar sua força, considerando qualquer hardware,

é Argon2id ou Argon2i .

Ele fornece a ferramenta de calibração necessária para encontrar parâmetros de força otimizados, considerando um tempo de hash desejado e o hardware usado.

  • Argon2i é especializado em hashing ganancioso de memória
  • Argon2d é especializado em hashing ganancioso de CPU
  • Argon2id usa os dois métodos.

O hashing ganancioso de memória ajudaria contra o uso da GPU para quebrar.

A segurança da primavera / a implementação do castelo insuflável não é otimizada e relativamente uma semana, considerando o que o invasor poderia usar. cf: documentação do Spring

A implementação atualmente usa o castelo Bouncy, que não explora paralelismo / otimizações que os crackers de senha, portanto, há uma assimetria desnecessária entre atacante e defensor.

A implementação mais credível em uso para java é a do mkammerer ,

uma jarra / biblioteca de wrapper da implementação nativa oficial escrita em Rust.

É bem escrito e simples de usar.

A versão incorporada fornece versões nativas para Linux, Windows e OSX.

Como exemplo, ele é usado pelo jpmorganchase em seu projeto de segurança de tessera usado para proteger o Quorum , sua implementação de criptomoeda Ethereum.

Aqui está o código de exemplo do tessera.

A calibração pode ser realizada usando de.mkammerer.argon2.Argon2Helper # findIterations

Os algoritmos SCRYPT e Pbkdf2 também podem ser calibrados, escrevendo alguns benchmarks simples, mas os valores mínimos atuais das iterações seguras exigirão tempos de hash mais altos.


0

Inclinei isso de um vídeo no udemy e editei para ser uma senha aleatória mais 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);
}






}

Estou aberto a ser corrigido, mas acho que você não deve usar números aleatórios ao fazer o hash. Isso é para que sua função hash permaneça determinística; isto é, se você hash uma string várias vezes, sempre receberá o mesmo valor de hash para essa string.
duldi 6/06
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.