Files
krypto-teacher/app.js
2026-03-01 11:44:12 +01:00

249 lines
6.8 KiB
JavaScript

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();