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:
- Hash a mensagem $M$ para obter $h = H(M)$
- Assinar o hash com chave privada (ex: RSA)
- 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¶
- NIST Special Publication 800-180: SHA-2 e SHA-3
- Stinson, D. R., & Trappe, W. (2015). Cryptography: Theory and Practice. CRC Press.
- Daemen, J., & Rijmen, V. (2002). The Design of Rijndael. Springer.
- Bernstein, D. J. (2009). "SHA-3 Candidate Designs".