diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml
index 8a3aff54..a45c6a9c 100644
--- a/.github/workflows/master.yml
+++ b/.github/workflows/master.yml
@@ -15,7 +15,7 @@ jobs:
- name: Set node version
uses: actions/setup-node@v3
with:
- node-version: '18.x'
+ node-version: '20.x'
- name: Install
run: |
diff --git a/src/core/Utils.mjs b/src/core/Utils.mjs
index a9c381d7..08eadbf1 100755
--- a/src/core/Utils.mjs
+++ b/src/core/Utils.mjs
@@ -1266,19 +1266,26 @@ class Utils {
/**
* Finds the modular inverse of two values.
+ * Uses the Extended Euclidean Algorithm.
*
- * @author Matt C [matt@artemisbot.uk]
- * @param {number} x
- * @param {number} y
- * @returns {number}
+ * @author Barry B [profbbrown@gmail.com]
+ * @param {number} a
+ * @param {number} n
+ * @returns {number|null}
*/
- static modInv(x, y) {
- x %= y;
- for (let i = 1; i < y; i++) {
- if ((x * i) % 26 === 1) {
- return i;
- }
+ static modInv(a, n) {
+ let t = 0, newT = 1, r = n, newR = a;
+
+ while (newR !== 0) {
+ const q = Math.floor(r / newR);
+ [t, newT] = [newT, t - q * newT];
+ [r, newR] = [newR, r - q * newR];
}
+
+ if (r > 1) return null;
+ if (t < 0) t = t + n;
+
+ return t;
}
diff --git a/src/core/lib/Ciphers.mjs b/src/core/lib/Ciphers.mjs
index 6266a8e1..9b9d4a7a 100644
--- a/src/core/lib/Ciphers.mjs
+++ b/src/core/lib/Ciphers.mjs
@@ -4,6 +4,7 @@
* @author Matt C [matt@artemisbot.uk]
* @author n1474335 [n1474335@gmail.com]
* @author Evie H [evie@evie.sh]
+ * @author Barry B [profbbrown@gmail.com]
*
* @copyright Crown Copyright 2018
* @license Apache-2.0
@@ -17,6 +18,7 @@ import CryptoJS from "crypto-js";
/**
* Affine Cipher Encode operation.
*
+ * @deprecated Use affineEcrypt instead.
* @author Matt C [matt@artemisbot.uk]
* @param {string} input
* @param {Object[]} args
@@ -51,6 +53,166 @@ export function affineEncode(input, args) {
return output;
}
+/**
+ * Generic affine encrypt/decrypt operation.
+ * Allows for an expanded alphabet.
+ *
+ * @author Barry B [profbbrown@gmail.com]
+ * @param {string} input
+ * @param {number} a
+ * @param {number} b
+ * @param {string} alphabet
+ * @param {function} affineFn
+ * @returns {string}
+ */
+export function affineApplication(input, a, b, alphabet, affineFn) {
+ if (alphabet === "")
+ throw new OperationError("The alphabet cannot be empty.");
+
+ alphabet = Utils.expandAlphRange(alphabet);
+ let output = "";
+ const modulus = alphabet.length;
+
+ // If the alphabet contains letters of all the same case,
+ // the assumption will be to match case.
+ const hasLower = /[a-z]/.test(alphabet);
+ const hasUpper = /[A-Z]/.test(alphabet);
+ const matchCase = (hasLower && hasUpper) ? false : true;
+
+ // If we are matching case, convert entire alphabet to lowercase.
+ // This will simplify the encryption.
+ if (matchCase)
+ alphabet = alphabet.map((c) => c.toLowerCase());
+
+ if (a === undefined || a === "" || isNaN(a)) a = 1;
+ if (b === undefined || b === "" || isNaN(b)) b = 0;
+
+ if (!/^\+?(0|[1-9]\d*)$/.test(a) || !/^\+?(0|[1-9]\d*)$/.test(b)) {
+ throw new OperationError("The values of a and b can only be integers.");
+ }
+
+ if (Utils.gcd(a, modulus) !== 1) {
+ throw new OperationError("The value of `a` (" + a + ") must be coprime to " + modulus + ".");
+ }
+
+ // Apply affine function to each character in the input
+ for (let i = 0; i < input.length; i++) {
+ let outChar = "";
+
+ let inChar = input[i];
+ if (matchCase && isUpperCase(inChar)) inChar = inChar.toLowerCase();
+
+ const inVal = alphabet.indexOf(inChar);
+
+ if (inVal >= 0) {
+ outChar = alphabet[affineFn(inVal, a, b, modulus)];
+ if (matchCase && isUpperCase(input[i])) outChar = outChar.toUpperCase();
+ } else {
+ outChar += input[i];
+ }
+
+ output += outChar;
+ }
+ return output;
+}
+
+/**
+ * Apply the affine encryption function to p.
+ *
+ * @author Barry B [profbbrown@gmail.com]
+ * @param {integer} p - Plaintext value
+ * @param {integer} a - Multiplier coefficient
+ * @param {integer} b - Addition coefficient
+ * @param {integer} m - Modulus
+ * @returns {integer}
+ */
+const encryptFn = function(p, a, b, m) {
+ return (a * p + b) % m;
+};
+
+/**
+ * Apply the affine decryption function to c.
+ *
+ * @author Barry B [profbbrown@gmail.com]
+ * @param {integer} c - Ciphertext value
+ * @param {integer} a - Multiplicative inverse coefficient
+ * @param {integer} b - Additive inverse coefficient
+ * @param {integer} m - Modulus
+ * @returns {integer}
+ */
+const decryptFn = function(c, a, b, m) {
+ return ((c + b) * a) % m;
+};
+
+/**
+ * Affine encrypt operation.
+ * Allows for an expanded alphabet.
+ *
+ * @author Barry B [profbbrown@gmail.com]
+ * @param {string} input
+ * @param {integer} a
+ * @param {integer} b
+ * @param {string} alphabet
+ * @returns {string}
+ */
+export function affineEncrypt(input, a, b, alphabet="a-z") {
+ return affineApplication(input, a, b, alphabet, encryptFn);
+}
+
+/**
+ * Affine Cipher Decrypt operation using the coefficients that were used to encrypt.
+ * The modular inverses will be calculated.
+ *
+ * @author Barry B [profbbrown@gmail.com]
+ * @param {string} input
+ * @param {integer} a
+ * @param {integer} b
+ * @param {string} alphabet
+ * @returns {string}
+ */
+export function affineDecrypt(input, a, b, alphabet="a-z") {
+ // Because we are calculating the modulus and inverses here, we have to perform
+ // many of the same tests that the affineApplication function does.
+ // TODO: figure out a way to avoid doing the tests twice.
+ // Idea: make a checkInputs function.
+ // Idea: move the tests into the affineEncrypt and affineDecryptInverse functions
+ // so that affineApplication assumes valid inputs
+ if (alphabet === "")
+ throw new OperationError("The alphabet cannot be empty.");
+
+ if (a === undefined || a === "" || isNaN(a)) a = 1;
+ if (b === undefined || b === "" || isNaN(b)) b = 0;
+
+ if (!/^\+?(0|[1-9]\d*)$/.test(a) || !/^\+?(0|[1-9]\d*)$/.test(b)) {
+ throw new OperationError("The values of a and b can only be integers.");
+ }
+
+ const m = Utils.expandAlphRange(alphabet).length;
+ if (Utils.gcd(a, m) !== 1)
+ throw new OperationError("The value of `a` (" + a + ") must be coprime to " + m + ".");
+
+ const aInv = Utils.modInv(a, m);
+ const bInv = (m - b) % m;
+ if (aInv === null || aInv === undefined)
+ throw new OperationError("The value of `a` (" + a + ") must be coprime to " + m + ".");
+ else return affineApplication(input, aInv, bInv, alphabet, decryptFn);
+}
+
+/**
+ * Affine Cipher Decrypt operation using modular inverse coefficients
+ * supplied by the user.
+ *
+ * @author Barry B [profbbrown@gmail.com]
+ * @param {string} input
+ * @param {number} a
+ * @param {number} b
+ * @param {string} alphabet
+ * @returns {string}
+ */
+export function affineDecryptInverse(input, a, b, alphabet="a-z") {
+ return affineApplication(input, a, b, alphabet, decryptFn);
+}
+
/**
* Generates a polybius square for the given keyword
*
@@ -86,3 +248,22 @@ export const format = {
"UTF16BE": CryptoJS.enc.Utf16BE,
"Latin1": CryptoJS.enc.Latin1,
};
+
+export const AFFINE_ALPHABETS = [
+ {name: "Letters, match case: a-z", value: "a-z"},
+ {name: "Letters, case sensitive: A-Za-z", value: "A-Za-z"},
+ {name: "Word characters: A-Za-z0-9_", value: "A-Za-z0-9_"},
+ {name: "Printable ASCII: space-~", value: "\\x20-~"}
+];
+
+/**
+ * Returns true if the given character is uppercase
+ *
+ * @private
+ * @author Barry B [profbbrown@gmail.com]
+ * @param {string} c - A character
+ * @returns {boolean}
+ */
+function isUpperCase(c) {
+ return c.toUpperCase() === c;
+}
diff --git a/src/core/operations/AffineCipherDecode.mjs b/src/core/operations/AffineCipherDecode.mjs
index 869f231a..9c649079 100644
--- a/src/core/operations/AffineCipherDecode.mjs
+++ b/src/core/operations/AffineCipherDecode.mjs
@@ -5,8 +5,7 @@
*/
import Operation from "../Operation.mjs";
-import Utils from "../Utils.mjs";
-import OperationError from "../errors/OperationError.mjs";
+import { affineDecrypt, affineDecryptInverse, AFFINE_ALPHABETS } from "../lib/Ciphers.mjs";
/**
* Affine Cipher Decode operation
@@ -21,7 +20,7 @@ class AffineCipherDecode extends Operation {
this.name = "Affine Cipher Decode";
this.module = "Ciphers";
- this.description = "The Affine cipher is a type of monoalphabetic substitution cipher. To decrypt, each letter in an alphabet is mapped to its numeric equivalent, decrypted by a mathematical function, and converted back to a letter.";
+ this.description = "The Affine cipher is a type of monoalphabetic substitution cipher. To decrypt, each letter in an alphabet is mapped to its numeric equivalent, decrypted by a mathematical function (the inverse of ax+b % m), and converted back to a letter.";
this.infoURL = "https://wikipedia.org/wiki/Affine_cipher";
this.inputType = "string";
this.outputType = "string";
@@ -35,6 +34,16 @@ class AffineCipherDecode extends Operation {
"name": "b",
"type": "number",
"value": 0
+ },
+ {
+ "name": "Alphabet",
+ "type": "editableOption",
+ "value": AFFINE_ALPHABETS
+ },
+ {
+ "name": "Use modular inverse values",
+ "type": "boolean",
+ "value": false
}
];
}
@@ -47,32 +56,9 @@ class AffineCipherDecode extends Operation {
* @throws {OperationError} if a or b values are invalid
*/
run(input, args) {
- const alphabet = "abcdefghijklmnopqrstuvwxyz",
- [a, b] = args,
- aModInv = Utils.modInv(a, 26); // Calculates modular inverse of a
- let output = "";
-
- if (!/^\+?(0|[1-9]\d*)$/.test(a) || !/^\+?(0|[1-9]\d*)$/.test(b)) {
- throw new OperationError("The values of a and b can only be integers.");
- }
-
- if (Utils.gcd(a, 26) !== 1) {
- throw new OperationError("The value of `a` must be coprime to 26.");
- }
-
- for (let i = 0; i < input.length; i++) {
- if (alphabet.indexOf(input[i]) >= 0) {
- // Uses the affine decode function (y-b * A') % m = x (where m is length of the alphabet and A' is modular inverse)
- output += alphabet[Utils.mod((alphabet.indexOf(input[i]) - b) * aModInv, 26)];
- } else if (alphabet.indexOf(input[i].toLowerCase()) >= 0) {
- // Same as above, accounting for uppercase
- output += alphabet[Utils.mod((alphabet.indexOf(input[i].toLowerCase()) - b) * aModInv, 26)].toUpperCase();
- } else {
- // Non-alphabetic characters
- output += input[i];
- }
- }
- return output;
+ const a = args[0], b = args[1], alphabet = args[2], useInverse = args[3];
+ if (useInverse) return affineDecryptInverse(input, a, b, alphabet);
+ else return affineDecrypt(input, a, b, alphabet);
}
/**
diff --git a/src/core/operations/AffineCipherEncode.mjs b/src/core/operations/AffineCipherEncode.mjs
index a9462ae8..8a04fcd5 100644
--- a/src/core/operations/AffineCipherEncode.mjs
+++ b/src/core/operations/AffineCipherEncode.mjs
@@ -5,7 +5,7 @@
*/
import Operation from "../Operation.mjs";
-import { affineEncode } from "../lib/Ciphers.mjs";
+import { affineEncrypt, AFFINE_ALPHABETS } from "../lib/Ciphers.mjs";
/**
* Affine Cipher Encode operation
@@ -20,7 +20,7 @@ class AffineCipherEncode extends Operation {
this.name = "Affine Cipher Encode";
this.module = "Ciphers";
- this.description = "The Affine cipher is a type of monoalphabetic substitution cipher, wherein each letter in an alphabet is mapped to its numeric equivalent, encrypted using simple mathematical function, (ax + b) % 26
, and converted back to a letter.";
+ this.description = "The Affine cipher is a type of monoalphabetic substitution cipher, wherein each letter in an alphabet is mapped to its numeric equivalent, encrypted using simple mathematical function, (ax + b) % m
, and converted back to a letter.";
this.infoURL = "https://wikipedia.org/wiki/Affine_cipher";
this.inputType = "string";
this.outputType = "string";
@@ -34,6 +34,11 @@ class AffineCipherEncode extends Operation {
"name": "b",
"type": "number",
"value": 0
+ },
+ {
+ "name": "Alphabet",
+ "type": "editableOption",
+ "value": AFFINE_ALPHABETS
}
];
}
@@ -44,7 +49,8 @@ class AffineCipherEncode extends Operation {
* @returns {string}
*/
run(input, args) {
- return affineEncode(input, args);
+ const a = args[0], b = args[1], alphabet = args[2];
+ return affineEncrypt(input, a, b, alphabet);
}
/**
diff --git a/tests/operations/tests/Ciphers.mjs b/tests/operations/tests/Ciphers.mjs
index 47453cf7..dd6fef08 100644
--- a/tests/operations/tests/Ciphers.mjs
+++ b/tests/operations/tests/Ciphers.mjs
@@ -2,6 +2,7 @@
* Cipher tests.
*
* @author Matt C [matt@artemisbot.uk]
+ * @author Barry B [profbbrown@gmail.com]
* @author n1474335 [n1474335@gmail.com]
*
* @copyright Crown Copyright 2018
@@ -18,7 +19,7 @@ TestRegister.addTests([
recipeConfig: [
{
op: "Affine Cipher Encode",
- args: [1, 0]
+ args: [1, 0, "a-z"]
}
],
},
@@ -29,7 +30,29 @@ TestRegister.addTests([
recipeConfig: [
{
op: "Affine Cipher Encode",
- args: [0.1, 0.00001]
+ args: [0.1, 0.00001, "a-z"]
+ }
+ ],
+ },
+ {
+ name: "Affine Encode: invalid a & b, empty alphabet",
+ input: "some keys are shaped as locks. index[me]",
+ expectedOutput: "The alphabet cannot be empty.",
+ recipeConfig: [
+ {
+ op: "Affine Cipher Encode",
+ args: [0.1, 0.00001, ""]
+ }
+ ],
+ },
+ {
+ name: "Affine Encode: valid a & b, empty alphabet",
+ input: "some keys are shaped as locks. index[me]",
+ expectedOutput: "The alphabet cannot be empty.",
+ recipeConfig: [
+ {
+ op: "Affine Cipher Encode",
+ args: [7, 23, ""]
}
],
},
@@ -40,18 +63,40 @@ TestRegister.addTests([
recipeConfig: [
{
op: "Affine Cipher Encode",
- args: [1, 0]
+ args: [1, 0, "a-z"]
}
],
},
{
- name: "Affine Encode: normal",
- input: "some keys are shaped as locks. index[me]",
- expectedOutput: "vhnl tldv xyl vcxelo xv qhrtv. zkolg[nl]",
+ name: "Affine Encode: normal a-z",
+ input: "Some Keys Are Shaped As Locks. index[me]",
+ expectedOutput: "Vhnl Tldv Xyl Vcxelo Xv Qhrtv. zkolg[nl]",
recipeConfig: [
{
op: "Affine Cipher Encode",
- args: [23, 23]
+ args: [23, 23, "a-z"]
+ }
+ ],
+ },
+ {
+ name: "Affine Encode: normal A-Za-z",
+ input: "Some Keys Are Shaped As Locks. index[me]",
+ expectedOutput: "VHNl tldv XYl VCxelO Xv QHrTv. ZkOlG[Nl]",
+ recipeConfig: [
+ {
+ op: "Affine Cipher Encode",
+ args: [23, 23, "A-Za-z"]
+ }
+ ],
+ },
+ {
+ name: "Affine Encode: normal, printable ASCII",
+ input: "Some Keys Are Shaped As Locks. index[me]",
+ expectedOutput: "XCtz7^zk@76)z7X`}Zzc76@7uCLF@\\7w,czTRtz!",
+ recipeConfig: [
+ {
+ op: "Affine Cipher Encode",
+ args: [23, 23, "\\u0020-~"]
}
],
},
@@ -62,7 +107,7 @@ TestRegister.addTests([
recipeConfig: [
{
op: "Affine Cipher Decode",
- args: [1, 0]
+ args: [1, 0, "a-z", false]
}
],
},
@@ -73,40 +118,128 @@ TestRegister.addTests([
recipeConfig: [
{
op: "Affine Cipher Decode",
- args: [0.1, 0.00001]
+ args: [0.1, 0.00001, "a-z", false]
}
],
},
{
- name: "Affine Decode: invalid a (coprime)",
+ name: "Affine Decode: valid a & b, empty alphabet",
input: "vhnl tldv xyl vcxelo xv qhrtv. zkolg[nl]",
- expectedOutput: "The value of `a` must be coprime to 26.",
+ expectedOutput: "The alphabet cannot be empty.",
recipeConfig: [
{
op: "Affine Cipher Decode",
- args: [8, 23]
+ args: [23, 23, "", false]
}
],
},
{
- name: "Affine Decode: no effect",
+ name: "Affine Decode: invalid a & b (non-integer), empty alphabet",
input: "vhnl tldv xyl vcxelo xv qhrtv. zkolg[nl]",
- expectedOutput: "vhnl tldv xyl vcxelo xv qhrtv. zkolg[nl]",
+ expectedOutput: "The alphabet cannot be empty.",
recipeConfig: [
{
op: "Affine Cipher Decode",
- args: [1, 0]
+ args: [0.1, 0.00001, "", false]
}
],
},
{
- name: "Affine Decode: normal",
+ name: "Affine Decode: invalid a (non-coprime)",
input: "vhnl tldv xyl vcxelo xv qhrtv. zkolg[nl]",
- expectedOutput: "some keys are shaped as locks. index[me]",
+ expectedOutput: "The value of `a` (8) must be coprime to 26.",
recipeConfig: [
{
op: "Affine Cipher Decode",
- args: [23, 23]
+ args: [8, 23, "a-z", false]
+ }
+ ],
+ },
+ {
+ name: "Affine Decode: invalid a (non-coprime), printable ASCII",
+ input: "vhnl tldv xyl vcxelo xv qhrtv. zkolg[nl]",
+ expectedOutput: "The value of `a` (5) must be coprime to 95.",
+ recipeConfig: [
+ {
+ op: "Affine Cipher Decode",
+ args: [5, 23, "\\u0020-~", false]
+ }
+ ],
+ },
+ {
+ name: "Affine Decode: no effect, match case",
+ input: "Vhnl Tldv Xyl Vcxelo xv qhrtv. zkolg[nl]",
+ expectedOutput: "Vhnl Tldv Xyl Vcxelo xv qhrtv. zkolg[nl]",
+ recipeConfig: [
+ {
+ op: "Affine Cipher Decode",
+ args: [1, 0, "a-z", false]
+ }
+ ],
+ },
+ {
+ name: "Affine Decode: no effect, case sensitive",
+ input: "Vhnl Tldv Xyl Vcxelo xv qhrtv. zkolg[nl]",
+ expectedOutput: "Vhnl Tldv Xyl Vcxelo xv qhrtv. zkolg[nl]",
+ recipeConfig: [
+ {
+ op: "Affine Cipher Decode",
+ args: [1, 0, "A-Za-z", false]
+ }
+ ],
+ },
+ {
+ name: "Affine Decode: normal, case sensitive",
+ input: "Vhnl Tldv Xyl Vcxelo xv qhrtv. zkolg[nl]",
+ expectedOutput: "SOMe keys ARe SHapeD as lOcKs. InDeX[Me]",
+ recipeConfig: [
+ {
+ op: "Affine Cipher Decode",
+ args: [23, 23, "A-Za-z", false]
+ }
+ ],
+ },
+ {
+ name: "Affine Decode: normal, match case",
+ input: "Vhnl Tldv Xyl Vcxelo Xv Qhrtv. zkolg[nl]",
+ expectedOutput: "Some Keys Are Shaped As Locks. index[me]",
+ recipeConfig: [
+ {
+ op: "Affine Cipher Decode",
+ args: [23, 23, "a-z", false]
+ }
+ ],
+ },
+ {
+ name: "Affine Decode: normal, inverse",
+ input: "Vhnl Tldv Xyl Vcxelo Xv Qhrtv. zkolg[nl]",
+ expectedOutput: "Some Keys Are Shaped As Locks. index[me]",
+ recipeConfig: [
+ {
+ op: "Affine Cipher Decode",
+ args: [17, 3, "a-z", true]
+ }
+ ],
+ },
+ {
+ name: "Affine Decode: normal, printable ASCII",
+ input: "XCtz7^zk@76)z7X`}Zzc76@7uCLF@\\7w,czTRtz!",
+ expectedOutput: "Some Keys Are Shaped As Locks. index[me]",
+ recipeConfig: [
+ {
+ op: "Affine Cipher Decode",
+ args: [23, 23, "\u0020-~", false]
+ }
+ ],
+ },
+ {
+ name: "Affine Decode: normal, printable ASCII, inverse",
+ input: "XCtz7^zk@76)z7X`}Zzc76@7uCLF@\\7w,czTRtz!",
+ expectedOutput: "Some Keys Are Shaped As Locks. index[me]",
+ recipeConfig: [
+ {
+ op: "Affine Cipher Decode",
+ args: [62, 72, "\u0020-~", true]
}
],
},