From e5ee170e8d7c155f1f1afa2e12239fd9ece53612 Mon Sep 17 00:00:00 2001
From: p-leriche <7701190+p-leriche@users.noreply.github.com>
Date: Fri, 17 Jul 2026 17:24:44 +0100
Subject: [PATCH] Add Extended GCD operation (#2206)
---
src/core/config/Categories.json | 2 -
src/core/lib/BigIntUtils.mjs | 1 -
src/core/operations/ExtendedGCD.mjs | 101 +++++++++++++++++++++++++
tests/operations/tests/ExtendedGCD.mjs | 78 +++++++++++++++++++
4 files changed, 179 insertions(+), 3 deletions(-)
create mode 100644 src/core/operations/ExtendedGCD.mjs
create mode 100644 tests/operations/tests/ExtendedGCD.mjs
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"],
+ },
+ ],
+ },
+]);