Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 29 additions & 21 deletions lib/asymmetric/rsaKeyGenerationWizard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export interface RsaWizardResult {
input: RsaWizardInput;
modulus: number;
totient: number;
lambda: number;
privateExponent: number;
publicKey: string;
privateKey: string;
Expand Down Expand Up @@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -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 };
Expand All @@ -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[] = [
{
Expand All @@ -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",
Expand All @@ -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.",
Expand All @@ -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.",
];
}
}
2 changes: 1 addition & 1 deletion lib/cipher/asymmetric/rsa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 13 additions & 6 deletions lib/cipher/classical/four-square.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
}
}
Expand Down
27 changes: 25 additions & 2 deletions lib/cipher/classical/hill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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)
}

Expand All @@ -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(
Expand Down Expand Up @@ -175,4 +198,4 @@ export const TEST_VECTORS: TestVector[] = [
expected: 'WBDBQCWBVHYV',
description: 'Multi-block vector (spaces stripped): "ATTACKATDAWN" -> 6 blocks',
},
]
]
Loading