diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index aac00ca1ca..e0b8e8dd40 100644 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -222,6 +222,7 @@ "Subtract", "Multiply", "Divide", + "Extended GCD", "Mean", "Median", "Standard Deviation", diff --git a/src/core/lib/BigIntUtils.mjs b/src/core/lib/BigIntUtils.mjs new file mode 100644 index 0000000000..dce23f2670 --- /dev/null +++ b/src/core/lib/BigIntUtils.mjs @@ -0,0 +1,53 @@ +/** + * @author p-leriche [philip.leriche@cantab.net] + * @copyright Crown Copyright 2025 + * @license Apache-2.0 + */ + +import OperationError from "../errors/OperationError.mjs"; + +/** + * Number theory utilities used by cryptographic operations. + * + * Currently provides: + * - parseBigInt + * - Extended Euclidean Algorithm + * + * Additional algorithms may be added as required. + */ + +/** + * parseBigInt helper operation + */ +export function parseBigInt(value, param) { + const v = (value ?? "").trim(); + if (/^0x[0-9a-f]+$/i.test(v)) return BigInt(v); + if (/^[+-]?[0-9]+$/.test(v)) return BigInt(v); + throw new OperationError(param + " must be decimal or hex (0x...)"); +} + +/** + * Extended Euclidean Algorithm + * + * Returns [g, x, y] such that: + * a*x + b*y = g = gcd(a, b) + * + * (Uses an iterative algorithm to avoid possible stack overflow) + */ +export function egcd(a, b) { + let oldR = a, r = b; + let oldS = 1n, s = 0n; + let oldT = 0n, t = 1n; + + while (r !== 0n) { + const quotient = oldR / r; + + [oldR, r] = [r, oldR - quotient * r]; + [oldS, s] = [s, oldS - quotient * s]; + [oldT, t] = [t, oldT - quotient * t]; + } + + // oldR is the gcd + // oldS and oldT are the Bézout coefficients + return [oldR, oldS, oldT]; +} diff --git a/src/core/operations/ExtendedGCD.mjs b/src/core/operations/ExtendedGCD.mjs new file mode 100644 index 0000000000..88069c7459 --- /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 0000000000..aeefe5e4d2 --- /dev/null +++ b/tests/operations/tests/ExtendedGCD.mjs @@ -0,0 +1,126 @@ +/** + * 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: "Extended Euclidean Algorithm Results:\n" + + "=====================================\n\n" + + "gcd(a, b) = 1\n\n" + + "Béut coefficients:\n" + + " x = 4\n" + + " y = -1\n\n" + + "Verification:\n" + + " a·x + b·y = gcd(a, b)\n" + + " (3) ×(4) + (11) ×(-1) = 1", + recipeConfig: [ + { + op: "Extended GCD", + args: ["3", "11"], + }, + ], + }, + { + name: "Extended GCD: non-coprime numbers (240, 46)", + input: "", + expectedOutput: "Extended Euclidean Algorithm Results:\n" + + "=====================================\n\n" + + "gcd(a, b) = 2\n\n" + + "Béut coefficients:\n" + + " x = -9\n" + + " y = 47\n\n" + + "Verification:\n" + + " a·x + b·y = gcd(a, b)\n" + + " (240) ×(-9) + (46) ×(47) = 2", + recipeConfig: [ + { + op: "Extended GCD", + args: ["240", "46"], + }, + ], + }, + { + name: "Extended GCD: with zero (17, 0)", + input: "", + expectedOutput: "Extended Euclidean Algorithm Results:\n" + + "=====================================\n\n" + + "gcd(a, b) = 17\n\n" + + "Béut coefficients:\n" + + " x = 1\n" + + " y = 0\n\n" + + "Verification:\n" + + " a·x + b·y = gcd(a, b)\n" + + " (17) ×(1) + (0) ×(0) = 17", + recipeConfig: [ + { + op: "Extended GCD", + args: ["17", "0"], + }, + ], + }, + { + name: "Extended GCD: hexadecimal input (0xFF, 0x11)", + input: "", + expectedOutput: "Extended Euclidean Algorithm Results:\n" + + "=====================================\n\n" + + "gcd(a, b) = 17\n\n" + + "Béut coefficients:\n" + + " x = 1\n" + + " y = -15\n\n" + + "Verification:\n" + + " a·x + b·y = gcd(a, b)\n" + + " (255) ×(1) + (17) ×(-15) = 17", + recipeConfig: [ + { + op: "Extended GCD", + args: ["0xFF", "0x11"], + }, + ], + }, + { + name: "Extended GCD: using input field for value a", + input: "42", + expectedOutput: "Extended Euclidean Algorithm Results:\n" + + "=====================================\n\n" + + "gcd(a, b) = 7\n\n" + + "Béut coefficients:\n" + + " x = -2\n" + + " y = 3\n\n" + + "Verification:\n" + + " a·x + b·y = gcd(a, b)\n" + + " (42) ×(-2) + (35) ×(3) = 7", + recipeConfig: [ + { + op: "Extended GCD", + args: ["", "35"], + }, + ], + }, + { + name: "Extended GCD: large numbers", + input: "", + expectedOutput: "Extended Euclidean Algorithm Results:\n" + + "=====================================\n\n" + + "gcd(a, b) = 1\n\n" + + "Béut coefficients:\n" + + " x = -80538738812075595\n" + + " y = 10000000000000000\n\n" + + "Verification:\n" + + " a·x + b·y = gcd(a, b)\n" + + " (123456789012345678901234567890) ×(-80538738812075595) + (994064509324197316) ×(10000000000000000) = 1", + recipeConfig: [ + { + op: "Extended GCD", + args: ["123456789012345678901234567890", "994064509324197316"], + }, + ], + }, +]);