<div class="panel">
<div class="section">
<h3>Parameters</h3>
<div class="params">
<span>N = <b>11</b> (ring degree)</span>
<span>p = <b>3</b> (message mod)</span>
<span>q = <b>32</b> (ciphertext mod)</span>
</div>
</div>
<div class="section">
<h3>Message bits (click to flip)</h3>
<div id="msg-bits" class="bits"></div>
<div class="hint">Each bit becomes a polynomial coefficient. Encrypt wraps them in the quotient ring.</div>
</div>
<div class="btns">
<button id="btn-enc" type="button">Encrypt</button>
<button id="btn-dec" type="button">Decrypt</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div id="steps"></div>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; font-size: 14px; color: #222; margin: 0; padding: 4px; }
h3 { margin: 0 0 .4rem; font-size: .9rem; color: #1d3557; font-weight: 700; }
.panel { display: flex; flex-direction: column; gap: .6rem; }
.section { background: #f0f4f8; border-radius: 8px; padding: .6rem .8rem; }
.params { display: flex; gap: 1rem; flex-wrap: wrap; font-size: .82rem; }
.bits { display: flex; gap: 5px; flex-wrap: wrap; margin-bottom: .3rem; }
.bit { width: 28px; height: 28px; display: flex; align-items: center; justify-content: center;
font: 700 13px ui-monospace,monospace; border-radius: 5px; cursor: pointer;
background: #c9ccd1; border: 1px solid #adb1b8; transition: background .1s; }
.bit.on { background: #1d3557; color: #fff; border-color: #1d3557; }
.hint { font-size: .76rem; color: #555; margin-top: .2rem; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
#steps { display: flex; flex-direction: column; gap: .45rem; }
.step { background: #fff; border: 1px solid #d0d9e3; border-radius: 8px; padding: .5rem .7rem; }
.step-title { font-weight: 700; color: #1d3557; margin-bottom: .25rem; font-size: .83rem; }
.poly { font: 12px ui-monospace,monospace; color: #333; word-break: break-all; line-height: 1.5; }
.ok { color: #0a7d33; font-weight: 700; }
.fail { color: #c92f3c; font-weight: 700; }
.label { font-size: .75rem; color: #667; }
// NTRU simplified demo: N=11, p=3, q=32=2^5
// Polynomial arithmetic in Z[x]/(x^N - 1)
const N = 11, p = 3, q = 32;
function polyMod(a, mod) { return a.map(x => ((x % mod) + mod) % mod); }
function polyAdd(a, b, mod) {
const r = new Array(N).fill(0);
for (let i = 0; i < N; i++) r[i] = (a[i] + b[i]) % mod;
return r;
}
// Cyclic convolution: multiply mod (x^N - 1, mod)
function polyMul(a, b, mod) {
const c = new Array(N).fill(0);
for (let i = 0; i < N; i++)
for (let j = 0; j < N; j++)
c[(i + j) % N] = (c[(i + j) % N] + a[i] * b[j]) % mod;
return c;
}
// Poly inverse mod prime via Gaussian elimination
function polyInvPrime(a, mod) {
const M = [];
for (let row = 0; row < N; row++) {
const r = new Array(N + 1).fill(0);
for (let j = 0; j < N; j++) r[j] = ((a[(row - j + N) % N]) % mod + mod) % mod;
r[N] = row === 0 ? 1 : 0;
M.push(r);
}
const mInv = (x) => { let [o,r,os,s]=[x%mod,mod,1,0]; while(r){const q2=Math.floor(o/r);[o,r]=[r,o-q2*r];[os,s]=[s,os-q2*s];} return o===1?((os%mod)+mod)%mod:null; };
for (let col = 0; col < N; col++) {
let piv = -1;
for (let row = col; row < N; row++) { if (M[row][col] !== 0) { piv = row; break; } }
if (piv === -1) return null;
[M[col], M[piv]] = [M[piv], M[col]];
const inv = mInv(M[col][col]); if (!inv) return null;
for (let j = col; j <= N; j++) M[col][j] = M[col][j] * inv % mod;
for (let row = 0; row < N; row++) {
if (row === col) continue;
const f = M[row][col];
for (let j = col; j <= N; j++) M[row][j] = ((M[row][j] - f * M[col][j]) % mod + mod) % mod;
}
}
return M.map(r => r[N]);
}
// Poly inverse mod 2^bits via Hensel lifting (Newton iteration)
function polyInvPow2(a, bits) {
const a2 = polyMod(a, 2);
let inv = polyInvPrime(a2, 2);
if (!inv) return null;
let mod = 2;
while (mod < (1 << bits)) {
mod *= 2;
const amod = polyMod(a, mod);
const prod = polyMul(amod, polyMod(inv, mod), mod);
// Newton step: inv_new = inv * (2 - a*inv) mod mod
const twoMinus = prod.map((v, i) => i === 0 ? ((2 - v) % mod + mod) % mod : ((-v) % mod + mod) % mod);
inv = polyMul(polyMod(inv, mod), twoMinus, mod);
}
return inv;
}
// Center-lift from [0, mod) to (-mod/2, mod/2]
function centerLift(a, mod) { return a.map(x => { let v = ((x % mod) + mod) % mod; return v > mod / 2 ? v - mod : v; }); }
function polyStr(a) { return '[' + a.map(x => String(x).padStart(4)).join('') + ' ]'; }
// Fixed NTRU keys (pre-computed, verified by test suite)
// Private f: ternary polynomial invertible mod 3 and mod 32
const F = [1, 1,-1, 0, 0,-1, 0, 0, 1, 0, 0];
// f^{-1} mod 3
const FP = [2, 0, 0, 1, 0, 1, 2, 2, 2, 0, 0];
// f^{-1} mod 32 (via Hensel lifting)
const FQ = [25, 7,26,14,17,23, 0,22,31, 1,27];
// Small g used to build public key h = p·fq·g mod q
const G = [0, 1,-1, 0, 1, 0, 0,-1, 0, 1, 0];
// Blinding poly r (fixed for demo reproducibility)
const R = [0, 0, 1,-1, 0, 0, 1, 0, 0,-1, 0];
// Build public key h = p * FQ * G mod q
const pfq = polyMul([p,0,0,0,0,0,0,0,0,0,0], polyMod(FQ, q), q);
const H = polyMul(pfq, polyMod(G, q), q);
// State
let msgBits = [1,0,1,0,1,0,0,0,0,0,0];
let lastE = null;
function encrypt() {
const m = msgBits.slice();
const rh = polyMul(polyMod(R, q), H, q);
const e = polyAdd(rh, polyMod(m, q), q);
lastE = e;
showSteps([
{ title: 'Public key h = p·f⁻¹·g mod q', body: polyStr(H) },
{ title: 'Message polynomial m', body: polyStr(m) },
{ title: 'Blinding poly r', body: polyStr(R) },
{ title: 'Ciphertext e = r·h + m mod q', body: polyStr(e) },
{ title: '', body: '→ Decrypt to verify recovery.' }
]);
}
function decrypt() {
if (!lastE) { showSteps([{ title: 'Encrypt first!', body: '' }]); return; }
const fe = polyMul(polyMod(F, q), polyMod(lastE, q), q);
const a = centerLift(fe, q);
const am3 = polyMod(a, p);
const rec = polyMul(FP, am3, p);
const mOrig = msgBits.map(x => ((x % p) + p) % p);
const ok = rec.every((v, i) => v === mOrig[i]);
showSteps([
{ title: 'Step 1 — compute a = f·e mod q (center-lifted)', body: polyStr(a) },
{ title: 'Step 2 — reduce mod p', body: polyStr(am3) },
{ title: 'Step 3 — apply f⁻¹ mod p → recovered m', body: polyStr(rec) },
{ title: 'Original m', body: polyStr(mOrig) },
{ title: ok ? '✓ Decryption successful!' : '✗ Mismatch', body: '' }
], ok ? 'ok' : 'fail');
}
function showSteps(items, cls) {
const el = document.getElementById('steps');
el.innerHTML = '';
if (!items) return;
items.forEach(({ title, body }) => {
const div = document.createElement('div'); div.className = 'step';
if (title) {
const t = document.createElement('div');
t.className = 'step-title' + (title.startsWith('✓') ? ' ok' : title.startsWith('✗') ? ' fail' : '');
t.textContent = title; div.appendChild(t);
}
if (body) { const p2 = document.createElement('div'); p2.className = 'poly'; p2.textContent = body; div.appendChild(p2); }
el.appendChild(div);
});
}
function renderBits() {
const el = document.getElementById('msg-bits'); el.innerHTML = '';
msgBits.forEach((b, i) => {
const d = document.createElement('div');
d.className = 'bit' + (b ? ' on' : '');
d.textContent = b;
d.addEventListener('click', () => { msgBits[i] ^= 1; lastE = null; renderBits(); showSteps(); });
el.appendChild(d);
});
}
document.getElementById('btn-enc').addEventListener('click', encrypt);
document.getElementById('btn-dec').addEventListener('click', decrypt);
document.getElementById('btn-reset').addEventListener('click', () => {
msgBits = [1,0,1,0,1,0,0,0,0,0,0]; lastE = null; renderBits(); showSteps();
});
renderBits();
// Auto-show public key on load
showSteps([{ title: 'Public key h = p·f⁻¹·g mod q', body: polyStr(H) },
{ title: 'Click bits to set your message, then Encrypt.', body: '' }]);
<div class="panel">
<div class="section">
<h3>Parameters</h3>
<div class="params">
<span>N = <b>11</b> (ring degree)</span>
<span>p = <b>3</b> (message mod)</span>
<span>q = <b>32</b> (ciphertext mod)</span>
</div>
</div>
<div class="section">
<h3>Message bits (click to flip)</h3>
<div id="msg-bits" class="bits"></div>
<div class="hint">Each bit becomes a polynomial coefficient. Encrypt wraps them in the quotient ring.</div>
</div>
<div class="btns">
<button id="btn-enc" type="button">Encrypt</button>
<button id="btn-dec" type="button">Decrypt</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div id="steps"></div>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; font-size: 14px; color: #222; margin: 0; padding: 4px; }
h3 { margin: 0 0 .4rem; font-size: .9rem; color: #1d3557; font-weight: 700; }
.panel { display: flex; flex-direction: column; gap: .6rem; }
.section { background: #f0f4f8; border-radius: 8px; padding: .6rem .8rem; }
.params { display: flex; gap: 1rem; flex-wrap: wrap; font-size: .82rem; }
.bits { display: flex; gap: 5px; flex-wrap: wrap; margin-bottom: .3rem; }
.bit { width: 28px; height: 28px; display: flex; align-items: center; justify-content: center;
font: 700 13px ui-monospace,monospace; border-radius: 5px; cursor: pointer;
background: #c9ccd1; border: 1px solid #adb1b8; transition: background .1s; }
.bit.on { background: #1d3557; color: #fff; border-color: #1d3557; }
.hint { font-size: .76rem; color: #555; margin-top: .2rem; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
#steps { display: flex; flex-direction: column; gap: .45rem; }
.step { background: #fff; border: 1px solid #d0d9e3; border-radius: 8px; padding: .5rem .7rem; }
.step-title { font-weight: 700; color: #1d3557; margin-bottom: .25rem; font-size: .83rem; }
.poly { font: 12px ui-monospace,monospace; color: #333; word-break: break-all; line-height: 1.5; }
.ok { color: #0a7d33; font-weight: 700; }
.fail { color: #c92f3c; font-weight: 700; }
.label { font-size: .75rem; color: #667; }
// NTRU simplified demo: N=11, p=3, q=32=2^5
// Polynomial arithmetic in Z[x]/(x^N - 1)
const N = 11, p = 3, q = 32;
function polyMod(a, mod) { return a.map(x => ((x % mod) + mod) % mod); }
function polyAdd(a, b, mod) {
const r = new Array(N).fill(0);
for (let i = 0; i < N; i++) r[i] = (a[i] + b[i]) % mod;
return r;
}
// Cyclic convolution: multiply mod (x^N - 1, mod)
function polyMul(a, b, mod) {
const c = new Array(N).fill(0);
for (let i = 0; i < N; i++)
for (let j = 0; j < N; j++)
c[(i + j) % N] = (c[(i + j) % N] + a[i] * b[j]) % mod;
return c;
}
// Poly inverse mod prime via Gaussian elimination
function polyInvPrime(a, mod) {
const M = [];
for (let row = 0; row < N; row++) {
const r = new Array(N + 1).fill(0);
for (let j = 0; j < N; j++) r[j] = ((a[(row - j + N) % N]) % mod + mod) % mod;
r[N] = row === 0 ? 1 : 0;
M.push(r);
}
const mInv = (x) => { let [o,r,os,s]=[x%mod,mod,1,0]; while(r){const q2=Math.floor(o/r);[o,r]=[r,o-q2*r];[os,s]=[s,os-q2*s];} return o===1?((os%mod)+mod)%mod:null; };
for (let col = 0; col < N; col++) {
let piv = -1;
for (let row = col; row < N; row++) { if (M[row][col] !== 0) { piv = row; break; } }
if (piv === -1) return null;
[M[col], M[piv]] = [M[piv], M[col]];
const inv = mInv(M[col][col]); if (!inv) return null;
for (let j = col; j <= N; j++) M[col][j] = M[col][j] * inv % mod;
for (let row = 0; row < N; row++) {
if (row === col) continue;
const f = M[row][col];
for (let j = col; j <= N; j++) M[row][j] = ((M[row][j] - f * M[col][j]) % mod + mod) % mod;
}
}
return M.map(r => r[N]);
}
// Poly inverse mod 2^bits via Hensel lifting (Newton iteration)
function polyInvPow2(a, bits) {
const a2 = polyMod(a, 2);
let inv = polyInvPrime(a2, 2);
if (!inv) return null;
let mod = 2;
while (mod < (1 << bits)) {
mod *= 2;
const amod = polyMod(a, mod);
const prod = polyMul(amod, polyMod(inv, mod), mod);
// Newton step: inv_new = inv * (2 - a*inv) mod mod
const twoMinus = prod.map((v, i) => i === 0 ? ((2 - v) % mod + mod) % mod : ((-v) % mod + mod) % mod);
inv = polyMul(polyMod(inv, mod), twoMinus, mod);
}
return inv;
}
// Center-lift from [0, mod) to (-mod/2, mod/2]
function centerLift(a, mod) { return a.map(x => { let v = ((x % mod) + mod) % mod; return v > mod / 2 ? v - mod : v; }); }
function polyStr(a) { return '[' + a.map(x => String(x).padStart(4)).join('') + ' ]'; }
// Fixed NTRU keys (pre-computed, verified by test suite)
// Private f: ternary polynomial invertible mod 3 and mod 32
const F = [1, 1,-1, 0, 0,-1, 0, 0, 1, 0, 0];
// f^{-1} mod 3
const FP = [2, 0, 0, 1, 0, 1, 2, 2, 2, 0, 0];
// f^{-1} mod 32 (via Hensel lifting)
const FQ = [25, 7,26,14,17,23, 0,22,31, 1,27];
// Small g used to build public key h = p·fq·g mod q
const G = [0, 1,-1, 0, 1, 0, 0,-1, 0, 1, 0];
// Blinding poly r (fixed for demo reproducibility)
const R = [0, 0, 1,-1, 0, 0, 1, 0, 0,-1, 0];
// Build public key h = p * FQ * G mod q
const pfq = polyMul([p,0,0,0,0,0,0,0,0,0,0], polyMod(FQ, q), q);
const H = polyMul(pfq, polyMod(G, q), q);
// State
let msgBits = [1,0,1,0,1,0,0,0,0,0,0];
let lastE = null;
function encrypt() {
const m = msgBits.slice();
const rh = polyMul(polyMod(R, q), H, q);
const e = polyAdd(rh, polyMod(m, q), q);
lastE = e;
showSteps([
{ title: 'Public key h = p·f⁻¹·g mod q', body: polyStr(H) },
{ title: 'Message polynomial m', body: polyStr(m) },
{ title: 'Blinding poly r', body: polyStr(R) },
{ title: 'Ciphertext e = r·h + m mod q', body: polyStr(e) },
{ title: '', body: '→ Decrypt to verify recovery.' }
]);
}
function decrypt() {
if (!lastE) { showSteps([{ title: 'Encrypt first!', body: '' }]); return; }
const fe = polyMul(polyMod(F, q), polyMod(lastE, q), q);
const a = centerLift(fe, q);
const am3 = polyMod(a, p);
const rec = polyMul(FP, am3, p);
const mOrig = msgBits.map(x => ((x % p) + p) % p);
const ok = rec.every((v, i) => v === mOrig[i]);
showSteps([
{ title: 'Step 1 — compute a = f·e mod q (center-lifted)', body: polyStr(a) },
{ title: 'Step 2 — reduce mod p', body: polyStr(am3) },
{ title: 'Step 3 — apply f⁻¹ mod p → recovered m', body: polyStr(rec) },
{ title: 'Original m', body: polyStr(mOrig) },
{ title: ok ? '✓ Decryption successful!' : '✗ Mismatch', body: '' }
], ok ? 'ok' : 'fail');
}
function showSteps(items, cls) {
const el = document.getElementById('steps');
el.innerHTML = '';
if (!items) return;
items.forEach(({ title, body }) => {
const div = document.createElement('div'); div.className = 'step';
if (title) {
const t = document.createElement('div');
t.className = 'step-title' + (title.startsWith('✓') ? ' ok' : title.startsWith('✗') ? ' fail' : '');
t.textContent = title; div.appendChild(t);
}
if (body) { const p2 = document.createElement('div'); p2.className = 'poly'; p2.textContent = body; div.appendChild(p2); }
el.appendChild(div);
});
}
function renderBits() {
const el = document.getElementById('msg-bits'); el.innerHTML = '';
msgBits.forEach((b, i) => {
const d = document.createElement('div');
d.className = 'bit' + (b ? ' on' : '');
d.textContent = b;
d.addEventListener('click', () => { msgBits[i] ^= 1; lastE = null; renderBits(); showSteps(); });
el.appendChild(d);
});
}
document.getElementById('btn-enc').addEventListener('click', encrypt);
document.getElementById('btn-dec').addEventListener('click', decrypt);
document.getElementById('btn-reset').addEventListener('click', () => {
msgBits = [1,0,1,0,1,0,0,0,0,0,0]; lastE = null; renderBits(); showSteps();
});
renderBits();
// Auto-show public key on load
showSteps([{ title: 'Public key h = p·f⁻¹·g mod q', body: polyStr(H) },
{ title: 'Click bits to set your message, then Encrypt.', body: '' }]);
Notice how small everything is: polynomials with just N = 11 coefficients, coefficients reduced mod q = 32. That compactness is why NTRU is orders of magnitude faster than RSA at equivalent security levels, and why its key sizes are measured in hundreds of bytes rather than thousands.
Comments
Loading comments...