Senhas: hashing com bcrypt, argon2 e pbkdf2

1. Por que hashing de senhas é essencial?

Armazenar senhas em texto puro é um dos erros mais graves que um desenvolvedor pode cometer. Em caso de vazamento do banco de dados, todas as senhas ficam expostas, comprometendo não apenas o sistema atual, mas também outros serviços onde o usuário reutiliza a mesma senha.

A diferença entre hash e criptografia é fundamental: criptografia é bidirecional (pode ser descriptografada com uma chave), enquanto hash é unidirecional — uma vez gerado, não é possível reverter ao valor original. Para senhas, hash é a escolha correta, pois nunca precisamos recuperar a senha original, apenas verificá-la.

Propriedades desejadas em um hash de senha:
- Unidirecionalidade: impossibilidade de reverter o hash para a senha original
- Resistência a colisão: dois valores diferentes não devem gerar o mesmo hash
- Lentidão computacional: dificultar ataques de força bruta tornando cada tentativa custosa

2. Conceitos fundamentais de hashing seguro

Salt é um valor aleatório único gerado para cada senha e armazenado junto com o hash. Impede que dois usuários com a mesma senha tenham hashes iguais e torna inviável o uso de rainbow tables. Deve ser gerado com um gerador criptograficamente seguro (ex: os.urandom() em Python, crypto/rand em Go).

Pepper é um valor secreto global (não armazenado no banco de dados, mas sim em uma variável de ambiente ou cofre de segredos). Adiciona uma camada extra: mesmo que o banco vaze, o atacante não tem o pepper para calcular hashes. A diferença crucial: salt é público (armazenado com o hash), pepper é secreto.

Custo computacional (work factor) define quantas iterações ou memória o algoritmo consumirá. Deve ser ajustado periodicamente conforme o hardware evolui. Quanto maior o custo, mais lento o ataque de força bruta.

3. bcrypt: o padrão consolidado

bcrypt foi projetado em 1999 por Niels Provos e David Mazières, baseado no cifrador Blowfish. É um algoritmo adaptativo: seu custo pode ser aumentado ao longo do tempo.

Funcionamento interno: bcrypt usa uma variação do Blowfish chamada "eksblowfish" (expensive key schedule Blowfish), que executa múltiplas rodadas de expansão de chave para tornar o hashing intencionalmente lento.

Parâmetro de configuração: o cost factor (potência de 2 para o número de rodadas). Exemplo: cost=12 significa 2^12 = 4096 iterações. O valor recomendado atualmente é entre 10 e 14, dependendo da capacidade do servidor.

// Exemplo de hash com bcrypt (cost=12)
hash = bcrypt.hashpw(senha, bcrypt.gensalt(rounds=12))

// Exemplo de verificação
bcrypt.checkpw(senha, hash_armazenado)  // retorna True/False

Limitações: o tamanho máximo da senha é 72 bytes. Senhas maiores são truncadas silenciosamente, o que pode ser um problema de segurança. Além disso, bcrypt não é resistente a ataques com GPU/ASIC da mesma forma que Argon2.

4. PBKDF2: o legado amplamente suportado

PBKDF2 (Password-Based Key Derivation Function 2) faz parte do padrão PKCS#5 e é amplamente suportado em praticamente todas as linguagens e frameworks. Funciona aplicando repetidamente uma função HMAC (ex: HMAC-SHA256) milhares de vezes.

Configuração: define-se o número de iterações e o algoritmo hash subjacente. O NIST recomenda atualmente no mínimo 600.000 iterações para SHA-256 (recomendação de 2023).

// Exemplo com PBKDF2-HMAC-SHA256 (600.000 iterações)
hash = pbkdf2_hmac('sha256', senha, salt, 600000)

// Verificação: repete o processo e compara hashes
hash_verificacao = pbkdf2_hmac('sha256', senha_tentativa, salt_armazenado, 600000)
if hash_verificacao == hash_armazenado:
    // senha correta

Quando usar PBKDF2: ideal para cenários de compatibilidade com sistemas legados, especialmente quando a biblioteca disponível não suporta bcrypt ou Argon2. Desvantagens: é mais vulnerável a ataques com hardware especializado (GPU, FPGA, ASIC) por não exigir memória significativa, permitindo paralelização massiva.

5. Argon2: o vencedor da competição PHC

Argon2 venceu a Password Hashing Competition (PHC) em 2015 e é o algoritmo mais moderno e seguro disponível. Possui três variantes:

  • Argon2d: resistente a ataques de side-channel via GPU, usa acesso à memória dependente dos dados
  • Argon2i: resistente a ataques de side-channel via timing, usa acesso à memória independente dos dados
  • Argon2id (recomendado): combina as abordagens de Argon2i e Argon2d, oferecendo a melhor proteção geral

Parâmetros críticos:
- t (time): número de iterações
- m (memory): uso de memória em KiB
- p (parallelism): grau de paralelismo (threads)

// Exemplo com Argon2id (parâmetros recomendados para 2024)
hash = argon2.hash_password(senha, 
    time_cost=3,        // t = 3 iterações
    memory_cost=65536,  // m = 64 MiB
    parallelism=4,      // p = 4 threads
    hash_len=32,        // tamanho do hash em bytes
    salt_len=16)        // tamanho do salt em bytes

// Verificação
argon2.verify_password(hash_armazenado, senha_tentativa)  // retorna True/False

Vantagens sobre bcrypt e PBKDF2: Argon2 exige uma quantidade configurável de memória, tornando ataques com GPU e ASIC extremamente caros (um ASIC precisa de chips de memória caros, não apenas lógica). Além disso, não tem limite de tamanho de senha.

6. Comparação prática entre os algoritmos

Característica bcrypt PBKDF2 Argon2id
Resistência a GPU Média Baixa Alta
Resistência a ASIC Média Baixa Alta
Uso de memória Baixo Baixo Alto (configurável)
Tamanho máximo senha 72 bytes Ilimitado Ilimitado
Maturidade Alta (desde 1999) Muito alta (desde 2000) Média (desde 2015)
Suporte em linguagens Muito alto Muito alto Crescente

Recomendações por cenário:
- Sistemas legados: PBKDF2 é seguro se configurado com iterações adequadas (600k+)
- Novos projetos: Argon2id é a escolha ideal
- Ambientes com restrição severa de memória: bcrypt com cost factor >= 12
- Sistemas críticos (bancos, saúde): Argon2id com parâmetros elevados (m=128MiB, t=4, p=4)

Para escolher o work factor ideal, meça o tempo de hashing no servidor alvo. O hash deve levar entre 250ms e 500ms — tempo suficiente para dificultar ataques, mas não a ponto de prejudicar a experiência do usuário no login.

7. Implementação segura e boas práticas

Exemplo completo com bcrypt (Python):

import bcrypt

# Hashing
senha = b"minha_senha_segura_123"
salt = bcrypt.gensalt(rounds=12)
hash_armazenado = bcrypt.hashpw(senha, salt)

# Verificação (ex: durante login)
senha_tentativa = b"minha_senha_segura_123"
if bcrypt.checkpw(senha_tentativa, hash_armazenado):
    print("Senha correta!")
else:
    print("Senha incorreta.")

Exemplo completo com Argon2id (Python):

from argon2 import PasswordHasher

ph = PasswordHasher(
    time_cost=3,
    memory_cost=65536,
    parallelism=4,
    hash_len=32,
    salt_len=16
)

# Hashing
hash_armazenado = ph.hash("minha_senha_segura_123")

# Verificação
try:
    ph.verify(hash_armazenado, "minha_senha_segura_123")
    print("Senha correta!")
except:
    print("Senha incorreta.")

Erros comuns a evitar:
1. Salt fixo ou previsível — use gerador criptograficamente seguro
2. Work factor muito baixo — teste periodicamente e aumente conforme hardware evolui
3. Ignorar atualização periódica — re-hash senhas no próximo login quando aumentar o custo
4. Truncar senhas sem aviso — especialmente com bcrypt (limite de 72 bytes)
5. Armazenar pepper no banco de dados — use variáveis de ambiente ou cofre de segredos

Lembre-se: a segurança de senhas é uma corrida armamentista. O que é seguro hoje pode não ser amanhã. Mantenha-se atualizado, monitore as recomendações do OWASP e do NIST, e ajuste seus parâmetros regularmente.

Referências