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.
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:
- Dois usuários com a mesma senha têm o mesmo hash → identificação imediata
- Nenhum custo computacional para o atacante além de iterar uma lista de hashes conhecidos
- 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.