Pick two numbers and a secret key that never leaves your side. Encrypt each number, then send only the scrambled ciphertexts to an "untrusted server." The server adds the two ciphertexts together — it has no key, so to it they are just noise — and returns a single scrambled result. Decrypt it, and out comes the exact sum.
<p class="hint">Pick two numbers. <b>Encrypt</b> them with a secret key that never leaves you. The untrusted <b>server</b> sees only scrambled ciphertexts, yet adds them blindly. <b>Decrypt</b> the server's reply and out comes the exact sum.</p>
<div class="inputs">
<label>a <input id="a" type="number" value="7"></label>
<label>b <input id="b" type="number" value="35"></label>
</div>
<div class="btns">
<button id="enc" type="button">1 · Encrypt & send</button>
<button id="add" type="button" disabled>2 · Server adds (blind)</button>
<button id="dec" type="button" disabled>3 · Decrypt result</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div class="panels">
<div class="panel you">
<div class="ptitle">Your side <span>(holds the key)</span></div>
<div id="you" class="rows"></div>
</div>
<div class="panel srv">
<div class="ptitle">Server view <span>(no key)</span></div>
<div id="srv" class="rows"></div>
</div>
</div>
<div class="status" id="status">Press "Encrypt & send" 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; }
.inputs { display: flex; gap: 1rem; margin: .4rem 0 .7rem; }
.inputs label { font-weight: 600; font-size: .95rem; display: flex; align-items: center; gap: .4rem; }
.inputs input { width: 70px; font: 600 15px ui-monospace, monospace; padding: .3rem .4rem;
border: 1px solid #adb1b8; border-radius: 6px; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .8rem; }
button { font: 600 13px system-ui, sans-serif; padding: .45rem .8rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button:disabled { opacity: .4; cursor: not-allowed; }
button.ghost { background: #fff; color: #1d3557; }
.panels { display: grid; grid-template-columns: 1fr 1fr; gap: .7rem; }
.panel { border: 1px solid #cdd9e3; border-radius: 10px; padding: .6rem .7rem; min-height: 96px; }
.panel.you { background: #eef5ec; border-color: #bcd6b6; }
.panel.srv { background: #f1f1f4; border-color: #cdd0d6; }
.ptitle { font-weight: 700; font-size: .82rem; margin-bottom: .4rem; color: #1d3557; }
.ptitle span { font-weight: 400; color: #667; }
.rows { font: 600 13px ui-monospace, monospace; line-height: 1.55; word-break: break-all; }
.rows .lbl { color: #555; font-weight: 400; }
.rows .ct { color: #8a4b00; }
.rows .pt { color: #0a7d33; }
.status { font-size: .95rem; font-weight: 600; margin: .7rem 0 0; min-height: 1.3em; color: #1d3557; }
.status.ok { color: #0a7d33; }
@media (max-width: 460px) { .panels { grid-template-columns: 1fr; } }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// A toy ADDITIVELY HOMOMORPHIC scheme, just enough to show the idea honestly.
// Secret key s and a large modulus N live ONLY on "your side".
// Encrypt: c = (m + s) mod N -- m hidden behind the secret offset s
// The server adds two ciphertexts: cSum = (c1 + c2) mod N (no key needed)
// Decrypt a sum of k ciphertexts: m = (cSum - k*s) mod N
// Because (m1+s)+(m2+s) = (m1+m2) + 2s, subtracting 2*s on decrypt recovers the
// true sum. Adding ciphertexts mirrors adding the secrets inside them -> homomorphic.
const N = 1000003; // large prime modulus
const s = 524287; // the SECRET key (never sent to the server)
const mod = (x) => ((x % N) + N) % N;
const enc = (m) => mod(m + s);
const decSum = (c, k) => mod(c - k * s);
let c1 = null, c2 = null, cSum = null, A = 0, B = 0;
const $ = (id) => document.getElementById(id);
const setStatus = (t, ok) => { const e = $('status'); e.textContent = t; e.className = 'status' + (ok ? ' ok' : ''); };
function renderYou(lines) { $('you').innerHTML = lines.join('<br>'); }
function renderSrv(lines) { $('srv').innerHTML = lines.join('<br>'); }
function encrypt() {
A = parseInt($('a').value, 10) || 0;
B = parseInt($('b').value, 10) || 0;
c1 = enc(A); c2 = enc(B); cSum = null;
renderYou([
'<span class="lbl">a =</span> ' + A,
'<span class="lbl">b =</span> ' + B,
fmt('<span class="lbl">key s =</span> %s% <span class="lbl">(secret)</span>', { s: s })
]);
renderSrv([
fmt('<span class="lbl">recv c1 =</span> <span class="ct">%c1%</span>', { c1: c1 }),
fmt('<span class="lbl">recv c2 =</span> <span class="ct">%c2%</span>', { c2: c2 }),
'<span class="lbl">key?</span> none'
]);
$('add').disabled = false; $('dec').disabled = true;
setStatus('Ciphertexts sent. The server sees only the orange numbers.');
}
function serverAdd() {
cSum = mod(c1 + c2); // blind addition: no key involved
renderSrv([
'<span class="lbl">c1 =</span> <span class="ct">' + c1 + '</span>',
'<span class="lbl">c2 =</span> <span class="ct">' + c2 + '</span>',
'<span class="lbl">c1+c2 mod N =</span> <span class="ct">' + cSum + '</span>'
]);
$('dec').disabled = false;
setStatus(fmt('Server added the ciphertexts blindly and returned %cSum%.', { cSum: cSum }));
}
function decrypt() {
const result = decSum(cSum, 2); // two ciphertexts were combined -> subtract 2*s
const truth = A + B;
renderYou([
'<span class="lbl">a =</span> ' + A + ' <span class="lbl">b =</span> ' + B,
fmt('<span class="lbl">decrypt(server) =</span> <span class="pt">%result%</span>', { result: result }),
'<span class="lbl">a + b =</span> <span class="pt">' + truth + '</span>'
]);
if (result === truth) setStatus(fmt('✓ Decrypted %result% = a + b. The server computed it without ever reading a or b!', { result: result }), true);
else setStatus('Mismatch — that should not happen.');
}
function reset() {
c1 = c2 = cSum = null; A = B = 0;
renderYou([]); renderSrv([]);
$('add').disabled = true; $('dec').disabled = true;
setStatus('Press "Encrypt & send" to begin.');
}
$('enc').addEventListener('click', encrypt);
$('add').addEventListener('click', serverAdd);
$('dec').addEventListener('click', decrypt);
$('reset').addEventListener('click', reset);
reset();
<p class="hint">Pick two numbers. <b>Encrypt</b> them with a secret key that never leaves you. The untrusted <b>server</b> sees only scrambled ciphertexts, yet adds them blindly. <b>Decrypt</b> the server's reply and out comes the exact sum.</p>
<div class="inputs">
<label>a <input id="a" type="number" value="7"></label>
<label>b <input id="b" type="number" value="35"></label>
</div>
<div class="btns">
<button id="enc" type="button">1 · Encrypt & send</button>
<button id="add" type="button" disabled>2 · Server adds (blind)</button>
<button id="dec" type="button" disabled>3 · Decrypt result</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div class="panels">
<div class="panel you">
<div class="ptitle">Your side <span>(holds the key)</span></div>
<div id="you" class="rows"></div>
</div>
<div class="panel srv">
<div class="ptitle">Server view <span>(no key)</span></div>
<div id="srv" class="rows"></div>
</div>
</div>
<div class="status" id="status">Press "Encrypt & send" 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; }
.inputs { display: flex; gap: 1rem; margin: .4rem 0 .7rem; }
.inputs label { font-weight: 600; font-size: .95rem; display: flex; align-items: center; gap: .4rem; }
.inputs input { width: 70px; font: 600 15px ui-monospace, monospace; padding: .3rem .4rem;
border: 1px solid #adb1b8; border-radius: 6px; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .8rem; }
button { font: 600 13px system-ui, sans-serif; padding: .45rem .8rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button:disabled { opacity: .4; cursor: not-allowed; }
button.ghost { background: #fff; color: #1d3557; }
.panels { display: grid; grid-template-columns: 1fr 1fr; gap: .7rem; }
.panel { border: 1px solid #cdd9e3; border-radius: 10px; padding: .6rem .7rem; min-height: 96px; }
.panel.you { background: #eef5ec; border-color: #bcd6b6; }
.panel.srv { background: #f1f1f4; border-color: #cdd0d6; }
.ptitle { font-weight: 700; font-size: .82rem; margin-bottom: .4rem; color: #1d3557; }
.ptitle span { font-weight: 400; color: #667; }
.rows { font: 600 13px ui-monospace, monospace; line-height: 1.55; word-break: break-all; }
.rows .lbl { color: #555; font-weight: 400; }
.rows .ct { color: #8a4b00; }
.rows .pt { color: #0a7d33; }
.status { font-size: .95rem; font-weight: 600; margin: .7rem 0 0; min-height: 1.3em; color: #1d3557; }
.status.ok { color: #0a7d33; }
@media (max-width: 460px) { .panels { grid-template-columns: 1fr; } }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// A toy ADDITIVELY HOMOMORPHIC scheme, just enough to show the idea honestly.
// Secret key s and a large modulus N live ONLY on "your side".
// Encrypt: c = (m + s) mod N -- m hidden behind the secret offset s
// The server adds two ciphertexts: cSum = (c1 + c2) mod N (no key needed)
// Decrypt a sum of k ciphertexts: m = (cSum - k*s) mod N
// Because (m1+s)+(m2+s) = (m1+m2) + 2s, subtracting 2*s on decrypt recovers the
// true sum. Adding ciphertexts mirrors adding the secrets inside them -> homomorphic.
const N = 1000003; // large prime modulus
const s = 524287; // the SECRET key (never sent to the server)
const mod = (x) => ((x % N) + N) % N;
const enc = (m) => mod(m + s);
const decSum = (c, k) => mod(c - k * s);
let c1 = null, c2 = null, cSum = null, A = 0, B = 0;
const $ = (id) => document.getElementById(id);
const setStatus = (t, ok) => { const e = $('status'); e.textContent = t; e.className = 'status' + (ok ? ' ok' : ''); };
function renderYou(lines) { $('you').innerHTML = lines.join('<br>'); }
function renderSrv(lines) { $('srv').innerHTML = lines.join('<br>'); }
function encrypt() {
A = parseInt($('a').value, 10) || 0;
B = parseInt($('b').value, 10) || 0;
c1 = enc(A); c2 = enc(B); cSum = null;
renderYou([
'<span class="lbl">a =</span> ' + A,
'<span class="lbl">b =</span> ' + B,
fmt('<span class="lbl">key s =</span> %s% <span class="lbl">(secret)</span>', { s: s })
]);
renderSrv([
fmt('<span class="lbl">recv c1 =</span> <span class="ct">%c1%</span>', { c1: c1 }),
fmt('<span class="lbl">recv c2 =</span> <span class="ct">%c2%</span>', { c2: c2 }),
'<span class="lbl">key?</span> none'
]);
$('add').disabled = false; $('dec').disabled = true;
setStatus('Ciphertexts sent. The server sees only the orange numbers.');
}
function serverAdd() {
cSum = mod(c1 + c2); // blind addition: no key involved
renderSrv([
'<span class="lbl">c1 =</span> <span class="ct">' + c1 + '</span>',
'<span class="lbl">c2 =</span> <span class="ct">' + c2 + '</span>',
'<span class="lbl">c1+c2 mod N =</span> <span class="ct">' + cSum + '</span>'
]);
$('dec').disabled = false;
setStatus(fmt('Server added the ciphertexts blindly and returned %cSum%.', { cSum: cSum }));
}
function decrypt() {
const result = decSum(cSum, 2); // two ciphertexts were combined -> subtract 2*s
const truth = A + B;
renderYou([
'<span class="lbl">a =</span> ' + A + ' <span class="lbl">b =</span> ' + B,
fmt('<span class="lbl">decrypt(server) =</span> <span class="pt">%result%</span>', { result: result }),
'<span class="lbl">a + b =</span> <span class="pt">' + truth + '</span>'
]);
if (result === truth) setStatus(fmt('✓ Decrypted %result% = a + b. The server computed it without ever reading a or b!', { result: result }), true);
else setStatus('Mismatch — that should not happen.');
}
function reset() {
c1 = c2 = cSum = null; A = B = 0;
renderYou([]); renderSrv([]);
$('add').disabled = true; $('dec').disabled = true;
setStatus('Press "Encrypt & send" to begin.');
}
$('enc').addEventListener('click', encrypt);
$('add').addEventListener('click', serverAdd);
$('dec').addEventListener('click', decrypt);
$('reset').addEventListener('click', reset);
reset();
Watch the Server view: it sees only large meaningless numbers, yet its blind addition lines up perfectly with your plaintext sum once you decrypt. This works because the scheme is additively homomorphic — adding ciphertexts mirrors adding the secrets inside them. Full schemes add multiplication too, and with both operations you can evaluate any program on encrypted data. The catch is cost, which is where the complexity comes in.
Comments
Loading comments...