Skip to content

Commit 1cfbc2b

Browse files
authored
Merge pull request #1105 from linuxgemini/linuxgemini-patch-modhex
Introduce Yubico's Modhex for Conversion
2 parents bcf62ec + eb91254 commit 1cfbc2b

File tree

6 files changed

+458
-1
lines changed

6 files changed

+458
-1
lines changed

Diff for: src/core/config/Categories.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,9 @@
7474
"CBOR Decode",
7575
"Caret/M-decode",
7676
"Rison Encode",
77-
"Rison Decode"
77+
"Rison Decode",
78+
"To Modhex",
79+
"From Modhex"
7880
]
7981
},
8082
{

Diff for: src/core/lib/Modhex.mjs

+165
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
/**
2+
* @author linuxgemini [[email protected]]
3+
* @copyright Crown Copyright 2024
4+
* @license Apache-2.0
5+
*/
6+
7+
import Utils from "../Utils.mjs";
8+
import OperationError from "../errors/OperationError.mjs";
9+
import { fromHex, toHex } from "./Hex.mjs";
10+
11+
/**
12+
* Modhex alphabet.
13+
*/
14+
const MODHEX_ALPHABET = "cbdefghijklnrtuv";
15+
16+
17+
/**
18+
* Modhex alphabet map.
19+
*/
20+
const MODHEX_ALPHABET_MAP = MODHEX_ALPHABET.split("");
21+
22+
23+
/**
24+
* Hex alphabet to substitute Modhex.
25+
*/
26+
const HEX_ALPHABET = "0123456789abcdef";
27+
28+
29+
/**
30+
* Hex alphabet map to substitute Modhex.
31+
*/
32+
const HEX_ALPHABET_MAP = HEX_ALPHABET.split("");
33+
34+
35+
/**
36+
* Convert a byte array into a modhex string.
37+
*
38+
* @param {byteArray|Uint8Array|ArrayBuffer} data
39+
* @param {string} [delim=" "]
40+
* @param {number} [padding=2]
41+
* @returns {string}
42+
*
43+
* @example
44+
* // returns "cl bf bu"
45+
* toModhex([10,20,30]);
46+
*
47+
* // returns "cl:bf:bu"
48+
* toModhex([10,20,30], ":");
49+
*/
50+
export function toModhex(data, delim=" ", padding=2, extraDelim="", lineSize=0) {
51+
if (!data) return "";
52+
if (data instanceof ArrayBuffer) data = new Uint8Array(data);
53+
54+
const regularHexString = toHex(data, "", padding, "", 0);
55+
56+
let modhexString = "";
57+
for (const letter of regularHexString.split("")) {
58+
modhexString += MODHEX_ALPHABET_MAP[HEX_ALPHABET_MAP.indexOf(letter)];
59+
}
60+
61+
let output = "";
62+
const groupingRegexp = new RegExp(`.{1,${padding}}`, "g");
63+
const groupedModhex = modhexString.match(groupingRegexp);
64+
65+
for (let i = 0; i < groupedModhex.length; i++) {
66+
const group = groupedModhex[i];
67+
output += group + delim;
68+
69+
if (extraDelim) {
70+
output += extraDelim;
71+
}
72+
// Add LF after each lineSize amount of bytes but not at the end
73+
if ((i !== groupedModhex.length - 1) && ((i + 1) % lineSize === 0)) {
74+
output += "\n";
75+
}
76+
}
77+
78+
// Remove the extraDelim at the end (if there is one)
79+
// and remove the delim at the end, but if it's prepended there's nothing to remove
80+
const rTruncLen = extraDelim.length + delim.length;
81+
if (rTruncLen) {
82+
// If rTruncLen === 0 then output.slice(0,0) will be returned, which is nothing
83+
return output.slice(0, -rTruncLen);
84+
} else {
85+
return output;
86+
}
87+
}
88+
89+
90+
/**
91+
* Convert a byte array into a modhex string as efficiently as possible with no options.
92+
*
93+
* @param {byteArray|Uint8Array|ArrayBuffer} data
94+
* @returns {string}
95+
*
96+
* @example
97+
* // returns "clbfbu"
98+
* toModhexFast([10,20,30]);
99+
*/
100+
export function toModhexFast(data) {
101+
if (!data) return "";
102+
if (data instanceof ArrayBuffer) data = new Uint8Array(data);
103+
104+
const output = [];
105+
106+
for (let i = 0; i < data.length; i++) {
107+
output.push(MODHEX_ALPHABET_MAP[(data[i] >> 4) & 0xf]);
108+
output.push(MODHEX_ALPHABET_MAP[data[i] & 0xf]);
109+
}
110+
return output.join("");
111+
}
112+
113+
114+
/**
115+
* Convert a modhex string into a byte array.
116+
*
117+
* @param {string} data
118+
* @param {string} [delim]
119+
* @param {number} [byteLen=2]
120+
* @returns {byteArray}
121+
*
122+
* @example
123+
* // returns [10,20,30]
124+
* fromModhex("cl bf bu");
125+
*
126+
* // returns [10,20,30]
127+
* fromModhex("cl:bf:bu", "Colon");
128+
*/
129+
export function fromModhex(data, delim="Auto", byteLen=2) {
130+
if (byteLen < 1 || Math.round(byteLen) !== byteLen)
131+
throw new OperationError("Byte length must be a positive integer");
132+
133+
// The `.replace(/\s/g, "")` an interesting workaround: Hex "multiline" tests aren't actually
134+
// multiline. Tests for Modhex fixes that, thus exposing the issue.
135+
data = data.toLowerCase().replace(/\s/g, "");
136+
137+
if (delim !== "None") {
138+
const delimRegex = delim === "Auto" ? /[^cbdefghijklnrtuv]/gi : Utils.regexRep(delim);
139+
data = data.split(delimRegex);
140+
} else {
141+
data = [data];
142+
}
143+
144+
let regularHexString = "";
145+
for (let i = 0; i < data.length; i++) {
146+
for (const letter of data[i].split("")) {
147+
regularHexString += HEX_ALPHABET_MAP[MODHEX_ALPHABET_MAP.indexOf(letter)];
148+
}
149+
}
150+
151+
const output = fromHex(regularHexString, "None", byteLen);
152+
return output;
153+
}
154+
155+
156+
/**
157+
* To Modhex delimiters.
158+
*/
159+
export const TO_MODHEX_DELIM_OPTIONS = ["Space", "Percent", "Comma", "Semi-colon", "Colon", "Line feed", "CRLF", "None"];
160+
161+
162+
/**
163+
* From Modhex delimiters.
164+
*/
165+
export const FROM_MODHEX_DELIM_OPTIONS = ["Auto"].concat(TO_MODHEX_DELIM_OPTIONS);

Diff for: src/core/operations/FromModhex.mjs

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/**
2+
* @author linuxgemini [[email protected]]
3+
* @copyright Crown Copyright 2024
4+
* @license Apache-2.0
5+
*/
6+
7+
import Operation from "../Operation.mjs";
8+
import { FROM_MODHEX_DELIM_OPTIONS, fromModhex } from "../lib/Modhex.mjs";
9+
10+
/**
11+
* From Modhex operation
12+
*/
13+
class FromModhex extends Operation {
14+
15+
/**
16+
* FromModhex constructor
17+
*/
18+
constructor() {
19+
super();
20+
21+
this.name = "From Modhex";
22+
this.module = "Default";
23+
this.description = "Converts a modhex byte string back into its raw value.";
24+
this.infoURL = "https://en.wikipedia.org/wiki/YubiKey#ModHex";
25+
this.inputType = "string";
26+
this.outputType = "byteArray";
27+
this.args = [
28+
{
29+
name: "Delimiter",
30+
type: "option",
31+
value: FROM_MODHEX_DELIM_OPTIONS
32+
}
33+
];
34+
this.checks = [
35+
{
36+
pattern: "^(?:[cbdefghijklnrtuv]{2})+$",
37+
flags: "i",
38+
args: ["None"]
39+
},
40+
{
41+
pattern: "^[cbdefghijklnrtuv]{2}(?: [cbdefghijklnrtuv]{2})*$",
42+
flags: "i",
43+
args: ["Space"]
44+
},
45+
{
46+
pattern: "^[cbdefghijklnrtuv]{2}(?:,[cbdefghijklnrtuv]{2})*$",
47+
flags: "i",
48+
args: ["Comma"]
49+
},
50+
{
51+
pattern: "^[cbdefghijklnrtuv]{2}(?:;[cbdefghijklnrtuv]{2})*$",
52+
flags: "i",
53+
args: ["Semi-colon"]
54+
},
55+
{
56+
pattern: "^[cbdefghijklnrtuv]{2}(?::[cbdefghijklnrtuv]{2})*$",
57+
flags: "i",
58+
args: ["Colon"]
59+
},
60+
{
61+
pattern: "^[cbdefghijklnrtuv]{2}(?:\\n[cbdefghijklnrtuv]{2})*$",
62+
flags: "i",
63+
args: ["Line feed"]
64+
},
65+
{
66+
pattern: "^[cbdefghijklnrtuv]{2}(?:\\r\\n[cbdefghijklnrtuv]{2})*$",
67+
flags: "i",
68+
args: ["CRLF"]
69+
}
70+
];
71+
}
72+
73+
/**
74+
* @param {string} input
75+
* @param {Object[]} args
76+
* @returns {byteArray}
77+
*/
78+
run(input, args) {
79+
const delim = args[0] || "Auto";
80+
return fromModhex(input, delim, 2);
81+
}
82+
}
83+
84+
export default FromModhex;

Diff for: src/core/operations/ToModhex.mjs

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/**
2+
* @author linuxgemini [[email protected]]
3+
* @copyright Crown Copyright 2024
4+
* @license Apache-2.0
5+
*/
6+
7+
import Operation from "../Operation.mjs";
8+
import { TO_MODHEX_DELIM_OPTIONS, toModhex } from "../lib/Modhex.mjs";
9+
import Utils from "../Utils.mjs";
10+
11+
/**
12+
* To Modhex operation
13+
*/
14+
class ToModhex extends Operation {
15+
16+
/**
17+
* ToModhex constructor
18+
*/
19+
constructor() {
20+
super();
21+
22+
this.name = "To Modhex";
23+
this.module = "Default";
24+
this.description = "Converts the input string to modhex bytes separated by the specified delimiter.";
25+
this.infoURL = "https://en.wikipedia.org/wiki/YubiKey#ModHex";
26+
this.inputType = "ArrayBuffer";
27+
this.outputType = "string";
28+
this.args = [
29+
{
30+
name: "Delimiter",
31+
type: "option",
32+
value: TO_MODHEX_DELIM_OPTIONS
33+
},
34+
{
35+
name: "Bytes per line",
36+
type: "number",
37+
value: 0
38+
}
39+
];
40+
}
41+
42+
/**
43+
* @param {ArrayBuffer} input
44+
* @param {Object[]} args
45+
* @returns {string}
46+
*/
47+
run(input, args) {
48+
const delim = Utils.charRep(args[0]);
49+
const lineSize = args[1];
50+
51+
return toModhex(new Uint8Array(input), delim, 2, "", lineSize);
52+
}
53+
}
54+
55+
export default ToModhex;

Diff for: tests/operations/index.mjs

+1
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ import "./tests/LZNT1Decompress.mjs";
104104
import "./tests/LZString.mjs";
105105
import "./tests/Magic.mjs";
106106
import "./tests/Media.mjs";
107+
import "./tests/Modhex.mjs";
107108
import "./tests/MorseCode.mjs";
108109
import "./tests/MS.mjs";
109110
import "./tests/MultipleBombe.mjs";

0 commit comments

Comments
 (0)