Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Affine enhancements #1892

Open
wants to merge 19 commits into
base: master
Choose a base branch
from
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
2 changes: 1 addition & 1 deletion .github/workflows/master.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
27 changes: 17 additions & 10 deletions src/core/Utils.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}


Expand Down
181 changes: 181 additions & 0 deletions src/core/lib/Ciphers.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
*
Expand Down Expand Up @@ -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;
}
44 changes: 15 additions & 29 deletions src/core/operations/AffineCipherDecode.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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";
Expand All @@ -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
}
];
}
Expand All @@ -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);
}

/**
Expand Down
12 changes: 9 additions & 3 deletions src/core/operations/AffineCipherEncode.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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";
Expand All @@ -34,6 +34,11 @@ class AffineCipherEncode extends Operation {
"name": "b",
"type": "number",
"value": 0
},
{
"name": "Alphabet",
"type": "editableOption",
"value": AFFINE_ALPHABETS
}
];
}
Expand All @@ -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);
}

/**
Expand Down
Loading
Loading