diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json
index 20846692..a9d54d7b 100644
--- a/src/core/config/Categories.json
+++ b/src/core/config/Categories.json
@@ -244,6 +244,7 @@
"Multiply",
"Divide",
"Extended GCD",
+ "Modular Inverse",
"Mean",
"Median",
"Standard Deviation",
diff --git a/src/core/operations/ModularInverse.mjs b/src/core/operations/ModularInverse.mjs
new file mode 100644
index 00000000..88f4bff7
--- /dev/null
+++ b/src/core/operations/ModularInverse.mjs
@@ -0,0 +1,107 @@
+/**
+ * @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 ---------- */
+
+/**
+ * Modular Inverse operation
+ */
+class ModularInverse extends Operation {
+
+ /**
+ * ModularInverse constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "Modular Inverse";
+ this.module = "Crypto";
+ this.description =
+ "Computes the modular multiplicative inverse of a modulo m.
" +
+ "Finds x such that a*x = 1 (mod m).
" +
+ "Input handling: If either a or m is left blank, " +
+ "its value is taken from the Input field.";
+ this.infoURL = "https://wikipedia.org/wiki/Modular_multiplicative_inverse";
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [
+ {
+ name: "Value (a)",
+ type: "string",
+ value: ""
+ },
+ {
+ name: "Modulus (m)",
+ type: "string",
+ value: ""
+ }
+ ];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ const [aStr, mStr] = args;
+
+ // Trim everything so "" and " " count as empty
+ const aParam = aStr?.trim();
+ const mParam = mStr?.trim();
+ const inputVal = input?.trim();
+
+ let a, m;
+
+ if (aParam && mParam) {
+ // Case 1: value and modulus both given as parameters
+ a = aParam;
+ m = mParam;
+ } else if (!aParam && mParam) {
+ // Case 2: value missing - take from input
+ a = inputVal;
+ m = mParam;
+ if (!a) throw new OperationError("Value (a) must be defined");
+ } else if (aParam && !mParam) {
+ // Case 3: modulus missing - take from input
+ a = aParam;
+ m = inputVal;
+ if (!m) throw new OperationError("Modulus (m) must be defined");
+ } else if (!aParam && !mParam) {
+ // Case 4: value and modulus both missing
+ throw new OperationError("Value (a) and Modulus (m) must be defined");
+ }
+
+ const aBI = parseBigInt(a, "Value (a)");
+ const mBI = parseBigInt(m, "Modulus (m)");
+
+ if (mBI <= 0n) {
+ throw new OperationError("Modulus must be greater than zero");
+ }
+
+ const aNorm = ((aBI % mBI) + mBI) % mBI;
+ const [g, x] = egcd(aNorm, mBI);
+
+ if (g !== 1n && g !== -1n) {
+ throw new OperationError("Inverse does not exist because gcd(a, m) ≠ 1");
+ }
+
+ let inv = x;
+ if (g === -1n) inv = -inv;
+
+ inv = ((inv % mBI) + mBI) % mBI;
+
+
+ return inv.toString();
+ }
+}
+
+export default ModularInverse;
diff --git a/tests/operations/tests/ModularInverse.mjs b/tests/operations/tests/ModularInverse.mjs
new file mode 100644
index 00000000..53cb46a0
--- /dev/null
+++ b/tests/operations/tests/ModularInverse.mjs
@@ -0,0 +1,78 @@
+/**
+ * Modular Inverse 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: "Modular Inverse: basic example (3 mod 11)",
+ input: "",
+ expectedOutput: "4",
+ recipeConfig: [
+ {
+ op: "Modular Inverse",
+ args: ["3", "11"],
+ },
+ ],
+ },
+ {
+ name: "Modular Inverse: another coprime pair (7 mod 26)",
+ input: "",
+ expectedOutput: "15",
+ recipeConfig: [
+ {
+ op: "Modular Inverse",
+ args: ["7", "26"],
+ },
+ ],
+ },
+ {
+ name: "Modular Inverse: hexadecimal input (0x10 mod 0x11)",
+ input: "",
+ expectedOutput: "16",
+ recipeConfig: [
+ {
+ op: "Modular Inverse",
+ args: ["0x10", "0x11"],
+ },
+ ],
+ },
+ {
+ name: "Modular Inverse: using input field for value",
+ input: "5",
+ expectedOutput: "21",
+ recipeConfig: [
+ {
+ op: "Modular Inverse",
+ args: ["", "26"],
+ },
+ ],
+ },
+ {
+ name: "Modular Inverse: using input field for modulus",
+ input: "17",
+ expectedOutput: "7",
+ recipeConfig: [
+ {
+ op: "Modular Inverse",
+ args: ["5", ""],
+ },
+ ],
+ },
+ {
+ name: "Modular Inverse: large number (RSA-like)",
+ input: "",
+ expectedOutput: "934281398294",
+ recipeConfig: [
+ {
+ op: "Modular Inverse",
+ args: ["65537", "9999999999999"],
+ },
+ ],
+ },
+]);