From f87666f659b612c7e25187571439422884f62dbf Mon Sep 17 00:00:00 2001 From: Matt C Date: Wed, 9 May 2018 20:18:33 +0100 Subject: [PATCH] Converted Affine/Atbash operations to mjs & added tests --- src/core/lib/Ciphers.mjs | 41 ++++++++ src/core/operations/AffineCipherDecode.mjs | 103 +++++++++++++++++++++ src/core/operations/AffineCipherEncode.mjs | 76 +++++++++++++++ src/core/operations/AtbashCipher.mjs | 66 +++++++++++++ test/index.mjs | 12 +-- test/tests/operations/Ciphers.mjs | 102 ++++++++++++++++++++ 6 files changed, 394 insertions(+), 6 deletions(-) create mode 100644 src/core/lib/Ciphers.mjs create mode 100644 src/core/operations/AffineCipherDecode.mjs create mode 100644 src/core/operations/AffineCipherEncode.mjs create mode 100644 src/core/operations/AtbashCipher.mjs create mode 100644 test/tests/operations/Ciphers.mjs diff --git a/src/core/lib/Ciphers.mjs b/src/core/lib/Ciphers.mjs new file mode 100644 index 0000000..419e5b7 --- /dev/null +++ b/src/core/lib/Ciphers.mjs @@ -0,0 +1,41 @@ +/** + * Cipher functions. + * + * @author Matt C [matt@artemisbot.uk] + * @copyright Crown Copyright 2018 + * @license Apache-2.0 + * + */ + +/** + * Affine Cipher Encode operation. + * + * @author Matt C [matt@artemisbot.uk] + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ +export function affineEncode(input, args) { + const alphabet = "abcdefghijklmnopqrstuvwxyz", + a = args[0], + b = args[1]; + let output = ""; + + if (!/^\+?(0|[1-9]\d*)$/.test(a) || !/^\+?(0|[1-9]\d*)$/.test(b)) { + return "The values of a and b can only be integers."; + } + + for (let i = 0; i < input.length; i++) { + if (alphabet.indexOf(input[i]) >= 0) { + // Uses the affine function ax+b % m = y (where m is length of the alphabet) + output += alphabet[((a * alphabet.indexOf(input[i])) + b) % 26]; + } else if (alphabet.indexOf(input[i].toLowerCase()) >= 0) { + // Same as above, accounting for uppercase + output += alphabet[((a * alphabet.indexOf(input[i].toLowerCase())) + b) % 26].toUpperCase(); + } else { + // Non-alphabetic characters + output += input[i]; + } + } + return output; +} diff --git a/src/core/operations/AffineCipherDecode.mjs b/src/core/operations/AffineCipherDecode.mjs new file mode 100644 index 0000000..788d740 --- /dev/null +++ b/src/core/operations/AffineCipherDecode.mjs @@ -0,0 +1,103 @@ +/** + * @author Matt C [matt@artemisbot.uk] + * @copyright Crown Copyright 2018 + * @license Apache-2.0 + */ + +import Operation from "../Operation"; +import Utils from "../Utils"; + +/** + * Affine Cipher Decode operation + */ +class AffineCipherDecode extends Operation { + + /** + * AffineCipherDecode constructor + */ + constructor() { + super(); + + 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.inputType = "string"; + this.outputType = "string"; + this.args = [ + { + "name": "a", + "type": "number", + "value": 1 + }, + { + "name": "b", + "type": "number", + "value": 0 + } + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + const alphabet = "abcdefghijklmnopqrstuvwxyz", + a = args[0], + b = args[1], + 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)) { + return "The values of a and b can only be integers."; + } + + if (Utils.gcd(a, 26) !== 1) { + return "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; + } + + /** + * Highlight Affine Cipher Decode + * + * @param {Object[]} pos + * @param {number} pos[].start + * @param {number} pos[].end + * @param {Object[]} args + * @returns {Object[]} pos + */ + highlight(pos, args) { + return pos; + } + + /** + * Highlight Affine Cipher Decode in reverse + * + * @param {Object[]} pos + * @param {number} pos[].start + * @param {number} pos[].end + * @param {Object[]} args + * @returns {Object[]} pos + */ + highlightReverse(pos, args) { + return pos; + } + +} + +export default AffineCipherDecode; diff --git a/src/core/operations/AffineCipherEncode.mjs b/src/core/operations/AffineCipherEncode.mjs new file mode 100644 index 0000000..939bf23 --- /dev/null +++ b/src/core/operations/AffineCipherEncode.mjs @@ -0,0 +1,76 @@ +/** + * @author Matt C [matt@artemisbot.uk] + * @copyright Crown Copyright 2018 + * @license Apache-2.0 + */ + +import Operation from "../Operation"; +import { affineEncode } from "../lib/Ciphers"; +/** + * Affine Cipher Encode operation + */ +class AffineCipherEncode extends Operation { + + /** + * AffineCipherEncode constructor + */ + constructor() { + super(); + + 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.inputType = "string"; + this.outputType = "string"; + this.args = [ + { + "name": "a", + "type": "number", + "value": 1 + }, + { + "name": "b", + "type": "number", + "value": 0 + } + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + return affineEncode(input, args); + } + + /** + * Highlight Affine Cipher Encode + * + * @param {Object[]} pos + * @param {number} pos[].start + * @param {number} pos[].end + * @param {Object[]} args + * @returns {Object[]} pos + */ + highlight(pos, args) { + return pos; + } + + /** + * Highlight Affine Cipher Encode in reverse + * + * @param {Object[]} pos + * @param {number} pos[].start + * @param {number} pos[].end + * @param {Object[]} args + * @returns {Object[]} pos + */ + highlightReverse(pos, args) { + return pos; + } + +} + +export default AffineCipherEncode; diff --git a/src/core/operations/AtbashCipher.mjs b/src/core/operations/AtbashCipher.mjs new file mode 100644 index 0000000..9c4657e --- /dev/null +++ b/src/core/operations/AtbashCipher.mjs @@ -0,0 +1,66 @@ +/** + * @author Matt C [matt@artemisbot.uk] + * @copyright Crown Copyright 2016 + * @license Apache-2.0 + */ + +import Operation from "../Operation"; +import { affineEncode } from "../lib/Ciphers"; + +/** + * Atbash Cipher operation + */ +class AtbashCipher extends Operation { + + /** + * AtbashCipher constructor + */ + constructor() { + super(); + + this.name = "Atbash Cipher"; + this.module = "Ciphers"; + this.description = "Atbash is a mono-alphabetic substitution cipher originally used to encode the Hebrew alphabet. It has been modified here for use with the Latin alphabet."; + this.inputType = "string"; + this.outputType = "string"; + this.args = []; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + return affineEncode(input, [25, 25]); + } + + /** + * Highlight Atbash Cipher + * + * @param {Object[]} pos + * @param {number} pos[].start + * @param {number} pos[].end + * @param {Object[]} args + * @returns {Object[]} pos + */ + highlight(pos, args) { + return pos; + } + + /** + * Highlight Atbash Cipher in reverse + * + * @param {Object[]} pos + * @param {number} pos[].start + * @param {number} pos[].end + * @param {Object[]} args + * @returns {Object[]} pos + */ + highlightReverse(pos, args) { + return pos; + } + +} + +export default AtbashCipher; diff --git a/test/index.mjs b/test/index.mjs index dd592f6..f40d38c 100644 --- a/test/index.mjs +++ b/test/index.mjs @@ -30,8 +30,9 @@ import "./tests/operations/Base64"; // import "./tests/operations/BitwiseOp.js"; // import "./tests/operations/BSON.js"; // import "./tests/operations/ByteRepr.js"; +import "./tests/operations/CartesianProduct"; // import "./tests/operations/CharEnc.js"; -// import "./tests/operations/Cipher.js"; +import "./tests/operations/Ciphers"; // import "./tests/operations/Code.js"; // import "./tests/operations/Compress.js"; // import "./tests/operations/DateTime.js"; @@ -44,16 +45,15 @@ import "./tests/operations/Base64"; // import "./tests/operations/PHP.js"; // import "./tests/operations/NetBIOS.js"; // import "./tests/operations/OTP.js"; +import "./tests/operations/PowerSet"; // import "./tests/operations/Regex.js"; -import "./tests/operations/Rotate.mjs"; +import "./tests/operations/Rotate"; // import "./tests/operations/StrUtils.js"; // import "./tests/operations/SeqUtils.js"; -import "./tests/operations/SetUnion"; -import "./tests/operations/SetIntersection"; import "./tests/operations/SetDifference"; +import "./tests/operations/SetIntersection"; +import "./tests/operations/SetUnion"; import "./tests/operations/SymmetricDifference"; -import "./tests/operations/CartesianProduct"; -import "./tests/operations/PowerSet"; let allTestsPassing = true; const testStatusCounts = { diff --git a/test/tests/operations/Ciphers.mjs b/test/tests/operations/Ciphers.mjs new file mode 100644 index 0000000..165f4dc --- /dev/null +++ b/test/tests/operations/Ciphers.mjs @@ -0,0 +1,102 @@ +/** + * Cipher tests. + * + * @author Matt C [matt@artemisbot.uk] + * @author n1474335 [n1474335@gmail.com] + * + * @copyright Crown Copyright 2018 + * @license Apache-2.0 + */ +import TestRegister from "../../TestRegister"; + + +TestRegister.addTests([ + { + name: "Affine Encode: no input", + input: "", + expectedOutput: "", + recipeConfig: [ + { + op: "Affine Cipher Encode", + args: [1, 0] + } + ], + }, + { + name: "Affine Encode: no effect", + input: "some keys are shaped as locks. index[me]", + expectedOutput: "some keys are shaped as locks. index[me]", + recipeConfig: [ + { + op: "Affine Cipher Encode", + args: [1, 0] + } + ], + }, + { + name: "Affine Encode: normal", + 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] + } + ], + }, + { + name: "Affine Decode: no input", + input: "", + expectedOutput: "", + recipeConfig: [ + { + op: "Affine Cipher Decode", + args: [1, 0] + } + ], + }, + { + name: "Affine Decode: no effect", + 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] + } + ], + }, + { + name: "Affine Decode: normal", + 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] + } + ], + }, + { + name: "Atbash: no input", + input: "", + expectedOutput: "", + recipeConfig: [ + { + op: "Atbash Cipher", + args: [] + } + ], + }, + { + name: "Atbash: normal", + input: "old slow slim horn", + expectedOutput: "low hold horn slim", + recipeConfig: [ + { + op: "Atbash Cipher", + args: [] + } + ], + }, +]);