Hash de senha com bcrypt no Node: salvar e comparar
Por que senha nunca vai em texto puro nem em MD5, como gerar o hash com bcrypt, escolher o custo e comparar no login sem entregar pista nenhuma.
Senha não se guarda. Guarda-se o hash dela. No Node isso são duas funções —
bcrypt.hash na hora do cadastro e bcrypt.compare na hora do login — e uma
decisão no meio que quase ninguém explica direito: o custo.
Os exemplos todos são da PetVida, uma clínica veterinária cujo portal deixa o tutor entrar para ver a carteirinha de vacina do bichinho. Tem cadastro, tem login, e tem uma tabela de tutores que um dia pode vazar.
O termo correto é função de derivação de senha: o bcrypt mistura senha, salt e custo para produzir um hash lento de verificar. Em palavras simples, o sistema guarda uma impressão comparável, não uma versão escondida da senha.
A ficha guarda a impressão digital, não a mão
Uma clínica consegue comparar a impressão digital apresentada com a ficha sem
reconstruir a mão da pessoa a partir do papel. O bcrypt segue essa direção:
hash cria o registro e compare processa a tentativa para conferir se ela
combina. O salt faz senhas iguais gerarem registros diferentes; o custo aumenta
o trabalho de cada tentativa.
Faça duas verificações antes de avançar: gere dois hashes para a mesma senha e preveja se serão iguais; depois compare a senha correta com ambos. A saída deve mostrar hashes diferentes e dois resultados verdadeiros. É a prova pequena do papel do salt e de como o login funciona sem descriptografar nada.
Criptografia tem volta: você cifra com uma chave e decifra com ela de novo. Hash não tem. É uma conta que transforma qualquer texto num resultado de tamanho fixo, e não existe conta inversa que devolva o texto original.
Isso é exatamente o que você quer para senha. Repare no que acontece sem hash nenhum, com a tabela de tutores em texto puro:
import { DatabaseSync } from 'node:sqlite';
const db = new DatabaseSync(':memory:');
db.exec(`CREATE TABLE tutores (id INTEGER PRIMARY KEY, email TEXT, senha TEXT)`);
db.exec(`INSERT INTO tutores (email, senha) VALUES
('ana@petvida.com.br', 'consultaVet2026'),
('bruno@petvida.com.br', 'bruno123'),
('carla@petvida.com.br', 'consultaVet2026')`);
console.table(db.prepare('SELECT * FROM tutores').all());Um SELECT e o atacante tem tudo. E não só o portal da clínica: como muita
gente repete senha, ele acabou de ganhar o e-mail e o banco dessas pessoas
também. Quem tem acesso ao banco por qualquer caminho — um backup mal guardado,
uma rota que não soube validar a entrada, uma
credencial exposta — lê a coluna inteira.
Com hash, o que está guardado não serve para logar em lugar nenhum. Nem no seu próprio sistema, porque o servidor não compara o que recebeu com o que está salvo: ele refaz a conta e compara o resultado.
Por que MD5 e SHA-256 não servem para senha
A primeira ideia de quem descobre hash é usar o que já vem no Node:
import { createHash } from 'node:crypto';
const sha = (s) => createHash('sha256').update(s).digest('hex');
console.log('sha256:', sha('consultaVet2026'));
console.log('sha256:', sha('consultaVet2026'));
const inicio = performance.now();
const N = 1_000_000;
for (let i = 0; i < N; i++) sha('senha' + i);
const ms = performance.now() - inicio;
console.log(`${N} hashes SHA-256 em ${ms.toFixed(0)} ms`);
console.log(`${Math.round(N / (ms / 1000)).toLocaleString('pt-BR')} hashes por segundo`);Dois problemas aparecem nessa saída, e os dois são fatais.
O primeiro: a mesma senha dá sempre o mesmo hash. Quem rouba o banco não
precisa quebrar nada — basta procurar aquele 54877e59… numa lista pronta de
senhas já calculadas. É a chamada rainbow table, e ela existe há décadas para
MD5 e SHA.
O segundo é a velocidade. Quase 3 milhões de hashes por segundo numa máquina comum, num processo só, sem placa de vídeo. SHA-256 foi projetado para ser rápido — é o que se quer de um hash de arquivo. Para senha, velocidade é a característica errada: ela é a velocidade do atacante testando chutes.
Salt: a mesma senha, dois hashes diferentes
O bcrypt resolve o primeiro problema sorteando um valor aleatório para cada hash — o salt — e misturando ele na conta. Compare as duas listas, com as mesmas três pessoas da PetVida:
import bcrypt from 'bcrypt';
import { createHash } from 'node:crypto';
const senhas = [
['ana@petvida.com.br', 'consultaVet2026'],
['bruno@petvida.com.br', 'bruno123'],
['carla@petvida.com.br', 'consultaVet2026'],
];
for (const [email, senha] of senhas) {
console.log(email.padEnd(22), createHash('sha256').update(senha).digest('hex').slice(0, 24));
}
console.log('');
for (const [email, senha] of senhas) {
console.log(email.padEnd(22), await bcrypt.hash(senha, 12));
}ana@petvida.com.br $2b$12$mifJNWkKHLYjkCmYNOis/Ohy0rviMck5IGq98cetvH0Oh7FfsfwbS bruno@petvida.com.br $2b$12$xQ8ntYW0aF3klKNH74H4ZeS57nN6KQCDpR.Lmwbs3vi6Hgq1KaSeC carla@petvida.com.br $2b$12$knshs8ppXji9rWM5ONhXJuX.jl6qOxqrqfwqNNgCQT3MvngEq1I5e
No SHA-256, Ana e Carla têm o hash idêntico — o vazamento entrega de graça que as duas usam a mesma senha. No bcrypt, os três hashes são diferentes, e o de Ana seria diferente de novo se você rodasse o script mais uma vez.
A consequência prática é grande: como cada linha tem o próprio salt, uma rainbow table pré-calculada não serve para nada, e quebrar mil contas custa mil vezes o trabalho de quebrar uma.
bcrypt.hash e o custo que você escolhe
Instale o pacote:
npm i bcryptfound 0 vulnerabilities
E o cadastro vira uma linha. O segundo argumento é o custo — as rounds:
import bcrypt from 'bcrypt';
const senha = 'consultaVet2026';
const hashA = await bcrypt.hash(senha, 12);
const hashB = await bcrypt.hash(senha, 12);
console.log(hashA);
console.log(hashB);
console.log('são iguais?', hashA === hashB);
console.log('compare com hashA:', await bcrypt.compare(senha, hashA));
console.log('compare com hashB:', await bcrypt.compare(senha, hashB));Guarde essa saída, porque ela é a coisa mais estranha do bcrypt para quem está
começando: a mesma senha gerou duas strings diferentes, e o compare diz
true para as duas. Não tem contradição. O salt de cada hash está guardado
dentro do próprio hash, e o compare sabe pescar ele de lá antes de refazer a
conta. Já já a gente abre a string e vê onde ele mora.
Quanto cada round custa na sua máquina
O custo é um expoente, não um multiplicador: cada round a mais dobra o
trabalho. 12 significa 2¹² = 4096 iterações internas. Isso é abstrato demais
para decidir alguma coisa, então medi na máquina daqui:
import bcrypt from 'bcrypt';
const senha = 'consultaVet2026';
await bcrypt.hash(senha, 8); // aquecimento, descartado
for (const custo of [8, 10, 12, 14]) {
const tempos = [];
for (let i = 0; i < 5; i++) {
const inicio = performance.now();
await bcrypt.hash(senha, custo);
tempos.push(performance.now() - inicio);
}
const media = tempos.reduce((a, b) => a + b, 0) / tempos.length;
console.log(`custo ${custo}: ${media.toFixed(0)} ms por hash (${(1000 / media).toFixed(1)} hashes/s)`);
}MacBook com Apple M4 Pro, Node 24.16.0, bcrypt 6.0.0, média de 5 execuções depois de uma de aquecimento. Repeti a bateria três vezes e os números variaram menos de 5%.
Olhe a coluna do tempo: 14 → 55 → 217 → 874. É o dobro a cada round, exatamente como a conta promete. E olhe a última coluna, que é a que interessa para segurança: no custo 14 esta máquina faz 1,1 hash por segundo, contra os 2,9 milhões do SHA-256. É a mesma máquina. A diferença toda é o algoritmo ter sido feito para custar caro.
| custo | tempo por hash | tentativas por segundo | onde usar |
|---|---|---|---|
| 8 | 14 ms | ~73 | só em teste automatizado, para a suíte não demorar |
| 10 | 55 ms | ~18 | mínimo aceitável em produção hoje |
| 12 | 217 ms | ~4,6 | ponto de partida deste laboratório; meça no seu servidor |
| 14 | 874 ms | ~1,1 | dado muito sensível, e só se a sua máquina aguentar |
A régua não é “o maior número que couber”. É esta: escolha o maior custo cujo tempo o seu login ainda tolera, contando que ele roda uma vez por autenticação. Entre 200 e 300 ms é uma faixa confortável — o tutor da PetVida não percebe, e o atacante perde seis ordens de grandeza.
E repita a medição no servidor de verdade. Custo 12 no seu MacBook pode virar custo 12 a 900 ms num container pequeno de nuvem, e aí o login fica lento para todo mundo.
hashSync trava a fila inteira da API
Esses 217 ms são CPU ocupada. E aqui mora a pegadinha que separa código de
tutorial de código de produção: existe bcrypt.hashSync, ele parece mais
simples, e ele para o
event loop do Node — ou seja, para todas as
outras requisições ao mesmo tempo.
Dá para medir. O truque é bater um ponto a cada 10 ms e olhar o maior buraco entre duas batidas:
import bcrypt from 'bcrypt';
function baterPonto() {
let ultimo = performance.now();
let maiorBuraco = 0;
const t = setInterval(() => {
const agora = performance.now();
maiorBuraco = Math.max(maiorBuraco, agora - ultimo);
ultimo = agora;
}, 10);
return () => (clearInterval(t), maiorBuraco);
}
const respirar = () => new Promise((r) => setTimeout(r, 60));
let parar = baterPonto();
await respirar();
for (let i = 0; i < 3; i++) bcrypt.hashSync('consultaVet2026', 12);
await respirar();
console.log('3x hashSync :', parar().toFixed(0), 'ms de event loop travado');
parar = baterPonto();
await respirar();
await Promise.all([0, 1, 2].map(() => bcrypt.hash('consultaVet2026', 12)));
await respirar();
console.log('3x hash :', parar().toFixed(0), 'ms de event loop travado');Três cadastros simultâneos com a versão síncrona deixaram a API surda por 662 ms. Com a versão assíncrona, 12 ms — e os três hashes ainda assim terminaram juntos (215 ms para os três, contra 194 ms para um sozinho), porque o bcrypt nativo joga o trabalho para a pool de threads do Node e o event loop continua atendendo o resto.
Anatomia do hash: o $2b$12$ carrega o próprio salt
Aqueles 60 caracteres não são uma bolha aleatória. São quatro campos separados
por $, e você pode abrir eles na mão:
import bcrypt from 'bcrypt';
const hash = await bcrypt.hash('consultaVet2026', 12);
const [, versao, custo, resto] = hash.split('$');
console.log('hash completo :', hash);
console.log('tamanho :', hash.length, 'caracteres');
console.log('versão :', versao);
console.log('custo :', custo);
console.log('salt :', resto.slice(0, 22));
console.log('digest :', resto.slice(22));
console.log('getRounds :', bcrypt.getRounds(hash));| campo | valor no exemplo | o que é |
|---|---|---|
| versão | 2b |
a variante do bcrypt; 2b é a atual e corrigiu um bug de 2014 |
| custo | 12 |
o expoente das rounds, gravado junto |
| salt | 22 caracteres | o aleatório sorteado para este hash |
| digest | 31 caracteres | o resultado da conta em si |
É por isso que você não cria uma coluna salt no banco. Uma coluna de 60
caracteres guarda tudo: o algoritmo sabe se reconstituir a partir da própria
string. E é por isso que o compare funciona sem você passar nada além da
senha e do hash.
bcrypt.compare no login, e nunca ===
O login não desfaz o hash. Ele lê o salt e o custo de dentro da string guardada, refaz a conta com a senha que chegou e vê se o digest bate:
import bcrypt from 'bcrypt';
const guardado = await bcrypt.hash('consultaVet2026', 12);
console.log('senha certa :', await bcrypt.compare('consultaVet2026', guardado));
console.log('senha errada :', await bcrypt.compare('consultaVet2025', guardado));
console.log('caixa trocada :', await bcrypt.compare('Consultavet2026', guardado));
const hashDaTentativa = await bcrypt.hash('consultaVet2026', 12);
console.log('=== com hash novo :', hashDaTentativa === guardado);
console.log('=== com a senha :', 'consultaVet2026' === guardado);As duas últimas linhas são o erro de raciocínio mais comum de quem chega aqui.
Hashear a senha digitada e comparar com === parece a mesma coisa e nunca
funciona: o hash novo tem um salt novo, então dá diferente mesmo quando a
senha está certa. Só o compare sabe reaproveitar o salt antigo.
O login inteiro: cadastro, sessão e curl
Juntando tudo numa API Express — a mesma estrutura de rota que você já viu no Express, agora com as duas funções no lugar certo:
import express from 'express';
import bcrypt from 'bcrypt';
const app = express();
app.use(express.json());
const CUSTO = 12;
const tutores = []; // no lugar do banco, para o exemplo caber aqui
app.post('/tutores', async (req, res) => {
const { email, senha } = req.body;
if (!senha || senha.length < 8) {
return res.status(400).json({ erro: 'senha precisa de 8 caracteres ou mais' });
}
const senhaHash = await bcrypt.hash(senha, CUSTO);
tutores.push({ id: tutores.length + 1, email, senhaHash });
res.status(201).json({ id: tutores.length, email });
});
app.post('/sessoes', async (req, res) => {
const { email, senha } = req.body;
const tutor = tutores.find((t) => t.email === email);
const confere = tutor ? await bcrypt.compare(senha, tutor.senhaHash) : false;
if (!confere) {
return res.status(401).json({ erro: 'e-mail ou senha inválidos' });
}
res.json({ id: tutor.id, email: tutor.email });
});
app.listen(3401, () => console.log('PetVida ouvindo em http://localhost:3401'));Repare em três decisões. A senha em texto puro só existe dentro da variável
senha, e some no fim da requisição — ela nunca é gravada nem logada. A
resposta do cadastro devolve id e email, nunca o campo senhaHash. E a
mensagem de erro do login é uma só, e-mail ou senha inválidos, mesmo quando
o problema é só o e-mail: dizer “esse e-mail não existe” entrega ao atacante a
lista de quem é cliente da clínica.
Subindo e batendo com curl:
# sed -n '1p;$p' deixa só a linha de status e o corpo da resposta
curl -s -i -X POST localhost:3401/tutores -H 'Content-Type: application/json' \
-d '{"email":"ana@petvida.com.br","senha":"consultaVet2026"}' | sed -n '1p;$p'
curl -s -i -X POST localhost:3401/sessoes -H 'Content-Type: application/json' \
-d '{"email":"ana@petvida.com.br","senha":"consultaVet2026"}' | sed -n '1p;$p'
curl -s -i -X POST localhost:3401/sessoes -H 'Content-Type: application/json' \
-d '{"email":"ana@petvida.com.br","senha":"consultaVet2025"}' | sed -n '1p;$p'O 401 é o status certo aqui: a credencial não confere. Se você tem dúvida de
qual código devolver em cada caso, a lição de
métodos HTTP e status code tem a tabela
inteira. E aquela validação do senha.length < 8 escrita na mão fica bem
melhor com validação de entrada com Zod.
O erro que derruba a rota de login
Aquele tutor ? ... : false do exemplo não é firula. Tire ele e a rota morre
no primeiro e-mail que não existe no banco:
import bcrypt from 'bcrypt';
const tutores = [
{ email: 'ana@petvida.com.br', senhaHash: await bcrypt.hash('consultaVet2026', 12) },
];
const email = 'bruno@petvida.com.br'; // tutor que não existe
const tutor = tutores.find((t) => t.email === email);
const confere = await bcrypt.compare('consultaVet2026', tutor?.senhaHash);
console.log(confere);Error: data and hash arguments required at Object.compare (/private/tmp/petvida/node_modules/bcrypt/bcrypt.js:204:17) at /private/tmp/petvida/node_modules/bcrypt/promises.js:26:12 at new Promise (<anonymous>) at Object.promise (/private/tmp/petvida/node_modules/bcrypt/promises.js:17:12) at Object.compare (/private/tmp/petvida/node_modules/bcrypt/bcrypt.js:200:25) at file:///private/tmp/petvida/login.mjs:10:30
Node.js v24.16.0
Leia a mensagem literalmente: data and hash arguments required. O bcrypt
recebeu undefined no lugar do hash e se recusou a trabalhar — com razão, já
que devolver false calado esconderia um bug. O undefined veio do find que
não achou ninguém.
Em produção, o rastro é pior: o find vira uma consulta ao banco, a promise
rejeita, e o tutor recebe um 500 genérico em vez do 401. Você vai debugar
“erro no login” achando que é o bcrypt, quando o problema é um e-mail digitado
errado.
A correção é a da rota lá em cima: só chame o compare quando existir hash
para comparar — ou, melhor ainda, use o hash fantasma da última seção.
Subir o custo sem quebrar quem já tem conta
Como o custo está gravado dentro do hash, hash antigo continua funcionando depois que você aumenta o padrão do projeto. Dá para migrar aos poucos, no login de cada pessoa:
import bcrypt from 'bcrypt';
const CUSTO_ATUAL = 12;
const antigo = await bcrypt.hash('consultaVet2026', 8); // gravado quando o padrão era 8
console.log('hash antigo :', antigo);
console.log('ainda valida:', await bcrypt.compare('consultaVet2026', antigo));
console.log('custo dele :', bcrypt.getRounds(antigo));
if (bcrypt.getRounds(antigo) < CUSTO_ATUAL) {
const novo = await bcrypt.hash('consultaVet2026', CUSTO_ATUAL);
console.log('regravado :', novo, '→ custo', bcrypt.getRounds(novo));
}Coloque esse bloco depois do compare bem-sucedido na rota de sessão, onde
você tem a senha em texto puro na mão pela última vez. Quem loga, migra sozinho.
Quem nunca mais voltar continua com o hash antigo, que é ruim, mas é melhor do
que forçar redefinição de senha para a base inteira.
Deixe o custo numa variável de ambiente: assim o teste roda com 8 e a produção com 12 sem trocar uma linha de código.
bcrypt, argon2 e o scrypt que já vem no node:crypto
bcrypt não é a única opção, e desde 2015 nem é a mais recomendada. Medi as três na mesma máquina:
import bcrypt from 'bcrypt';
import argon2 from 'argon2';
import { scrypt as scryptCb } from 'node:crypto';
import { promisify } from 'node:util';
const scrypt = promisify(scryptCb);
const senha = 'consultaVet2026';
async function medir(rotulo, fn) {
await fn(); // aquecimento
const t = [];
for (let i = 0; i < 5; i++) {
const inicio = performance.now();
await fn();
t.push(performance.now() - inicio);
}
console.log(`${rotulo.padEnd(28)} ${(t.reduce((a, b) => a + b, 0) / t.length).toFixed(0).padStart(4)} ms`);
}
await medir('bcrypt custo 12', () => bcrypt.hash(senha, 12));
await medir('argon2id (padrão da lib)', () => argon2.hash(senha));
await medir('scrypt N=16384 (node:crypto)', () => scrypt(senha, 'sal-de-16-bytes', 64));
console.log('\nargon2 :', await argon2.hash(senha));argon2 : $argon2id$v=19$m=65536,p=4,t=3$yEyp2ojfoSCQ1bBq5eYeLA$EYw7moGdS62spvOWh9Hd28cNSJ/fTcTUrTAXn8ei5V4
Cuidado com a leitura preguiçosa desses números. O argon2 ficou oito vezes mais
rápido que o bcrypt, mas não é “dez vezes mais fraco”: olhe o m=65536 no
hash dele — são 64 MB de memória por hash. Ele foi feito para ser caro em
memória, não em tempo, justamente porque placa de vídeo tem muito núcleo e
pouca memória por núcleo. É a defesa que o bcrypt, de 1999, não tem.
| algoritmo | precisa instalar | custa caro em | quando eu escolho |
|---|---|---|---|
bcrypt |
sim, pacote nativo | tempo de CPU | projeto que já usa, ou time que precisa de algo simples e testado |
argon2id |
sim, pacote nativo | memória e tempo | projeto novo, se o servidor tiver RAM sobrando |
scrypt |
não, vem no node:crypto |
memória e tempo | quando não posso adicionar dependência nativa |
Minha recomendação, assinada: em projeto novo, argon2id; em projeto que já
roda com bcrypt, fique no bcrypt e suba o custo. Trocar de algoritmo por moda,
com base de usuários viva, dá muito mais trabalho do que rende. E se
dependência nativa for problema no seu deploy, o scrypt do node:crypto
resolve sem instalar nada — só lembre que ali você guarda o salt e os
parâmetros por conta própria, porque não existe a string pronta do bcrypt.
O que ainda falta proteger depois do hash
Hash bom não é login seguro. Repare no que a rota da PetVida ainda entrega, só medindo o tempo de resposta de dois logins que falharam igual:
#!/bin/bash
# 5 logins que falham em cada caso, medindo só o tempo total da resposta
medir() {
local soma=0
for i in 1 2 3 4 5; do
t=$(curl -s -o /dev/null -w '%{time_total}' -X POST localhost:3401/sessoes \
-H 'Content-Type: application/json' \
-d "{\"email\":\"$1\",\"senha\":\"chuteQualquer\"}")
soma=$(node -p "$soma + $t")
done
node -p "'$1: ' + (($soma/5)*1000).toFixed(0) + ' ms em média'"
}
medir "ana@petvida.com.br" # existe, senha errada
medir "bruno@petvida.com.br" # não existeAs duas respostas são 401 com o mesmo texto, mas o relógio conta a verdade:
quando o e-mail existe, o servidor gasta 206 ms rodando o compare; quando não
existe, responde em 1 ms. Com isso um atacante descobre, em minutos, quais
e-mails de uma lista são clientes da clínica. A mensagem igual não adiantou
nada.
A correção é comparar sempre, exista o tutor ou não, contra um hash fantasma gerado no boot:
const HASH_FANTASMA = await bcrypt.hash('nenhum-tutor-usa-esta-senha', CUSTO);
app.post('/sessoes', async (req, res) => {
const { email, senha } = req.body;
const tutor = tutores.find((t) => t.email === email);
const confere = await bcrypt.compare(senha, tutor?.senhaHash ?? HASH_FANTASMA);
if (!tutor || !confere) return res.status(401).json({ erro: 'e-mail ou senha inválidos' });
res.json({ id: tutor.id, email: tutor.email });
});Rodando o mesmo script contra a versão corrigida:
201 contra 197 ms: o relógio parou de contar segredo. De quebra, esse desenho
mata o data and hash arguments required da seção anterior, porque o compare
sempre recebe um hash válido.
Ainda faltam três coisas que este artigo não cobre e que nenhum algoritmo de
hash resolve: limite de tentativas por e-mail e por IP, para o atacante não
poder chutar à vontade; política de senha que rejeite as óbvias, já que
bruno123 cai em qualquer wordlist mesmo com custo 14; e HTTPS, porque a
senha viaja em texto puro entre o navegador e a sua API.
O que vem depois
O hash resolve “esta pessoa é quem diz ser” uma vez, no login. Falta a segunda
metade: manter isso de pé nas próximas requisições, sem pedir a senha de novo a
cada clique. É o assunto de
autenticação com JWT no Express, a próxima
lição — e ela começa exatamente onde a rota /sessoes daqui parou, trocando o
res.json do tutor por um token assinado.
Se quiser ver onde a autenticação entra no caminho completo do back-end, o guia de Node.js mostra a ordem inteira, do primeiro servidor ao deploy.
Perguntas frequentes
Dá para descriptografar um hash bcrypt e recuperar a senha?
Qual tamanho de coluna usar no banco para guardar o hash?
bcrypt ou bcryptjs? Qual dos dois instalar?
Posso gerar o hash no front-end antes de enviar a senha?
E quando a pessoa esquece a senha, como faço para reenviar?
Dúvidas e comentários
Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.
Entrar para perguntarÉ o mesmo login gratuito dos cursos.
Nenhuma dúvida por aqui ainda — a primeira pode ser a sua.
Todo o código deste artigo foi executado em Node 24.16.0 com bcrypt 6.0.0, e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- OWASP — Password Storage Cheat Sheet — cheatsheetseries.owasp.org
- npm — bcrypt — npmjs.com
- Node.js — crypto.scrypt — nodejs.org
- Provos e Mazières — A Future-Adaptable Password Scheme (USENIX 1999) — usenix.org


