DevGuideTK
← Trilha Backend

Hash de senhas

Por que senhas nunca são guardadas, nem criptografadas, e quais algoritmos resistem a um vazamento do banco.

Nenhuma linguagem deveria ter uma implementação de hash de senha escrita à mão no seu projeto. Os exemplos usam a biblioteca mais adotada em cada uma e mostram as três operações que todo sistema precisa: gerar o hash, conferir a senha e saber quando refazer o hash.

Gerar, conferir e atualizar

import argon2 from "argon2";

const PARAMETROS = {
  type: argon2.argon2id,
  memoryCost: 19456, // KiB = 19 MiB
  timeCost: 2,
  parallelism: 1,
};

export function gerarHash(senha: string) {
  return argon2.hash(senha, PARAMETROS);
}

export function conferir(hash: string, senha: string) {
  return argon2.verify(hash, senha);
}

export function precisaRefazer(hash: string) {
  return argon2.needsRehash(hash, PARAMETROS);
}

O que muda de uma linguagem para outra

Biblioteca do exemploAlgoritmoComo sabe que precisa refazer
Nodeargon2Argon2idneedsRehash compara com os parâmetros atuais
Pythonargon2-cffiArgon2idcheck_needs_rehash
Gox/crypto/bcryptbcryptCompare o custo do hash com o atual
JavaSpring SecurityArgon2idupgradeEncoding
C#ASP.NET Core IdentityPBKDF2-HMAC-SHA512O resultado SuccessRehashNeeded

Três observações sobre a tabela:

  • Go tem Argon2 em x/crypto/argon2, mas só a função de baixo nível: gerar o sal, montar a string e comparar ficam por sua conta. Por isso o bcrypt é a escolha mais segura para quem não quer escrever esse código.
  • C#: o PasswordHasher do Identity usa PBKDF2 com um número de iterações que sobe entre versões do .NET. Ao atualizar o .NET, os hashes antigos passam a voltar SuccessRehashNeeded.
  • Java: o DelegatingPasswordEncoder do Spring guarda o algoritmo como prefixo ({bcrypt}, {argon2}) e confere hashes de algoritmos diferentes, o que facilita migrar.

O login completo

Conferir a senha é só uma parte. O login precisa responder no mesmo tempo quando o usuário não existe e aproveitar o momento para atualizar o hash.

// Hash de uma senha qualquer, calculado uma vez na inicialização
const HASH_FALSO = await gerarHash("senha-que-ninguem-usa");

export async function login(email: string, senha: string) {
  const usuario = await usuarios.buscarPorEmail(email);

  // Mesmo trabalho com e sem usuário: o tempo não revela quem existe
  const hash = usuario?.hashSenha ?? HASH_FALSO;
  const confere = await conferir(hash, senha);

  if (!usuario || !confere) {
    throw new CredenciaisInvalidas(); // mesma mensagem nos dois casos
  }

  if (precisaRefazer(usuario.hashSenha)) {
    await usuarios.atualizarHash(usuario.id, await gerarHash(senha));
  }

  return usuario;
}
DetalhePor que existe
HASH_FALSOSem ele, o login de um e-mail inexistente responde em milissegundos e o de um existente em centenas. O tempo de resposta revela quais e-mails estão cadastrados
Mesma mensagem de erro"Usuário não encontrado" e "senha errada" contam ao atacante quais contas existem
Refazer depois de conferirÉ o único momento em que o sistema tem a senha original em mãos

Lista de revisão

Armazenamento

  • O algoritmo é Argon2id, scrypt, bcrypt ou PBKDF2, de uma biblioteca mantida?
  • Os parâmetros são iguais ou maiores que os mínimos atuais da OWASP?
  • A coluna guarda a string completa da biblioteca, com tamanho suficiente para ela?
  • Com bcrypt, senhas acima de 72 bytes são tratadas?

Login

  • A conferência usa a função verify da biblioteca?
  • O tempo de resposta é o mesmo para e-mail existente e inexistente?
  • A mensagem de erro é a mesma nos dois casos?
  • O hash é refeito quando os parâmetros mudam?
  • Há limite de tentativas por conta e por endereço?

Em volta

  • A senha nunca aparece em logs, mensagens de erro ou ferramentas de monitoramento?
  • Senhas vazadas conhecidas são recusadas no cadastro e na troca?
  • Existe um plano para os hashes legados mais fracos?

Prompts para aprofundar

Ao abrir no ChatGPT ou no Claude, o prompt é enviado ao serviço escolhido pela URL e também fica copiado na sua área de transferência.

  • Implementar cadastro e login seguros

    Quero implementar o cadastro e o login com hash de senha do jeito certo.
    
    Pergunte em qual linguagem e framework eu trabalho.
    
    Vamos em etapas. Em cada uma, explique o objetivo, deixe que eu tente, e depois mostre a sua versão comentando as diferenças:
    1. Escolher a biblioteca e configurar os parâmetros, explicando cada um.
    2. O cadastro, com validação de tamanho e verificação contra senhas vazadas.
    3. O login, com conferência pela função da biblioteca, tempo de resposta igual para e-mail existente e inexistente e mensagem de erro única.
    4. A atualização do hash no login quando os parâmetros mudarem.
    5. A troca de senha, encerrando as outras sessões do usuário.
    6. Testes automatizados para cada um desses comportamentos.
    
    No final, me ajude a medir o tempo de resposta do login para um e-mail existente e outro inexistente, e a confirmar que a diferença não é significativa.
  • Medir o custo do hash no meu servidor

    Quero calibrar os parâmetros de hash de senha medindo no meu próprio servidor.
    
    Pergunte qual linguagem, biblioteca e algoritmo eu uso, e as características do servidor.
    
    Depois:
    1. Escreva um pequeno programa que mede o tempo médio e o pico de memória de um hash com diferentes parâmetros.
    2. Explique como interpretar os resultados e qual faixa de tempo mirar.
    3. Calcule, a partir dos resultados, quantos logins por segundo o servidor aguenta e quanta memória isso consome.
    4. Mostre como simular um pico de logins e observar o efeito na latência das outras rotas.
    5. Sugira o limite de tentativas da rota de login com base nos números.
    
    Explique também como repetir essa medição periodicamente e quando aumentar o custo.
  • Revisar o armazenamento de senhas

    Vou colar abaixo o código de cadastro, login ou troca de senha do meu sistema. Revise com foco no armazenamento e na conferência de senhas.
    
    Verifique, nesta ordem, e para cada item diga se está correto, incorreto ou ausente, citando a linha:
    1. O algoritmo é próprio para senhas e vem de uma biblioteca mantida? Os parâmetros são adequados?
    2. O sal é gerado pela biblioteca, e a string completa é guardada?
    3. A conferência usa a função da biblioteca, em tempo constante?
    4. O login leva o mesmo tempo e devolve a mesma mensagem para e-mail existente e inexistente?
    5. O hash é refeito quando os parâmetros mudam?
    6. Com bcrypt, senhas acima de 72 bytes são tratadas?
    7. A senha aparece em algum log, exceção ou resposta?
    8. Há limite de tentativas?
    
    Para cada problema, descreva o ataque possível e mostre a correção. Não reescreva o que já está correto.
    
    Código:
    [cole aqui]