Choose a secret number and a threshold k. The demo builds a random degree-(k−1) polynomial whose constant term is the secret, then hands out shares — points on that curve. Pick which shares to combine and rebuild the secret by Lagrange interpolation.
<p class="hint">Pick a <b>secret</b> and a threshold <b>k</b>. We build a random degree-(k−1) polynomial whose value at x=0 is the secret, then hand out 5 <b>shares</b> (points on it). Tick any shares and recover the secret. With <b>k</b> ticked it always lands; with <b>k−1</b> it can't — every secret stays possible.</p>
<div class="controls">
<label>Secret <input id="secret" type="number" min="0" max="96" value="42"></label>
<label>Threshold k
<select id="k"><option>2</option><option selected>3</option><option>4</option></select>
</label>
<button id="split" type="button">Split into shares</button>
</div>
<div id="shares" class="shares"></div>
<div class="btns">
<button id="recover" type="button">Recover from ticked shares</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div class="status" id="status">Press "Split into shares" to begin.</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 .7rem; line-height: 1.45; }
.controls { display: flex; gap: .8rem; flex-wrap: wrap; align-items: flex-end; margin-bottom: .6rem; }
label { font-size: .85rem; font-weight: 600; display: flex; flex-direction: column; gap: .25rem; }
input, select { font: 600 14px system-ui, sans-serif; padding: .35rem .5rem; border: 1px solid #adb1b8; border-radius: 6px; }
input { width: 90px; }
.shares { display: grid; grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); gap: .5rem; margin: .5rem 0; }
.share { display: flex; align-items: center; gap: .5rem; background: #e8eef3; border: 1px solid #cdd9e3;
border-radius: 8px; padding: .5rem .6rem; font: 700 14px ui-monospace, monospace; color: #1d3557; cursor: pointer; user-select: none; }
.share input { width: auto; }
.share.on { background: #d2e7d8; border-color: #0a7d33; }
.status { font-size: .98rem; font-weight: 600; margin: .6rem 0 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: .3rem; }
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] ?? ''));
// Shamir secret sharing over the prime field GF(P). The secret is the constant
// term a0 of a random degree-(k-1) polynomial. A share is a point (x, f(x) mod P).
// Any k shares recover a0 by Lagrange interpolation at x=0; any k-1 shares leave
// every secret equally possible (information-theoretic / perfect secrecy).
const P = 97; // prime field; secret must be < P
const N = 5; // number of shares handed out
let coeffs = [], shares = [], K = 3, SECRET = 42;
const mod = x => ((x % P) + P) % P;
function inv(a){ // modular inverse via Fermat: a^(P-2) mod P
a = mod(a); let r = 1, e = P - 2;
while(e > 0){ if(e & 1) r = mod(r * a); a = mod(a * a); e >>= 1; }
return r;
}
function evalPoly(c, x){ let y = 0; for(let i = c.length - 1; i >= 0; i--) y = mod(y * x + c[i]); return y; }
// Lagrange interpolation evaluated at x=0 over the given points.
function recoverAtZero(pts){
let secret = 0;
for(let i = 0; i < pts.length; i++){
let num = 1, den = 1;
for(let j = 0; j < pts.length; j++){ if(i === j) continue;
num = mod(num * (0 - pts[j].x)); // (0 - xj)
den = mod(den * (pts[i].x - pts[j].x)); // (xi - xj)
}
secret = mod(secret + pts[i].y * mod(num * inv(den)));
}
return secret;
}
function split(){
SECRET = Math.max(0, Math.min(P - 1, parseInt(document.getElementById('secret').value || '0', 10)));
document.getElementById('secret').value = SECRET;
K = parseInt(document.getElementById('k').value, 10);
coeffs = [SECRET];
for(let i = 1; i < K; i++) coeffs.push(Math.floor(Math.random() * P)); // random a1..a(k-1)
shares = [];
for(let x = 1; x <= N; x++) shares.push({ x, y: evalPoly(coeffs, x) });
renderShares();
setStatus(fmt('Built a degree-%v% polynomial. Tick shares and recover. (k = %K%)', { v: K - 1, K: K }), '');
}
function renderShares(){
const box = document.getElementById('shares'); box.innerHTML = '';
shares.forEach((s, idx) => {
const d = document.createElement('label'); d.className = 'share';
const cb = document.createElement('input'); cb.type = 'checkbox'; cb.dataset.i = idx;
cb.addEventListener('change', () => { d.classList.toggle('on', cb.checked); });
const span = document.createElement('span'); span.textContent = 'P' + s.x + ' = (' + s.x + ', ' + s.y + ')';
d.appendChild(cb); d.appendChild(span); box.appendChild(d);
});
}
function selected(){
return [...document.querySelectorAll('#shares input:checked')].map(cb => shares[+cb.dataset.i]);
}
function recover(){
if(shares.length === 0){ setStatus('Split a secret first.', 'bad'); return; }
const pts = selected();
if(pts.length === 0){ setStatus('Tick some shares to combine.', 'bad'); return; }
const guess = recoverAtZero(pts);
if(pts.length >= K){
setStatus(fmt('Recovered with %length% shares (k = %K%): secret = %guess%%v%', { length: pts.length, K: K, guess: guess, v: guess === SECRET ? ' ✓ correct!' : '' }), 'ok');
} else {
// With fewer than k points, count how many of the P secrets are still consistent.
setStatus(fmt('Only %length% of %K% shares. These points fit a curve through every one of the %P% possible secrets — they reveal nothing. (This combo would interpolate to %guess%, but that is meaningless: any secret is equally possible.)', { length: pts.length, K: K, P: P, guess: guess }), 'bad');
}
}
document.getElementById('split').addEventListener('click', split);
document.getElementById('recover').addEventListener('click', recover);
document.getElementById('reset').addEventListener('click', () => {
shares = []; coeffs = []; document.getElementById('shares').innerHTML = '';
setStatus('Press "Split into shares" to begin.', '');
});
function setStatus(t, cls){ const s = document.getElementById('status'); s.textContent = t; s.className = 'status ' + cls; }
split();
<p class="hint">Pick a <b>secret</b> and a threshold <b>k</b>. We build a random degree-(k−1) polynomial whose value at x=0 is the secret, then hand out 5 <b>shares</b> (points on it). Tick any shares and recover the secret. With <b>k</b> ticked it always lands; with <b>k−1</b> it can't — every secret stays possible.</p>
<div class="controls">
<label>Secret <input id="secret" type="number" min="0" max="96" value="42"></label>
<label>Threshold k
<select id="k"><option>2</option><option selected>3</option><option>4</option></select>
</label>
<button id="split" type="button">Split into shares</button>
</div>
<div id="shares" class="shares"></div>
<div class="btns">
<button id="recover" type="button">Recover from ticked shares</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div class="status" id="status">Press "Split into shares" to begin.</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 .7rem; line-height: 1.45; }
.controls { display: flex; gap: .8rem; flex-wrap: wrap; align-items: flex-end; margin-bottom: .6rem; }
label { font-size: .85rem; font-weight: 600; display: flex; flex-direction: column; gap: .25rem; }
input, select { font: 600 14px system-ui, sans-serif; padding: .35rem .5rem; border: 1px solid #adb1b8; border-radius: 6px; }
input { width: 90px; }
.shares { display: grid; grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); gap: .5rem; margin: .5rem 0; }
.share { display: flex; align-items: center; gap: .5rem; background: #e8eef3; border: 1px solid #cdd9e3;
border-radius: 8px; padding: .5rem .6rem; font: 700 14px ui-monospace, monospace; color: #1d3557; cursor: pointer; user-select: none; }
.share input { width: auto; }
.share.on { background: #d2e7d8; border-color: #0a7d33; }
.status { font-size: .98rem; font-weight: 600; margin: .6rem 0 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: .3rem; }
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] ?? ''));
// Shamir secret sharing over the prime field GF(P). The secret is the constant
// term a0 of a random degree-(k-1) polynomial. A share is a point (x, f(x) mod P).
// Any k shares recover a0 by Lagrange interpolation at x=0; any k-1 shares leave
// every secret equally possible (information-theoretic / perfect secrecy).
const P = 97; // prime field; secret must be < P
const N = 5; // number of shares handed out
let coeffs = [], shares = [], K = 3, SECRET = 42;
const mod = x => ((x % P) + P) % P;
function inv(a){ // modular inverse via Fermat: a^(P-2) mod P
a = mod(a); let r = 1, e = P - 2;
while(e > 0){ if(e & 1) r = mod(r * a); a = mod(a * a); e >>= 1; }
return r;
}
function evalPoly(c, x){ let y = 0; for(let i = c.length - 1; i >= 0; i--) y = mod(y * x + c[i]); return y; }
// Lagrange interpolation evaluated at x=0 over the given points.
function recoverAtZero(pts){
let secret = 0;
for(let i = 0; i < pts.length; i++){
let num = 1, den = 1;
for(let j = 0; j < pts.length; j++){ if(i === j) continue;
num = mod(num * (0 - pts[j].x)); // (0 - xj)
den = mod(den * (pts[i].x - pts[j].x)); // (xi - xj)
}
secret = mod(secret + pts[i].y * mod(num * inv(den)));
}
return secret;
}
function split(){
SECRET = Math.max(0, Math.min(P - 1, parseInt(document.getElementById('secret').value || '0', 10)));
document.getElementById('secret').value = SECRET;
K = parseInt(document.getElementById('k').value, 10);
coeffs = [SECRET];
for(let i = 1; i < K; i++) coeffs.push(Math.floor(Math.random() * P)); // random a1..a(k-1)
shares = [];
for(let x = 1; x <= N; x++) shares.push({ x, y: evalPoly(coeffs, x) });
renderShares();
setStatus(fmt('Built a degree-%v% polynomial. Tick shares and recover. (k = %K%)', { v: K - 1, K: K }), '');
}
function renderShares(){
const box = document.getElementById('shares'); box.innerHTML = '';
shares.forEach((s, idx) => {
const d = document.createElement('label'); d.className = 'share';
const cb = document.createElement('input'); cb.type = 'checkbox'; cb.dataset.i = idx;
cb.addEventListener('change', () => { d.classList.toggle('on', cb.checked); });
const span = document.createElement('span'); span.textContent = 'P' + s.x + ' = (' + s.x + ', ' + s.y + ')';
d.appendChild(cb); d.appendChild(span); box.appendChild(d);
});
}
function selected(){
return [...document.querySelectorAll('#shares input:checked')].map(cb => shares[+cb.dataset.i]);
}
function recover(){
if(shares.length === 0){ setStatus('Split a secret first.', 'bad'); return; }
const pts = selected();
if(pts.length === 0){ setStatus('Tick some shares to combine.', 'bad'); return; }
const guess = recoverAtZero(pts);
if(pts.length >= K){
setStatus(fmt('Recovered with %length% shares (k = %K%): secret = %guess%%v%', { length: pts.length, K: K, guess: guess, v: guess === SECRET ? ' ✓ correct!' : '' }), 'ok');
} else {
// With fewer than k points, count how many of the P secrets are still consistent.
setStatus(fmt('Only %length% of %K% shares. These points fit a curve through every one of the %P% possible secrets — they reveal nothing. (This combo would interpolate to %guess%, but that is meaningless: any secret is equally possible.)', { length: pts.length, K: K, P: P, guess: guess }), 'bad');
}
}
document.getElementById('split').addEventListener('click', split);
document.getElementById('recover').addEventListener('click', recover);
document.getElementById('reset').addEventListener('click', () => {
shares = []; coeffs = []; document.getElementById('shares').innerHTML = '';
setStatus('Press "Split into shares" to begin.', '');
});
function setStatus(t, cls){ const s = document.getElementById('status'); s.textContent = t; s.className = 'status ' + cls; }
split();
Notice the sharp threshold. With any k shares, interpolation lands on the exact secret every time. With k−1 shares, every possible secret remains equally consistent with what you hold — the demo shows that fewer than k points fit infinitely many curves, so the leftover share leaks nothing. All arithmetic is done modulo a prime so the security is exact, not approximate.
Comments
Loading comments...