Implementing SHA-224 in Python

A step-by-step guide to building your own SHA-224 implementation in Python, with clear explanations and code examples.

20 min read
Last updated: May 10, 2025
Python 3.8+

Introduction

In this tutorial, we'll implement the SHA-224 cryptographic hash function in Python. SHA-224 is a member of the SHA-2 family of hash functions, producing a 224-bit (28-byte) hash value. It's essentially SHA-256 with different initialization vectors and truncated output.

By the end of this tutorial, you'll have:

  • A clear understanding of how SHA-224 works
  • Knowledge of Python's built-in cryptographic libraries
  • Your own custom SHA-224 implementation
  • Insights into performance optimization and security considerations
Important: While building your own implementation is educational, for production applications, you should use Python's built-in hashlib library, which provides optimized, well-tested implementations of cryptographic hash functions.

Understanding SHA-224

Before diving into implementation, let's understand what makes SHA-224 unique:

  • Relationship to SHA-256: SHA-224 is derived from SHA-256 with different initialization values and truncated output (224 bits instead of 256 bits).
  • Security Properties: SHA-224 offers 112-bit security against collision attacks (compared to SHA-256's 128-bit security level).
  • Process Overview: Like other SHA-2 variants, SHA-224 processes messages in 512-bit blocks and maintains an internal state that's updated through multiple rounds of computation.

At a high level, the SHA-224 algorithm follows these steps:

  1. Initialize internal state with specific values
  2. Pad the message to a multiple of 512 bits
  3. Process the message in 512-bit chunks through 64 rounds of computation
  4. Return the final 224-bit digest
SHA-224 Algorithm Flow

The basic flow of the SHA-224 algorithm

Python's Built-in Libraries

Python's standard library includes the hashlib module, which provides an easy-to-use interface for many cryptographic hash functions, including SHA-224:

Python - Using hashlib for SHA-224
import hashlib

# Create a SHA-224 hash object
hash_object = hashlib.sha224()

# Update the hash object with the data to be hashed
hash_object.update(b'Hello, world!')

# Get the hexadecimal digest
hex_digest = hash_object.hexdigest()
print(f"SHA-224 hash: {hex_digest}")

# Get the binary digest
binary_digest = hash_object.digest()
print(f"Digest length: {len(binary_digest)} bytes")  # Should be 28 bytes (224 bits)

The hashlib implementation is highly optimized and uses C extensions under the hood. It supports:

  • Incremental Updates: You can update the hash object with new data repeatedly
  • Different Output Formats: Get the digest in hexadecimal or binary form
  • Thread Safety: Hash objects can be used safely across threads
Python - Incremental Updates with hashlib
import hashlib

hash_object = hashlib.sha224()

# Add data incrementally
hash_object.update(b'Part 1 of the message. ')
hash_object.update(b'Part 2 of the message. ')
hash_object.update(b'Part 3 of the message.')

# This is equivalent to hashing the entire message at once
print(hash_object.hexdigest())

# Verification
verification = hashlib.sha224(b'Part 1 of the message. Part 2 of the message. Part 3 of the message.').hexdigest()
print(verification)

# The results should match
assert hash_object.hexdigest() == verification
Note: The hashlib module also supports other hash functions like MD5, SHA-1, SHA-256, SHA-384, and SHA-512. You can check which algorithms are available on your system using hashlib.algorithms_available.

Building a Custom Implementation

Now, let's implement SHA-224 from scratch in Python for educational purposes. We'll break this down into steps, following the official NIST FIPS 180-4 specification.

Step 1: Setting Up Constants

SHA-224 requires several constants for its operation, including initialization vectors and round constants:

Python - SHA-224 Constants
class SHA224:
    """A pure Python implementation of the SHA-224 hash algorithm.
    
    Based on the NIST FIPS 180-4 standard.
    """
    
    # Initial hash values for SHA-224
    # These are the first 32 bits of the fractional parts of the square 
    # roots of the first 8 prime numbers (2, 3, 5, 7, 11, 13, 17, 19)
    # Unique to SHA-224 compared to SHA-256
    initial_hash_values = [
        0xc1059ed8, 0x367cd507, 0x3070dd17, 0xf70e5939,
        0xffc00b31, 0x68581511, 0x64f98fa7, 0xbefa4fa4
    ]
    
    # SHA-224 and SHA-256 use the same round constants
    # First 32 bits of the fractional parts of the cube roots of the first 64 prime numbers
    round_constants = [
        0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
        0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
        0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
        0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
        0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
        0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
        0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
        0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
    ]

These constants are specific to the SHA-224 algorithm and are defined in the NIST standard. The initial hash values are what differentiate SHA-224 from SHA-256 (along with the truncated output).

Step 2: Helper Functions

Next, we'll implement the bitwise operations used in the SHA-224 algorithm. These functions operate on 32-bit words:

Python - SHA-224 Helper Functions
def _rotr(x, n):
    """Perform a right circular rotation on a 32-bit word.
    
    Args:
        x: The 32-bit integer to rotate
        n: The number of bits to rotate by
        
    Returns:
        The rotated 32-bit integer
    """
    return ((x >> n) | (x << (32 - n))) & 0xFFFFFFFF

def _ch(x, y, z):
    """SHA-224/256 Ch function.
    
    For each bit position i, Ch(x,y,z) = (x_i AND y_i) XOR ((NOT x_i) AND z_i)
    """
    return (x & y) ^ (~x & z)

def _maj(x, y, z):
    """SHA-224/256 Maj function.
    
    For each bit position i, Maj(x,y,z) = (x_i AND y_i) XOR (x_i AND z_i) XOR (y_i AND z_i)
    """
    return (x & y) ^ (x & z) ^ (y & z)

def _sigma0(x):
    """SHA-224/256 sigma_0 function.
    
    ROTR_2(x) XOR ROTR_13(x) XOR ROTR_22(x)
    """
    return _rotr(x, 2) ^ _rotr(x, 13) ^ _rotr(x, 22)

def _sigma1(x):
    """SHA-224/256 sigma_1 function.
    
    ROTR_6(x) XOR ROTR_11(x) XOR ROTR_25(x)
    """
    return _rotr(x, 6) ^ _rotr(x, 11) ^ _rotr(x, 25)

def _gamma0(x):
    """SHA-224/256 gamma_0 function.
    
    ROTR_7(x) XOR ROTR_18(x) XOR SHR_3(x)
    """
    return _rotr(x, 7) ^ _rotr(x, 18) ^ (x >> 3)

def _gamma1(x):
    """SHA-224/256 gamma_1 function.
    
    ROTR_17(x) XOR ROTR_19(x) XOR SHR_10(x)
    """
    return _rotr(x, 17) ^ _rotr(x, 19) ^ (x >> 10)

These functions perform the core operations of the SHA-224 algorithm, including bitwise rotations, shifts, and logical operations.

Bitwise Operations: The & (AND), | (OR), ^ (XOR), ~ (NOT), << (left shift), and >> (right shift) operators are fundamental to the SHA-224 algorithm. The & 0xFFFFFFFF ensures that the result is a 32-bit integer.

Step 3: Message Padding

SHA-224 requires that the input message be padded to a multiple of 512 bits. Let's implement the padding function:

Python - SHA-224 Message Padding
def _pad_message(message):
    """Pad the message according to SHA-224/256 padding rules.
    
    The message is padded as follows:
    1. Append a '1' bit to the message
    2. Append k '0' bits, where k is the smallest non-negative solution to:
       (message_length + 1 + k + 64) % 512 = 0
    3. Append the message length as a 64-bit big-endian integer
    
    Args:
        message: The message as bytes
        
    Returns:
        The padded message as bytes
    """
    message_length_bits = len(message) * 8
    
    # Append the bit '1' to the message
    padded_message = bytearray(message) + b'\x80'
    
    # Append '0' bits until the message length (in bits) + 1 + k + 64 is a multiple of 512
    # Since we're working with bytes, we need (message_length % 64) to be 56
    padding_length = 64 - ((len(padded_message) + 8) % 64)
    if padding_length == 64:
        padding_length = 0
        
    padded_message += b'\x00' * padding_length
    
    # Append the original message length as a 64-bit big-endian integer
    padded_message += message_length_bits.to_bytes(8, byteorder='big')
    
    return padded_message

This padding ensures that the message length is a multiple of 512 bits (64 bytes) as required by the algorithm. The padding includes:

  1. A '1' bit (represented by the byte 0x80, which is 10000000 in binary)
  2. Zero or more '0' bits
  3. The original message length as a 64-bit big-endian integer
SHA-224 Message Padding

The message padding process for SHA-224

Step 4: Message Processing

Now, let's implement the core message processing function that will process each 512-bit (64-byte) block of the padded message:

Python - SHA-224 Message Processing
def _process_block(block, hash_values):
    """Process a single 512-bit block according to the SHA-224/256 algorithm.
    
    Args:
        block: A 64-byte block of data
        hash_values: The current hash values (8 32-bit integers)
        
    Returns:
        Updated hash values after processing the block
    """
    # Create the message schedule (64 32-bit words)
    w = [0] * 64
    
    # Copy the block's 16 words into the first 16 words of the message schedule
    for i in range(16):
        w[i] = int.from_bytes(block[i*4:i*4+4], byteorder='big')
    
    # Extend the message schedule
    for i in range(16, 64):
        w[i] = (_gamma1(w[i-2]) + w[i-7] + _gamma0(w[i-15]) + w[i-16]) & 0xFFFFFFFF
    
    # Initialize working variables with current hash values
    a, b, c, d, e, f, g, h = hash_values
    
    # Main loop
    for i in range(64):
        t1 = (h + _sigma1(e) + _ch(e, f, g) + self.round_constants[i] + w[i]) & 0xFFFFFFFF
        t2 = (_sigma0(a) + _maj(a, b, c)) & 0xFFFFFFFF
        h = g
        g = f
        f = e
        e = (d + t1) & 0xFFFFFFFF
        d = c
        c = b
        b = a
        a = (t1 + t2) & 0xFFFFFFFF
    
    # Compute intermediate hash values
    hash_values[0] = (hash_values[0] + a) & 0xFFFFFFFF
    hash_values[1] = (hash_values[1] + b) & 0xFFFFFFFF
    hash_values[2] = (hash_values[2] + c) & 0xFFFFFFFF
    hash_values[3] = (hash_values[3] + d) & 0xFFFFFFFF
    hash_values[4] = (hash_values[4] + e) & 0xFFFFFFFF
    hash_values[5] = (hash_values[5] + f) & 0xFFFFFFFF
    hash_values[6] = (hash_values[6] + g) & 0xFFFFFFFF
    hash_values[7] = (hash_values[7] + h) & 0xFFFFFFFF
    
    return hash_values

This function performs the core of the SHA-224 algorithm:

  1. It prepares a message schedule (W) of 64 32-bit words
  2. It initializes working variables with the current hash values
  3. It performs 64 rounds of computation, updating the working variables in each round
  4. It adds the final working variables to the hash values
SHA-224 Compression Function

The SHA-224 compression function for a single block

Step 5: Putting It All Together

Finally, let's implement the main SHA-224 class that ties everything together:

Python - Complete SHA-224 Implementation
class SHA224:
    """A pure Python implementation of the SHA-224 hash algorithm.
    
    Based on the NIST FIPS 180-4 standard.
    """
    
    # Initial hash values for SHA-224
    initial_hash_values = [
        0xc1059ed8, 0x367cd507, 0x3070dd17, 0xf70e5939,
        0xffc00b31, 0x68581511, 0x64f98fa7, 0xbefa4fa4
    ]
    
    # SHA-224 and SHA-256 use the same round constants
    round_constants = [
        0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
        0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
        0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
        0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
        0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
        0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
        0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
        0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
    ]
    
    def __init__(self):
        """Initialize a new SHA-224 hash object."""
        # Initialize hash values
        self.hash_values = self.initial_hash_values.copy()
        # Initialize message buffer
        self.buffer = bytearray()
        # Initialize message length (in bits)
        self.message_length = 0
    
    @staticmethod
    def _rotr(x, n):
        """Perform a right circular rotation on a 32-bit word."""
        return ((x >> n) | (x << (32 - n))) & 0xFFFFFFFF

    @staticmethod
    def _ch(x, y, z):
        """SHA-224/256 Ch function."""
        return (x & y) ^ (~x & z)

    @staticmethod
    def _maj(x, y, z):
        """SHA-224/256 Maj function."""
        return (x & y) ^ (x & z) ^ (y & z)

    @staticmethod
    def _sigma0(x):
        """SHA-224/256 sigma_0 function."""
        return SHA224._rotr(x, 2) ^ SHA224._rotr(x, 13) ^ SHA224._rotr(x, 22)

    @staticmethod
    def _sigma1(x):
        """SHA-224/256 sigma_1 function."""
        return SHA224._rotr(x, 6) ^ SHA224._rotr(x, 11) ^ SHA224._rotr(x, 25)

    @staticmethod
    def _gamma0(x):
        """SHA-224/256 gamma_0 function."""
        return SHA224._rotr(x, 7) ^ SHA224._rotr(x, 18) ^ (x >> 3)

    @staticmethod
    def _gamma1(x):
        """SHA-224/256 gamma_1 function."""
        return SHA224._rotr(x, 17) ^ SHA224._rotr(x, 19) ^ (x >> 10)
    
    def update(self, message):
        """Update the hash object with the message.
        
        Args:
            message: The message bytes to hash
        """
        if isinstance(message, str):
            message = message.encode('utf-8')
        
        # Update message length
        self.message_length += len(message) * 8
        
        # Add the new message to the buffer
        self.buffer.extend(message)
        
        # Process complete blocks
        while len(self.buffer) >= 64:
            self._process_block(self.buffer[:64])
            self.buffer = self.buffer[64:]
    
    def _process_block(self, block):
        """Process a single 512-bit block according to the SHA-224/256 algorithm."""
        # Create the message schedule (64 32-bit words)
        w = [0] * 64
        
        # Copy the block's 16 words into the first 16 words of the message schedule
        for i in range(16):
            w[i] = int.from_bytes(block[i*4:i*4+4], byteorder='big')
        
        # Extend the message schedule
        for i in range(16, 64):
            w[i] = (SHA224._gamma1(w[i-2]) + w[i-7] + SHA224._gamma0(w[i-15]) + w[i-16]) & 0xFFFFFFFF
        
        # Initialize working variables with current hash values
        a, b, c, d, e, f, g, h = self.hash_values
        
        # Main loop
        for i in range(64):
            t1 = (h + SHA224._sigma1(e) + SHA224._ch(e, f, g) + self.round_constants[i] + w[i]) & 0xFFFFFFFF
            t2 = (SHA224._sigma0(a) + SHA224._maj(a, b, c)) & 0xFFFFFFFF
            h = g
            g = f
            f = e
            e = (d + t1) & 0xFFFFFFFF
            d = c
            c = b
            b = a
            a = (t1 + t2) & 0xFFFFFFFF
        
        # Compute intermediate hash values
        self.hash_values[0] = (self.hash_values[0] + a) & 0xFFFFFFFF
        self.hash_values[1] = (self.hash_values[1] + b) & 0xFFFFFFFF
        self.hash_values[2] = (self.hash_values[2] + c) & 0xFFFFFFFF
        self.hash_values[3] = (self.hash_values[3] + d) & 0xFFFFFFFF
        self.hash_values[4] = (self.hash_values[4] + e) & 0xFFFFFFFF
        self.hash_values[5] = (self.hash_values[5] + f) & 0xFFFFFFFF
        self.hash_values[6] = (self.hash_values[6] + g) & 0xFFFFFFFF
        self.hash_values[7] = (self.hash_values[7] + h) & 0xFFFFFFFF
    
    def digest(self):
        """Return the digest of the data passed to update() so far.
        
        Returns:
            The 224-bit digest as a bytes object
        """
        # Create a copy of the current state
        current_hash = SHA224()
        current_hash.hash_values = self.hash_values.copy()
        current_hash.buffer = self.buffer.copy()
        current_hash.message_length = self.message_length
        
        # Pad the message
        message_len_bytes = current_hash.message_length.to_bytes(8, byteorder='big')
        
        # Append the bit '1' to the message
        current_hash.buffer.append(0x80)
        
        # Append '0' bits until the message length (in bits) + 1 + k + 64 is a multiple of 512
        # Since we're working with bytes, we need (buffer_length % 64) to be 56
        padding_length = 56 - (len(current_hash.buffer) % 64)
        if padding_length <= 0:
            padding_length += 64
            
        current_hash.buffer.extend([0] * padding_length)
        
        # Append the original message length as a 64-bit big-endian integer
        current_hash.buffer.extend(message_len_bytes)
        
        # Process the remaining blocks
        for i in range(0, len(current_hash.buffer), 64):
            current_hash._process_block(current_hash.buffer[i:i+64])
        
        # Concatenate the first 7 hash values (224 bits)
        # SHA-224 only uses 7 out of the 8 hash values (truncates the result)
        result = bytearray()
        for i in range(7):
            result.extend(current_hash.hash_values[i].to_bytes(4, byteorder='big'))
        
        return bytes(result)
    
    def hexdigest(self):
        """Return the digest of the data passed to update() so far as a hexadecimal string.
        
        Returns:
            The 224-bit digest as a hexadecimal string
        """
        return self.digest().hex()
    
    @classmethod
    def hash(cls, message):
        """Convenience method to compute the SHA-224 digest of a message in one step.
        
        Args:
            message: The message to hash (string or bytes)
            
        Returns:
            The 224-bit digest as a bytes object
        """
        sha = cls()
        sha.update(message)
        return sha.digest()
    
    @classmethod
    def hexhash(cls, message):
        """Convenience method to compute the SHA-224 hexadecimal digest of a message in one step.
        
        Args:
            message: The message to hash (string or bytes)
            
        Returns:
            The 224-bit digest as a hexadecimal string
        """
        sha = cls()
        sha.update(message)
        return sha.hexdigest()

This implementation provides a complete SHA-224 hasher with an interface similar to Python's built-in hashlib module. It includes:

  • An update() method for adding data incrementally
  • A digest() method for getting the binary digest
  • A hexdigest() method for getting the hexadecimal digest
  • Convenience classmethods hash() and hexhash() for one-step hashing

Note that we only use the first 7 of the 8 hash values for the final digest, which is what gives SHA-224 its 224-bit output (7 * 32 = 224 bits).

Testing and Verification

Let's test our implementation against known test vectors to ensure it's working correctly:

Python - SHA-224 Testing
import hashlib

def test_sha224():
    """Test the SHA-224 implementation against the standard library."""
    test_cases = [
        b"",
        b"abc",
        b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq",
        b"a" * 1000000
    ]
    
    for i, test_case in enumerate(test_cases):
        # Our implementation
        custom_sha = SHA224()
        custom_sha.update(test_case)
        custom_result = custom_sha.hexdigest()
        
        # Standard library implementation
        std_result = hashlib.sha224(test_case).hexdigest()
        
        # Compare results
        assert custom_result == std_result, f"Test case {i} failed: {custom_result} != {std_result}"
        
        print(f"Test case {i} passed: {custom_result}")
    
    print("All test cases passed!")

# Run the tests
test_sha224()

These test cases include:

  1. An empty string
  2. A simple string ("abc")
  3. A complex string with various characters
  4. A long string (one million 'a' characters) to test performance with large inputs

The expected outputs for these test cases are:

Expected SHA-224 Test Vector Results
Empty string:
d14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f

"abc":
23097d223405d8228642a477bda255b32aadbce4bda0b3f7e36c9da7

"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq":
75388b16512776cc5dba5da1fd890150b0c6455cb4f58b1952522525

One million "a" characters:
20794655980c91d8bbb4c1ea97618a4bf03f42581948b2ee4ee7ad67
Note: The last test case (one million 'a' characters) may take a long time to complete with our pure Python implementation, as it's not optimized for performance like the built-in hashlib implementation.

Performance Considerations

Our pure Python implementation is significantly slower than the built-in hashlib implementation, which uses C extensions. Let's compare the performance:

Python - Performance Comparison
import hashlib
import time

def benchmark():
    """Compare the performance of our implementation with the standard library."""
    test_data = b"a" * 10000  # 10KB of data
    iterations = 100
    
    # Benchmark custom implementation
    start_time = time.time()
    for _ in range(iterations):
        custom_sha = SHA224()
        custom_sha.update(test_data)
        custom_sha.hexdigest()
    custom_time = time.time() - start_time
    
    # Benchmark standard library
    start_time = time.time()
    for _ in range(iterations):
        std_sha = hashlib.sha224()
        std_sha.update(test_data)
        std_sha.hexdigest()
    std_time = time.time() - start_time
    
    print(f"Custom implementation: {custom_time:.4f} seconds for {iterations} iterations")
    print(f"Standard library: {std_time:.4f} seconds for {iterations} iterations")
    print(f"The standard library is {custom_time/std_time:.1f}x faster")

# Run the benchmark
benchmark()

The built-in implementation is typically 50-100x faster than our pure Python implementation, depending on the system and Python version.

Here are some ways to optimize our implementation if needed:

  1. Use NumPy: Utilizing NumPy arrays for the internal operations can improve performance
  2. Cython: Converting the implementation to Cython can provide significant speedups
  3. PyPy: Running the implementation with PyPy instead of CPython can improve performance
  4. Parallelization: For large messages, processing multiple blocks in parallel could improve performance
Python - NumPy Optimization Example
import numpy as np

# Use NumPy for the message schedule
def _process_block_numpy(self, block):
    """Process a single 512-bit block using NumPy for better performance."""
    # Create the message schedule (64 32-bit words) as a NumPy array
    w = np.zeros(64, dtype=np.uint32)
    
    # Copy the block's 16 words into the first 16 words of the message schedule
    for i in range(16):
        w[i] = int.from_bytes(block[i*4:i*4+4], byteorder='big')
    
    # Extend the message schedule using NumPy operations
    for i in range(16, 64):
        w[i] = (SHA224._gamma1(w[i-2]) + w[i-7] + SHA224._gamma0(w[i-15]) + w[i-16]) & 0xFFFFFFFF
    
    # Rest of the function remains the same...
    # ...
Warning: For real-world applications, always use the built-in hashlib implementation, which is well-tested, optimized, and regularly updated with security patches. Our implementation is for educational purposes only.

Security Considerations

When implementing cryptographic algorithms, security is paramount. Here are some important considerations:

Timing Attacks

Our implementation is not designed to be resistant to timing attacks. In particular, comparisons of hash values should be done in constant time to prevent timing side-channel attacks.

Python - Constant-Time Comparison
import hmac

def constant_time_compare(a, b):
    """Compare two strings/bytes in constant time."""
    return hmac.compare_digest(a, b)

Memory Security

Sensitive data should be cleared from memory when no longer needed. Python doesn't provide direct memory management, so this is challenging to implement correctly.

Input Validation

Our implementation includes basic input validation, but in a production environment, more robust validation would be necessary.

Use Cases

SHA-224 is a general-purpose cryptographic hash function. For specific use cases like password hashing, specialized algorithms like Argon2, bcrypt, or PBKDF2 should be used instead.

Python - Password Hashing with SHA-224 and PBKDF2
import hashlib
import os
import binascii

def hash_password(password):
    """Securely hash a password using PBKDF2-SHA224."""
    # Generate a random salt
    salt = os.urandom(16)
    
    # Use PBKDF2 with SHA-224 as the hash function
    # 100,000 iterations is a reasonable default as of 2025
    iterations = 100000
    
    # Derive a key using PBKDF2
    derived_key = hashlib.pbkdf2_hmac('sha224', password.encode(), salt, iterations)
    
    # Format salt:iterations:key
    return binascii.hexlify(salt).decode() + ':' + str(iterations) + ':' + binascii.hexlify(derived_key).decode()

def verify_password(stored_password, provided_password):
    """Verify a password against a stored hash."""
    # Extract components
    salt_hex, iterations_str, key_hex = stored_password.split(':')
    salt = binascii.unhexlify(salt_hex)
    iterations = int(iterations_str)
    stored_key = binascii.unhexlify(key_hex)
    
    # Derive the key from the provided password
    derived_key = hashlib.pbkdf2_hmac('sha224', provided_password.encode(), salt, iterations)
    
    # Compare in constant time
    return hmac.compare_digest(stored_key, derived_key)

Conclusion

In this tutorial, we've implemented the SHA-224 hash function from scratch in Python. We've learned:

  • How SHA-224 works internally, including its padding, processing, and finalization steps
  • How to use Python's built-in hashlib module for efficient SHA-224 hashing
  • How to create a custom implementation with an API similar to the standard library
  • Performance and security considerations for cryptographic hash functions

Remember, while implementing cryptographic algorithms is educational, for production use, always rely on well-tested libraries like hashlib.