Guia Completo de Funções Hash: SHA-2, SHA-3, BLAKE3, Rainbow Tables e Salt Já leu

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

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.

Referências


Artigos relacionados