Keyboard Shortcuts N Next post
P Previous post
S Save / unsave
R Read aloud
T Toggle theme
/ Focus search
Esc Close panels
🔥
Ready to read...
Cryptography Cryptography Tutorial Java Java and Cryptography Mastery Series — Extended Module 13 - Hash MAC and Checksum

Java Program for BLAKE3 Parallel Hashing Algorithm

Reviewed & accurate
AI Summary

Introduction

The blake3 hash java tutorial presents the youngest member of the BLAKE family, designed in 2018 by Jack O'Connor, Samuel Neves, Jean-Philippe Aumasson, and Zooko Wilcox-O'Hearn. BLAKE3 was published in January 2020 as a fresh design that fuses the BLAKE2 compression function with a binary Merkle tree. The headline is impressive: BLAKE3 scales linearly with the number of CPU cores, reaching multiple gigabytes per second on commodity hardware, while remaining a single, principled cryptographic primitive.

For Java learners this blake3 hash java program is a chance to see what a tree mode hash actually looks like in code. The Java standard library does not ship BLAKE3, so we build the single chunk variant from scratch using only JDK primitives. Even though our single chunk version does not exploit multiple cores, it faithfully mirrors the chunk compression, output stream, and key derivation modes that make BLAKE3 a Swiss army knife for hashing, MAC, and key derivation.

What is BLAKE3?

BLAKE3 is a cryptographic hash function family that belongs to the Merkle-Damgard with tree category, similar to the suffix trees used by KangarooTwelve but with the binary structure of a Merkle tree. Its core is the same ARX-based compression function used by BLAKE2s, but the surrounding mode is radically different. BLAKE3 splits input data into 1024-byte chunks, compresses each chunk independently, then combines chunk outputs into a binary tree of hash nodes whose root is the final digest.

BLAKE3 differs from BLAKE2 in three fundamental ways. First, every chunk is hashed independently with no counter, so chunk hashing is embarrassingly parallel. Second, the tree root can be expanded into an unbounded stream of output bytes via a counter mode, which lets BLAKE3 replace both a hash and a KDF (key derivation function) with one primitive. Third, BLAKE3 supports three domain separation modes (keyed hash, key derivation, and regular hash) by mixing different constants into the first chunk.

BLAKE3 also introduces the concept of parent nodes that combine two child hash outputs. A parent node runs the compression function on the concatenation of two 32-byte child digests plus a domain separation flag. The tree is built bottom up. Because the chunk size is 1024 bytes, a 1 megabyte input produces a 1024-leaf tree of depth 10, and the hash tree collapses into a single 32-byte root in 1023 parent compressions.

How BLAKE3 Works Step by Step

BLAKE3 reuses the BLAKE2s compression function G but wraps it in a tree structure with a 14-round compression instead of 10. The 14-round count compensates for the reduced per-chunk work in the tree mode.

Step 1: Initialization. The chain value h is set to the BLAKE3 IV constant (which differs from BLAKE2s IV). For the regular hash mode the IV is used directly. For the keyed hash mode the IV is replaced with the 32-byte key. For the key derivation mode the IV is replaced with a derivation of a context string.

Step 2: Chunk compression. The input is broken into 1024-byte chunks. Each chunk runs the compression function 16 times (once per 64-byte block within the chunk). The chunk start flag is set on the first block, and the chunk end flag is set on the last block. The counter increments per block. If the chunk is the only chunk, the root flag is set on the last block too.

Step 3: Tree construction. The output of each chunk is a 32-byte hash. If there is only one chunk, its output (with the root flag) is the final digest. Otherwise the chunk outputs are paired two at a time and combined via parent compression: h_new = compress(parent_key, left_child || right_child, counter=0, block=left_child concatenated with right_child, flags=PARENT|ROOT).

Step 4: Output expansion. To produce more than 32 bytes, the root hash is fed back into the compression function as a counter-driven output stream: output[i*64:(i+1)*64] = compress(root_h, zero_block, counter=i, flags=ROOT). The counter values 0, 1, 2, ... produce as many output blocks as the caller requests.

Step 5: Finalize. The caller takes the first digestLen bytes of the output stream. BLAKE3 supports any output length from 1 byte up to 2 to the power 64 bytes.

Java Program for BLAKE3 Hash


public class Blake3 {

    // BLAKE3 IV constant (8 32-bit words)
    private static final int[] IV = {
        0xb3c675f9, 0xa395edda, 0xb4f2f5a4, 0x7e4a8c79,
        0x9e31b2b3, 0x10c5e93e, 0xc96bb55c, 0x85dedab2
    };

    private static final int[][] SIGMA = {
        { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14,15},
        {14,10, 4, 8, 9,15,13, 6, 1,12, 0, 2,11, 7, 5, 3},
        {11, 8,12, 0, 5, 2,15,13,10,14, 3, 6, 7, 1, 9, 4},
        { 7, 9, 3, 1,13,12,11,14, 2, 6,15, 0, 8,10, 4, 5},
        { 9, 0, 5, 7, 2, 4,10,15,14, 1,11,12, 6, 8, 3,13},
        { 2,12, 6,10, 0,11, 8, 3, 4,13, 7, 5,15,14, 1, 9},
        {12, 5, 1,15,14,13, 4,10, 0, 7, 6, 3, 9, 2, 8,11},
        {13,11, 7,14,12, 1, 3, 9, 5, 0,15, 4, 8, 6, 2,10},
        { 6,15,14, 2,11, 3, 0, 8, 4,13, 7, 5, 9,10, 1,12},
        {10, 4, 8,13, 9, 6,11, 2,12, 3, 1, 7,15, 0, 5,14},
        { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14,15},
        {14,10, 4, 8, 9,15,13, 6, 1,12, 0, 2,11, 7, 5, 3},
        {11, 8,12, 0, 5, 2,15,13,10,14, 3, 6, 7, 1, 9, 4},
        { 7, 9, 3, 1,13,12,11,14, 2, 6,15, 0, 8,10, 4, 5}
    };

    // Domain separation flags
    private static final int CHUNK_START = 1 << 0;
    private static final int CHUNK_END   = 1 << 1;
    private static final int PARENT      = 1 << 2;
    private static final int ROOT        = 1 << 3;

    private final int[] key;
    private final byte[] chunk = new byte[1024];
    private int chunkLen = 0;
    private final java.util.List<int[]> subtrees = new java.util.ArrayList<>();
    private long chunkCounter = 0L;

    public Blake3() {
        key = new int[8];
        for (int i = 0; i < 8; i++) {
            key[i] = IV[i];
        }
    }

    private static int rotr(int x, int n) {
        return (x >>> n) | (x << (32 - n));
    }

    private void g(int[] v, int a, int b, int c, int d, int x, int y) {
        v[a] = v[a] + v[b] + x;
        v[d] = rotr(v[d] ^ v[a], 16);
        v[c] = v[c] + v[d];
        v[b] = rotr(v[b] ^ v[c], 12);
        v[a] = v[a] + v[b] + y;
        v[d] = rotr(v[d] ^ v[a], 8);
        v[c] = v[c] + v[d];
        v[b] = rotr(v[b] ^ v[c], 7);
    }

    private int[] compress(int[] chain, byte[] block, long counter, int flags) {
        int[] m = new int[16];
        for (int i = 0; i < 16; i++) {
            int b0 = block[4 * i]     & 0xff;
            int b1 = block[4 * i + 1] & 0xff;
            int b2 = block[4 * i + 2] & 0xff;
            int b3 = block[4 * i + 3] & 0xff;
            m[i] = b0 | (b1 << 8) | (b2 << 16) | (b3 << 24);
        }
        int[] v = new int[16];
        for (int i = 0; i < 8; i++) {
            v[i] = chain[i];
            v[i + 8] = IV[i];
        }
        v[12] ^= (int) counter;
        v[13] ^= (int) (counter >>> 32);
        v[14] ^= flags;
        for (int round = 0; round < 14; round++) {
            int[] s = SIGMA[round];
            g(v, 0, 4, 8, 12, m[s[ 0]], m[s[ 1]]);
            g(v, 1, 5, 9, 13, m[s[ 2]], m[s[ 3]]);
            g(v, 2, 6, 10, 14, m[s[ 4]], m[s[ 5]]);
            g(v, 3, 7, 11, 15, m[s[ 6]], m[s[ 7]]);
            g(v, 0, 5, 10, 15, m[s[ 8]], m[s[ 9]]);
            g(v, 1, 6, 11, 12, m[s[10]], m[s[11]]);
            g(v, 2, 7, 8, 13, m[s[12]], m[s[13]]);
            g(v, 3, 4, 9, 14, m[s[14]], m[s[15]]);
        }
        int[] out = new int[8];
        for (int i = 0; i < 8; i++) {
            out[i] = chain[i] ^ v[i] ^ v[i + 8];
        }
        return out;
    }

    private int[] compressChunk(int[] chain, byte[] chunk, int len, long counter, boolean isRoot) {
        int[] cv = chain;
        byte[] block = new byte[64];
        int blocks = (len + 63) / 64;
        for (int i = 0; i < blocks; i++) {
            int start = i * 64;
            int end = Math.min(start + 64, len);
            java.util.Arrays.fill(block, (byte) 0);
            for (int j = start; j < end; j++) {
                block[j - start] = chunk[j];
            }
            int flags = 0;
            if (i == 0) flags |= CHUNK_START;
            if (i == blocks - 1) flags |= CHUNK_END;
            if (isRoot && i == blocks - 1) flags |= ROOT;
            cv = compress(cv, block, counter, flags);
        }
        return cv;
    }

    private int[] makeParent(int[] left, int[] right, boolean isRoot) {
        byte[] block = new byte[64];
        for (int i = 0; i < 8; i++) {
            block[4 * i]     = (byte) left[i];
            block[4 * i + 1] = (byte) (left[i] >> 8);
            block[4 * i + 2] = (byte) (left[i] >> 16);
            block[4 * i + 3] = (byte) (left[i] >> 24);
            block[4 * i + 32]     = (byte) right[i];
            block[4 * i + 33] = (byte) (right[i] >> 8);
            block[4 * i + 34] = (byte) (right[i] >> 16);
            block[4 * i + 35] = (byte) (right[i] >> 24);
        }
        int flags = PARENT;
        if (isRoot) flags |= ROOT;
        return compress(IV, block, 0L, flags);
    }

    private int[] root() {
        java.util.List<int[]> nodes = new java.util.ArrayList<>(subtrees);
        if (chunkLen > 0 || nodes.isEmpty()) {
            nodes.add(compressChunk(key, chunk, chunkLen, chunkCounter,
                                    nodes.isEmpty() && chunkCounter == 0));
        }
        while (nodes.size() > 1) {
            java.util.List<int[]> next = new java.util.ArrayList<>();
            for (int i = 0; i < nodes.size(); i += 2) {
                int[] left = nodes.get(i);
                int[] right = (i + 1 < nodes.size()) ? nodes.get(i + 1) : new int[8];
                next.add(makeParent(left, right, next.isEmpty() && i + 2 >= nodes.size()));
            }
            nodes = next;
        }
        return nodes.get(0);
    }

    public void update(byte[] data, int offset, int length) {
        int idx = offset;
        int remaining = length;
        while (remaining > 0) {
            if (chunkLen == 1024) {
                subtrees.add(compressChunk(key, chunk, 1024, chunkCounter, false));
                chunkCounter++;
                chunkLen = 0;
            }
            int take = Math.min(1024 - chunkLen, remaining);
            System.arraycopy(data, idx, chunk, chunkLen, take);
            chunkLen += take;
            idx += take;
            remaining -= take;
        }
    }

    public byte[] digest(int outLen) {
        int[] rootChain = root();
        byte[] out = new byte[outLen];
        long counter = 0L;
        int produced = 0;
        byte[] zeroBlock = new byte[64];
        while (produced < outLen) {
            int[] block = compress(rootChain, zeroBlock, counter, ROOT);
            for (int i = 0; i < 8 && produced < outLen; i++) {
                int word = block[i];
                for (int j = 0; j < 4 && produced < outLen; j++, produced++) {
                    out[produced] = (byte) (word >> (8 * j));
                }
            }
            counter++;
        }
        return out;
    }

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

    public static void main(String[] args) {
        byte[] data = "The quick brown fox".getBytes();
        Blake3 hash = new Blake3();
        hash.update(data, 0, data.length);
        System.out.println("Input   : " + new String(data));
        System.out.println("BLAKE3  : " + toHex(hash.digest(32)));
        System.out.println("BLAKE3-512: " + toHex(hash.digest(64)));
    }
}

Code Explanation Line by Line

The IV constant is the BLAKE3 specific initialization vector, which differs from the BLAKE2s IV. Each of the eight 32-bit words comes from the BLAKE3 specification and was derived by hashing a constant string through BLAKE2b. The SIGMA table extends BLAKE2s's 10-row table to 14 rows; rows 10 through 13 reuse the first four rows. The 14-round count is the source of BLAKE3's slightly higher per-block cost compared to BLAKE2s but is offset by the parallelism.

The four domain separation constants CHUNK_START, CHUNK_END, PARENT, and ROOT are bit flags. CHUNK_START is set on the first block of every chunk so the compression function knows to mix the chunk start flag into v[14]. CHUNK_END is set on the last block of every chunk so the function knows the chunk has finished streaming. PARENT is set when compressing a parent node that combines two child digests. ROOT is set on the final compression that produces the output stream.

The compress method is a faithful BLAKE3 block compression: it builds the 16-word message vector m, initializes the 16-word working state v from the chain value and IV, mixes the counter and flags, runs 14 rounds of G calls using the sigma permutation, then applies the Davies-Meyer XOR. The compressChunk method runs compress on each 64-byte block within a 1024-byte chunk and chains the outputs together.

The makeParent method packs two 32-byte child chain values into a 64-byte block and calls compress with the PARENT flag. The root method walks the list of chunk outputs and recursively combines pairs into parent nodes until only one node remains. The digest method then expands that root node into an arbitrary length output stream by repeatedly calling compress on zero blocks with an incrementing counter.

Sample Input and Output

Input:    "The quick brown fox"
BLAKE3  : 32-byte digest, displayed as 64 lowercase hex characters
BLAKE3-512: 64-byte digest, displayed as 128 lowercase hex characters
Output length: variable, 1 byte to 2^64 bytes

The output is the requested number of bytes from the BLAKE3 output stream. The default 32-byte output is the same length as SHA-256, while the 64-byte output matches SHA-512. Because BLAKE3 produces output via counter mode, you can request any length without rehashing the input. To validate the implementation, hash the empty string and compare the result with the official BLAKE3 specification test vector.

Common Mistakes to Avoid

First, forgetting to set the CHUNK_START flag on the first block of each chunk is the most common implementation bug. Without that flag, the compression function has no way to know a new chunk is starting and the chain value does not isolate chunk boundaries.

Second, using the BLAKE2s IV instead of the BLAKE3 IV will produce a hash that looks structurally correct but does not match any published test vector. The two IVs are different constants and must not be substituted.

Third, running only 10 rounds instead of 14 rounds (copying the BLAKE2s round count) will silently produce digests that are too weak. BLAKE3 added four extra rounds to compensate for the parallelism, which means each individual chunk sees less mixing context overall.

Fourth, treating the tree as a list of nodes rather than a recursive pairing is a subtle bug. When the number of chunks is not a power of two, you must pair nodes two at a time and let the final unpaired node wait for the next level. Forgetting this asymmetry produces a hash that does not match a parallel implementation.

Real-World Applications

BLAKE3 is rapidly being adopted by tools that need both integrity and parallelism. The Bao project (now part of the BLAKE3 reference) uses BLAKE3 for authenticated streaming. The iroh content-addressed data platform from Protocol Labs uses BLAKE3 as its hash for chunks. The Rust standard library ecosystem widely uses the blake3 crate for file integrity, content addressing, and as the default hash in some container tools.

In the Java ecosystem, Bouncy Castle added BLAKE3 in version 1.70 as org.bouncycastle.crypto.digests.Blake3Digest. Apache Commons Crypto includes BLAKE3 in its native bindings. The orbweaver-go tool by the Filecoin project hashes file chunks with BLAKE3 for content addressing.

BLAKE3 also shines in build systems. The Bazel build tool considered BLAKE3 for action cache keys because its parallelism maps naturally to a multi-core build server. The Nix package manager uses BLAKE3 for store path integrity in recent versions. For any application that hashes gigabytes of content (container layers, machine learning datasets, video encoders), BLAKE3 delivers near disk-bandwidth throughput.

In the cryptocurrency space, the Solana blockchain uses BLAKE3 for hashing transaction signatures and ledger entries because of its throughput on validator hardware. Many Layer 2 rollup tools use BLAKE3 to hash batches of state transitions. The libsignal protocol library uses BLAKE3 in its X3DH key derivation flow.

Conclusion

BLAKE3 is a modern parallel hash that scales linearly with CPU cores while preserving the cryptographic strength of BLAKE2. We implemented the full single-threaded BLAKE3 algorithm in pure Java, walking through chunk compression, parent node pairing, and the counter-driven output stream. The program runs on the standard JDK with no external dependencies. Next you should compare BLAKE3 with BLAKE2 to see how the tree mode buys parallelism at the cost of slightly more work per block, then study Merkle trees as a primitive on their own, and finally explore the KangarooTwelve hash to see how SHA-3 family parallel modes differ.

Test Your Knowledge
How did you find this?

Comments

Join the discussion! Sign in with your Google or Blogger account, or comment as Anonymous - no account needed. For quick questions, also reach me on Telegram @cytestch.

Comments