Guardando senhas criptografadas em Java

Eu e o Thiago Ferreira estavámos mais uma vez na Caelum passando pela situação de gravar as senhas do usuário no banco de dados. Ainda hoje alguns grandes sites cometem o grave erro de guardar as senhas dos usuários em texto puro, fazendo com que um possível roubo de dados acarrete num problema ainda maior.

O processo clássico é guardar um hash (chamado também de digest nesse caso) da senha do usuário, usando algum algoritmo de hash unidirecional. Isso pode ser feito utilizando uma chamada de função na query do banco de dados (como MD5()no MySQL), ou, o mais utilizado para não ter de trafegar a senha entre o servidor web e o banco de dados: com o MessageDigest do javax.security. Através dessa classe você pode facilmente gerar o hash de uma senha:

MessageDigest algorithm = MessageDigest.getInstance("MD5");
byte messageDigest[] = algorithm.digest("senha".getBytes("UTF-8"));

Agora temos um array de bytes que podemos guardar no banco de dados. Quando o usuário logar, basta digerirmos novamente a senha colocada no formulário web, e comparar o hash resultante com o que há no banco. Você poderia guardar esse array de byte como uma String fazendo new String(bytes, encoding), porém muito mais usual é guardar os hexadecimais desses bytes dentro de uma String:

StringBuilder hexString = new StringBuilder();
for (byte b : messageDigest) {
	hexString.append(String.format("%02X", 0xFF & b));
}
String senha = hexString.toString();

Isso gerará e8d95a51f3af4a3b134bf6bb68a213a.

Apesar de muito usado, o MD5 já é considerado um algoritmo de hash quebrado, pois hoje em dia podemos rapidamente, através de força bruta, descobrir uma String que gere o mesmo hash, já que neste algoritmo ocorrem mais colisões do que o que foi inicialmente imaginado por seus criadores. Muitos passaram a usar o SHA-1, porém este já da sinais de que, num futuro próximo, será quebrado com força bruta. O próprio governo americano já evita utilizar a família SHA-1, priorizando o uso do SHA-2. Logo, o procedimento completo mais adequado atualmente seria usar algo como:

			MessageDigest algorithm = MessageDigest.getInstance("SHA-256");
			byte messageDigest[] = algorithm.digest(original.getBytes("UTF-8"));

			StringBuilder hexString = new StringBuilder();
			for (byte b : messageDigest) {
				hexString.append(String.format("%02X", 0xFF & b));
			}
			String senha = hexString.toString();

Para tornar seu sistema ainda mais seguro em relação as senhas guardadas, pode-se ainda adicionar salts, mais iterações de hash e outras técnicas que tornaria ainda mais difícil um possível ataque de força bruta contra uma base de dados roubada, se protegendo também contra o provável uso de senhas fracas por parte de seus usuários.

40 Comentários

  1. Lucas Uyezu 17/06/2010 at 19:50 #

    Ótimo post!

    Existe também o bcrypt, um algoritmo de criptografia que vem sendo utilizado recentemente para guardar senhas, acho interessante dar uma lida sobre ele.

  2. Paulo Silveira 17/06/2010 at 20:04 #

    Oi Lucas. Perfeito, e voce pode usar o blowfish no Java através do Cipher:
    http://java.sun.com/javase/6/docs/api/javax/crypto/Cipher.html

    Mas aí é o caso que queremos tambem poder descriptografar. No post a idéia é que isso seja inviavel, garantindo assim a segurança das senhas mesmo numa hipotética perda da base de dados.

  3. Guilherme Silveira 17/06/2010 at 20:30 #

    Lembro a primeira vez que recebi um email com minha senha aberta e logo depois guardava criptografada no banco. Meu medo era os servidores de smtp não estarem usando criptografia no envio do email: lá estaria minha senha navegando seca pela net novamente.

    A criptografia da senha é fundamental, e ainda tenho medo dos servidores de email mal configurados.

    Direto ao ponto… parabens

  4. Otávio Garcia 17/06/2010 at 21:14 #

    Ótimo post. Um assunto que infelizmente muitos programadores desconhecem.

    Há um aplicativo muito bom para ser usado no Java, o Jasypt, que inclusive possui integração transparente com o Hibernate e suporte ao bounce castle.

    Em um sistema que caiu aqui na empresa as senhas eram “criptografadas” usando String.hashCode, e o pior, o nome do método era Utils.md5Cript. Hahahaha.

  5. André Silva 17/06/2010 at 21:39 #

    Eu ia precisar de um algoritmo pra gerar senha para um sistema amanhã, você me economizou pesquisa e trabalho!!

    Obrigado!

  6. Eric Torti 17/06/2010 at 21:58 #

    Valeu, Nakamura.

    Assunto clássico muito bem descrito e analisado. Vai ser útil pra mim e pra muitos outros, certamente.

    Valeu.

  7. Luca Bastos 18/06/2010 at 00:42 #

    Muito bom, todo mundo deve usar algo parecido. Guardar senha aberta de cliente na base de dados é quase crime.

    Mas se for de usuário do sistema e caso o sistema seja do tipo que precisa autorizar o uso em cada tela, aí acho que alguns já sabem minha opinião: a senha criptografada não deve ir para o BD tradicional porque necessita consulta ao BD em cada tela navegada. É um caso típico em que um NoSQL mostra seu valor.

  8. Sami Koivu 18/06/2010 at 00:43 #

    Legal o post.

    Três coisas:

    1) Utilizar salts realmente é legal contra ataques tipo “rainbow tables”.

    2) A conversão para hex no exemplo está com um pequeno problema. Os bytes com valor entre 0x00 e 0x0f) acabam gerando somente um digito. Isso se torna problematico, por exemplo, no caso da sequencia 0x0f e 0xff, pois esses dois bytes geram o mesmo string que a sequencia 0xff, 0x0f (ambras produzem “fff”). Isso acaba enfraquecendo o hash um pouco. Não deve ser muito significativo, mas é desnecessário.

    3) Como isso foi brevemente apresentado como opção, talvez vale salientar que guardar o hash diretamente como String é problematico. Utilizar a apresentação em hexa, ou usar Base64 (ocupa um pouquinho menos espaço) é melhor.

    Vou dar um exemplo prático de um projeto real que eu participei que demonstra o problema de utilizar uma simples conversão para String: O hash foi guardado num String, só que acontece que a codificação padrão em ambiente de produção foi US-ASCII. US-ASCII guarda todos os bytes cujo valor é maior do que 127 como o carater de interrogação (?). Isso quer dizer, que por média, metade dos 16 bytes do hash ficavam interrogações. Isso resultava em MUITAS colisões. Por exemplo, a senha razoavelmente boa:

    dndj436!6W2qdkSfkv#

    Produz o mesmo resultado que todos os seguintes:

    collages
    pirating
    18792
    81235
    148178
    261119
    298901
    335309
    490029
    496074
    539779
    564881
    632427
    686474
    691613
    768822
    790968
    839477
    851547
    880129
    992981

  9. Luiz C. F. dos Santos 18/06/2010 at 10:18 #

    Ótimo post!

    Mas tenho duas perguntas:

    1- O que acontece nessa linha hexString.append(Integer.toHexString(0xFF & b)); ?

    2- Só por curiosidade, se eu fosse descriptografar, como seria o algorítimo?

  10. Paulo Silveira 18/06/2010 at 11:42 #

    @Sami Que honra receber seu comentario!

    Impressionante como a colisão aumentou drasticamente no seu exemplo. Por isso fazemos questao de puxar a string em UTF-8 (e tambem porque se nao especificamos, ele usa o default da plataforma, que pode mudar, e ai o digest nao bateria!)

    Sami, o & é apenas para evitar os bytes negativos. -1 em hexa geraria “ffffffff” e ai cada byte poderia ter mais de dois caracteres na sua representação, deixando a String gigante e de tamanho variavel. Como o byte vai de -128 a 127, nao consegui entender quais dois valores que, quando feito o “& 0xFF”, gerem o mesmo novo byte.

    @Luiz Santos: a mascara de bits é para evitar numeros negativos. Sobre descriptografar, o ponto é justo esse: nao da! Dessa forma as senhas estao teoricamente seguras.

  11. Sami Koivu 18/06/2010 at 13:52 #

    @Paulo: Opa, eu continuo grande fã da Caelum.

    É verdade que UTF-8 resolve.

    Quanto a terceira questão, acho que não consegui me explicar com muita claridade. Um exemplo de código com dois hash diferentes (digest1 e digest2) que produzem o mesmo string (o problema é a omissão dos zeros):

    byte[] digest1 = {1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17};
    byte[] digest2 = {17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1};
    
    StringBuilder hexString1 = new StringBuilder();
    for (byte b : digest1) {
      hexString1.append(Integer.toHexString(0xFF & b));
    }
    String senha1 = hexString1.toString();
    System.out.println(senha1);
    
    StringBuilder hexString2 = new StringBuilder();
    for (byte b : digest2) {
      hexString2.append(Integer.toHexString(0xFF & b));
    }
    String senha2 = hexString2.toString();
    System.out.println(senha2);
    
  12. Paulo Silveira 18/06/2010 at 14:18 #

    Impressionante sua percepção Sami! Agora entendi, um gera 1+11 e o outro gera 11+1, colidindo! O problema vai ser raro, mas realmente é a omissao dos zeros. Para melhorarmos entao, basta fazer o padding com zeros:

    hexString.append(String.format("%02X", 0xFF & element));
    
  13. Sami Koivu 18/06/2010 at 15:28 #

    >Impressionante sua percepção Sami!
    É um vicio 🙂

    > O problema vai ser raro
    Concordo.

    > Para melhorarmos entao, basta fazer o padding com zeros
    Isso mesmo. Vai deixar o exemplo mais perfeitinho.

    Abraço

  14. Paulo Silveira 18/06/2010 at 16:27 #

    Post atualizado com a dica de melhoria do Sami! Agora tambem nao gera mais Strings de tamanhos diferentes (se o byte era menor que 16, gerava apenas um caractere hexadecimal, agora ele prefixa com 0).

  15. Marco Antonio Maciel 25/06/2010 at 17:35 #

    Cara!! O post está muito legal, com ótimas contribuiões, e também é bem oportuno. Não o tinha lido. Só li hoje 😀

    Mas ontem fiz uma apresentação sobre Certificado Digital, e demonstrei o uso de hash com um programinha simples usando SHA-512.

    Se alguém se interessar pela apresentação ou no fonte do programa aí está o link do blog. 😉 Ah! o programa eu fiz usando Netbeans 6.8.

    http://www.mmaciel.com.br/2010/06/25/seguranca-e-certificado-digital/

  16. Paulo Silveira 16/07/2010 at 16:18 #

    Pra quem preferir usar uma biblioteca pronta e nao se incomodar de um jar a mais, o Apache Commons Codec faz isso:
    http://commons.apache.org/codec/

    Ai a classe DigestUtils possui metodos estaticos como sha512hex(String), ja devolvendo o resultado encodado.

  17. Felipe Regalgo 26/07/2010 at 22:34 #

    Como vcs fazem para acessar o banco de dados sem que a senha esteja exposta e desprotegida?

    Deixam a senha do banco em um arquivo properties sem criptografia? Afinal de contas se criptografar a senha pra deixar no properties nao tem como descriptografar depois e acessar.

    Como faço para acessar o banco deixando a senha protegida?

    Valeu! E parebéns pelo post

  18. Regis 24/10/2012 at 17:11 #

    @Felipe Regalgo:
    A ideia é não deixar lugar algum sem criptografia. Para acessar o banco, é só criptografar a senha fornecida e comparar com o que está no banco.

    A ideia por trás de um algoritmo de criptografia é que, dada uma senha criptografada, seja muito difícil encontrar uma outra string que não a senha, que produza o mesmo resultado da senha criptografada, ou seja, parte-se do pressuposto (nem sempre correto) de que, se uma string foi criptografada e bateu com o que está no banco, é por que a string é a própria senha.

  19. Alex 30/07/2013 at 22:22 #

    Certo, usei aquele ultimo codigo do post… até ai beleza. Mas e o código para descriptografar? Ou eu testo se o hash dele é igual? #PS..Novato ehuwehuwhew

  20. Alex 30/07/2013 at 22:52 #

    Opa consegui ja kkkkkk. Comparei mesmon e perfeito. Otimo post!!!!

  21. Paulo Neves (PH) 29/10/2013 at 21:58 #

    Muito Bom. Parabéns.

    Utilizei o exemplo da Apache.

    Obrigado.

  22. Luciano Bezerra 29/12/2013 at 21:06 #

    Embora meio antigo o Post, continua excelente, parabéns. Infelizmente o hash gerado pode ser facilmente descoberto no google, mas, para sistemas simples, caseiros, é perfeito!

    Me economizou um tempão!
    Abraços!

  23. Paulo Silveira 30/12/2013 at 01:23 #

    oi Luciano!

    Como assim pode facilmente ser descoberto no google? Um SHA1 ja pode ser quebrado asssim? Creio que nao.

  24. Levi's 23/04/2014 at 14:38 #

    byte messageDigest[] = algorithm.digest(original.getBytes(“UTF-8”));

    o que se refere o original :

  25. Paulo Silveira 23/04/2014 at 14:41 #

    original é uma string que voce quer gerar o hash.

  26. Kevin Oliveira 12/08/2014 at 14:16 #

    Primeiramente, parabéns pelo post, aprendi muito coisa com ele.

    Segundo, gostaria de saber se já existe tecnologia de criptografia mais avançada que a SHA-512.

  27. daianne 25/09/2015 at 12:41 #

    import java.io.DataInputStream;
    import java.io.DataOutputStream;
    import java.net.ServerSocket;
    import java.net.Socket;

    import javax.crypto.Cipher;
    import javax.crypto.KeyGenerator;
    import javax.crypto.SecretKey;

    public class TCPServidor {

    public static void main(String[] args) {
    ServerSocket servidor;
    int porta = 5678;
    try {
    System.out.println(“Servidor iniciado na porta ” + porta + “…\n”);
    servidor = new ServerSocket(porta);
    Socket conexao = servidor.accept();

    DataInputStream in = new DataInputStream(conexao.getInputStream());
    String mensagem = in.readUTF();

    System.out.println(“Usuario e senha recebidos do cliente” + mensagem);

    KeyGenerator kgen = KeyGenerator.getInstance(“Blowfish”);
    SecretKey chave = kgen.generateKey();

    Cipher cipher = Cipher.getInstance(“Blowfish”);
    cipher.init(Cipher.DECRYPT_MODE, chave);

    byte descriptografada[] = cipher.doFinal(mensagem.getBytes());
    System.out.println(“Mensagem descriptografada:” + descriptografada);

    DataOutputStream out = new DataOutputStream(conexao.getOutputStream());
    out.writeUTF(“Recebi os dados”);

    conexao.getLocalAddress();

    } catch (Exception exc) {
    exc.printStackTrace();
    System.err.println(exc.toString());

    }
    }

    }

  28. Programação Simples 28/11/2015 at 00:34 #

    Gostei do artigo e dos comentários da galera, me ajudaram muito! Obrigado…

  29. Pedro Filho 14/02/2016 at 22:42 #

    HMAC é a solução…

    https://pt.wikipedia.org/wiki/HMAC

  30. Alessandra 18/03/2016 at 22:19 #

    Saudações. Desculpe senhores se estiver postando em local errado. Vou deixar aqui o conteúdo de uma classe para criptografar senhas… Como não sou programadora e o que conheço de programação é com pascal…

    Gostaria de saber se é possível transformar esta classe numa versão para pascal sem usar o j2op?

    Escrevendo para pascal mesmo.

    Segue o conteúdo da classe:

    package services;

    import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
    import android.util.Base64;
    import java.io.UnsupportedEncodingException;
    import java.math.BigInteger;
    import java.security.MessageDigest;
    import java.security.NoSuchAlgorithmException;

    public class CriptografiaService {
    public String calcularMD5SenhaDigitada(String senha) {
    try {
    MessageDigest md5 = MessageDigest.getInstance(“MD5”);
    md5.reset();
    md5.update(senha.getBytes());
    String bigInteger = new BigInteger(1, md5.digest()).toString(16);
    while (bigInteger.length() < 32) {
    bigInteger = "0" + bigInteger;
    }
    return bigInteger;
    } catch (NoSuchAlgorithmException e) {
    e.printStackTrace();
    return null;
    }
    }

    public String calcular(String informacao, byte[] saltoBytes) {
    try {
    int i;
    byte[] informacaoBytes = informacao.getBytes("US-ASCII");
    byte[] informacaoComSaltoBytes = new byte[(informacaoBytes.length + saltoBytes.length)];
    for (i = 0; i < informacaoBytes.length; i++) {
    informacaoComSaltoBytes[i] = informacaoBytes[i];
    }
    for (i = 0; i < saltoBytes.length; i++) {
    informacaoComSaltoBytes[informacaoBytes.length + i] = saltoBytes[i];
    }
    byte[] hashBytes = computeHash(informacaoComSaltoBytes);
    byte[] hashComSaltoBytes = new byte[(hashBytes.length + saltoBytes.length)];
    for (i = 0; i < hashBytes.length; i++) {
    hashComSaltoBytes[i] = hashBytes[i];
    }
    for (i = 0; i < saltoBytes.length; i++) {
    hashComSaltoBytes[hashBytes.length + i] = saltoBytes[i];
    }
    return Base64.encodeToString(hashComSaltoBytes, 0).trim();
    } catch (Exception e) {
    return null;
    }
    }

    public byte[] computeHash(byte[] informacaoComSaltoBytes) throws NoSuchAlgorithmException, UnsupportedEncodingException {
    MessageDigest digest = MessageDigest.getInstance("SHA-256");
    digest.reset();
    return digest.digest(informacaoComSaltoBytes);
    }

    public Boolean testar(String informacao, String valorHash) {
    byte[] hashComSaltoBytes = Base64.decode(valorHash, 0);
    if (hashComSaltoBytes.length < AccessibilityNodeInfoCompat.ACTION_NEXT_AT_MOVEMENT_GRANULARITY / 8) {
    return Boolean.valueOf(false);
    }
    byte[] saltoBytes = new byte[(hashComSaltoBytes.length – 32)];
    for (int i = 0; i < saltoBytes.length; i++) {
    saltoBytes[i] = hashComSaltoBytes[i + 32];
    }
    return Boolean.valueOf(valorHash.equals(calcular(informacao, saltoBytes)));
    }
    }

    Desde já agradeço…. Aos que puderem me ajudar…

  31. Ronald 17/08/2016 at 14:21 #

    oi.
    estou tambem iniciando nesse mundo bacana da programação.
    seria possivel mostrar como seriam as classes e metodos pra usar esse codigo que calcula o hash das senhas?

  32. Anderson Leony 28/11/2016 at 10:35 #

    Como faço para comparar a senha digitada com a senha Hash que está no banco de dados?

  33. Paulo Silveira 30/11/2016 at 11:43 #

    exatamente com esse codigo do post. o que nao funcionou anderson?

  34. William 06/12/2016 at 12:08 #

    Olá Paulo, gostaria de uma dica sua. Vejo algumas aplicações serem configuradas com usuário e senha dentro de arquivos textos dentro do servidor. A ideia de segurança é dar segurança ao arquivo esquecendo de criptografia. Há uma forma melhor de tratar este assunto? Por exemplo, tenho uma aplicação que acessa um servidor linux. Para este acesso tenho um usuário e senha que atualmente guardo dentro do código da aplicação. Teria uma forma melhor de guardar esta informação? De forma segura? Tirando do código da app?

  35. Paulo Silveira 07/12/2016 at 09:48 #

    william, é uma boa questao

    alguns casos voce consegue sim tirar muita coisa de arquivos com login/senha, mas vai depender dos servicos que voce ta consumindo. mas algumas coisas voce vai precisar sim ter o login/senha em arquivos portperties/xml. o ideal é que o diretorio onde ficam essas configuracoes sejam so acessados por um pequeno grupo. ou melhor ainda: sejam acessados de maneira automatizada por quem tem direito de rodar o script de deploy em producao, que atualizara esses arquivos

  36. Joe 27/11/2017 at 03:53 #

    Paulo Silveira, Boa noite!

    por favor, me ajude com duas questões:

    1) Ao usar o algorítimo acima com “MessageDigest.getInstance(“SHA-512″)” para fazer o hash para String “teste” retorna:
    “b123e9e19d217169b981a61188920f9d28638709a5132201684d792b9264271b7f09157ed4321b1c097f7a4abecfc0977d40a7ee599c845883bd1074ca23c4af”

    Ao usar utiliza o Apache Codec “DigestUtils.sha512hex(“teste”)” para fazer o hash para String “teste” retorna:
    “B123E9E19D217169B981A61188920F9D28638709A5132201684D792B9264271B7F09157ED4321B1C097F7A4ABECFC0977D40A7EE599C845883BD1074CA23C4AF”

    A diferença que um retorna maiúsculo e outro minusculo. Isso é problema?

    2) Hoje ainda o “SHA-512” é o melhor caminho para armazenar senhas ou seria melhor implementar o tal do “bcrypt”?

  37. Paulo Silveira 27/11/2017 at 09:32 #

    oi Joe. Nao é problema no geral, mas voce precisa ficar atento se vai misturar duas fontes de algoritmos. nesse caso pra fazer a comparacao com ignore case.

    Sobre a pergunta 2, nao sei te responder. Sou apenas usuário de criptografia básica. mas eu chutaria que voce está muito bem com sha512.

  38. Joe 27/11/2017 at 16:29 #

    Show! Obrigado!

  39. Cesar 16/05/2018 at 18:38 #

    Paulo Silveira, Boa noite!
    Por favor gostaria de saber se é possível encriptar e decriptar um ficheiro .txt com mais ou menos 5 páginas de informações, usando este programa, como implementaria o código:
    MessageDigest algorithm = MessageDigest.getInstance(“SHA-256”);
    byte messageDigest[] = algorithm.digest(original.getBytes(“UTF-8”));

    StringBuilder hexString = new StringBuilder();
    for (byte b : messageDigest) {
    hexString.append(String.format(“%02X”, 0xFF & b));
    }
    String senha = hexString.toString();

    Obrigado…

Deixe uma resposta