Skip to content

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

Cryptographic hash functions are fundamental building blocks of modern security systems. They transform arbitrary-length input data into fixed-size outputs called "hashes" or "digests," enabling integrity verification, digital signatures, and secure password storage.

Mathematical Foundation

Definition

A cryptographic hash function $H$ maps an arbitrary-length message $M$ to a fixed-length output:

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

Where: - Input $M$ can be any length (bits or bytes) - Output is always exactly $n$ bits (e.g., 256 bits for SHA-256)

Key Properties

1. Determinism

The same input always produces the same output: $$H(M_1) = H(M_2) \implies M_1 = M_2$$

2. Pre-image Resistance (One-way Property)

Given a hash $h$, it should be computationally infeasible to find any message $M$ such that: $$H(M) = h$$

3. Second Pre-image Resistance

Given an input $M_1$, it should be infeasible to find a different input $M_2$ such that: $$H(M_1) = H(M_2)$$

4. Collision Resistance

It should be computationally infeasible to find any two distinct messages $M_1, M_2$ such that: $$H(M_1) = H(M_2)$$

SHA-256 (Secure Hash Algorithm 256-bit)

SHA-256 is part of the SHA-2 family developed by NIST and published in 2001. It produces a 256-bit (32-byte) hash value.

Structure

SHA-256 processes input data in 512-bit blocks using: - 8 working variables ($a, b, c, d, e, f, g, h$), each 32 bits - 64 rounds of computation with different constants and functions - Message schedule expansion: Expands 512-bit block to 64 words

Pseudocode Overview

def SHA256(message):
    # Initialize hash values (first 32 bits of fractional parts of square roots)
    h = [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
         0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19]

    # Process each 512-bit block
    for block in message_blocks:
        # Expand message schedule (64 words)
        w = expand_message_schedule(block)

        # Initialize working variables
        a, b, c, d, e, f, g, h = h

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

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

        # Add to hash value
        for i in range(8):
            h[i] = (h[i] + result[i]) & 0xFFFFFFFF

    return concatenate(h)

Example Hashes

Input SHA-256 Hash
"abc" ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
"" (empty) e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
"hello" a591a6d40bf420404a011733cfb7b190d62c65bf0bcda31b171af4bd51b39a6f

Collision Attacks and Birthday Paradox

The Birthday Problem

The probability of finding a collision increases dramatically with the number of attempts. For SHA-256 (256-bit output):

Attempts Probability of Finding Collision
$2^{128}$ ~50%
$2^{129}$ ~75%
$2^{256}$ Required for brute force (practically impossible)

This is known as the birthday attack - you only need $\sqrt{N}$ attempts to find a collision among $N$ possible outputs.

Real-World Collision Attacks

MD5 Collisions (2004-2017)

MD5 was broken for practical purposes:

# Example of finding MD5 collisions (simplified)
def create_md5_collision():
    # Two different files with same MD5 hash
    file1 = generate_file_with_prefix("A")
    file2 = generate_file_with_prefix("B")

    assert md5(file1) == md5(file2)  # True!

SHA-1 was also broken in 2017 (SHAttered attack), demonstrating that even "secure" algorithms can be compromised.

Java Implementation

SHA-256 Hash Function

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

public class HashFunctions {

    /**
     * Computes the SHA-256 hash of a string.
     * @param input The input string to hash
     * @return Hexadecimal representation of the 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();
    }

    /**
     * Computes the SHA-256 hash of raw bytes.
     */
    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();
    }

    /**
     * Verifies a message against its expected hash.
     */
    public static boolean verify(String message, String expectedHash) throws NoSuchAlgorithmException {
        return sha256(message).equals(expectedHash);
    }

    public static void main(String[] args) throws Exception {
        String input = "Hello, World!";

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

        // Verify
        boolean isValid = verify(input, sha256(input));
        System.out.println("Verification passed: " + isValid);
    }
}

Using Java's MessageDigest (Alternative)

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

public class SimpleHash {

    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 bytesToHex(hashBytes);
        }
    }

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

Applications of Cryptographic Hash Functions

1. Password Storage

Never store passwords in plaintext! Use a hash function with salt:

public class PasswordStorage {

    private static final int SALT_LENGTH = 32; // bytes

    /**
     * Hashes a password with a random salt.
     */
    public static String hashPassword(String password) throws Exception {
        byte[] salt = generateRandomSalt();

        try (MessageDigest md = MessageDigest.getInstance("SHA-256")) {
            // Combine salt and password
            byte[] combined = new byte[salt.length + password.getBytes().length];
            System.arraycopy(salt, 0, combined, 0, salt.length);
            System.arraycopy(password.getBytes(), 0, combined, salt.length, password.getBytes().length);

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

            // Store salt and hash together (e.g., "salt:hash")
            return base64Encode(salt) + ":" + bytesToHex(hash);
        }
    }

    /**
     * Verifies a password against stored hash.
     */
    public static boolean verifyPassword(String password, String storedHash) throws Exception {
        String[] parts = storedHash.split(":");
        byte[] salt = base64Decode(parts[0]);

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

            byte[] computedHash = md.digest(combined);
            return Arrays.equals(computedHash, base64Decode(parts[1]));
        }
    }

    private static byte[] generateRandomSalt() {
        // Use secure random source in production
        return new java.security.SecureRandom().generateNewByteArray(SALT_LENGTH);
    }
}

2. Digital Signatures

Hash functions are used to create digital signatures:

  1. Hash the message $M$ to get $h = H(M)$
  2. Sign the hash with private key (e.g., RSA)
  3. Verify by hashing and comparing

3. Blockchain and Cryptocurrencies

Bitcoin uses SHA-256 for: - Mining proof-of-work - Creating block hashes - Ensuring chain integrity

# Simplified Bitcoin mining example
def mine_block(previous_hash, transactions, difficulty):
    nonce = 0

    while True:
        # Create block data
        block_data = previous_hash + str(transactions) + str(nonce)

        # Hash the block
        block_hash = sha256(block_data.encode())

        # Check if hash meets difficulty requirement
        if int(block_hash, 16)[:difficulty] == '0' * difficulty:
            return block_hash

        nonce += 1

4. File Integrity Verification

Downloaded files often include a checksum (hash) to verify integrity:

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

# Windows
certutil -hashfile file.txt SHA256

Security Considerations

1. Salt for Passwords

Always use a unique random salt per password to prevent: - Rainbow table attacks - Identifying duplicate passwords across users

2. Key Stretching

For password hashing, consider using specialized algorithms like: - bcrypt: Built-in cost factor, adaptive security - scrypt: Memory-hard function - Argon2: Winner of Password Hashing Competition (most secure)

// Using bcrypt for password hashing
import org.mindrot.jbcrypt.BCrypt;

public class SecurePasswordHashing {

    public static String hashPassword(String password) {
        // Cost factor 12 = ~4096 iterations
        return BCrypt.hashpw(password, BCrypt.gensalt(12));
    }

    public static boolean verifyPassword(String password, String hashed) {
        return BCrypt.checkpw(password, hashed);
    }
}

3. Hash Length Selection

Algorithm Output Size Status Recommendation
MD5 128 bits Broken Never use
SHA-1 160 bits Broken Avoid
SHA-256 256 bits Secure Recommended
SHA-384 384 bits Secure For high security
SHA-512 512 bits Secure Alternative

4. Quantum Computing Threats

Future quantum computers could break current hash functions using Grover's algorithm, reducing effective security by half: - SHA-256 would provide ~128-bit security instead of 256-bit - Migration to post-quantum cryptography may be necessary

Common Attacks

Length Extension Attack

Some hash constructions (like Merkle-Damgård) are vulnerable to length extension attacks. Given $H(M)$ and the length of $M$, an attacker can compute $H(M || \text{padding} || \text{suffix})$ without knowing $M$.

Mitigation: Use HMAC or SHA-3 instead of raw hash functions for keyed operations.

Second Pre-image Attack

Finding a different message that produces the same hash as a given message. While theoretically possible, it's computationally infeasible for properly designed hash functions.

References

  1. NIST Special Publication 800-180: SHA-2 and 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".