Step through a full game between Alice and Bob. Every number is computed with exact big-integer arithmetic modulo a small prime, so you can follow the ciphertexts by eye. Next step locks the deck twice, deals two cards to each player and ends with a showdown in which both keys are revealed and everything is checked.
<p class="hint">Two players, no dealer, ten cards. Every number is real arithmetic modulo the 31-bit toy prime p = 2147483647 (real deployments use 2048 bits or more, never this).</p>
<label class="enc">Card encoding
<select id="enc" aria-label="Card encoding">
<option value="naive">Naive: card k is the number k + 2 (leaks)</option>
<option value="safe">Safe: card k is (k + 2)² mod p (quadratic residue)</option>
</select>
</label>
<div class="btns">
<button id="next" type="button">Next step</button>
<button id="reset" type="button" class="ghost">New game</button>
</div>
<div class="stage" id="stage"></div>
<div id="view"></div>
<div id="leak"></div>
:root { --fg: #1c2530; --bg: #ffffff; --muted: #55606c; --card: #f1f4f8; --line: #cdd6e0; --accent: #1d3557; --accent-fg: #ffffff; --good: #0a7d33; --bad: #c92f3c; --warn-bg: #fdecee; --ok-bg: #e7f6ec; }
@media (prefers-color-scheme: dark) {
:root { --fg: #e6ebf1; --bg: #161b22; --muted: #9aa7b6; --card: #222a34; --line: #3a4553; --accent: #7fb0e8; --accent-fg: #0d1117; --good: #4cc77a; --bad: #ff7b87; --warn-bg: #3a2125; --ok-bg: #1b3326; }
}
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: var(--fg); background: var(--bg); margin: 0; padding: 4px; }
.hint { font-size: .85rem; color: var(--muted); margin: 0 0 .6rem; line-height: 1.45; }
.enc { display: block; font-size: .85rem; font-weight: 600; margin-bottom: .5rem; }
select { display: block; width: 100%; margin-top: .25rem; font: 14px system-ui, sans-serif; padding: .4rem; color: var(--fg); background: var(--card); border: 1px solid var(--line); border-radius: 8px; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .6rem; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid var(--accent); background: var(--accent); color: var(--accent-fg); border-radius: 8px; cursor: pointer; }
button.ghost { background: transparent; color: var(--accent); }
button:disabled { opacity: .45; cursor: default; }
.stage { font-weight: 700; font-size: .95rem; margin: .2rem 0; }
.msg { font-size: .88rem; line-height: 1.45; margin: .2rem 0 .6rem; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(104px, 1fr)); gap: 6px; margin-bottom: .6rem; }
.chip { display: flex; flex-direction: column; gap: 2px; padding: 5px 7px; background: var(--card); border: 1px solid var(--line); border-radius: 8px; position: relative; min-width: 0; }
.chip b { font-size: .85rem; }
.chip code { font: 11px ui-monospace, monospace; color: var(--muted); overflow-wrap: anywhere; }
.chip i { position: absolute; top: 4px; right: 7px; font-style: normal; font-weight: 800; }
.qr { color: var(--good); }
.nr { color: var(--bad); }
table { border-collapse: collapse; width: 100%; font-size: .8rem; margin-bottom: .6rem; }
th, td { border-bottom: 1px solid var(--line); padding: 3px 4px; text-align: left; }
td code { font: 11px ui-monospace, monospace; overflow-wrap: anywhere; }
.keys { font: 11px ui-monospace, monospace; color: var(--muted); overflow-wrap: anywhere; margin: .2rem 0 .6rem; }
.verdict { padding: .5rem .7rem; border-radius: 8px; background: var(--ok-bg); color: var(--good); font-weight: 600; font-size: .88rem; margin-bottom: .6rem; }
.verdict.bad { background: var(--warn-bg); color: var(--bad); }
.leakbox { border: 1px dashed var(--line); border-radius: 10px; padding: .5rem .6rem; margin-top: .4rem; }
.leakbox h4 { margin: 0 0 .4rem; font-size: .88rem; }
.leakbox p { margin: 0; font-size: .85rem; line-height: 1.45; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
const P = 2147483647n;
const PM1 = P - 1n;
const NAMES = ['A♠', 'K♠', 'Q♠', 'J♠', '10♠', 'A♥', 'K♥', 'Q♥', 'J♥', '10♥'];
const modpow = (b, e, m) => { let r = 1n; b %= m; while (e > 0n) { if (e & 1n) r = r * b % m; b = b * b % m; e >>= 1n; } return r; };
const gcd = (a, b) => { while (b) { [a, b] = [b, a % b]; } return a; };
const modinv = (a, m) => {
let r0 = a, r1 = m, s0 = 1n, s1 = 0n;
while (r1) { const q = r0 / r1; [r0, r1] = [r1, r0 - q * r1]; [s0, s1] = [s1, s0 - q * s1]; }
return ((s0 % m) + m) % m;
};
const rnd = (n) => BigInt(Math.floor(Math.random() * Number(n)));
const makeKey = () => { for (;;) { const e = 3n + rnd(PM1 - 3n); if (gcd(e, PM1) === 1n) return { e, d: modinv(e, PM1) }; } };
const legendre = (x) => (modpow(x, (P - 1n) / 2n, P) === 1n ? 1 : -1);
const encode = (i, safe) => { const b = BigInt(i + 2); return safe ? b * b % P : b; };
const lock = (m, k) => modpow(m, k, P);
const shuffled = (n) => { const idx = Array.from({ length: n }, (_, i) => i); for (let i = n - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [idx[i], idx[j]] = [idx[j], idx[i]]; } return idx; };
let G = null;
const newGame = (safe) => {
G = { safe, step: 0, a: makeKey(), b: makeKey(), plain: NAMES.map((_, i) => encode(i, safe)) };
G.lookup = new Map(G.plain.map((v, i) => [v.toString(), i]));
};
const advance = () => {
if (G.step >= 4) return;
G.step++;
if (G.step === 1) {
G.permA = shuffled(10);
G.deckA = G.permA.map((i) => lock(G.plain[i], G.a.e));
} else if (G.step === 2) {
G.permB = shuffled(10);
G.deckAB = G.permB.map((i) => G.deckA[i]).map((c) => lock(c, G.b.e));
G.cardAt = G.permB.map((i) => G.permA[i]);
} else if (G.step === 3) {
G.deal = [];
for (let s = 0; s < 4; s++) {
const alice = s < 2;
const half = alice ? lock(G.deckAB[s], G.b.d) : lock(G.deckAB[s], G.a.d);
const fin = alice ? lock(half, G.a.d) : lock(half, G.b.d);
G.deal.push({ s, alice, half, fin, card: G.lookup.get(fin.toString()) });
}
} else if (G.step === 4) {
const seen = new Set();
G.reveal = G.deckAB.map((c) => {
const m = lock(lock(c, G.a.d), G.b.d);
const idx = G.lookup.has(m.toString()) ? G.lookup.get(m.toString()) : -1;
if (idx >= 0) seen.add(idx);
return idx;
});
const handsOk = G.deal.every((d) => G.reveal[d.s] === d.card);
G.verified = seen.size === 10 && handsOk && G.reveal.every((x, s) => x === G.cardAt[s]);
}
};
const chip = (nm, v, bit) => '<div class="chip"><b>' + nm + '</b><code>' + v + '</code>' + (bit === undefined ? '' : '<i class="' + (bit > 0 ? 'qr' : 'nr') + '">' + (bit > 0 ? '+' : '−') + '</i>') + '</div>';
const STAGES = ["Step 0 · Public deck", "Step 1 · Alice locks and shuffles", "Step 2 · Bob locks and shuffles", "Step 3 · Dealing the hands", "Step 4 · Showdown"];
const MSGS = ["The ten cards are public numbers below p. The sign shows each number's quadratic-residue bit (+ residue, − non-residue).", "Alice picks a secret exponent a, replaces every card m by m^a mod p and shuffles. Bob receives ten numbers he cannot map back to cards.", "Bob picks a secret exponent b, locks every number again with b and shuffles. Both locks are now on every card and nobody knows the order.", "Slots 1 and 2 go to Alice: Bob removes his lock, then Alice removes hers and reads the card. Slots 3 and 4 go to Bob: Alice removes her lock first. Each player ends up seeing only their own two cards.", "Both players reveal their keys. Anyone can now unlock all ten slots and check the deck and the hands."];
const who = (alice) => (alice ? "Alice" : "Bob");
function render() {
const g = G;
document.getElementById('stage').textContent = STAGES[g.step];
document.getElementById('next').disabled = g.step >= 4;
let h = '<p class="msg">' + MSGS[g.step] + '</p>';
const hidden = "secret";
const ka = g.step >= 4 ? g.a.e + ' / ' + g.a.d : (g.step >= 1 ? hidden : '–');
const kb = g.step >= 4 ? g.b.e + ' / ' + g.b.d : (g.step >= 2 ? hidden : '–');
h += '<p class="keys">' + i18nFmt("Alice a / a⁻¹ = %a% · Bob b / b⁻¹ = %b%", { a: ka, b: kb }) + '</p>';
if (g.step === 0) {
h += '<div class="grid">' + g.plain.map((v, i) => chip(NAMES[i], v, legendre(v))).join('') + '</div>';
} else if (g.step === 1) {
h += '<div class="grid">' + g.deckA.map((v, s) => chip('#' + (s + 1), v)).join('') + '</div>';
} else if (g.step === 2) {
h += '<div class="grid">' + g.deckAB.map((v, s) => chip('#' + (s + 1), v)).join('') + '</div>';
} else if (g.step === 3) {
h += '<table><tr><th>#</th><th>' + "Hand" + '</th><th>' + "After one unlock" + '</th><th>' + "Card" + '</th></tr>';
h += g.deal.map((d) => '<tr><td>' + (d.s + 1) + '</td><td>' + who(d.alice) + '</td><td><code>' + d.half + '</code></td><td><b>' + NAMES[d.card] + '</b></td></tr>').join('');
h += '</table>';
} else {
h += '<table><tr><th>#</th><th>' + "Doubly locked" + '</th><th>' + "Card" + '</th></tr>';
h += g.reveal.map((x, s) => '<tr><td>' + (s + 1) + '</td><td><code>' + g.deckAB[s] + '</code></td><td><b>' + (x >= 0 ? NAMES[x] : '?') + '</b></td></tr>').join('');
h += '</table>';
h += '<div class="verdict' + (g.verified ? '' : ' bad') + '">' + (g.verified ? "Verified: the ten slots decode to ten distinct valid cards and both hands match the deal." : "Verification failed: the deck or a hand does not match.") + '</div>';
}
document.getElementById('view').innerHTML = h;
renderLeak();
}
function renderLeak() {
const g = G;
const box = document.getElementById('leak');
if (g.step < 1) { box.innerHTML = ''; return; }
const bits = g.deckA.map((c) => legendre(c));
const q = bits.filter((b) => b > 0).length;
const n = 10 - q;
let h = '<div class="leakbox"><h4>' + "What Bob computes from Alice's locked deck" + '</h4>';
h += '<div class="grid">' + g.deckA.map((v, s) => chip('#' + (s + 1), v, bits[s])).join('') + '</div>';
if (q === 10 || n === 10) {
h += '<p>' + "Every slot is a residue (+). The bit is identical for all cards, so Bob learns nothing from it." + '</p>';
} else {
const match = bits.filter((b, s) => b === legendre(g.plain[g.permA[s]])).length;
const avg = ((q * q + n * n) / 10).toFixed(1);
h += '<p>' + i18nFmt("Bob raises each number to (p−1)/2 and reads the residue bit: %q% residues, %n% non-residues. The bit equals the bit of the hidden card in %m% of 10 slots, so a slot is narrowed to a class of cards (on average %c% of the 10).", { q, n, m: match, c: avg }) + '</p>';
}
box.innerHTML = h + '</div>';
}
const start = () => { newGame(document.getElementById('enc').value === 'safe'); render(); };
document.getElementById('next').addEventListener('click', () => { advance(); render(); });
document.getElementById('reset').addEventListener('click', start);
document.getElementById('enc').addEventListener('change', start);
start();
<p class="hint">Two players, no dealer, ten cards. Every number is real arithmetic modulo the 31-bit toy prime p = 2147483647 (real deployments use 2048 bits or more, never this).</p>
<label class="enc">Card encoding
<select id="enc" aria-label="Card encoding">
<option value="naive">Naive: card k is the number k + 2 (leaks)</option>
<option value="safe">Safe: card k is (k + 2)² mod p (quadratic residue)</option>
</select>
</label>
<div class="btns">
<button id="next" type="button">Next step</button>
<button id="reset" type="button" class="ghost">New game</button>
</div>
<div class="stage" id="stage"></div>
<div id="view"></div>
<div id="leak"></div>
:root { --fg: #1c2530; --bg: #ffffff; --muted: #55606c; --card: #f1f4f8; --line: #cdd6e0; --accent: #1d3557; --accent-fg: #ffffff; --good: #0a7d33; --bad: #c92f3c; --warn-bg: #fdecee; --ok-bg: #e7f6ec; }
@media (prefers-color-scheme: dark) {
:root { --fg: #e6ebf1; --bg: #161b22; --muted: #9aa7b6; --card: #222a34; --line: #3a4553; --accent: #7fb0e8; --accent-fg: #0d1117; --good: #4cc77a; --bad: #ff7b87; --warn-bg: #3a2125; --ok-bg: #1b3326; }
}
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: var(--fg); background: var(--bg); margin: 0; padding: 4px; }
.hint { font-size: .85rem; color: var(--muted); margin: 0 0 .6rem; line-height: 1.45; }
.enc { display: block; font-size: .85rem; font-weight: 600; margin-bottom: .5rem; }
select { display: block; width: 100%; margin-top: .25rem; font: 14px system-ui, sans-serif; padding: .4rem; color: var(--fg); background: var(--card); border: 1px solid var(--line); border-radius: 8px; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .6rem; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid var(--accent); background: var(--accent); color: var(--accent-fg); border-radius: 8px; cursor: pointer; }
button.ghost { background: transparent; color: var(--accent); }
button:disabled { opacity: .45; cursor: default; }
.stage { font-weight: 700; font-size: .95rem; margin: .2rem 0; }
.msg { font-size: .88rem; line-height: 1.45; margin: .2rem 0 .6rem; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(104px, 1fr)); gap: 6px; margin-bottom: .6rem; }
.chip { display: flex; flex-direction: column; gap: 2px; padding: 5px 7px; background: var(--card); border: 1px solid var(--line); border-radius: 8px; position: relative; min-width: 0; }
.chip b { font-size: .85rem; }
.chip code { font: 11px ui-monospace, monospace; color: var(--muted); overflow-wrap: anywhere; }
.chip i { position: absolute; top: 4px; right: 7px; font-style: normal; font-weight: 800; }
.qr { color: var(--good); }
.nr { color: var(--bad); }
table { border-collapse: collapse; width: 100%; font-size: .8rem; margin-bottom: .6rem; }
th, td { border-bottom: 1px solid var(--line); padding: 3px 4px; text-align: left; }
td code { font: 11px ui-monospace, monospace; overflow-wrap: anywhere; }
.keys { font: 11px ui-monospace, monospace; color: var(--muted); overflow-wrap: anywhere; margin: .2rem 0 .6rem; }
.verdict { padding: .5rem .7rem; border-radius: 8px; background: var(--ok-bg); color: var(--good); font-weight: 600; font-size: .88rem; margin-bottom: .6rem; }
.verdict.bad { background: var(--warn-bg); color: var(--bad); }
.leakbox { border: 1px dashed var(--line); border-radius: 10px; padding: .5rem .6rem; margin-top: .4rem; }
.leakbox h4 { margin: 0 0 .4rem; font-size: .88rem; }
.leakbox p { margin: 0; font-size: .85rem; line-height: 1.45; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
const P = 2147483647n;
const PM1 = P - 1n;
const NAMES = ['A♠', 'K♠', 'Q♠', 'J♠', '10♠', 'A♥', 'K♥', 'Q♥', 'J♥', '10♥'];
const modpow = (b, e, m) => { let r = 1n; b %= m; while (e > 0n) { if (e & 1n) r = r * b % m; b = b * b % m; e >>= 1n; } return r; };
const gcd = (a, b) => { while (b) { [a, b] = [b, a % b]; } return a; };
const modinv = (a, m) => {
let r0 = a, r1 = m, s0 = 1n, s1 = 0n;
while (r1) { const q = r0 / r1; [r0, r1] = [r1, r0 - q * r1]; [s0, s1] = [s1, s0 - q * s1]; }
return ((s0 % m) + m) % m;
};
const rnd = (n) => BigInt(Math.floor(Math.random() * Number(n)));
const makeKey = () => { for (;;) { const e = 3n + rnd(PM1 - 3n); if (gcd(e, PM1) === 1n) return { e, d: modinv(e, PM1) }; } };
const legendre = (x) => (modpow(x, (P - 1n) / 2n, P) === 1n ? 1 : -1);
const encode = (i, safe) => { const b = BigInt(i + 2); return safe ? b * b % P : b; };
const lock = (m, k) => modpow(m, k, P);
const shuffled = (n) => { const idx = Array.from({ length: n }, (_, i) => i); for (let i = n - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [idx[i], idx[j]] = [idx[j], idx[i]]; } return idx; };
let G = null;
const newGame = (safe) => {
G = { safe, step: 0, a: makeKey(), b: makeKey(), plain: NAMES.map((_, i) => encode(i, safe)) };
G.lookup = new Map(G.plain.map((v, i) => [v.toString(), i]));
};
const advance = () => {
if (G.step >= 4) return;
G.step++;
if (G.step === 1) {
G.permA = shuffled(10);
G.deckA = G.permA.map((i) => lock(G.plain[i], G.a.e));
} else if (G.step === 2) {
G.permB = shuffled(10);
G.deckAB = G.permB.map((i) => G.deckA[i]).map((c) => lock(c, G.b.e));
G.cardAt = G.permB.map((i) => G.permA[i]);
} else if (G.step === 3) {
G.deal = [];
for (let s = 0; s < 4; s++) {
const alice = s < 2;
const half = alice ? lock(G.deckAB[s], G.b.d) : lock(G.deckAB[s], G.a.d);
const fin = alice ? lock(half, G.a.d) : lock(half, G.b.d);
G.deal.push({ s, alice, half, fin, card: G.lookup.get(fin.toString()) });
}
} else if (G.step === 4) {
const seen = new Set();
G.reveal = G.deckAB.map((c) => {
const m = lock(lock(c, G.a.d), G.b.d);
const idx = G.lookup.has(m.toString()) ? G.lookup.get(m.toString()) : -1;
if (idx >= 0) seen.add(idx);
return idx;
});
const handsOk = G.deal.every((d) => G.reveal[d.s] === d.card);
G.verified = seen.size === 10 && handsOk && G.reveal.every((x, s) => x === G.cardAt[s]);
}
};
const chip = (nm, v, bit) => '<div class="chip"><b>' + nm + '</b><code>' + v + '</code>' + (bit === undefined ? '' : '<i class="' + (bit > 0 ? 'qr' : 'nr') + '">' + (bit > 0 ? '+' : '−') + '</i>') + '</div>';
const STAGES = ["Step 0 · Public deck", "Step 1 · Alice locks and shuffles", "Step 2 · Bob locks and shuffles", "Step 3 · Dealing the hands", "Step 4 · Showdown"];
const MSGS = ["The ten cards are public numbers below p. The sign shows each number's quadratic-residue bit (+ residue, − non-residue).", "Alice picks a secret exponent a, replaces every card m by m^a mod p and shuffles. Bob receives ten numbers he cannot map back to cards.", "Bob picks a secret exponent b, locks every number again with b and shuffles. Both locks are now on every card and nobody knows the order.", "Slots 1 and 2 go to Alice: Bob removes his lock, then Alice removes hers and reads the card. Slots 3 and 4 go to Bob: Alice removes her lock first. Each player ends up seeing only their own two cards.", "Both players reveal their keys. Anyone can now unlock all ten slots and check the deck and the hands."];
const who = (alice) => (alice ? "Alice" : "Bob");
function render() {
const g = G;
document.getElementById('stage').textContent = STAGES[g.step];
document.getElementById('next').disabled = g.step >= 4;
let h = '<p class="msg">' + MSGS[g.step] + '</p>';
const hidden = "secret";
const ka = g.step >= 4 ? g.a.e + ' / ' + g.a.d : (g.step >= 1 ? hidden : '–');
const kb = g.step >= 4 ? g.b.e + ' / ' + g.b.d : (g.step >= 2 ? hidden : '–');
h += '<p class="keys">' + i18nFmt("Alice a / a⁻¹ = %a% · Bob b / b⁻¹ = %b%", { a: ka, b: kb }) + '</p>';
if (g.step === 0) {
h += '<div class="grid">' + g.plain.map((v, i) => chip(NAMES[i], v, legendre(v))).join('') + '</div>';
} else if (g.step === 1) {
h += '<div class="grid">' + g.deckA.map((v, s) => chip('#' + (s + 1), v)).join('') + '</div>';
} else if (g.step === 2) {
h += '<div class="grid">' + g.deckAB.map((v, s) => chip('#' + (s + 1), v)).join('') + '</div>';
} else if (g.step === 3) {
h += '<table><tr><th>#</th><th>' + "Hand" + '</th><th>' + "After one unlock" + '</th><th>' + "Card" + '</th></tr>';
h += g.deal.map((d) => '<tr><td>' + (d.s + 1) + '</td><td>' + who(d.alice) + '</td><td><code>' + d.half + '</code></td><td><b>' + NAMES[d.card] + '</b></td></tr>').join('');
h += '</table>';
} else {
h += '<table><tr><th>#</th><th>' + "Doubly locked" + '</th><th>' + "Card" + '</th></tr>';
h += g.reveal.map((x, s) => '<tr><td>' + (s + 1) + '</td><td><code>' + g.deckAB[s] + '</code></td><td><b>' + (x >= 0 ? NAMES[x] : '?') + '</b></td></tr>').join('');
h += '</table>';
h += '<div class="verdict' + (g.verified ? '' : ' bad') + '">' + (g.verified ? "Verified: the ten slots decode to ten distinct valid cards and both hands match the deal." : "Verification failed: the deck or a hand does not match.") + '</div>';
}
document.getElementById('view').innerHTML = h;
renderLeak();
}
function renderLeak() {
const g = G;
const box = document.getElementById('leak');
if (g.step < 1) { box.innerHTML = ''; return; }
const bits = g.deckA.map((c) => legendre(c));
const q = bits.filter((b) => b > 0).length;
const n = 10 - q;
let h = '<div class="leakbox"><h4>' + "What Bob computes from Alice's locked deck" + '</h4>';
h += '<div class="grid">' + g.deckA.map((v, s) => chip('#' + (s + 1), v, bits[s])).join('') + '</div>';
if (q === 10 || n === 10) {
h += '<p>' + "Every slot is a residue (+). The bit is identical for all cards, so Bob learns nothing from it." + '</p>';
} else {
const match = bits.filter((b, s) => b === legendre(g.plain[g.permA[s]])).length;
const avg = ((q * q + n * n) / 10).toFixed(1);
h += '<p>' + i18nFmt("Bob raises each number to (p−1)/2 and reads the residue bit: %q% residues, %n% non-residues. The bit equals the bit of the hidden card in %m% of 10 slots, so a slot is narrowed to a class of cards (on average %c% of the 10).", { q, n, m: match, c: avg }) + '</p>';
}
box.innerHTML = h + '</div>';
}
const start = () => { newGame(document.getElementById('enc').value === 'safe'); render(); };
document.getElementById('next').addEventListener('click', () => { advance(); render(); });
document.getElementById('reset').addEventListener('click', start);
document.getElementById('enc').addEventListener('change', start);
start();
Now the interesting part. Leave the encoding on Naive, where card k is simply the number k+2, and press Next step once. Below the deck, the dashed box shows what Bob can compute from Alice's locked deck alone: the sign of each ciphertext is the same sign as the hidden card. Bob has learned one bit about every card without breaking any lock. Switch to Safe, where cards are encoded as squares modulo p, and the signs all collapse to the same value.
Comments
Loading comments...