Dominando Gerenciamento de Senhas: bcrypt, Argon2, PBKDF2 e Políticas Seguras em Projetos Reais Já leu

Por que Gerenciar Senhas Corretamente é Crítico Antes de mergulharmos nas técnicas, precisamos entender por que armazenar senhas em texto plano é um suicídio de segurança. Quando você armazena uma senha como "usuario123" no banco de dados, qualquer pessoa que acesse esse banco — seja por negligência, falha de segurança ou ataque — terá acesso imediato às contas dos seus usuários. Não é uma questão de se será hackeado, mas quando. Senhas devem ser transformadas em um formato que seja computacionalmente impossível reverter. O maior erro que vejo em produção é o uso de hashing simples como MD5 ou SHA-1. Esses algoritmos foram projetados para velocidade, não para segurança de senhas. Um computador moderno consegue testar bilhões de combinações por segundo contra um hash MD5. O que precisamos são algoritmos lentos e adaptativos — quanto mais tempo levarem para processar uma senha, mais difícil fica fazer um ataque de força bruta. Entendendo os Algoritmos de Hashing de Senhas O Conceito

Por que Gerenciar Senhas Corretamente é Crítico

Antes de mergulharmos nas técnicas, precisamos entender por que armazenar senhas em texto plano é um suicídio de segurança. Quando você armazena uma senha como "usuario123" no banco de dados, qualquer pessoa que acesse esse banco — seja por negligência, falha de segurança ou ataque — terá acesso imediato às contas dos seus usuários. Não é uma questão de se será hackeado, mas quando. Senhas devem ser transformadas em um formato que seja computacionalmente impossível reverter.

O maior erro que vejo em produção é o uso de hashing simples como MD5 ou SHA-1. Esses algoritmos foram projetados para velocidade, não para segurança de senhas. Um computador moderno consegue testar bilhões de combinações por segundo contra um hash MD5. O que precisamos são algoritmos lentos e adaptativos — quanto mais tempo levarem para processar uma senha, mais difícil fica fazer um ataque de força bruta.

Entendendo os Algoritmos de Hashing de Senhas

O Conceito Fundamental: Salt e Iterations

Vamos começar com dois conceitos fundamentais que aparecem em todos os algoritmos modernos: salt e iterations.

Um salt é uma sequência aleatória de bytes adicionada à senha antes do hashing. Se você tem dois usuários com a mesma senha "123456", sem salt eles teriam o mesmo hash — permitindo que um atacante identifique padrões. Com salt, cada hash é único, mesmo para senhas idênticas. O salt não é secreto; fica armazenado junto do hash.

Iterations (ou rounds) significa o número de vezes que o algoritmo aplica sua função de hash internamente. Quanto maior esse número, mais tempo leva para calcular — tornando brute force impraticável. Um bom algoritmo permite aumentar o número de iterations conforme o poder computacional evolui.

bcrypt: O Padrão da Indústria

bcrypt é baseado no algoritmo Blowfish e foi especificamente projetado para hashing de senhas. Sua elegância está na simplicidade: você define um "custo" (cost factor), que determina quantas vezes o algoritmo itera. A cada 2 anos, você duplica esse custo e todos os novos hashes ficam mais seguros automaticamente. Hashes antigos continuam funcionando, mas quando o usuário faz login novamente, você pode re-hashear com o novo custo.

bcrypt também gera o salt automaticamente — você não precisa gerenciar isso separadamente. O hash resultante inclui o salt, o custo e o resultado, tudo em um formato pronto para armazenamento.

// Node.js com bcrypt
const bcrypt = require('bcrypt');

// Hashear uma senha no registro
async function hashPassword(password) {
  const saltRounds = 12; // Custo de computação
  const hash = await bcrypt.hash(password, saltRounds);
  return hash;
  // Resultado exemplo: $2b$12$R9h7cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUm
}

// Verificar senha no login
async function verifyPassword(password, hash) {
  const match = await bcrypt.compare(password, hash);
  return match;
}

// Uso
(async () => {
  const myPassword = 'MinhaSenh@Forte123';
  const storedHash = await hashPassword(myPassword);
  console.log('Hash:', storedHash);

  const isValid = await verifyPassword(myPassword, storedHash);
  console.log('Senha válida:', isValid);
})();

Recomendação prática: Use cost factor 12 como padrão em 2024. Isso leva aproximadamente 300ms em um servidor moderno — tempo aceitável para login. Se o servidor ficar mais rápido, aumente para 13.

Argon2: A Escolha Moderna

Argon2 venceu a Password Hashing Competition (2015) do OWASP e é considerada a melhor opção hoje. Diferente de bcrypt que é apenas CPU-bound, Argon2 é tanto CPU-bound quanto memory-hard: requer uma quantidade significativa de RAM para ser calculada. Isso torna ataques em GPUs ou ASICs muito menos eficientes.

Existem três variantes: Argon2i (otimizado contra ataques de timing), Argon2d (mais rápido, menos proteção contra timing) e Argon2id (híbrido, recomendado). Use Argon2id.

// Node.js com argon2
const argon2 = require('argon2');

async function hashPasswordArgon2(password) {
  try {
    const hash = await argon2.hash(password, {
      type: argon2.argon2id,
      memoryCost: 65536,  // 64 MB
      timeCost: 3,        // 3 iterações
      parallelism: 4      // 4 threads paralelos
    });
    return hash;
  } catch (err) {
    console.error(err);
  }
}

async function verifyPasswordArgon2(password, hash) {
  try {
    const match = await argon2.verify(hash, password);
    return match;
  } catch (err) {
    console.error(err);
    return false;
  }
}

// Uso
(async () => {
  const myPassword = 'OutraSenh@Forte456';
  const storedHash = await hashPasswordArgon2(myPassword);
  console.log('Hash Argon2:', storedHash);

  const isValid = await verifyPasswordArgon2(myPassword, storedHash);
  console.log('Senha válida:', isValid);
})();

O hash Argon2 inclui os parâmetros (memoryCost, timeCost, parallelism) no próprio string, permitindo que você altere estratégias sem quebrar verificações antigas.

PBKDF2: O Algoritmo Conservador

PBKDF2 (Password-Based Key Derivation Function 2) é um padrão estabelecido (RFC 2898) recomendado por NIST. É menos sofisticado que Argon2, sendo apenas CPU-bound, mas é amplamente suportado em bibliotecas criptográficas padrão e em hardware/smartcards.

PBKDF2 aplica uma função pseudoaleatória (geralmente HMAC-SHA256) repetidamente sobre a senha com salt. O número de iterações é completamente configurável e deve ser aumentado regularmente.

// Node.js com crypto (built-in)
const crypto = require('crypto');

function hashPasswordPBKDF2(password) {
  const salt = crypto.randomBytes(32).toString('hex');
  const iterations = 600000; // NIST recomenda mínimo 600k em 2024
  const keylen = 64;
  const digest = 'sha256';

  const derivedKey = crypto.pbkdf2Sync(
    password,
    salt,
    iterations,
    keylen,
    digest
  );

  // Formato: iterations$salt$key
  return `${iterations}$${salt}$${derivedKey.toString('hex')}`;
}

function verifyPasswordPBKDF2(password, hash) {
  const parts = hash.split('$');
  const iterations = parseInt(parts[0]);
  const salt = parts[1];
  const storedKey = parts[2];

  const derivedKey = crypto.pbkdf2Sync(
    password,
    salt,
    iterations,
    64,
    'sha256'
  );

  // Comparação timing-safe
  return crypto.timingSafeEqual(
    Buffer.from(storedKey, 'hex'),
    derivedKey
  );
}

// Uso
const myPassword = 'MaisUmaSenh@123';
const storedHash = hashPasswordPBKDF2(myPassword);
console.log('Hash PBKDF2:', storedHash);

const isValid = verifyPasswordPBKDF2(myPassword, storedHash);
console.log('Senha válida:', isValid);

Atenção: Note o uso de crypto.timingSafeEqual() — nunca compare hashes com === comum. Comparações normais vaza informação de tempo, permitindo ataques de timing.

Políticas Seguras de Senhas

Requisitos Mínimos e Máximos

A tentação comum é criar uma política rígida: "mínimo 12 caracteres, 1 maiúscula, 1 número, 1 símbolo especial". O NIST atualizou suas recomendações em 2017 e descartou essa abordagem. Políticas muito restritivas levam a senhas previsíveis ("Senha123!") ou anotadas em post-its.

Use em vez disso: mínimo de 8 caracteres para usuários humanos, permitindo qualquer caractere. Se quiser ser mais rigoroso, 12 caracteres é forte sem ser opressivo. Proíba apenas senhas muito comuns (as 100 mil mais usadas no mundo estão listadas publicamente).

Nunca imponha expiração periódica de senhas sem motivo. Só force mudança se houver evidência de comprometimento. Expiração forçada leva a senhas simples ou variações incrementais ("Senha2024!" → "Senha2025!").

# Python: Validação segura com ZXCVBN
from zxcvbn import zxcvbn
import requests

def validate_password(password, user_inputs=None):
    """
    user_inputs: lista com nome de usuário, email, etc para contextualizar
    """
    if len(password) < 8:
        return False, "Mínimo 8 caracteres"

    # Verifica contra senhas muito comuns
    common_passwords = {
        'password', '123456', '12345678', 'qwerty', 'abc123',
        'senha', '111111', '1234567', 'letmein', 'welcome'
    }

    if password.lower() in common_passwords:
        return False, "Senha muito comum"

    # Score ZXCVBN (0-4, onde 4 é excelente)
    result = zxcvbn(password, user_inputs or [])
    score = result['score']

    if score < 2:
        return False, f"Senha fraca. Sugestões: {result['feedback']['suggestions']}"

    return True, f"Força OK (score: {score}/4)"

# Uso
is_valid, message = validate_password('MyP@ssw0rdIsStrong123')
print(message)

Proteção Contra Ataques Comuns

Rate Limiting: Limite tentativas de login. Após 5 falhas em 15 minutos, bloqueie por 15 minutos. Implemente por IP e por usuário.

Account Lockout: Diferencie entre "usuário não existe" e "senha incorreta". Sempre retorne a mesma mensagem genérica ("Credenciais inválidas") para não revelar se o email está cadastrado.

Breach Detection: Integre verificação com base de dados de senhas comprometidas (HaveIBeenPwned API). Quando um usuário faz login, verifique se sua senha apareceu em algum vazamento conhecido.

# Python: Integração com HaveIBeenPwned
import hashlib
import requests

def check_password_breach(password):
    """
    Verifica se a senha foi comprometida usando HaveIBeenPwned API
    Usa k-anonymity: envia apenas os 5 primeiros caracteres do hash SHA-1
    """
    sha1 = hashlib.sha1(password.encode()).hexdigest().upper()
    prefix = sha1[:5]
    suffix = sha1[5:]

    try:
        response = requests.get(
            f'https://api.pwnedpasswords.com/range/{prefix}',
            headers={'User-Agent': 'MyApp/1.0'}
        )
        response.raise_for_status()

        hashes = response.text.split('\r\n')
        for hash_line in hashes:
            hash_suffix, count = hash_line.split(':')
            if hash_suffix == suffix:
                return True, int(count)  # Comprometida, visto N vezes

        return False, 0  # Segura
    except requests.RequestException as e:
        print(f"Erro ao consultar API: {e}")
        return None, None  # Erro na requisição

# Uso
is_breached, count = check_password_breach('mypassword123')
if is_breached:
    print(f"AVISO: Essa senha apareceu em {count} vazamentos conhecidos!")

Armazenamento Seguro de Hashes

Nunca coloque o hash de senha em logs. Nunca transmita hash pelo HTTP. Sempre use HTTPS. Se usar cookies de sessão, marque como HttpOnly e Secure:

// Express.js: Configuração segura de cookies
app.post('/login', async (req, res) => {
  const { email, password } = req.body;

  // Busca usuário (pseudocódigo)
  const user = await User.findByEmail(email);
  if (!user || !await bcrypt.compare(password, user.passwordHash)) {
    return res.status(401).json({ error: 'Credenciais inválidas' });
  }

  // Gera token JWT com duração curta (15 min)
  const token = jwt.sign(
    { userId: user.id, email: user.email },
    process.env.JWT_SECRET,
    { expiresIn: '15m' }
  );

  // Token em cookie seguro
  res.cookie('authToken', token, {
    httpOnly: true,      // Não acessível via JavaScript
    secure: true,        // Apenas HTTPS
    sameSite: 'Strict',  // CSRF protection
    maxAge: 15 * 60 * 1000  // 15 minutos
  });

  res.json({ success: true });
});

Implementação Prática em Produção

Escolhendo o Algoritmo Certo

  • bcrypt: Use se você precisa de simplicidade e compatibilidade. Seguro o suficiente para a maioria dos casos. Não exige gerenciamento de parâmetros complexos.
  • Argon2: Use se sua aplicação lida com dados sensíveis (fintech, healthcare, governo). Oferece a melhor proteção atual. Pequena curva de aprendizado nos parâmetros.
  • PBKDF2: Use se precisar de conformidade com padrões específicos (FIPS) ou integrações com hardware criptográfico. Menos moderno, mas confiável.

Na prática, tenho visto casos de sucesso com:
- Startups e MVPs: bcrypt com cost 12
- Aplicações médicas/financeiras: Argon2id
- Sistemas legados ou regulados: PBKDF2 com 600k+ iterations

# Python: Abstração para facilitar migração entre algoritmos
from abc import ABC, abstractmethod
import bcrypt
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError

class PasswordManager(ABC):
    @abstractmethod
    def hash(self, password: str) -> str:
        pass

    @abstractmethod
    def verify(self, password: str, hash: str) -> bool:
        pass

class BcryptManager(PasswordManager):
    def __init__(self, rounds=12):
        self.rounds = rounds

    def hash(self, password: str) -> str:
        return bcrypt.hashpw(password.encode(), bcrypt.gensalt(self.rounds)).decode()

    def verify(self, password: str, hash: str) -> bool:
        return bcrypt.checkpw(password.encode(), hash.encode())

class Argon2Manager(PasswordManager):
    def __init__(self):
        self.hasher = PasswordHasher()

    def hash(self, password: str) -> str:
        return self.hasher.hash(password)

    def verify(self, password: str, hash: str) -> bool:
        try:
            self.hasher.verify(hash, password)
            return True
        except VerifyMismatchError:
            return False

# Uso: Pode trocar o manager sem alterar lógica de negócio
manager = Argon2Manager()  # Troque para BcryptManager() se necessário
password_hash = manager.hash('usuario_password')
is_valid = manager.verify('usuario_password', password_hash)

Migração de Senhas Antigas

Se você herda um sistema com senhas em MD5 ou SHA-1, não desespere. Não tente "reverter" — é impossível com hashes criptográficos. Em vez disso, implemente rehashing transparente:

  1. Quando o usuário faz login com sucesso, recalcule o hash com bcrypt/Argon2
  2. Armazene o novo hash
  3. O hash antigo fica descartado

Isso requer armazenar qual algoritmo foi usado em cada hash:

-- Schema com suporte a múltiplos algoritmos
CREATE TABLE users (
  id INT PRIMARY KEY,
  email VARCHAR(255) UNIQUE,
  password_hash VARCHAR(255),
  password_algorithm VARCHAR(20),  -- 'md5', 'bcrypt', 'argon2'
  created_at TIMESTAMP,
  updated_at TIMESTAMP
);
// Login com rehashing transparente
app.post('/login', async (req, res) => {
  const { email, password } = req.body;

  const user = await db.query('SELECT * FROM users WHERE email = ?', [email]);
  if (!user) return res.status(401).json({ error: 'Inválido' });

  let isValid = false;

  // Verifica com algoritmo antigo
  if (user.password_algorithm === 'md5') {
    isValid = crypto.createHash('md5').update(password).digest('hex') === user.password_hash;
  } else if (user.password_algorithm === 'bcrypt') {
    isValid = await bcrypt.compare(password, user.password_hash);
  }

  if (!isValid) return res.status(401).json({ error: 'Inválido' });

  // Se era hash antigo, rehasheia com bcrypt
  if (user.password_algorithm !== 'bcrypt') {
    const newHash = await bcrypt.hash(password, 12);
    await db.query(
      'UPDATE users SET password_hash = ?, password_algorithm = ? WHERE id = ?',
      [newHash, 'bcrypt', user.id]
    );
  }

  res.json({ success: true });
});

Conclusão

Aprendi ao longo dos anos que gerenciamento de senhas não é sobre complexidade, mas sobre sensatez. O erro mais custoso não é escolher bcrypt ou Argon2 (ambos são seguros com boas configurações), mas negligenciar completamente: armazenar senhas em texto plano, usar hashes rápidos como MD5, ou implementar policies ridículas que levam a senhas fracas.

Os dois pilares que você deve memorizar:

  1. Use um algoritmo adaptativo e lento: bcrypt ou Argon2. Configure para levar pelo menos 100-300ms. Não negocie nesse ponto.

  2. Implemente defesas em camadas: rate limiting, breach detection, HTTPS obrigatório, cookies seguros, e senhas comuns bloqueadas. Nenhum algoritmo sozinho é à prova de balas.

Por fim, desconfie de políticas de senha que parecem "mais seguras" (expiração forçada, caracteres especiais obrigatórios). O NIST — a autoridade em segurança nos EUA — descartou essas ideias. Seu trabalho é permitir boas senhas e tornar as fracas inúteis.

Referências


Artigos relacionados