Segurança de senhas: hashing, salt e bcrypt
Entenda por que nunca armazenar senhas em texto puro e como usar hashing, salt e funções lentas como bcrypt e Argon2 para proteger as credenciais dos seus usuários.

Vazamentos de banco de dados são inevitáveis o suficiente para que você deva projetar seu sistema assumindo que um dia eles vão acontecer. A pergunta não é se alguém vai roubar sua tabela de usuários, mas o que ele encontra quando isso ocorre. Se as senhas estiverem bem protegidas, o estrago é contido; se estiverem mal guardadas, é catástrofe. Neste artigo você vai entender, passo a passo, como armazenar senhas do jeito certo.
O pecado capital: senhas em texto puro
Armazenar senhas em texto puro (plaintext) significa guardar no banco exatamente o que o usuário digitou. Se o banco vaza, o atacante tem, de imediato, todas as senhas de todos os usuários. Pior: como muita gente reutiliza senhas, esse vazamento compromete as contas dessas pessoas em outros serviços também.
Uma variação igualmente ruim é cifrar a senha de forma reversível. Parece melhor, mas se o sistema consegue descifrar para comparar no login, então a chave de descifragem está em algum lugar acessível — e quem rouba o banco provavelmente rouba a chave também. A conclusão da indústria é clara: senhas não devem ser recuperáveis, nem por você.
Um sintoma prático para diagnosticar um serviço inseguro: se ao clicar em "esqueci minha senha" o sistema te envia a senha antiga por e-mail, ele a armazena de forma recuperável — um sinal claro de má prática. Serviços corretos só conseguem oferecer um redefinir, nunca um relembrar, porque genuinamente não sabem qual era a sua senha.
A solução correta passa por hashing, um conceito que vale revisar em O que é criptografia? Simétrica, assimétrica e hashing para entender por que ele é unidirecional.
Hashing: guardar a impressão digital, não a senha
A ideia central é nunca guardar a senha, mas sim um hash dela — uma transformação unidirecional da qual não se recupera a entrada original. No cadastro, você calcula o hash e guarda só ele. No login, você calcula o hash da senha digitada e compara com o armazenado.
Cadastro: hash("MinhaSenha!") -> guarda "a3f9c2..."
Login: hash(digitada) == "a3f9c2..." ? -> autenticaTrês propriedades tornam uma função de hash adequada como impressão digital:
Como o hash é unidirecional, mesmo quem rouba o banco não consegue, em tese, voltar à senha original. Em tese — porque, sozinho, o hashing tem duas fraquezas sérias que precisamos resolver: hashes idênticos e velocidade demais.
Por que hashing puro não basta: rainbow tables
Funções de hash são determinísticas: a mesma entrada produz sempre a mesma saída. Isso cria dois problemas práticos:
Hashes de propósito geral, como o SHA-256, foram projetados para serem rápidos — ótimo para verificar integridade de arquivos, péssimo para senhas, porque rapidez ajuda quem faz força bruta. Para dimensionar: hardware moderno com GPUs consegue testar bilhões de hashes SHA-256 por segundo. Uma senha de oito caracteres aleatórios, que parece razoável, cai em horas sob esse ataque. Precisamos de duas defesas adicionais.
Salt: tornando cada hash único
O salt é um valor aleatório, único por usuário, que se combina com a senha antes do hashing:
hash(senha + salt_unico) -> armazena hash E saltO salt não é secreto — ele fica guardado junto ao hash, em texto. Sua função não é esconder, e sim garantir unicidade. Com salt:
O salt obriga o atacante a quebrar cada senha individualmente, em vez de atacar todas de uma vez. É uma defesa barata e obrigatória.
Um conceito relacionado, e frequentemente confundido, é o pepper. Diferente do salt, o pepper é um valor secreto, igual para todos os usuários, guardado fora do banco (em um gerenciador de segredos ou variável de ambiente). A ideia é que, se só o banco vazar mas o pepper não, os hashes ficam protegidos por uma camada extra. O pepper é opcional e complementar; o salt é obrigatório e não negociável.
Funções lentas e o bcrypt
Mesmo com salt, se a função de hash for muito rápida, um atacante com hardware potente testa bilhões de combinações por segundo contra um único usuário. A defesa é usar funções deliberadamente lentas e ajustáveis.
É aqui que entra o bcrypt, baseado no trabalho de Provos e Mazières (1999) sobre um esquema de senha "adaptável ao futuro". A genialidade do bcrypt está em três pontos:
Um hash bcrypt tem esta cara, com tudo embutido:
$2b$12$Gx3o2k8sZ1qE0a/example.saltAndHashCombinedHere1234567890
| | |
| | +-- salt + hash
| +----- fator de custo (12)
+--------- identificador do algoritmoEsse formato auto-contido é prático: tudo de que você precisa para verificar uma senha — algoritmo, custo, salt e hash — viaja num único campo de texto. Por isso, na tabela do banco, você guarda apenas uma coluna password_hash; não precisa de uma coluna separada para o salt.
Como usar bcrypt na prática
A regra de ouro é não implemente você mesmo — use a biblioteca consolidada da sua linguagem. Um exemplo em Python com a biblioteca bcrypt:
import bcrypt
# Cadastro: gerar o hash (salt é criado automaticamente)
def criar_hash(senha: str) -> bytes:
return bcrypt.hashpw(senha.encode("utf-8"), bcrypt.gensalt(rounds=12))
# Login: comparar de forma segura
def verificar(senha: str, hash_armazenado: bytes) -> bool:
return bcrypt.checkpw(senha.encode("utf-8"), hash_armazenado)Repare que checkpw faz a comparação internamente, em tempo constante, e extrai o salt e o custo do próprio hash armazenado. Você não recalcula nem manipula o salt manualmente — a biblioteca cuida disso. Uma pegadinha histórica do bcrypt: ele trunca a entrada em 72 bytes, então senhas muito longas têm os caracteres extras ignorados. Para acomodar senhas longas com segurança, alguns sistemas aplicam um SHA-256 antes (e então passam o resultado ao bcrypt), ou simplesmente escolhem Argon2.
Argon2 e scrypt: as alternativas modernas
Além do bcrypt, alternativas modernas e recomendadas incluem scrypt e Argon2 — este último vencedor da Password Hashing Competition em 2015 e considerado hoje a escolha preferencial para projetos novos. A vantagem do Argon2 (e do scrypt) é serem memory-hard: além de lentos, exigem bastante memória por cálculo. Isso ataca diretamente o ponto forte do adversário: GPUs e ASICs têm muitos núcleos, mas memória é cara e limitada, então a paralelização massiva deixa de compensar.
O Argon2 tem três parâmetros para calibrar: custo de tempo (iterações), custo de memória (quanta RAM por hash) e paralelismo (quantas threads). A variante recomendada para senhas é a Argon2id, que combina resistência a ataques de canal lateral e a trade-offs de tempo-memória. O padrão de verificação OWASP ASVS (OWASP Foundation, 2021) exige justamente o uso de funções de hash de senha resistentes a força bruta com salt único, e o OWASP Password Storage Cheat Sheet recomenda Argon2id como primeira opção, com bcrypt como alternativa sólida.
Como escolher entre eles
Boas práticas de armazenamento de senhas
Reunindo tudo, eis a receita confiável:
Re-hashing transparente: subindo o custo sem incomodar
Como você atualiza o fator de custo de milhões de usuários existentes sem pedir que todos troquem a senha? Com re-hashing oportunístico: no momento em que o usuário faz login (única hora em que você tem a senha em texto na memória), você verifica se o hash armazenado usa um custo defasado e, se sim, recalcula com o novo custo:
def login(senha: str, hash_atual: bytes) -> bytes | None:
if not bcrypt.checkpw(senha.encode(), hash_atual):
return None # senha errada
# custo embutido está abaixo do desejado? re-hashe.
custo_atual = int(hash_atual.split(b"$")[2])
if custo_atual < 12:
return bcrypt.hashpw(senha.encode(), bcrypt.gensalt(rounds=12))
return hash_atual # já está no custo desejadoAssim a sua base se moderniza gradualmente, sem fricção para o usuário e sem que você precise da senha em texto fora do momento do login.
Essas práticas se conectam diretamente ao processo de login, que faz parte do tema mais amplo de Autenticação vs autorização: qual a diferença?. Vale lembrar também que, uma vez autenticado, o usuário normalmente recebe um O que é JWT (JSON Web Token)? para manter a sessão — ou seja, o hashing de senha é só o primeiro elo de uma cadeia.
O ciclo completo de uma autenticação por senha
Vale enxergar onde o hashing se encaixa no fluxo inteiro, porque a senha bem guardada é só uma peça. Um cadastro e um login seguros percorrem mais ou menos estes passos:
Cadastro
1. Cliente envia senha sobre HTTPS (nunca em texto sobre HTTP).
2. Servidor valida força mínima e checa contra listas de senhas vazadas.
3. Servidor calcula hash com Argon2id/bcrypt (salt automático).
4. Guarda apenas o hash; descarta a senha em texto da memória.
Login
1. Cliente envia senha sobre HTTPS.
2. Servidor busca o hash do usuário pelo identificador.
3. Verifica em tempo constante (checkpw/verify).
4. Aplica rate limiting e bloqueio após N falhas.
5. Em sucesso, opcionalmente re-hashe se o custo estiver defasado.
6. Emite uma sessão ou token e segue para a autorização.Repare em dois pontos fáceis de esquecer. Primeiro, a senha precisa trafegar sempre sobre HTTPS: de nada adianta o hash perfeito no banco se a senha viaja em texto puro pela rede e pode ser capturada. Segundo, mesmo quando o usuário não existe, o servidor deve gastar um tempo equivalente ao de uma verificação real (calculando um hash descartável), para não revelar, por timing, se um e-mail está ou não cadastrado — uma forma sutil de enumeração de usuários.
Defesas além do hash
Proteger a senha guardada é essencial, mas pensar como atacante revela ameaças que o hash sozinho não cobre. Exercícios de Threat modeling: como pensar como um atacante ajudam a enxergar vetores como:
Vale destacar um ponto da experiência do usuário que virou consenso de segurança: as recomendações modernas (NIST, 2017) desencorajam regras de composição obrigatória ("precisa de maiúscula, número e símbolo") e a troca periódica forçada, porque levam a senhas previsíveis e anotadas em post-its. O que funciona melhor é incentivar frases longas, permitir todo o teclado, verificar a senha contra listas de vazamentos e não impor trocas sem motivo. Comprimento vence complexidade.
Quando suas APIs expõem endpoints de autenticação, todas essas defesas se somam às práticas descritas em Segurança de APIs: protegendo seus endpoints, formando camadas que se reforçam.
Perguntas frequentes
Posso usar SHA-256 se eu adicionar um salt? Não. O salt resolve as rainbow tables, mas não a velocidade. SHA-256 continua rápido demais; com salt e tudo, um atacante ainda testa bilhões de palpites por segundo contra cada hash. O problema da velocidade só se resolve com uma função deliberadamente lenta como bcrypt ou Argon2.
Preciso guardar o salt numa coluna separada? Não, se você usa bcrypt, scrypt ou Argon2 — todos embutem o salt (e os parâmetros) no próprio string do hash. Basta uma coluna para o hash completo.
Qual fator de custo devo usar? Não existe número universal, porque depende do seu hardware. A regra é calibrar para que cada hash leve algo entre 200 e 500 ms na sua máquina de produção. Meça, ajuste e reavalie a cada ano ou dois conforme o hardware evolui.
Hashing de senha me dispensa de MFA? De forma alguma. O hash protege as senhas caso o banco vaze. Ele não faz nada contra phishing, credential stuffing ou uma senha fraca que o usuário escolheu. MFA é uma camada independente e altamente recomendada.
Conclusão
Armazenar senhas com segurança se resume a três decisões corretas: hashing unidirecional em vez de texto puro, salt único por usuário para frustrar rainbow tables, e uma função lenta e ajustável como Argon2id, scrypt ou bcrypt para tornar a força bruta inviável. Some a isso re-hashing transparente para evoluir o custo, comparação em tempo constante, rate limiting, MFA, políticas focadas em comprimento e uma postura de "assuma que o banco vai vazar", e você transforma um possível desastre em um incidente controlável. Senha bem guardada é, no fim, respeito pelos seus usuários.