Skip to content

Affine enhancements #1892

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

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
de57002
Checkbox for mod inverse; calculations implemented
profbbrown Jun 12, 2024
979de8e
Refactor affine encrypt/decrypt functions
profbbrown Jun 13, 2024
7bef120
Uses affineEcrypt instead of affineEncode
profbbrown Jun 13, 2024
41585e0
Throws error if modInv didn't work
profbbrown Jun 13, 2024
ede3dc7
Decrypt with user-specified alphabet; allow user to input inverse coe…
profbbrown Jun 13, 2024
6876d90
Encrypt with user-supplied alphabets
profbbrown Jun 13, 2024
9c696b5
Remove assumption of 26 as modulus
profbbrown Jun 13, 2024
8530c47
modInv uses Extended Euclidean Algorithm
profbbrown Jun 13, 2024
cb6b1f7
Merge remote-tracking branch 'origin/more-efficient-modInv' into affi…
profbbrown Jun 13, 2024
7ae35bc
Uses new Utils.modInv function
profbbrown Jun 13, 2024
86ad598
Updated description
profbbrown Jun 13, 2024
1017294
Deprecate affineEncode
profbbrown Jun 13, 2024
e1e88c3
affineDecrypt accepts either null or undefined from modInv
profbbrown Jun 14, 2024
ca3aef7
Tests for the new affine functionality
profbbrown Jun 14, 2024
7806fd9
More error checking to conform to the new tests
profbbrown Jun 14, 2024
3bc95f4
Merge branch 'master' into affine_enhancements
profbbrown Jun 14, 2024
6a84192
Little fixups
profbbrown Jun 14, 2024
7ed5885
Update master.yml
profbbrown Sep 1, 2024
1890ab6
Merge branch 'gchq:master' into affine_enhancements
profbbrown Sep 1, 2024
2256590
Merge branch 'master' into affine_enhancements
profbbrown Dec 2, 2024
31e6ce5
Merge branch 'master' into affine_enhancements
a3957273 Feb 11, 2025
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