SHA-3: Deep Dive

SHA-3 is the odd one out in the SHA family. Unlike SHA-1 and SHA-2 (which share the same NSA-designed Merkle-Damgård structure), SHA-3 was selected through a public competition and uses a completely different design called Keccak. This post explores the entire SHA-3 family, including its unique extendable-output functions (XOFs).

The SHA-3 Family

SHA-3 includes four fixed-output hash functions and two extendable-output functions (XOFs):

Algorithm Output Size Security Level Type
SHA3-224 224 bits 112 bits Fixed output
SHA3-256 256 bits 128 bits Fixed output
SHA3-384 384 bits 192 bits Fixed output
SHA3-512 512 bits 256 bits Fixed output
SHAKE128 Variable 128 bits XOF
SHAKE256 Variable 256 bits XOF

XOF (Extendable-Output Function): Unlike traditional hash functions that produce fixed-size output, XOFs can produce output of any desired length.

History: The SHA-3 Competition

Unlike SHA-1 and SHA-2 (designed by the NSA), SHA-3 emerged from a public competition:

Timeline

2004 — Wang breaks MD5, weakens SHA-1
2007 — NIST announces SHA-3 competition
2008 — 64 submissions received
2009 — Round 1: 51 candidates selected
2010 — Round 2: 14 candidates selected
2011 — Round 3: 5 finalists
2012 — Keccak selected as SHA-3 winner
2015 — FIPS 202 published (official SHA-3 standard)

The Five Finalists

Algorithm Designer Notes
Keccak Bertoni, Daemen, Peeters, Van Assche Winner — Sponge construction
BLAKE Aumasson, Henzen, Meier, Phan Very fast, later became BLAKE2/BLAKE3
Grøstl Knudsen et al. AES-based design
JH Wu Novel structure
Skein Schneier, Ferguson et al. Threefish block cipher based

Why Keccak won:

  • Completely different design from SHA-2 (diversity = backup if SHA-2 breaks)
  • Strong security margins
  • Elegant mathematical structure
  • Efficient in both hardware and software
  • Built-in resistance to length extension attacks

The Keccak Sponge Construction

SHA-3's most distinctive feature is its sponge construction, fundamentally different from the Merkle-Damgård design used by SHA-1/SHA-2.

Sponge vs Merkle-Damgård

Merkle-Damgård (SHA-2):

┌─────────┐   ┌─────────┐   ┌─────────┐
│ Block 1 │──►│ Block 2 │──►│ Block 3 │──► Hash
└─────────┘   └─────────┘   └─────────┘
    Chain the compression function

Sponge (SHA-3):

        ┌──────── ABSORB ────────┐  ┌─── SQUEEZE ───┐
        │                        │  │               │
Input ──┼──►[f]──►[f]──►[f]──►───┼──┼──►[f]──►[f]───┼──► Output
        │   ▲     ▲     ▲        │  │    │     │    │
        │   │     │     │        │  │    ▼     ▼    │
        │  M[0]  M[1]  M[2]      │  │   Out   Out   │
        └────────────────────────┘  └───────────────┘

The sponge works in two phases:

  1. Absorbing: Input is XORed into the state, then permuted
  2. Squeezing: Output is extracted from the state, with permutations between extractions

The State and Parameters

The Keccak state is a 1600-bit array organized as a 5×5×64 three-dimensional structure:

         y
         ▲
       4 │ □ □ □ □ □
       3 │ □ □ □ □ □
       2 │ □ □ □ □ □
       1 │ □ □ □ □ □
       0 │ □ □ □ □ □
         └──────────► x
           0 1 2 3 4

Each □ = 64 bits (a "lane")
Total = 5 × 5 × 64 = 1600 bits

The state is divided into two parts:

  • Rate (r): The portion that absorbs input and produces output
  • Capacity (c): The "hidden" portion that provides security
State (1600 bits) = Rate (r) + Capacity (c)
Algorithm Rate (r) Capacity (c) Security
SHA3-224 1152 bits 448 bits 112 bits
SHA3-256 1088 bits 512 bits 128 bits
SHA3-384 832 bits 768 bits 192 bits
SHA3-512 576 bits 1024 bits 256 bits
SHAKE128 1344 bits 256 bits 128 bits
SHAKE256 1088 bits 512 bits 256 bits

Security level = capacity / 2 (for collision resistance)

The Keccak-f Permutation

The heart of SHA-3 is the Keccak-f[1600] permutation, consisting of 24 rounds. Each round applies five operations:

θ (Theta): Column parity mixing

For each column, XOR with the parity of adjacent columns
Provides diffusion across the x-axis

ρ (Rho): Bitwise rotation

Rotate each lane by a different offset (0 to 63 bits)
Provides diffusion along the z-axis (within lanes)

π (Pi): Lane permutation

Rearrange lanes: (x,y) → (y, 2x+3y)
Provides diffusion across x and y axes

χ (Chi): Nonlinear operation

a[x] = a[x] XOR (NOT a[x+1] AND a[x+2])
The only nonlinear step — provides confusion

ι (Iota): Round constant addition

XOR a round constant into lane (0,0)
Breaks symmetry, prevents slide attacks

After 24 rounds of these five operations, the state is thoroughly mixed.

The SHA-3 Algorithms

SHA3-256

Output: 256 bits (32 bytes, 64 hex characters)
Rate: 1088 bits
Capacity: 512 bits

SHA3-256("Hello, World!") = 1af17a664e3fa8e419b8ba05c2a173169df76162a5a286e0c405b460d478f7ef

SHA3-256 is the direct counterpart to SHA-256. Same output size, same security level, completely different internals.

When to use: When you need SHA-256 security but want a backup algorithm with a different design, or when length extension resistance matters.

SHA3-224

Output: 224 bits (28 bytes, 56 hex characters)
Rate: 1152 bits
Capacity: 448 bits

SHA3-224("Hello, World!") = 853048fb8b11462b6100385633c0cc8dcdc6e2b8e376c28102bc84f2

When to use: Rarely. Use when you specifically need 224-bit output with SHA-3's properties.

SHA3-512

Output: 512 bits (64 bytes, 128 hex characters)
Rate: 576 bits
Capacity: 1024 bits

SHA3-512("Hello, World!") = 38e05c33d7b067127f217d8c856e554fcff09c9320b8a5979ce2ff5d95dd27ba35d1fba50c562dfd1d6cc48bc9c5baa4390894418cc942d968f97bcb659419ed

When to use: Maximum security requirements with SHA-3.

SHA3-384

Output: 384 bits (48 bytes, 96 hex characters)
Rate: 832 bits
Capacity: 768 bits

SHA3-384("Hello, World!") = aa9ad8a49f31d2ddcabbb7010a1566417cff803fef50eba239558826f872e468c5743e7f026b0a8e5b2d7a1cc465cdbe

When to use: When 192-bit security is required with SHA-3.

SHAKE: Extendable-Output Functions

SHAKE128 and SHAKE256 are XOFs (Extendable-Output Functions)—they can produce output of any length you specify.

SHAKE128

Security: 128 bits
Rate: 1344 bits
Output: Variable (you choose)

# Python example: Get 32 bytes from SHAKE128
import hashlib
shake = hashlib.shake_128(b"Hello, World!")
output = shake.hexdigest(32)  # 32 bytes = 64 hex chars

SHAKE256

Security: 256 bits
Rate: 1088 bits
Output: Variable (you choose)

# Python example: Get 64 bytes from SHAKE256
import hashlib
shake = hashlib.shake_256(b"Hello, World!")
output = shake.hexdigest(64)  # 64 bytes = 128 hex chars

Why XOFs Matter

Traditional hash functions produce fixed output:

  • Need 128 bits? Use SHA-256 and truncate (wastes computation)
  • Need 1024 bits? Hash multiple times with counters (awkward)

XOFs solve this elegantly:

# Need exactly 100 bytes of output? Easy.
output = hashlib.shake_256(b"seed").digest(100)

# Need 1000 bytes? Also easy.
long_output = hashlib.shake_256(b"seed").digest(1000)

Use cases for XOFs:

  • Key derivation: Generate keys of exact required length
  • Mask generation: For OAEP padding in RSA
  • Stream cipher: XOF output XORed with plaintext
  • Deterministic random: Reproducible random bytes from a seed

SHA-3 vs SHA-2: When to Use Which

Key Differences

Property SHA-2 SHA-3
Design Merkle-Damgård Sponge
Origin NSA Public competition
Length extension Vulnerable Resistant
Parallelizable No No
Hardware efficiency Good Excellent
Software speed Fast Moderate
XOF variants No Yes (SHAKE)
Age 24+ years 10+ years

When to Use SHA-3

  1. Length extension attacks are a concern

    • SHA-3 is inherently immune
    • No need for HMAC construction in some cases
  2. Algorithm diversity required

    • Backup if SHA-2 is ever broken
    • Defense in depth
  3. Variable output needed

    • SHAKE128/SHAKE256 provide any output length
  4. Hardware implementation

    • SHA-3 is very efficient in hardware
    • Good for FPGAs and ASICs

When to Use SHA-2

  1. Compatibility

    • SHA-256 is everywhere
    • Better library/tool support
  2. Software performance

    • SHA-256 is faster in pure software
    • Especially with CPU extensions (SHA-NI)
  3. Proven track record

    • 24+ years of cryptanalysis
    • No significant weaknesses found
  4. Standards compliance

    • Many standards specify SHA-256
    • Changing requires compliance updates

The "Paranoid" Approach

Some high-security applications use both:

final_hash = SHA3-256(SHA-256(data))

If either algorithm breaks, the combination remains secure. This is overkill for most applications but demonstrates SHA-3's role as a backup.

Security Analysis

Theoretical Security

Algorithm Collision Preimage Second Preimage
SHA3-224 2^112 2^224 2^224
SHA3-256 2^128 2^256 2^256
SHA3-384 2^192 2^384 2^384
SHA3-512 2^256 2^512 2^512
SHAKE128 2^min(d/2, 128) 2^min(d, 128) 2^min(d, 128)
SHAKE256 2^min(d/2, 256) 2^min(d, 256) 2^min(d, 256)

(For SHAKE, d = output length in bits)

Known Attacks

As of 2025, no practical attacks exist against SHA-3. The best known results:

  • Reduced-round attacks (up to 8 of 24 rounds for some properties)
  • Theoretical improvements that don't threaten the full algorithm

The 24 rounds provide a substantial security margin.

Quantum Resistance

Like SHA-2, SHA-3 is affected by quantum computers:

  • Preimage: Grover's algorithm provides quadratic speedup (2^n → 2^(n/2))
  • Collision: Less affected by quantum algorithms

SHA3-256 with 128-bit collision resistance becomes ~64-bit against quantum collision attacks—still requiring significant quantum resources.

Code Examples

Node.js

Requirements: Node.js 10+ for SHA3, Node.js 12.8+ for SHAKE with outputLength option (PR #28805)

const crypto = require('crypto');

const message = 'Hello, World!';

// ============ Fixed-Output Hash Functions ============

// SHA3-224
const sha3_224 = crypto.createHash('sha3-224')
    .update(message)
    .digest('hex');
console.log(`SHA3-224: ${sha3_224}`);

// SHA3-256
const sha3_256 = crypto.createHash('sha3-256')
    .update(message)
    .digest('hex');
console.log(`SHA3-256: ${sha3_256}`);

// SHA3-384
const sha3_384 = crypto.createHash('sha3-384')
    .update(message)
    .digest('hex');
console.log(`SHA3-384: ${sha3_384}`);

// SHA3-512
const sha3_512 = crypto.createHash('sha3-512')
    .update(message)
    .digest('hex');
console.log(`SHA3-512: ${sha3_512}`);

// ============ Extendable-Output Functions (XOFs) ============

// SHAKE128 - 32 bytes output
const shake128_32 = crypto.createHash('shake128', { outputLength: 32 })
    .update(message)
    .digest('hex');
console.log(`SHAKE128 (32 bytes): ${shake128_32}`);

// SHAKE128 - 64 bytes output
const shake128_64 = crypto.createHash('shake128', { outputLength: 64 })
    .update(message)
    .digest('hex');
console.log(`SHAKE128 (64 bytes): ${shake128_64}`);

// SHAKE256 - 32 bytes output
const shake256_32 = crypto.createHash('shake256', { outputLength: 32 })
    .update(message)
    .digest('hex');
console.log(`SHAKE256 (32 bytes): ${shake256_32}`);

// SHAKE256 - 64 bytes output
const shake256_64 = crypto.createHash('shake256', { outputLength: 64 })
    .update(message)
    .digest('hex');
console.log(`SHAKE256 (64 bytes): ${shake256_64}`);

// ============ Practical Example: Key Derivation ============

function deriveKey(password, salt, keyLength) {
    return crypto.createHash('shake256', { outputLength: keyLength })
        .update(password + salt)
        .digest();
}

const derivedKey = deriveKey('my_password', 'random_salt', 32);
console.log(`Derived key: ${derivedKey.toString('hex')}`);

C# (.NET)

Requirements: .NET 8+ for native SHA3 support (SHA3_256, Shake128, Shake256). For earlier .NET versions, use BouncyCastle.

using System;
using System.Security.Cryptography;
using System.Text;

class SHA3Examples
{
    static void Main()
    {
        string message = "Hello, World!";
        byte[] messageBytes = Encoding.UTF8.GetBytes(message);

        // ============ Fixed-Output Hash Functions ============

        // SHA3-256 (.NET 8+)
        using (var sha3_256 = SHA3_256.Create())
        {
            byte[] hash = sha3_256.ComputeHash(messageBytes);
            Console.WriteLine($"SHA3-256: {Convert.ToHexString(hash).ToLower()}");
        }

        // SHA3-384 (.NET 8+)
        using (var sha3_384 = SHA3_384.Create())
        {
            byte[] hash = sha3_384.ComputeHash(messageBytes);
            Console.WriteLine($"SHA3-384: {Convert.ToHexString(hash).ToLower()}");
        }

        // SHA3-512 (.NET 8+)
        using (var sha3_512 = SHA3_512.Create())
        {
            byte[] hash = sha3_512.ComputeHash(messageBytes);
            Console.WriteLine($"SHA3-512: {Convert.ToHexString(hash).ToLower()}");
        }

        // ============ Extendable-Output Functions (XOFs) ============

        // SHAKE128 (.NET 8+)
        byte[] shake128Output = new byte[32];
        Shake128.HashData(messageBytes, shake128Output);
        Console.WriteLine($"SHAKE128 (32 bytes): {Convert.ToHexString(shake128Output).ToLower()}");

        // SHAKE128 - 64 bytes
        byte[] shake128Output64 = new byte[64];
        Shake128.HashData(messageBytes, shake128Output64);
        Console.WriteLine($"SHAKE128 (64 bytes): {Convert.ToHexString(shake128Output64).ToLower()}");

        // SHAKE256
        byte[] shake256Output = new byte[32];
        Shake256.HashData(messageBytes, shake256Output);
        Console.WriteLine($"SHAKE256 (32 bytes): {Convert.ToHexString(shake256Output).ToLower()}");

        // SHAKE256 - 64 bytes
        byte[] shake256Output64 = new byte[64];
        Shake256.HashData(messageBytes, shake256Output64);
        Console.WriteLine($"SHAKE256 (64 bytes): {Convert.ToHexString(shake256Output64).ToLower()}");
    }

    // ============ Practical Example: Key Derivation ============

    static byte[] DeriveKey(string password, string salt, int keyLength)
    {
        byte[] input = Encoding.UTF8.GetBytes(password + salt);
        byte[] key = new byte[keyLength];
        Shake256.HashData(input, key);
        return key;
    }
}

Note: SHA-3 support in .NET requires .NET 8 or later. For earlier versions, use a library like BouncyCastle:

// Using BouncyCastle for .NET Framework / .NET Core < 8
using Org.BouncyCastle.Crypto.Digests;

var sha3 = new Sha3Digest(256); // SHA3-256
sha3.BlockUpdate(messageBytes, 0, messageBytes.Length);
byte[] hash = new byte[sha3.GetDigestSize()];
sha3.DoFinal(hash, 0);

Go (Golang)

Requirements: Go 1.4+ with the golang.org/x/crypto/sha3 package (pkg.go.dev/golang.org/x/crypto/sha3). Install with go get golang.org/x/crypto/sha3.

package main

import (
    "encoding/hex"
    "fmt"
    "golang.org/x/crypto/sha3"
)

func main() {
    message := []byte("Hello, World!")

    // ============ Fixed-Output Hash Functions ============

    // SHA3-224
    hash224 := sha3.Sum224(message)
    fmt.Printf("SHA3-224: %s\n", hex.EncodeToString(hash224[:]))

    // SHA3-256
    hash256 := sha3.Sum256(message)
    fmt.Printf("SHA3-256: %s\n", hex.EncodeToString(hash256[:]))

    // SHA3-384
    hash384 := sha3.Sum384(message)
    fmt.Printf("SHA3-384: %s\n", hex.EncodeToString(hash384[:]))

    // SHA3-512
    hash512 := sha3.Sum512(message)
    fmt.Printf("SHA3-512: %s\n", hex.EncodeToString(hash512[:]))

    // ============ Using the Hash Interface ============

    // Useful for streaming/incremental hashing
    hasher := sha3.New256()
    hasher.Write(message)
    result := hasher.Sum(nil)
    fmt.Printf("SHA3-256 (incremental): %s\n", hex.EncodeToString(result))

    // ============ Extendable-Output Functions (XOFs) ============

    // SHAKE128 - 32 bytes output
    shake128_32 := make([]byte, 32)
    sha3.ShakeSum128(shake128_32, message)
    fmt.Printf("SHAKE128 (32 bytes): %s\n", hex.EncodeToString(shake128_32))

    // SHAKE128 - 64 bytes output
    shake128_64 := make([]byte, 64)
    sha3.ShakeSum128(shake128_64, message)
    fmt.Printf("SHAKE128 (64 bytes): %s\n", hex.EncodeToString(shake128_64))

    // SHAKE256 - 32 bytes output
    shake256_32 := make([]byte, 32)
    sha3.ShakeSum256(shake256_32, message)
    fmt.Printf("SHAKE256 (32 bytes): %s\n", hex.EncodeToString(shake256_32))

    // SHAKE256 - 64 bytes output
    shake256_64 := make([]byte, 64)
    sha3.ShakeSum256(shake256_64, message)
    fmt.Printf("SHAKE256 (64 bytes): %s\n", hex.EncodeToString(shake256_64))

    // ============ Using SHAKE with io.Reader Interface ============

    // Useful for generating arbitrary amounts of output
    shakeReader := sha3.NewShake256()
    shakeReader.Write(message)

    // Read any amount of bytes
    output := make([]byte, 100)
    shakeReader.Read(output)
    fmt.Printf("SHAKE256 (100 bytes): %s\n", hex.EncodeToString(output))

    // ============ Practical Example: Key Derivation ============

    key := deriveKey("my_password", "random_salt", 32)
    fmt.Printf("Derived key: %s\n", hex.EncodeToString(key))
}

func deriveKey(password, salt string, keyLength int) []byte {
    input := []byte(password + salt)
    key := make([]byte, keyLength)
    sha3.ShakeSum256(key, input)
    return key
}

Python

Requirements: Python 3.6+ (hashlib SHA-3 support added in 3.6)

import hashlib

message = b"Hello, World!"

# ============ Fixed-Output Hash Functions ============

print(f"SHA3-224: {hashlib.sha3_224(message).hexdigest()}")
print(f"SHA3-256: {hashlib.sha3_256(message).hexdigest()}")
print(f"SHA3-384: {hashlib.sha3_384(message).hexdigest()}")
print(f"SHA3-512: {hashlib.sha3_512(message).hexdigest()}")

# ============ Extendable-Output Functions (XOFs) ============

print(f"SHAKE128 (32 bytes): {hashlib.shake_128(message).hexdigest(32)}")
print(f"SHAKE128 (64 bytes): {hashlib.shake_128(message).hexdigest(64)}")
print(f"SHAKE256 (32 bytes): {hashlib.shake_256(message).hexdigest(32)}")
print(f"SHAKE256 (64 bytes): {hashlib.shake_256(message).hexdigest(64)}")

# ============ Practical Example: Key Derivation ============

def derive_key(password: str, salt: bytes, key_length: int) -> bytes:
    """Derive a key of exact length using SHAKE256"""
    data = password.encode() + salt
    return hashlib.shake_256(data).digest(key_length)

key = derive_key("my_password", b"random_salt", 32)
print(f"Derived key: {key.hex()}")

Command Line (OpenSSL)

Requirements: OpenSSL 1.1.1+ for SHA3 (release notes), OpenSSL 3.0+ for SHAKE with -xoflen option (openssl-dgst manpage)

# Using OpenSSL (1.1.1+ for SHA3, 3.0+ for SHAKE)
echo -n "Hello, World!" | openssl dgst -sha3-224
echo -n "Hello, World!" | openssl dgst -sha3-256
echo -n "Hello, World!" | openssl dgst -sha3-384
echo -n "Hello, World!" | openssl dgst -sha3-512

# SHAKE with variable output length
echo -n "Hello, World!" | openssl dgst -shake128 -xoflen 32
echo -n "Hello, World!" | openssl dgst -shake256 -xoflen 64

# Using rhash
echo -n "Hello, World!" | rhash --sha3-256 -

Real-World Usage

Current Adoption

SHA-3 adoption is growing but still trails SHA-2:

Application SHA-2 SHA-3
TLS/SSL Default Supported
Code signing Standard Rare
Cryptocurrencies Bitcoin, Ethereum Some newer chains
Password hashing Not recommended Not recommended
File integrity Common Growing

Where SHA-3 Shines

  1. EdDSA (Ed448)

    • Uses SHAKE256 internally
    • High-security digital signatures
  2. Post-quantum cryptography

    • Several NIST PQC algorithms use SHA-3/SHAKE
    • CRYSTALS-Kyber, CRYSTALS-Dilithium use SHAKE
  3. Hardware security modules

    • SHA-3's efficient hardware implementation
    • Smart cards, secure elements
  4. Ethereum 2.0

    • Uses SHA-256 and SHA3-256 (Keccak-256 variant)

Keccak vs SHA-3: A Note on Ethereum

Ethereum uses "Keccak-256," which is not identical to SHA3-256:

Keccak-256("") = c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470
SHA3-256("")   = a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a

The difference: NIST added domain separation padding when standardizing SHA-3. Ethereum adopted Keccak before this standardization, so it uses the original padding.

Bottom line: When working with Ethereum, use Keccak-256, not SHA3-256.

Conclusion

SHA-3 represents a fundamental shift in hash function design. Its sponge construction, XOF capability, and inherent length extension resistance make it a powerful tool—and a crucial backup if SHA-2 ever falls.

Quick recommendations:

  • Default choice: SHA-256 (compatibility and speed)
  • Length extension concerns: SHA3-256 or HMAC-SHA256
  • Variable output needed: SHAKE128 or SHAKE256
  • Maximum security: SHA3-512 or SHA-512
  • Post-quantum preparation: SHAKE256 (used in PQC algorithms)

SHA-3 may not have replaced SHA-2 as the default, but it has earned its place as the essential backup and the foundation for next-generation cryptography.


References

Standards and Theory

  1. NIST FIPS 202. SHA-3 Standard: Permutation-Based Hash and Extendable-Output Functions
  2. Bertoni, G., et al. The Keccak Reference
  3. Bertoni, G., et al. Sponge Functions
  4. NIST. SHA-3 Competition
  5. Ethereum Yellow Paper. Keccak-256 specification

Implementation Documentation

  1. Node.js. Crypto module - Hash class
  2. Node.js. PR #28805 - Add outputLength option for XOF hash functions
  3. Microsoft .NET. SHA3_256 Class
  4. Microsoft .NET. Shake128 Class
  5. Microsoft .NET. Shake256 Class
  6. Go. golang.org/x/crypto/sha3 package
  7. Python. hashlib — Secure hashes and message digests
  8. OpenSSL. OpenSSL 1.1.1 Release Notes
  9. OpenSSL. openssl-dgst Manual Page
  10. BouncyCastle. C# Cryptography APIs