Trilha 13 · Segurança e criptografia

Funções hash criptográficas

Uma função que transforma qualquer coisa — uma senha, um filme de 4 GB, um contrato — num número de tamanho fixo, de tal forma que é fácil calcular e inviável desfazer. Essa assimetria é o tijolo com que todo o resto desta trilha é construído: assinaturas, certificados, blockchains e a forma como sua senha é guardada.

① Intuição

A impressão digital de um dado

Uma impressão digital é minúscula perto de uma pessoa, mas identifica essa pessoa e só ela. E não existe caminho de volta: da impressão não se reconstrói o rosto, a voz ou o nome. Uma função hash faz exatamente isso com dados — 256 bits que identificam um arquivo de qualquer tamanho, sem guardar nada dele.

A analogia falha num ponto importante, e vale corrigi-la: impressões digitais parecidas pertencem a pessoas parecidas. Hashes, não. Trocar um único bit da entrada muda, em média, metade dos bits da saída — o efeito avalanche. Não existe "hash parecido", e é justamente por isso que nenhuma informação sobre a entrada sobrevive de forma explorável.

Hash não é criptografia. Criptografar é reversível: existe uma chave e existe a volta. Hash é uma via de mão única por projeto — não há chave, não há segredo e não há decifração, nem para você. Quando um site diz "recupere sua senha" e envia a senha antiga por e-mail, isso revela que ele não usou hash. Um site que só sabe redefinir a senha está fazendo certo.
② Visualização interativa

SHA-256 de verdade, rodando aqui

A primeira aba calcula SHA-256 real (os valores batem com sha256sum): mude uma letra e conte quantos dos 256 bits viraram. Na segunda, um banco vazou — ative e desative o salt e tente quebrar as senhas com uma tabela rainbow.

Uma função hash criptográfica transforma qualquer entrada — 3 letras ou 3 gigabytes — em 256 bits de tamanho fixo. Este é o SHA-256 de verdade, implementado em JavaScript: os valores abaixo batem com sha256sum e com hashlib.sha256.

ENTRADA — mude uma letra só
16 caracteres
SHA-256 (256 bits, 64 dígitos hex)
542867886239ff4587c89f8ea38f591eb0f890c0b51e3568b7ffdcf047c91261
os 256 bits da saída — em vermelho, os que mudaram em relação ao texto original
Mude qualquer coisa no texto — troque um 1 por um 2, apague um acento — e compare os dois hashes.
③ Explicação técnica

As três propriedades

# Uma função hash criptográfica precisa de três propriedades.
# Cada uma corresponde a um ataque diferente:
#
#  1. RESISTÊNCIA À PRÉ-IMAGEM
#     Dado h, é inviável achar m tal que hash(m) = h.
#     "não dá para reverter o hash de uma senha"
#
#  2. RESISTÊNCIA À SEGUNDA PRÉ-IMAGEM
#     Dado m1, é inviável achar m2 ≠ m1 com o mesmo hash.
#     "não dá para trocar um contrato assinado por outro"
#
#  3. RESISTÊNCIA À COLISÃO
#     É inviável achar QUALQUER par m1 ≠ m2 com o mesmo hash.
#     "não dá para preparar dois contratos com o mesmo hash"
#
# A 3ª é a mais difícil de garantir por causa do PARADOXO DO
# ANIVERSÁRIO: achar uma colisão qualquer custa ~2^(n/2), não 2^n.
# Para SHA-256 isso ainda são 2^128 operações — inviável.
# Mas para MD5 (128 bits) são 2^64, e hoje se faz em SEGUNDOS.

import hashlib
hashlib.sha256(b"mensagem").hexdigest()
# sempre 64 caracteres hex = 256 bits, para QUALQUER entrada

O que está quebrado, e o que usar

# O cemitério: nenhum destes deve ser usado hoje para segurança.
#
#  MD5   (1992) — colisões práticas desde 2004. Segundos num laptop.
#  SHA-1 (1995) — colisão real demonstrada em 2017 (ataque SHAttered:
#                 dois PDFs diferentes com o MESMO SHA-1). Aposentado.
#
#  SHA-256 / SHA-512 (família SHA-2) — o padrão atual, sólido.
#  SHA-3 (Keccak)   — construção interna diferente, como reserva
#                     estratégica caso o SHA-2 caia um dia.
#  BLAKE3           — moderno e muito rápido, paralelizável.

# ⚠ MD5 ainda é ACEITÁVEL para o que não é segurança:
#   detectar arquivos duplicados, particionar dados, checksums
#   contra corrupção acidental de disco. O que ele não resiste é a
#   um ADVERSÁRIO tentando fabricar uma colisão de propósito.

# Por que uma colisão é grave na prática?
# Assinaturas digitais assinam o HASH do documento, não o documento.
# Se eu consigo produzir dois arquivos com o mesmo hash, consigo
# pedir sua assinatura no inofensivo e colar essa assinatura no outro.
O paradoxo do aniversário, e por que ele importa. Num grupo de 23 pessoas, a chance de duas fazerem aniversário no mesmo dia já passa de 50% — muito antes do que a intuição sugere, porque o que importa é o número de pares, não de pessoas. O mesmo vale para hashes: achar uma colisão qualquer custa cerca de 2n/2, não 2n. É por isso que um hash de 128 bits oferece apenas 64 bits de resistência a colisão — e por que "256 bits" não é exagero, é a margem necessária.

Senhas: o caso em que rápido é ruim

# ✗ NUNCA: hash rápido para senhas
hashlib.sha256(senha.encode()).hexdigest()
# Uma GPU moderna calcula BILHÕES de SHA-256 por segundo.
# Toda senha comum cai em minutos.

# ✗ NUNCA: hash sem salt
# Usuários com a mesma senha ficam com o mesmo hash — e uma
# tabela pré-computada (rainbow table) quebra todos de uma vez.

# ✓ SEMPRE: função lenta, com salt automático
import bcrypt
hash = bcrypt.hashpw(senha.encode(), bcrypt.gensalt(rounds=12))
# O salt já vai embutido no resultado; você não gerencia nada.
bcrypt.checkpw(tentativa.encode(), hash)   # → True/False

# Ou Argon2id, o vencedor da Password Hashing Competition e a
# recomendação atual do OWASP:
from argon2 import PasswordHasher
ph = PasswordHasher()          # custo de tempo E de memória
h = ph.hash(senha)
ph.verify(h, tentativa)

# Por que "lento" é uma VIRTUDE aqui?
#   Você verifica 1 senha por login: 100 ms não incomoda ninguém.
#   O atacante precisa testar bilhões: 100 ms cada inviabiliza tudo.
# O custo de MEMÓRIA (scrypt, Argon2) é ainda mais importante:
# é o que impede o atacante de rodar milhares de tentativas em
# paralelo numa GPU, que tem muitos núcleos mas pouca memória cada.

# E compare sempre em TEMPO CONSTANTE, para não vazar informação
# pelo tempo de resposta:
import hmac
hmac.compare_digest(a, b)      # não para no primeiro byte diferente

Onde mais o hash aparece

# Hash não é só para senhas. Onde mais ele aparece:

# INTEGRIDADE — o arquivo baixado é o arquivo publicado?
#   sha256sum ubuntu.iso  →  compare com o publicado no site

# HMAC — integridade COM autenticidade, usando chave compartilhada
import hmac, hashlib
assinatura = hmac.new(chave_secreta, corpo, hashlib.sha256).hexdigest()
# É assim que webhooks (Stripe, GitHub) provam que a requisição
# veio mesmo deles, e não de alguém que descobriu a URL.
# ⚠ hash(chave + msg) NÃO é HMAC — essa construção ingênua sofre o
#   ataque de extensão de comprimento. Use a função hmac pronta.

# IDENTIDADE POR CONTEÚDO — o hash VIRA o nome
#   Git: cada commit, árvore e blob é endereçado pelo seu hash.
#   Mudar qualquer byte do passado muda todos os hashes seguintes —
#   é isso que torna o histórico verificável.
#   IPFS, Docker (sha256:...), Nix: mesma ideia.

# ENCADEAMENTO — cada bloco contém o hash do anterior
#   Blockchains e logs de auditoria à prova de adulteração:
#   alterar o passado exigiria recalcular tudo desde então.

# DEDUPLICAÇÃO E PARTICIONAMENTO
#   Backups que guardam cada bloco uma única vez; sharding por hash.
④ Projeto para programar

Experimentando

Mini projeto: meça o efeito avalanche você mesmo. Gere 10 mil pares de strings que diferem em um único caractere, calcule o SHA-256 de cada uma e conte quantos bits mudam. Plote o histograma: ele deve se concentrar em torno de 128, com o formato de uma binomial. Depois repita com uma função hash ruim (a soma dos códigos dos caracteres, por exemplo) e compare os dois histogramas — a diferença é visualmente óbvia.

Projeto principal: implemente um log de auditoria à prova de adulteração. Cada entrada guarda (dados, hash_anterior) e seu próprio hash cobre os dois campos. Escreva a função verificar() que percorre a cadeia do início ao fim conferindo tudo. Depois altere à mão uma entrada no meio do arquivo e mostre que a verificação aponta exatamente onde a cadeia quebrou. Você acabou de construir o mecanismo central de uma blockchain — sem nenhuma criptomoeda envolvida.

Desafio extra: demonstre a fraqueza do MD5 na prática. Baixe os dois arquivos do ataque SHAttered (SHA-1) ou um par de colisão de MD5 já publicado, confirme que os hashes são idênticos e que os arquivos não são, e depois confira que o SHA-256 dos dois difere. Em seguida implemente um ataque de força bruta a um hash truncado — use apenas os primeiros 4 bytes do SHA-256 e cronometre quanto leva para achar uma colisão. Multiplique mentalmente para 32 bytes e você terá a intuição de por que 256 bits basta.

⑤ Exercícios rápidos

Teste sua intuição

O que acontece com o hash quando você muda um único caractere da entrada?
Por que não se deve usar SHA-256 puro para guardar senhas?
Para que serve o salt?
Por que uma colisão em MD5 ou SHA-1 é grave para assinaturas digitais?
⑥ Aplicações no mundo real

Onde você encontra isso

🌿

Git é um banco de dados endereçado por conteúdo

Cada commit, árvore e blob do Git tem como nome o hash do próprio conteúdo. Como o commit inclui o hash do commit pai, alterar qualquer coisa no passado muda todos os hashes seguintes — é por isso que um rebase reescreve os identificadores. O Git está migrando de SHA-1 para SHA-256 justamente por causa das colisões demonstradas em 2017.

🔓

Vazamentos e o que eles revelam

Quando um vazamento é noticiado, a diferença entre "as senhas estavam em MD5 sem salt" e "estavam em bcrypt" é a diferença entre milhões de contas quebradas em horas e um incidente sem consequência prática. O serviço Have I Been Pwned permite consultar se uma senha aparece em vazamentos usando k-anonimato: seu navegador envia só os 5 primeiros caracteres do hash SHA-1, e o servidor devolve todos os que começam assim — sem nunca saber qual era o seu.

📦

Reprodutibilidade e cadeia de suprimentos

Docker identifica imagens por sha256:..., o npm e o pip guardam hashes de integridade no lockfile, e o Nix endereça pacotes pelo hash de todas as suas entradas. Tudo isso responde à mesma pergunta: "o artefato que estou instalando é exatamente o que foi publicado?" — a defesa básica contra ataques à cadeia de suprimentos.

← Trilha: Segurança e criptografia Próxima: Criptografia simétrica →