Por que senhas exigem tratamento especial
Qualquer outro segredo no seu sistema pode ser rotacionado: API keys, tokens, certificados. Senhas são diferentes. Você nunca as armazena em um formato que possa ser regenerado, os usuários as reutilizam em diversos sites e, no momento em que seu banco de dados vaza, o invasor tem todo o tempo do mundo para tentar adivinhá-las offline.
O hashing de senhas é a prática de armazenar uma transformação unidirecional da senha em vez da própria senha. Quando um usuário faz login, você aplica a mesma transformação e compara os resultados. Você nunca descobre a senha, e nem quem quer que roube a tabela.
Este não é o lugar para tentar ser “criativo”. As regras são simples e bem estabelecidas, e quebrá-las é o que torna as violações de segurança catastróficas.
Hashing não é criptografia
A criptografia é reversível. Se você criptografar senhas, a chave ficará armazenada em algum lugar do seu sistema, e qualquer pessoa que a obtenha — seja por meio de um vazamento, um backup, uma variável de ambiente mal configurada ou um insider — poderá ler todas as senhas de uma vez. Não existe cenário onde você precise da senha original, portanto, a reversibilidade é puro risco.
Uma função de hash recebe uma entrada e produz um digest de comprimento fixo, e não há como reverter esse processo na prática. Para senhas, um hash simples ainda não é suficiente: funções rápidas como SHA-256 são projetadas para serem velozes, que é exatamente o que um atacante deseja. Uma GPU pode testar bilhões de candidatos por segundo.
Você precisa de uma função que seja deliberadamente lenta e memory-hard, para que a tentativa de adivinhação seja cara em larga escala.
Salt: a mesma senha, um hash diferente
Sem um salt, dois usuários com a senha hunter2 produzem o mesmo digest. Um invasor pode pré-computar uma tabela gigante de senhas comuns e seus hashes — uma rainbow table — e buscar correspondências instantaneamente. Eles também conseguem notar rapidamente quais contas compartilham a mesma senha.
Um salt é um valor aleatório único gerado por senha e armazenado junto ao digest. Ele não é secreto; sua única função é tornar cada hash único. Agora, o invasor precisa atacar cada conta separadamente, tornando a pré-computação inútil.
Bibliotecas modernas geram o salt para você e o codificam, junto com os parâmetros, em uma única string. Você armazena essa string exatamente como ela é.
Escolhendo um algoritmo
Existem três funções que valem a pena conhecer. Todas são adaptativas: o custo pode ser aumentado conforme o hardware evolui.
Argon2id
O vencedor da Password Hashing Competition de 2015 e a recomendação atual. Ele é memory-hard, o que significa que um atacante precisa investir em memória além de processamento, reduzindo a vantagem de GPUs e ASICs. O Argon2id equilibra a resistência a ataques de canal lateral (side-channel) e ataques de GPU, sendo a escolha padrão ideal para novos sistemas.
scrypt
Padronizado na RFC 7914 e também memory-hard, o scrypt é uma escolha sólida quando o Argon2 não está disponível. Ele expõe custo, tamanho do bloco e paralelismo, permitindo ajustes, mas seus parâmetros são mais fáceis de configurar incorretamente do que os do Argon2.
bcrypt
Lançado em 1999 e ainda onipresente, o bcrypt é bem compreendido e testado em batalha. Seu fator de custo dobra o trabalho a cada incremento. A principal ressalva é que ele trunca a entrada em 72 bytes, portanto, frases de senha muito longas são cortadas silenciosamente — faça um pré-hash se isso for importante para você.
Independentemente da sua escolha, nunca utilize MD5, SHA-1 ou SHA-256 puro. Eles são as ferramentas erradas, e empilhar rodadas de hashing sobre eles é um esquema caseiro em que ninguém deve confiar.
Ajustando o fator de trabalho (work factor)
O custo deve equilibrar duas pressões. Se for muito baixo, o hash fica vulnerável a ataques de força bruta; se for muito alto, um pico de tentativas de login pode se tornar um vetor de negação de serviço (DoS) contra a sua própria CPU.
Ajuste-o para que um único hash leve aproximadamente 100 a 500 milissegundos no seu hardware de produção. Meça o endpoint de login sob carga realista, e não apenas em um loop de benchmark, e defina o número com base nesses dados. Depois, revise-o periodicamente: os mesmos parâmetros tornam-se mais fracos a cada ano conforme o hardware evolui.
// Argon2id parameters (memory in KiB, iterations, parallelism)
await hash(password, { memoryCost: 19456, timeCost: 2, parallelism: 1 });
Armazenando e verificando
O registro e o login são os únicos dois lugares onde você manipula uma senha.
import { hash, verify } from "@node-rs/argon2";
// Registration: hash before the password ever reaches the database.
const digest = await hash(password);
await db.users.insert({ email, passwordHash: digest });
// Login: verify against the stored digest.
const user = await db.users.findByEmail(email);
const ok = user && (await verify(user.passwordHash, password));
if (!ok) return res.status(401).json({ error: "invalid_credentials" });
Retorne o mesmo erro, independentemente de o e-mail ou a senha estarem incorretos. Uma mensagem como “usuário não encontrado” oferece a um invasor uma maneira de enumerar contas.
Ataques de timing e comparação segura
Comparar dois hashes com === interrompe o processo no primeiro byte diferente. Portanto, o tempo gasto depende de quanto do palpite estava correto, e um atacante paciente pode medir isso para reconstruir um valor. É um canal estreito, mas a comparação de senhas é exatamente onde isso importa.
Use a função de verificação da biblioteca, que realiza a comparação em tempo constante. Nunca compare os digests por conta própria e nunca compare a senha em texto puro.
Atualizando hashes com o tempo
Você mudará de ideia: o custo de processamento aumentará ou você migrará do bcrypt para o Argon2. Como você nunca armazenou o texto simples, a única maneira de gerar um novo hash é durante o login, quando a senha está brevemente na memória.
Armazene o algoritmo e os parâmetros com cada digest — a string codificada já faz isso — e verifique-os a cada login bem-sucedido:
if (await verify(stored, password)) {
if (needsRehash(stored)) {
await db.users.update(user.id, { passwordHash: await hash(password) });
}
// continue the login
}
Usuários que nunca mais fizerem login manterão o hash antigo, o que não é um problema; eles continuam protegidos. Com o tempo, as contas ativas migram naturalmente.
Política de senhas na prática
O algoritmo protege você após uma violação. A política reduz a chance de que ela ocorra.
- Prefira comprimento em vez de complexidade. Uma frase-senha (passphrase) é melhor que
P@ssw0rd!. - Verifique novas senhas contra listas de vazamentos conhecidos, como a API de k-anonymity do Have I Been Pwned.
- Não imponha rotações arbitrárias. A expiração forçada leva a padrões de
Summer2026!. - Aplique rate-limit em tentativas de login e reset, e adicione exponential backoff ou lockout.
- Use um fluxo de reset genérico com tokens de uso único e com data de expiração, e nunca envie senhas por e-mail.
Melhores práticas
- Faça o hash com Argon2id, scrypt ou bcrypt — nunca utilize um hash rápido de propósito geral.
- Deixe que a biblioteca gere e armazene o salt.
- Ajuste o custo para entre 100 e 500 ms e aumente esse valor com o tempo.
- Verifique utilizando a função de tempo constante da biblioteca.
- Faça o rehash após um login bem-sucedido quando os parâmetros mudarem.
- Retorne erros genéricos e aplique rate-limit nas tentativas.
- Mantenha o hashing no servidor; nunca no cliente.
Erros comuns
- Criptografar senhas ou armazená-las em texto simples “temporariamente”.
- Usar SHA-256 ou MD5 com poucas rodadas e chamar isso de hashing.
- Utilizar um único salt global ou reutilizar o mesmo salt entre contas.
- Comparar hashes com
==ou===. - Registrar senhas em logs ou incluí-las em relatórios de erro.
- Limitar o comprimento da senha a um valor baixo, o que prejudica o uso de passphrases.
- Esquecer de atualizar o fator de custo por anos.
Próximos passos
As credenciais são apenas a primeira etapa. Uma vez que o usuário seja verificado, você precisará de um lugar para manter esse estado: leia o guia de Session Auth para sessões baseadas em cookies, ou o guia de JWT para tokens stateless. Quando você preferir não lidar com senhas, o OAuth 2.0 e o OpenID Connect delegam o login a um provedor de identidade.