Password hashing moderno: argon2, bcrypt, peppers
⏱ 10 min·⭐ 45 XP
Pré-requisitos (0/1)0%
- ⬜🎫 JWT, Paseto ou sessions: quando cada um(Security Engineering)
Recomendamos completar os pré-requisitos antes de seguir, mas nada te impede de continuar.
Argon2id em Node/TS
import { hash, verify, argon2id } from '@node-rs/argon2'; // ou 'argon2' (native)
const PEPPER = process.env.PASSWORD_PEPPER!; // 32+ random bytes, nunca no git
async function hashPassword(plaintext: string): Promise<string> {
// Pepper concatenado (convenção: antes do hash)
const peppered = plaintext + PEPPER;
return await hash(peppered, {
algorithm: argon2id,
timeCost: 2, // t
memoryCost: 19456, // m em KiB (19 MiB — OWASP 2024)
parallelism: 1, // p
});
}
async function verifyPassword(plaintext: string, hashStr: string): Promise<boolean> {
const peppered = plaintext + PEPPER;
return await verify(hashStr, peppered);
}Timing-safe compare e clock attack
Nunca use === pra comparar hashes ou tokens. String compare retorna early no primeiro byte diferente — atacante mede tempo e infere byte-a-byte.
import { timingSafeEqual } from 'node:crypto';
// ❌
if (userHash === storedHash) allow(); // timing leak
// ✅
const a = Buffer.from(userHash, 'hex');
const b = Buffer.from(storedHash, 'hex');
if (a.length === b.length && timingSafeEqual(a, b)) allow();Parâmetros ao longo do tempo
| Ano | Argon2id recomendado | bcrypt work factor |
|---|---|---|
| 2018 | t=3, m=12MiB | 10 |
| 2021 | t=2, m=15MiB | 11 |
| 2024 | t=2, m=19MiB | 12 |
| 2027 (projetado) | t=2, m=32MiB | 13 |
Ano2018
Argon2id recomendadot=3, m=12MiB
bcrypt work factor10
Ano2021
Argon2id recomendadot=2, m=15MiB
bcrypt work factor11
Ano2024
Argon2id recomendadot=2, m=19MiB
bcrypt work factor12
Ano2027 (projetado)
Argon2id recomendadot=2, m=32MiB
bcrypt work factor13
💡
Hardware fica mais rápido; parâmetros sobem. Armazene + no hash (argon2 já faz: ). No login, detecte hash desatualizado e rehash silenciosamente.
Quiz rápido
Por que funções de resumo criptográfico rápidas são inadequadas para senha?
Rehash silencioso no login
async function login(email: string, password: string) {
const user = await db.user.findUnique({ where: { email } });
if (!user) return null;
const ok = await verifyPassword(password, user.passwordHash);
if (!ok) return null;
// Detecta hash desatualizado e rehash
const currentParams = extractParams(user.passwordHash);
if (needsRehash(currentParams, TARGET_PARAMS)) {
const newHash = await hashPassword(password);
await db.user.update({ where: { id: user.id }, data: { passwordHash: newHash } });
}
return user;
}Invisível pro user, migração gradual: cada login atualiza hash pro padrão novo. Em 3–6 meses, base inteira está em parâmetros modernos sem reset forçado.
Perguntas frequentes
❓ Por que resumo criptográfico simples não serve para senha?
Porque ele é rápido por projeto, e rapidez é o que o atacante quer: bilhões de tentativas por segundo em hardware comum. Função de derivação para senha é lenta e ajustável de propósito, com custo de memória — é o que torna a tentativa em massa inviável.
❓ Qual função usar hoje?
A recomendada atualmente é a de derivação com custo de memória configurável, na variante indicada para senha; a alternativa consolidada anterior continua aceitável com parâmetro adequado. O que não é aceitável é resumo simples, com ou sem sal.
❓ Sal e pimenta são a mesma coisa?
Não. O sal é único por senha, guardado junto e serve para impedir tabela pré-computada. A pimenta é um segredo do servidor, igual para todos, guardado fora do banco — ela protege quando só o banco vaza. São defesas contra cenários diferentes.
Fixando
Quiz rápido
Por que a comparação do resumo deve ser feita em tempo constante?
Quiz rápido
O que fazer quando os parâmetros recomendados de custo aumentam com o tempo?
Terminou de ler?
Marcar como concluído registra o XP, mantém sua sequência e coloca 3 cartas deste módulo na fila de revisão espaçada.
Próximos passos sugeridos
Temas deste módulo
Discussão
Carregando comentários…