Introduction

A cryptographic hash function takes a message of any length and returns a short, fixed-size fingerprint — for SHA-256, exactly 256 bits, written as 64 hex digits. Hash the empty string and you get e3b0c442…b855; hash a whole film and you still get 64 hex digits. Change a single comma and the fingerprint looks completely unrelated.

SHA-256 belongs to the SHA-2 family, designed by the U.S. National Security Agency (NSA) and published by NIST: the new functions were released as a draft in 2001 and standardized in FIPS 180-2 (2002). Today they live in FIPS 180-4 (2015), alongside SHA-224, SHA-384 and SHA-512. SHA-2 was meant to replace SHA-1, whose 160-bit output was already looking thin; a full SHA-1 collision was finally demonstrated in 2017.

The architecture

SHA-256 is two classic ideas stacked together:

  1. Merkle–Damgård: pad the message, cut it into 512-bit blocks M1,…,MNM_1, \dots, M_N, and fold them one by one into a 256-bit chaining value, starting from a fixed initial value H(0)H^{(0)}.
  2. Davies–Meyer: the folding step is built from a block cipher EE — the message block plays the role of the key — plus a feed-forward: H(i)=EMi(H(i−1))⊞H(i−1)H^{(i)} = E_{M_i}\big(H^{(i-1)}\big) \boxplus H^{(i-1)}, where ⊞\boxplus is word-wise addition modulo 2322^{32}. Without the final addition you could simply decrypt; with it, the step becomes one-way.

Step 1 — padding

Append a single 1 bit, then kk zero bits, then the message length ℓ\ell as a 64-bit big-endian integer, with kk the smallest non-negative number such that ℓ+1+k≡448(mod512)\ell + 1 + k \equiv 448 \pmod{512}. The total is a multiple of 512 bits. For "abc" (ℓ=24\ell = 24): 24 message bits, one 1, 423 zeros, 64 length bits — exactly one block. A 56-byte message no longer fits the 64-bit length field in the same block, so it spills into a second one.

Step 2 — the message schedule

Each block is read as sixteen 32-bit words W0,…,W15W_0, \dots, W_{15}, and expanded to 64 words:

Wt=σ1(Wt−2)+Wt−7+σ0(Wt−15)+Wt−16,16≤t≤63W_t = \sigma_1(W_{t-2}) + W_{t-7} + \sigma_0(W_{t-15}) + W_{t-16}, \qquad 16 \le t \le 63

with σ0(x)=(x⋙7)⊕(x⋙18)⊕(x≫3)\sigma_0(x) = (x \ggg 7) \oplus (x \ggg 18) \oplus (x \gg 3) and σ1(x)=(x⋙17)⊕(x⋙19)⊕(x≫10)\sigma_1(x) = (x \ggg 17) \oplus (x \ggg 19) \oplus (x \gg 10) (⋙\ggg = rotate right, ≫\gg = shift right).

Step 3 — 64 rounds

Eight working variables a,b,…,ha, b, \dots, h start as the current chaining value. Each round tt computes

  • T1=h+Σ1(e)+Ch(e,f,g)+Kt+WtT_1 = h + \Sigma_1(e) + \mathrm{Ch}(e,f,g) + K_t + W_t
  • T2=Σ0(a)+Maj(a,b,c)T_2 = \Sigma_0(a) + \mathrm{Maj}(a,b,c)

then shifts everything one place (h←gh \leftarrow g, g←fg \leftarrow f, …) and sets e←d+T1e \leftarrow d + T_1 and a←T1+T2a \leftarrow T_1 + T_2. The helpers are

  • Ch(e,f,g)=(e∧f)⊕(¬e∧g)\mathrm{Ch}(e,f,g) = (e \wedge f) \oplus (\lnot e \wedge g) — "ee chooses": each bit of ee picks the bit from ff or from gg;
  • Maj(a,b,c)=(a∧b)⊕(a∧c)⊕(b∧c)\mathrm{Maj}(a,b,c) = (a \wedge b) \oplus (a \wedge c) \oplus (b \wedge c) — the bitwise majority vote;
  • Σ0(a)=(a⋙2)⊕(a⋙13)⊕(a⋙22)\Sigma_0(a) = (a \ggg 2) \oplus (a \ggg 13) \oplus (a \ggg 22) and Σ1(e)=(e⋙6)⊕(e⋙11)⊕(e⋙25)\Sigma_1(e) = (e \ggg 6) \oplus (e \ggg 11) \oplus (e \ggg 25).

The 64 constants KtK_t are the first 32 bits of the fractional parts of the cube roots of the first 64 primes, and the eight words of H(0)H^{(0)} are the first 32 bits of the fractional parts of the square roots of the first 8 primes. These are "nothing-up-my-sleeve" numbers: anyone can recompute them, so nobody can have hidden a trapdoor in them. For example, 2=1.41421356…\sqrt{2} = 1.41421356\ldots and 0.41421356×232≈0.41421356 \times 2^{32} \approx 0x6a09e667 — the first word of H(0)H^{(0)}.

SHA256(M):
  H ← H0                                  // √ of the first 8 primes
  for each 512-bit block of pad(M):
      W[0..15] ← the block as 16 big-endian words
      for t in 16..63: W[t] ← σ1(W[t-2]) + W[t-7] + σ0(W[t-15]) + W[t-16]
      (a,b,c,d,e,f,g,h) ← H
      for t in 0..63:
          T1 ← h + Σ1(e) + Ch(e,f,g) + K[t] + W[t]
          T2 ← Σ0(a) + Maj(a,b,c)
          (h,g,f,e,d,c,b,a) ← (g,f,e,d+T1,c,b,a,T1+T2)
      H ← H + (a,b,c,d,e,f,g,h)            // Davies–Meyer feed-forward
  return H[0] ‖ H[1] ‖ … ‖ H[7]           // all additions mod 2^32

That is the entire algorithm. No tables, no secret keys, no floating point — just 32-bit additions, rotations and Boolean functions, which is why it is fast in silicon and easy to get exactly right.

Try It

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:

  1. Padding shows every bit of the 512-bit block: your message in blue, the appended 1 in red, the zeros, and the 64-bit length at the end.
  2. Message schedule lists W0,…,W63W_0, \dots, W_{63}. Tap any expanded word to see which four earlier words it was mixed from.
  3. Rounds: press Step or Run and watch the eight registers. Only aa and ee get genuinely new values each round (yellow); the other six just slide one place along (blue). After round 64 the feed-forward adds the starting value back in.
  4. Avalanche: flip one input bit and count how many of the 256 output bits change.
<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();
Result
Console

Try "abc" and compare with the official test vector ba7816bf…f20015ad. Then try the 56-character preset: it is short, but its padding needs a second block. Finally press Try every bit in the avalanche panel — the average lands remarkably close to 128, exactly what a random function would give.

The Real Complexity

Computing SHA-256 is cheap. A message of ℓ\ell bits becomes N=⌊(ℓ+64)/512⌋+1N = \lfloor (\ell + 64)/512 \rfloor + 1 blocks, each costing 48 schedule expansions and 64 rounds — a fixed amount of work. So hashing takes Θ(ℓ)\Theta(\ell) time and O(1)O(1) extra memory (eight working words, the 64-word schedule and the chaining value), and it streams: you never need the whole message in memory. The maximum message length is 264−12^{64} - 1 bits.

Inverting it is meant to be hopeless. For an ideal 256-bit hash the generic costs are:

Goal Generic cost Why
Collision: any M≠M′M \ne M' with H(M)=H(M′)H(M) = H(M') ≈2128\approx 2^{128} birthday bound
Preimage: given yy, find MM with H(M)=yH(M) = y ≈2256\approx 2^{256} exhaustive search
Second preimage: given MM, find M′≠MM' \ne M ≈2256\approx 2^{256} exhaustive search

Two footnotes keep this honest. For very long messages of 2k2^k blocks, a generic second-preimage attack on any Merkle–Damgård hash costs about 2256−k2^{256-k} (Kelsey and Schneier, 2005) — still astronomically out of reach. And a quantum computer running Grover's algorithm would cut preimage search to roughly 21282^{128} steps, which is why SHA-256 is still considered a comfortable post-quantum hash.

The best real attacks

Cryptanalysts measure progress by how many of the 64 steps they can break:

  • Collisions. Mendel, Nad and Schläffer (EUROCRYPT 2013) gave a 31-step collision attack, practical only up to 28 steps. In 2024 Li, Liu, Wang, Dong and Sun published the first practical collision for 31-step SHA-256 (ASIACRYPT 2024) — found in about 1.2 hours on 64 threads. In 2026 Zhang, Li, Gao and Wang announced the first collision attack on 37 steps (EUROCRYPT 2026), a theoretical attack and the first extension of the step count in twelve years.
  • Semi-free-start collisions, where the attacker also picks the starting chaining value, are easier: Li, Liu and Wang (EUROCRYPT 2024) found a practical one for 39 steps.
  • Preimages have been pushed to around 45 steps (biclique attacks, 2012), at a cost barely below brute force.

Full 64-step SHA-256 has no known attack better than the generic bounds above.

The length-extension trap

Because the digest is the final chaining value, anyone who knows H(M)H(M) and the length of MM can keep hashing: they can compute H(M ∥ pad(M) ∥ X)H(M \,\|\, \mathrm{pad}(M) \,\|\, X) for any XX without knowing MM. So SHA256(secret ‖ message) is not a safe message authentication code. The fix is HMAC, which nests two hash calls, or a truncated variant like SHA-512/256, or SHA-3, whose sponge construction hides part of its state.

Where It Matters

SHA-256 is probably the most-executed cryptographic function on Earth:

  • Bitcoin hashes block headers and transactions with double SHA-256, SHA256(SHA256(x))\mathrm{SHA256}(\mathrm{SHA256}(x)). Proof of work means searching for a header whose double hash falls below a target — the whole network now performs around 102110^{21} such hashes per second. Transactions inside a block are committed through a Merkle tree of SHA-256 hashes.
  • TLS and certificates: virtually every HTTPS certificate is signed over a SHA-256 hash (e.g. sha256WithRSAEncryption or ECDSA with SHA-256) since browsers rejected SHA-1 certificates around 2016–2017.
  • Git identifies every object by a SHA-1 hash, and has been moving to SHA-256: repositories in the SHA-256 object format have been available as an option since Git 2.29 (2020), and making SHA-256 the default for new repositories is one of the headline changes planned for Git 3.0.
  • Software distribution: package managers, container images and download pages publish SHA-256 checksums so you can verify that what you received is exactly what was released.
  • Building blocks: HMAC-SHA-256 authenticates API requests and JSON Web Tokens; HKDF derives keys inside TLS 1.3; hash-based signatures and many password-hashing schemes are built on top of it.

The backup plan

After the attacks on MD5 and SHA-1, NIST ran a public competition for a structurally different hash. The winner, Keccak, became SHA-3 in FIPS 202 (2015). It is not a replacement — SHA-256 is unbroken — but an insurance policy: if a breakthrough ever hit the SHA-2 design, a standardized hash built on entirely different principles is ready to go.

Conclusion

Strip away the mystique and SHA-256 is short enough to fit on one page: pad, expand sixteen words to sixty-four, run sixty-four rounds of additions, rotations, Ch and Maj, add the starting value back in, repeat for the next block. Every operation is easy to compute and easy to verify — and that is exactly the point. The security does not come from any single clever step, but from stacking simple non-linear steps until each output bit depends on every input bit in a way nobody can untangle. The avalanche you saw in the demo, about 128 of 256 bits changing per flipped input bit, is the visible symptom of that mixing.

More than twenty years after publication, the best attacks still stop around halfway through the 64 steps. That margin is why SHA-256 secures money, software and the web — and why, if you need to authenticate with it, you should still reach for HMAC rather than gluing a secret onto the front.

Share this article

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

Comments

Loading comments...

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