diff --git a/lib/asymmetric/rsaKeyGenerationWizard.ts b/lib/asymmetric/rsaKeyGenerationWizard.ts index a9ee0c19..dad52ae3 100644 --- a/lib/asymmetric/rsaKeyGenerationWizard.ts +++ b/lib/asymmetric/rsaKeyGenerationWizard.ts @@ -16,6 +16,7 @@ export interface RsaWizardResult { input: RsaWizardInput; modulus: number; totient: number; + lambda: number; privateExponent: number; publicKey: string; privateKey: string; @@ -63,6 +64,11 @@ export function gcd(a: number, b: number): number { return x; } +export function lcm(a: number, b: number): number { + if (a === 0 || b === 0) return 0; + return Math.abs((a / gcd(a, b)) * b); +} + export function extendedGcd( a: number, b: number, @@ -84,7 +90,7 @@ export function modInverse(value: number, modulus: number): number { const result = extendedGcd(value, modulus); if (result.gcd !== 1) { - throw new Error("Public exponent must be coprime with φ(n)."); + throw new Error("Public exponent must be coprime with modulus."); } return ((result.x % modulus) + modulus) % modulus; @@ -121,18 +127,18 @@ export function validateRsaWizardInput(input: RsaWizardInput): RsaWizardInput { throw new Error("p and q must be different primes."); } - const totient = (primeP - 1) * (primeQ - 1); + const lambda = lcm(primeP - 1, primeQ - 1); if (!Number.isInteger(publicExponent) || publicExponent <= 1) { throw new Error("Public exponent e must be an integer greater than 1."); } - if (publicExponent >= totient) { - throw new Error("Public exponent e must be smaller than φ(n)."); + if (publicExponent >= lambda) { + throw new Error("Public exponent e must be smaller than λ(n)."); } - if (gcd(publicExponent, totient) !== 1) { - throw new Error("Public exponent e must be coprime with φ(n)."); + if (gcd(publicExponent, lambda) !== 1) { + throw new Error("Public exponent e must be coprime with λ(n)."); } return { primeP, primeQ, publicExponent }; @@ -143,7 +149,8 @@ export function generateRsaWizard(input: RsaWizardInput): RsaWizardResult { const { primeP, primeQ, publicExponent } = safeInput; const modulus = primeP * primeQ; const totient = (primeP - 1) * (primeQ - 1); - const privateExponent = modInverse(publicExponent, totient); + const lambda = lcm(primeP - 1, primeQ - 1); + const privateExponent = modInverse(publicExponent, lambda); const steps: RsaWizardStep[] = [ { @@ -164,27 +171,27 @@ export function generateRsaWizard(input: RsaWizardInput): RsaWizardResult { }, { id: "compute-totient", - title: "Compute Euler's totient", - formula: `φ(n) = (p - 1)(q - 1) = ${primeP - 1} × ${primeQ - 1}`, - result: `φ(n) = ${totient}`, + title: "Compute Carmichael's totient λ(n)", + formula: `λ(n) = lcm(p - 1, q - 1) = lcm(${primeP - 1}, ${primeQ - 1})`, + result: `λ(n) = ${lambda} (Euler's φ(n) = ${totient})`, explanation: - "Euler's totient φ(n) counts how many values below n are coprime with n. It connects public and private exponents following the original 1978 RSA paper. Note: Modern standards (RFC 8017 / PKCS#1 v2.2) prefer Carmichael's lambda λ(n) = lcm(p - 1, q - 1). Because λ(n) divides φ(n), both produce valid exponents satisfy e·d ≡ 1 (mod λ(n)), with λ(n) yielding the minimal valid d.", + "PKCS#1 v2.2 (RFC 8017) specifies Carmichael's lambda λ(n) = lcm(p - 1, q - 1) to derive the private exponent. While the original 1978 RSA paper used Euler's totient φ(n) = (p - 1)(q - 1), λ(n) yields the minimal private exponent d satisfying e · d ≡ 1 (mod λ(n)).", }, { id: "choose-exponent", title: "Choose the public exponent", - formula: `gcd(e, φ(n)) = gcd(${publicExponent}, ${totient})`, - result: `gcd = ${gcd(publicExponent, totient)}`, + formula: `gcd(e, λ(n)) = gcd(${publicExponent}, ${lambda})`, + result: `gcd = ${gcd(publicExponent, lambda)}`, explanation: - "The public exponent e must be coprime with φ(n), otherwise it will not have a valid modular inverse.", + "The public exponent e must be coprime with Carmichael's lambda λ(n), ensuring a valid modular inverse exists.", }, { id: "compute-private-exponent", title: "Compute the private exponent", - formula: `d ≡ e⁻¹ mod φ(n) = ${publicExponent}⁻¹ mod ${totient}`, + formula: `d ≡ e⁻¹ mod λ(n) = ${publicExponent}⁻¹ mod ${lambda}`, result: `d = ${privateExponent}`, explanation: - "The private exponent d reverses the public exponent under modular arithmetic. It must remain secret in real RSA systems.", + "The private exponent d is derived mod λ(n) in compliance with RFC 8017 / PKCS#1 v2.2. It reverses the public exponent transformation and must remain secret.", }, { id: "assemble-keys", @@ -200,13 +207,14 @@ export function generateRsaWizard(input: RsaWizardInput): RsaWizardResult { input: safeInput, modulus, totient, + lambda, privateExponent, publicKey: `(${modulus}, ${publicExponent})`, privateKey: `(${modulus}, ${privateExponent})`, steps, securityNotes: [ "This wizard uses small primes for education only.", - "This wizard computes d mod φ(n) per the original 1978 RSA paper. Modern RFC 8017 / PKCS#1 v2.2 implementations use Carmichael's lambda λ(n) = lcm(p - 1, q - 1), yielding smaller but equivalent private exponents.", + "This wizard derives d mod λ(n) = lcm(p - 1, q - 1) in accordance with PKCS#1 v2.2 (RFC 8017). This produces the minimal valid private exponent.", "Real RSA keys should be generated with audited cryptographic libraries.", "Production RSA commonly uses 2048-bit or larger moduli.", "Never reuse demo primes or private exponents for real security.", @@ -225,12 +233,12 @@ export function getRecommendedPublicExponents(totient: number): number[] { export function buildRsaWizardManualChecklist(): string[] { return [ "Open the RSA Key Generation Wizard page.", - "Confirm the default p=61, q=53, e=17 example generates n=3233 and d=2753.", - "Change p or q to another prime and confirm n, φ(n), and d update.", + "Confirm the default p=61, q=53, e=17 example generates n=3233, λ(n)=780, and d=413 (or d=2753 if using φ(n)=3000).", + "Change p or q to another prime and confirm n, λ(n), and d update.", "Enter a non-prime value and confirm a friendly validation error appears.", "Enter the same value for p and q and confirm validation prevents it.", - "Try an exponent that is not coprime with φ(n) and confirm an error appears.", + "Try an exponent that is not coprime with λ(n) and confirm an error appears.", "Click each step and confirm the formula, result, and explanation update.", "Resize to mobile width and confirm the wizard remains usable.", ]; -} +} \ No newline at end of file diff --git a/lib/cipher/asymmetric/rsa.ts b/lib/cipher/asymmetric/rsa.ts index 3315a0b0..14aee651 100644 --- a/lib/cipher/asymmetric/rsa.ts +++ b/lib/cipher/asymmetric/rsa.ts @@ -340,7 +340,7 @@ function parseRsaKey(keyStr: string, isPrivateKey: boolean): { n: bigint; e?: bi } } } catch {} - + const parts = cleanKey.split(/[\s,]+/).map(p => p.trim()).filter(Boolean) if (parts.length === 3) { let p: bigint diff --git a/lib/cipher/classical/four-square.ts b/lib/cipher/classical/four-square.ts index b57e21e2..4b093393 100644 --- a/lib/cipher/classical/four-square.ts +++ b/lib/cipher/classical/four-square.ts @@ -50,17 +50,21 @@ function parseKeys(key: string): { topRight: Grid; bottomLeft: Grid } { return { topRight: buildGrid(parts[0]), bottomLeft: buildGrid(parts[1]) } } -function prepareText(input: string): string { +function prepareText(input: string): { prepared: string; wasPadded: boolean } { let clean = input.toUpperCase().replace(/J/g, 'I').replace(/[^A-Z]/g, '') - if (clean.length === 0) return clean - if (clean.length % 2 !== 0) clean += 'X' - return clean + if (clean.length === 0) return { prepared: clean, wasPadded: false } + let wasPadded = false + if (clean.length % 2 !== 0) { + clean += 'X' + wasPadded = true + } + return { prepared: clean, wasPadded } } function fourSquareCore(input: string, key: string, decrypt: boolean, instrument: boolean): CipherResult { const start = performance.now() const { topRight, bottomLeft } = parseKeys(key) - const prepared = prepareText(input) + const { prepared, wasPadded } = prepareText(input) const steps: CipherStep[] = [] if (instrument) { @@ -111,7 +115,10 @@ function fourSquareCore(input: string, key: string, decrypt: boolean, instrument output, outputEncoding: 'utf8', steps, - metadata: METADATA, + metadata: { + ...METADATA, + paddedCharacters: wasPadded ? 1 : 0, + } as typeof METADATA & { paddedCharacters: number }, durationMs: performance.now() - start, } } diff --git a/lib/cipher/classical/hill.ts b/lib/cipher/classical/hill.ts index 53057cc8..63e0be2e 100644 --- a/lib/cipher/classical/hill.ts +++ b/lib/cipher/classical/hill.ts @@ -26,6 +26,18 @@ function mod(n: number, m: number): number { return ((n % m) + m) % m } +// Computes the greatest common divisor of two integers +function gcd(a: number, b: number): number { + let x = Math.abs(a) + let y = Math.abs(b) + while (y !== 0) { + const t = y + y = x % y + x = t + } + return x +} + // Extended Euclidean algorithm — returns [gcd, x] such that a*x + b*y = gcd function egcd(a: number, b: number): [number, number, number] { if (b === 0) return [a, 1, 0] @@ -34,8 +46,9 @@ function egcd(a: number, b: number): [number, number, number] { } function modInverse(a: number, m: number): number | null { - const [g, x] = egcd(mod(a, m), m) + const g = gcd(a, m) if (g !== 1) return null + const [, x] = egcd(mod(a, m), m) return mod(x, m) } @@ -58,6 +71,16 @@ export function parseHillKey(key: string): { matrix: Matrix2x2; det: number; det [v[2], v[3]], ] const det = mod(matrix[0][0] * matrix[1][1] - matrix[0][1] * matrix[1][0], 26) + + // Explicit coprimality check against modulus 26 + const commonFactor = gcd(det, 26) + if (commonFactor !== 1) { + throw new CipherError( + 'INVALID_KEY', + `Key matrix determinant (${det}) is not coprime with 26 (gcd(${det}, 26) = ${commonFactor}). Common factor '${commonFactor}' prevents matrix inversion mod 26. Choose a key whose determinant is coprime with 26 — try "HILL", "GYBN", or "PQRS".` + ) + } + const detInverse = modInverse(det, 26) if (detInverse === null) { throw new CipherError( @@ -175,4 +198,4 @@ export const TEST_VECTORS: TestVector[] = [ expected: 'WBDBQCWBVHYV', description: 'Multi-block vector (spaces stripped): "ATTACKATDAWN" -> 6 blocks', }, -] +] \ No newline at end of file