diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 6cf7e107..20846692 100644 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -243,8 +243,6 @@ "Subtract", "Multiply", "Divide", - "Modular Exponentiation", - "Modular Inverse", "Extended GCD", "Mean", "Median", diff --git a/src/core/lib/BigIntUtils.mjs b/src/core/lib/BigIntUtils.mjs index 5ddd8786..a7187e90 100644 --- a/src/core/lib/BigIntUtils.mjs +++ b/src/core/lib/BigIntUtils.mjs @@ -70,4 +70,3 @@ export function modPow(base, exponent, modulus) { return result; } - diff --git a/src/core/operations/ExtendedGCD.mjs b/src/core/operations/ExtendedGCD.mjs new file mode 100644 index 00000000..88069c74 --- /dev/null +++ b/src/core/operations/ExtendedGCD.mjs @@ -0,0 +1,101 @@ +/** + * @author p-leriche [philip.leriche@cantab.net] + * @copyright Crown Copyright 2025 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import OperationError from "../errors/OperationError.mjs"; +import { parseBigInt, egcd } from "../lib/BigIntUtils.mjs"; + +/* ---------- operation class ---------- */ + +/** + * Extended GCD operation + */ +class ExtendedGCD extends Operation { + /** + * ExtendedGCD constructor + */ + constructor() { + super(); + + this.name = "Extended GCD"; + this.module = "Crypto"; + this.description = + "Computes the Extended Euclidean Algorithm for integers a and b.

" + + "Finds integers x and y (Bezout coefficients) such that:
" + + "a*x + b*y = gcd(a, b)

" + + "This is fundamental to many number theory algorithms including modular inverse, " + + "solving linear Diophantine equations, and cryptographic operations.

" + + "Input handling: If either a or b is left blank, " + + "its value is taken from the Input field."; + this.infoURL = "https://wikipedia.org/wiki/Extended_Euclidean_algorithm"; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { + name: "Value a", + type: "string", + value: "" + }, + { + name: "Value b", + type: "string", + value: "" + } + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + const [aStr, bStr] = args; + + // Trim everything so "" and " " count as empty + const aParam = aStr?.trim(); + const bParam = bStr?.trim(); + const inputVal = input?.trim(); + + let a, b; + + if (aParam && bParam) { + // Case 1: both values given as parameters + a = aParam; + b = bParam; + } else if (!aParam && bParam) { + // Case 2: a missing - take from input + a = inputVal; + b = bParam; + if (!a) throw new OperationError("Value a must be defined"); + } else if (aParam && !bParam) { + // Case 3: b missing - take from input + a = aParam; + b = inputVal; + if (!b) throw new OperationError("Value b must be defined"); + } else if (!aParam && !bParam) { + // Case 4: both values missing + throw new OperationError("Values a and b must be defined"); + } + + const aBI = parseBigInt(a, "Value a"); + const bBI = parseBigInt(b, "Value b"); + + const [g, x, y] = egcd(aBI, bBI); + const gcd = g < 0n ? -g : g; + + // Format output string bearing in mind that crypto-grade numbers + // may greatly exceed the line length. + let output = "gcd: " + gcd.toString() + "\n\n"; + output += "Bezout coefficients:\n"; + output += "x = " + x.toString() + "\n"; + output += "y = " + y.toString() + "\n\n"; + + return output; + } +} + +export default ExtendedGCD; diff --git a/tests/operations/tests/ExtendedGCD.mjs b/tests/operations/tests/ExtendedGCD.mjs new file mode 100644 index 00000000..fcc195ca --- /dev/null +++ b/tests/operations/tests/ExtendedGCD.mjs @@ -0,0 +1,78 @@ +/** + * Extended GCD tests. + * + * @author p-leriche [philip.leriche@cantab.net] + * + * @copyright Crown Copyright 2025 + * @license Apache-2.0 + */ +import TestRegister from "../../lib/TestRegister.mjs"; + +TestRegister.addTests([ + { + name: "Extended GCD: coprime numbers (3, 11)", + input: "", + expectedOutput: "gcd: 1\n\nBezout coefficients:\nx = 4\ny = -1\n\n", + recipeConfig: [ + { + op: "Extended GCD", + args: ["3", "11"], + }, + ], + }, + { + name: "Extended GCD: non-coprime numbers (240, 46)", + input: "", + expectedOutput: "gcd: 2\n\nBezout coefficients:\nx = -9\ny = 47\n\n", + recipeConfig: [ + { + op: "Extended GCD", + args: ["240", "46"], + }, + ], + }, + { + name: "Extended GCD: with zero (17, 0)", + input: "", + expectedOutput: "gcd: 17\n\nBezout coefficients:\nx = 1\ny = 0\n\n", + recipeConfig: [ + { + op: "Extended GCD", + args: ["17", "0"], + }, + ], + }, + { + name: "Extended GCD: hexadecimal input (0xFF, 0x11)", + input: "", + expectedOutput: "gcd: 17\n\nBezout coefficients:\nx = 0\ny = 1\n\n", + recipeConfig: [ + { + op: "Extended GCD", + args: ["0xFF", "0x11"], + }, + ], + }, + { + name: "Extended GCD: using input field for value a", + input: "42", + expectedOutput: "gcd: 7\n\nBezout coefficients:\nx = 1\ny = -1\n\n", + recipeConfig: [ + { + op: "Extended GCD", + args: ["", "35"], + }, + ], + }, + { + name: "Extended GCD: large numbers", + input: "", + expectedOutput: "gcd: 2\n\nBezout coefficients:\nx = 12703973750415151\ny = -1577756566311408967124629843\n\n", + recipeConfig: [ + { + op: "Extended GCD", + args: ["123456789012345678901234567890", "994064509324197316"], + }, + ], + }, +]);