The demo below is a complete SHA-256 written from scratch in plain JavaScript — and it checks its own answer against your browser's built-in crypto.subtle implementation. Type a message or pick a preset, then work through the four panels:
<p class="hint">A real, from-scratch SHA-256 running in your browser. Type anything: you will see how it is padded, expanded into 64 schedule words and squeezed through 64 rounds — and how one flipped bit scrambles the whole digest.</p>
<label for="msg" class="lbl">Message (encoded as UTF-8)</label>
<textarea id="msg" rows="2" spellcheck="false">abc</textarea>
<div class="btns">
<span class="small">Try:</span>
<button type="button" class="ghost preset" data-i="0">"abc"</button>
<button type="button" class="ghost preset" data-i="1">empty string</button>
<button type="button" class="ghost preset" data-i="2">quick brown fox</button>
<button type="button" class="ghost preset" data-i="3">56 chars (2 blocks)</button>
</div>
<div class="digest"><span class="dl">SHA-256</span><code id="digest"></code><span id="verify" class="verify"></span></div>
<h4>1 · Padding into 512-bit blocks</h4>
<p class="small" id="padinfo"></p>
<div class="nav">
<button type="button" id="prevB" class="ghost" aria-label="Previous block">‹</button>
<span id="blockLbl" class="small"></span>
<button type="button" id="nextB" class="ghost" aria-label="Next block">›</button>
</div>
<div class="legend small">
<span><i class="m">1</i> message</span>
<span><i class="o">1</i> appended 1</span>
<span><i class="z">0</i> zero padding</span>
<span><i class="l">0</i> length (64 bits)</span>
</div>
<div id="bits" class="bits"></div>
<h4>2 · Message schedule W0…W63</h4>
<p class="small">The 16 words of the block are copied into W0…W15 (blue border); the other 48 are mixed from earlier words. Tap any word to see its formula.</p>
<div id="sched" class="sched"></div>
<p class="small mono" id="schedf"></p>
<h4>3 · 64 rounds of compression</h4>
<div class="btns">
<button type="button" id="step">Step</button>
<button type="button" id="run">Run</button>
<button type="button" id="rreset" class="ghost">Reset</button>
<span id="rcount" class="small"></span>
</div>
<input id="tslider" type="range" min="0" max="64" value="0" aria-label="Round number" />
<div id="regs" class="regs"></div>
<p class="small" id="rinfo"></p>
<p class="small mono" id="rdetail"></p>
<div id="ff" class="ff hidden"></div>
<h4>4 · Avalanche effect</h4>
<p class="small">Flip a single bit of the input and compare the two digests. Each red square is an output bit that changed.</p>
<div class="btns">
<label for="flip" class="small">Input bit</label>
<input id="flip" type="number" min="0" value="0" />
<button type="button" id="flipRand">Flip a random bit</button>
<button type="button" id="flipAll" class="ghost">Try every bit</button>
</div>
<div id="avalDigests" class="small mono"></div>
<div id="avalGrid" class="agrid"></div>
<p class="status" id="avalStatus"></p>
:root { --fg:#1f2933; --muted:#52606d; --bg:#ffffff; --panel:#f1f4f8; --line:#cfd8e3; --accent:#1d3557; --accent-fg:#ffffff;
--msg:#1d4ed8; --one:#d00000; --zero:#9aa5b1; --len:#b45309; --fresh:#fde68a; --shift:#dbeafe; --ok:#0a7d33; --bad:#c92f3c; --on:#e63946; }
@media (prefers-color-scheme: dark) {
:root { --fg:#e4e7eb; --muted:#9aa5b1; --bg:#14181f; --panel:#1f2630; --line:#3a4656; --accent:#8fb3e8; --accent-fg:#0d1117;
--msg:#7fb2ff; --one:#ff6b6b; --zero:#5d6b7a; --len:#f0a35e; --fresh:#6b5512; --shift:#1e3a5f; --ok:#4cc38a; --bad:#ff7b84; --on:#ff6b6b; }
}
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: var(--fg); background: var(--bg); margin: 0; padding: 4px; font-size: 14px; }
h4 { margin: 1rem 0 .3rem; font-size: 1rem; }
.hint { margin: 0 0 .6rem; line-height: 1.45; color: var(--muted); }
.small { font-size: 12.5px; color: var(--muted); line-height: 1.45; }
.mono { font-family: ui-monospace, Menlo, Consolas, monospace; word-break: break-all; }
.lbl { font-weight: 600; display: block; margin-bottom: .2rem; }
textarea { width: 100%; font: 14px ui-monospace, Menlo, Consolas, monospace; padding: .4rem; border: 1px solid var(--line); border-radius: 6px; background: var(--panel); color: var(--fg); resize: vertical; }
.btns { display: flex; gap: .4rem; flex-wrap: wrap; align-items: center; margin: .4rem 0; }
button { font: 600 13px system-ui, sans-serif; padding: .35rem .7rem; border: 1px solid var(--accent); background: var(--accent); color: var(--accent-fg); border-radius: 7px; cursor: pointer; }
button.ghost { background: transparent; color: var(--accent); }
button:disabled { opacity: .35; cursor: default; }
.digest { display: flex; flex-wrap: wrap; gap: .4rem; align-items: center; background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: .45rem .6rem; margin: .4rem 0; }
.digest code { font: 700 12.5px ui-monospace, Menlo, Consolas, monospace; word-break: break-all; }
.dl { font-weight: 700; }
.verify { font-size: 12px; color: var(--muted); }
.verify.ok { color: var(--ok); } .verify.bad { color: var(--bad); }
.nav { display: flex; align-items: center; gap: .5rem; margin: .3rem 0; }
.legend { display: flex; gap: .7rem; flex-wrap: wrap; margin: .2rem 0 .3rem; }
.legend i, .bits i { font-style: normal; }
.m { color: var(--msg); font-weight: 700; } .o { color: var(--one); font-weight: 800; } .z { color: var(--zero); } .l { color: var(--len); font-weight: 700; }
.bits { font: 10px/1.35 ui-monospace, Menlo, Consolas, monospace; overflow-x: auto; }
.bitrow { display: flex; gap: 6px; white-space: nowrap; }
.wlab { width: 26px; color: var(--muted); text-align: right; }
.whex { color: var(--muted); }
.sched { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 3px; margin: .3rem 0; }
.wc { font: 10.5px ui-monospace, Menlo, Consolas, monospace; padding: 3px 2px; border-radius: 5px; background: var(--panel); color: var(--fg); border: 1px solid var(--line); white-space: nowrap; overflow: hidden; }
.wc.fromblk { border-color: var(--msg); }
.wc.sel { outline: 2px solid var(--accent); }
.wc.cur { background: var(--fresh); }
input[type=range] { width: 100%; margin: .3rem 0; }
input[type=number] { width: 6.5em; padding: .3rem; border: 1px solid var(--line); border-radius: 6px; background: var(--panel); color: var(--fg); }
.regs { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 4px; }
.reg { background: var(--panel); border: 1px solid var(--line); border-radius: 7px; padding: 4px; text-align: center; transition: background .15s; }
.reg.fresh { background: var(--fresh); }
.reg.shifted { background: var(--shift); }
.rn { display: block; font-weight: 800; font-size: 12px; }
.rv { font: 12px ui-monospace, Menlo, Consolas, monospace; }
.ff { background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: .5rem; margin-top: .4rem; font-size: 12px; overflow-x: auto; }
.ff table { border-collapse: collapse; font: 11.5px ui-monospace, Menlo, Consolas, monospace; }
.ff td { padding: 1px 4px; }
.hidden { display: none; }
.agrid { display: grid; grid-template-columns: repeat(32, 1fr); gap: 1px; max-width: 360px; margin: .4rem 0; }
.agrid i { aspect-ratio: 1; background: var(--panel); border: 1px solid var(--line); }
.agrid i.on { background: var(--on); border-color: var(--on); }
.status { font-weight: 600; margin: .3rem 0; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// A complete SHA-256 (FIPS 180-4) in plain JavaScript: padding, message schedule, 64 rounds, Davies–Meyer feed-forward.
// Round constants K0…K63: the first 32 bits of the fractional parts of the cube roots of the first 64 primes.
const K = [
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
];
// Initial hash value H(0): the first 32 bits of the fractional parts of the square roots of the first 8 primes.
const H0 = [0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19];
// The first 64 primes, only used to show where each constant comes from.
const PRIMES = [];
for (let n = 2; PRIMES.length < 64; n++) {
let isP = true;
for (const p of PRIMES) { if (p * p > n) break; if (n % p === 0) { isP = false; break; } }
if (isP) PRIMES.push(n);
}
const rotr = (x, n) => (x >>> n) | (x << (32 - n));
const bigS0 = (a) => (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) >>> 0;
const bigS1 = (e) => (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) >>> 0;
const smallS0 = (x) => (rotr(x, 7) ^ rotr(x, 18) ^ (x >>> 3)) >>> 0;
const smallS1 = (x) => (rotr(x, 17) ^ rotr(x, 19) ^ (x >>> 10)) >>> 0;
const chF = (e, f, g) => ((e & f) ^ (~e & g)) >>> 0;
const majF = (a, b, c) => ((a & b) ^ (a & c) ^ (b & c)) >>> 0;
const hex8 = (x) => (x >>> 0).toString(16).padStart(8, '0');
// Padding: append a 1 bit, then zeros, then the message length in bits as a 64-bit big-endian integer, so the total is a multiple of 512 bits.
function padMessage(bytes) {
const L = bytes.length;
const total = Math.ceil((L + 9) / 64) * 64;
const out = new Uint8Array(total);
out.set(bytes);
out[L] = 0x80;
const bitLen = L * 8;
const dv = new DataView(out.buffer);
dv.setUint32(total - 8, Math.floor(bitLen / 0x100000000));
dv.setUint32(total - 4, bitLen >>> 0);
return out;
}
// Hash the bytes and keep every intermediate value (schedule, the state after each round) so the page can display them.
function sha256Trace(bytes) {
const padded = padMessage(bytes);
const dv = new DataView(padded.buffer);
let H = H0.slice();
const blocks = [];
for (let off = 0; off < padded.length; off += 64) {
const W = new Array(64);
for (let t = 0; t < 16; t++) W[t] = dv.getUint32(off + 4 * t);
for (let t = 16; t < 64; t++) W[t] = (smallS1(W[t - 2]) + W[t - 7] + smallS0(W[t - 15]) + W[t - 16]) >>> 0;
const states = [H.slice()];
const rounds = [];
let [a, b, c, d, e, f, g, h] = H;
for (let t = 0; t < 64; t++) {
const s1 = bigS1(e), ch = chF(e, f, g);
const T1 = (h + s1 + ch + K[t] + W[t]) >>> 0;
const s0 = bigS0(a), mj = majF(a, b, c);
const T2 = (s0 + mj) >>> 0;
h = g; g = f; f = e; e = (d + T1) >>> 0;
d = c; c = b; b = a; a = (T1 + T2) >>> 0;
states.push([a, b, c, d, e, f, g, h]);
rounds.push({ s1, ch, T1, s0, mj, T2 });
}
const Hin = H;
// Feed-forward: add the input chaining value to the output of the 64 rounds (this makes the step one-way).
H = H.map((x, i) => (x + states[64][i]) >>> 0);
blocks.push({ W, states, rounds, Hin, Hout: H.slice() });
}
return { padded, blocks, digest: H.map(hex8).join('') };
}
// Everything below only draws the page; the hash itself is complete above.
const $ = (id) => document.getElementById(id);
const enc = new TextEncoder();
const PRESETS = ['abc', '', 'The quick brown fox jumps over the lazy dog', 'abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq'];
const NAMES = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];
let tr = null, bytes = new Uint8Array(0), blk = 0, t = 0, timer = null, selW = 16;
function recompute() {
bytes = enc.encode($('msg').value);
tr = sha256Trace(bytes);
blk = 0; t = 0; stop();
$('digest').textContent = tr.digest;
$('flip').max = Math.max(0, bytes.length * 8 - 1);
if (+$('flip').value > bytes.length * 8 - 1) $('flip').value = 0;
renderAll();
verify();
avalanche();
}
async function verify() {
const el = $('verify');
const want = tr.digest;
try {
if (!(window.crypto && crypto.subtle)) throw new Error('no subtle');
const buf = await crypto.subtle.digest('SHA-256', bytes);
const got = Array.from(new Uint8Array(buf), (x) => x.toString(16).padStart(2, '0')).join('');
if (want !== tr.digest) return;
el.textContent = got === want ? "✓ matches the browser’s built-in crypto.subtle" : "✗ differs from crypto.subtle";
el.className = 'verify ' + (got === want ? 'ok' : 'bad');
} catch (_) {
el.textContent = "(built-in check not available here)";
el.className = 'verify';
}
}
function renderPad() {
const L = bytes.length, total = tr.padded.length, nb = total / 64;
const zeros = total * 8 - L * 8 - 1 - 64;
$('padinfo').textContent = i18nFmt("%bytes% bytes = %bits% bits of message, then one 1 bit, %zeros% zero bits and a 64-bit length field → %blocks% block(s) of 512 bits.", { bytes: L, bits: L * 8, zeros, blocks: nb });
$('blockLbl').textContent = i18nFmt("Block %i% of %n%", { i: blk + 1, n: nb });
$('prevB').disabled = blk === 0;
$('nextB').disabled = blk === nb - 1;
const g = $('bits');
g.innerHTML = '';
const off = blk * 64;
for (let w = 0; w < 16; w++) {
const row = document.createElement('div');
row.className = 'bitrow';
const lab = document.createElement('span');
lab.className = 'wlab';
lab.textContent = 'W' + w;
row.appendChild(lab);
const bitsEl = document.createElement('span');
for (let k = 0; k < 4; k++) {
const i = off + w * 4 + k;
const byte = tr.padded[i];
for (let j = 7; j >= 0; j--) {
const s = document.createElement('i');
s.textContent = (byte >> j) & 1;
let cls = 'z';
if (i < L) cls = 'm';
else if (i >= total - 8) cls = 'l';
else if (i === L && j === 7) cls = 'o';
s.className = cls;
bitsEl.appendChild(s);
}
}
row.appendChild(bitsEl);
const hx = document.createElement('span');
hx.className = 'whex';
hx.textContent = hex8(tr.blocks[blk].W[w]);
row.appendChild(hx);
g.appendChild(row);
}
}
function renderSched() {
const W = tr.blocks[blk].W;
const g = $('sched');
g.innerHTML = '';
for (let i = 0; i < 64; i++) {
const d = document.createElement('button');
d.type = 'button';
d.className = 'wc' + (i < 16 ? ' fromblk' : '') + (i === selW ? ' sel' : '') + (t > 0 && i === t - 1 ? ' cur' : '');
d.textContent = 'W' + i + ' ' + hex8(W[i]);
d.addEventListener('click', () => { selW = i; renderSched(); });
g.appendChild(d);
}
$('schedf').textContent = selW < 16
? i18nFmt("W%t% is word %t% of the padded block, copied unchanged.", { t: selW })
: i18nFmt("W%t% = σ1(W%a%) + W%b% + σ0(W%c%) + W%d% = %v% (mod 2³²)", { t: selW, a: selW - 2, b: selW - 7, c: selW - 15, d: selW - 16, v: hex8(W[selW]) });
}
function renderRounds() {
const B = tr.blocks[blk];
const cur = B.states[t], prev = t > 0 ? B.states[t - 1] : null;
$('tslider').value = t;
$('rcount').textContent = i18nFmt("Round %t% / 64", { t });
const g = $('regs');
g.innerHTML = '';
NAMES.forEach((n, i) => {
const d = document.createElement('div');
let cls = 'reg';
if (prev && (i === 0 || i === 4)) cls += ' fresh';
else if (prev) cls += ' shifted';
d.className = cls;
const rn = document.createElement('span'); rn.className = 'rn'; rn.textContent = n;
const rv = document.createElement('span'); rv.className = 'rv'; rv.textContent = hex8(cur[i]);
d.appendChild(rn); d.appendChild(rv);
g.appendChild(d);
});
if (t === 0) {
$('rinfo').textContent = blk === 0 ? "Start: a…h hold the initial hash value H(0) — the first 32 bits of the fractional parts of the square roots of 2, 3, 5, 7, 11, 13, 17, 19." : "Start: a…h hold the chaining value produced by the previous block.";
$('rdetail').textContent = '';
} else {
const r = B.rounds[t - 1], p = PRIMES[t - 1];
$('rinfo').textContent = i18nFmt("Round %t% used W%i% = %w% and K%i% = %k% (first 32 bits of the fractional part of ∛%p% = %root%…). Yellow = freshly computed a and e; blue = values that just slid one place.", { t, i: t - 1, w: hex8(B.W[t - 1]), k: hex8(K[t - 1]), p, root: Math.cbrt(p).toFixed(6) });
$('rdetail').textContent = i18nFmt("Σ1(e)=%s1% Ch(e,f,g)=%ch% T1=h+Σ1+Ch+K+W=%t1% · Σ0(a)=%s0% Maj(a,b,c)=%maj% T2=%t2% → a=T1+T2, e=d+T1", { s1: hex8(r.s1), ch: hex8(r.ch), t1: hex8(r.T1), s0: hex8(r.s0), maj: hex8(r.mj), t2: hex8(r.T2) });
}
const ff = $('ff');
if (t === 64) {
ff.className = 'ff';
ff.innerHTML = '';
const head = document.createElement('div');
head.textContent = "Davies–Meyer feed-forward: add the block’s input chaining value to the final a…h, word by word (mod 2³²).";
ff.appendChild(head);
const tb = document.createElement('table');
NAMES.forEach((n, i) => {
const row = document.createElement('tr');
[ 'H' + i, hex8(B.Hin[i]), '+', n + ' = ' + hex8(B.states[64][i]), '=', hex8(B.Hout[i]) ].forEach((txt) => {
const td = document.createElement('td'); td.textContent = txt; row.appendChild(td);
});
tb.appendChild(row);
});
ff.appendChild(tb);
const foot = document.createElement('div');
foot.textContent = blk === tr.blocks.length - 1 ? "This was the last block, so H0‖H1‖…‖H7 is the SHA-256 digest shown above." : i18nFmt("These eight words become the chaining value for block %n%.", { n: blk + 2 });
ff.appendChild(foot);
} else {
ff.className = 'ff hidden';
}
}
function renderAll() { renderPad(); renderSched(); renderRounds(); }
function avalanche() {
const st = $('avalStatus'), grid = $('avalGrid'), dg = $('avalDigests');
const nbits = bytes.length * 8;
grid.innerHTML = ''; dg.innerHTML = '';
if (!nbits) { st.textContent = "Type at least one character to have a bit to flip."; return; }
let bit = Math.min(Math.max(0, Math.floor(+$('flip').value || 0)), nbits - 1);
$('flip').value = bit;
const b2 = bytes.slice();
b2[bit >> 3] ^= 0x80 >> (bit & 7);
const d1 = tr.digest, d2 = sha256Trace(b2).digest;
let diff = 0;
for (let i = 0; i < 64; i++) {
const x = parseInt(d1[i], 16) ^ parseInt(d2[i], 16);
for (let j = 3; j >= 0; j--) {
const on = (x >> j) & 1;
diff += on;
const c = document.createElement('i');
if (on) c.className = 'on';
grid.appendChild(c);
}
}
[["original:", d1], ["flipped:", d2]].forEach(([lab, d]) => {
const line = document.createElement('div');
line.textContent = lab + ' ' + d;
dg.appendChild(line);
});
st.textContent = i18nFmt("Flipped input bit %bit% (in byte %byte%): %n% of 256 output bits changed (%pct%). A good hash changes about 128 — half, at random.", { bit, byte: bit >> 3, n: diff, pct: (diff / 256 * 100).toFixed(1) + '%' });
}
function popcountDiff(d1, d2) {
let n = 0;
for (let i = 0; i < 64; i++) { let x = parseInt(d1[i], 16) ^ parseInt(d2[i], 16); while (x) { n += x & 1; x >>= 1; } }
return n;
}
function flipAll() {
const nbits = Math.min(bytes.length * 8, 1024);
if (!nbits) { $('avalStatus').textContent = "Type at least one character to have a bit to flip."; return; }
let sum = 0, mn = 256, mx = 0;
for (let bit = 0; bit < nbits; bit++) {
const b2 = bytes.slice();
b2[bit >> 3] ^= 0x80 >> (bit & 7);
const n = popcountDiff(tr.digest, sha256Trace(b2).digest);
sum += n; mn = Math.min(mn, n); mx = Math.max(mx, n);
}
$('avalStatus').textContent = i18nFmt("Flipped each of the first %n% input bits in turn: on average %avg% of 256 output bits changed (min %min%, max %max%). Ideal: 128.", { n: nbits, avg: (sum / nbits).toFixed(1), min: mn, max: mx });
}
function stop() {
if (timer) { clearInterval(timer); timer = null; }
$('run').textContent = "Run";
}
$('msg').addEventListener('input', recompute);
document.querySelectorAll('.preset').forEach((b) => b.addEventListener('click', () => { $('msg').value = PRESETS[+b.dataset.i]; recompute(); }));
$('prevB').addEventListener('click', () => { if (blk > 0) { blk--; t = 0; stop(); renderAll(); } });
$('nextB').addEventListener('click', () => { if (blk < tr.blocks.length - 1) { blk++; t = 0; stop(); renderAll(); } });
$('step').addEventListener('click', () => { stop(); if (t < 64) { t++; renderSched(); renderRounds(); } });
$('rreset').addEventListener('click', () => { stop(); t = 0; renderSched(); renderRounds(); });
$('tslider').addEventListener('input', () => { stop(); t = +$('tslider').value; renderSched(); renderRounds(); });
$('run').addEventListener('click', () => {
if (timer) { stop(); return; }
if (t >= 64) t = 0;
$('run').textContent = "Pause";
timer = setInterval(() => {
t++;
renderSched(); renderRounds();
if (t >= 64) stop();
}, 70);
});
$('flip').addEventListener('input', avalanche);
$('flipRand').addEventListener('click', () => {
const nbits = bytes.length * 8;
if (nbits) $('flip').value = Math.floor(Math.random() * nbits);
avalanche();
});
$('flipAll').addEventListener('click', flipAll);
recompute();
<p class="hint">A real, from-scratch SHA-256 running in your browser. Type anything: you will see how it is padded, expanded into 64 schedule words and squeezed through 64 rounds — and how one flipped bit scrambles the whole digest.</p>
<label for="msg" class="lbl">Message (encoded as UTF-8)</label>
<textarea id="msg" rows="2" spellcheck="false">abc</textarea>
<div class="btns">
<span class="small">Try:</span>
<button type="button" class="ghost preset" data-i="0">"abc"</button>
<button type="button" class="ghost preset" data-i="1">empty string</button>
<button type="button" class="ghost preset" data-i="2">quick brown fox</button>
<button type="button" class="ghost preset" data-i="3">56 chars (2 blocks)</button>
</div>
<div class="digest"><span class="dl">SHA-256</span><code id="digest"></code><span id="verify" class="verify"></span></div>
<h4>1 · Padding into 512-bit blocks</h4>
<p class="small" id="padinfo"></p>
<div class="nav">
<button type="button" id="prevB" class="ghost" aria-label="Previous block">‹</button>
<span id="blockLbl" class="small"></span>
<button type="button" id="nextB" class="ghost" aria-label="Next block">›</button>
</div>
<div class="legend small">
<span><i class="m">1</i> message</span>
<span><i class="o">1</i> appended 1</span>
<span><i class="z">0</i> zero padding</span>
<span><i class="l">0</i> length (64 bits)</span>
</div>
<div id="bits" class="bits"></div>
<h4>2 · Message schedule W0…W63</h4>
<p class="small">The 16 words of the block are copied into W0…W15 (blue border); the other 48 are mixed from earlier words. Tap any word to see its formula.</p>
<div id="sched" class="sched"></div>
<p class="small mono" id="schedf"></p>
<h4>3 · 64 rounds of compression</h4>
<div class="btns">
<button type="button" id="step">Step</button>
<button type="button" id="run">Run</button>
<button type="button" id="rreset" class="ghost">Reset</button>
<span id="rcount" class="small"></span>
</div>
<input id="tslider" type="range" min="0" max="64" value="0" aria-label="Round number" />
<div id="regs" class="regs"></div>
<p class="small" id="rinfo"></p>
<p class="small mono" id="rdetail"></p>
<div id="ff" class="ff hidden"></div>
<h4>4 · Avalanche effect</h4>
<p class="small">Flip a single bit of the input and compare the two digests. Each red square is an output bit that changed.</p>
<div class="btns">
<label for="flip" class="small">Input bit</label>
<input id="flip" type="number" min="0" value="0" />
<button type="button" id="flipRand">Flip a random bit</button>
<button type="button" id="flipAll" class="ghost">Try every bit</button>
</div>
<div id="avalDigests" class="small mono"></div>
<div id="avalGrid" class="agrid"></div>
<p class="status" id="avalStatus"></p>
:root { --fg:#1f2933; --muted:#52606d; --bg:#ffffff; --panel:#f1f4f8; --line:#cfd8e3; --accent:#1d3557; --accent-fg:#ffffff;
--msg:#1d4ed8; --one:#d00000; --zero:#9aa5b1; --len:#b45309; --fresh:#fde68a; --shift:#dbeafe; --ok:#0a7d33; --bad:#c92f3c; --on:#e63946; }
@media (prefers-color-scheme: dark) {
:root { --fg:#e4e7eb; --muted:#9aa5b1; --bg:#14181f; --panel:#1f2630; --line:#3a4656; --accent:#8fb3e8; --accent-fg:#0d1117;
--msg:#7fb2ff; --one:#ff6b6b; --zero:#5d6b7a; --len:#f0a35e; --fresh:#6b5512; --shift:#1e3a5f; --ok:#4cc38a; --bad:#ff7b84; --on:#ff6b6b; }
}
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: var(--fg); background: var(--bg); margin: 0; padding: 4px; font-size: 14px; }
h4 { margin: 1rem 0 .3rem; font-size: 1rem; }
.hint { margin: 0 0 .6rem; line-height: 1.45; color: var(--muted); }
.small { font-size: 12.5px; color: var(--muted); line-height: 1.45; }
.mono { font-family: ui-monospace, Menlo, Consolas, monospace; word-break: break-all; }
.lbl { font-weight: 600; display: block; margin-bottom: .2rem; }
textarea { width: 100%; font: 14px ui-monospace, Menlo, Consolas, monospace; padding: .4rem; border: 1px solid var(--line); border-radius: 6px; background: var(--panel); color: var(--fg); resize: vertical; }
.btns { display: flex; gap: .4rem; flex-wrap: wrap; align-items: center; margin: .4rem 0; }
button { font: 600 13px system-ui, sans-serif; padding: .35rem .7rem; border: 1px solid var(--accent); background: var(--accent); color: var(--accent-fg); border-radius: 7px; cursor: pointer; }
button.ghost { background: transparent; color: var(--accent); }
button:disabled { opacity: .35; cursor: default; }
.digest { display: flex; flex-wrap: wrap; gap: .4rem; align-items: center; background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: .45rem .6rem; margin: .4rem 0; }
.digest code { font: 700 12.5px ui-monospace, Menlo, Consolas, monospace; word-break: break-all; }
.dl { font-weight: 700; }
.verify { font-size: 12px; color: var(--muted); }
.verify.ok { color: var(--ok); } .verify.bad { color: var(--bad); }
.nav { display: flex; align-items: center; gap: .5rem; margin: .3rem 0; }
.legend { display: flex; gap: .7rem; flex-wrap: wrap; margin: .2rem 0 .3rem; }
.legend i, .bits i { font-style: normal; }
.m { color: var(--msg); font-weight: 700; } .o { color: var(--one); font-weight: 800; } .z { color: var(--zero); } .l { color: var(--len); font-weight: 700; }
.bits { font: 10px/1.35 ui-monospace, Menlo, Consolas, monospace; overflow-x: auto; }
.bitrow { display: flex; gap: 6px; white-space: nowrap; }
.wlab { width: 26px; color: var(--muted); text-align: right; }
.whex { color: var(--muted); }
.sched { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 3px; margin: .3rem 0; }
.wc { font: 10.5px ui-monospace, Menlo, Consolas, monospace; padding: 3px 2px; border-radius: 5px; background: var(--panel); color: var(--fg); border: 1px solid var(--line); white-space: nowrap; overflow: hidden; }
.wc.fromblk { border-color: var(--msg); }
.wc.sel { outline: 2px solid var(--accent); }
.wc.cur { background: var(--fresh); }
input[type=range] { width: 100%; margin: .3rem 0; }
input[type=number] { width: 6.5em; padding: .3rem; border: 1px solid var(--line); border-radius: 6px; background: var(--panel); color: var(--fg); }
.regs { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 4px; }
.reg { background: var(--panel); border: 1px solid var(--line); border-radius: 7px; padding: 4px; text-align: center; transition: background .15s; }
.reg.fresh { background: var(--fresh); }
.reg.shifted { background: var(--shift); }
.rn { display: block; font-weight: 800; font-size: 12px; }
.rv { font: 12px ui-monospace, Menlo, Consolas, monospace; }
.ff { background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: .5rem; margin-top: .4rem; font-size: 12px; overflow-x: auto; }
.ff table { border-collapse: collapse; font: 11.5px ui-monospace, Menlo, Consolas, monospace; }
.ff td { padding: 1px 4px; }
.hidden { display: none; }
.agrid { display: grid; grid-template-columns: repeat(32, 1fr); gap: 1px; max-width: 360px; margin: .4rem 0; }
.agrid i { aspect-ratio: 1; background: var(--panel); border: 1px solid var(--line); }
.agrid i.on { background: var(--on); border-color: var(--on); }
.status { font-weight: 600; margin: .3rem 0; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// A complete SHA-256 (FIPS 180-4) in plain JavaScript: padding, message schedule, 64 rounds, Davies–Meyer feed-forward.
// Round constants K0…K63: the first 32 bits of the fractional parts of the cube roots of the first 64 primes.
const K = [
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
];
// Initial hash value H(0): the first 32 bits of the fractional parts of the square roots of the first 8 primes.
const H0 = [0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19];
// The first 64 primes, only used to show where each constant comes from.
const PRIMES = [];
for (let n = 2; PRIMES.length < 64; n++) {
let isP = true;
for (const p of PRIMES) { if (p * p > n) break; if (n % p === 0) { isP = false; break; } }
if (isP) PRIMES.push(n);
}
const rotr = (x, n) => (x >>> n) | (x << (32 - n));
const bigS0 = (a) => (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) >>> 0;
const bigS1 = (e) => (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) >>> 0;
const smallS0 = (x) => (rotr(x, 7) ^ rotr(x, 18) ^ (x >>> 3)) >>> 0;
const smallS1 = (x) => (rotr(x, 17) ^ rotr(x, 19) ^ (x >>> 10)) >>> 0;
const chF = (e, f, g) => ((e & f) ^ (~e & g)) >>> 0;
const majF = (a, b, c) => ((a & b) ^ (a & c) ^ (b & c)) >>> 0;
const hex8 = (x) => (x >>> 0).toString(16).padStart(8, '0');
// Padding: append a 1 bit, then zeros, then the message length in bits as a 64-bit big-endian integer, so the total is a multiple of 512 bits.
function padMessage(bytes) {
const L = bytes.length;
const total = Math.ceil((L + 9) / 64) * 64;
const out = new Uint8Array(total);
out.set(bytes);
out[L] = 0x80;
const bitLen = L * 8;
const dv = new DataView(out.buffer);
dv.setUint32(total - 8, Math.floor(bitLen / 0x100000000));
dv.setUint32(total - 4, bitLen >>> 0);
return out;
}
// Hash the bytes and keep every intermediate value (schedule, the state after each round) so the page can display them.
function sha256Trace(bytes) {
const padded = padMessage(bytes);
const dv = new DataView(padded.buffer);
let H = H0.slice();
const blocks = [];
for (let off = 0; off < padded.length; off += 64) {
const W = new Array(64);
for (let t = 0; t < 16; t++) W[t] = dv.getUint32(off + 4 * t);
for (let t = 16; t < 64; t++) W[t] = (smallS1(W[t - 2]) + W[t - 7] + smallS0(W[t - 15]) + W[t - 16]) >>> 0;
const states = [H.slice()];
const rounds = [];
let [a, b, c, d, e, f, g, h] = H;
for (let t = 0; t < 64; t++) {
const s1 = bigS1(e), ch = chF(e, f, g);
const T1 = (h + s1 + ch + K[t] + W[t]) >>> 0;
const s0 = bigS0(a), mj = majF(a, b, c);
const T2 = (s0 + mj) >>> 0;
h = g; g = f; f = e; e = (d + T1) >>> 0;
d = c; c = b; b = a; a = (T1 + T2) >>> 0;
states.push([a, b, c, d, e, f, g, h]);
rounds.push({ s1, ch, T1, s0, mj, T2 });
}
const Hin = H;
// Feed-forward: add the input chaining value to the output of the 64 rounds (this makes the step one-way).
H = H.map((x, i) => (x + states[64][i]) >>> 0);
blocks.push({ W, states, rounds, Hin, Hout: H.slice() });
}
return { padded, blocks, digest: H.map(hex8).join('') };
}
// Everything below only draws the page; the hash itself is complete above.
const $ = (id) => document.getElementById(id);
const enc = new TextEncoder();
const PRESETS = ['abc', '', 'The quick brown fox jumps over the lazy dog', 'abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq'];
const NAMES = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];
let tr = null, bytes = new Uint8Array(0), blk = 0, t = 0, timer = null, selW = 16;
function recompute() {
bytes = enc.encode($('msg').value);
tr = sha256Trace(bytes);
blk = 0; t = 0; stop();
$('digest').textContent = tr.digest;
$('flip').max = Math.max(0, bytes.length * 8 - 1);
if (+$('flip').value > bytes.length * 8 - 1) $('flip').value = 0;
renderAll();
verify();
avalanche();
}
async function verify() {
const el = $('verify');
const want = tr.digest;
try {
if (!(window.crypto && crypto.subtle)) throw new Error('no subtle');
const buf = await crypto.subtle.digest('SHA-256', bytes);
const got = Array.from(new Uint8Array(buf), (x) => x.toString(16).padStart(2, '0')).join('');
if (want !== tr.digest) return;
el.textContent = got === want ? "✓ matches the browser’s built-in crypto.subtle" : "✗ differs from crypto.subtle";
el.className = 'verify ' + (got === want ? 'ok' : 'bad');
} catch (_) {
el.textContent = "(built-in check not available here)";
el.className = 'verify';
}
}
function renderPad() {
const L = bytes.length, total = tr.padded.length, nb = total / 64;
const zeros = total * 8 - L * 8 - 1 - 64;
$('padinfo').textContent = i18nFmt("%bytes% bytes = %bits% bits of message, then one 1 bit, %zeros% zero bits and a 64-bit length field → %blocks% block(s) of 512 bits.", { bytes: L, bits: L * 8, zeros, blocks: nb });
$('blockLbl').textContent = i18nFmt("Block %i% of %n%", { i: blk + 1, n: nb });
$('prevB').disabled = blk === 0;
$('nextB').disabled = blk === nb - 1;
const g = $('bits');
g.innerHTML = '';
const off = blk * 64;
for (let w = 0; w < 16; w++) {
const row = document.createElement('div');
row.className = 'bitrow';
const lab = document.createElement('span');
lab.className = 'wlab';
lab.textContent = 'W' + w;
row.appendChild(lab);
const bitsEl = document.createElement('span');
for (let k = 0; k < 4; k++) {
const i = off + w * 4 + k;
const byte = tr.padded[i];
for (let j = 7; j >= 0; j--) {
const s = document.createElement('i');
s.textContent = (byte >> j) & 1;
let cls = 'z';
if (i < L) cls = 'm';
else if (i >= total - 8) cls = 'l';
else if (i === L && j === 7) cls = 'o';
s.className = cls;
bitsEl.appendChild(s);
}
}
row.appendChild(bitsEl);
const hx = document.createElement('span');
hx.className = 'whex';
hx.textContent = hex8(tr.blocks[blk].W[w]);
row.appendChild(hx);
g.appendChild(row);
}
}
function renderSched() {
const W = tr.blocks[blk].W;
const g = $('sched');
g.innerHTML = '';
for (let i = 0; i < 64; i++) {
const d = document.createElement('button');
d.type = 'button';
d.className = 'wc' + (i < 16 ? ' fromblk' : '') + (i === selW ? ' sel' : '') + (t > 0 && i === t - 1 ? ' cur' : '');
d.textContent = 'W' + i + ' ' + hex8(W[i]);
d.addEventListener('click', () => { selW = i; renderSched(); });
g.appendChild(d);
}
$('schedf').textContent = selW < 16
? i18nFmt("W%t% is word %t% of the padded block, copied unchanged.", { t: selW })
: i18nFmt("W%t% = σ1(W%a%) + W%b% + σ0(W%c%) + W%d% = %v% (mod 2³²)", { t: selW, a: selW - 2, b: selW - 7, c: selW - 15, d: selW - 16, v: hex8(W[selW]) });
}
function renderRounds() {
const B = tr.blocks[blk];
const cur = B.states[t], prev = t > 0 ? B.states[t - 1] : null;
$('tslider').value = t;
$('rcount').textContent = i18nFmt("Round %t% / 64", { t });
const g = $('regs');
g.innerHTML = '';
NAMES.forEach((n, i) => {
const d = document.createElement('div');
let cls = 'reg';
if (prev && (i === 0 || i === 4)) cls += ' fresh';
else if (prev) cls += ' shifted';
d.className = cls;
const rn = document.createElement('span'); rn.className = 'rn'; rn.textContent = n;
const rv = document.createElement('span'); rv.className = 'rv'; rv.textContent = hex8(cur[i]);
d.appendChild(rn); d.appendChild(rv);
g.appendChild(d);
});
if (t === 0) {
$('rinfo').textContent = blk === 0 ? "Start: a…h hold the initial hash value H(0) — the first 32 bits of the fractional parts of the square roots of 2, 3, 5, 7, 11, 13, 17, 19." : "Start: a…h hold the chaining value produced by the previous block.";
$('rdetail').textContent = '';
} else {
const r = B.rounds[t - 1], p = PRIMES[t - 1];
$('rinfo').textContent = i18nFmt("Round %t% used W%i% = %w% and K%i% = %k% (first 32 bits of the fractional part of ∛%p% = %root%…). Yellow = freshly computed a and e; blue = values that just slid one place.", { t, i: t - 1, w: hex8(B.W[t - 1]), k: hex8(K[t - 1]), p, root: Math.cbrt(p).toFixed(6) });
$('rdetail').textContent = i18nFmt("Σ1(e)=%s1% Ch(e,f,g)=%ch% T1=h+Σ1+Ch+K+W=%t1% · Σ0(a)=%s0% Maj(a,b,c)=%maj% T2=%t2% → a=T1+T2, e=d+T1", { s1: hex8(r.s1), ch: hex8(r.ch), t1: hex8(r.T1), s0: hex8(r.s0), maj: hex8(r.mj), t2: hex8(r.T2) });
}
const ff = $('ff');
if (t === 64) {
ff.className = 'ff';
ff.innerHTML = '';
const head = document.createElement('div');
head.textContent = "Davies–Meyer feed-forward: add the block’s input chaining value to the final a…h, word by word (mod 2³²).";
ff.appendChild(head);
const tb = document.createElement('table');
NAMES.forEach((n, i) => {
const row = document.createElement('tr');
[ 'H' + i, hex8(B.Hin[i]), '+', n + ' = ' + hex8(B.states[64][i]), '=', hex8(B.Hout[i]) ].forEach((txt) => {
const td = document.createElement('td'); td.textContent = txt; row.appendChild(td);
});
tb.appendChild(row);
});
ff.appendChild(tb);
const foot = document.createElement('div');
foot.textContent = blk === tr.blocks.length - 1 ? "This was the last block, so H0‖H1‖…‖H7 is the SHA-256 digest shown above." : i18nFmt("These eight words become the chaining value for block %n%.", { n: blk + 2 });
ff.appendChild(foot);
} else {
ff.className = 'ff hidden';
}
}
function renderAll() { renderPad(); renderSched(); renderRounds(); }
function avalanche() {
const st = $('avalStatus'), grid = $('avalGrid'), dg = $('avalDigests');
const nbits = bytes.length * 8;
grid.innerHTML = ''; dg.innerHTML = '';
if (!nbits) { st.textContent = "Type at least one character to have a bit to flip."; return; }
let bit = Math.min(Math.max(0, Math.floor(+$('flip').value || 0)), nbits - 1);
$('flip').value = bit;
const b2 = bytes.slice();
b2[bit >> 3] ^= 0x80 >> (bit & 7);
const d1 = tr.digest, d2 = sha256Trace(b2).digest;
let diff = 0;
for (let i = 0; i < 64; i++) {
const x = parseInt(d1[i], 16) ^ parseInt(d2[i], 16);
for (let j = 3; j >= 0; j--) {
const on = (x >> j) & 1;
diff += on;
const c = document.createElement('i');
if (on) c.className = 'on';
grid.appendChild(c);
}
}
[["original:", d1], ["flipped:", d2]].forEach(([lab, d]) => {
const line = document.createElement('div');
line.textContent = lab + ' ' + d;
dg.appendChild(line);
});
st.textContent = i18nFmt("Flipped input bit %bit% (in byte %byte%): %n% of 256 output bits changed (%pct%). A good hash changes about 128 — half, at random.", { bit, byte: bit >> 3, n: diff, pct: (diff / 256 * 100).toFixed(1) + '%' });
}
function popcountDiff(d1, d2) {
let n = 0;
for (let i = 0; i < 64; i++) { let x = parseInt(d1[i], 16) ^ parseInt(d2[i], 16); while (x) { n += x & 1; x >>= 1; } }
return n;
}
function flipAll() {
const nbits = Math.min(bytes.length * 8, 1024);
if (!nbits) { $('avalStatus').textContent = "Type at least one character to have a bit to flip."; return; }
let sum = 0, mn = 256, mx = 0;
for (let bit = 0; bit < nbits; bit++) {
const b2 = bytes.slice();
b2[bit >> 3] ^= 0x80 >> (bit & 7);
const n = popcountDiff(tr.digest, sha256Trace(b2).digest);
sum += n; mn = Math.min(mn, n); mx = Math.max(mx, n);
}
$('avalStatus').textContent = i18nFmt("Flipped each of the first %n% input bits in turn: on average %avg% of 256 output bits changed (min %min%, max %max%). Ideal: 128.", { n: nbits, avg: (sum / nbits).toFixed(1), min: mn, max: mx });
}
function stop() {
if (timer) { clearInterval(timer); timer = null; }
$('run').textContent = "Run";
}
$('msg').addEventListener('input', recompute);
document.querySelectorAll('.preset').forEach((b) => b.addEventListener('click', () => { $('msg').value = PRESETS[+b.dataset.i]; recompute(); }));
$('prevB').addEventListener('click', () => { if (blk > 0) { blk--; t = 0; stop(); renderAll(); } });
$('nextB').addEventListener('click', () => { if (blk < tr.blocks.length - 1) { blk++; t = 0; stop(); renderAll(); } });
$('step').addEventListener('click', () => { stop(); if (t < 64) { t++; renderSched(); renderRounds(); } });
$('rreset').addEventListener('click', () => { stop(); t = 0; renderSched(); renderRounds(); });
$('tslider').addEventListener('input', () => { stop(); t = +$('tslider').value; renderSched(); renderRounds(); });
$('run').addEventListener('click', () => {
if (timer) { stop(); return; }
if (t >= 64) t = 0;
$('run').textContent = "Pause";
timer = setInterval(() => {
t++;
renderSched(); renderRounds();
if (t >= 64) stop();
}, 70);
});
$('flip').addEventListener('input', avalanche);
$('flipRand').addEventListener('click', () => {
const nbits = bytes.length * 8;
if (nbits) $('flip').value = Math.floor(Math.random() * nbits);
avalanche();
});
$('flipAll').addEventListener('click', flipAll);
recompute();
Comments
Loading comments...