Below is a secret binary vector of 6 values — each is 0 or 1, representing a private database row. You cannot see it directly, but you can ask linear counting queries: each query reveals the sum of a subset of the entries.
<!-- reconstruction attack demo layout -->
<p class="hint">A secret 6-bit vector is hidden. Pick which positions to include in a <b>sum query</b>, then click Ask. Each exact answer eliminates candidates until only the secret remains — no hacking required, just linear algebra.</p>
<div class="panel">
<div class="secret-row">
<span class="label">Secret vector:</span>
<div id="secret-display" class="bit-row hidden-bits"></div>
<button id="reveal-btn" type="button" class="ghost small">Show secret</button>
</div>
<div class="candidates-row">
<span class="label">Remaining candidates:</span>
<span id="cand-count" class="count-badge">64</span>
</div>
</div>
<div class="query-panel">
<p class="query-label">Select positions to sum (x1 … x6):</p>
<div id="query-checkboxes" class="query-bits"></div>
<div class="query-row">
<button id="ask-btn" type="button">Ask query</button>
<span id="query-result" class="query-result"></span>
</div>
</div>
<div id="history" class="history"></div>
<div class="btns">
<button id="auto-btn" type="button">Best next query</button>
<button id="reset-btn" type="button" class="ghost">New secret</button>
</div>
<div id="status" class="status"></div>
/* styles for the reconstruction attack demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: .93rem; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .6rem; line-height: 1.4; }
.panel { background: #f0f4f8; border-radius: 10px; padding: .7rem 1rem; margin-bottom: .7rem; }
.secret-row, .candidates-row { display: flex; align-items: center; gap: .6rem; margin: .25rem 0; }
.label { font-size: .8rem; font-weight: 600; color: #555; min-width: 100px; }
.bit-row { display: flex; gap: 4px; }
.bit { width: 32px; height: 32px; border-radius: 6px; display: flex; align-items: center;
justify-content: center; font: 700 15px ui-monospace, monospace; }
.bit-0 { background: #d1e8ff; color: #1a5fa0; border: 1px solid #99c4ef; }
.bit-1 { background: #ffd6d6; color: #9b1c1c; border: 1px solid #f5a0a0; }
.bit-hidden { background: #c9ccd1; color: transparent; border: 1px solid #adb1b8; }
.count-badge { background: #1d3557; color: #fff; border-radius: 20px; padding: .1rem .6rem;
font-weight: 700; font-size: .9rem; transition: background .3s; }
.count-badge.solved { background: #0a7d33; }
.small { font-size: .78rem; padding: .2rem .55rem; }
.query-panel { border: 1px solid #cdd9e3; border-radius: 10px; padding: .7rem 1rem; margin-bottom: .6rem; }
.query-label { font-size: .82rem; font-weight: 600; color: #444; margin: 0 0 .4rem; }
.query-bits { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: .5rem; }
.qb { width: 38px; height: 38px; border-radius: 8px; border: 2px solid #cdd9e3;
background: #f5f7fa; cursor: pointer; display: flex; align-items: center;
justify-content: center; font: 700 13px ui-monospace, monospace; color: #555;
transition: all .1s; user-select: none; }
.qb.selected { background: #1d3557; color: #fff; border-color: #1d3557; }
.query-row { display: flex; align-items: center; gap: .6rem; }
.query-result { font-weight: 700; font-size: 1rem; color: #1d3557; }
.history { max-height: 120px; overflow-y: auto; margin-bottom: .5rem; }
.hist-item { font-size: .8rem; color: #444; padding: .1rem 0; border-bottom: 1px solid #eee; }
.hist-item:last-child { border-bottom: none; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .4rem; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.status { font-size: .95rem; font-weight: 600; min-height: 1.3em; }
.status.ok { color: #0a7d33; }
.status.info { color: #1a5fa0; }
// Reconstruction attack demo: a secret binary vector is chosen at random.
// Each linear query returns the exact sum of chosen positions.
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
const N = 6; // number of private bits
// the private database — one random bit per entry
const SECRET = Array.from({ length: N }, () => Math.random() < 0.5 ? 1 : 0);
// all 2^N possible vectors — start with full uncertainty
let candidates = [];
for (let mask = 0; mask < (1 << N); mask++) {
const v = Array.from({ length: N }, (_, i) => (mask >> i) & 1);
candidates.push(v);
}
// which positions the current query includes
const queryMask = Array(N).fill(false);
let revealed = false;
let queryCount = 0;
// DOM refs
const secretDisplay = document.getElementById('secret-display');
const candCount = document.getElementById('cand-count');
const queryCheckboxes = document.getElementById('query-checkboxes');
const queryResult = document.getElementById('query-result');
const history = document.getElementById('history');
const status = document.getElementById('status');
function renderSecret() {
// show each bit, or a question mark if still hidden
secretDisplay.innerHTML = '';
SECRET.forEach((b, i) => {
const d = document.createElement('div');
if (revealed) {
d.className = 'bit bit-' + b;
d.textContent = b;
} else {
d.className = 'bit bit-hidden';
d.textContent = '?';
}
secretDisplay.appendChild(d);
});
}
function renderQueryBits() {
// render the toggle buttons for building a query
queryCheckboxes.innerHTML = '';
for (let i = 0; i < N; i++) {
const d = document.createElement('div');
d.className = 'qb' + (queryMask[i] ? ' selected' : '');
d.textContent = 'x' + (i + 1);
d.title = i18nFmt('Include position %i% in the query sum', { i: i + 1 });
d.addEventListener('click', () => {
queryMask[i] = !queryMask[i];
renderQueryBits();
});
queryCheckboxes.appendChild(d);
}
}
function updateCandCount() {
// update the candidate counter and highlight when solved
candCount.textContent = candidates.length;
if (candidates.length === 1) {
candCount.classList.add('solved');
} else {
candCount.classList.remove('solved');
}
}
function askQuery() {
// ask the selected linear query and filter candidates
const indices = queryMask.map((on, i) => on ? i : -1).filter(i => i >= 0);
if (indices.length === 0) {
status.textContent = 'Select at least one position.';
status.className = 'status info';
return;
}
const answer = indices.reduce((s, i) => s + SECRET[i], 0);
queryCount++;
// keep only candidates consistent with the answer
const before = candidates.length;
candidates = candidates.filter(v => indices.reduce((s, i) => s + v[i], 0) === answer);
const after = candidates.length;
const mask_label = indices.map(i => 'x' + (i + 1)).join('+');
queryResult.textContent = i18nFmt('%mask% = %ans%', { mask: mask_label, ans: answer });
// add a line to the query history
const item = document.createElement('div');
item.className = 'hist-item';
item.textContent = i18nFmt('Q%n%: %mask% = %ans% (%before% → %after% candidates)', { n: queryCount, mask: mask_label, ans: answer, before: before, after: after });
history.prepend(item);
updateCandCount();
if (candidates.length === 1) {
revealed = true;
renderSecret();
status.textContent = i18nFmt('Reconstructed in %n% queries!', { n: queryCount });
status.className = 'status ok';
} else {
status.textContent = i18nFmt('%n% candidates remain.', { n: candidates.length });
status.className = 'status info';
}
}
function autoQuery() {
// choose the query that maximally splits the remaining candidates
// (greedy: minimise the largest bucket after the query)
if (candidates.length === 1) {
status.textContent = 'Secret already reconstructed.';
status.className = 'status ok';
return;
}
// try every non-empty subset as a potential query mask
let bestMask = null, bestScore = -1;
for (let mask = 1; mask < (1 << N); mask++) {
const idx = Array.from({ length: N }, (_, i) => (mask >> i) & 1 ? i : -1).filter(i => i >= 0);
// bucket candidates by their answer to this query
const buckets = {};
for (const v of candidates) {
const s = idx.reduce((a, i) => a + v[i], 0);
buckets[s] = (buckets[s] || 0) + 1;
}
const largest = Math.max(...Object.values(buckets));
const score = candidates.length - largest;
if (score > bestScore) { bestScore = score; bestMask = mask; }
}
for (let i = 0; i < N; i++) queryMask[i] = !!((bestMask >> i) & 1);
renderQueryBits();
askQuery();
}
function reset() {
// pick a fresh secret and reset all state
candidates = [];
for (let mask = 0; mask < (1 << N); mask++) {
const v = Array.from({ length: N }, (_, i) => (mask >> i) & 1);
candidates.push(v);
}
queryMask.fill(false);
revealed = false;
queryCount = 0;
queryResult.textContent = '';
history.innerHTML = '';
status.textContent = '';
status.className = 'status';
renderSecret();
renderQueryBits();
updateCandCount();
candCount.classList.remove('solved');
}
document.getElementById('reveal-btn').addEventListener('click', () => {
revealed = !revealed;
renderSecret();
document.getElementById('reveal-btn').textContent = revealed ? 'Hide secret' : 'Show secret';
});
document.getElementById('ask-btn').addEventListener('click', askQuery);
document.getElementById('auto-btn').addEventListener('click', autoQuery);
document.getElementById('reset-btn').addEventListener('click', reset);
renderSecret();
renderQueryBits();
updateCandCount();
<!-- reconstruction attack demo layout -->
<p class="hint">A secret 6-bit vector is hidden. Pick which positions to include in a <b>sum query</b>, then click Ask. Each exact answer eliminates candidates until only the secret remains — no hacking required, just linear algebra.</p>
<div class="panel">
<div class="secret-row">
<span class="label">Secret vector:</span>
<div id="secret-display" class="bit-row hidden-bits"></div>
<button id="reveal-btn" type="button" class="ghost small">Show secret</button>
</div>
<div class="candidates-row">
<span class="label">Remaining candidates:</span>
<span id="cand-count" class="count-badge">64</span>
</div>
</div>
<div class="query-panel">
<p class="query-label">Select positions to sum (x1 … x6):</p>
<div id="query-checkboxes" class="query-bits"></div>
<div class="query-row">
<button id="ask-btn" type="button">Ask query</button>
<span id="query-result" class="query-result"></span>
</div>
</div>
<div id="history" class="history"></div>
<div class="btns">
<button id="auto-btn" type="button">Best next query</button>
<button id="reset-btn" type="button" class="ghost">New secret</button>
</div>
<div id="status" class="status"></div>
/* styles for the reconstruction attack demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: .93rem; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .6rem; line-height: 1.4; }
.panel { background: #f0f4f8; border-radius: 10px; padding: .7rem 1rem; margin-bottom: .7rem; }
.secret-row, .candidates-row { display: flex; align-items: center; gap: .6rem; margin: .25rem 0; }
.label { font-size: .8rem; font-weight: 600; color: #555; min-width: 100px; }
.bit-row { display: flex; gap: 4px; }
.bit { width: 32px; height: 32px; border-radius: 6px; display: flex; align-items: center;
justify-content: center; font: 700 15px ui-monospace, monospace; }
.bit-0 { background: #d1e8ff; color: #1a5fa0; border: 1px solid #99c4ef; }
.bit-1 { background: #ffd6d6; color: #9b1c1c; border: 1px solid #f5a0a0; }
.bit-hidden { background: #c9ccd1; color: transparent; border: 1px solid #adb1b8; }
.count-badge { background: #1d3557; color: #fff; border-radius: 20px; padding: .1rem .6rem;
font-weight: 700; font-size: .9rem; transition: background .3s; }
.count-badge.solved { background: #0a7d33; }
.small { font-size: .78rem; padding: .2rem .55rem; }
.query-panel { border: 1px solid #cdd9e3; border-radius: 10px; padding: .7rem 1rem; margin-bottom: .6rem; }
.query-label { font-size: .82rem; font-weight: 600; color: #444; margin: 0 0 .4rem; }
.query-bits { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: .5rem; }
.qb { width: 38px; height: 38px; border-radius: 8px; border: 2px solid #cdd9e3;
background: #f5f7fa; cursor: pointer; display: flex; align-items: center;
justify-content: center; font: 700 13px ui-monospace, monospace; color: #555;
transition: all .1s; user-select: none; }
.qb.selected { background: #1d3557; color: #fff; border-color: #1d3557; }
.query-row { display: flex; align-items: center; gap: .6rem; }
.query-result { font-weight: 700; font-size: 1rem; color: #1d3557; }
.history { max-height: 120px; overflow-y: auto; margin-bottom: .5rem; }
.hist-item { font-size: .8rem; color: #444; padding: .1rem 0; border-bottom: 1px solid #eee; }
.hist-item:last-child { border-bottom: none; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .4rem; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.status { font-size: .95rem; font-weight: 600; min-height: 1.3em; }
.status.ok { color: #0a7d33; }
.status.info { color: #1a5fa0; }
// Reconstruction attack demo: a secret binary vector is chosen at random.
// Each linear query returns the exact sum of chosen positions.
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
const N = 6; // number of private bits
// the private database — one random bit per entry
const SECRET = Array.from({ length: N }, () => Math.random() < 0.5 ? 1 : 0);
// all 2^N possible vectors — start with full uncertainty
let candidates = [];
for (let mask = 0; mask < (1 << N); mask++) {
const v = Array.from({ length: N }, (_, i) => (mask >> i) & 1);
candidates.push(v);
}
// which positions the current query includes
const queryMask = Array(N).fill(false);
let revealed = false;
let queryCount = 0;
// DOM refs
const secretDisplay = document.getElementById('secret-display');
const candCount = document.getElementById('cand-count');
const queryCheckboxes = document.getElementById('query-checkboxes');
const queryResult = document.getElementById('query-result');
const history = document.getElementById('history');
const status = document.getElementById('status');
function renderSecret() {
// show each bit, or a question mark if still hidden
secretDisplay.innerHTML = '';
SECRET.forEach((b, i) => {
const d = document.createElement('div');
if (revealed) {
d.className = 'bit bit-' + b;
d.textContent = b;
} else {
d.className = 'bit bit-hidden';
d.textContent = '?';
}
secretDisplay.appendChild(d);
});
}
function renderQueryBits() {
// render the toggle buttons for building a query
queryCheckboxes.innerHTML = '';
for (let i = 0; i < N; i++) {
const d = document.createElement('div');
d.className = 'qb' + (queryMask[i] ? ' selected' : '');
d.textContent = 'x' + (i + 1);
d.title = i18nFmt('Include position %i% in the query sum', { i: i + 1 });
d.addEventListener('click', () => {
queryMask[i] = !queryMask[i];
renderQueryBits();
});
queryCheckboxes.appendChild(d);
}
}
function updateCandCount() {
// update the candidate counter and highlight when solved
candCount.textContent = candidates.length;
if (candidates.length === 1) {
candCount.classList.add('solved');
} else {
candCount.classList.remove('solved');
}
}
function askQuery() {
// ask the selected linear query and filter candidates
const indices = queryMask.map((on, i) => on ? i : -1).filter(i => i >= 0);
if (indices.length === 0) {
status.textContent = 'Select at least one position.';
status.className = 'status info';
return;
}
const answer = indices.reduce((s, i) => s + SECRET[i], 0);
queryCount++;
// keep only candidates consistent with the answer
const before = candidates.length;
candidates = candidates.filter(v => indices.reduce((s, i) => s + v[i], 0) === answer);
const after = candidates.length;
const mask_label = indices.map(i => 'x' + (i + 1)).join('+');
queryResult.textContent = i18nFmt('%mask% = %ans%', { mask: mask_label, ans: answer });
// add a line to the query history
const item = document.createElement('div');
item.className = 'hist-item';
item.textContent = i18nFmt('Q%n%: %mask% = %ans% (%before% → %after% candidates)', { n: queryCount, mask: mask_label, ans: answer, before: before, after: after });
history.prepend(item);
updateCandCount();
if (candidates.length === 1) {
revealed = true;
renderSecret();
status.textContent = i18nFmt('Reconstructed in %n% queries!', { n: queryCount });
status.className = 'status ok';
} else {
status.textContent = i18nFmt('%n% candidates remain.', { n: candidates.length });
status.className = 'status info';
}
}
function autoQuery() {
// choose the query that maximally splits the remaining candidates
// (greedy: minimise the largest bucket after the query)
if (candidates.length === 1) {
status.textContent = 'Secret already reconstructed.';
status.className = 'status ok';
return;
}
// try every non-empty subset as a potential query mask
let bestMask = null, bestScore = -1;
for (let mask = 1; mask < (1 << N); mask++) {
const idx = Array.from({ length: N }, (_, i) => (mask >> i) & 1 ? i : -1).filter(i => i >= 0);
// bucket candidates by their answer to this query
const buckets = {};
for (const v of candidates) {
const s = idx.reduce((a, i) => a + v[i], 0);
buckets[s] = (buckets[s] || 0) + 1;
}
const largest = Math.max(...Object.values(buckets));
const score = candidates.length - largest;
if (score > bestScore) { bestScore = score; bestMask = mask; }
}
for (let i = 0; i < N; i++) queryMask[i] = !!((bestMask >> i) & 1);
renderQueryBits();
askQuery();
}
function reset() {
// pick a fresh secret and reset all state
candidates = [];
for (let mask = 0; mask < (1 << N); mask++) {
const v = Array.from({ length: N }, (_, i) => (mask >> i) & 1);
candidates.push(v);
}
queryMask.fill(false);
revealed = false;
queryCount = 0;
queryResult.textContent = '';
history.innerHTML = '';
status.textContent = '';
status.className = 'status';
renderSecret();
renderQueryBits();
updateCandCount();
candCount.classList.remove('solved');
}
document.getElementById('reveal-btn').addEventListener('click', () => {
revealed = !revealed;
renderSecret();
document.getElementById('reveal-btn').textContent = revealed ? 'Hide secret' : 'Show secret';
});
document.getElementById('ask-btn').addEventListener('click', askQuery);
document.getElementById('auto-btn').addEventListener('click', autoQuery);
document.getElementById('reset-btn').addEventListener('click', reset);
renderSecret();
renderQueryBits();
updateCandCount();
Each query answer eliminates huge swaths of possibility. With enough queries the attacker is left with a single candidate — the original secret. Notice that even exact query answers are enough: you do not need to hack anything, just do linear algebra. This is why the US Census Bureau redesigned its disclosure-avoidance system in 2020 after researchers showed that published tables could reconstruct individual records.
Comments
Loading comments...