Below are two contact lists — Alice's phone and a server's user database. Neither side should see the other's full list. Click Run PSI to simulate the protocol: only the names that appear in both lists will be revealed.
<p class="hint">Alice's phone holds her contact list (left). The server knows its registered users (right). Neither side sees the other's full list — only the <b>intersection</b> is revealed after the PSI protocol runs.</p>
<div class="psi-layout">
<div class="panel" id="alice-panel">
<div class="panel-title">Alice's contacts</div>
<ul id="alice-list" class="contact-list"></ul>
</div>
<div class="panel result-panel">
<div class="panel-title">Common contacts</div>
<ul id="result-list" class="contact-list result"></ul>
<div class="status" id="status">Press Run PSI to start.</div>
</div>
<div class="panel" id="server-panel">
<div class="panel-title">Server users</div>
<ul id="server-list" class="contact-list"></ul>
</div>
</div>
<div class="btns">
<button id="run" type="button">Run PSI</button>
<button id="shuffle" type="button" class="ghost">Shuffle contacts</button>
</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 .9rem; line-height: 1.45; }
.psi-layout { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 10px; margin-bottom: .7rem; }
.panel { background: #f0f4f8; border: 1px solid #cdd9e3; border-radius: 10px; padding: .6rem .7rem; min-height: 240px; }
.panel-title { font-size: .78rem; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; color: #5a7088; margin-bottom: .5rem; }
.contact-list { list-style: none; margin: 0; padding: 0; }
.contact-list li { font-size: .88rem; padding: .28rem .45rem; border-radius: 6px; margin-bottom: 3px; background: #dce4ec; color: #1d3557; transition: background .25s, color .25s; }
.contact-list li.hit { background: #0a7d33; color: #fff; font-weight: 600; }
.contact-list li.miss { opacity: .4; }
.result-panel { background: #e6f4ec; border-color: #89c4a3; }
.contact-list.result li { background: #0a7d33; color: #fff; font-weight: 600; }
.contact-list.result li.empty { background: transparent; color: #888; font-style: italic; }
.status { font-size: .85rem; font-weight: 600; color: #5a7088; margin-top: .6rem; min-height: 1.3em; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
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] ?? ''));
// Private Set Intersection simulation.
// In a real PSI protocol (e.g. Diffie-Hellman blinding), neither party sends
// plaintext items — each side blinds its set with a secret exponent and only
// the double-blinded values are compared. Here we simulate the *outcome*:
// both sides learn only the intersection; neither learns the other's full list.
// The "protocol steps" log shows the cryptographic round-trips without
// revealing either party's private data.
const ALL_CONTACTS = [
'Alice Kim', 'Bob Leal', 'Carol Ng', 'David Obi', 'Eva Soto',
'Felix Wu', 'Grace Park', 'Hana Müller', 'Ivan Cruz', 'Julia Bose',
'Kai Ramos', 'Lena Diaz', 'Marco Voss', 'Nina Chen', 'Omar Farouk'
];
// Build two sets by randomly sampling from ALL_CONTACTS.
function sample(arr, n) {
const a = arr.slice().sort(() => Math.random() - .5);
return a.slice(0, n);
}
let aliceSet, serverSet;
function buildSets() {
aliceSet = sample(ALL_CONTACTS, 7);
// Server overlaps ~3-4 contacts with Alice
const shared = sample(aliceSet, Math.floor(Math.random() * 3) + 2);
const extra = sample(ALL_CONTACTS.filter(c => !aliceSet.includes(c)), 4);
serverSet = [...shared, ...extra].sort(() => Math.random() - .5);
}
// The actual intersection — this is what a real PSI protocol computes.
function computeIntersection(a, b) {
const bSet = new Set(b);
return a.filter(x => bSet.has(x));
}
function renderList(id, items, highlightSet, ran) {
const ul = document.getElementById(id);
ul.innerHTML = '';
items.forEach(name => {
const li = document.createElement('li');
li.textContent = name;
if (ran) li.className = highlightSet.has(name) ? 'hit' : 'miss';
ul.appendChild(li);
});
}
function renderResult(intersection) {
const ul = document.getElementById('result-list');
ul.innerHTML = '';
if (!intersection.length) {
const li = document.createElement('li');
li.className = 'empty'; li.textContent = 'No common contacts.';
ul.appendChild(li);
} else {
intersection.forEach(name => {
const li = document.createElement('li');
li.textContent = name;
ul.appendChild(li);
});
}
}
function render(ran) {
const intersection = computeIntersection(aliceSet, serverSet);
const iSet = new Set(intersection);
renderList('alice-list', aliceSet, iSet, ran);
renderList('server-list', serverSet, iSet, ran);
if (ran) {
renderResult(intersection);
const steps = aliceSet.length + serverSet.length + 2; // simulated round-trips
document.getElementById('status').textContent =
fmt(intersection.length === 1 ? '1 common contact found. Protocol used %steps% blinded comparisons — no raw names exchanged.' : '%n% common contacts found. Protocol used %steps% blinded comparisons — no raw names exchanged.', { n: intersection.length, steps: steps });
console.log('PSI intersection:', intersection.join(', '));
console.log('Alice set size:', aliceSet.length, '| Server set size:', serverSet.length);
} else {
document.getElementById('result-list').innerHTML = '';
document.getElementById('status').textContent = 'Press Run PSI to start.';
}
}
document.getElementById('run').addEventListener('click', () => render(true));
document.getElementById('shuffle').addEventListener('click', () => { buildSets(); render(false); });
buildSets();
render(false);
<p class="hint">Alice's phone holds her contact list (left). The server knows its registered users (right). Neither side sees the other's full list — only the <b>intersection</b> is revealed after the PSI protocol runs.</p>
<div class="psi-layout">
<div class="panel" id="alice-panel">
<div class="panel-title">Alice's contacts</div>
<ul id="alice-list" class="contact-list"></ul>
</div>
<div class="panel result-panel">
<div class="panel-title">Common contacts</div>
<ul id="result-list" class="contact-list result"></ul>
<div class="status" id="status">Press Run PSI to start.</div>
</div>
<div class="panel" id="server-panel">
<div class="panel-title">Server users</div>
<ul id="server-list" class="contact-list"></ul>
</div>
</div>
<div class="btns">
<button id="run" type="button">Run PSI</button>
<button id="shuffle" type="button" class="ghost">Shuffle contacts</button>
</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 .9rem; line-height: 1.45; }
.psi-layout { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 10px; margin-bottom: .7rem; }
.panel { background: #f0f4f8; border: 1px solid #cdd9e3; border-radius: 10px; padding: .6rem .7rem; min-height: 240px; }
.panel-title { font-size: .78rem; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; color: #5a7088; margin-bottom: .5rem; }
.contact-list { list-style: none; margin: 0; padding: 0; }
.contact-list li { font-size: .88rem; padding: .28rem .45rem; border-radius: 6px; margin-bottom: 3px; background: #dce4ec; color: #1d3557; transition: background .25s, color .25s; }
.contact-list li.hit { background: #0a7d33; color: #fff; font-weight: 600; }
.contact-list li.miss { opacity: .4; }
.result-panel { background: #e6f4ec; border-color: #89c4a3; }
.contact-list.result li { background: #0a7d33; color: #fff; font-weight: 600; }
.contact-list.result li.empty { background: transparent; color: #888; font-style: italic; }
.status { font-size: .85rem; font-weight: 600; color: #5a7088; margin-top: .6rem; min-height: 1.3em; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
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] ?? ''));
// Private Set Intersection simulation.
// In a real PSI protocol (e.g. Diffie-Hellman blinding), neither party sends
// plaintext items — each side blinds its set with a secret exponent and only
// the double-blinded values are compared. Here we simulate the *outcome*:
// both sides learn only the intersection; neither learns the other's full list.
// The "protocol steps" log shows the cryptographic round-trips without
// revealing either party's private data.
const ALL_CONTACTS = [
'Alice Kim', 'Bob Leal', 'Carol Ng', 'David Obi', 'Eva Soto',
'Felix Wu', 'Grace Park', 'Hana Müller', 'Ivan Cruz', 'Julia Bose',
'Kai Ramos', 'Lena Diaz', 'Marco Voss', 'Nina Chen', 'Omar Farouk'
];
// Build two sets by randomly sampling from ALL_CONTACTS.
function sample(arr, n) {
const a = arr.slice().sort(() => Math.random() - .5);
return a.slice(0, n);
}
let aliceSet, serverSet;
function buildSets() {
aliceSet = sample(ALL_CONTACTS, 7);
// Server overlaps ~3-4 contacts with Alice
const shared = sample(aliceSet, Math.floor(Math.random() * 3) + 2);
const extra = sample(ALL_CONTACTS.filter(c => !aliceSet.includes(c)), 4);
serverSet = [...shared, ...extra].sort(() => Math.random() - .5);
}
// The actual intersection — this is what a real PSI protocol computes.
function computeIntersection(a, b) {
const bSet = new Set(b);
return a.filter(x => bSet.has(x));
}
function renderList(id, items, highlightSet, ran) {
const ul = document.getElementById(id);
ul.innerHTML = '';
items.forEach(name => {
const li = document.createElement('li');
li.textContent = name;
if (ran) li.className = highlightSet.has(name) ? 'hit' : 'miss';
ul.appendChild(li);
});
}
function renderResult(intersection) {
const ul = document.getElementById('result-list');
ul.innerHTML = '';
if (!intersection.length) {
const li = document.createElement('li');
li.className = 'empty'; li.textContent = 'No common contacts.';
ul.appendChild(li);
} else {
intersection.forEach(name => {
const li = document.createElement('li');
li.textContent = name;
ul.appendChild(li);
});
}
}
function render(ran) {
const intersection = computeIntersection(aliceSet, serverSet);
const iSet = new Set(intersection);
renderList('alice-list', aliceSet, iSet, ran);
renderList('server-list', serverSet, iSet, ran);
if (ran) {
renderResult(intersection);
const steps = aliceSet.length + serverSet.length + 2; // simulated round-trips
document.getElementById('status').textContent =
fmt(intersection.length === 1 ? '1 common contact found. Protocol used %steps% blinded comparisons — no raw names exchanged.' : '%n% common contacts found. Protocol used %steps% blinded comparisons — no raw names exchanged.', { n: intersection.length, steps: steps });
console.log('PSI intersection:', intersection.join(', '));
console.log('Alice set size:', aliceSet.length, '| Server set size:', serverSet.length);
} else {
document.getElementById('result-list').innerHTML = '';
document.getElementById('status').textContent = 'Press Run PSI to start.';
}
}
document.getElementById('run').addEventListener('click', () => render(true));
document.getElementById('shuffle').addEventListener('click', () => { buildSets(); render(false); });
buildSets();
render(false);
Notice what each side learns: Alice discovers which of her contacts are already on the service. The server learns nothing about Alice's contacts that aren't registered. That asymmetry — full intersection, zero leakage — is what PSI guarantees. Press Shuffle contacts to try different lists and see how the intersection changes.
Comments
Loading comments...