Como guardar senhas: por que não usar SHA-256 e o que usar
Senhas nunca devem ser guardadas em texto nem com um hash rápido como MD5 ou SHA-256. O certo é um hash de senha lento e com sal: Argon2id, scrypt ou bcrypt. Este guia explica por quê e mostra como gerar e conferir o hash em Node.js e Python com scrypt e em PHP com password_hash.
Por que não SHA-256
O SHA-256 foi feito para ser rápido, e é isso que o torna ruim para senhas. Se o banco vazar, quem tem os hashes testa bilhões de senhas por segundo em uma placa de vídeo, e as senhas comuns caem em minutos. Sem sal, pior ainda: senhas iguais geram hashes iguais, e tabelas prontas revelam as mais usadas.
Um hash de senha é lento de propósito (e o scrypt e o Argon2 também gastam memória), para que cada tentativa custe caro. E usa um sal aleatório por senha, para que duas senhas iguais gerem resultados diferentes.
Qual algoritmo usar
- Argon2id: a primeira escolha da OWASP, quando a linguagem oferece.
- scrypt: boa alternativa, e vem pronto no Node.js e no Python, sem instalar nada. Custo sugerido: N = 2^17, r = 8, p = 1.
- bcrypt: seguro e muito difundido, mas só considera os primeiros 72 bytes da senha. É o padrão do
password_hashdo PHP. - PBKDF2: use quando uma norma exigir (como a FIPS-140), com muitas iterações.
Em Node.js, com scrypt
O resultado guarda o sal e o hash juntos. Na conferência, timingSafeEqual compara em tempo constante, para que a demora da comparação não revele quantos bytes batem.
import { randomBytes, scryptSync, timingSafeEqual } from 'node:crypto';
// Custo recomendado pela OWASP para o scrypt: N = 2^17, r = 8, p = 1 (usa 128 MiB)
const CUSTO = { N: 2 ** 17, r: 8, p: 1, maxmem: 256 * 1024 * 1024 };
function hashSenha(senha) {
const sal = randomBytes(16); // um sal novo para cada senha
const hash = scryptSync(senha, sal, 64, CUSTO);
return `${sal.toString('hex')}:${hash.toString('hex')}`;
}
function conferirSenha(senha, guardado) {
const [sal, hash] = guardado.split(':').map((h) => Buffer.from(h, 'hex'));
return timingSafeEqual(scryptSync(senha, sal, 64, CUSTO), hash);
} Em Python, com scrypt
Com o mesmo custo e o mesmo formato, um sistema em Python confere as senhas gravadas pelo Node.js, e o contrário também.
import hashlib
import hmac
import os
# O mesmo custo do exemplo em Node.js: os dois leem as senhas um do outro
CUSTO = {"n": 2**17, "r": 8, "p": 1, "maxmem": 256 * 1024 * 1024, "dklen": 64}
def hash_senha(senha: str) -> str:
sal = os.urandom(16)
h = hashlib.scrypt(senha.encode("utf-8"), salt=sal, **CUSTO)
return f"{sal.hex()}:{h.hex()}"
def conferir_senha(senha: str, guardado: str) -> bool:
sal, esperado = (bytes.fromhex(x) for x in guardado.split(":"))
h = hashlib.scrypt(senha.encode("utf-8"), salt=sal, **CUSTO)
return hmac.compare_digest(h, esperado) Em PHP, com password_hash
O PHP já traz tudo pronto: password_hash gera o sal e grava o algoritmo e o custo no próprio resultado, e password_verify lê essas informações na conferência. Use password_needs_rehash no login para atualizar hashes antigos quando o custo padrão subir.
<?php
function hashSenha(string $senha): string
{
// O resultado já inclui o algoritmo, o custo e o sal
return password_hash($senha, PASSWORD_DEFAULT);
}
function conferirSenha(string $senha, string $guardado): bool
{
return password_verify($senha, $guardado);
} Cuidados
- Guarde os parâmetros de custo junto com o hash (o
password_hashjá faz isso). Assim dá para aumentar o custo no futuro sem invalidar as senhas antigas. - Nunca compare hashes com
==ou===: use a comparação em tempo constante da linguagem. - Limite o tamanho da senha aceita (por exemplo, 128 caracteres) para evitar que senhas enormes sejam usadas para sobrecarregar o servidor.
- Para gerar senhas temporárias e tokens, use um gerador seguro. Veja como gerar senha segura em código ou o gerador de senha.
Achou um erro neste guia? Avise por e-mail.
Perguntas frequentes
Posso guardar senha com SHA-256?
Não. O SHA-256 é rápido demais, e um vazamento permite testar bilhões de senhas por segundo. Use Argon2id, scrypt ou bcrypt, que são lentos de propósito e usam sal.
O que é o sal de uma senha?
É um valor aleatório gerado para cada senha e guardado junto com o hash. Ele faz duas senhas iguais terem hashes diferentes e inutiliza tabelas prontas de senhas comuns. O sal não precisa ser secreto.
bcrypt ou Argon2?
O Argon2id é a recomendação atual da OWASP. O bcrypt continua seguro para a maioria dos sistemas, mas ignora o que passa de 72 bytes na senha. Se a linguagem já oferece um dos dois pronto, é melhor usar o pronto do que implementar outro.
Dá para descobrir a senha a partir do hash?
Não diretamente: o hash não se desfaz. O que um atacante faz é testar senhas até achar uma com o mesmo hash. Por isso o hash de senha precisa ser lento e a senha precisa ser longa.
Outros guias
- Como gerar senha segura em código (sem Math.random)Por que Math.random não serve para senhas, como evitar o viés do módulo e como gerar senhas seguras em JavaScript, Python e PHP, com código testado.
- SHA-256 de arquivo: como gerar e conferir sem estourar a memóriaComo gerar e conferir o SHA-256 de um arquivo pelo terminal e em Node.js, Python e PHP, lendo em pedaços, com código testado e os cuidados na conferência.