"Como exatamente devo armazenar as senhas dos usuários no banco de dados?" — todo construtor esbarra nessa pergunta uma vez. A resposta é clara. Aqui está a forma segura de armazená-las, em ordem, sem passos de ataque.
Por que texto puro, criptografia e hashes crus falham
Presuma que o banco de dados vai vazar algum dia. Quando vazar, o dano difere muito conforme o método de armazenamento.
- Texto puro: toda senha é exposta no instante em que vaza. Pior, a reutilização de senhas encadeia a invasão para os outros serviços do usuário. O pior caso.
- Criptografia (reversível): a chave a reverte — então se a chave vazar junto, você volta ao texto puro. Uma senha nunca precisa ser lida de volta, então ser reversível não traz benefício nenhum.
- Hash cru (MD5/SHA-256): um hash rápido permite ao atacante testar tentativas rapidamente. Senhas comuns caem para rainbow tables e força bruta.
Quatro estágios até o armazenamento seguro
É mais rápido entender adicionando uma correção por vez a um método fraco.
A percepção-chave: um salt e um hash lento fazem trabalhos diferentes. Um salt derrota a pré-computação (rainbow tables) e a quebra em massa por reutilização. Um hash lento reduz a força bruta a uma taxa impraticável. Você precisa dos dois antes de estar seguro (→ o que é hashing).
Na prática: o que fazer agora
Use Argon2id (ou bcrypt)
Para sistemas novos, faça do Argon2id a primeira escolha — ele também pode exigir memória, o que resiste à força bruta por GPU. Se você prefere uma implementação bem testada, o bcrypt é mais do que suficiente na prática. Use a implementação da biblioteca padrão como está para qualquer um dos dois.
Deixe a biblioteca gerar o salt
bcrypt/Argon2 geram um salt por usuário e o embutem dentro da string de hash. Você não mantém uma coluna de salt separada. Montar à mão MD5(salt + senha) é o clássico que não se deve fazer.
Ajuste o custo para o seu ambiente
Defina o fator de custo do bcrypt, ou memória/iterações/paralelismo do Argon2, o mais alto que conseguir enquanto um login legítimo ainda parecer instantâneo. Conforme os servidores ficam mais rápidos, os atacantes também — revisite e eleve anualmente.
Verifique com a função de comparação padrão
No login, compare a entrada com o hash armazenado usando a função de verificação da biblioteca (a maioria usa uma comparação de tempo constante, segura contra timing). Não escreva sua própria igualdade de strings.
Migre hashes fracos refazendo o hash no login
Se você já armazena MD5/SHA-256, no momento em que um usuário faz login com sucesso, refaça o hash com o novo esquema e salve. Para usuários que não fizeram login, você pode reforçá-los enquanto isso aplicando o bcrypt sobre o hash existente (um hash em camadas).
Erros comuns vs. a implementação correta
Erro comum
- Criptografar senhas para armazenar (chave vaza → acabou)
- Armazenar
MD5(senha)ouSHA-256(senha)diretamente - Usar um único salt compartilhado para todos os usuários
- Montar à mão
hash(salt + senha)
Implementação correta
- Hash de mão única com Argon2id / bcrypt
- Um salt por usuário (a biblioteca o adiciona automaticamente)
- Custo o mais alto que parecer OK, elevado periodicamente
- Usar as funções padrão de hash/verificação como estão
Hash vs criptografia vs codificação
Esses três costumam ser confundidos, mas diferem completamente em poder ou não ser revertidos. Só o hash é aceitável para armazenar senhas.
| Hash | Criptografia | Codificação (Base64 etc.) | |
|---|---|---|---|
| Reversível? | Não (mão única) | Sim, com a chave | Sim, por qualquer pessoa |
| Chave | Nenhuma | Obrigatória | Nenhuma |
| Uso típico | Armazenar senhas, detectar adulteração | Manter dados ou tráfego confidenciais | Mudar o formato dos dados |
| Para armazenar senhas | Use (salt + hash lento) | Não use | Não use (equivale a texto puro) |
Configurações recomendadas (OWASP)
As configurações mínimas do OWASP Password Storage Cheat Sheet. Trate-as como piso e eleve-as conforme o seu servidor.
| Algoritmo | Configuração mínima | Quando usar |
|---|---|---|
| Argon2id | 19 MiB de memória, 2 iterações, paralelismo 1 | Primeira escolha para sistemas novos |
| scrypt | N=2^17 (128 MiB), r=8, p=1 | Quando o Argon2id não está disponível |
| bcrypt | Custo 10 ou mais. Entrada limitada a 72 bytes | Sistemas existentes, histórico comprovado |
| PBKDF2-HMAC-SHA256 | 600.000 iterações ou mais | Quando a conformidade com o FIPS 140 é exigida |
Verifique também o que a sua plataforma já traz. O password_hash() do PHP usa bcrypt por padrão (PASSWORD_DEFAULT), e o PHP 8.4 elevou o custo padrão de 10 para 12. O Django usa PBKDF2 por padrão; instale django[argon2] e coloque o Argon2 em primeiro lugar em PASSWORD_HASHERS para trocar. O Django refaz automaticamente o hash das senhas guardadas com um algoritmo antigo quando o usuário faz login.
Como verificar o seu próprio serviço
Normalmente dá para saber o esquema pelo começo da string de hash armazenada. Olhe algumas linhas num ambiente que você controla, sem copiar dados de produção para outro lugar.
| Como o hash se parece | Esquema | Ação |
|---|---|---|
$argon2id$v=19$m=…,t=…,p=…$ | Argon2id | Confira se m, t e p atendem ao mínimo da OWASP |
$2y$12$… / $2b$10$… | bcrypt (o número é o custo) | Eleve se estiver abaixo de 10 |
pbkdf2_sha256$1000000$… | PBKDF2 (o número é a quantidade de iterações) | Confira se é pelo menos 600.000 |
| 32 caracteres hexadecimais | Muito provavelmente MD5 | Migre refazendo o hash no login |
| 40 / 64 caracteres hexadecimais | Muito provavelmente SHA-1 / SHA-256 | Igual ao anterior |
| A string de login tal como foi digitada | Texto puro | Migre primeiro. Avalie o impacto se tiver vazado |
Verifique também o seguinte:
- As senhas digitadas não ficam registradas em logs do app ou do servidor web, em relatórios de erro nem em ferramentas de analytics
- Se você usa bcrypt, já decidiu como tratar entradas com mais de 72 bytes
- Backups e cópias do banco de dados feitas para testes contêm só o mesmo tipo de hash
Exemplo real: o armazenamento em texto puro ampliou o dano
No incidente do Takufile-bin (2019), um acesso não autorizado a um servidor vazou cerca de 4,8 milhões de registros, e as senhas de login entre eles estavam guardadas em texto puro. Se estivessem guardadas com um hash lento com salt, o risco de as senhas vazadas serem usadas diretamente em outros serviços teria sido bem menor. Mesmo que você não consiga impedir toda invasão, a forma de armazenar senhas muda o tamanho do dano de um vazamento.
A visão deste site: para armazenar senhas, use a implementação padrão comprovada
Armazenamento de senha não é lugar de ser criativo. Usar as implementações padrão do Argon2/bcrypt, amplamente testadas, é o mais seguro e o mais fácil de operar. Nossa posição: não inove aqui. Gaste seu esforço, em vez disso, em reduzir a dependência de senhas em si — autenticação multifator (MFA) minuciosa e uma eventual migração para passkeys (sem senha). Nem o hash mais forte salva uma senha fraca e reutilizada.
Leia a seguir
- Glossário: o que é hashing de senha / o que é um salt
- Fundamentos: guardando senhas do jeito certo (lado do usuário) / escolhendo um gerenciador de senhas
- Defesa: escolhendo MFA do jeito certo (reduza a dependência de senhas)
- Glossário: o que é uma passkey (rumo ao sem senha)
- Caso: o vazamento de dados do Takufile-bin (2019) (armazenamento de senhas em texto puro que ampliou o dano)
Fontes
- OWASP, "Password Storage Cheat Sheet": cheatsheetseries.owasp.org
- NIST, "SP 800-63B-4" (agosto de 2025), armazenamento de senhas (salt e hash): pages.nist.gov
- RFC 9106, "Argon2 Memory-Hard Function for Password Hashing and Proof-of-Work Applications": rfc-editor.org
- Manual do PHP, "password_hash": php.net
- Documentação do Django, "Password management in Django" (5.2): docs.djangoproject.com