Here is McEliece shrunk to seven bits. Pick a 4-bit message; we multiply it into a 7-bit codeword using a tiny error-correcting code, then flip exactly one bit on purpose. That noisy word is what gets sent.
<p class="hint">McEliece in miniature. Your <b>4-bit message</b> is multiplied into a 7-bit <b>codeword</b>, then we deliberately flip <b>one bit</b> (the secret error). With the trapdoor — knowing the code's parity structure — decoding is a single table lookup. Without it, an attacker must search.</p>
<div class="row">
<span class="lbl">Message (4 bits):</span>
<div id="msg" class="bits"></div>
<button id="rnd" type="button" class="ghost">Randomize</button>
</div>
<div class="pipe">
<div class="stage"><div class="cap">Codeword (G·m)</div><div id="code" class="bits big"></div></div>
<div class="arrow">+ error →</div>
<div class="stage"><div class="cap">Sent (with 1 flip)</div><div id="sent" class="bits big"></div></div>
</div>
<div class="btns">
<button id="enc" type="button">Encrypt & corrupt</button>
<button id="dec" type="button">Decode with trapdoor</button>
<button id="atk" type="button" class="ghost">Attack without trapdoor</button>
</div>
<div class="status" id="status">Pick a message, then press Encrypt & corrupt.</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .8rem; line-height: 1.45; }
.row { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; margin: .5rem 0; }
.lbl { font-size: .9rem; font-weight: 600; color: #1d3557; }
.bits { display: flex; gap: 4px; }
.bit { width: 30px; height: 34px; display: flex; align-items: center; justify-content: center;
font: 700 16px ui-monospace, monospace; border-radius: 7px; background: #e8eef3;
color: #1d3557; border: 1px solid #cdd9e3; user-select: none; }
.bits.big .bit { width: 34px; height: 38px; font-size: 17px; }
.bit.click { cursor: pointer; }
.bit.click:hover { background: #d6e1ea; }
.bit.one { background: #1d3557; color: #fff; border-color: #14243d; }
.bit.err { background: #e63946; color: #fff; border-color: #c92f3c; }
.bit.fixed { background: #0a7d33; color: #fff; border-color: #086627; }
.pipe { display: flex; align-items: center; gap: .8rem; flex-wrap: wrap; margin: .8rem 0; }
.stage { display: flex; flex-direction: column; gap: .25rem; }
.cap { font-size: .72rem; text-transform: uppercase; letter-spacing: .04em; color: #667; font-weight: 700; }
.arrow { font-size: .85rem; font-weight: 700; color: #c92f3c; }
.status { font-size: .95rem; font-weight: 600; margin: .6rem 0; min-height: 1.4em; line-height: 1.4; }
.status.ok { color: #0a7d33; } .status.bad { color: #c92f3c; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-top: .4rem; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// ── A toy McEliece, built on the Hamming(7,4) code ───────────────────────────
// Real McEliece hides a Goppa code that can correct t errors. We use the tiny
// Hamming(7,4) code (corrects 1 error) so the idea is visible:
// * Public side: encode message m (4 bits) into codeword c = G·m (7 bits), then add ONE bit of noise. That noisy word is what travels.
// * Trapdoor (secret): the parity-check matrix H. Compute the SYNDROME H·r; a nonzero syndrome equals the column of the flipped position — one lookup pinpoints and repairs the error. Decoding is instant.
// * Attacker: sees only the noisy word and a scrambled-looking code. With no structure to exploit, the honest move is to TRY every error pattern — "syndrome decoding" of a general linear code, proven NP-complete (Berlekamp, McEliece & van Tilborg, 1978).
// p1=d1^d2^d3, p2=d1^d2^d4, p3=d1^d3^d4 (systematic Hamming(7,4))
function encode(m){ const [a,b,c,d]=m; return [a,b,c,d, a^b^c, a^b^d, a^c^d]; }
const H = [[1,1,1,0,1,0,0],[1,1,0,1,0,1,0],[1,0,1,1,0,0,1]];
const syndrome = r => H.map(row => row.reduce((s,h,i)=>s^(h&r[i]),0));
let msg=[1,0,1,1], code=null, sent=null, errPos=-1, corrupted=false;
const $ = id => document.getElementById(id);
function setStatus(t,cls){ const s=$('status'); s.innerHTML=t; s.className='status '+(cls||''); }
function render(){
// message (clickable)
const mb=$('msg'); mb.innerHTML='';
msg.forEach((v,i)=>{ const d=document.createElement('div');
d.className='bit click'+(v?' one':''); d.textContent=v;
d.addEventListener('click',()=>{ msg[i]^=1; corrupted=false; reset(); render(); });
mb.appendChild(d); });
// codeword
const cb=$('code'); cb.innerHTML='';
(code||Array(7).fill('·')).forEach((v)=>{ const d=document.createElement('div');
d.className='bit'+(v===1?' one':''); d.textContent=v; cb.appendChild(d); });
// sent
const sb=$('sent'); sb.innerHTML='';
(sent||Array(7).fill('·')).forEach((v,i)=>{ const d=document.createElement('div');
let cls='bit'+(v===1?' one':''); if(i===errPos) cls='bit err';
d.className=cls; d.textContent=v; sb.appendChild(d); });
}
function reset(){ code=null; sent=null; errPos=-1; }
function doEncrypt(){
code = encode(msg);
errPos = Math.floor(Math.random()*7);
sent = code.slice(); sent[errPos]^=1;
corrupted=true; render();
setStatus(fmt('Codeword sent with a hidden flip at position %errPos%%v%. To anyone without the trapdoor it is just a noisy 7-bit string.', { errPos: errPos, v: 1 }),'');
}
function doDecode(){
if(!corrupted){ setStatus('Encrypt first.','bad'); return; }
const s = syndrome(sent);
let found=-1;
for(let i=0;i<7;i++){ const col=H.map(row=>row[i]);
if(col.every((b,k)=>b===s[k]) && s.some(x=>x)) { found=i; break; } }
const fixed = sent.slice(); if(found>=0) fixed[found]^=1;
const rec = fixed.slice(0,4);
// show repair
const sb=$('sent'); sb.innerHTML='';
fixed.forEach((v,i)=>{ const d=document.createElement('div');
d.className='bit'+(i===found?' fixed':(v===1?' one':'')); d.textContent=v; sb.appendChild(d); });
const okMsg = rec.join('')===msg.join('');
setStatus(fmt('Syndrome H·r = [%s%] → error at position %found%%v%. Repaired in <b>1 lookup</b>. Recovered message <b>%rec%</b> %v2%', { s: s.join(' '), found: found, v: 1, rec: rec.join(''), v2: okMsg?'— correct.':'— mismatch?!' }), okMsg?'ok':'bad');
console.log('syndrome', s, 'error pos', found+1, 'recovered', rec.join(''));
}
function doAttack(){
if(!corrupted){ setStatus('Encrypt first.','bad'); return; }
// Attacker with no trapdoor: try every error pattern until the word becomes a valid codeword (zero syndrome). Here, weight-≤1 patterns = 8 tries; real
// parameters make this 2^hundreds. We count the tries to show the blow-up.
let tries=0, hit=-1;
for(let pattern=0; pattern<(1<<7); pattern++){ tries++;
const cand = sent.map((b,i)=> b ^ ((pattern>>i)&1));
if(syndrome(cand).every(x=>x===0) &&
((pattern & (pattern-1))===0)) { hit=pattern; break; } // smallest-weight guess
}
setStatus('No trapdoor: the attacker brute-forces error patterns. Toy code → up to 2<sup>7</sup>=128 patterns. Real McEliece uses ~2<sup>800</sup>+ — <b>general decoding is NP-complete</b>, so this search never finishes.','bad');
console.log('attacker scanned', tries, 'patterns before a valid codeword');
}
$('rnd').addEventListener('click',()=>{ msg=msg.map(()=>Math.random()<.5?1:0); corrupted=false; reset(); render(); setStatus('New message. Press Encrypt & corrupt.',''); });
$('enc').addEventListener('click',doEncrypt);
$('dec').addEventListener('click',doDecode);
$('atk').addEventListener('click',doAttack);
render();
<p class="hint">McEliece in miniature. Your <b>4-bit message</b> is multiplied into a 7-bit <b>codeword</b>, then we deliberately flip <b>one bit</b> (the secret error). With the trapdoor — knowing the code's parity structure — decoding is a single table lookup. Without it, an attacker must search.</p>
<div class="row">
<span class="lbl">Message (4 bits):</span>
<div id="msg" class="bits"></div>
<button id="rnd" type="button" class="ghost">Randomize</button>
</div>
<div class="pipe">
<div class="stage"><div class="cap">Codeword (G·m)</div><div id="code" class="bits big"></div></div>
<div class="arrow">+ error →</div>
<div class="stage"><div class="cap">Sent (with 1 flip)</div><div id="sent" class="bits big"></div></div>
</div>
<div class="btns">
<button id="enc" type="button">Encrypt & corrupt</button>
<button id="dec" type="button">Decode with trapdoor</button>
<button id="atk" type="button" class="ghost">Attack without trapdoor</button>
</div>
<div class="status" id="status">Pick a message, then press Encrypt & corrupt.</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .8rem; line-height: 1.45; }
.row { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; margin: .5rem 0; }
.lbl { font-size: .9rem; font-weight: 600; color: #1d3557; }
.bits { display: flex; gap: 4px; }
.bit { width: 30px; height: 34px; display: flex; align-items: center; justify-content: center;
font: 700 16px ui-monospace, monospace; border-radius: 7px; background: #e8eef3;
color: #1d3557; border: 1px solid #cdd9e3; user-select: none; }
.bits.big .bit { width: 34px; height: 38px; font-size: 17px; }
.bit.click { cursor: pointer; }
.bit.click:hover { background: #d6e1ea; }
.bit.one { background: #1d3557; color: #fff; border-color: #14243d; }
.bit.err { background: #e63946; color: #fff; border-color: #c92f3c; }
.bit.fixed { background: #0a7d33; color: #fff; border-color: #086627; }
.pipe { display: flex; align-items: center; gap: .8rem; flex-wrap: wrap; margin: .8rem 0; }
.stage { display: flex; flex-direction: column; gap: .25rem; }
.cap { font-size: .72rem; text-transform: uppercase; letter-spacing: .04em; color: #667; font-weight: 700; }
.arrow { font-size: .85rem; font-weight: 700; color: #c92f3c; }
.status { font-size: .95rem; font-weight: 600; margin: .6rem 0; min-height: 1.4em; line-height: 1.4; }
.status.ok { color: #0a7d33; } .status.bad { color: #c92f3c; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-top: .4rem; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// ── A toy McEliece, built on the Hamming(7,4) code ───────────────────────────
// Real McEliece hides a Goppa code that can correct t errors. We use the tiny
// Hamming(7,4) code (corrects 1 error) so the idea is visible:
// * Public side: encode message m (4 bits) into codeword c = G·m (7 bits), then add ONE bit of noise. That noisy word is what travels.
// * Trapdoor (secret): the parity-check matrix H. Compute the SYNDROME H·r; a nonzero syndrome equals the column of the flipped position — one lookup pinpoints and repairs the error. Decoding is instant.
// * Attacker: sees only the noisy word and a scrambled-looking code. With no structure to exploit, the honest move is to TRY every error pattern — "syndrome decoding" of a general linear code, proven NP-complete (Berlekamp, McEliece & van Tilborg, 1978).
// p1=d1^d2^d3, p2=d1^d2^d4, p3=d1^d3^d4 (systematic Hamming(7,4))
function encode(m){ const [a,b,c,d]=m; return [a,b,c,d, a^b^c, a^b^d, a^c^d]; }
const H = [[1,1,1,0,1,0,0],[1,1,0,1,0,1,0],[1,0,1,1,0,0,1]];
const syndrome = r => H.map(row => row.reduce((s,h,i)=>s^(h&r[i]),0));
let msg=[1,0,1,1], code=null, sent=null, errPos=-1, corrupted=false;
const $ = id => document.getElementById(id);
function setStatus(t,cls){ const s=$('status'); s.innerHTML=t; s.className='status '+(cls||''); }
function render(){
// message (clickable)
const mb=$('msg'); mb.innerHTML='';
msg.forEach((v,i)=>{ const d=document.createElement('div');
d.className='bit click'+(v?' one':''); d.textContent=v;
d.addEventListener('click',()=>{ msg[i]^=1; corrupted=false; reset(); render(); });
mb.appendChild(d); });
// codeword
const cb=$('code'); cb.innerHTML='';
(code||Array(7).fill('·')).forEach((v)=>{ const d=document.createElement('div');
d.className='bit'+(v===1?' one':''); d.textContent=v; cb.appendChild(d); });
// sent
const sb=$('sent'); sb.innerHTML='';
(sent||Array(7).fill('·')).forEach((v,i)=>{ const d=document.createElement('div');
let cls='bit'+(v===1?' one':''); if(i===errPos) cls='bit err';
d.className=cls; d.textContent=v; sb.appendChild(d); });
}
function reset(){ code=null; sent=null; errPos=-1; }
function doEncrypt(){
code = encode(msg);
errPos = Math.floor(Math.random()*7);
sent = code.slice(); sent[errPos]^=1;
corrupted=true; render();
setStatus(fmt('Codeword sent with a hidden flip at position %errPos%%v%. To anyone without the trapdoor it is just a noisy 7-bit string.', { errPos: errPos, v: 1 }),'');
}
function doDecode(){
if(!corrupted){ setStatus('Encrypt first.','bad'); return; }
const s = syndrome(sent);
let found=-1;
for(let i=0;i<7;i++){ const col=H.map(row=>row[i]);
if(col.every((b,k)=>b===s[k]) && s.some(x=>x)) { found=i; break; } }
const fixed = sent.slice(); if(found>=0) fixed[found]^=1;
const rec = fixed.slice(0,4);
// show repair
const sb=$('sent'); sb.innerHTML='';
fixed.forEach((v,i)=>{ const d=document.createElement('div');
d.className='bit'+(i===found?' fixed':(v===1?' one':'')); d.textContent=v; sb.appendChild(d); });
const okMsg = rec.join('')===msg.join('');
setStatus(fmt('Syndrome H·r = [%s%] → error at position %found%%v%. Repaired in <b>1 lookup</b>. Recovered message <b>%rec%</b> %v2%', { s: s.join(' '), found: found, v: 1, rec: rec.join(''), v2: okMsg?'— correct.':'— mismatch?!' }), okMsg?'ok':'bad');
console.log('syndrome', s, 'error pos', found+1, 'recovered', rec.join(''));
}
function doAttack(){
if(!corrupted){ setStatus('Encrypt first.','bad'); return; }
// Attacker with no trapdoor: try every error pattern until the word becomes a valid codeword (zero syndrome). Here, weight-≤1 patterns = 8 tries; real
// parameters make this 2^hundreds. We count the tries to show the blow-up.
let tries=0, hit=-1;
for(let pattern=0; pattern<(1<<7); pattern++){ tries++;
const cand = sent.map((b,i)=> b ^ ((pattern>>i)&1));
if(syndrome(cand).every(x=>x===0) &&
((pattern & (pattern-1))===0)) { hit=pattern; break; } // smallest-weight guess
}
setStatus('No trapdoor: the attacker brute-forces error patterns. Toy code → up to 2<sup>7</sup>=128 patterns. Real McEliece uses ~2<sup>800</sup>+ — <b>general decoding is NP-complete</b>, so this search never finishes.','bad');
console.log('attacker scanned', tries, 'patterns before a valid codeword');
}
$('rnd').addEventListener('click',()=>{ msg=msg.map(()=>Math.random()<.5?1:0); corrupted=false; reset(); render(); setStatus('New message. Press Encrypt & corrupt.',''); });
$('enc').addEventListener('click',doEncrypt);
$('dec').addEventListener('click',doDecode);
$('atk').addEventListener('click',doAttack);
render();
Comments
Loading comments...