Blog
35 artigos · atualizado semanalmente Veja nossas Ferramentas
Todos os artigos
Tutoriais

Salt, pepper, bcrypt e Argon2id: como proteger senhas de verdade

Em 2012, o LinkedIn expôs 117mi de senhas. SHA-1 sem salt — 90% quebradas em 4h. Entenda o que cada camada de proteção resolve e por que Argon2id é a escolha certa hoje.

COVER · Tutoriais

Em junho de 2012, o LinkedIn foi comprometido. 117 milhões de senhas vazaram. A proteção usada era SHA-1 sem salt — e 90% das senhas foram quebradas em menos de quatro horas. O problema não era o vazamento do banco em si: era que a proteção adotada tornava o ataque trivial. GPUs modernas testam bilhões de hashes SHA por segundo.

Esse post cobre o que realmente protege senhas armazenadas: salt, pepper, bcrypt e Argon2id. Cada camada resolve um problema diferente.


Por que SHA-256 não é suficiente para senhas

SHA-256 é rápido. Extremamente rápido. Uma GPU RTX 4090 consegue calcular cerca de 22 bilhões de hashes SHA-256 por segundo.

Isso é ótimo para verificação de integridade de arquivos. Para senhas, é catastrófico.

O problema: um atacante que obtém o banco de dados hashado pode testar combinações em velocidade industrial. Uma senha de 8 caracteres alfanuméricos tem pouco mais de 218 trilhões de combinações. A 22 bilhões de hashes por segundo, isso leva cerca de 2,7 horas em força bruta pura — e com dicionários e regras de mutação, a maioria das senhas reais cai em minutos.

Velocidade é a característica errada para uma função de hash de senhas.


O que é salt e qual problema ele resolve

Salt é um valor aleatório gerado por senha, armazenado junto ao hash no banco de dados.

Antes do salt, um atacante podia usar rainbow tables — tabelas pré-computadas que mapeiam hashes conhecidos para as senhas originais. Você pega o hash 5f4dcc3b5aa765d61d8327deb882cf99 e procura na tabela: password. Sem custo de computação adicional.

Com salt, isso não funciona. Se cada senha tem um salt único de 128 bits, o atacante precisaria pré-computar uma rainbow table separada para cada salt possível — o que é computacionalmente inviável.

senha_original: "hunter2"
salt:           "x7Kp9mNqL2..."   (gerado aleatoriamente)
hash armazenado: SHA-256("hunter2" + "x7Kp9mNqL2...") → armazenado com o salt

O salt não é segredo. Fica no banco junto ao hash. A proteção vem da unicidade, não do sigilo.

O que o salt resolve: rainbow tables e ataques de hash compartilhado (dois usuários com a mesma senha terão hashes diferentes).

O que o salt não resolve: força bruta direta contra senhas fracas. Se a senha é 123456, um atacante ainda pode testá-la contra o salt específico daquele usuário.


O que é pepper e qual problema ele resolve

Pepper é um segredo global adicionado à senha antes do hash, armazenado fora do banco — em variável de ambiente, HSM ou vault.

hash = bcrypt(senha + pepper, salt)

O pepper não fica no banco. Se o banco vazar, o atacante tem os hashes mas não tem o pepper — e sem o pepper, não consegue validar tentativas de senha.

O que o pepper resolve: compromisso total do banco de dados. Mesmo com o dump, os hashes são inúteis sem o segredo externo.

O que o pepper não resolve: se o servidor inteiro for comprometido (código + banco + ambiente), o pepper fica exposto junto. É uma camada extra, não uma garantia absoluta.

import os
import bcrypt

PEPPER = os.environ["PASSWORD_PEPPER"]  # segredo fora do banco

def hash_password(plain: str) -> bytes:
    peppered = (plain + PEPPER).encode("utf-8")
    return bcrypt.hashpw(peppered, bcrypt.gensalt(rounds=12))

def verify_password(plain: str, stored_hash: bytes) -> bool:
    peppered = (plain + PEPPER).encode("utf-8")
    return bcrypt.checkpw(peppered, stored_hash)

Por que bcrypt é a escolha padrão

Bcrypt foi projetado em 1999 especificamente para hashing de senhas por Niels Provos e David Mazières. A ideia central: ser ajustável em custo.

O cost factor N faz a função executar 2ⁿ rounds de derivação de chave. Cost 10 = 1.024 rounds. Cost 12 = 4.096 rounds. Cost 14 = 16.384 rounds.

À medida que hardware fica mais rápido, você aumenta o cost factor — o tempo de verificação sobe, mas a verificação legítima é raramente uma operação crítica de latência (acontece uma vez por login). O brute-force, por outro lado, fica proporcionalmente mais caro.

$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/lewrHzPjBPJgTEyF2

$2b$   → versão do algoritmo
12     → cost factor
LQv3c1yqBWVHxkd0LHAkCO → salt (22 chars, 128 bits)
Yz6TtxMQJqhN8/lewrHzPjBPJgTEyF2 → hash (31 chars)

A estrutura do hash inclui o salt — por isso você não precisa armazená-lo separadamente. O hash bcrypt é auto-contido.

O limite de 72 bytes

Bcrypt trunca inputs em 72 bytes. Uma senha com 73+ caracteres tem os bytes excedentes ignorados. Na prática, isso não afeta a maioria dos usuários, mas é relevante se você permitir senhas longas.

Solução comum: fazer um SHA-256 da senha antes de passar ao bcrypt, convertendo para hex (64 bytes) — dentro do limite.

import hashlib
import bcrypt

def prepare_password(plain: str) -> bytes:
    # SHA-256 → hex → 64 bytes → dentro do limite bcrypt de 72 bytes
    return hashlib.sha256(plain.encode("utf-8")).hexdigest().encode("utf-8")

def hash_password(plain: str) -> bytes:
    return bcrypt.hashpw(prepare_password(plain), bcrypt.gensalt(rounds=12))

Cost factor recomendado

O OWASP recomenda cost 12 como mínimo para produção em 2025, com meta de ~250ms no hardware do servidor. Hardware mais rápido justifica cost 13 ou 14.

A regra prática: meça no servidor de produção e escolha o cost factor mais alto que mantém o tempo de login abaixo de 500ms.

// Node.js — benchmark de cost factor
const bcrypt = require('bcryptjs');

async function benchmark() {
  const password = 'benchmark-password';
  for (let cost = 10; cost <= 16; cost++) {
    const start = Date.now();
    await bcrypt.hash(password, cost);
    const elapsed = Date.now() - start;
    console.log(`Cost ${cost}: ${elapsed}ms`);
  }
}

benchmark();

Por que Argon2id é o mais seguro hoje

Bcrypt foi projetado para resistir a CPUs. O problema: ASICs e GPUs modernos têm memória por chip insignificante comparada ao throughput de computação. Bcrypt é intensivo em CPU mas não em memória — o que permite paralelização em hardware especializado.

Argon2id foi criado para ser memory-hard: exige que o atacante aloque gigabytes de RAM por thread de ataque. RAM cara e limitada elimina as vantagens do hardware especializado.

Argon2id ganhou o Password Hashing Competition em 2015 e é a recomendação do OWASP para novos sistemas desde 2023.

Os três parâmetros principais:

Parâmetro Significado OWASP mínimo (2025)
m (memória) KiB por operação 19.456 KiB (19 MB)
t (iterações) rounds de processamento 2
p (paralelismo) threads simultâneas 1
# Python — Argon2id com argon2-cffi
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError

ph = PasswordHasher(
    time_cost=2,        # iterações
    memory_cost=19456,  # 19 MB
    parallelism=1,
    hash_len=32,
    salt_len=16
)

hashed = ph.hash("minha-senha")
# $argon2id$v=19$m=19456,t=2,p=1$...

try:
    ph.verify(hashed, "minha-senha")
    print("senha válida")
except VerifyMismatchError:
    print("senha inválida")
// Go — golang.org/x/crypto/argon2
package main

import (
    "crypto/rand"
    "encoding/base64"
    "golang.org/x/crypto/argon2"
)

func HashPassword(password string) (string, error) {
    salt := make([]byte, 16)
    rand.Read(salt)

    hash := argon2.IDKey(
        []byte(password),
        salt,
        2,      // time
        19456,  // memory (KiB)
        1,      // threads
        32,     // keyLen
    )

    encoded := base64.RawStdEncoding.EncodeToString(hash)
    return "$argon2id$" + encoded, nil
}

Argon2i, Argon2d ou Argon2id?

  • Argon2d: maximiza resistência a GPU/ASIC (acesso sequencial à memória dependente de dados), mas vulnerável a side-channel attacks. Não use para senhas.
  • Argon2i: resistente a side-channel (acesso à memória independente dos dados), mas menos resistente a GPU. Bom para derivação de chaves em contextos adversariais.
  • Argon2id: híbrido. Usa Argon2i no primeiro passe e Argon2d nos seguintes. Melhor dos dois mundos. Use este.

scrypt: a alternativa memory-hard mais antiga

scrypt (Colin Percival, 2009) também é memory-hard e aparece em muitos projetos. É a base do Litecoin e de vários derivadores de chave.

import hashlib
import os

password = b"minha-senha"
salt = os.urandom(16)

# N=2^15, r=8, p=1 — parâmetros razoáveis para login
key = hashlib.scrypt(password, salt=salt, n=32768, r=8, p=1, dklen=32)

Para novos projetos, prefira Argon2id. scrypt tem uma interface menos ergonômica e os parâmetros são mais difíceis de calibrar corretamente. Se você já usa scrypt em produção, não há motivo urgente para migrar.


PBKDF2: quando você não tem outra opção

PBKDF2 ainda aparece em sistemas legados, FIPS compliance e algumas linguagens onde Argon2 não tem suporte nativo. É CPU-bound (não memory-hard), o que o torna inferir aos anteriores, mas ainda é vastamente superior a SHA não iterado.

OWASP recomenda PBKDF2-HMAC-SHA256 com 600.000 iterações se você precisar de FIPS compliance.

import hashlib
import os

password = b"minha-senha"
salt = os.urandom(16)

key = hashlib.pbkdf2_hmac('sha256', password, salt, iterations=600000)

Qual algoritmo usar — decisão direta

Cenário Escolha
Sistema novo, sem restrições Argon2id
FIPS compliance obrigatório PBKDF2-HMAC-SHA256 (600k iter)
Linguagem sem Argon2 nativo bcrypt (cost 12+)
Sistema legado em produção Mantenha o atual, migre no próximo login

Não migre hashes existentes em batch. A migração correta é silenciosa: quando um usuário autentica com sucesso, re-hashe a senha com o novo algoritmo e sobrescreva o hash antigo. Em algumas semanas a maior parte dos usuários ativos estará migrada.


O que o LinkedIn errou — e o que você aprende com isso

O vazamento de 2012 não foi um problema de criptografia sofisticada. Foi um problema de escolha de algoritmo.

SHA-1 sem salt significa:

  1. Dois usuários com a mesma senha têm o mesmo hash → identificação imediata
  2. Nenhum custo computacional para o atacante além de iterar uma lista de hashes conhecidos
  3. Rainbow tables pré-computadas tornam o ataque instantâneo para senhas comuns

A solução não exige matemática avançada. Exige usar a ferramenta certa: bcrypt, scrypt ou Argon2id, com salt automático (todos os três incluem o salt no output), cost factor calibrado para o hardware de produção.

Segurança de senha não é sobre algoritmos obscuros. É sobre ser intencionalmente lento.


Nota: o conteúdo editorial acabou aqui. O que vem abaixo é uma indicação de ferramenta relacionada ao tema do post.


Ferramenta relacionada

Para gerar e verificar hashes bcrypt sem instalar nada, visualizar a estrutura interna do hash (versão, cost factor, salt, hash) e calibrar o cost factor ideal com um benchmark local que roda no seu browser, use o Gerador Bcrypt do Quick Tools — nenhum dado sai do navegador.

RD
Autor
Rafael Duarte
Desenvolvedor backend com passagem por fintech e SaaS B2B — trabalhou em times que escalaram APIs de zero a milhões de requisições. Carrega cicatrizes de produção suficientes para ter opiniões fortes sobre ferramentas, padrões e decisões de arquitetura. Não é acadêmico: leu a RFC do UUID quando precisou escolher entre v4 e v7 para uma tabela de alta escrita.
Ver perfil