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"], + }, + ], + }, +]);