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);
}from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
hasher = PasswordHasher(
memory_cost=19456, # KiB = 19 MiB
time_cost=2,
parallelism=1,
)
def gerar_hash(senha: str) -> str:
return hasher.hash(senha)
def conferir(hash_: str, senha: str) -> bool:
try:
return hasher.verify(hash_, senha)
except VerifyMismatchError:
return False
def precisa_refazer(hash_: str) -> bool:
return hasher.check_needs_rehash(hash_)package senhas
import "golang.org/x/crypto/bcrypt"
const custo = 12
func GerarHash(senha string) (string, error) {
// Retorna erro para senhas com mais de 72 bytes
hash, err := bcrypt.GenerateFromPassword([]byte(senha), custo)
return string(hash), err
}
func Conferir(hash, senha string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(senha))
return err == nil
}
func PrecisaRefazer(hash string) bool {
atual, err := bcrypt.Cost([]byte(hash))
return err != nil || atual < custo
}import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
public class Senhas {
// Requer a dependência org.bouncycastle:bcprov
private static final PasswordEncoder ENCODER =
Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8();
public static String gerarHash(String senha) {
return ENCODER.encode(senha);
}
public static boolean conferir(String hash, String senha) {
return ENCODER.matches(senha, hash);
}
public static boolean precisaRefazer(String hash) {
return ENCODER.upgradeEncoding(hash);
}
}using Microsoft.AspNetCore.Identity;
public class Senhas
{
private static readonly PasswordHasher<Usuario> Hasher = new();
public static string GerarHash(Usuario usuario, string senha) =>
Hasher.HashPassword(usuario, senha);
// Success, SuccessRehashNeeded ou Failed
public static PasswordVerificationResult Conferir(
Usuario usuario, string hash, string senha) =>
Hasher.VerifyHashedPassword(usuario, hash, senha);
}
public record Usuario(string Id, string Email);O que muda de uma linguagem para outra
| Biblioteca do exemplo | Algoritmo | Como sabe que precisa refazer | |
|---|---|---|---|
| Node | argon2 | Argon2id | needsRehash compara com os parâmetros atuais |
| Python | argon2-cffi | Argon2id | check_needs_rehash |
| Go | x/crypto/bcrypt | bcrypt | Compare o custo do hash com o atual |
| Java | Spring Security | Argon2id | upgradeEncoding |
| C# | ASP.NET Core Identity | PBKDF2-HMAC-SHA512 | O 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
PasswordHasherdo 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 voltarSuccessRehashNeeded. - Java: o
DelegatingPasswordEncoderdo 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;
}| Detalhe | Por que existe |
|---|---|
HASH_FALSO | Sem 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
verifyda 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]