From edd22372d80663bf91f70b42ea95421b45328104 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0lteri=C5=9F=20Ya=C4=9F=C4=B1ztegin=20Ero=C4=9Flu?= Date: Mon, 17 Jun 2024 23:55:59 +0000 Subject: [PATCH] feat(Modhex): Introduce basic Modhex conversion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: İlteriş Yağıztegin Eroğlu --- src/core/config/Categories.json | 4 +- src/core/lib/Modhex.mjs | 165 +++++++++++++++++++++++++++++ src/core/operations/FromModhex.mjs | 84 +++++++++++++++ src/core/operations/ToModhex.mjs | 55 ++++++++++ tests/operations/index.mjs | 1 + tests/operations/tests/Modhex.mjs | 150 ++++++++++++++++++++++++++ 6 files changed, 458 insertions(+), 1 deletion(-) create mode 100644 src/core/lib/Modhex.mjs create mode 100644 src/core/operations/FromModhex.mjs create mode 100644 src/core/operations/ToModhex.mjs create mode 100644 tests/operations/tests/Modhex.mjs diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index bebdd6a5..34d3bfc8 100644 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -74,7 +74,9 @@ "CBOR Decode", "Caret/M-decode", "Rison Encode", - "Rison Decode" + "Rison Decode", + "To Modhex", + "From Modhex" ] }, { diff --git a/src/core/lib/Modhex.mjs b/src/core/lib/Modhex.mjs new file mode 100644 index 00000000..4f28e9a1 --- /dev/null +++ b/src/core/lib/Modhex.mjs @@ -0,0 +1,165 @@ +/** + * @author linuxgemini [ilteris@asenkron.com.tr] + * @copyright Crown Copyright 2024 + * @license Apache-2.0 + */ + +import Utils from "../Utils.mjs"; +import OperationError from "../errors/OperationError.mjs"; +import { fromHex, toHex } from "./Hex.mjs"; + +/** + * Modhex alphabet. + */ +const MODHEX_ALPHABET = "cbdefghijklnrtuv"; + + +/** + * Modhex alphabet map. + */ +const MODHEX_ALPHABET_MAP = MODHEX_ALPHABET.split(""); + + +/** + * Hex alphabet to substitute Modhex. + */ +const HEX_ALPHABET = "0123456789abcdef"; + + +/** + * Hex alphabet map to substitute Modhex. + */ +const HEX_ALPHABET_MAP = HEX_ALPHABET.split(""); + + +/** + * Convert a byte array into a modhex string. + * + * @param {byteArray|Uint8Array|ArrayBuffer} data + * @param {string} [delim=" "] + * @param {number} [padding=2] + * @returns {string} + * + * @example + * // returns "cl bf bu" + * toModhex([10,20,30]); + * + * // returns "cl:bf:bu" + * toModhex([10,20,30], ":"); + */ +export function toModhex(data, delim=" ", padding=2, extraDelim="", lineSize=0) { + if (!data) return ""; + if (data instanceof ArrayBuffer) data = new Uint8Array(data); + + const regularHexString = toHex(data, "", padding, "", 0); + + let modhexString = ""; + for (const letter of regularHexString.split("")) { + modhexString += MODHEX_ALPHABET_MAP[HEX_ALPHABET_MAP.indexOf(letter)]; + } + + let output = ""; + const groupingRegexp = new RegExp(`.{1,${padding}}`, "g"); + const groupedModhex = modhexString.match(groupingRegexp); + + for (let i = 0; i < groupedModhex.length; i++) { + const group = groupedModhex[i]; + output += group + delim; + + if (extraDelim) { + output += extraDelim; + } + // Add LF after each lineSize amount of bytes but not at the end + if ((i !== groupedModhex.length - 1) && ((i + 1) % lineSize === 0)) { + output += "\n"; + } + } + + // Remove the extraDelim at the end (if there is one) + // and remove the delim at the end, but if it's prepended there's nothing to remove + const rTruncLen = extraDelim.length + delim.length; + if (rTruncLen) { + // If rTruncLen === 0 then output.slice(0,0) will be returned, which is nothing + return output.slice(0, -rTruncLen); + } else { + return output; + } +} + + +/** + * Convert a byte array into a modhex string as efficiently as possible with no options. + * + * @param {byteArray|Uint8Array|ArrayBuffer} data + * @returns {string} + * + * @example + * // returns "clbfbu" + * toModhexFast([10,20,30]); + */ +export function toModhexFast(data) { + if (!data) return ""; + if (data instanceof ArrayBuffer) data = new Uint8Array(data); + + const output = []; + + for (let i = 0; i < data.length; i++) { + output.push(MODHEX_ALPHABET_MAP[(data[i] >> 4) & 0xf]); + output.push(MODHEX_ALPHABET_MAP[data[i] & 0xf]); + } + return output.join(""); +} + + +/** + * Convert a modhex string into a byte array. + * + * @param {string} data + * @param {string} [delim] + * @param {number} [byteLen=2] + * @returns {byteArray} + * + * @example + * // returns [10,20,30] + * fromModhex("cl bf bu"); + * + * // returns [10,20,30] + * fromModhex("cl:bf:bu", "Colon"); + */ +export function fromModhex(data, delim="Auto", byteLen=2) { + if (byteLen < 1 || Math.round(byteLen) !== byteLen) + throw new OperationError("Byte length must be a positive integer"); + + // The `.replace(/\s/g, "")` an interesting workaround: Hex "multiline" tests aren't actually + // multiline. Tests for Modhex fixes that, thus exposing the issue. + data = data.toLowerCase().replace(/\s/g, ""); + + if (delim !== "None") { + const delimRegex = delim === "Auto" ? /[^cbdefghijklnrtuv]/gi : Utils.regexRep(delim); + data = data.split(delimRegex); + } else { + data = [data]; + } + + let regularHexString = ""; + for (let i = 0; i < data.length; i++) { + for (const letter of data[i].split("")) { + regularHexString += HEX_ALPHABET_MAP[MODHEX_ALPHABET_MAP.indexOf(letter)]; + } + } + + const output = fromHex(regularHexString, "None", byteLen); + return output; +} + + +/** + * To Modhex delimiters. + */ +export const TO_MODHEX_DELIM_OPTIONS = ["Space", "Percent", "Comma", "Semi-colon", "Colon", "Line feed", "CRLF", "None"]; + + +/** + * From Modhex delimiters. + */ +export const FROM_MODHEX_DELIM_OPTIONS = ["Auto"].concat(TO_MODHEX_DELIM_OPTIONS); diff --git a/src/core/operations/FromModhex.mjs b/src/core/operations/FromModhex.mjs new file mode 100644 index 00000000..029d95d8 --- /dev/null +++ b/src/core/operations/FromModhex.mjs @@ -0,0 +1,84 @@ +/** + * @author linuxgemini [ilteris@asenkron.com.tr] + * @copyright Crown Copyright 2024 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import { FROM_MODHEX_DELIM_OPTIONS, fromModhex } from "../lib/Modhex.mjs"; + +/** + * From Modhex operation + */ +class FromModhex extends Operation { + + /** + * FromModhex constructor + */ + constructor() { + super(); + + this.name = "From Modhex"; + this.module = "Default"; + this.description = "Converts a modhex byte string back into its raw value."; + this.infoURL = "https://en.wikipedia.org/wiki/YubiKey#ModHex"; + this.inputType = "string"; + this.outputType = "byteArray"; + this.args = [ + { + name: "Delimiter", + type: "option", + value: FROM_MODHEX_DELIM_OPTIONS + } + ]; + this.checks = [ + { + pattern: "^(?:[cbdefghijklnrtuv]{2})+$", + flags: "i", + args: ["None"] + }, + { + pattern: "^[cbdefghijklnrtuv]{2}(?: [cbdefghijklnrtuv]{2})*$", + flags: "i", + args: ["Space"] + }, + { + pattern: "^[cbdefghijklnrtuv]{2}(?:,[cbdefghijklnrtuv]{2})*$", + flags: "i", + args: ["Comma"] + }, + { + pattern: "^[cbdefghijklnrtuv]{2}(?:;[cbdefghijklnrtuv]{2})*$", + flags: "i", + args: ["Semi-colon"] + }, + { + pattern: "^[cbdefghijklnrtuv]{2}(?::[cbdefghijklnrtuv]{2})*$", + flags: "i", + args: ["Colon"] + }, + { + pattern: "^[cbdefghijklnrtuv]{2}(?:\\n[cbdefghijklnrtuv]{2})*$", + flags: "i", + args: ["Line feed"] + }, + { + pattern: "^[cbdefghijklnrtuv]{2}(?:\\r\\n[cbdefghijklnrtuv]{2})*$", + flags: "i", + args: ["CRLF"] + } + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {byteArray} + */ + run(input, args) { + const delim = args[0] || "Auto"; + return fromModhex(input, delim, 2); + } +} + +export default FromModhex; diff --git a/src/core/operations/ToModhex.mjs b/src/core/operations/ToModhex.mjs new file mode 100644 index 00000000..6d91fb5d --- /dev/null +++ b/src/core/operations/ToModhex.mjs @@ -0,0 +1,55 @@ +/** + * @author linuxgemini [ilteris@asenkron.com.tr] + * @copyright Crown Copyright 2024 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import { TO_MODHEX_DELIM_OPTIONS, toModhex } from "../lib/Modhex.mjs"; +import Utils from "../Utils.mjs"; + +/** + * To Modhex operation + */ +class ToModhex extends Operation { + + /** + * ToModhex constructor + */ + constructor() { + super(); + + this.name = "To Modhex"; + this.module = "Default"; + this.description = "Converts the input string to modhex bytes separated by the specified delimiter."; + this.infoURL = "https://en.wikipedia.org/wiki/YubiKey#ModHex"; + this.inputType = "ArrayBuffer"; + this.outputType = "string"; + this.args = [ + { + name: "Delimiter", + type: "option", + value: TO_MODHEX_DELIM_OPTIONS + }, + { + name: "Bytes per line", + type: "number", + value: 0 + } + ]; + } + + /** + * @param {ArrayBuffer} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + const delim = Utils.charRep(args[0]); + const lineSize = args[1]; + + return toModhex(new Uint8Array(input), delim, 2, "", lineSize); + } +} + +export default ToModhex; diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs index 40ce7a2e..11e65e66 100644 --- a/tests/operations/index.mjs +++ b/tests/operations/index.mjs @@ -102,6 +102,7 @@ import "./tests/LZNT1Decompress.mjs"; import "./tests/LZString.mjs"; import "./tests/Magic.mjs"; import "./tests/Media.mjs"; +import "./tests/Modhex.mjs"; import "./tests/MorseCode.mjs"; import "./tests/MS.mjs"; import "./tests/MultipleBombe.mjs"; diff --git a/tests/operations/tests/Modhex.mjs b/tests/operations/tests/Modhex.mjs new file mode 100644 index 00000000..1e0f2791 --- /dev/null +++ b/tests/operations/tests/Modhex.mjs @@ -0,0 +1,150 @@ +/** + * Modhex operation tests. + * @author linuxgemini [ilteris@asenkron.com.tr] + * @copyright Crown Copyright 2024 + * @license Apache-2.0 + */ + +import TestRegister from "../../lib/TestRegister.mjs"; + +TestRegister.addTests([ + { + name: "ASCII to Modhex stream", + input: "aberystwyth", + expectedOutput: "hbhdhgidikieifiiikifhj", + recipeConfig: [ + { + "op": "To Modhex", + "args": [ + "None", + 0 + ] + }, + ] + }, + { + name: "ASCII to Modhex with colon deliminator", + input: "aberystwyth", + expectedOutput: "hb:hd:hg:id:ik:ie:if:ii:ik:if:hj", + recipeConfig: [ + { + "op": "To Modhex", + "args": [ + "Colon", + 0 + ] + } + ] + }, + { + name: "Modhex stream to UTF-8", + input: "uhkgkbuhkgkbugltlkugltkc", + expectedOutput: "救救孩子", + recipeConfig: [ + { + "op": "From Modhex", + "args": [ + "Auto" + ] + } + ] + + }, + { + name: "Mixed case Modhex stream to UTF-8", + input: "uhKGkbUHkgkBUGltlkugltkc", + expectedOutput: "救救孩子", + recipeConfig: [ + { + "op": "From Modhex", + "args": [ + "Auto" + ] + } + ] + + }, + { + name: "Mutiline Modhex with comma to ASCII (Auto Mode)", + input: "fk,dc,ie,hb,ii,dc,ht,ik,ie,hg,hr,hh,dc,ie,hk,\n\ +if,if,hk,hu,hi,dc,hk,hu,dc,if,hj,hg,dc,he,id,\n\ +hv,if,he,hj,dc,hv,hh,dc,if,hj,hg,dc,if,hj,hk,\n\ +ie,dc,hh,hk,hi,dc,if,id,hg,hg,dr,dc,ie,if,hb,\n\ +id,ih,hk,hu,hi,dc,if,hv,dc,hf,hg,hb,if,hj,dr,\n\ +dc,hl,ig,ie,if,dc,hd,hg,he,hb,ig,ie,hg,dc,fk,\n\ +dc,he,hv,ig,hr,hf,hu,di,if,dc,ht,hb,hn,hg,dc,\n\ +ig,ic,dc,ht,ik,dc,ht,hk,hu,hf,dc,ii,hj,hk,he,\n\ +hj,dc,hv,hh,dc,if,hj,hg,dc,hh,hk,hi,ie,dc,fk,\n\ +dc,ii,hv,ig,hr,hf,dc,he,hj,hv,hv,ie,hg,du", + expectedOutput: "I saw myself sitting in the crotch of the this fig tree, starving to death, just because I couldn't make up my mind which of the figs I would choose.", + recipeConfig: [ + { + "op": "From Modhex", + "args": [ + "Auto" + ] + } + ] + + }, + { + name: "Mutiline Modhex with percent to ASCII (Percent Mode)", + input: "fk%dc%ie%hb%ii%dc%ht%ik%ie%hg%hr%hh%dc%ie%hk%\n\ +if%if%hk%hu%hi%dc%hk%hu%dc%if%hj%hg%dc%he%id%\n\ +hv%if%he%hj%dc%hv%hh%dc%if%hj%hg%dc%if%hj%hk%\n\ +ie%dc%hh%hk%hi%dc%if%id%hg%hg%dr%dc%ie%if%hb%\n\ +id%ih%hk%hu%hi%dc%if%hv%dc%hf%hg%hb%if%hj%dr%\n\ +dc%hl%ig%ie%if%dc%hd%hg%he%hb%ig%ie%hg%dc%fk%\n\ +dc%he%hv%ig%hr%hf%hu%di%if%dc%ht%hb%hn%hg%dc%\n\ +ig%ic%dc%ht%ik%dc%ht%hk%hu%hf%dc%ii%hj%hk%he%\n\ +hj%dc%hv%hh%dc%if%hj%hg%dc%hh%hk%hi%ie%dc%fk%\n\ +dc%ii%hv%ig%hr%hf%dc%he%hj%hv%hv%ie%hg%du", + expectedOutput: "I saw myself sitting in the crotch of the this fig tree, starving to death, just because I couldn't make up my mind which of the figs I would choose.", + recipeConfig: [ + { + "op": "From Modhex", + "args": [ + "Percent" + ] + } + ] + + }, + { + name: "Mutiline Modhex with semicolon to ASCII (Semi-colon Mode)", + input: "fk;dc;ie;hb;ii;dc;ht;ik;ie;hg;hr;hh;dc;ie;hk;\n\ +if;if;hk;hu;hi;dc;hk;hu;dc;if;hj;hg;dc;he;id;\n\ +hv;if;he;hj;dc;hv;hh;dc;if;hj;hg;dc;if;hj;hk;\n\ +ie;dc;hh;hk;hi;dc;if;id;hg;hg;dr;dc;ie;if;hb;\n\ +id;ih;hk;hu;hi;dc;if;hv;dc;hf;hg;hb;if;hj;dr;\n\ +dc;hl;ig;ie;if;dc;hd;hg;he;hb;ig;ie;hg;dc;fk;\n\ +dc;he;hv;ig;hr;hf;hu;di;if;dc;ht;hb;hn;hg;dc;\n\ +ig;ic;dc;ht;ik;dc;ht;hk;hu;hf;dc;ii;hj;hk;he;\n\ +hj;dc;hv;hh;dc;if;hj;hg;dc;hh;hk;hi;ie;dc;fk;\n\ +dc;ii;hv;ig;hr;hf;dc;he;hj;hv;hv;ie;hg;du", + expectedOutput: "I saw myself sitting in the crotch of the this fig tree, starving to death, just because I couldn't make up my mind which of the figs I would choose.", + recipeConfig: [ + { + "op": "From Modhex", + "args": [ + "Semi-colon" + ] + } + ] + + }, + { + name: "ASCII to Modhex with comma and line breaks", + input: "aberystwyth", + expectedOutput: "hb,hd,hg,id,\nik,ie,if,ii,\nik,if,hj", + recipeConfig: [ + { + "op": "To Modhex", + "args": [ + "Comma", + 4 + ] + } + ] + }, +]);