Cryptography Backend Passwords BCrypt
Blueprint de Criptografia

Salt & Hashing: Armazenamento Seguro de Passwords

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.

Armazenamento em texto limpo é inadmissível Guardar passwords em texto limpo não é uma decisão técnica — é uma falha de responsabilidade. Uma única brecha na base de dados expõe as credenciais de todos os utilizadores, muitos dos quais reutilizam a mesma password noutros serviços. O dano não se limita à tua plataforma.

O Que é uma Função de Hash Criptográfica

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).

Porque o Hash Simples Não é Suficiente

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.

Ataque 1
Rainbow Tables

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.

Ataque 2
Dictionary & Brute Force

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.

O Salt: Derrotar as Rainbow Tables

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.
O salt não é secreto — é único O valor do salt pode ser guardado em claro na mesma coluna que o hash. A sua função não é ser secreto — é garantir que cada hash é único, destruindo a utilidade de qualquer tabela pré-calculada. O segredo está na password, não no salt.

Algoritmos Adaptativos Lentos

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.

AlgoritmoMecanismo de lentidãoFator adaptativoRecomendaçã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.

BCrypt em Detalhe

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.

Implementação em Spring Boot

@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);
}
Mensagem de erro unificada "Credenciais inválidas" é intencional — nunca diferencias "email não existe" de "password errada". Respostas distintas permitem enumerar utilizadores registados (user enumeration attack).

Modelo de Dados

-- 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()
);

Migração de Hashes Legados

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");
    }
}

Checklist de Produção

BCrypt e JWT: camadas independentes BCrypt protege o que está em repouso na base de dados. O JWT protege a identidade em trânsito após autenticação. Uma brecha numa não compromete automaticamente a outra.