Uma password nunca deve ser armazenada — apenas a sua impressão digital unidirecional. Este artigo explica o que é uma função de hash criptográfica, porque o hash simples não é suficiente, o que é um salt e como resolve os ataques de Rainbow Table, e porque os algoritmos adaptativos lentos como BCrypt são o padrão correto para passwords em produção.
Uma função de hash criptográfica transforma um input de qualquer tamanho num output de tamanho fixo — o digest — com três propriedades fundamentais:
// SHA-256 — exemplo conceptual (NÃO usar para passwords)
SHA256("password123")
// → "ef92b778bafe771e89245b89ecbc08a44a4e166c06659911881f383d4473e94f"
SHA256("password124")
// → "0b14d501a594442a01c6859541bc69166495ae7f3c7491b6cd9eab8d52b6e7d8"
// Uma letra diferente → digest completamente diferente (efeito avalanche).
SHA-256 foi concebido para ser rápido. Essa velocidade é a sua maior fraqueza quando aplicado a passwords. Um atacante com uma GPU moderna consegue calcular bilhões de hashes SHA-256 por segundo — força bruta sobre passwords fracas é trivial.
Uma base de dados pré-calculada de pares password → hash.
O atacante não calcula nada — consulta a tabela. Com SHA-256 puro,
"password123" tem sempre o mesmo hash em qualquer sistema.
Com GPUs a calcular ~10 mil milhões de SHA-256/segundo, uma password de 8 caracteres alfanuméricos é testada exaustivamente em minutos. A velocidade de SHA-256 torna a força bruta viável em hardware comum.
Um salt é um valor aleatório gerado unicamente para cada utilizador, concatenado à password antes do hash. Garante que duas passwords idênticas produzem hashes completamente diferentes — tornando qualquer tabela pré-calculada inútil.
// Sem salt — hashes idênticos para passwords idênticas
SHA256("hunter2") // → "f52fbd..." ← Rainbow Table encontra imediatamente
SHA256("hunter2") // → "f52fbd..." ← igual para todos os utilizadores
// Com salt — hashes únicos mesmo para passwords idênticas
salt_alice = "a3f9c2..." // gerado aleatoriamente para a Alice
salt_bob = "7e2b1d..." // gerado aleatoriamente para o Bob
SHA256("hunter2" + salt_alice) // → "9c2e17..." ← único para a Alice
SHA256("hunter2" + salt_bob) // → "4a8f3b..." ← único para o Bob
// O salt é guardado em texto limpo — não é secreto, é único.
Salt + SHA-256 resolve Rainbow Tables mas não resolve a velocidade da força bruta. A solução é um algoritmo deliberadamente lento — projetado para tornar cada tentativa de login cara o suficiente para inviabilizar ataques em escala.
| Algoritmo | Mecanismo de lentidão | Fator adaptativo | Recomendação |
|---|---|---|---|
| BCrypt | Rounds de expansão de chave | Cost factor (2–31). Cada +1 duplica o tempo. | ✅ Padrão da indústria. Cost 10–12 em produção. |
| SCrypt | CPU + memória intensivo | N, r, p | ✅ Resistente a ASICs. Mais complexo de configurar. |
| Argon2id | CPU + memória + paralelismo | Time cost, memory cost, parallelism | ✅ Recomendação atual do OWASP. Vencedor PHC 2015. |
| SHA-256 / MD5 | — | Nenhum — concebido para ser rápido | 🚫 Nunca para passwords. |
O BCrypt incorpora o salt automaticamente no resultado final. A string que guardas na base de dados já contém o salt, o cost factor e o hash — num formato autocontido. Não precisas de gerir o salt manualmente.
// Anatomia de um hash BCrypt
$2a$12$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
│ │ │ │
│ │ └─ salt (22 chars Base64) └─ hash (31 chars Base64)
│ └─ cost factor = 12 (2^12 = 4096 rounds)
└─ versão do algoritmo (2a)
// Cost factor e tempo de cálculo (hardware médio 2024):
// cost 10 → ~100ms (mínimo aceitável)
// cost 12 → ~400ms (recomendado — equilíbrio segurança/UX)
// cost 14 → ~1.5s (alta segurança)
// Regra: alvo de ~300ms no servidor de produção.
@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12);
}
}
// Registo — encode() gera salt + hash automaticamente
public User register(RegisterRequest req) {
if (userRepo.existsByEmail(req.email())) {
throw new ConflictException("Email já registado");
}
User user = new User();
user.setEmail(req.email());
user.setPasswordHash(encoder.encode(req.password()));
return userRepo.save(user);
}
// Login — matches() extrai salt do hash e compara com timing seguro
public AuthResponse login(LoginRequest req) {
User user = userRepo.findByEmail(req.email())
.orElseThrow(() -> new UnauthorizedException("Credenciais inválidas"));
if (!encoder.matches(req.password(), user.getPasswordHash())) {
throw new UnauthorizedException("Credenciais inválidas");
}
return jwtService.issueTokens(user);
}
-- Apenas o hash — nunca a password
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
password_hash VARCHAR(60) NOT NULL, -- BCrypt produz sempre 60 chars
created_at TIMESTAMPTZ DEFAULT NOW()
);
Sistemas com MD5 ou SHA-1 podem migrar sem reset de passwords: no próximo login bem-sucedido, recalcula em BCrypt e substitui. Os utilizadores que nunca voltarem a fazer login são migrados na próxima sessão de reset forçado.
if (user.isLegacyHash()) {
if (!legacyEncoder.matches(req.password(), user.getPasswordHash())) {
throw new UnauthorizedException("Credenciais inválidas");
}
// Migração lazy: substitui por BCrypt no próximo login bem-sucedido
user.setPasswordHash(bcryptEncoder.encode(req.password()));
user.setLegacyHash(false);
userRepo.save(user);
} else {
if (!bcryptEncoder.matches(req.password(), user.getPasswordHash())) {
throw new UnauthorizedException("Credenciais inválidas");
}
}
password_hash apenas — sem coluna password.matches() — nunca com equals().