Fundamentos de Funções Hash
Uma função hash é um algoritmo determinístico que transforma dados de tamanho arbitrário em uma sequência de bits de tamanho fixo, chamada de digest ou hash. A característica fundamental é que sempre produz a mesma saída para a mesma entrada, mas uma pequena alteração nos dados de entrada gera um hash completamente diferente. Isso torna as funções hash extremamente úteis para verificação de integridade, armazenamento seguro de senhas e estruturas de dados.
As funções hash criptográficas modernas devem atender a três propriedades críticas: resistência à pré-imagem (é computacionalmente impossível encontrar a entrada original dado um hash), resistência à segunda pré-imagem (é impossível encontrar duas entradas diferentes que produzam o mesmo hash) e resistência à colisão (é impossível encontrar duas entradas distintas com o mesmo hash). Quando qualquer uma dessas propriedades é quebrada, a função é considerada comprometida e deve ser descontinuada para fins criptográficos.
SHA-2: O Padrão Consolidado
O SHA-2 (Secure Hash Algorithm 2) foi publicado pelo NIST em 2001 e permanece como o padrão de facto para aplicações criptográficas em todo o mundo. A família SHA-2 inclui SHA-224, SHA-256, SHA-384 e SHA-512, onde o número indica o tamanho do digest em bits. O SHA-256 é o mais utilizado, produzindo um hash de 256 bits (64 caracteres hexadecimais), e é amplamente adotado em blockchain, certificados digitais e sistemas de segurança críticos.
SHA-2 é baseado em operações matemáticas bem definidas, incluindo rotações bit a bit, operações lógicas AND/XOR e adições modulares. O algoritmo processa os dados de entrada em blocos de 512 bits (para SHA-256) ou 1024 bits (para SHA-512), aplicando múltiplas rodadas de transformação. A segurança do SHA-2 ainda não foi quebrada em ataques práticos, embora pesquisadores continuem investigando melhorias teóricas.
Implementação SHA-256 em Python
import hashlib
# Exemplo básico de SHA-256
mensagem = "Olá, mundo!"
hash_objeto = hashlib.sha256(mensagem.encode('utf-8'))
hash_hexadecimal = hash_objeto.hexdigest()
print(f"Mensagem: {mensagem}")
print(f"SHA-256: {hash_hexadecimal}")
# Saída: SHA-256: 315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3
# Verificação de integridade
def verificar_integridade(dados, hash_esperado):
hash_calculado = hashlib.sha256(dados.encode('utf-8')).hexdigest()
return hash_calculado == hash_esperado
dados = "Arquivo importante"
hash_arquivo = hashlib.sha256(dados.encode('utf-8')).hexdigest()
print(f"\nVerificação: {verificar_integridade(dados, hash_arquivo)}")
SHA-512 para Maior Segurança
import hashlib
# SHA-512 oferece maior resistência a ataques futuros
mensagem = "Dados sensíveis"
hash_512 = hashlib.sha512(mensagem.encode('utf-8')).hexdigest()
print(f"SHA-512 ({len(hash_512)} caracteres): {hash_512}")
# Comparação de desempenho entre SHA-256 e SHA-512
import timeit
tempo_sha256 = timeit.timeit(
lambda: hashlib.sha256(b"teste" * 1000).hexdigest(),
number=10000
)
tempo_sha512 = timeit.timeit(
lambda: hashlib.sha512(b"teste" * 1000).hexdigest(),
number=10000
)
print(f"\nSHA-256: {tempo_sha256:.4f}s para 10000 iterações")
print(f"SHA-512: {tempo_sha512:.4f}s para 10000 iterações")
SHA-3 e BLAKE3: Algoritmos de Próxima Geração
SHA-3 (Keccak) foi selecionado como padrão oficial pelo NIST em 2015, após uma competição internacional de 5 anos. Diferentemente do SHA-2, que usa construção de Merkle-Damgård, SHA-3 utiliza a esponja criptográfica, uma construção diferente que oferece maior flexibilidade e segurança comprovada. SHA-3 produz hashes de tamanho similar ao SHA-2 (SHA3-256, SHA3-512), mas sua estrutura interna oferece proteção adicional contra certos tipos de ataques teóricos.
BLAKE3 é um algoritmo mais recente (2020) que oferece velocidade superior ao SHA-3 enquanto mantém segurança equivalente. Desenvolvido por Jean-Philippe Aumasson, BLAKE3 é extremamente rápido em processadores modernos, parallelizável e possui design elegante baseado em árvore binária. Embora ainda não seja tão amplamente adotado quanto SHA-2 ou SHA-3, BLAKE3 está ganhando tração em projetos modernos como Btrfs (sistema de arquivos Linux) e Argon2 (derivação de chaves).
Implementação SHA-3 em Python
from Crypto.Hash import SHA3_256, SHA3_512
# SHA-3 requer a biblioteca pycryptodome
# Instale com: pip install pycryptodome
mensagem = b"Exemplo com SHA-3"
# SHA3-256
hash_sha3_256 = SHA3_256.new(mensagem)
print(f"SHA3-256: {hash_sha3_256.hexdigest()}")
# SHA3-512
hash_sha3_512 = SHA3_512.new(mensagem)
print(f"SHA3-512: {hash_sha3_512.hexdigest()}")
# Comparação com SHA-2
import hashlib
hash_sha256 = hashlib.sha256(mensagem).hexdigest()
print(f"\nSHA-256: {hash_sha256}")
print(f"\nNote que SHA3-256 e SHA-256 produzem saídas diferentes")
print(f"mesmo para a mesma entrada, apesar de nomes similares.")
Implementação BLAKE3 em Python
# BLAKE3 requer: pip install blake3
import blake3
mensagem = b"Exemplo com BLAKE3"
# Hash básico
hash_blake3 = blake3.blake3(mensagem).hexdigest()
print(f"BLAKE3: {hash_blake3}")
# BLAKE3 suporta comprimento customizável de saída
hash_256bits = blake3.blake3(mensagem).digest(32) # 32 bytes = 256 bits
print(f"BLAKE3 (256 bits): {hash_256bits.hex()}")
hash_512bits = blake3.blake3(mensagem).digest(64) # 64 bytes = 512 bits
print(f"BLAKE3 (512 bits): {hash_512bits.hex()}")
# BLAKE3 com chave (para uso em MAC - Message Authentication Code)
chave = b"chave_secreta"
blake3_com_chave = blake3.blake3(mensagem, key=chave)
print(f"\nBLAKE3 com chave: {blake3_com_chave.hexdigest()}")
Comparação de Velocidade
import hashlib
import timeit
import blake3
dados = b"X" * 10000 # 10KB de dados
# SHA-256
tempo_sha256 = timeit.timeit(
lambda: hashlib.sha256(dados).hexdigest(),
number=1000
)
# SHA3-256
from Crypto.Hash import SHA3_256
tempo_sha3 = timeit.timeit(
lambda: SHA3_256.new(dados).hexdigest(),
number=1000
)
# BLAKE3
tempo_blake3 = timeit.timeit(
lambda: blake3.blake3(dados).hexdigest(),
number=1000
)
print(f"Processamento de 10KB x 1000 iterações:")
print(f"SHA-256: {tempo_sha256:.4f}s")
print(f"SHA3-256: {tempo_sha3:.4f}s")
print(f"BLAKE3: {tempo_blake3:.4f}s")
print(f"\nBLAKE3 é aproximadamente 2-3x mais rápido que SHA-3")
Rainbow Tables: O Ataque Clássico
Rainbow tables são estruturas de dados pré-computadas que armazenam millions de hashes comuns juntamente com seus valores originais. O ataque funciona da seguinte forma: um atacante compila uma tabela contendo hashes de palavras-chave comuns, dicionários inteiros, variações com números e símbolos, e expressões regulares. Quando obtém um arquivo com hashes de senhas (por exemplo, através de um vazamento de banco de dados), simplesmente procura o hash na tabela para encontrar a senha original em tempo O(1).
A efetividade das rainbow tables depende de dois fatores críticos: a qualidade do dicionário utilizado e o tamanho da saída da função hash. Hashes menores (como MD5 de 128 bits) requerem menos armazenamento para cobrir todo o espaço de saída possível. Hashes maiores como SHA-256 aumentam exponencialmente o tamanho da tabela necessária. Uma rainbow table completa para SHA-256 seria fisicamente impossível de construir e armazenar com tecnologia atual, tornando-a impraticável para esse algoritmo específico.
Demonstração de Rainbow Table Simples
import hashlib
import json
# Criar uma rainbow table pequena (demonstração educacional)
def criar_rainbow_table(palavras_lista):
"""Cria uma tabela rainbow básica"""
rainbow_table = {}
for palavra in palavras_lista:
hash_sha256 = hashlib.sha256(palavra.encode()).hexdigest()
rainbow_table[hash_sha256] = palavra
return rainbow_table
# Dicionário pequeno de teste
dicionario = [
"senha123", "admin", "12345678", "qwerty",
"password", "letmein", "welcome", "monkey",
"dragon", "master", "sunshine", "princess"
]
# Gerar a tabela
rainbow_table = criar_rainbow_table(dicionario)
print(f"Rainbow table criada com {len(rainbow_table)} entradas")
# Exemplo de ataque: descobrir senha dado um hash
hash_alvo = hashlib.sha256(b"senha123").hexdigest()
print(f"\nHash alvo: {hash_alvo}")
if hash_alvo in rainbow_table:
senha_descoberta = rainbow_table[hash_alvo]
print(f"Senha descoberta: {senha_descoberta}")
else:
print("Senha não encontrada na rainbow table")
# Demonstração: salvar e carregar a tabela
with open("rainbow_table.json", "w") as f:
json.dump(rainbow_table, f)
print(f"\nRainbow table salva em disco ({len(json.dumps(rainbow_table))} bytes)")
Por Que Salt Anula Rainbow Tables
import hashlib
# Sem salt - vulnerável a rainbow tables
senha = "senha123"
hash_sem_salt = hashlib.sha256(senha.encode()).hexdigest()
print(f"Hash sem salt: {hash_sem_salt}")
# Com salt - imune a rainbow tables
import os
salt = os.urandom(16) # 16 bytes aleatórios
hash_com_salt = hashlib.sha256(salt + senha.encode()).hexdigest()
print(f"Salt (hex): {salt.hex()}")
print(f"Hash com salt: {hash_com_salt}")
# Mesmo com rainbow table, o atacante não consegue encontrar
# porque o salt torna cada hash único mesmo para mesma senha
print(f"\nGerando hash da mesma senha com salt diferente:")
salt2 = os.urandom(16)
hash_com_salt2 = hashlib.sha256(salt2 + senha.encode()).hexdigest()
print(f"Salt2 (hex): {salt2.hex()}")
print(f"Hash com salt2: {hash_com_salt2}")
print(f"\nOs hashes são completamente diferentes apesar da mesma senha!")
Salt: Proteção Contra Ataques Pré-Computados
Salt é um valor aleatório e único adicionado aos dados antes do hash ser calculado. Sua função principal é destruir qualquer eficiência de ataque por rainbow tables, uma vez que cada password, quando combinada com um salt diferente, produz um hash completamente diferente. Um atacante precisaria pré-computar não apenas hashes de palavras comuns, mas também todas as combinações desses hashes com todos os possíveis salts, tornando o ataque impraticável.
A implementação correta de salt requer três práticas essenciais: usar geradores de números aleatórios criptograficamente seguros (como os.urandom em Python), usar salts de tamanho adequado (mínimo 16 bytes para resistir a ataques de força bruta), e armazenar o salt junto com o hash (não é necessário manter o salt secreto, apenas aleatório e único). Algoritmos como bcrypt, scrypt e PBKDF2 implementam salt automaticamente com parâmetros seguros padrão.
Implementação Segura com PBKDF2
import hashlib
import os
import binascii
def hash_senha_com_salt(senha, salt=None):
"""
Hash de senha usando PBKDF2 com salt
PBKDF2 é específico para derivação de chaves a partir de senhas
"""
if salt is None:
salt = os.urandom(32) # 32 bytes de salt aleatório
# PBKDF2 com 100.000 iterações (padrão OWASP)
hash_bytes = hashlib.pbkdf2_hmac(
'sha256',
senha.encode('utf-8'),
salt,
iterations=100000
)
return salt, hash_bytes
def verificar_senha(senha, salt, hash_armazenado):
"""Verifica se a senha corresponde ao hash armazenado"""
_, hash_novo = hash_senha_com_salt(senha, salt)
return hash_novo == hash_armazenado
# Exemplo de uso
senha_usuario = "MinhaSenhaForte123!"
salt, hash_resultado = hash_senha_com_salt(senha_usuario)
print(f"Senha: {senha_usuario}")
print(f"Salt (hex): {binascii.hexlify(salt).decode()}")
print(f"Hash (hex): {binascii.hexlify(hash_resultado).decode()}")
# Armazenar salt + hash no banco de dados
dados_armazenados = {
'salt': binascii.hexlify(salt).decode(),
'hash': binascii.hexlify(hash_resultado).decode()
}
# Verificação posterior
print(f"\nVerificação com senha correta:")
print(verificar_senha(
"MinhaSenhaForte123!",
binascii.unhexlify(dados_armazenados['salt']),
binascii.unhexlify(dados_armazenados['hash'])
))
print(f"Verificação com senha incorreta:")
print(verificar_senha(
"SenhaErrada",
binascii.unhexlify(dados_armazenados['salt']),
binascii.unhexlify(dados_armazenados['hash'])
))
Usando bcrypt (Recomendado para Produção)
import bcrypt
# bcrypt já implementa salt automaticamente
def hash_senha_bcrypt(senha):
"""
bcrypt é especificamente desenvolvido para hashing de senhas
e automaticamente usa salt seguro
"""
# Cost parameter (padrão 12) controla a quantidade de computação
hash_bytes = bcrypt.hashpw(
senha.encode('utf-8'),
bcrypt.gensalt(rounds=12)
)
return hash_bytes.decode('utf-8')
def verificar_senha_bcrypt(senha, hash_armazenado):
"""Verifica senha contra hash bcrypt"""
return bcrypt.checkpw(
senha.encode('utf-8'),
hash_armazenado.encode('utf-8')
)
# Uso
senha = "SenhaSegura123"
hash_bcrypt = hash_senha_bcrypt(senha)
print(f"Senha original: {senha}")
print(f"Hash bcrypt: {hash_bcrypt}")
print(f"\nVerificação: {verificar_senha_bcrypt(senha, hash_bcrypt)}")
# bcrypt inclui informações sobre parâmetros no próprio hash
print(f"\nAnalisando componentes do hash bcrypt:")
print(f"Algoritmo: {hash_bcrypt[:4]}") # $2b$
print(f"Cost: {hash_bcrypt[4:6]}") # 12
print(f"Salt + Hash: {hash_bcrypt[7:]}")
Comparação: Com vs. Sem Salt
import hashlib
import os
import binascii
senhas = ["admin", "password", "12345678"]
print("HASHES SEM SALT - VULNERÁVEL A RAINBOW TABLES:")
print("=" * 60)
for senha in senhas:
hash_objeto = hashlib.sha256(senha.encode()).hexdigest()
print(f"{senha:15} -> {hash_objeto}")
print("\n\nHASHES COM SALT - RESISTENTE A RAINBOW TABLES:")
print("=" * 60)
for senha in senhas:
salt = os.urandom(16)
hash_com_salt = hashlib.pbkdf2_hmac(
'sha256',
senha.encode(),
salt,
100000
)
salt_hex = binascii.hexlify(salt).decode()
hash_hex = binascii.hexlify(hash_com_salt).decode()
print(f"{senha:15} -> Salt: {salt_hex[:16]}... Hash: {hash_hex[:32]}...")
print("\n\nNota: Cada execução produz diferentes hashes com salt")
print("porque o salt é aleatório a cada vez!")
Aplicações Práticas e Boas Práticas
Na prática moderna de segurança, as escolhas de algoritmo devem ser baseadas no contexto específico. Para armazenamento de senhas, nunca use SHA-256 ou SHA-3 diretamente — use algoritmos específicos como bcrypt, scrypt ou Argon2 que implementam iterações múltiplas e salt automático. Para verificação de integridade de arquivos, SHA-256 é adequado. Para novos projetos críticos que requerem resistência futura, considere SHA-3 ou BLAKE3.
O tamanho do salt é crítico: menos de 8 bytes é insuficiente mesmo com funções de derivação lenta, enquanto 16 bytes ou mais é padrão industrial. O número de iterações também importa — PBKDF2 deve usar pelo menos 100.000 iterações (OWASP 2023 recomenda 1.000.000 iterações para novos sistemas), bcrypt usa parâmetro "cost" equivalente, e Argon2 oferece controle sobre tempo, memória e paralelismo. Em nenhum cenário implementar sua própria função hash é aconselhável — use bibliotecas testadas e auditadas.
import argon2
import hashlib
import os
# Argon2 é considerado a melhor opção atual para derivação de chaves
def hash_argon2(senha, salt=None):
"""
Argon2 é vencedor da competição Password Hashing Competition (2015)
Oferece resistência a GPU e ASIC attacks através de uso intensivo de memória
"""
if salt is None:
salt = os.urandom(16)
hasher = argon2.PasswordHasher(
time_cost=2, # número de iterações
memory_cost=65536, # ~64MB de memória
parallelism=4 # 4 threads paralelos
)
return hasher.hash(senha)
# Verificação
try:
senha = "SenhaForte123"
hash_argon = hash_argon2(senha)
print(f"Hash Argon2: {hash_argon}")
# Verificação
from argon2 import PasswordHasher
verificador = PasswordHasher()
try:
verificador.verify(hash_argon, senha)
print("Senha verificada com sucesso!")
except argon2.exceptions.VerifyMismatchError:
print("Senha incorreta!")
except ImportError:
print("Para usar Argon2: pip install argon2-cffi")
Conclusão
Aprendemos que funções hash criptográficas SHA-2, SHA-3 e BLAKE3 são ferramentas fundamentais de segurança, cada uma com características distintas: SHA-2 permanece o padrão consolidado com segurança comprovada, SHA-3 oferece construção matemática diferente com proteções teóricas adicionais, e BLAKE3 fornece velocidade superior com segurança equivalente. Rainbow tables representam um ataque clássico mas devastador contra hashes sem proteção, demonstrando por que pré-computação em massa é possível quando não há variação nos dados de entrada. Salt é a defesa fundamental e obrigatória contra esse tipo de ataque, garantindo que cada hash seja único mesmo para senhas idênticas, tornando pré-computação impraticável — mas salt sozinho é insuficiente; algoritmos específicos como bcrypt, scrypt e Argon2 devem ser usados para senhas em produção.