Pular para o conteúdo
Categoria: Segurança da Informação13 min de leitura

Segurança de senhas: hashing, salt e bcrypt

Por Lucas Andrade ·

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.

Segurança de senhas: hashing, salt e bcrypt

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..." ?  -> autentica

Três propriedades tornam uma função de hash adequada como impressão digital:

  • Determinismo: a mesma entrada sempre produz a mesma saída, senão a comparação no login não funcionaria.
  • Unidirecionalidade: é computacionalmente inviável reverter o hash para descobrir a entrada.
  • Efeito avalanche: mudar um único caractere da senha muda radicalmente o hash, sem padrão previsível.

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:

  1. Senhas iguais geram hashes iguais. Se dois usuários usam "123456", a tabela mostra o mesmo hash duas vezes. Um atacante percebe padrões e ataca os mais comuns primeiro.
  2. Rainbow tables. São tabelas enormes pré-computadas que mapeiam senhas comuns aos seus hashes. Com elas, o atacante simplesmente procura o hash roubado e descobre a senha em segundos, sem recalcular nada.

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 salt

O salt não é secreto — ele fica guardado junto ao hash, em texto. Sua função não é esconder, e sim garantir unicidade. Com salt:

  • Dois usuários com a mesma senha terão hashes diferentes, porque os salts diferem.
  • Rainbow tables genéricas se tornam inúteis, pois teriam que ser pré-computadas para cada salt possível — algo inviável.

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:

  • Lentidão proposital: ele é projetado para levar um tempo perceptível (milissegundos) por cálculo. Para um login isso é irrelevante; para um atacante testando bilhões de senhas, é proibitivo.
  • Fator de custo (work factor): um parâmetro que você aumenta para tornar o cálculo mais caro. Conforme o hardware evolui, você sobe o fator e mantém a proteção — daí "adaptável ao futuro". Cada incremento de 1 no fator dobra o trabalho.
  • Salt embutido: o bcrypt gera e armazena o salt automaticamente dentro do próprio hash resultante.

Um hash bcrypt tem esta cara, com tudo embutido:

$2b$12$Gx3o2k8sZ1qE0a/example.saltAndHashCombinedHere1234567890
   |   |  |
   |   |  +-- salt + hash
   |   +----- fator de custo (12)
   +--------- identificador do algoritmo

Esse 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

  • Argon2id: melhor escolha para projetos novos, se a sua plataforma oferece uma implementação confiável.
  • scrypt: boa opção memory-hard, especialmente onde já está disponível.
  • bcrypt: maduro, onipresente, sem o limite de memória mas com o limite de 72 bytes; uma escolha perfeitamente segura quando bem configurado.
  • SHA-256, MD5, SHA-1 puros: nunca, jamais, para senhas. São rápidos demais.

Boas práticas de armazenamento de senhas

Reunindo tudo, eis a receita confiável:

  • Nunca armazene senha em texto puro nem cifrada de forma reversível.
  • Use uma função de hash de senha dedicada: Argon2id, scrypt ou bcrypt — nunca SHA-256 puro.
  • Use salt único por usuário (as funções acima já fazem isso por você).
  • Calibre o fator de custo para que o hashing leve algo em torno de algumas centenas de milissegundos no seu hardware. Um custo que demora 250 ms é invisível no login mas devastador para força bruta em escala.
  • Reavalie o custo periodicamente e re-hashe senhas no próximo login quando subir o fator.
  • Compare em tempo constante, usando a função de verificação da biblioteca, para não vazar informação por timing.
  • Imponha políticas de senha razoáveis: prefira comprimento mínimo generoso e bloqueio de senhas vazadas, em vez de regras complexas que irritam o usuário.

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 desejado

Assim 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:

  • Ataques de força bruta no login: mitigue com rate limiting) e bloqueio temporário após tentativas falhas.
  • Credential stuffing: atacantes testam senhas vazadas de outros sites; ofereça autenticação multifator (MFA) e bloqueie senhas conhecidamente vazadas.
  • Phishing: nenhum hash protege contra o usuário entregar a senha a um site falso; eduque e use MFA, idealmente com fatores resistentes a phishing como chaves de segurança.
  • Timing attacks: comparações de hash que terminam cedo ao primeiro byte diferente vazam informação; por isso a comparação em tempo constante.

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.

Referências

  • Provos, N., Mazières, D. (1999). A Future-Adaptable Password Scheme. USENIX Annual Technical Conference.
  • OWASP Foundation (2021). OWASP Application Security Verification Standard 4.0. OWASP.
  • NIST (2017). Special Publication 800-63B: Digital Identity Guidelines — Authentication and Lifecycle Management. NIST.

Leituras relacionadas

Nenhum comentário ainda

Seja o primeiro a comentar.

Deixe seu comentário

Entre com sua conta Canverly para comentar. Você pode usar a mesma conta em qualquer site da rede.

Entrar com Canverly