Como se guardam passwords (e como não se guardam)
Uma base de dados acaba por sair. Acontece a empresas com equipas de segurança a sério, e vai acontecer ao seu projeto de fim de semana. A pergunta que interessa não é se alguém a vai ler — é o que essa pessoa encontra lá dentro.
1. O que nunca se faz
Guardar em texto simples. Não precisa de explicação.
Guardar um MD5 ou um SHA-256. Este é o erro subtil: parece um hash, e é. O problema é serem rápidos — foram feitos para verificar ficheiros a milhões por segundo, e é isso que uma placa gráfica faz a tentar adivinhar passwords.
Inventar. Um SHA-256 com o nome do utilizador à mistura continua a ser um SHA-256.
2. O que se faz
Usa-se um algoritmo desenhado para ser lento e caro: argon2id, bcrypt ou scrypt. A lentidão é a funcionalidade — verificar uma password uma vez custa uns milissegundos, e tentar mil milhões custa séculos.
import bcrypt from 'bcrypt';
// Ao registar. O 12 é o custo: cada +1 duplica o tempo.
const hash = await bcrypt.hash(password, 12);
// Ao entrar
const certa = await bcrypt.compare(password, hash);3. O sal já lá está
Muita gente guarda um campo salt à parte. Não é preciso: o bcrypt gera um sal único por password e escreve-o dentro do próprio hash.
$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewKyFG5rGtRq3K.C
│ │ └── sal (22 caracteres) └── hash
│ └── custo
└── versão do algoritmoÉ o sal que impede que duas pessoas com a mesma password fiquem com o mesmo hash — e é isso que torna inútil uma tabela de hashes pré-calculados.
4. E o resto
Nunca registar a password. Nem em logs de erro, nem no corpo de um pedido que vá parar a uma ferramenta de monitorização.
O bcrypt corta aos 72 bytes. Passwords muito longas ficam truncadas em silêncio; o
argon2idnão tem esse limite.Comparar com a função da biblioteca, nunca com
===sobre o hash calculado.
E a melhor opção continua a ser não guardar password nenhuma: delegar a autenticação num fornecedor de identidade tira o problema todo da sua base de dados.