<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 & 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 = '';
});
<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 & 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 = '';
});
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.
Comments
Loading comments...