Pular para conteúdo

Funções Hash Criptográficas (Cryptographic Hash Functions)

Funções hash criptográficas são blocos fundamentais dos sistemas de segurança modernos. Elas transformam dados de entrada de comprimento arbitrário em saídas de tamanho fixo chamadas "hashes" ou "digests", permitindo verificação de integridade, assinaturas digitais e armazenamento seguro de senhas.

Fundamento Matemático

Definição

Uma função hash criptográfica $H$ mapeia uma mensagem de comprimento arbitrário $M$ para uma saída de tamanho fixo:

$$H: {0,1}^* \rightarrow {0,1}^n$$

Onde: - Entrada $M$ pode ter qualquer comprimento (bits ou bytes) - Saída é sempre exatamente $n$ bits (ex: 256 bits para SHA-256)

Propriedades Chave

1. Determinismo

A mesma entrada sempre produz a mesma saída: $$H(M_1) = H(M_2) \implies M_1 = M_2$$

2. Resistência à Pré-imagem (Propriedade de Um Só Sentido)

Dado um hash $h$, deve ser computacionalmente inviável encontrar qualquer mensagem $M$ tal que: $$H(M) = h$$

3. Resistência à Pré-imagem Secundária

Dada uma entrada $M_1$, deve ser inviável encontrar uma entrada diferente $M_2$ tal que: $$H(M_1) = H(M_2)$$

4. Resistência a Colisões

Deve ser computacionalmente inviável encontrar duas mensagens distintas $M_1, M_2$ tais que: $$H(M_1) = H(M_2)$$

SHA-256 (Secure Hash Algorithm 256-bit)

SHA-256 é parte da família SHA-2 desenvolvida pelo NIST e publicada em 2001. Produz um valor hash de 256 bits (32 bytes).

Estrutura

O SHA-256 processa dados de entrada em blocos de 512 bits usando: - 8 variáveis de trabalho ($a, b, c, d, e, f, g, h$), cada uma com 32 bits - 64 rodadas de computação com constantes e funções diferentes - Expansão do schedule da mensagem: Expande bloco de 512 bits para 64 palavras

Visão Geral do Pseudocódigo

def SHA256(mensagem):
    # Inicializar valores hash (primeiros 32 bits das partes fracionárias de raízes quadradas)
    h = [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
         0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19]

    # Processar cada bloco de 512 bits
    for bloco in blocos_da_mensagem:
        # Expandir schedule da mensagem (64 palavras)
        w = expandir_schedule_mensagem(bloco)

        # Inicializar variáveis de trabalho
        a, b, c, d, e, f, g, h = h

        # Realizar 64 rodadas
        for i in range(64):
            S1 = rotacao_direita(e, 6)
            ch = (e & f) ^ ((~e) & g)
            temp1 = h + S1 + ch + K[i] + w[i]
            S0 = rotacao_direita(a, 2)
            maj = (a & b) ^ (a & c) ^ (b & c)
            temp2 = maj + constante

            h = g
            g = f
            f = e
            e = d + temp1
            d = c
            c = b
            b = a
            a = temp1 + temp2

        # Adicionar ao valor hash
        for i in range(8):
            h[i] = (h[i] + resultado[i]) & 0xFFFFFFFF

    return concatenar(h)

Exemplos de Hashes

Entrada SHA-256 Hash
"abc" ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
"" (vazio) e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
"hello" a591a6d40bf420404a011733cfb7b190d62c65bf0bcda31b171af4bd51b39a6f

Ataques de Colisão e Paradoxo do Aniversário

O Problema do Aniversário

A probabilidade de encontrar uma colisão aumenta dramaticamente com o número de tentativas. Para SHA-256 (saída de 256 bits):

Tentativas Probabilidade de Encontrar Colisão
$2^{128}$ ~50%
$2^{129}$ ~75%
$2^{256}$ Necessário para força bruta (praticamente impossível)

Isso é conhecido como ataque do aniversário - você precisa apenas $\sqrt{N}$ tentativas para encontrar uma colisão entre $N$ saídas possíveis.

Ataques de Colisão no Mundo Real

Colisões MD5 (2004-2017)

O MD5 foi quebrado para propósitos práticos:

# Exemplo de criação de colisões MD5 (simplificado)
def criar_colisao_md5():
    # Dois arquivos diferentes com o mesmo hash MD5
    arquivo1 = gerar_arquivo_com_prefixo("A")
    arquivo2 = gerar_arquivo_com_prefixo("B")

    assert md5(arquivo1) == md5(arquivo2)  # Verdadeiro!

O SHA-1 também foi quebrado em 2017 (ataque SHAttered), demonstrando que mesmo algoritmos "seguros" podem ser comprometidos.

Implementação em Java

Função Hash SHA-256

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.nio.charset.StandardCharsets;

public class FuncoesHash {

    /**
     * Calcula o hash SHA-256 de uma string.
     * @param input A string de entrada para hashing
     * @return Representação hexadecimal do hash
     */
    public static String sha256(String input) throws NoSuchAlgorithmException {
        MessageDigest digest = MessageDigest.getInstance("SHA-256");
        byte[] hashBytes = digest.digest(input.getBytes(StandardCharsets.UTF_8));

        StringBuilder hexString = new StringBuilder();
        for (byte b : hashBytes) {
            String hex = Integer.toHexString(0xff & b);
            if (hex.length() == 1) hexString.append('0');
            hexString.append(hex);
        }

        return hexString.toString();
    }

    /**
     * Calcula o hash SHA-256 de bytes brutos.
     */
    public static String sha256Bytes(byte[] input) throws NoSuchAlgorithmException {
        MessageDigest digest = MessageDigest.getInstance("SHA-256");
        byte[] hashBytes = digest.digest(input);

        StringBuilder hexString = new StringBuilder();
        for (byte b : hashBytes) {
            String hex = Integer.toHexString(0xff & b);
            if (hex.length() == 1) hexString.append('0');
            hexString.append(hex);
        }

        return hexString.toString();
    }

    /**
     * Verifica uma mensagem contra seu hash esperado.
     */
    public static boolean verificar(String mensagem, String hashEsperado) throws NoSuchAlgorithmException {
        return sha256(mensagem).equals(hashEsperado);
    }

    public static void main(String[] args) throws Exception {
        String input = "Olá, Mundo!";

        System.out.println("Entrada: " + input);
        System.out.println("SHA-256: " + sha256(input));

        // Verificar
        boolean isValid = verificar(input, sha256(input));
        System.out.println("Verificação passou: " + isValid);
    }
}

Usando MessageDigest do Java (Alternativa)

import java.security.MessageDigest;
import java.nio.charset.StandardCharsets;

public class HashSimples {

    public static String hash(String input) throws Exception {
        try (MessageDigest md = MessageDigest.getInstance("SHA-256")) {
            byte[] hashBytes = md.digest(input.getBytes(StandardCharsets.UTF_8));
            return bytesParaHex(hashBytes);
        }
    }

    private static String bytesParaHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }
}

Aplicações de Funções Hash Criptográficas

1. Armazenamento de Senhas

Nunca armazene senhas em texto puro! Use uma função hash com salt:

public class ArmazenamentoSenha {

    private static final int TAMANHO_SALT = 32; // bytes

    /**
     * Hasha uma senha com um salt aleatório.
     */
    public static String hasharSenha(String senha) throws Exception {
        byte[] salt = gerarSaltAleatorio();

        try (MessageDigest md = MessageDigest.getInstance("SHA-256")) {
            // Combinar salt e senha
            byte[] combinado = new byte[salt.length + senha.getBytes().length];
            System.arraycopy(salt, 0, combinado, 0, salt.length);
            System.arraycopy(senha.getBytes(), 0, combinado, salt.length, senha.getBytes().length);

            byte[] hash = md.digest(combinado);

            // Armazenar salt e hash juntos (ex: "salt:hash")
            return codificarBase64(salt) + ":" + bytesParaHex(hash);
        }
    }

    /**
     * Verifica uma senha contra o hash armazenado.
     */
    public static boolean verificarSenha(String senha, String hashArmazenado) throws Exception {
        String[] partes = hashArmazenado.split(":");
        byte[] salt = decodificarBase64(partes[0]);

        try (MessageDigest md = MessageDigest.getInstance("SHA-256")) {
            byte[] combinado = new byte[salt.length + senha.getBytes().length];
            System.arraycopy(salt, 0, combinado, 0, salt.length);
            System.arraycopy(senha.getBytes(), 0, combinado, salt.length, senha.getBytes().length);

            byte[] hashCalculado = md.digest(combinado);
            return Arrays.equals(hashCalculado, decodificarBase64(partes[1]));
        }
    }

    private static byte[] gerarSaltAleatorio() {
        // Usar fonte segura de aleatoriedade em produção
        return new java.security.SecureRandom().generateNewByteArray(TAMANHO_SALT);
    }
}

2. Assinaturas Digitais

Funções hash são usadas para criar assinaturas digitais:

  1. Hash a mensagem $M$ para obter $h = H(M)$
  2. Assinar o hash com chave privada (ex: RSA)
  3. Verificar hashando e comparando

3. Blockchain e Criptomoedas

O Bitcoin usa SHA-256 para: - Prova de trabalho no mineração - Criação de hashes de bloco - Garantia da integridade da cadeia

# Exemplo simplificado de mineração do Bitcoin
def minerar_bloco(hash_anterior, transacoes, dificuldade):
    nonce = 0

    while True:
        # Criar dados do bloco
        dados_bloco = hash_anterior + str(transacoes) + str(nonce)

        # Hashar o bloco
        hash_bloco = sha256(dados_bloco.encode())

        # Verificar se hash atende ao requisito de dificuldade
        if int(hash_bloco, 16)[:dificuldade] == '0' * dificuldade:
            return hash_bloco

        nonce += 1

4. Verificação de Integridade de Arquivos

Arquivos baixados frequentemente incluem um checksum (hash) para verificar integridade:

# Linux/Mac
sha256sum arquivo.txt > arquivo.txt.sha256
sha256sum -c arquivo.txt.sha256

# Windows
certutil -hashfile arquivo.txt SHA256

Considerações de Segurança

1. Salt para Senhas

Sempre use um salt único e aleatório por senha para prevenir: - Ataques com tabelas arco-íris - Identificar senhas duplicadas entre usuários

2. Estiramento de Chaves (Key Stretching)

Para hashing de senhas, considere usar algoritmos especializados como: - bcrypt: Fator de custo embutido, segurança adaptativa - scrypt: Função hard-core em memória - Argon2: Vencedor do Password Hashing Competition (mais seguro)

// Usando bcrypt para hashing de senhas
import org.mindrot.jbcrypt.BCrypt;

public class HashingSenhaSegura {

    public static String hasharSenha(String senha) {
        // Fator de custo 12 = ~4096 iterações
        return BCrypt.hashpw(senha, BCrypt.gensalt(12));
    }

    public static boolean verificarSenha(String senha, String hashada) {
        return BCrypt.checkpw(senha, hashada);
    }
}

3. Seleção de Comprimento do Hash

Algoritmo Tamanho da Saída Status Recomendação
MD5 128 bits Quebrado Nunca use
SHA-1 160 bits Quebrado Evite
SHA-256 256 bits Seguro Recomendado
SHA-384 384 bits Seguro Para alta segurança
SHA-512 512 bits Seguro Alternativa

4. Ameaças da Computação Quântica

Computadores quânticos futuros poderiam quebrar funções hash atuais usando o algoritmo de Grover, reduzindo a segurança efetiva pela metade: - SHA-256 forneceria ~128 bits de segurança em vez de 256 bits - Migração para criptografia pós-quântica pode ser necessária

Ataques Comuns

Ataque de Extensão de Comprimento

Algumas construções hash (como Merkle-Damgård) são vulneráveis a ataques de extensão de comprimento. Dado $H(M)$ e o comprimento de $M$, um atacante pode calcular $H(M || \text{padding} || \text{sufixo})$ sem conhecer $M$.

Mitigação: Use HMAC ou SHA-3 em vez de funções hash cruas para operações com chave.

Ataque à Pré-imagem Secundária

Encontrar uma mensagem diferente que produza o mesmo hash como uma mensagem dada. Embora teoricamente possível, é computacionalmente inviável para funções hash bem projetadas.

Referências

  1. NIST Special Publication 800-180: SHA-2 e SHA-3
  2. Stinson, D. R., & Trappe, W. (2015). Cryptography: Theory and Practice. CRC Press.
  3. Daemen, J., & Rijmen, V. (2002). The Design of Rijndael. Springer.
  4. Bernstein, D. J. (2009). "SHA-3 Candidate Designs".