Fix RC4/RC4Drop hex passphrase parsing and add format validation
This commit is contained in:
parent
9f87fec52d
commit
8882a481e7
@ -11,6 +11,7 @@ import {fromBase64, toBase64} from "./lib/Base64.mjs";
|
||||
import {fromHex} from "./lib/Hex.mjs";
|
||||
import {fromDecimal} from "./lib/Decimal.mjs";
|
||||
import {fromBinary} from "./lib/Binary.mjs";
|
||||
import OperationError from "./errors/OperationError.mjs";
|
||||
|
||||
/**
|
||||
* Utility functions for use in operations, the core framework and the stage.
|
||||
@ -321,6 +322,48 @@ class Utils {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Validates that a string contains only characters valid for the given format type.
|
||||
* Recognised delimiters (spaces, commas, colons, 0x prefix, etc.) are always
|
||||
* permitted in Hex and Binary formats.
|
||||
* Throws an OperationError if genuinely invalid characters are found.
|
||||
*
|
||||
* @param {string} str
|
||||
* @param {string} type - One of "Hex", "Base64", "Binary", "UTF8", "Latin1", etc.
|
||||
* @throws {OperationError}
|
||||
*/
|
||||
static validateFormatInput(str, type) {
|
||||
if (!str) return;
|
||||
switch (type.toLowerCase()) {
|
||||
case "hex": {
|
||||
const stripped = str.replace(/0x|\\x|%|[\s,;:\n\r]/gi, "");
|
||||
const invalid = stripped.match(/[^0-9a-fA-F]/);
|
||||
if (invalid) throw new OperationError(
|
||||
`Invalid character '${invalid[0]}' in Hex input. ` +
|
||||
`Hex accepts 0-9, a-f, A-F, and delimiters (space, comma, colon, 0x prefix).`
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "base64": {
|
||||
const invalid = str.replace(/[\s]/g, "").match(/[^A-Za-z0-9+/=]/);
|
||||
if (invalid) throw new OperationError(
|
||||
`Invalid character '${invalid[0]}' in Base64 input. ` +
|
||||
`Base64 accepts A-Z, a-z, 0-9, +, /, and = (padding).`
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "binary": {
|
||||
const stripped = str.replace(/[\s,;:\n\r]/g, "");
|
||||
const invalid = stripped.match(/[^01]/);
|
||||
if (invalid) throw new OperationError(
|
||||
`Invalid character '${invalid[0]}' in Binary input. Binary accepts only 0 and 1.`
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Coverts data of varying types to a byteArray.
|
||||
* Accepts hex, Base64, UTF8 and Latin1 strings.
|
||||
@ -340,6 +383,7 @@ class Utils {
|
||||
* Utils.convertToByteArray("0JfQtNGA0LDQstGB0YLQstGD0LnRgtC1", "base64");
|
||||
*/
|
||||
static convertToByteArray(str, type) {
|
||||
Utils.validateFormatInput(str, type);
|
||||
switch (type.toLowerCase()) {
|
||||
case "binary":
|
||||
return fromBinary(str);
|
||||
@ -377,6 +421,7 @@ class Utils {
|
||||
* Utils.convertToByteString("0JfQtNGA0LDQstGB0YLQstGD0LnRgtC1", "base64");
|
||||
*/
|
||||
static convertToByteString(str, type) {
|
||||
Utils.validateFormatInput(str, type);
|
||||
switch (type.toLowerCase()) {
|
||||
case "binary":
|
||||
return Utils.byteArrayToChars(fromBinary(str));
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
import OperationError from "../errors/OperationError.mjs";
|
||||
import Utils from "../Utils.mjs";
|
||||
import CryptoJS from "crypto-js";
|
||||
import { fromHex, toHexFast } from "./Hex.mjs";
|
||||
|
||||
/**
|
||||
* Affine Cipher Encode operation.
|
||||
@ -86,3 +87,22 @@ export const format = {
|
||||
"UTF16BE": CryptoJS.enc.Utf16BE,
|
||||
"Latin1": CryptoJS.enc.Latin1,
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Parses a user-entered string in a given CryptoJS format, normalising common
|
||||
* hex delimiter conventions (commas, spaces, 0x prefix, etc.) before parsing.
|
||||
*
|
||||
* Use this instead of format[name].parse() for user-supplied passphrase/key inputs.
|
||||
*
|
||||
* @param {string} str
|
||||
* @param {string} formatName - Key into the format map (e.g. "Hex", "UTF8")
|
||||
* @returns {CryptoJS.lib.WordArray}
|
||||
*/
|
||||
export function parseFormatString(str, formatName) {
|
||||
Utils.validateFormatInput(str, formatName);
|
||||
if (formatName === "Hex") {
|
||||
return CryptoJS.enc.Hex.parse(toHexFast(fromHex(str)));
|
||||
}
|
||||
return format[formatName].parse(str);
|
||||
}
|
||||
|
||||
@ -121,27 +121,3 @@ CryptoJS.kdf.OpenSSL.execute = function (password, keySize, ivSize, salt) {
|
||||
// Return params
|
||||
return CryptoJS.lib.CipherParams.create({ key: key, iv: iv, salt: salt });
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Override for the CryptoJS Hex encoding parser to remove whitespace before attempting to parse
|
||||
* the hex string.
|
||||
*
|
||||
* @param {string} hexStr
|
||||
* @returns {CryptoJS.lib.WordArray}
|
||||
*/
|
||||
CryptoJS.enc.Hex.parse = function (hexStr) {
|
||||
// Remove whitespace
|
||||
hexStr = hexStr.replace(/\s/g, "");
|
||||
|
||||
// Shortcut
|
||||
const hexStrLength = hexStr.length;
|
||||
|
||||
// Convert
|
||||
const words = [];
|
||||
for (let i = 0; i < hexStrLength; i += 2) {
|
||||
words[i >>> 3] |= parseInt(hexStr.substr(i, 2), 16) << (24 - (i % 8) * 4);
|
||||
}
|
||||
|
||||
return new CryptoJS.lib.WordArray.init(words, hexStrLength / 2);
|
||||
};
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
|
||||
import Operation from "../Operation.mjs";
|
||||
import CryptoJS from "crypto-js";
|
||||
import { format } from "../lib/Ciphers.mjs";
|
||||
import { format, parseFormatString } from "../lib/Ciphers.mjs";
|
||||
|
||||
/**
|
||||
* RC4 operation
|
||||
@ -52,7 +52,7 @@ class RC4 extends Operation {
|
||||
*/
|
||||
run(input, args) {
|
||||
const message = format[args[1]].parse(input),
|
||||
passphrase = format[args[0].option].parse(args[0].string),
|
||||
passphrase = parseFormatString(args[0].string, args[0].option),
|
||||
encrypted = CryptoJS.RC4.encrypt(message, passphrase);
|
||||
|
||||
return encrypted.ciphertext.toString(format[args[2]]);
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import Operation from "../Operation.mjs";
|
||||
import { format } from "../lib/Ciphers.mjs";
|
||||
import { format, parseFormatString } from "../lib/Ciphers.mjs";
|
||||
import CryptoJS from "crypto-js";
|
||||
|
||||
/**
|
||||
@ -57,7 +57,7 @@ class RC4Drop extends Operation {
|
||||
*/
|
||||
run(input, args) {
|
||||
const message = format[args[1]].parse(input),
|
||||
passphrase = format[args[0].option].parse(args[0].string),
|
||||
passphrase = parseFormatString(args[0].string, args[0].option),
|
||||
drop = args[3],
|
||||
encrypted = CryptoJS.RC4Drop.encrypt(message, passphrase, { drop: drop });
|
||||
|
||||
|
||||
@ -658,7 +658,7 @@ WWFkYSBZYWRh\r
|
||||
}),
|
||||
|
||||
it("HMAC", () => {
|
||||
assert.strictEqual(chef.HMAC("On Cloud Nine", {key: "idea"}).toString(), "e15c268b4ee755c9e52db094ed50add7");
|
||||
assert.strictEqual(chef.HMAC("On Cloud Nine", {key: {string: "idea", option: "Latin1"}}).toString(), "b128b48ec0d6b0f1a27220c396d0f3e5");
|
||||
}),
|
||||
|
||||
it("JPathExpression", () => {
|
||||
@ -882,8 +882,8 @@ pCGTErs=
|
||||
|
||||
it("Scrypt", () => {
|
||||
assert.strictEqual(
|
||||
chef.scrypt("Playing For Keeps", {salt: {string: "salty", option: "Hex"}}).toString(),
|
||||
"5446b6d86d88515894a163201765bceed0bc39610b1506cdc4d939ffc638bc46e051bce756e2865165d89d955a43a7eb5504502567dea8bfc9e7d49aaa894c07");
|
||||
chef.scrypt("Playing For Keeps", {salt: {string: "salty", option: "UTF8"}}).toString(),
|
||||
"2f1417e682a7bf008c60abf8e8b8dc33bde9eead9290f9f03d83a8acb49392cfd22ad1425db88f821c67f06a12bb203ccdd47ca5109b8364451d6ebf2215bbdf");
|
||||
}),
|
||||
|
||||
it("SHA3", () => {
|
||||
|
||||
148
tests/operations/tests/RC4.mjs
Normal file
148
tests/operations/tests/RC4.mjs
Normal file
@ -0,0 +1,148 @@
|
||||
/**
|
||||
* RC4 tests.
|
||||
*
|
||||
* @author Stuart Wilson
|
||||
* @copyright Crown Copyright 2025
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
import TestRegister from "../../lib/TestRegister.mjs";
|
||||
|
||||
TestRegister.addTests([
|
||||
{
|
||||
"name": "RC4: Hex passphrase, clean hex (1f10)",
|
||||
"input": "test",
|
||||
"expectedOutput": "8aee3802",
|
||||
"recipeConfig": [
|
||||
{
|
||||
"op": "RC4",
|
||||
"args": [
|
||||
{"option": "Hex", "string": "1f10"},
|
||||
"Latin1",
|
||||
"Hex"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "RC4: Hex passphrase, comma-delimited (1f,10) equals clean hex",
|
||||
"input": "test",
|
||||
"expectedOutput": "8aee3802",
|
||||
"recipeConfig": [
|
||||
{
|
||||
"op": "RC4",
|
||||
"args": [
|
||||
{"option": "Hex", "string": "1f,10"},
|
||||
"Latin1",
|
||||
"Hex"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "RC4: Hex passphrase, 0x-prefixed (0x1f,0x10) equals clean hex",
|
||||
"input": "test",
|
||||
"expectedOutput": "8aee3802",
|
||||
"recipeConfig": [
|
||||
{
|
||||
"op": "RC4",
|
||||
"args": [
|
||||
{"option": "Hex", "string": "0x1f,0x10"},
|
||||
"Latin1",
|
||||
"Hex"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "RC4: Hex passphrase, space-delimited (1f 10) equals clean hex",
|
||||
"input": "test",
|
||||
"expectedOutput": "8aee3802",
|
||||
"recipeConfig": [
|
||||
{
|
||||
"op": "RC4",
|
||||
"args": [
|
||||
{"option": "Hex", "string": "1f 10"},
|
||||
"Latin1",
|
||||
"Hex"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "RC4: Hex passphrase, colon-delimited uppercase (1F:10) equals clean hex",
|
||||
"input": "test",
|
||||
"expectedOutput": "8aee3802",
|
||||
"recipeConfig": [
|
||||
{
|
||||
"op": "RC4",
|
||||
"args": [
|
||||
{"option": "Hex", "string": "1F:10"},
|
||||
"Latin1",
|
||||
"Hex"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "RC4: invalid hex character in passphrase shows error in output",
|
||||
"input": "test",
|
||||
"expectedOutput": "Invalid character 'G' in Hex input. Hex accepts 0-9, a-f, A-F, and delimiters (space, comma, colon, 0x prefix).",
|
||||
"recipeConfig": [
|
||||
{
|
||||
"op": "RC4",
|
||||
"args": [
|
||||
{"option": "Hex", "string": "1fG0"},
|
||||
"Latin1",
|
||||
"Hex"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "RC4: UTF8 passphrase still works",
|
||||
"input": "test",
|
||||
"expectedOutput": "8b904b7a",
|
||||
"recipeConfig": [
|
||||
{
|
||||
"op": "RC4",
|
||||
"args": [
|
||||
{"option": "UTF8", "string": "password"},
|
||||
"Latin1",
|
||||
"Hex"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "RC4 Drop: Hex passphrase, comma-delimited (1f,10) equals clean hex",
|
||||
"input": "test",
|
||||
"expectedOutput": "47055271",
|
||||
"recipeConfig": [
|
||||
{
|
||||
"op": "RC4 Drop",
|
||||
"args": [
|
||||
{"option": "Hex", "string": "1f,10"},
|
||||
"Latin1",
|
||||
"Hex",
|
||||
192
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "RC4 Drop: Hex passphrase, 0x-prefixed equals clean hex",
|
||||
"input": "test",
|
||||
"expectedOutput": "47055271",
|
||||
"recipeConfig": [
|
||||
{
|
||||
"op": "RC4 Drop",
|
||||
"args": [
|
||||
{"option": "Hex", "string": "0x1f,0x10"},
|
||||
"Latin1",
|
||||
"Hex",
|
||||
192
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]);
|
||||
Loading…
x
Reference in New Issue
Block a user