Usar Argon2 no Node.js é uma das formas mais seguras de armazenar senhas em aplicações modernas. Em vez de guardar a senha original ou aplicar um hash rápido, como SHA-256, a aplicação utiliza uma função deliberadamente lenta e intensiva em memória. Isso aumenta muito o custo de ataques offline quando um banco de dados é vazado.
O objetivo não é tornar o login perceptivelmente lento, mas fazer cada tentativa de quebra consumir memória e processamento suficientes para dificultar milhões de palpites por segundo. A implementação correta também exige salt aleatório, parâmetros ajustados ao servidor, limites de requisição, comparação segura, migração de hashes antigos e uma política de recuperação de conta.
Neste guia, você aprenderá a usar Argon2id, criar hashes, verificar senhas, ajustar memória e tempo, armazenar o resultado, migrar usuários, aplicar pepper, limitar tentativas e testar o fluxo de autenticação.
Por que senhas não devem ser criptografadas?
Criptografia é reversível quando a chave está disponível. Para autenticação comum, o servidor não precisa recuperar a senha original. Ele precisa apenas confirmar se a senha informada produz o mesmo resultado esperado.
Por isso, senhas devem ser processadas por uma função de derivação resistente a ataques, e não por uma cifra reversível. A referência Password Storage Cheat Sheet da OWASP reúne recomendações práticas. O RFC 9106 descreve o Argon2.
Por que SHA-256 não basta?
SHA-256 foi projetado para ser rápido. Essa característica é excelente para integridade de arquivos, assinaturas e outras tarefas, mas é ruim para senhas. Um atacante com hashes vazados pode testar enormes quantidades de combinações usando GPUs e hardware especializado.
Argon2 permite configurar custo de memória, número de iterações e paralelismo. Assim, cada tentativa exige recursos significativos.
Argon2id
Existem variantes do Argon2. Para armazenamento de senhas, Argon2id é normalmente a escolha preferida porque combina resistência a ataques de canal lateral com proteção contra hardware paralelo.
Não invente uma variante própria. Use uma biblioteca mantida que implemente o formato completo, incluindo salt, parâmetros e versão.
Instalando a biblioteca
npm install argon2Bibliotecas nativas podem exigir ferramentas de compilação em algumas plataformas. Em ambientes Docker, confirme que a imagem possui as dependências necessárias ou que o pacote oferece binários compatíveis.
Criando um hash
import argon2 from 'argon2';
export async function hashPassword(password) {
return argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 19_456,
timeCost: 2,
parallelism: 1
});
}Os valores acima são apenas um ponto de partida. O ajuste ideal depende da capacidade do servidor, do volume de logins e do risco da aplicação.
Formato armazenado
O resultado costuma conter algoritmo, versão, parâmetros, salt e hash:
$argon2id$v=19$m=19456,t=2,p=1$...$...Armazene a string completa em uma coluna suficientemente grande. Não separe o salt manualmente, a menos que a biblioteca exija.
Verificando a senha
export async function verifyPassword(hash, password) {
try {
return await argon2.verify(hash, password);
} catch {
return false;
}
}A biblioteca interpreta os parâmetros incorporados no hash e realiza a comparação corretamente.
Fluxo de login
const user = await users.findByEmail(normalizedEmail);
const hash = user?.passwordHash
?? DUMMY_PASSWORD_HASH;
const valid = await verifyPassword(hash, password);
if (!user || !valid) {
throw new InvalidCredentialsError();
}Usar um hash fictício quando o usuário não existe ajuda a reduzir diferenças de tempo entre “e-mail inexistente” e “senha incorreta”.
Mensagem de erro genérica
{
"code": "INVALID_CREDENTIALS",
"message": "E-mail ou senha inválidos"
}Não revele qual campo falhou. Isso dificulta enumeração de contas.
Normalização da senha
Evite transformar silenciosamente a senha. Espaços, letras maiúsculas e caracteres Unicode podem ser intencionais. A política deve ser definida na criação da conta e permanecer consistente.
Para e-mail, uma normalização controlada pode ser adequada. Para senha, preserve exatamente o valor informado, depois de validar tamanho e encoding.
Limite de tamanho
Senhas enormes podem causar consumo excessivo de memória e processamento. Defina um limite de bytes razoável, por exemplo 1.024 bytes, sem impor um máximo artificialmente pequeno.
const passwordBytes = Buffer.byteLength(password, 'utf8');
if (passwordBytes < 12 || passwordBytes > 1024) {
throw new ValidationError('Senha inválida');
}Comprimento mínimo
Prefira senhas longas e permita frases. Regras como exigir símbolo, número e letra maiúscula podem gerar padrões previsíveis. Um comprimento mínimo adequado, bloqueio de senhas comprometidas e suporte a gerenciadores de senha costumam ser mais úteis.
Salt
O salt deve ser único e aleatório para cada hash. Bibliotecas de Argon2 normalmente o geram automaticamente. Não use e-mail, ID do usuário ou timestamp como salt.
O salt não é secreto. Ele existe para impedir que hashes iguais apareçam para usuários com a mesma senha e para inviabilizar tabelas pré-calculadas.
Pepper
Um pepper é um segredo adicional mantido fora do banco. Ele pode ser combinado à senha antes do Argon2 ou aplicado com HMAC ao resultado, conforme o desenho escolhido.
const prepared = createHmac('sha256', pepper)
.update(password, 'utf8')
.digest();
const hash = await argon2.hash(prepared, options);O pepper deve ficar em um gerenciador de segredos, KMS ou Secret da plataforma. Se ele for perdido, as senhas não poderão ser verificadas.
Rotação do pepper
Rotacionar o pepper exige estratégia. Uma opção é manter versões e atualizar o hash após um login válido. Outra é forçar redefinição de senha em um incidente crítico.
Nunca coloque o pepper no mesmo banco que os hashes.
Ajustando parâmetros
Meça o tempo de hash na infraestrutura real. Um alvo comum é tornar a operação suficientemente cara sem causar uma experiência ruim. Considere picos de login, autoscaling, memória por processo e concorrência.
const startedAt = performance.now();
await hashPassword('senha-de-teste');
console.log(performance.now() - startedAt);Custo de memória
O parâmetro de memória dificulta ataques em GPU, mas também afeta seu servidor. Se cada hash usa dezenas de megabytes e cem logins ocorrem ao mesmo tempo, o consumo pode crescer rapidamente.
Time cost
O número de iterações aumenta o trabalho realizado. Ele deve ser ajustado junto ao custo de memória, e não isoladamente.
Paralelismo
Mais paralelismo pode aproveitar múltiplos núcleos, mas aumenta concorrência. Em servidores com muitas requisições, um valor baixo costuma oferecer comportamento previsível.
Fila ou limite de concorrência
Não permita milhares de hashes simultâneos. Use rate limiting e, quando necessário, um semáforo interno para limitar operações concorrentes.
await passwordHashSemaphore.run(async () => {
return hashPassword(password);
});Rate limiting no login
Limite tentativas por conta, IP confiável e dispositivo. Consulte Rate Limiting no Node.js.
Evite bloqueio permanente simples, pois um atacante pode negar acesso ao usuário. Use atraso progressivo, desafios adicionais e alertas.
Rehash automático
Parâmetros recomendados evoluem. Depois de verificar uma senha, confira se o hash precisa ser atualizado:
if (argon2.needsRehash(user.passwordHash, currentOptions)) {
const newHash = await hashPassword(password);
await users.updatePasswordHash(user.id, newHash);
}Faça a atualização em segundo plano controlado ou dentro de uma transação curta.
Migração de bcrypt
Não é possível converter um hash bcrypt em Argon2 sem a senha original. Migre no próximo login:
- Identifique o algoritmo pelo prefixo.
- Verifique com a biblioteca antiga.
- Se válido, gere Argon2id.
- Substitua o hash.
- Registre a migração sem incluir a senha.
Usuários inativos
Contas que nunca voltam a entrar permanecem com o hash antigo. Para uma mudança crítica, force redefinição de senha após determinado prazo.
Cadastro
Antes de criar o hash, valide política, verifique senha comprometida quando houver serviço adequado e confirme que a requisição está protegida por HTTPS.
Não registre a senha em logs, analytics, rastreamento de erro ou eventos de auditoria.
Recuperação de senha
O fluxo de recuperação deve usar token aleatório, de uso único e curta duração. Depois da redefinição, revogue sessões e refresh tokens.
Consulte Sessões Seguras no Node.js e Refresh Tokens no Node.js.
MFA e passkeys
Hash forte protege a senha armazenada, mas não impede phishing. Ofereça MFA e, para maior resistência, passkeys. Veja Passkeys no Node.js e TOTP no Node.js.
Banco de dados
ALTER TABLE users
ADD COLUMN password_hash TEXT,
ADD COLUMN password_changed_at TIMESTAMPTZ,
ADD COLUMN password_algorithm TEXT;O campo de algoritmo pode ajudar na migração, embora o prefixo do hash já carregue essa informação.
Permissões do banco
A aplicação precisa ler hashes para verificar login, portanto controle estritamente acesso à tabela. Ferramentas administrativas e réplicas analíticas não devem receber essa coluna sem necessidade.
Logs
Registre resultado, duração, algoritmo e necessidade de rehash, mas nunca senha, hash completo ou pepper.
Para logging estruturado, consulte Logs com Pino no Node.js.
Métricas
Monitore tempo de hash, falhas, fila, uso de memória, rehashes e bloqueios. Não use e-mail ou user ID como label de alta cardinalidade.
Teste unitário
test('verifica a senha correta', async () => {
const hash = await hashPassword('frase longa e segura');
assert.equal(
await verifyPassword(hash, 'frase longa e segura'),
true
);
assert.equal(
await verifyPassword(hash, 'senha errada'),
false
);
});Teste de salt
Gere dois hashes da mesma senha e confirme que são diferentes, mas ambos verificam corretamente.
Teste de migração
Crie uma fixture bcrypt, autentique e confirme que o banco termina com Argon2id.
Teste de rehash
Use parâmetros antigos em uma fixture e confirme atualização depois do login válido.
Teste de carga
Meça o comportamento com múltiplos logins concorrentes. Observe memória, CPU, latência p95 e event loop.
Erros comuns
- Usar SHA-256: o hash é rápido demais.
- Salt fixo: senhas iguais produzem resultados relacionados.
- Parâmetros copiados sem benchmark: o servidor pode ficar vulnerável ou instável.
- Senha em logs: a credencial é exposta.
- Pepper no banco: a camada adicional perde valor.
- Sem rate limiting: o endpoint vira ferramenta de ataque online.
- Não migrar hashes antigos: contas permanecem com proteção inferior.
Boas práticas
- Use Argon2id.
- Deixe a biblioteca gerar o salt.
- Ajuste parâmetros com benchmark.
- Limite concorrência e tentativas.
- Armazene a string completa do hash.
- Mantenha pepper fora do banco.
- Atualize hashes após login.
- Revogue sessões após redefinição.
- Nunca registre senhas.
- Ofereça MFA ou passkeys.
Conclusão
Usar Argon2 no Node.js reduz significativamente o impacto de um vazamento de hashes. O algoritmo força cada tentativa de quebra a consumir memória e processamento, dificultando ataques em grande escala.
A segurança depende do conjunto completo: Argon2id, salt aleatório, parâmetros medidos, pepper protegido, rate limiting e migração contínua. Com recuperação segura, revogação de sessões e autenticação resistente a phishing, a senha deixa de ser um ponto único de falha.




