Salvando senhas corretamente: bcrypt e argon2
1. Visão geral
Salvar senhas incorretamente é uma das vulnerabilidades mais comuns que causa vazamento de dados de usuários quando o banco de dados é atacado. Este artigo ajuda você a entender por que não deve usar MD5/SHA256 puro para fazer hash de senhas, e como usar BCryptPasswordEncoder junto com Argon2PasswordEncoder no Spring Security. Após ler, você será capaz de configurar um PasswordEncoder seguro, escrever testes para verificar o comportamento e saber qual algoritmo escolher para seu projeto.
2. Por que não fazer hash de senhas com SHA-256 puro
Funções de hash criptográfico como SHA-256, SHA-512 foram projetadas para rodar o mais rápido possível. Isso é uma vantagem ao verificar a integridade de arquivos, mas é um desastre quando usado para senhas.
import java.security.MessageDigest;
import java.nio.charset.StandardCharsets;
public class NaiveHasher {
public static String sha256(String password) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(password.getBytes(StandardCharsets.UTF_8));
return java.util.HexFormat.of().formatHex(hash);
}
}
A função acima faz hash de bilhões de senhas a cada segundo em uma GPU de médio alcance. Um atacante só precisa ter uma tabela rainbow ou fazer brute-force para descobrir a senha original, especialmente com senhas comuns como 123456, password1.
Dois problemas principais:
- Sem salt: dois usuários com a mesma senha terão o mesmo hash, sendo fácil de atacar usando tabela rainbow.
- Muito rápido: não há como “desacelerar” o atacante.
Nota: Não tente adicionar salt manualmente e concatenar com SHA-256. Ainda é uma solução fraca porque falta o “work factor” — a dificuldade computacional não aumenta conforme o hardware fica mais potente.
3. O que é uma Password hashing function
Algoritmos como bcrypt, scrypt, argon2 foram projetados especificamente para fazer hash de senhas, com três características:
- Salt automático: cada vez que faz hash, gera um salt aleatório, embutindo-o direto na string de saída do hash.
- Intencionalmente lento (work factor): tem parâmetro para aumentar a dificuldade, combatendo brute-force conforme o hardware fica mais potente.
- Memory-hard (com argon2/scrypt): consome muita RAM, tornando a execução paralela em GPU/ASIC menos eficiente.
O resultado de uma string de hash bcrypt se parece com isto:
$2a$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW
Estrutura: $<version>$<cost>$<salt+hash base64>. Aqui 10 é o cost factor (2^10 iterações).
4. Usando bcrypt com Spring Security
4.1. Dependência
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
BCryptPasswordEncoder já vem incluído em spring-security-crypto, não precisa adicionar nada mais.
4.2. Declarando o bean
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12); // cost factor = 12
}
}
O parâmetro 12 define que o número de iterações será 2^12. Cost factor mais alto → mais lento → mais seguro, mas também consome mais CPU ao fazer login. Um valor de 10–12 é razoável para a maioria das aplicações em 2026.
4.3. Usando ao registrar e fazer login
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@Service
public class UserRegistrationService {
private final PasswordEncoder passwordEncoder;
private final UserRepository userRepository;
public UserRegistrationService(PasswordEncoder passwordEncoder, UserRepository userRepository) {
this.passwordEncoder = passwordEncoder;
this.userRepository = userRepository;
}
public void register(String email, String rawPassword) {
String hashed = passwordEncoder.encode(rawPassword);
userRepository.save(new Customer(email, hashed));
}
public boolean authenticate(String email, String rawPassword) {
Customer customer = userRepository.findByEmail(email)
.orElseThrow(() -> new IllegalArgumentException("Email não existe"));
return passwordEncoder.matches(rawPassword, customer.getPasswordHash());
}
}
encode()gera um salt aleatório a cada chamada, então a mesmarawPasswordproduzirá um hash diferente cada vez — este é o comportamento correto, não é um bug.matches()extrai automaticamente o salt do hash armazenado no BD, faz hash novamente da senha digitada e depois compara, você não precisa gerenciar o salt manualmente.
4.4. Testando o comportamento
import org.junit.jupiter.api.Test;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import static org.assertj.core.api.Assertions.assertThat;
class BcryptPasswordEncoderTest {
private final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(10);
@Test
void shouldGenerateDifferentHashesForSamePassword() {
String hash1 = encoder.encode("S3cretPass!");
String hash2 = encoder.encode("S3cretPass!");
assertThat(hash1).isNotEqualTo(hash2);
assertThat(encoder.matches("S3cretPass!", hash1)).isTrue();
assertThat(encoder.matches("S3cretPass!", hash2)).isTrue();
}
@Test
void shouldRejectWrongPassword() {
String hash = encoder.encode("S3cretPass!");
assertThat(encoder.matches("WrongPass!", hash)).isFalse();
}
}
Este teste confirma dois pontos importantes: salt aleatório faz o hash ser diferente a cada vez, e matches() ainda reconhece corretamente a senha original apesar dos hashes serem diferentes.
Nota: bcrypt tem um limite de comprimento de entrada de 72 bytes. Se a senha for mais longa, a parte que exceder é silenciosamente descartada. Com senhas normais (menos de 72 caracteres) não há problema, mas se você permite passphrase muito longa, considere usar argon2.
5. Usando argon2 — a escolha mais moderna
Argon2 venceu o Password Hashing Competition em 2015 e é recomendado pela OWASP como a primeira escolha atualmente, graças à sua melhor capacidade de resistir a ataques em GPU comparado ao bcrypt (memory-hard).
5.1. Dependência
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>1.78.1</version>
</dependency>
Spring Security usa Bouncy Castle como provider para Argon2PasswordEncoder, então você precisa adicionar esta dependência.
5.2. Declarando o bean
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
// saltLength=16, hashLength=32, parallelism=1, memory=1<<16 (64MB), iterations=3
return Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8();
}
}
O método factory defaultsForSpringSecurity_v5_8() retorna uma configuração de parâmetros que foi validada pelo time do Spring Security como segura para a maioria das aplicações, você não precisa calcular manualmente memory/iterations.
5.3. Comparando o output
$argon2id$v=19$m=65536,t=3,p=1$c29tZXNhbHQ$5t1h3u7K3iC2rT0...
A estrutura inclui a variante (argon2id), versão, parâmetros de memória (m), iterações (t), paralelismo (p), depois salt e hash — mais transparente que bcrypt porque expõe claramente o cost.
5.4. Teste comparando os dois encoders
import org.junit.jupiter.api.Test;
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
import static org.assertj.core.api.Assertions.assertThat;
class Argon2PasswordEncoderTest {
private final Argon2PasswordEncoder encoder =
Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8();
@Test
void shouldVerifyCorrectPassword() {
String hash = encoder.encode("MyP@ssw0rd123");
assertThat(encoder.matches("MyP@ssw0rd123", hash)).isTrue();
assertThat(encoder.matches("wrong", hash)).isFalse();
}
}
6. Suportando múltiplos encoders simultaneamente com DelegatingPasswordEncoder
Na prática, seu sistema pode precisar trocar de algoritmo ao longo do tempo (por exemplo, migrar de bcrypt para argon2) sem quebrar os hashes antigos. Spring Security fornece uma solução pronta para isso.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
createDelegatingPasswordEncoder() cria um encoder que sabe ler prefixos como {bcrypt}, {argon2}, {noop}… na string de hash para escolher o algoritmo correto de descriptografia. Por padrão, ele usa bcrypt para fazer encode de novas senhas, mas continua fazendo matches() corretamente com hashes antigos criados com outros algoritmos.
{bcrypt}$2a$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW
{argon2}$argon2id$v=19$m=65536,t=3,p=1$...
Graças a este prefixo, você migra gradualmente: usuários antigos ainda conseguem fazer login, e novos usuários ou quem muda senha serão codificados com o algoritmo mais novo.
7. Comparação rápida entre bcrypt e argon2
| Critério | bcrypt | argon2 |
|---|---|---|
| Ano de criação | 1999 | 2015 (vencedor PHC) |
| Resistência a GPU/ASIC | Boa | Melhor (memory-hard) |
| Limite de entrada | 72 bytes | Sem limite prático |
| Popularidade | Muito alta, suporta em todo lugar | Crescendo |
| Recomendação OWASP | Segunda escolha | Primeira escolha |
Se seu projeto é novo e não está vinculado a um sistema legado, escolha argon2id (a variante argon2id é padrão no Spring Security, equilibra bem entre proteção contra side-channel e ataques em GPU). Se seu sistema já usa bcrypt há muitos anos de forma estável, não é necessário migrar já — aumentar o cost factor do bcrypt para 12–14 ainda é suficientemente seguro para a maioria das aplicações.
8. Erros comuns
- Implementar salt + SHA-256 manualmente: como mencionado na seção 2, falta o work factor, portanto continua fraco contra brute-force moderno.
- Usar cost factor muito baixo:
BCryptPasswordEncoder()sem parâmetro tem cost 10 como padrão, mas se você reduzir para 4–5 para “acelerar” durante testes, é fácil esquecer de mudar novamente ao fazer deploy em produção. - Comparar hash usando
String.equals(): nunca faça comparação manual de strings de hash — sempre usematches()doPasswordEncoder, pois estas bibliotecas fazem comparação em tempo constante, evitando timing attacks. - Armazenar salt em uma coluna separada do BD: não é necessário, tanto bcrypt quanto argon2 já embutem o salt na string de hash de saída.
- Não limitar taxa de login: por mais forte que seja o hash, sem rate limiting ou bloqueio de conta, o brute-force online ainda é viável.
Nota: Nunca use o encoder
{noop}(plaintext) em produção. Deve aparecer apenas em testes ou demos locais.
9. Conclusão
Fazer hash de senhas da forma correta significa usar um algoritmo projetado especificamente para isso — bcrypt ou argon2 — em vez de funções de hash rápidas como SHA-256. Spring Security torna isso simples: basta declarar um bean PasswordEncoder, chamar encode() ao salvar e matches() ao autenticar, sem precisar gerenciar salt manualmente. Se precisar migrar flexivelmente entre algoritmos, DelegatingPasswordEncoder é a escolha segura e com compatibilidade reversa. Quer escolha bcrypt ou argon2, lembre-se que hashing forte é apenas uma camada de defesa — combine com rate limiting e MFA para criar um sistema de autenticação realmente seguro.