249 lines
6.8 KiB
JavaScript
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();
|