Blog da Caelum Blog da Caelum
Search
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.

arrow17 Responses

  1. 23 mos, 1 wk ago

    Ó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. 23 mos, 1 wk ago

    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. 23 mos, 1 wk ago

    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
    23 mos, 1 wk ago

    Ó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
    23 mos, 1 wk ago

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

    Obrigado!

  6. Eric Torti
    23 mos, 1 wk ago

    Valeu, Nakamura.

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

    Valeu.

  7. Luca Bastos
    23 mos, 1 wk ago

    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. 23 mos, 1 wk ago

    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 0×00 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. 23 mos, 1 wk ago

    Ó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. 23 mos, 1 wk ago

    @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. 23 mos, 1 wk ago

    @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. 23 mos, 1 wk ago

    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. 23 mos, 1 wk ago

    >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. 23 mos, 1 wk ago

    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. Cara!! O post está muito legal, com ótimas contribuiões, e também é bem oportuno. Não o tinha lido. Só li hoje :D

    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. 22 mos, 2 wks ago

    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
    22 mos ago

    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

Leave A Comment