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 Leonardo Gaertner · Publicado em

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_hash do 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.

JavaScript
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.

Python
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
<?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_hash já 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.