Introduction

Every time your browser downloads a file, verifies a password, or opens an encrypted connection, it relies on a hash function — a mathematical meat-grinder that turns any message into a short, fixed-size fingerprint. Change one bit of the message and the fingerprint changes completely; given only the fingerprint, finding the original message should be computationally impossible.

For three decades the dominant design was the Merkle–Damgård construction: chop the message into blocks, feed each block into a fixed compression function one by one, carry the result as running state into the next block. MD5, SHA-1 and SHA-2 are all built this way. It worked — until researchers started finding structural weaknesses. MD5 and SHA-1 were eventually broken for collision resistance.

In 2007 NIST launched a public competition to find a new paradigm. In 2012 the winner was announced: Keccak, built on a completely different idea called the sponge construction. Instead of chaining blocks through a compression function, a sponge absorbs the entire message into a large internal state, then squeezes out as many output bits as you need.

In 2015 NIST standardized Keccak as SHA-3 (FIPS 202), giving the world a hash family with provable security properties unlike anything that came before.

Try the Sponge

The demo below runs a toy sponge with a tiny 16-bit state (real Keccak uses 1600 bits). Type a message, choose a block size, and watch each block get XORed into the rate portion of the state, then mixed by a simplified permutation. After absorbing all blocks the sponge squeezes out your digest.

<div class="controls">
  <label>Message: <input id="msg" type="text" value="Hello, Keccak!" maxlength="48"></label>
  <label>Block size (bits): <select id="rate">
    <option value="4">4 bits (r=4, c=12)</option>
    <option value="8" selected>8 bits (r=8, c=8)</option>
  </select></label>
  <button id="absorb" type="button">Absorb &amp; Squeeze</button>
  <button id="reset" type="button" class="ghost">Reset</button>
</div>
<div id="sponge-vis" class="sponge-vis" aria-live="polite"></div>
<div id="digest-out" class="digest-out"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.controls { display: flex; flex-wrap: wrap; gap: .5rem .8rem; align-items: center; margin-bottom: .9rem; }
.controls label { display: flex; align-items: center; gap: .3rem; font-size: .88rem; }
input[type=text] { font: inherit; border: 1px solid #bbb; border-radius: 6px; padding: .25rem .5rem; width: 160px; }
select { font: inherit; border: 1px solid #bbb; border-radius: 6px; padding: .25rem .4rem; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557; background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.sponge-vis { display: flex; flex-direction: column; gap: .45rem; min-height: 2rem; }
.step { display: flex; align-items: flex-start; gap: .6rem; padding: .4rem .6rem; border-radius: 8px; background: #f0f4f8; border-left: 3px solid #8aabcc; font-size: .83rem; line-height: 1.45; }
.step.absorb { border-left-color: #2a7abf; }
.step.squeeze { border-left-color: #0a7d33; background: #eaf6ee; }
.step.perm { border-left-color: #e08800; background: #fef8ec; }
.step-label { font-weight: 700; min-width: 68px; color: #1d3557; }
.step.squeeze .step-label { color: #0a7d33; }
.step.perm .step-label { color: #b06800; }
.state-bar { display: flex; gap: 2px; flex-wrap: nowrap; margin-top: .2rem; }
.state-bar span { display: inline-block; width: 14px; height: 14px; border-radius: 3px; background: #c9d6e3; font-size: 9px; line-height: 14px; text-align: center; font-family: monospace; }
.state-bar span.rate-bit { background: #2a7abf; color: #fff; }
.state-bar span.cap-bit { background: #8aabcc; color: #fff; }
.state-bar span.out-bit { background: #0a7d33; color: #fff; }
.digest-out { margin-top: .75rem; font: 700 13px ui-monospace, monospace; color: #1d3557; word-break: break-all; min-height: 1.4em; }
.digest-out .label { font-weight: 400; color: #555; margin-right: .4rem; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Toy sponge construction: state = 16 bits, split into rate r and capacity c=16-r.
// Permutation: a simple reversible mixing (NOT cryptographic — illustrative only).
// Each absorb step XORs a message block (r bits) into the rate portion, then permutes.
// Squeeze: read r bits of output (permute again if more needed).

function permute(state, bits) {
  // Simple bijective mixing: rotate left by 3, then XOR with a constant.
  const CONST = 0b1010110011010101 & ((1 << bits) - 1);
  let s = state & ((1 << bits) - 1);
  // left rotate by 3
  s = ((s << 3) | (s >>> (bits - 3))) & ((1 << bits) - 1);
  s ^= CONST;
  return s;
}

function msgToBlocks(msg, rBits) {
  // Convert string to bit-array via char codes (low rBits of each char pair)
  const bytes = [];
  for (let i = 0; i < msg.length; i++) bytes.push(msg.charCodeAt(i) & 0xff);
  // Padding: append 0x01 then zeros up to block boundary, then 0x80 at end
  bytes.push(0x01);
  while ((bytes.length * 8) % rBits !== 0) bytes.push(0x00);
  bytes[bytes.length - 1] |= 0x80;
  // Pack into r-bit blocks
  const allBits = [];
  for (const b of bytes) for (let i = 7; i >= 0; i--) allBits.push((b >> i) & 1);
  const blocks = [];
  for (let i = 0; i < allBits.length; i += rBits) {
    let block = 0;
    for (let j = 0; j < rBits; j++) block = (block << 1) | (allBits[i + j] || 0);
    blocks.push(block);
  }
  return blocks;
}

function toBin(val, bits) {
  return val.toString(2).padStart(bits, '0');
}

function renderStateBits(state, totalBits, rateBits, highlight) {
  // highlight: 'rate', 'cap', 'out', null
  const bar = document.createElement('div');
  bar.className = 'state-bar';
  for (let i = totalBits - 1; i >= 0; i--) {
    const bit = (state >> i) & 1;
    const sp = document.createElement('span');
    sp.textContent = bit;
    const isRate = i >= (totalBits - rateBits);
    if (highlight === 'out' && isRate) sp.className = 'out-bit';
    else if (isRate) sp.className = 'rate-bit';
    else sp.className = 'cap-bit';
    bar.appendChild(sp);
  }
  return bar;
}

function addStep(vis, cls, label, text, state, totalBits, rateBits, highlight) {
  const div = document.createElement('div');
  div.className = 'step ' + cls;
  const lbl = document.createElement('span');
  lbl.className = 'step-label';
  lbl.textContent = label;
  div.appendChild(lbl);
  const info = document.createElement('span');
  info.textContent = text;
  div.appendChild(info);
  if (state !== null) {
    div.appendChild(renderStateBits(state, totalBits, rateBits, highlight));
  }
  vis.appendChild(div);
}

function runSponge() {
  const msg = document.getElementById('msg').value || 'Keccak';
  const rBits = parseInt(document.getElementById('rate').value);
  const totalBits = 16;
  const cBits = totalBits - rBits;
  const rateMask = ((1 << rBits) - 1) << cBits;

  const vis = document.getElementById('sponge-vis');
  const digestOut = document.getElementById('digest-out');
  vis.innerHTML = '';
  digestOut.innerHTML = '';

  const blocks = msgToBlocks(msg, rBits);
  let state = 0;

  addStep(vis, 'absorb', 'Init', fmt('State = %toBin%  (rate=%rBits%b blue | cap=%cBits%b teal)', { toBin: toBin(state, totalBits), rBits: rBits, cBits: cBits }), state, totalBits, rBits, null);

  // Absorb phase
  for (let i = 0; i < blocks.length; i++) {
    const block = blocks[i];
    // XOR block into rate (top rBits)
    const blockInState = block << cBits;
    state ^= blockInState;
    addStep(vis, 'absorb', `XOR[${i+1}]`,
      fmt('block %toBin% → XOR into rate', { toBin: toBin(block, rBits) }),
      state, totalBits, rBits, 'rate');
    // Permute
    state = permute(state, totalBits);
    addStep(vis, 'perm', 'Permute',
      `f(state) = ${toBin(state, totalBits)}`,
      state, totalBits, rBits, null);
  }

  // Squeeze phase: read rBits from rate
  const outputBits = (state >> cBits) & ((1 << rBits) - 1);
  const hexOut = outputBits.toString(16).toUpperCase().padStart(Math.ceil(rBits / 4), '0');
  addStep(vis, 'squeeze', 'Squeeze',
    fmt('Read rate bits → digest = %toBin% (0x%hexOut%)', { toBin: toBin(outputBits, rBits), hexOut: hexOut }),
    state, totalBits, rBits, 'out');

  digestOut.innerHTML = fmt('<span class="label">Digest:</span>%toBin% (hex: 0x%hexOut%)', { toBin: toBin(outputBits, rBits), hexOut: hexOut });

  console.log(fmt('Sponge demo: msg="%msg%" r=%rBits% blocks=%length% digest=0x%hexOut%', { msg: msg, rBits: rBits, length: blocks.length, hexOut: hexOut }));
}

document.getElementById('absorb').addEventListener('click', runSponge);
document.getElementById('reset').addEventListener('click', () => {
  document.getElementById('msg').value = 'Hello, Keccak!';
  document.getElementById('rate').value = '8';
  document.getElementById('sponge-vis').innerHTML = '';
  document.getElementById('digest-out').innerHTML = '';
});
Result
Console

Notice the key properties. Absorb phase: each message block is XORed into the first r bits (the rate), then the full state is permuted. Squeeze phase: once all blocks are absorbed, the first r bits of state are read as output — and if you need more bits, permute again and read more. The capacity c bits are never directly exposed to the message or the output, providing the security margin. Change one character of input and the digest changes entirely.

The Real Complexity

What makes the sponge more than a curiosity?

  • Security from the permutation. Keccak's core is a fixed, public permutation f on 1600 bits. If f behaves like a random permutation, the sponge is indifferentiable from a random oracle — a much stronger claim than anything Merkle–Damgård offered.
  • Capacity is the security parameter. With capacity c bits, finding a collision requires roughly 2c/22^{c/2} evaluations of f; a preimage requires 2c2^{c}. SHA3-256 sets c = 512, giving 256-bit collision resistance and 512-bit preimage resistance.
  • No length-extension attacks. In Merkle–Damgård, knowing H(m) lets an attacker compute H(m ∥ extra) without knowing m. The sponge prevents this because the capacity bits are hidden — you cannot resume the state from the output alone.
  • Variable output length for free. Need a 224-bit hash? A 512-bit hash? An unlimited stream (SHAKE128/SHAKE256)? Just squeeze more bits. Merkle–Damgård needs separate algorithms; the sponge is one design.
  • Status: proven secure under a standard assumption. Keccak's security is not an open problem — it rests on the hardness of inverting a specific permutation. Unlike factoring or discrete log, no quantum speedup breaks it catastrophically, making SHA-3 a strong post-quantum candidate.

Where It Matters

The sponge construction is not just theory — it shows up throughout modern software:

  • Ethereum and blockchain: Ethereum's core hashing (Keccak-256) is a direct application of the sponge. Every transaction ID, block hash and Merkle root is a sponge squeeze.
  • TLS and secure communications: SHA-3 variants are part of modern cipher suites. Every HTTPS handshake can invoke a sponge.
  • Password hashing: schemes like Argon2 use sponge-based primitives internally to stretch passwords before storage.
  • Digital signatures: EdDSA (used in SSH keys, Signal, Tor) uses a SHA-512 hash internally; post-quantum signatures like SPHINCS+ use SHAKE variants.
  • XOFs (Extendable Output Functions): SHAKE128 and SHAKE256 squeeze out as many bytes as a protocol needs — perfect for key derivation, stream ciphers, and learning-with-errors schemes.
  • CAESAR and NIST PQC competitions: many lightweight authenticated-encryption schemes (Ascon, Xoodyak) are built on sponge variants.

Whenever a system needs to hash variable-length data into a fixed fingerprint with strong provable guarantees, the sponge is the modern answer.

Conclusion

The sponge construction is one of cryptography's most elegant ideas: absorb any message into a large, opaque state; squeeze out any number of output bits; keep a hidden capacity that attackers can never see. It replaces a thirty-year chaining habit with a single, flexible, provably secure primitive.

Keccak — now SHA-3 — won NIST's decade-long competition not just by being secure, but by being structurally different from the family it joined. Where SHA-2 fixed known weaknesses in the Merkle–Damgård chain, SHA-3 threw out the chain entirely.

As cryptography moves toward a post-quantum world, the sponge's resistance to quantum speedups makes it one of the cornerstones of tomorrow's security infrastructure. The next time you see a 0x prefix followed by 64 hex characters, you may be looking at a sponge that just finished squeezing.

Share this article

Pick a channel — or use your device's native share sheet.

Comments

Loading comments...

https://www.kipuhub.com/en/article/sponge-keccak/Content licensed under CC BY-NC 4.0.