Narrow hex/base64/binary format validation to RC4/RC4Drop only

Move validateFormatInput() out of Utils.convertToByteArray()/convertToByteString(),
which are shared by ~50 operations, and into Ciphers.mjs where it's only
reachable via parseFormatString() (used by RC4/RC4Drop). This keeps the fix
scoped to issue #2004 instead of changing accepted input globally, and avoids
the unrelated HMAC/Scrypt regressions the broader validation caused.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Allan Leary 2026-07-02 09:42:22 +01:00
parent 8882a481e7
commit 075d84ef43
3 changed files with 46 additions and 49 deletions

View File

@ -11,7 +11,6 @@ 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.
@ -322,48 +321,6 @@ 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.
@ -383,7 +340,6 @@ class Utils {
* Utils.convertToByteArray("0JfQtNGA0LDQstGB0YLQstGD0LnRgtC1", "base64");
*/
static convertToByteArray(str, type) {
Utils.validateFormatInput(str, type);
switch (type.toLowerCase()) {
case "binary":
return fromBinary(str);
@ -421,7 +377,6 @@ class Utils {
* Utils.convertToByteString("0JfQtNGA0LDQstGB0YLQstGD0LnRgtC1", "base64");
*/
static convertToByteString(str, type) {
Utils.validateFormatInput(str, type);
switch (type.toLowerCase()) {
case "binary":
return Utils.byteArrayToChars(fromBinary(str));

View File

@ -89,6 +89,48 @@ export const format = {
};
/**
* Validates that a passphrase/key string contains only characters valid for the
* given format. 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}
*/
function 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;
}
}
}
/**
* Parses a user-entered string in a given CryptoJS format, normalising common
* hex delimiter conventions (commas, spaces, 0x prefix, etc.) before parsing.
@ -100,7 +142,7 @@ export const format = {
* @returns {CryptoJS.lib.WordArray}
*/
export function parseFormatString(str, formatName) {
Utils.validateFormatInput(str, formatName);
validateFormatInput(str, formatName);
if (formatName === "Hex") {
return CryptoJS.enc.Hex.parse(toHexFast(fromHex(str)));
}

View File

@ -658,7 +658,7 @@ WWFkYSBZYWRh\r
}),
it("HMAC", () => {
assert.strictEqual(chef.HMAC("On Cloud Nine", {key: {string: "idea", option: "Latin1"}}).toString(), "b128b48ec0d6b0f1a27220c396d0f3e5");
assert.strictEqual(chef.HMAC("On Cloud Nine", {key: "idea"}).toString(), "e15c268b4ee755c9e52db094ed50add7");
}),
it("JPathExpression", () => {
@ -882,8 +882,8 @@ pCGTErs=
it("Scrypt", () => {
assert.strictEqual(
chef.scrypt("Playing For Keeps", {salt: {string: "salty", option: "UTF8"}}).toString(),
"2f1417e682a7bf008c60abf8e8b8dc33bde9eead9290f9f03d83a8acb49392cfd22ad1425db88f821c67f06a12bb203ccdd47ca5109b8364451d6ebf2215bbdf");
chef.scrypt("Playing For Keeps", {salt: {string: "salty", option: "Hex"}}).toString(),
"5446b6d86d88515894a163201765bceed0bc39610b1506cdc4d939ffc638bc46e051bce756e2865165d89d955a43a7eb5504502567dea8bfc9e7d49aaa894c07");
}),
it("SHA3", () => {