Criptografia
Criptografia, codificação e hash: diferenças explicadas
Separe codificação, criptografia e hash com laboratórios de Base64, SHA-256 e OpenSSL. Entenda por que Base64 não protege e o hash é de mão única.
Por que hashes rápidos falham em senhas, o que fazem o salt e o fator de trabalho, e como gerar e verificar hashes com Argon2id em Python.
Bancos de senhas vazam com mais frequência do que as equipes planejam. Quando isso acontece, o atacante fica com hashes, não com senhas. O trabalho para sair de um para o outro depende de duas escolhas: a função que você usou e as configurações que deu a ela. Este artigo explica por que o armazenamento de senhas precisa de um tipo de hash diferente, o que fazem o salt e o fator de trabalho, e por que o Argon2id é a escolha que as recomendações atuais apontam. Você vai gerar e verificar o hash de uma senha em Python, ler cada parte da string codificada e recalcular o hash das senhas guardadas quando suas configurações mudarem.
Você precisa de um terminal e de Python 3. O laboratório instala um único pacote, o argon2-cffi.
Uma função de hash transforma uma entrada em uma saída de tamanho fixo. O SHA-256 faz isso muito rápido, e é exatamente o que você quer para checksums e integridade de arquivos. Para senhas, a velocidade é o problema. Uma tabela de senhas roubada é atacada por tentativa: pega-se um candidato, calcula-se o hash, compara-se e repete-se. Se cada tentativa custa pouco, o atacante consegue fazer bilhões delas em placas de vídeo.
O OWASP Password Storage Cheat Sheet é direto nesse ponto. O SHA-256 é rápido, e a rapidez é exatamente o que um atacante precisa quando a entrada é uma senha, uma string curta escolhida por uma pessoa.
Você mesmo pode medir a diferença. O benchmark do laboratório calcula o hash de uma senha com SHA-256 salgado e com Argon2id, e mostra a taxa de cada um.
O salt é um valor aleatório, diferente para cada senha, guardado ao lado do hash. Ele faz duas coisas.
O salt não é segredo e não torna uma senha fraca forte. A função dele é impedir que o atacante reaproveite trabalho entre usuários. A RFC 9106 recomenda um salt de 128 bits e aceita 64 bits quando o espaço é limitado. A NIST SP 800-63B exige pelo menos 32 bits. O padrão do argon2-cffi são 16 bytes, ou seja, 128 bits.
O fator de trabalho é uma configuração que deixa cada tentativa cara de propósito. Esquemas mais antigos, como o PBKDF2, contam sobretudo iterações. O Argon2 tem três parâmetros.
m): a RAM, em KiB, que cada hash usa.t): quantas passadas a função faz sobre essa memória.p): quantas trilhas podem rodar ao mesmo tempo.A memória é o que mais importa. Uma GPU tem milhares de núcleos pequenos, e cada núcleo tem pouca memória própria. Se cada tentativa exige dezenas de megabytes, o atacante não consegue rodar tantas tentativas em paralelo quanto conseguiria contra uma função que precisa de poucos kilobytes. Essa propriedade se chama dureza de memória (memory-hardness).
O fator de trabalho é um botão de ajuste, não uma constante. Aumente-o conforme o hardware fica mais rápido e conforme o orçamento de login permitir. Guarde as configurações junto de cada hash, para que hashes antigos sejam reconhecidos e atualizados depois. As últimas etapas do laboratório fazem isso.
O Argon2 venceu a Password Hashing Competition em 2015. Ele tem três variantes.
O Argon2id cobre as duas preocupações. A RFC 9106 o recomenda como variante de uso geral, e o OWASP o coloca em primeiro lugar entre as opções de hash para senhas.
A RFC 9106 traz dois conjuntos de configuração recomendados. O primeiro, para sistemas com memória sobrando, usa 2 GiB, 1 passada e 4 trilhas. O segundo, para ambientes com memória limitada, usa 64 MiB, 3 passadas e 4 trilhas. O mínimo do OWASP é 19 MiB, 2 iterações e 1 trilha. O OWASP também lista alternativas que trocam memória por passadas, como 12 MiB com 3 passadas ou 7 MiB com 5 passadas, todas com 1 trilha. Os padrões do argon2-cffi coincidem com o segundo conjunto da RFC.
Trate esses valores como pisos e pontos de partida, não como metas. Uma regra prática comum é ajustar as configurações até um hash levar algumas centenas de milissegundos no seu hardware de produção. Depois, confira se a memória por hash, multiplicada pelo número de logins que podem chegar no mesmo instante, ainda cabe nos seus servidores. Essa regra vem da prática, não de nenhum dos dois padrões.
A NIST segue outro caminho. A SP 800-63B exige que as senhas sejam salgadas e tenham hash calculado com um esquema adequado, e diz que se deve usar um esquema aprovado da SP 800-132, que cobre o PBKDF2. Ela não cita o Argon2id. Se um programa de conformidade exige algoritmos aprovados pela NIST, confirme com o seu auditor antes de escolher o Argon2id.
Quando uma biblioteca de hash de senhas guarda uma senha, acontecem cinco coisas.
O passo 3 é o motivo de você não precisar de uma coluna separada para o salt. A string codificada tem este formato.
$argon2id$v=19$m=65536,t=3,p=4$<salt>$<hash>argon2id é a variante.v=19 é a versão do Argon2, 1.3, escrita em decimal. Em hexadecimal, é 0x13.m, t e p são memória em KiB, custo de tempo e paralelismo.<salt> e <hash> estão em base64 sem preenchimento. O hash é a saída derivada, com 32 bytes por padrão.A string do laboratório tem 97 caracteres. Use uma coluna que aceite pelo menos 255 caracteres, ou um tipo texto. Não separe a string em colunas, a menos que você goste de escrever o seu próprio parser.
O laboratório usa um ambiente virtual local e não precisa de nenhum serviço de rede. A saída abaixo vem de uma máquina Windows 11 com Python 3.12.10, argon2-cffi 25.1.0 e argon2-cffi-bindings 26.1.0. Nessa máquina, o python3 não estava no PATH do Git Bash, então os comandos rodaram como python. Use python3 se funcionar no seu. Os hashes que você vai ver serão diferentes dos mostrados, porque o salt é aleatório.
python3 -m venv .venv && . .venv/bin/activate
pip install argon2-cffiNo Windows, ative com .venv\Scripts\activate no PowerShell ou no cmd, ou com . .venv/Scripts/activate no Git Bash.
python3 -c "from argon2 import PasswordHasher; ph=PasswordHasher(); h=ph.hash('correct horse battery'); print(h); print(ph.verify(h,'correct horse battery')); print(ph.check_needs_rehash(h))"Saída observada:
$argon2id$v=19$m=65536,t=3,p=4$abVC6VF2EGJW1SCX/D6WBQ$XHKggONKypxBhdq7sWTbL/sMkglTAKu9y1FlL5p4dic
True
Falseverify devolve True para a senha certa. check_needs_rehash devolve False porque este hash foi gerado com os padrões atuais. Esses padrões são custo de tempo 3, custo de memória 65536 KiB (64 MiB), paralelismo 4, hash de 32 bytes, salt de 16 bytes e a variante ID.
python3 -c "from argon2 import PasswordHasher; from argon2.exceptions import VerifyMismatchError; ph=PasswordHasher(); h=ph.hash('a');
try: ph.verify(h,'b')
except VerifyMismatchError: print('mismatch detected')"Saída observada:
mismatch detectedSenha errada não devolve False. O verify lança VerifyMismatchError, então a checagem fica dentro de um bloco except. Uma string guardada malformada lança outra exceção, InvalidHashError. Trate isso como problema de dados do servidor e não deixe a mensagem chegar à tela do usuário.
Gere o hash da mesma senha duas vezes.
python3 -c "from argon2 import PasswordHasher; ph=PasswordHasher(); h=ph.hash('correct horse battery'); print(h)"
python3 -c "from argon2 import PasswordHasher; ph=PasswordHasher(); h=ph.hash('correct horse battery'); print(h)"Saída observada:
$argon2id$v=19$m=65536,t=3,p=4$p8OU0plYOCO2/v70mjYTFA$FwhMW3ECJ0F/8rulKrJ158HjyNekURguaCIfoVk0i/o
$argon2id$v=19$m=65536,t=3,p=4$MTgAgrtBHJmc1eL2iKy8nA$h/7+8J6Cdb82XTBGWsLjbYBIh05934Yel4y6tyZ6NYgUma senha, duas strings diferentes. Os parâmetros são iguais porque as duas execuções usaram os mesmos padrões. Só o salt e a saída mudam. Quem lê a tabela não consegue saber que essas duas contas usam a mesma senha.
Salve isto como parts.py.
from argon2 import PasswordHasher
h = PasswordHasher().hash("correct horse battery")
print(h.split("$"))Rode com python3 parts.py. Saída observada:
['', 'argon2id', 'v=19', 'm=65536,t=3,p=4', 'waS+Ec4iQCNIuhRQbv2sWQ', 'yLioGsHJoVv+vTu2kRoYmXTLjbbjJSe2tPSExz3Sd5A']O primeiro elemento vazio vem do $ inicial. Depois vêm a variante, a versão, os parâmetros, o salt e o hash. A verificação não precisa de mais nada, por isso uma coluna basta.
As configurações mudam com o tempo. Este script gera um hash com configurações antigas e mais fracas e depois o confere com os padrões atuais. Salve como rehash.py.
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
old = PasswordHasher(time_cost=2, memory_cost=19456, parallelism=1)
stored = old.hash('correct horse battery')
print('stored:', stored)
new = PasswordHasher() # padrões atuais
print('needs rehash under new params:', new.check_needs_rehash(stored))
try:
new.verify(stored, 'correct horse battery')
print('verify ok')
if new.check_needs_rehash(stored):
stored = new.hash('correct horse battery')
print('rehashed:', stored)
except VerifyMismatchError:
print('wrong password')
print('after rehash needs rehash:', new.check_needs_rehash(stored))Rode python3 rehash.py. Saída observada:
stored: $argon2id$v=19$m=19456,t=2,p=1$EWji+p6+3HbAKY+wkykcXg$NxPq/zGPsx9okK4vQCKfZ4BVoSOqeF1QkHxd3f2g2uE
needs rehash under new params: True
verify ok
rehashed: $argon2id$v=19$m=65536,t=3,p=4$FkifxeWUHV9eU7d8JFt6mg$ZFoMkNjYqo4D/bax/jPz80Hvr4mjLWRi4TVoqCoR6ng
after rehash needs rehash: FalseA ordem importa. Verifique primeiro e só depois recalcule, para que apenas uma senha correta chegue à atualização. Em uma aplicação real, grave a nova string na mesma linha logo depois de um login bem-sucedido. Se pular essa etapa, as configurações antigas ficam na tabela para sempre.
Salve isto como bench.py.
import hashlib, time, os
from argon2 import PasswordHasher
pw = b'correct horse battery'
salt = os.urandom(16)
n = 1_000_000
t = time.perf_counter()
for _ in range(n):
hashlib.sha256(salt + pw).digest()
dt = time.perf_counter() - t
print(f"sha256 salted: {n} hashes in {dt:.2f} s, {n/dt:,.0f} per second")
ph = PasswordHasher()
m = 10
t = time.perf_counter()
for _ in range(m):
ph.hash('correct horse battery')
dt = time.perf_counter() - t
print(f"argon2id default: {m} hashes in {dt:.2f} s, {dt/m*1000:.0f} ms per hash")Rode python3 bench.py. Saída observada:
sha256 salted: 1000000 hashes in 4.06 s, 246,343 per second
argon2id default: 10 hashes in 6.40 s, 640 ms per hashSão medições únicas em uma máquina Windows. A linha do SHA-256 mede um laço em Python, não código otimizado, então ela subestima o que um hardware dedicado faz com SHA-256. A linha do Argon2id é o custo que cada login paga. Os números exatos importam menos que a diferença. Uma chamada de SHA-256 salgado leva cerca de 4 microssegundos aqui. Uma chamada de Argon2id leva cerca de 640 milissegundos, e isso é o projeto. Ajuste m, t e p em um hardware parecido com o de produção, até que um login custe algumas centenas de milissegundos.
verify. Não compare strings com ==.PasswordHasher, verify e check_needs_rehash.Criptografia
Separe codificação, criptografia e hash com laboratórios de Base64, SHA-256 e OpenSSL. Entenda por que Base64 não protege e o hash é de mão única.
Criptografia
Chaves pública e privada do zero: o que uma assinatura prova, por que a chave privada fica em segredo e um lab Ed25519 em que um arquivo alterado falha.
Criptografia
Criptografia do zero: cifragem, chaves, hashes, assinaturas e por que a cifra de César ensina ideias, mas não protege nada.