Hashing criptográfico: SHA-256 e integridade de dados

1. O que é Hashing Criptográfico e Por Que Importa para Devs

1.1. Definição: Função hash unidirecional e determinística

Uma função hash criptográfica é um algoritmo matemático que transforma dados de entrada de qualquer tamanho em uma saída de tamanho fixo, chamada de digest ou hash value. A operação é unidirecional: é computacionalmente inviável reverter o hash para obter os dados originais. É também determinística: a mesma entrada sempre produz a mesma saída.

1.2. Propriedades essenciais

Para ser considerada segura, uma função hash criptográfica deve possuir três propriedades fundamentais:

  • Resistência a colisão: é extremamente difícil encontrar duas entradas diferentes que produzam o mesmo hash.
  • Resistência a pré-imagem: dado um hash, é inviável encontrar qualquer entrada que gere aquele hash.
  • Resistência a segunda pré-imagem: dada uma entrada e seu hash, é inviável encontrar outra entrada que produza o mesmo hash.

1.3. Diferenças cruciais: Hashing criptográfico vs. não criptográfico

Hashes não criptográficos como CRC32 ou MD5 (considerado obsoleto para segurança) são rápidos e úteis para verificação de erros acidentais, mas não resistem a ataques intencionais. MD5, por exemplo, sofre de vulnerabilidades de colisão desde 2004. SHA-256 pertence à família SHA-2, projetada especificamente para resistir a ataques criptográficos modernos.

2. SHA-256: O Padrão Ouro na Prática

2.1. Estrutura interna

SHA-256 processa dados em blocos de 512 bits, utilizando funções de compressão e operações lógicas bit a bit. Sua saída é de 256 bits (32 bytes), geralmente representada como uma string hexadecimal de 64 caracteres.

2.2. Por que SHA-256 é preferível a SHA-1 e MD5

SHA-1 (160 bits) sofreu ataques de colisão práticos em 2017 (SHAttered). MD5 (128 bits) tem ataques de colisão que podem ser executados em segundos em hardware moderno. SHA-256, até o momento, não possui ataques públicos viáveis que comprometam suas propriedades.

2.3. Exemplo de código: hash de string e arquivo com Python

import hashlib

# Hash de string
texto = "Dados sensíveis para integridade"
hash_string = hashlib.sha256(texto.encode('utf-8')).hexdigest()
print(f"Hash da string: {hash_string}")

# Hash de arquivo (ideal para grandes arquivos)
def hash_arquivo(caminho_arquivo):
    sha256 = hashlib.sha256()
    with open(caminho_arquivo, 'rb') as f:
        for bloco in iter(lambda: f.read(4096), b''):
            sha256.update(bloco)
    return sha256.hexdigest()

# Exemplo de uso (crie um arquivo teste.txt antes)
# print(hash_arquivo('teste.txt'))

3. Integridade de Dados: Verificando que Nada Foi Alterado

3.1. Checksums: comparar hashes de arquivos baixados

Distribuidores de software frequentemente publicam checksums SHA-256 para que usuários verifiquem a integridade dos downloads. O comando sha256sum no Linux permite essa verificação:

# Terminal Linux
sha256sum arquivo-baixado.iso
# Compare a saída com o hash publicado no site oficial

3.2. Hashing de mensagens em APIs

APIs podem incluir um hash do payload em headers para garantir que a mensagem não foi adulterada em trânsito. O servidor recalcula o hash e compara com o enviado.

3.3. Exemplo prático: verificar integridade de um arquivo de configuração

import hashlib
import json

config_original = {
    "host": "localhost",
    "porta": 5432,
    "debug": False
}

# Salvar hash esperado (simulando distribuição segura)
hash_esperado = hashlib.sha256(
    json.dumps(config_original, sort_keys=True).encode()
).hexdigest()

# Simular recebimento do arquivo (poderia ser adulterado)
config_recebida = {
    "host": "localhost",
    "porta": 5432,
    "debug": True  # Alterado maliciosamente
}

hash_recebido = hashlib.sha256(
    json.dumps(config_recebida, sort_keys=True).encode()
).hexdigest()

if hash_recebido == hash_esperado:
    print("Integridade verificada: arquivo não foi alterado")
else:
    print("ALERTA: O arquivo foi adulterado!")

4. Armazenamento Seguro de Senhas: Nunca em Texto Plano

4.1. Erro comum: hash simples de senhas

Usar apenas SHA-256(senha) é inseguro. Atacantes podem usar rainbow tables (tabelas pré-computadas de hashes) para reverter senhas comuns rapidamente.

4.2. Solução: salt + SHA-256 (ou melhor, bcrypt/argon2)

Um salt é um valor aleatório único por senha, concatenado antes do hash. Isso torna rainbow tables ineficazes. Para produção, prefira algoritmos específicos para senhas como bcrypt, scrypt ou argon2, que são intencionalmente lentos.

4.3. Exemplo de código: hash de senha com salt aleatório

import hashlib
import os

def hash_senha(senha):
    # Gerar salt aleatório de 16 bytes
    salt = os.urandom(16)

    # Combinar salt + senha e aplicar SHA-256 (múltiplas iterações para segurança)
    hash_obj = hashlib.pbkdf2_hmac(
        'sha256',
        senha.encode('utf-8'),
        salt,
        100000  # Número de iterações
    )

    # Retornar salt + hash (ambos necessários para verificação)
    return salt.hex() + ':' + hash_obj.hex()

def verificar_senha(senha, armazenado):
    salt_hex, hash_hex = armazenado.split(':')
    salt = bytes.fromhex(salt_hex)

    novo_hash = hashlib.pbkdf2_hmac(
        'sha256',
        senha.encode('utf-8'),
        salt,
        100000
    )

    return novo_hash.hex() == hash_hex

# Exemplo de uso
senha = "MinhaSenhaSegura123!"
armazenado = hash_senha(senha)
print(f"Armazenado: {armazenado}")
print(f"Verificação correta: {verificar_senha(senha, armazenado)}")
print(f"Verificação errada: {verificar_senha('senha_errada', armazenado)}")

5. HMAC: Hashing com Chave Secreta para Autenticação

5.1. O problema: hash simples não prova autoria

SHA-256 simples é vulnerável a ataques de extensão de comprimento: dado H(m), um atacante pode calcular H(m || padding || dados_adicionais) sem conhecer m. HMAC resolve isso.

5.2. HMAC-SHA256: combinação de chave secreta e hash

HMAC (Hash-based Message Authentication Code) usa uma chave secreta para autenticar mensagens. Garante tanto integridade quanto autenticidade.

5.3. Exemplo de código: gerar e verificar HMAC para tokens de API

import hmac
import hashlib

CHAVE_SECRETA = b"minha-chave-super-secreta-123"

def gerar_token_api(usuario_id, timestamp):
    mensagem = f"{usuario_id}:{timestamp}".encode('utf-8')
    token = hmac.new(
        CHAVE_SECRETA,
        mensagem,
        hashlib.sha256
    ).hexdigest()
    return f"{usuario_id}:{timestamp}:{token}"

def verificar_token_api(token_completo):
    try:
        usuario_id, timestamp, token_recebido = token_completo.split(':')
        mensagem = f"{usuario_id}:{timestamp}".encode('utf-8')

        token_esperado = hmac.new(
            CHAVE_SECRETA,
            mensagem,
            hashlib.sha256
        ).hexdigest()

        # Comparação segura contra timing attack
        return hmac.compare_digest(token_recebido, token_esperado)
    except:
        return False

# Exemplo de uso
token = gerar_token_api("user42", "2024-01-15T10:30:00")
print(f"Token gerado: {token}")
print(f"Token válido: {verificar_token_api(token)}")
print(f"Token adulterado: {verificar_token_api(token + 'x')}")

6. Armadilhas Comuns e Boas Práticas para Devs

6.1. Nunca usar hash para comparar senhas diretamente (timing attacks)

Comparações diretas com == vazam informações temporais. Sempre use hmac.compare_digest() (Python) ou funções equivalentes em outras linguagens.

6.2. Cuidado com encoding: UTF-8 vs bytes, normalização de strings

Sempre converta strings para bytes com .encode('utf-8') antes de hashing. Para dados que podem ter múltiplas representações Unicode (como "é" vs "e" + acento), normalize com unicodedata.normalize('NFC', texto).

6.3. Quando NÃO usar SHA-256

  • Para senhas: use bcrypt, scrypt ou argon2
  • Para certificados digitais: SHA-256 ainda é aceito, mas SHA-384 ou SHA-512 oferecem margem extra
  • Para sistemas que precisam de resistência quântica: considere SHA-3 (Keccak) ou algoritmos pós-quânticos
  • Para hashing extremamente rápido em sistemas embarcados: SHA-512/256 pode ser mais eficiente em hardware 64-bit

Referências