Skip to main content

Como fazer hash de senhas em aplicações Java do jeito certo!

Escrito por
Java engineering feature

24 de março de 2022

0 minutos de leitura

Há várias maneiras de armazenar senhas confidenciais. Ter opções é ótimo, mas, quando se trata de armazenar senhas, escolher a opção errada pode se transformar em um pesadelo de segurança. Pensando nisso, vamos analisar algumas das suas opções. Neste artigo, vamos explicar como fazer hash de senhas nas suas aplicações Java. Embora você possa aplicar esses princípios a qualquer ecossistema, vamos mostrar especificamente a melhor forma de lidar com hash de senhas em Java.

A primeira coisa a considerar ao decidir como armazenar senhas confidenciais é se você realmente quer cuidar da autenticação e da autorização por conta própria. Há muitos serviços disponíveis que podem fazer isso por você. Usar um serviço de autenticação e autorização elimina a responsabilidade de armazenar senhas com segurança na sua aplicação. Há vários serviços comerciais, como o Auth0 e o Okta, que simplificam esse processo. Desde que o provedor escolhido ofereça suporte a padrões abertos, como OpenID Connect, você poderá integrá-lo facilmente às suas aplicações Java (como o Spring Boot, por exemplo). Além disso, o provedor talvez ofereça suporte à MFA (autenticação multifator), reforçando ainda mais a segurança dos usuários finais.

Se você precisar armazenar senhas por conta própria na sua aplicação Java, este artigo é para você.

Como fazer hash de senhas para armazená-las

Se você armazena nomes de usuário e senhas na sua aplicação, faça o que fizer, não armazene senhas em texto simples (eu sei, eu sei, parece óbvio!). Se o seu sistema for invadido, um invasor poderá simplesmente ler essas senhas e se passar pelos usuários.

Para evitar o temido armazenamento de senhas em texto simples, armazene uma versão com hash das senhas dos usuários. Para isso, você precisará de um algoritmo de hash de senhas.

O que é um algoritmo de hash de senhas?

Existem diferentes tipos de algoritmos de hash, mas os algoritmos de hash de senhas são funções unidirecionais projetadas especificamente para trabalhar com senhas. Eles transformam uma senha em texto simples em uma sequência de dados que pode ser armazenada com segurança em um banco de dados e nunca pode ser revertida. (Você consegue transformar uma senha em texto simples em um hash, mas é impossível transformar o hash em uma senha — daí o nome “função unidirecional”.)

Como funciona o hash de senhas?

Os algoritmos de hash de senhas são usados durante a autenticação do usuário. Sempre que um usuário envia a senha (em texto simples), ela é transformada em hash imediatamente. Em seguida, o hash é armazenado (no cadastro) ou comparado ao hash salvo do usuário para verificar se a senha está correta (na autenticação).

Existem muitos algoritmos de hash, mas poucos são boas opções para senhas. Vários tipos de funções de hash têm usos específicos, como verificar a integridade de um arquivo. Esses algoritmos de hash não são adequados para senhas.

O que você precisa é de um algoritmo robusto de hash de senhas para as senhas dos seus usuários. Um bom algoritmo de hash de senhas consome uma quantidade considerável de poder computacional e memória. Quando se trata desses algoritmos, quanto mais recursos computacionais eles consomem, melhor!

Algoritmos robustos de hash de senhas são lentos, o que reduz a eficácia de ataques de força bruta. Há muito mais a dizer sobre o assunto, mas, em resumo: para proteger senhas, use um algoritmo robusto e que consuma muitos recursos, oferecendo aos usuários o máximo de segurança possível.

Qual é o melhor algoritmo de hash de senhas?

A primeira regra dos algoritmos de hash de senhas é: não crie seu próprio algoritmo de hash de senhas! A exceção é se você for um criptógrafo experiente. Caso contrário, confie nos padrões definidos pelo setor e escolha algoritmos e bibliotecas testados e validados para usar na sua aplicação.

Segundo a fundação OWASP, apenas alguns algoritmos devem ser considerados para fazer hash de senhas atualmente. Vamos conhecer os algoritmos recomendados!

Argon2id

Atualmente (em 2022), o melhor algoritmo é o Argon2id. O Argon2 é uma função de derivação de chaves que venceu a competição de hash de senhas. Ele tem três versões:

  • Argon2d: maximiza a resistência a ataques de quebra com GPU

  • Argon2i: otimizado para resistir a ataques de canal lateral

  • Argon2id: versão híbrida

A versão Argon2id é recomendada para senhas por oferecer um equilíbrio contra ataques de canal lateral e ataques com GPU. Você pode configurar o algoritmo Argon2id com três parâmetros: tamanho da memória (m), número de iterações (t) e grau de paralelismo (p).

Atualmente, recomenda-se usar o Argon2id com pelo menos 15 MiB de memória, 2 iterações e grau de paralelismo 1. O algoritmo já inclui salt automático para cada usuário, impedindo ataques com hashes pré-computados. Portanto, não é preciso fazer nada além disso.

Usando Spring Security Crypto com Bouncy Castle

A maneira mais fácil de implementar o Arong2id na sua aplicação Java é usar a biblioteca spring-security-crypto. Embora ela faça parte do framework Spring, você não precisa usar o restante do framework.

A biblioteca spring-security-crypto oferece um Argon2PasswordEncoder que você pode usar. Na minha opinião, o nome não é muito preciso, pois tecnicamente não se trata de um codificador, mas de uma função de hash. A biblioteca Spring usa bouncycastle como dependência, que contém uma implementação completa do algoritmo Argon2 em Java. A biblioteca spring-security-crypto pode ser vista como uma interface intuitiva sobre o bouncycastle, que facilita o uso. Além do bouncycastle, você também precisa da dependência common-logging para, como você já deve imaginar, fazer a geração de logs.

<dependency>
   <groupId>org.springframework.security</groupId>
   <artifactId>spring-security-crypto</artifactId>
   <version>5.6.2</version>
</dependency>
<dependency>
   <groupId>commons-logging</groupId>
   <artifactId>commons-logging</artifactId>
   <version>1.2</version>
</dependency>
<dependency>
   <groupId>org.bouncycastle</groupId>
   <artifactId>bcpkix-jdk15on</artifactId>
   <version>1.70</version>
</dependency>

Por padrão, o Argon2PasswordEncoder usa a versão Argon2id com m=4MiB, t=3 e p=1. Embora o número de iterações seja maior que o mínimo, ainda vale considerar um consumo maior de memória. No exemplo Java abaixo, uso os requisitos mínimos: m=15Mib, t=2 e p=1. Também mantenho os valores padrão para o salt e o tamanho do hash.

Argon2PasswordEncoder encoder = new Argon2PasswordEncoder(32,64,1,15*1024,2);
var myPassword = "ThisIsMyPassword";

var encodedPassword = encoder.encode(myPassword);
System.out.println(encodedPassword);

var validPassword = encoder.matches(myPassword, encodedPassword);
System.out.println(validPassword);

O resultado deste código será semelhante a:

$argon2id$v=19$m=15360,t=2,p=1$YpRuuQhW1dHOimAnWD5TRU6Sebitu+fIrmIrenr+YOM$hkEXhhHpu2NUcPwhV4IUQelQdf4I8V+iyFsFiC8BYEisE3oWFv96zYeNA1i/awhaDo1XHz6Pp/1r55SS/I4AIA
True

Esse hash contém todas as informações necessárias, incluindo as configurações fornecidas ao codificador e os dados do salt. Assim, você pode verificar facilmente se a senha original corresponde usando encoder.matches(). Isso também funciona com outra instância de Argon2PasswordEncoder configurada com parâmetros diferentes.

Usando o Argon2 Binding para JVM

A biblioteca Argon2 Binding para JVM é uma alternativa à biblioteca Spring. Criada por Moritz Halbritter, ela usa JNA para chamar uma biblioteca nativa em C. A vantagem dessa abordagem é o uso da implementação original dos autores do Argon2, o que pode oferecer melhor desempenho. Como resultado, você precisa ter acesso à biblioteca C subjacente no seu sistema. 

Você pode instalar essa biblioteca C usando o gerenciador de pacotes ou usar a dependência que contém um conjunto de bibliotecas Argon2 pré-compiladas. A primeira opção é recomendada pelo motivo óbvio: ela é menor.

Sem bibliotecas pré-compiladas:

<dependency>
    <groupId>de.mkammerer</groupId>
    <artifactId>argon2-jvm-nolibs</artifactId>
    <version>2.11</version>
</dependency>

Com bibliotecas pré-compiladas:

<dependency>
    <groupId>de.mkammerer</groupId>
    <artifactId>argon2-jvm</artifactId>
    <version>2.11</version>
</dependency>

O uso dessa biblioteca é bastante simples. No exemplo de código abaixo, você verá um exemplo equivalente ao do Spring, com os mesmos parâmetros mínimos.

Argon2 argon2 = Argon2Factory.create(Argon2Factory.Argon2Types.ARGON2id, 32, 64);
var myPassword = "ThisIsMyPassword";

var hash = argon2.hash(2,15*1024,1, myPassword.toCharArray());
System.out.println(hash);

var validPassword = argon2.verify(hash, myPassword.toCharArray());
System.out.println(validPassword);

scrypt

Se, por algum motivo, você não puder usar Argon2id, o scrypt é uma boa segunda opção. Assim como o Argon2id, o scrypt é um algoritmo robusto de hash de senhas que permite configurar vários parâmetros de custo para aumentar os recursos que consome.

De acordo com a folha de dicas da OWASP sobre senhas, você deve usar um parâmetro mínimo de custo de CPU/memória (N), um tamanho mínimo de bloco (r) e um parâmetro de paralelização (p). As opções abaixo são consideradas o mínimo recomendado.

  • N=2^16 (64 MiB), r=8 (1024 bytes), p=1

  • N=2^15 (32 MiB), r=8 (1024 bytes), p=2

  • N=2^14 (16 MiB), r=8 (1024 bytes), p=4

  • N=2^13 (8 MiB), r=8 (1024 bytes), p=8

  • N=2^12 (4 MiB), r=8 (1024 bytes), p=15

Observe que a lista acima de parâmetros do scrypt resulta em hashes de senhas igualmente robustos. As opções mostram que, ao especificar valores diferentes para os parâmetros N, r e p, você pode fazer o algoritmo scrypt consumir mais recursos de CPU ou memória, conforme sua preferência. Minha recomendação abaixo mostra um uso prático que você pode copiar e colar para simplificar.

A biblioteca spring-security-crypto é compatível com vários algoritmos, incluindo SCrypt. Você pode usar o SCryptEncoder da mesma forma que usamos o Argon2Encoder. No exemplo abaixo, usei a sugestão intermediária para equilibrar o uso de CPU e memória.

SCryptPasswordEncoder encoder = new SCryptPasswordEncoder(16384, 8, 4, 32, 64);
var myPassword = "ThisIsMyPassword";

var encodedPassword = encoder.encode(myPassword);
System.out.println(encodedPassword);

var validPassword = encoder.matches(myPassword, encodedPassword);
System.out.println(validPassword);

Saída:

e0804$8FQ4x/ntwEz2ZNu8QRyIQJlAXR+gQkiG3WulLMEq/kioVtaFiKE7sZDGgtmqUmwB8OE+f7Eagux9QXG478unLw==$RS1Bz5Uf30dWGxc+vtlkjj7tPnPdgq8YD1V8odhPW4A=
true

Usando BCrypt

Se Argon2id e scrypt não estiverem disponíveis, outra opção robusta é o BCrypt. Se decidir usar BCrypt, o fator de trabalho deve ser definido como no mínimo 10.

Você pode usar a biblioteca spring-security-crypto de maneira semelhante à anterior. A carga de trabalho padrão é 10, mas, no exemplo a seguir, usamos 14 (um valor razoável em 2022).

BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(14);

Quanto maior o fator de trabalho, mais robusto será o hash, mas também serão necessários mais recursos de CPU (e tempo!) para processá-lo. Na minha máquina local, executar o BCrypt com carga de trabalho 10 leva, em média, cerca de 89 ms. Com carga de trabalho 14, leva, em média, 1.033 ms. Isso é quase 12 vezes mais. Este não é, de forma alguma, um benchmark preciso, mas mostra o impacto de aumentar o fator de trabalho.

Práticas recomendadas para fazer hash de senhas

Sempre que precisar implementar o hash de senhas na sua aplicação, tenha em mente algumas práticas recomendadas.

  1. Nunca implemente algoritmos de hash de senhas por conta própria — use uma biblioteca para desenvolvedores bem testada e validada! A criptografia é um campo complexo, e muitas coisas podem dar errado se você tentar implementar por conta própria um algoritmo conhecido.

  2. Como os computadores ficam mais potentes e rápidos a cada ano, os algoritmos de hash de senhas (e seus parâmetros) precisam ser ajustados! Os algoritmos modernos de hash de senhas dependem de você, desenvolvedor, para especificar quantos recursos devem usar ao calcular um hash. Com o passar do tempo, será necessário atualizar esses parâmetros. A única maneira de acompanhar essas mudanças é ficar por dentro das últimas notícias e tendências de segurança. O blog da Snyk, por exemplo, é uma ótima fonte de informações e tendências de segurança ao longo do tempo.

Use ferramentas gratuitas como a Snyk para identificar proativamente problemas de segurança no seu código e garantir que as bibliotecas e técnicas escolhidas sejam seguras. Ferramentas como a Snyk podem identificar problemas de segurança no seu código desde o início, além de sugerir correções e fornecer informações adicionais. Elas são essenciais para manter uma base de código segura à medida que a tecnologia avança.

Comece a participar de desafios de Capture the Flag

Aprenda a resolver desafios de Capture the Flag assistindo sob demanda ao nosso workshop virtual introdutório.

Leia mais

blog feature ai violet
Blog

VulnBump: agentes de IA conseguem corrigir dependências vulneráveis sem quebrar o build?

O VulnBump testa agentes de IA em 307 atualizações reais de dependências. Uma correção só é considerada válida se eliminar a vulnerabilidade e mantiver o build e os testes funcionando.

blog feature snyk container custom base recommendations
Blog

Conheça o Evo Agentic AppSec: segurança que acompanha o ritmo do desenvolvimento com IA

O desenvolvimento impulsionado por IA está avançando mais rápido do que os fluxos de trabalho de segurança tradicionais. O AppSec agentivo da Snyk usa agentes de segurança para encontrar, corrigir e verificar vulnerabilidades continuamente, enquanto as pessoas definem os critérios.

Blog

Modelos de ponta encontraram as vulnerabilidades. Só o atacante encontrou as cadeias.

A análise estática encontrou as falhas, mas só os testes de ataque em aplicações ativas provaram como elas poderiam ser encadeadas para causar invasões. Uma comparação entre Evo COS, Claude Security e Claude Code Security.