From de57002c6f831ca357c1495c378676906d0e0426 Mon Sep 17 00:00:00 2001 From: Barry Brown Date: Wed, 12 Jun 2024 14:57:27 +0000 Subject: [PATCH 01/16] Checkbox for mod inverse; calculations implemented --- src/core/operations/AffineCipherDecode.mjs | 26 +++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/core/operations/AffineCipherDecode.mjs b/src/core/operations/AffineCipherDecode.mjs index 869f231a4e..78666d1057 100644 --- a/src/core/operations/AffineCipherDecode.mjs +++ b/src/core/operations/AffineCipherDecode.mjs @@ -35,6 +35,11 @@ class AffineCipherDecode extends Operation { "name": "b", "type": "number", "value": 0 + }, + { + "name": "Use modular inverse values", + "type": "boolean", + "value": false } ]; } @@ -48,7 +53,7 @@ class AffineCipherDecode extends Operation { */ run(input, args) { const alphabet = "abcdefghijklmnopqrstuvwxyz", - [a, b] = args, + [a, b, useInv] = args, aModInv = Utils.modInv(a, 26); // Calculates modular inverse of a let output = ""; @@ -62,11 +67,22 @@ class AffineCipherDecode extends Operation { 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)]; + if (useInv) { + // Uses the affine decode function (y+b)*a % m, where m is the length + // of the alphabet and a,b are modular inverses of the original a,b + output += alphabet[Utils.mod((alphabet.indexOf(input[i]) + b) * a, 26)]; + } else { + // 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(); + if (useInv) { + // Same as above, accounting for uppercase + output += alphabet[Utils.mod((alphabet.indexOf(input[i].toLowerCase()) + b) * a, 26)].toUpperCase(); + } else { + // 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]; From 979de8e488f62ce1324683d404bee767d597e338 Mon Sep 17 00:00:00 2001 From: Barry Brown Date: Thu, 13 Jun 2024 01:44:08 +0000 Subject: [PATCH 02/16] Refactor affine encrypt/decrypt functions --- src/core/lib/Ciphers.mjs | 154 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) diff --git a/src/core/lib/Ciphers.mjs b/src/core/lib/Ciphers.mjs index 6266a8e1de..2d6fdfdafd 100644 --- a/src/core/lib/Ciphers.mjs +++ b/src/core/lib/Ciphers.mjs @@ -51,6 +51,141 @@ 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) { + 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) { + for (let i = 0; i < alphabet.length; i++) + alphabet[i] = alphabet[i].toLowerCase(); + } + + 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` 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 - Multiplier coefficient + * @param {integer} b - Addition 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") { + const m = Utils.expandAlphRange(alphabet).length; + const aInv = Utils.modInv(a, m); + const bInv = (m - b) % m; + 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 +221,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: sp-~", value: " -~"} +]; + +/** + * 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; +} From 7bef120a0ad748fb6391475fd289ce6d8b09e200 Mon Sep 17 00:00:00 2001 From: Barry Brown Date: Thu, 13 Jun 2024 01:44:56 +0000 Subject: [PATCH 03/16] Uses affineEcrypt instead of affineEncode --- src/core/operations/AffineCipherEncode.mjs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/core/operations/AffineCipherEncode.mjs b/src/core/operations/AffineCipherEncode.mjs index a9462ae8f2..c3da8f766a 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 } from "../lib/Ciphers.mjs"; /** * Affine Cipher Encode operation @@ -44,7 +44,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, "a-z"); } /** From 41585e0d3df0bba2734a5266d224a168b5912f4a Mon Sep 17 00:00:00 2001 From: Barry Brown Date: Thu, 13 Jun 2024 06:43:54 +0000 Subject: [PATCH 04/16] Throws error if modInv didn't work --- src/core/lib/Ciphers.mjs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/core/lib/Ciphers.mjs b/src/core/lib/Ciphers.mjs index 2d6fdfdafd..9f8aa41008 100644 --- a/src/core/lib/Ciphers.mjs +++ b/src/core/lib/Ciphers.mjs @@ -166,9 +166,13 @@ export function affineEncrypt(input, a, b, alphabet="a-z") { */ export function affineDecrypt(input, a, b, alphabet="a-z") { 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; - return affineApplication(input, aInv, bInv, alphabet, decryptFn); + if (aInv === undefined) + throw new OperationError("The value of `a` (" + a + ") must be coprime to " + m + "."); + else return affineApplication(input, aInv, bInv, alphabet, decryptFn); } /** @@ -226,7 +230,7 @@ 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: sp-~", value: " -~"} + {name: "Printable ASCII: space-~", value: "\\u0020-~"} ]; /** From ede3dc747104b5c41cc6f908c654c111a3f66b45 Mon Sep 17 00:00:00 2001 From: Barry Brown Date: Thu, 13 Jun 2024 06:44:48 +0000 Subject: [PATCH 05/16] Decrypt with user-specified alphabet; allow user to input inverse coefficients --- src/core/operations/AffineCipherDecode.mjs | 48 ++++------------------ 1 file changed, 9 insertions(+), 39 deletions(-) diff --git a/src/core/operations/AffineCipherDecode.mjs b/src/core/operations/AffineCipherDecode.mjs index 78666d1057..45316f9ad1 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 @@ -36,6 +35,11 @@ class AffineCipherDecode extends Operation { "type": "number", "value": 0 }, + { + "name": "Alphabet", + "type": "editableOption", + "value": AFFINE_ALPHABETS + }, { "name": "Use modular inverse values", "type": "boolean", @@ -52,43 +56,9 @@ class AffineCipherDecode extends Operation { * @throws {OperationError} if a or b values are invalid */ run(input, args) { - const alphabet = "abcdefghijklmnopqrstuvwxyz", - [a, b, useInv] = 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) { - if (useInv) { - // Uses the affine decode function (y+b)*a % m, where m is the length - // of the alphabet and a,b are modular inverses of the original a,b - output += alphabet[Utils.mod((alphabet.indexOf(input[i]) + b) * a, 26)]; - } else { - // 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) { - if (useInv) { - // Same as above, accounting for uppercase - output += alphabet[Utils.mod((alphabet.indexOf(input[i].toLowerCase()) + b) * a, 26)].toUpperCase(); - } else { - // 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); } /** From 6876d90e78053c4b85a7396c864cdf967ac6245f Mon Sep 17 00:00:00 2001 From: Barry Brown Date: Thu, 13 Jun 2024 06:45:11 +0000 Subject: [PATCH 06/16] Encrypt with user-supplied alphabets --- src/core/operations/AffineCipherEncode.mjs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/core/operations/AffineCipherEncode.mjs b/src/core/operations/AffineCipherEncode.mjs index c3da8f766a..7ba0ad606f 100644 --- a/src/core/operations/AffineCipherEncode.mjs +++ b/src/core/operations/AffineCipherEncode.mjs @@ -5,7 +5,7 @@ */ import Operation from "../Operation.mjs"; -import { affineEncrypt } from "../lib/Ciphers.mjs"; +import { affineEncrypt, AFFINE_ALPHABETS } from "../lib/Ciphers.mjs"; /** * Affine Cipher Encode operation @@ -34,6 +34,11 @@ class AffineCipherEncode extends Operation { "name": "b", "type": "number", "value": 0 + }, + { + "name": "Alphabet", + "type": "editableOption", + "value": AFFINE_ALPHABETS } ]; } @@ -45,7 +50,7 @@ class AffineCipherEncode extends Operation { */ run(input, args) { const a = args[0], b = args[1], alphabet = args[2]; - return affineEncrypt(input, a, b, "a-z"); + return affineEncrypt(input, a, b, alphabet); } /** From 9c696b523de502a966af283afcd7f7bceb5479d7 Mon Sep 17 00:00:00 2001 From: Barry Brown Date: Thu, 13 Jun 2024 06:49:14 +0000 Subject: [PATCH 07/16] Remove assumption of 26 as modulus --- src/core/Utils.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/Utils.mjs b/src/core/Utils.mjs index a9c381d766..707720cc57 100755 --- a/src/core/Utils.mjs +++ b/src/core/Utils.mjs @@ -1275,7 +1275,7 @@ class Utils { static modInv(x, y) { x %= y; for (let i = 1; i < y; i++) { - if ((x * i) % 26 === 1) { + if ((x * i) % y === 1) { return i; } } From 8530c47cb8c92dc0ab3a9b93469c1c0a56e13465 Mon Sep 17 00:00:00 2001 From: Barry Brown Date: Thu, 13 Jun 2024 17:19:11 +0000 Subject: [PATCH 08/16] modInv uses Extended Euclidean Algorithm --- src/core/Utils.mjs | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/core/Utils.mjs b/src/core/Utils.mjs index 707720cc57..08eadbf17f 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) % y === 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; } From 7ae35bc587d4090a51b71267b38c0beeeb64c57f Mon Sep 17 00:00:00 2001 From: Barry Brown Date: Thu, 13 Jun 2024 17:32:21 +0000 Subject: [PATCH 09/16] Uses new Utils.modInv function --- src/core/lib/Ciphers.mjs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/core/lib/Ciphers.mjs b/src/core/lib/Ciphers.mjs index 9f8aa41008..9ab5e811ec 100644 --- a/src/core/lib/Ciphers.mjs +++ b/src/core/lib/Ciphers.mjs @@ -86,7 +86,7 @@ export function affineApplication(input, a, b, alphabet, affineFn) { } if (Utils.gcd(a, modulus) !== 1) { - throw new OperationError("The value of `a` must be coprime to " + modulus + "."); + throw new OperationError("The value of `a` (" + a + ") must be coprime to " + modulus + "."); } // Apply affine function to each character in the input @@ -168,9 +168,10 @@ export function affineDecrypt(input, a, b, alphabet="a-z") { 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 === undefined) + if (aInv === null) throw new OperationError("The value of `a` (" + a + ") must be coprime to " + m + "."); else return affineApplication(input, aInv, bInv, alphabet, decryptFn); } From 86ad59842fccc77c27f52f0945ba80d2f442e5eb Mon Sep 17 00:00:00 2001 From: Barry Brown Date: Thu, 13 Jun 2024 17:42:54 +0000 Subject: [PATCH 10/16] Updated description --- src/core/operations/AffineCipherDecode.mjs | 2 +- src/core/operations/AffineCipherEncode.mjs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/operations/AffineCipherDecode.mjs b/src/core/operations/AffineCipherDecode.mjs index 45316f9ad1..9c64907972 100644 --- a/src/core/operations/AffineCipherDecode.mjs +++ b/src/core/operations/AffineCipherDecode.mjs @@ -20,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"; diff --git a/src/core/operations/AffineCipherEncode.mjs b/src/core/operations/AffineCipherEncode.mjs index 7ba0ad606f..8a04fcd59f 100644 --- a/src/core/operations/AffineCipherEncode.mjs +++ b/src/core/operations/AffineCipherEncode.mjs @@ -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"; From 1017294942bfa1bdd8077b391c69d95c1f572cb3 Mon Sep 17 00:00:00 2001 From: Barry Brown Date: Thu, 13 Jun 2024 17:43:10 +0000 Subject: [PATCH 11/16] Deprecate affineEncode --- src/core/lib/Ciphers.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/lib/Ciphers.mjs b/src/core/lib/Ciphers.mjs index 9ab5e811ec..fe40677a8a 100644 --- a/src/core/lib/Ciphers.mjs +++ b/src/core/lib/Ciphers.mjs @@ -17,6 +17,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 @@ -54,7 +55,7 @@ export function affineEncode(input, args) { /** * Generic affine encrypt/decrypt operation. * Allows for an expanded alphabet. - * + * * @author Barry B [profbbrown@gmail.com] * @param {string} input * @param {number} a From e1e88c35eff175e4f90c16a2ddc57e0abbc3b9f9 Mon Sep 17 00:00:00 2001 From: Barry Brown Date: Fri, 14 Jun 2024 02:40:24 +0000 Subject: [PATCH 12/16] affineDecrypt accepts either null or undefined from modInv --- src/core/lib/Ciphers.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/lib/Ciphers.mjs b/src/core/lib/Ciphers.mjs index fe40677a8a..aaa2423259 100644 --- a/src/core/lib/Ciphers.mjs +++ b/src/core/lib/Ciphers.mjs @@ -172,7 +172,7 @@ export function affineDecrypt(input, a, b, alphabet="a-z") { const aInv = Utils.modInv(a, m); const bInv = (m - b) % m; - if (aInv === null) + 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); } From ca3aef7b85f68f2ebc1de3e00609d6f178a36cf8 Mon Sep 17 00:00:00 2001 From: Barry Brown Date: Fri, 14 Jun 2024 03:44:13 +0000 Subject: [PATCH 13/16] Tests for the new affine functionality --- tests/operations/tests/Ciphers.mjs | 169 ++++++++++++++++++++++++++--- 1 file changed, 151 insertions(+), 18 deletions(-) diff --git a/tests/operations/tests/Ciphers.mjs b/tests/operations/tests/Ciphers.mjs index 47453cf751..dd6fef08bd 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: [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: [23, 23] + args: [62, 72, "\u0020-~", true] } ], }, From 7806fd999aa746af260e72168ec020724150dc8d Mon Sep 17 00:00:00 2001 From: Barry Brown Date: Fri, 14 Jun 2024 03:44:35 +0000 Subject: [PATCH 14/16] More error checking to conform to the new tests --- src/core/lib/Ciphers.mjs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/core/lib/Ciphers.mjs b/src/core/lib/Ciphers.mjs index aaa2423259..8be973d52f 100644 --- a/src/core/lib/Ciphers.mjs +++ b/src/core/lib/Ciphers.mjs @@ -65,6 +65,9 @@ export function affineEncode(input, args) { * @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; @@ -166,6 +169,19 @@ export function affineEncrypt(input, a, b, alphabet="a-z") { * @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 (!/^\+?(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 + "."); From 6a841927a305b1676a18cb104c4510acece0f55c Mon Sep 17 00:00:00 2001 From: Barry Brown Date: Fri, 14 Jun 2024 18:13:57 +0000 Subject: [PATCH 15/16] Little fixups --- src/core/lib/Ciphers.mjs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/core/lib/Ciphers.mjs b/src/core/lib/Ciphers.mjs index 8be973d52f..9b9d4a7a9b 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 @@ -80,10 +81,11 @@ export function affineApplication(input, a, b, alphabet, affineFn) { // If we are matching case, convert entire alphabet to lowercase. // This will simplify the encryption. - if (matchCase) { - for (let i = 0; i < alphabet.length; i++) - alphabet[i] = alphabet[i].toLowerCase(); - } + 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."); @@ -133,8 +135,8 @@ const encryptFn = function(p, a, b, m) { * * @author Barry B [profbbrown@gmail.com] * @param {integer} c - Ciphertext value - * @param {integer} a - Multiplier coefficient - * @param {integer} b - Addition coefficient + * @param {integer} a - Multiplicative inverse coefficient + * @param {integer} b - Additive inverse coefficient * @param {integer} m - Modulus * @returns {integer} */ @@ -178,6 +180,9 @@ export function affineDecrypt(input, a, b, alphabet="a-z") { 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."); } @@ -248,7 +253,7 @@ 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: "\\u0020-~"} + {name: "Printable ASCII: space-~", value: "\\x20-~"} ]; /** From 7ed588572d0556816b18b431ecdda714fbf91528 Mon Sep 17 00:00:00 2001 From: Barry Brown Date: Sun, 1 Sep 2024 01:19:46 -0700 Subject: [PATCH 16/16] Update master.yml Updated to use Node 20 --- .github/workflows/master.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 9fd6d68dc4..06d47e3ff8 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: |