Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/core/config/Categories.json
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@
"Subtract",
"Multiply",
"Divide",
"Modular Inverse",
"Mean",
"Median",
"Standard Deviation",
Expand Down
53 changes: 53 additions & 0 deletions src/core/lib/BigIntUtils.mjs
Original file line number Diff line number Diff line change
@@ -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];
}
107 changes: 107 additions & 0 deletions src/core/operations/ModularInverse.mjs
Original file line number Diff line number Diff line change
@@ -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 <i>a</i> modulo <i>m</i>.<br><br>" +
"Finds <i>x</i> such that a*x = 1 (mod m).<br><br>" +
"<b>Input handling:</b> If either <i>a</i> or <i>m</i> 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;
78 changes: 78 additions & 0 deletions tests/operations/tests/ModularInverse.mjs
Original file line number Diff line number Diff line change
@@ -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: "38",
recipeConfig: [
{
op: "Modular Inverse",
args: ["5", ""],
},
],
},
{
name: "Modular Inverse: large number (RSA-like)",
input: "",
expectedOutput: "4969696969697",
recipeConfig: [
{
op: "Modular Inverse",
args: ["65537", "9999999999999"],
},
],
},
]);