Last active
April 29, 2026 15:37
-
-
Save bquast/a0636da5576e7372b28bcbec1fc25785 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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 — does not provide actual protection — Bastiaan Quast — bastiaanquast.com</p> | |
| </header> | |
| <main> | |
| <!-- KEY PARAMETERS --> | |
| <section> | |
| <p class="sec-label">01 — 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 · Q</p> | |
| <p class="val" id="out-n">14</p> | |
| </div> | |
| <div class="card-sunken"> | |
| <p class="lbl-sm">φ(n) = (P−1)(Q−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 — public exponent</p> | |
| <p class="val-inv" id="out-e">5</p> | |
| <p class="val-sm-inv" id="out-e-check">gcd(e,φ)=1 ✓</p> | |
| </div> | |
| <div class="card-inv"> | |
| <p class="lbl-sm-inv">d — private exponent</p> | |
| <p class="val-inv" id="out-d">5</p> | |
| <p class="val-sm-inv" id="out-d-check">e·d mod φ = 1 ✓</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- PLAINTEXTS --> | |
| <section> | |
| <p class="sec-label">02 — Plaintext inputs</p> | |
| <div class="g2" style="gap:24px;align-items:start"> | |
| <div class="g2"> | |
| <div> | |
| <label for="inp-m1">m₁</label> | |
| <input type="number" id="inp-m1" value="3" min="1"> | |
| </div> | |
| <div> | |
| <label for="inp-m2">m₂</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₁·m₂ 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² / decrypt: m = c<sup>d</sup> mod n</p> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- ENCRYPTION / DECRYPTION --> | |
| <section> | |
| <p class="sec-label">03 — Encryption & decryption</p> | |
| <div class="g3" style="gap:16px"> | |
| <div> | |
| <p class="lbl-sm" style="margin-bottom:12px">encrypt(m₁)</p> | |
| <div class="arrow-row"> | |
| <div class="card-sunken"> | |
| <p class="lbl-sm">c₁</p> | |
| <p class="val" id="out-c1">47</p> | |
| <p class="val-sm" id="out-c1f">3⁵ mod 196</p> | |
| </div> | |
| <span class="arr">→</span> | |
| <div class="card-sunken"> | |
| <p class="lbl-sm">decrypt(c₁)</p> | |
| <p class="val" id="out-dc1">3</p> | |
| <p class="val-sm" id="out-dc1f">47⁵ mod 14</p> | |
| </div> | |
| </div> | |
| </div> | |
| <div> | |
| <p class="lbl-sm" style="margin-bottom:12px">encrypt(m₂)</p> | |
| <div class="arrow-row"> | |
| <div class="card-sunken"> | |
| <p class="lbl-sm">c₂</p> | |
| <p class="val" id="out-c2">44</p> | |
| <p class="val-sm" id="out-c2f">4⁵ mod 196</p> | |
| </div> | |
| <span class="arr">→</span> | |
| <div class="card-sunken"> | |
| <p class="lbl-sm">decrypt(c₂)</p> | |
| <p class="val" id="out-dc2">4</p> | |
| <p class="val-sm" id="out-dc2f">44⁵ 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₁·c₂ mod n²</p> | |
| <p class="val-inv" id="out-cp">108</p> | |
| <p class="val-sm-inv" id="out-cpf">47·44 mod 196</p> | |
| </div> | |
| <span class="arr">→</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⁵ mod 14</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- RESULT --> | |
| <section> | |
| <p class="sec-label">04 — Result</p> | |
| <div class="prop-box"> | |
| decrypt(c₁ · c₂ mod n²) ≡ m₁ · m₂ (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₁·c₂ mod n²) = 12 = 3·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₁ or m₂ — only their encryptions. | |
| </p> | |
| <p id="warn-msg"><span class="chip chip-warn">warning</span> inputs should be < n for well-defined results</p> | |
| </div> | |
| </section> | |
| </main> | |
| <footer> | |
| <span>Bastiaan Quast — [email protected] — bastiaanquast.com</span> | |
| <span>Educational example — 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> |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
LIVE: https://bquast.github.io/RSA_homomorphic_demo/
Repo (same code, to deploy to gh-pages): https://github.com/bquast/RSA_homomorphic_demo