Skip to content

Instantly share code, notes, and snippets.

@bquast
Last active April 29, 2026 15:37
Show Gist options
  • Select an option

  • Save bquast/a0636da5576e7372b28bcbec1fc25785 to your computer and use it in GitHub Desktop.

Select an option

Save bquast/a0636da5576e7372b28bcbec1fc25785 to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RSA — Multiplicative Homomorphism</title>
<link href="https://fonts.googleapis.com/css2?family=Archivo+Black&family=Work+Sans:wght@400;600&family=Space+Mono&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--black: #000;
--white: #fff;
--success: #008000;
--warning: #FFA500;
--error: #FF0000;
--surface-sunken: #F0F0F0;
--font-head: 'Archivo Black', sans-serif;
--font-body: 'Work Sans', sans-serif;
--font-mono: 'Space Mono', monospace;
}
body {
background: var(--white);
color: var(--black);
font-family: var(--font-body);
font-size: 16px;
line-height: 1.6;
min-height: 100vh;
}
/* LAYOUT */
header {
border-bottom: 5px solid var(--black);
padding: 40px;
}
header h1 {
font-family: var(--font-head);
font-size: 48px;
line-height: 1.05;
text-transform: uppercase;
}
header p {
font-family: var(--font-mono);
font-size: 13px;
margin-top: 8px;
color: #444;
}
main { padding: 40px; max-width: 1100px; }
/* SECTION LABELS */
.sec-label {
font-family: var(--font-head);
font-size: 12px;
text-transform: uppercase;
letter-spacing: 2px;
border-bottom: 3px solid var(--black);
padding-bottom: 6px;
margin-bottom: 20px;
}
/* CARDS */
.card {
border: 3px solid var(--black);
padding: 24px;
background: var(--white);
}
.card-inv {
border: 3px solid var(--black);
padding: 20px;
background: var(--black);
color: var(--white);
}
.card-sunken {
border: 3px solid var(--black);
padding: 16px 20px;
background: var(--surface-sunken);
}
/* GRIDS */
.g2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.g3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16px; }
.g4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; }
section { margin-bottom: 40px; }
/* INPUTS */
label {
display: block;
font-family: var(--font-head);
font-size: 12px;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 4px;
}
input[type=number] {
width: 100%;
font-family: var(--font-mono);
font-size: 20px;
padding: 10px 12px;
border: 3px solid var(--black);
background: var(--surface-sunken);
color: var(--black);
outline: none;
}
input[type=number]:focus { border-width: 5px; }
input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button { opacity: 1; }
/* MONO DISPLAY VALUES */
.val {
font-family: var(--font-mono);
font-size: 22px;
font-weight: 400;
line-height: 1.2;
word-break: break-all;
}
.val-sm {
font-family: var(--font-mono);
font-size: 14px;
color: #555;
margin-top: 4px;
line-height: 1.4;
}
.lbl-sm {
font-family: var(--font-head);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 6px;
}
.lbl-sm-inv {
font-family: var(--font-head);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 6px;
color: #aaa;
}
.val-inv { font-family: var(--font-mono); font-size: 22px; color: var(--white); }
.val-sm-inv { font-family: var(--font-mono); font-size: 13px; color: #888; margin-top: 4px; }
/* ARROW */
.arrow-row {
display: flex;
align-items: center;
gap: 12px;
}
.arrow-row > * { flex: 1; }
.arr {
flex: 0 0 auto !important;
font-family: var(--font-mono);
font-size: 20px;
font-weight: 700;
}
/* RESULT BAR */
.result-bar {
border: 5px solid var(--black);
padding: 24px 32px;
display: flex;
align-items: flex-start;
gap: 32px;
flex-wrap: wrap;
}
.result-bar.ok { border-color: var(--success); }
.result-bar.fail { border-color: var(--error); }
.chip {
font-family: var(--font-head);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 1px;
padding: 3px 10px;
border: 2px solid;
display: inline-block;
white-space: nowrap;
}
.chip-ok { color: var(--success); border-color: var(--success); }
.chip-fail { color: var(--error); border-color: var(--error); }
.chip-warn { color: var(--warning); border-color: var(--warning); }
.result-eq {
font-family: var(--font-mono);
font-size: 16px;
flex: 1;
min-width: 200px;
}
.result-note {
font-family: var(--font-body);
font-size: 13px;
color: #555;
margin-top: 10px;
width: 100%;
}
/* PROP BOX */
.prop-box {
border: 3px solid var(--black);
background: var(--black);
color: var(--white);
padding: 20px 24px;
font-family: var(--font-mono);
font-size: 15px;
margin-bottom: 16px;
}
/* WARN */
#warn-msg { display: none; margin-top: 8px; }
/* FOOTER */
footer {
border-top: 3px solid var(--black);
padding: 24px 40px;
font-family: var(--font-mono);
font-size: 12px;
color: #666;
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 8px;
margin-top: 40px;
}
</style>
</head>
<body>
<header>
<h1>RSA<br>Homomorphic<br>Property</h1>
<p>Educational demo &mdash; does not provide actual protection &mdash; Bastiaan Quast &mdash; bastiaanquast.com</p>
</header>
<main>
<!-- KEY PARAMETERS -->
<section>
<p class="sec-label">01 &mdash; Key parameters</p>
<div class="g2" style="gap:24px">
<div class="card">
<p class="sec-label" style="font-size:11px;margin-bottom:16px">Primes</p>
<div class="g2">
<div>
<label for="inp-p">P (prime)</label>
<input type="number" id="inp-p" value="2" min="2" max="97">
</div>
<div>
<label for="inp-q">Q (prime)</label>
<input type="number" id="inp-q" value="7" min="2" max="97">
</div>
</div>
</div>
<div>
<div class="g2" style="margin-bottom:16px">
<div class="card-sunken">
<p class="lbl-sm">n = P &middot; Q</p>
<p class="val" id="out-n">14</p>
</div>
<div class="card-sunken">
<p class="lbl-sm">&phi;(n) = (P&minus;1)(Q&minus;1)</p>
<p class="val" id="out-phi">6</p>
</div>
</div>
<div class="g2">
<div class="card-inv">
<p class="lbl-sm-inv">e &mdash; public exponent</p>
<p class="val-inv" id="out-e">5</p>
<p class="val-sm-inv" id="out-e-check">gcd(e,&phi;)=1 ✓</p>
</div>
<div class="card-inv">
<p class="lbl-sm-inv">d &mdash; private exponent</p>
<p class="val-inv" id="out-d">5</p>
<p class="val-sm-inv" id="out-d-check">e&middot;d mod &phi; = 1 ✓</p>
</div>
</div>
</div>
</div>
</section>
<!-- PLAINTEXTS -->
<section>
<p class="sec-label">02 &mdash; Plaintext inputs</p>
<div class="g2" style="gap:24px;align-items:start">
<div class="g2">
<div>
<label for="inp-m1">m&#8321;</label>
<input type="number" id="inp-m1" value="3" min="1">
</div>
<div>
<label for="inp-m2">m&#8322;</label>
<input type="number" id="inp-m2" value="4" min="1">
</div>
</div>
<div class="card" style="border-width:5px">
<p class="lbl-sm">plaintext product m&#8321;&middot;m&#8322; mod n</p>
<p class="val" style="font-size:32px" id="out-plainprod">12</p>
<p class="val-sm" style="margin-top:8px">encrypt: c = m<sup>e</sup> mod n&sup2; &nbsp;/&nbsp; decrypt: m = c<sup>d</sup> mod n</p>
</div>
</div>
</section>
<!-- ENCRYPTION / DECRYPTION -->
<section>
<p class="sec-label">03 &mdash; Encryption &amp; decryption</p>
<div class="g3" style="gap:16px">
<div>
<p class="lbl-sm" style="margin-bottom:12px">encrypt(m&#8321;)</p>
<div class="arrow-row">
<div class="card-sunken">
<p class="lbl-sm">c&#8321;</p>
<p class="val" id="out-c1">47</p>
<p class="val-sm" id="out-c1f">3&#8309; mod 196</p>
</div>
<span class="arr">&rarr;</span>
<div class="card-sunken">
<p class="lbl-sm">decrypt(c&#8321;)</p>
<p class="val" id="out-dc1">3</p>
<p class="val-sm" id="out-dc1f">47&#8309; mod 14</p>
</div>
</div>
</div>
<div>
<p class="lbl-sm" style="margin-bottom:12px">encrypt(m&#8322;)</p>
<div class="arrow-row">
<div class="card-sunken">
<p class="lbl-sm">c&#8322;</p>
<p class="val" id="out-c2">44</p>
<p class="val-sm" id="out-c2f">4&#8309; mod 196</p>
</div>
<span class="arr">&rarr;</span>
<div class="card-sunken">
<p class="lbl-sm">decrypt(c&#8322;)</p>
<p class="val" id="out-dc2">4</p>
<p class="val-sm" id="out-dc2f">44&#8309; mod 14</p>
</div>
</div>
</div>
<div>
<p class="lbl-sm" style="margin-bottom:12px">cipher product (no decryption)</p>
<div class="arrow-row">
<div class="card-inv" style="flex:1">
<p class="lbl-sm-inv">c&#8321;&middot;c&#8322; mod n&sup2;</p>
<p class="val-inv" id="out-cp">108</p>
<p class="val-sm-inv" id="out-cpf">47&middot;44 mod 196</p>
</div>
<span class="arr">&rarr;</span>
<div class="card-inv" style="flex:1">
<p class="lbl-sm-inv">decrypt</p>
<p class="val-inv" id="out-dcp">12</p>
<p class="val-sm-inv" id="out-dcpf">108&#8309; mod 14</p>
</div>
</div>
</div>
</div>
</section>
<!-- RESULT -->
<section>
<p class="sec-label">04 &mdash; Result</p>
<div class="prop-box">
decrypt(c&#8321; &middot; c&#8322; mod n&sup2;) &equiv; m&#8321; &middot; m&#8322; (mod n)
</div>
<div class="result-bar" id="result-bar">
<span id="result-badge" class="chip chip-ok">homomorphism holds</span>
<span class="result-eq" id="result-eq">decrypt(c&#8321;&middot;c&#8322; mod n&sup2;) = 12 = 3&middot;4 mod 14</span>
<p class="result-note">
RSA is multiplicatively homomorphic: multiplying two ciphertexts and decrypting yields the same result as multiplying the plaintexts directly. The server never sees m&#8321; or m&#8322; &mdash; only their encryptions.
</p>
<p id="warn-msg"><span class="chip chip-warn">warning</span>&nbsp; inputs should be &lt; n for well-defined results</p>
</div>
</section>
</main>
<footer>
<span>Bastiaan Quast &mdash; [email protected] &mdash; bastiaanquast.com</span>
<span>Educational example &mdash; does not provide actual cryptographic protection</span>
</footer>
<script>
function modpow(base, exp, mod) {
if (mod === 1n) return 0n;
let result = 1n;
base = base % mod;
while (exp > 0n) {
if (exp % 2n === 1n) result = result * base % mod;
exp = exp >> 1n;
base = base * base % mod;
}
return result;
}
function gcd(a, b) { return b === 0n ? a : gcd(b, a % b); }
function modInv(a, m) {
let [old_r, r] = [a, m];
let [old_s, s] = [1n, 0n];
while (r !== 0n) {
const q = old_r / r;
[old_r, r] = [r, old_r - q * r];
[old_s, s] = [s, old_s - q * s];
}
return ((old_s % m) + m) % m;
}
function findE(phi) {
for (let e = 3n; e < phi; e += 2n) if (gcd(e, phi) === 1n) return e;
return 3n;
}
function recalc() {
const P = BigInt(parseInt(document.getElementById('inp-p').value) || 2);
const Q = BigInt(parseInt(document.getElementById('inp-q').value) || 7);
const m1 = BigInt(parseInt(document.getElementById('inp-m1').value) || 3);
const m2 = BigInt(parseInt(document.getElementById('inp-m2').value) || 4);
const n = P * Q;
const phi = (P - 1n) * (Q - 1n);
const e = findE(phi);
let d;
try { d = modInv(e, phi); } catch { d = 1n; }
const n2 = n * n;
const c1 = modpow(m1, e, n2);
const c2 = modpow(m2, e, n2);
const cp = (c1 * c2) % n2;
const dc1 = modpow(c1, d, n);
const dc2 = modpow(c2, d, n);
const dcp = modpow(cp, d, n);
const plainprod = (m1 * m2) % n;
document.getElementById('out-n').textContent = n.toString();
document.getElementById('out-phi').textContent = phi.toString();
document.getElementById('out-e').textContent = e.toString();
document.getElementById('out-e-check').textContent = `gcd(${e}, ${phi}) = 1 \u2713`;
document.getElementById('out-d').textContent = d.toString();
document.getElementById('out-d-check').textContent = `e\u00b7d mod \u03c6 = ${(e*d)%phi} \u2713`;
document.getElementById('out-plainprod').textContent = plainprod.toString();
document.getElementById('out-c1').textContent = c1.toString();
document.getElementById('out-c1f').textContent = `${m1}^${e} mod ${n2}`;
document.getElementById('out-dc1').textContent = dc1.toString();
document.getElementById('out-dc1f').textContent = `${c1}^${d} mod ${n}`;
document.getElementById('out-c2').textContent = c2.toString();
document.getElementById('out-c2f').textContent = `${m2}^${e} mod ${n2}`;
document.getElementById('out-dc2').textContent = dc2.toString();
document.getElementById('out-dc2f').textContent = `${c2}^${d} mod ${n}`;
document.getElementById('out-cp').textContent = cp.toString();
document.getElementById('out-cpf').textContent = `${c1}\u00b7${c2} mod ${n2}`;
document.getElementById('out-dcp').textContent = dcp.toString();
document.getElementById('out-dcpf').textContent = `${cp}^${d} mod ${n}`;
const holds = dcp === plainprod;
const bar = document.getElementById('result-bar');
bar.className = 'result-bar ' + (holds ? 'ok' : 'fail');
const badge = document.getElementById('result-badge');
badge.textContent = holds ? 'homomorphism holds' : 'mismatch';
badge.className = 'chip ' + (holds ? 'chip-ok' : 'chip-fail');
document.getElementById('result-eq').textContent =
`decrypt(c\u2081\u00b7c\u2082 mod n\u00b2) = ${dcp} = ${m1}\u00b7${m2} mod ${n}`;
const warn = m1 >= n || m2 >= n;
document.getElementById('warn-msg').style.display = warn ? 'block' : 'none';
}
['inp-p','inp-q','inp-m1','inp-m2'].forEach(id =>
document.getElementById(id).addEventListener('input', recalc)
);
recalc();
</script>
</body>
</html>
@bquast
Copy link
Copy Markdown
Author

bquast commented Apr 29, 2026

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment