commit 9f51a829b885c5beb35c745af5ebf92ca4fec07e Author: Felix Förtsch Date: Sun Mar 1 11:44:12 2026 +0100 sync current state diff --git a/app.js b/app.js new file mode 100644 index 0000000..d819412 --- /dev/null +++ b/app.js @@ -0,0 +1,248 @@ +function gcd(a, b) { + let x = Math.abs(a); + let y = Math.abs(b); + while (y !== 0) { + const t = y; + y = x % y; + x = t; + } + return x; +} + +function modPow(base, exp, mod) { + let result = 1; + let b = base % mod; + let e = exp; + while (e > 0) { + if (e % 2 === 1) { + result = (result * b) % mod; + } + b = (b * b) % mod; + e = Math.floor(e / 2); + } + return result; +} + +function modInverse(e, phi) { + let oldR = e; + let r = phi; + let oldS = 1; + let s = 0; + + while (r !== 0) { + const q = Math.floor(oldR / r); + [oldR, r] = [r, oldR - q * r]; + [oldS, s] = [s, oldS - q * s]; + } + + if (oldR !== 1) { + return null; + } + + return ((oldS % phi) + phi) % phi; +} + +function isPrime(n) { + if (n < 2) { + return false; + } + for (let i = 2; i * i <= n; i += 1) { + if (n % i === 0) { + return false; + } + } + return true; +} + +const translations = { + en: { + heroTag: "Cryptography Trainer", + heroTitle: "RSA Study Coach", + heroCopy: "Step through RSA from intuition to full key generation, then test yourself.", + languageLabel: "Language", + roadmapTitle: "Learning Path", + nextStep: "Mark step as understood", + labTitle: "RSA Lab", + runLab: "Run RSA walk-through", + randomExample: "Random small example", + quizTitle: "Quick Check", + quizIntro: "What must hold for a valid public exponent e?" + }, + de: { + heroTag: "Kryptografie-Trainer", + heroTitle: "RSA-Lerncoach", + heroCopy: "Gehe RSA Schritt für Schritt durch und überprüfe danach dein Verständnis.", + languageLabel: "Sprache", + roadmapTitle: "Lernpfad", + nextStep: "Schritt als verstanden markieren", + labTitle: "RSA-Labor", + runLab: "RSA-Ablauf starten", + randomExample: "Zufallsbeispiel", + quizTitle: "Kurztest", + quizIntro: "Was muss für einen gültigen öffentlichen Exponenten e gelten?" + }, + es: { + heroTag: "Entrenador de criptografía", + heroTitle: "Tutor de RSA", + heroCopy: "Avanza por RSA paso a paso y comprueba tu comprensión.", + languageLabel: "Idioma", + roadmapTitle: "Ruta de aprendizaje", + nextStep: "Marcar paso como entendido", + labTitle: "Laboratorio RSA", + runLab: "Ejecutar recorrido RSA", + randomExample: "Ejemplo aleatorio", + quizTitle: "Comprobación rápida", + quizIntro: "¿Qué debe cumplirse para un exponente público e válido?" + }, + fr: { + heroTag: "Entraîneur de cryptographie", + heroTitle: "Coach RSA", + heroCopy: "Parcours RSA étape par étape puis teste ta compréhension.", + languageLabel: "Langue", + roadmapTitle: "Parcours d'apprentissage", + nextStep: "Marquer l'étape comme comprise", + labTitle: "Laboratoire RSA", + runLab: "Lancer le parcours RSA", + randomExample: "Exemple aléatoire", + quizTitle: "Vérification rapide", + quizIntro: "Que faut-il pour qu'un exposant public e soit valide ?" + } +}; + +const steps = [ + "Why modular arithmetic allows reversible operations", + "How two primes p and q produce n = p * q", + "Why phi(n) = (p - 1)(q - 1) matters", + "How to choose e with gcd(e, phi(n)) = 1", + "How to compute d so e * d ≡ 1 (mod phi(n))", + "Why c = m^e mod n and m = c^d mod n work" +]; + +let activeStep = 0; + +const roadmapEl = document.getElementById("roadmap"); +const nextStepBtn = document.getElementById("nextStep"); +const runLabBtn = document.getElementById("runLab"); +const randomBtn = document.getElementById("randomSmall"); +const outputEl = document.getElementById("labOutput"); +const langEl = document.getElementById("lang"); +const quizFeedbackEl = document.getElementById("quizFeedback"); + +function renderRoadmap() { + roadmapEl.innerHTML = ""; + steps.forEach((step, index) => { + const li = document.createElement("li"); + li.textContent = step; + if (index < activeStep) { + li.classList.add("done"); + } + roadmapEl.appendChild(li); + }); +} + +function runLab() { + const p = Number(document.getElementById("p").value); + const q = Number(document.getElementById("q").value); + const e = Number(document.getElementById("e").value); + const m = Number(document.getElementById("m").value); + + const lines = []; + if (!isPrime(p) || !isPrime(q)) { + lines.push("Error: p and q must both be prime."); + outputEl.textContent = lines.join("\n"); + return; + } + + const n = p * q; + const phi = (p - 1) * (q - 1); + const g = gcd(e, phi); + lines.push(`1) Choose primes p=${p}, q=${q}`); + lines.push(`2) Compute n = p*q = ${n}`); + lines.push(`3) Compute phi(n) = (p-1)(q-1) = ${phi}`); + lines.push(`4) Check gcd(e, phi(n)): gcd(${e}, ${phi}) = ${g}`); + + if (g !== 1) { + lines.push("Invalid e: it must be coprime to phi(n). Try another e."); + outputEl.textContent = lines.join("\n"); + return; + } + + const d = modInverse(e, phi); + if (d === null) { + lines.push("Could not compute d (mod inverse does not exist)."); + outputEl.textContent = lines.join("\n"); + return; + } + + lines.push(`5) Compute private exponent d so e*d ≡ 1 mod phi(n): d = ${d}`); + if (m < 0 || m >= n) { + lines.push(`Message m must satisfy 0 <= m < n (${n}).`); + outputEl.textContent = lines.join("\n"); + return; + } + + const c = modPow(m, e, n); + const recovered = modPow(c, d, n); + lines.push(`6) Encrypt c = m^e mod n = ${m}^${e} mod ${n} = ${c}`); + lines.push(`7) Decrypt m = c^d mod n = ${c}^${d} mod ${n} = ${recovered}`); + lines.push(""); + lines.push(`Public key: (n=${n}, e=${e})`); + lines.push(`Private key: (n=${n}, d=${d})`); + lines.push(recovered === m ? "Result: success, decrypted message matches original." : "Result: mismatch."); + + outputEl.textContent = lines.join("\n"); +} + +function randomSmallExample() { + const primePairs = [ + [11, 13], + [17, 19], + [23, 29], + [31, 37] + ]; + const es = [3, 5, 7, 11, 17]; + const pair = primePairs[Math.floor(Math.random() * primePairs.length)]; + document.getElementById("p").value = pair[0]; + document.getElementById("q").value = pair[1]; + + const phi = (pair[0] - 1) * (pair[1] - 1); + const validE = es.find((candidate) => candidate < phi && gcd(candidate, phi) === 1) || 3; + document.getElementById("e").value = validE; + + const n = pair[0] * pair[1]; + document.getElementById("m").value = Math.floor(Math.random() * (n - 1)) + 1; + runLab(); +} + +function applyLanguage(lang) { + const map = translations[lang] || translations.en; + document.querySelectorAll("[data-i18n]").forEach((el) => { + const key = el.dataset.i18n; + if (map[key]) { + el.textContent = map[key]; + } + }); +} + +nextStepBtn.addEventListener("click", () => { + if (activeStep < steps.length) { + activeStep += 1; + renderRoadmap(); + } +}); + +runLabBtn.addEventListener("click", runLab); +randomBtn.addEventListener("click", randomSmallExample); +langEl.addEventListener("change", (event) => applyLanguage(event.target.value)); + +document.querySelectorAll(".quiz-option").forEach((button) => { + button.addEventListener("click", () => { + const isCorrect = button.dataset.correct === "true"; + quizFeedbackEl.textContent = isCorrect ? "Correct: e must be coprime to phi(n)." : "Not quite. Focus on coprimality with phi(n)."; + quizFeedbackEl.className = `feedback ${isCorrect ? "good" : "bad"}`; + }); +}); + +renderRoadmap(); +applyLanguage("en"); +runLab(); diff --git a/crypto01.pdf b/crypto01.pdf new file mode 100644 index 0000000..e8b7afa Binary files /dev/null and b/crypto01.pdf differ diff --git a/crypto02.pdf b/crypto02.pdf new file mode 100644 index 0000000..1152ffa Binary files /dev/null and b/crypto02.pdf differ diff --git a/crypto03.pdf b/crypto03.pdf new file mode 100644 index 0000000..dece363 Binary files /dev/null and b/crypto03.pdf differ diff --git a/crypto04.pdf b/crypto04.pdf new file mode 100644 index 0000000..0570f63 Binary files /dev/null and b/crypto04.pdf differ diff --git a/crypto05.pdf b/crypto05.pdf new file mode 100644 index 0000000..2dd7322 Binary files /dev/null and b/crypto05.pdf differ diff --git a/crypto06.pdf b/crypto06.pdf new file mode 100644 index 0000000..d82227e Binary files /dev/null and b/crypto06.pdf differ diff --git a/crypto07.pdf b/crypto07.pdf new file mode 100644 index 0000000..bc2a228 Binary files /dev/null and b/crypto07.pdf differ diff --git a/crypto08.pdf b/crypto08.pdf new file mode 100644 index 0000000..dd77b24 Binary files /dev/null and b/crypto08.pdf differ diff --git a/crypto09.pdf b/crypto09.pdf new file mode 100644 index 0000000..b6b1b12 Binary files /dev/null and b/crypto09.pdf differ diff --git a/crypto10.pdf b/crypto10.pdf new file mode 100644 index 0000000..cbd5161 Binary files /dev/null and b/crypto10.pdf differ diff --git a/crypto11.pdf b/crypto11.pdf new file mode 100644 index 0000000..e9530f6 Binary files /dev/null and b/crypto11.pdf differ diff --git a/crypto12.pdf b/crypto12.pdf new file mode 100644 index 0000000..655f524 Binary files /dev/null and b/crypto12.pdf differ diff --git a/crypto13.pdf b/crypto13.pdf new file mode 100644 index 0000000..6b06259 Binary files /dev/null and b/crypto13.pdf differ diff --git a/crypto14.pdf b/crypto14.pdf new file mode 100644 index 0000000..922f630 Binary files /dev/null and b/crypto14.pdf differ diff --git a/crypto15.pdf b/crypto15.pdf new file mode 100644 index 0000000..455613e Binary files /dev/null and b/crypto15.pdf differ diff --git a/exercise1.pdf b/exercise1.pdf new file mode 100644 index 0000000..6ec9d08 Binary files /dev/null and b/exercise1.pdf differ diff --git a/exercise10.pdf b/exercise10.pdf new file mode 100644 index 0000000..ddceac1 Binary files /dev/null and b/exercise10.pdf differ diff --git a/exercise11.pdf b/exercise11.pdf new file mode 100644 index 0000000..d964bf0 Binary files /dev/null and b/exercise11.pdf differ diff --git a/exercise2.pdf b/exercise2.pdf new file mode 100644 index 0000000..4001088 Binary files /dev/null and b/exercise2.pdf differ diff --git a/exercise3.pdf b/exercise3.pdf new file mode 100644 index 0000000..9484988 Binary files /dev/null and b/exercise3.pdf differ diff --git a/exercise4.pdf b/exercise4.pdf new file mode 100644 index 0000000..aaa011a Binary files /dev/null and b/exercise4.pdf differ diff --git a/exercise5.pdf b/exercise5.pdf new file mode 100644 index 0000000..31ee7e3 Binary files /dev/null and b/exercise5.pdf differ diff --git a/exercise6.pdf b/exercise6.pdf new file mode 100644 index 0000000..c3dcba8 Binary files /dev/null and b/exercise6.pdf differ diff --git a/exercise7.pdf b/exercise7.pdf new file mode 100644 index 0000000..d6ce2c3 Binary files /dev/null and b/exercise7.pdf differ diff --git a/exercise8.pdf b/exercise8.pdf new file mode 100644 index 0000000..c73129a Binary files /dev/null and b/exercise8.pdf differ diff --git a/exercise8_2_solution.pdf b/exercise8_2_solution.pdf new file mode 100644 index 0000000..4aff948 Binary files /dev/null and b/exercise8_2_solution.pdf differ diff --git a/exercise9.pdf b/exercise9.pdf new file mode 100644 index 0000000..4637f7e Binary files /dev/null and b/exercise9.pdf differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..65913b2 --- /dev/null +++ b/index.html @@ -0,0 +1,77 @@ + + + + + + RSA Study Coach + + + +
+
+

Cryptography Trainer

+

RSA Study Coach

+

+ Step through RSA from intuition to full key generation, then test yourself. +

+
+ + +
+
+
+ +
+
+

Learning Path

+
    + +
    + +
    +

    RSA Lab

    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    + + +
    +
    
    +		
    + +
    +

    Quick Check

    +

    What must hold for a valid public exponent e?

    +
    + + + +
    + +
    +
    + + + + diff --git a/rsa-core.mjs b/rsa-core.mjs new file mode 100644 index 0000000..7d06d9d --- /dev/null +++ b/rsa-core.mjs @@ -0,0 +1,55 @@ +export function gcd(a, b) { + let x = Math.abs(a); + let y = Math.abs(b); + while (y !== 0) { + const t = y; + y = x % y; + x = t; + } + return x; +} + +export function modPow(base, exp, mod) { + let result = 1; + let b = base % mod; + let e = exp; + while (e > 0) { + if (e % 2 === 1) { + result = (result * b) % mod; + } + b = (b * b) % mod; + e = Math.floor(e / 2); + } + return result; +} + +export function modInverse(e, phi) { + let oldR = e; + let r = phi; + let oldS = 1; + let s = 0; + + while (r !== 0) { + const q = Math.floor(oldR / r); + [oldR, r] = [r, oldR - q * r]; + [oldS, s] = [s, oldS - q * s]; + } + + if (oldR !== 1) { + return null; + } + + return ((oldS % phi) + phi) % phi; +} + +export function isPrime(n) { + if (n < 2) { + return false; + } + for (let i = 2; i * i <= n; i += 1) { + if (n % i === 0) { + return false; + } + } + return true; +} diff --git a/rsa-core.test.mjs b/rsa-core.test.mjs new file mode 100644 index 0000000..da5f25e --- /dev/null +++ b/rsa-core.test.mjs @@ -0,0 +1,33 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { gcd, isPrime, modInverse, modPow } from "./rsa-core.mjs"; + +test("gcd computes expected values", () => { + assert.equal(gcd(54, 24), 6); + assert.equal(gcd(17, 3120), 1); +}); + +test("modInverse finds RSA example inverse", () => { + assert.equal(modInverse(17, 3120), 2753); +}); + +test("modPow encrypt/decrypt roundtrip for classic RSA sample", () => { + const p = 61; + const q = 53; + const n = p * q; + const phi = (p - 1) * (q - 1); + const e = 17; + const d = modInverse(e, phi); + const m = 65; + assert.notEqual(d, null); + const c = modPow(m, e, n); + const recovered = modPow(c, d, n); + assert.equal(c, 2790); + assert.equal(recovered, m); +}); + +test("isPrime identifies primes and composites", () => { + assert.equal(isPrime(61), true); + assert.equal(isPrime(51), false); + assert.equal(isPrime(1), false); +}); diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..67f8aba --- /dev/null +++ b/styles.css @@ -0,0 +1,189 @@ +:root { + --bg: #f5f7fb; + --card: #ffffff; + --text: #1d1d1f; + --muted: #4d4d52; + --primary: #0a84ff; + --primary-strong: #0066cc; + --ok: #2e7d32; + --bad: #b00020; + --border: #d7dbe4; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: "SF Pro Text", "Avenir Next", system-ui, -apple-system, sans-serif; + background: radial-gradient(circle at 20% -10%, #deecff 0%, #f5f7fb 55%); + color: var(--text); + line-height: 1.5; +} + +.hero { + padding: 2rem 1rem 1rem; +} + +.hero-inner { + max-width: 980px; + margin: 0 auto; +} + +.eyebrow { + margin: 0; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--muted); + font-size: 0.78rem; +} + +h1 { + margin: 0.25rem 0; + font-size: clamp(1.8rem, 3vw, 2.8rem); +} + +.hero-copy { + margin: 0; + color: var(--muted); + max-width: 65ch; +} + +.toolbar { + display: flex; + gap: 0.75rem; + align-items: center; + margin-top: 1rem; +} + +.layout { + max-width: 980px; + margin: 0 auto; + padding: 0 1rem 2rem; + display: grid; + gap: 1rem; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); +} + +.card { + background: var(--card); + border: 1px solid var(--border); + border-radius: 18px; + padding: 1rem; + box-shadow: 0 10px 30px rgba(45, 65, 99, 0.08); +} + +h2 { + margin-top: 0; +} + +.roadmap { + padding-left: 1.25rem; + margin: 0 0 1rem; +} + +.roadmap li { + margin: 0.4rem 0; +} + +.roadmap li.done { + color: var(--ok); + font-weight: 600; +} + +.grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.75rem; +} + +label { + display: block; + font-size: 0.88rem; + font-weight: 600; + margin-bottom: 0.2rem; +} + +input, +select, +button { + font: inherit; +} + +input, +select { + width: 100%; + padding: 0.5rem; + border-radius: 10px; + border: 1px solid var(--border); + background: #fff; +} + +.actions { + display: flex; + gap: 0.6rem; + margin-top: 0.8rem; + flex-wrap: wrap; +} + +button { + padding: 0.55rem 0.8rem; + border: 1px solid var(--border); + background: #fff; + border-radius: 10px; + cursor: pointer; +} + +button:focus-visible, +input:focus-visible, +select:focus-visible { + outline: 2px solid var(--primary); + outline-offset: 2px; +} + +.primary { + background: var(--primary); + border-color: var(--primary); + color: #fff; +} + +.primary:hover { + background: var(--primary-strong); + border-color: var(--primary-strong); +} + +.output { + margin-top: 1rem; + padding: 0.8rem; + border-radius: 10px; + background: #f2f6ff; + min-height: 220px; + overflow-x: auto; + white-space: pre-wrap; +} + +.quiz-options { + display: grid; + gap: 0.5rem; +} + +.quiz-option { + text-align: left; +} + +.feedback.good { + color: var(--ok); + font-weight: 600; +} + +.feedback.bad { + color: var(--bad); + font-weight: 600; +} + +@media (max-width: 640px) { + .grid { + grid-template-columns: 1fr; + } +}