This commit is contained in:
Barry Brown 2025-04-14 18:45:23 +01:00 committed by GitHub
commit a67291d212
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 374 additions and 61 deletions

View file

@ -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: |

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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);
}
/**

View file

@ -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, <code>(ax + b) % 26</code>, 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, <code>(ax + b) % m</code>, 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);
}
/**

View file

@ -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]
}
],
},