Voltar para Artigos
seguranca13 min de leitura

Criptografia na Prática: Protegendo Senhas, Tokens e Dados Sensíveis

Hash vs criptografia simétrica vs assimétrica. bcrypt com custo calibrado, Argon2 como alternativa moderna, crypto nativo para tokens seguros, e criptografia de dados PII no banco.

12 de agosto de 2026

Em 2024, o RockYou2024 foi o maior vazamento de senhas da história: 10 bilhões de credenciais. Em 2012, o LinkedIn vazou 117 milhões de senhas hasheadas com SHA-1 sem salt — todas foram quebradas em dias. Em contraste, quando o Slack vazou hashes bcrypt em 2015, eles emitiram o aviso mas não precisaram exigir reset de senhas de todos os usuários, porque bcrypt com salt adequado é computacionalmente inviável de reverter.

Este artigo cobre as três camadas de proteção criptográfica que toda aplicação séria precisa: hashing de senhas com bcrypt/Argon2, geração segura de tokens aleatórios para refresh tokens e recuperação de senha, e criptografia simétrica para PII (Personally Identifiable Information) armazenada no banco.

Hashing vs Criptografia: A Diferença Fundamental

  • Hash (função de mão única) — transforma uma entrada em uma saída de tamanho fixo. Irreversível por design. Use para senhas. Exemplos: bcrypt, Argon2, SHA-256 (mas SHA não é adequado para senhas — veja abaixo).
  • Criptografia Simétrica — transforma dados usando uma chave secreta. Reversível com a mesma chave. Use para dados que você precisa recuperar (PII, dados financeiros). Exemplos: AES-256-GCM.
  • Criptografia Assimétrica — par de chaves pública/privada. A pública cifra, a privada decifra. Use para assinaturas digitais (JWT), troca de chaves. Exemplos: RSA, ECDSA.

Nunca use SHA-256, MD5 ou SHA-1 para senhas. São algoritmos de hash de propósito geral, projetados para serem rápidos. Uma GPU moderna calcula 10 bilhões de SHA-256 por segundo — uma senha de 8 caracteres é quebrada em segundos. Use apenas algoritmos projetados especificamente para senhas: bcrypt, Argon2 ou scrypt.

bcrypt: O Padrão da Indústria

O bcrypt foi projetado em 1999 com um princípio simples: ser intencionalmente lento e ajustável. O parâmetro cost (ou saltRounds) controla a quantidade de iterações — cada incremento de 1 dobra o tempo de computação. Isso é chamado de work factor e pode ser aumentado conforme o hardware evolui.

BCryptHashProvider.ts
import { hash, compare, getRounds } from 'bcrypt';
import type { IHashProvider } from './IHashProvider';

// Calibre o COST baseado no hardware do seu servidor.
// Meta: 100-300ms por operação de hash (não muito lento para o usuário,
// não muito rápido para ataques de força bruta).
// Em 2024, cost=12 é um bom ponto de partida para servidores modernos.
const BCRYPT_COST = Number(process.env.BCRYPT_COST ?? 12);

export class BCryptHashProvider implements IHashProvider {
  async hash(plaintext: string): Promise<string> {
    // bcrypt.hash() gera um salt aleatório internamente e o inclui no resultado
    // O output inclui: algoritmo ($2b$), custo ($12$), salt (22 chars) + hash
    // Ex: $2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/lewEKVkN8F.NpJMy
    return hash(plaintext, BCRYPT_COST);
  }

  async compare(plaintext: string, hashed: string): Promise<boolean> {
    // bcrypt.compare() extrai o salt do hash armazenado e rehasha o plaintext
    // Se os resultados baterem, a senha está correta
    return compare(plaintext, hashed);
  }

  // Útil para detectar hashes com cost desatualizado (pós-migração de cost)
  needsRehash(hashed: string): boolean {
    const currentCost = getRounds(hashed);
    return currentCost < BCRYPT_COST;
  }
}
Rehash automático no login (cost migration)
// Quando um usuário faz login com sucesso, verifique se o hash precisa ser atualizado
async execute({ email, password }: AuthDTO) {
  const user = await this.usersRepo.findByEmail(email);
  if (!user?.passwordHash) throw new AppError('Credenciais inválidas.', 401);

  const isValid = await this.hashProvider.compare(password, user.passwordHash);
  if (!isValid) throw new AppError('Credenciais inválidas.', 401);

  // Rehash transparente: atualiza o hash se o cost foi aumentado
  if (this.hashProvider.needsRehash(user.passwordHash)) {
    user.passwordHash = await this.hashProvider.hash(password);
    await this.usersRepo.save(user);
    // O usuário não percebe — na próxima vez o hash já estará atualizado
  }

  return this.generateTokenPair(user.id, user.email, user.name);
}

Argon2: A Alternativa Mais Moderna

O Argon2 ganhou o Password Hashing Competition em 2015 e é a recomendação do OWASP desde 2022. Ele supera o bcrypt em dois aspectos: memory-hardness (usa muita RAM, tornando ataques com ASICs e GPUs muito mais caros) e paralelismo configurável.

bash
npm install argon2
Argon2HashProvider.ts
import argon2 from 'argon2';
import type { IHashProvider } from './IHashProvider';

export class Argon2HashProvider implements IHashProvider {
  // Configuração recomendada pelo OWASP (2024):
  // type: argon2id (resiste a ataques de lado a lado e tempo-memória)
  // memoryCost: 64MB de RAM por operação
  // timeCost: 3 iterações
  // parallelism: 4 threads
  private readonly options: argon2.Options = {
    type: argon2.argon2id,
    memoryCost: 64 * 1024, // 64 MB
    timeCost: 3,
    parallelism: 4,
  };

  async hash(plaintext: string): Promise<string> {
    return argon2.hash(plaintext, this.options);
  }

  async compare(plaintext: string, hashed: string): Promise<boolean> {
    return argon2.verify(hashed, plaintext);
  }

  needsRehash(hashed: string): boolean {
    // argon2.needsRehash() detecta automaticamente se os parâmetros mudaram
    return argon2.needsRehash(hashed, this.options);
  }
}

// Para novos projetos, use Argon2id.
// Para projetos existentes com bcrypt, migre progressivamente via rehash no login.

Tokens Seguros: crypto.randomBytes

Para gerar tokens de recuperação de senha, refresh tokens ou códigos de confirmação de e-mail, nunca use `Math.random()` — não é criptograficamente seguro. Use o módulo crypto nativo do Node.js:

generateSecureToken.ts
import { randomBytes, createHash } from 'crypto';

// Gera um token URL-safe de n bytes (padrão: 32 bytes = 64 chars hex)
export function generateSecureToken(bytes: number = 32): string {
  return randomBytes(bytes).toString('hex');
  // Exemplo de output: 'a3f8c2d1e4b5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1'
}

// Para links de recuperação de senha:
// 1. Gere o token com generateSecureToken()
// 2. Salve o HASH do token no banco (não o token em si)
// 3. Envie o token original no e-mail
// 4. Na verificação, faça hash do token recebido e compare com o banco
// (mesmo princípio do bcrypt — o token real nunca fica no banco)

export function hashToken(token: string): string {
  // SHA-256 é adequado aqui: não é para senhas, é para comparação de tokens
  // (tokens já têm alta entropia — 256 bits — então não precisa de salt)
  return createHash('sha256').update(token).digest('hex');
}

// Fluxo de reset de senha:
// 1. generateSecureToken() → token (vai no e-mail)
// 2. hashToken(token) → hashedToken (vai no banco com expiração)
// 3. Usuário clica no link com token original
// 4. hashToken(tokenRecebido) === hashedToken no banco? → permite reset

Criptografia Simétrica para PII no Banco

Alguns dados precisam ser recuperados integralmente (CPF, número de cartão mascarado, data de nascimento) mas não devem ficar em texto puro no banco. Use AES-256-GCM — o modo GCM garante autenticação (detecta adulteração):

encryption.ts
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';

const ALGORITHM = 'aes-256-gcm';
// A chave deve ter 32 bytes (256 bits). Carregue de variável de ambiente.
// Nunca gere uma nova chave a cada restart — os dados ficarão ilegíveis!
// Gere uma vez: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
const KEY = Buffer.from(process.env.ENCRYPTION_KEY as string, 'hex');

export function encrypt(plaintext: string): string {
  // IV (Initialization Vector): 12 bytes aleatórios para AES-GCM
  // Deve ser único por operação, mas não precisa ser secreto
  const iv = randomBytes(12);

  const cipher = createCipheriv(ALGORITHM, KEY, iv);
  const encrypted = Buffer.concat([
    cipher.update(plaintext, 'utf8'),
    cipher.final(),
  ]);

  // Auth tag: garante integridade — detecta adulteração
  const authTag = cipher.getAuthTag();

  // Armazena: iv:authTag:encrypted (tudo em hex)
  return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted.toString('hex')}`;
}

export function decrypt(ciphertext: string): string {
  const [ivHex, authTagHex, encryptedHex] = ciphertext.split(':');

  const iv = Buffer.from(ivHex, 'hex');
  const authTag = Buffer.from(authTagHex, 'hex');
  const encrypted = Buffer.from(encryptedHex, 'hex');

  const decipher = createDecipheriv(ALGORITHM, KEY, iv);
  decipher.setAuthTag(authTag);

  return (
    decipher.update(encrypted, undefined, 'utf8') +
    decipher.final('utf8')
  );
}

// Uso:
// const encryptedCPF = encrypt('123.456.789-00');
// await userRepo.save({ ...user, cpf: encryptedCPF });
//
// const cpf = decrypt(user.cpf);
// → '123.456.789-00'

Key Management é o problema mais difícil de criptografia. A chave AES deve ser armazenada separadamente dos dados — de preferência em um Key Management Service (KMS) como AWS KMS, HashiCorp Vault ou GCP Cloud KMS. Um banco de dados comprometido com a chave no mesmo servidor anula toda a proteção.

Conclusão

As três ferramentas têm usos distintos e não são intercambiáveis: bcrypt/Argon2 para senhas (irreversível, lento por design), crypto.randomBytes + hash para tokens seguros (alta entropia, comparação via hash), e AES-256-GCM para PII que precisa ser recuperada (reversível com chave). Conhecer a ferramenta certa para cada problema é o que separa implementações seguras das vulneráveis.