Three clients each hold a private value. They generate pairwise random masks: client i adds rij and client j subtracts rij. When the server adds the masked values, every mask cancels and only the true sum survives.
<p class="hint">Each client hides its private value with a random mask. The masks are chosen in pairs so they cancel in the sum — the server learns only the total, never any individual value. Edit the values and click <b>Run Protocol</b> to see it in action.</p>
<div class="clients" id="clients"></div>
<div class="server-box">
<div class="server-label">Server (sees only masked values)</div>
<div class="masked-row" id="masked-row"></div>
<div class="sum-row" id="sum-row"></div>
</div>
<div class="status" id="status"></div>
<div class="btns">
<button id="btn-run" type="button">Run Protocol</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
/* Base styles for the demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: .5rem; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
.clients { display: flex; gap: .6rem; flex-wrap: wrap; margin-bottom: .8rem; }
/* Client card styles */
.client-card {
flex: 1 1 120px; background: #e8eef3; border: 1px solid #cdd9e3;
border-radius: 10px; padding: .65rem .75rem; min-width: 110px;
}
.client-name { font-weight: 700; font-size: .85rem; color: #1d3557; margin-bottom: .4rem; }
.private-label { font-size: .75rem; color: #555; }
.private-val { display: flex; align-items: center; gap: .4rem; }
.private-val input {
width: 62px; font: 700 1rem ui-monospace, monospace; text-align: center;
border: 1px solid #adb1b8; border-radius: 6px; padding: .2rem .3rem;
background: #fff; color: #1d3557;
}
.mask-info { font-size: .75rem; color: #666; margin-top: .35rem; min-height: 1.1em; }
/* Server box styles */
.server-box {
background: #f7f0e6; border: 1px solid #d6c6a8; border-radius: 10px;
padding: .65rem .75rem; margin-bottom: .6rem;
}
.server-label { font-weight: 700; font-size: .85rem; color: #7a5c1e; margin-bottom: .4rem; }
.masked-row, .sum-row { font-size: .88rem; min-height: 1.3em; color: #444; }
.sum-row { font-weight: 700; color: #1d3557; margin-top: .3rem; }
/* Status message styles */
.status { font-size: 1rem; font-weight: 600; margin: .4rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.btns { display: flex; gap: .5rem; }
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; }
/* Secure Aggregation demo: pairwise random masks that cancel in the sum. */
/* Each pair (i,j) shares a random r_ij. Client i adds r_ij, client j subtracts it. */
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
const NAMES = ['Alice', 'Bob', 'Carol'];
const DEFAULTS = [42, 17, 65];
/* We work modulo M to keep numbers small and well-defined. */
const M = 1000; // modulus — keeps values in [0, M)
let masks = null; // pairwise mask matrix, built on each run
function getValues() {
return NAMES.map((_, i) => {
const el = document.getElementById('inp-' + i);
const v = parseInt(el ? el.value : DEFAULTS[i], 10);
return isNaN(v) ? DEFAULTS[i] : v;
});
}
function randInt(max) {
/* uniform random integer in [0, max) */
return Math.floor(Math.random() * max);
}
/* Build the n x n pairwise mask matrix. */
function generateMasks(n) {
// r[i][j] = mask Alice adds; r[j][i] = mask Bob subtracts (they cancel).
const r = Array.from({length: n}, () => Array(n).fill(0));
for (let i = 0; i < n; i++) {
for (let j = i + 1; j < n; j++) {
const rij = randInt(M);
r[i][j] = rij; // i adds rij
r[j][i] = M - rij; // j subtracts rij (mod M)
}
}
return r;
}
/* Net mask for client i = sum of row i (mod M). */
function netMask(r, i) {
// Row i of mask matrix: positive contributions from pairs where i < j, negative from i > j.
return r[i].reduce((s, v) => s + v, 0) % M;
}
function buildUI() {
const c = document.getElementById('clients');
c.innerHTML = '';
NAMES.forEach((name, i) => {
const card = document.createElement('div');
card.className = 'client-card';
card.innerHTML = `
<div class="client-name">${name}</div>
<div class="private-label">Private value</div>
<div class="private-val">
<input type="number" id="inp-${i}" value="${DEFAULTS[i]}" min="0" max="999" />
</div>
<div class="mask-info" id="mask-${i}"></div>
`;
c.appendChild(card);
});
document.getElementById('masked-row').textContent = '';
document.getElementById('sum-row').textContent = '';
document.getElementById('status').textContent = '';
document.getElementById('status').className = 'status';
masks = null;
}
function run() {
const vals = getValues();
const n = vals.length;
/* Generate fresh pairwise masks for this run. */
masks = generateMasks(n);
/* Each client computes: masked_i = (value_i + net_mask_i) mod M. */
const masked = vals.map((v, i) => (v + netMask(masks, i)) % M);
/* Display the net mask for each client card. */
NAMES.forEach((_, i) => {
const nm = netMask(masks, i);
document.getElementById('mask-' + i).textContent =
i18nFmt('net mask: %net%', { net: nm >= M/2 ? nm - M : nm });
});
/* Show the masked values the server receives. */
const maskedStr = masked.map((v, i) =>
i18nFmt('%name%=%val%', { name: NAMES[i], val: v })
).join(' + ');
document.getElementById('masked-row').textContent =
'Receives: ' + maskedStr;
/* Server sums masked values; masks cancel, leaving true sum (mod M). */
const serverSum = masked.reduce((a, b) => a + b, 0) % M;
const trueSum = vals.reduce((a, b) => a + b, 0);
const normalizedServerSum = ((serverSum % M) + M) % M;
const normalizedTrue = trueSum % M;
document.getElementById('sum-row').textContent =
i18nFmt('Sum of masked values (mod 1000) = %sum%', { sum: serverSum });
if (normalizedServerSum === normalizedTrue) {
document.getElementById('status').textContent =
i18nFmt('✓ Server computed sum = %sum% — correct! No individual value was revealed.', { sum: trueSum });
document.getElementById('status').className = 'status ok';
} else {
document.getElementById('status').textContent = '✗ Unexpected mismatch — check for integer overflow.';
document.getElementById('status').className = 'status bad';
}
}
document.getElementById('btn-run').addEventListener('click', run);
document.getElementById('btn-reset').addEventListener('click', buildUI);
buildUI();
<p class="hint">Each client hides its private value with a random mask. The masks are chosen in pairs so they cancel in the sum — the server learns only the total, never any individual value. Edit the values and click <b>Run Protocol</b> to see it in action.</p>
<div class="clients" id="clients"></div>
<div class="server-box">
<div class="server-label">Server (sees only masked values)</div>
<div class="masked-row" id="masked-row"></div>
<div class="sum-row" id="sum-row"></div>
</div>
<div class="status" id="status"></div>
<div class="btns">
<button id="btn-run" type="button">Run Protocol</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
/* Base styles for the demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: .5rem; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
.clients { display: flex; gap: .6rem; flex-wrap: wrap; margin-bottom: .8rem; }
/* Client card styles */
.client-card {
flex: 1 1 120px; background: #e8eef3; border: 1px solid #cdd9e3;
border-radius: 10px; padding: .65rem .75rem; min-width: 110px;
}
.client-name { font-weight: 700; font-size: .85rem; color: #1d3557; margin-bottom: .4rem; }
.private-label { font-size: .75rem; color: #555; }
.private-val { display: flex; align-items: center; gap: .4rem; }
.private-val input {
width: 62px; font: 700 1rem ui-monospace, monospace; text-align: center;
border: 1px solid #adb1b8; border-radius: 6px; padding: .2rem .3rem;
background: #fff; color: #1d3557;
}
.mask-info { font-size: .75rem; color: #666; margin-top: .35rem; min-height: 1.1em; }
/* Server box styles */
.server-box {
background: #f7f0e6; border: 1px solid #d6c6a8; border-radius: 10px;
padding: .65rem .75rem; margin-bottom: .6rem;
}
.server-label { font-weight: 700; font-size: .85rem; color: #7a5c1e; margin-bottom: .4rem; }
.masked-row, .sum-row { font-size: .88rem; min-height: 1.3em; color: #444; }
.sum-row { font-weight: 700; color: #1d3557; margin-top: .3rem; }
/* Status message styles */
.status { font-size: 1rem; font-weight: 600; margin: .4rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.btns { display: flex; gap: .5rem; }
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; }
/* Secure Aggregation demo: pairwise random masks that cancel in the sum. */
/* Each pair (i,j) shares a random r_ij. Client i adds r_ij, client j subtracts it. */
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
const NAMES = ['Alice', 'Bob', 'Carol'];
const DEFAULTS = [42, 17, 65];
/* We work modulo M to keep numbers small and well-defined. */
const M = 1000; // modulus — keeps values in [0, M)
let masks = null; // pairwise mask matrix, built on each run
function getValues() {
return NAMES.map((_, i) => {
const el = document.getElementById('inp-' + i);
const v = parseInt(el ? el.value : DEFAULTS[i], 10);
return isNaN(v) ? DEFAULTS[i] : v;
});
}
function randInt(max) {
/* uniform random integer in [0, max) */
return Math.floor(Math.random() * max);
}
/* Build the n x n pairwise mask matrix. */
function generateMasks(n) {
// r[i][j] = mask Alice adds; r[j][i] = mask Bob subtracts (they cancel).
const r = Array.from({length: n}, () => Array(n).fill(0));
for (let i = 0; i < n; i++) {
for (let j = i + 1; j < n; j++) {
const rij = randInt(M);
r[i][j] = rij; // i adds rij
r[j][i] = M - rij; // j subtracts rij (mod M)
}
}
return r;
}
/* Net mask for client i = sum of row i (mod M). */
function netMask(r, i) {
// Row i of mask matrix: positive contributions from pairs where i < j, negative from i > j.
return r[i].reduce((s, v) => s + v, 0) % M;
}
function buildUI() {
const c = document.getElementById('clients');
c.innerHTML = '';
NAMES.forEach((name, i) => {
const card = document.createElement('div');
card.className = 'client-card';
card.innerHTML = `
<div class="client-name">${name}</div>
<div class="private-label">Private value</div>
<div class="private-val">
<input type="number" id="inp-${i}" value="${DEFAULTS[i]}" min="0" max="999" />
</div>
<div class="mask-info" id="mask-${i}"></div>
`;
c.appendChild(card);
});
document.getElementById('masked-row').textContent = '';
document.getElementById('sum-row').textContent = '';
document.getElementById('status').textContent = '';
document.getElementById('status').className = 'status';
masks = null;
}
function run() {
const vals = getValues();
const n = vals.length;
/* Generate fresh pairwise masks for this run. */
masks = generateMasks(n);
/* Each client computes: masked_i = (value_i + net_mask_i) mod M. */
const masked = vals.map((v, i) => (v + netMask(masks, i)) % M);
/* Display the net mask for each client card. */
NAMES.forEach((_, i) => {
const nm = netMask(masks, i);
document.getElementById('mask-' + i).textContent =
i18nFmt('net mask: %net%', { net: nm >= M/2 ? nm - M : nm });
});
/* Show the masked values the server receives. */
const maskedStr = masked.map((v, i) =>
i18nFmt('%name%=%val%', { name: NAMES[i], val: v })
).join(' + ');
document.getElementById('masked-row').textContent =
'Receives: ' + maskedStr;
/* Server sums masked values; masks cancel, leaving true sum (mod M). */
const serverSum = masked.reduce((a, b) => a + b, 0) % M;
const trueSum = vals.reduce((a, b) => a + b, 0);
const normalizedServerSum = ((serverSum % M) + M) % M;
const normalizedTrue = trueSum % M;
document.getElementById('sum-row').textContent =
i18nFmt('Sum of masked values (mod 1000) = %sum%', { sum: serverSum });
if (normalizedServerSum === normalizedTrue) {
document.getElementById('status').textContent =
i18nFmt('✓ Server computed sum = %sum% — correct! No individual value was revealed.', { sum: trueSum });
document.getElementById('status').className = 'status ok';
} else {
document.getElementById('status').textContent = '✗ Unexpected mismatch — check for integer overflow.';
document.getElementById('status').className = 'status bad';
}
}
document.getElementById('btn-run').addEventListener('click', run);
document.getElementById('btn-reset').addEventListener('click', buildUI);
buildUI();
Notice what the server receives: three masked numbers that individually look like random noise. Only their sum is meaningful — and that sum equals the exact total of the private inputs, no individual value exposed.
Comments
Loading comments...