Merge branch 'master' into master

This commit is contained in:
rebase 2026-02-14 17:56:37 -06:00 committed by GitHub
commit daa027b4e9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 323 additions and 133 deletions

View file

@ -1,18 +1,19 @@
name: "Master Build, Test & Deploy"
permissions:
contents: read
on:
workflow_dispatch:
push:
branches:
- master
permissions:
contents: read
jobs:
main:
permissions:
contents: write
pages: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

View file

@ -7,7 +7,6 @@ on:
- "v*"
permissions:
id-token: write
contents: read
env:
@ -18,6 +17,9 @@ env:
jobs:
main:
permissions:
packages: write
contents: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
@ -93,5 +95,32 @@ jobs:
file_glob: true
body: "See the [CHANGELOG](https://github.com/gchq/CyberChef/blob/master/CHANGELOG.md) and [commit messages](https://github.com/gchq/CyberChef/commits/master) for details."
npm-publish:
permissions:
id-token: write
contents: read
needs: main
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Set node version
uses: actions/setup-node@v6
with:
node-version: 18
registry-url: "https://registry.npmjs.org"
- name: Install
run: npm ci
- name: Create machine generated files
run: npm run node
- name: Reset node version ready for publish
uses: actions/setup-node@v6
with:
node-version: ^24.5
registry-url: "https://registry.npmjs.org"
- name: Publish to NPM
run: npm publish

View file

@ -13,6 +13,35 @@ All major and minor version changes will be documented in this file. Details of
## Details
### [10.22.0] - 2026-02-11
- Separate npm publish out into separate job and run with Node 24.5 [@GCHQDeveloper581] | [#2188]
- Fixed Percent delimiter for hex encoding [@beneri] [@C85297] | [#2137]
- Added the ability to paste one or more Images from the Clipboard [@t-martine] [@a3957273] [@C85297] | [#1876]
- Quoted Printable - consistent reference to 'email' [@wesinator] | [#2186]
- Fix freeze when output text decoding fails [@Raka-loah] | [#1573]
- Update Browserslist DB [@C85297] | [#2183]
- Add contents write permission to releases workflow [@C85297] | [#2182]
- Fix release workflow permissions [@C85297] | [#2181]
### [10.21.0] - 2026-02-05
- Fix import operations with special chars in them [@d98762625] [@jg42526] | [#1040]
- Remove custom CodeQL workflow [@C85297] | [#2176]
- Fix code scanning warnings in workflows [@GCHQDeveloper581] | [#2177]
- Use NPM trusted publishing [@C85297] [@GCHQDeveloper581] | [#2174]
- Fix: Correctly parse xxd odd byte hexdumps [@ThomasNotTom] [@GCHQDeveloper581] | [#2058]
- Update Sitemap URLs to Use Valid Paths in sitemap.mjs [@rbpi] [@C85297] | [#1861]
- Use recommended GitHub Actions to build image [@AlexGustafsson] [@C85297] | [#2055]
- Remove version 10 message from banner [@C85297] | [#2169]
- Bump form-data from 4.0.1 to 4.0.5 | [#2175]
- Bump node-forge from 1.3.1 to 1.3.3 | [#2173]
- Update crypto browserify [@C85297] | [#2172]
- Update kbpgp package (resolves #2135) [@GCHQDeveloper581] | [#2136]
- Fix the processing of ALPNs for JA4 to align with new specification update [@tuliperis] | [#2165]
- Add Bech32 and Bech32m encoding/decoding operations [@thomasxm] | [#2159]
- Exclude Delete character from hex dump output [@mikecat] [@C85297] | [#2086]
- Tiny typo fix in "To Base85" operation [@twostraws] | [#2118]
- Bump jsonpath-plus [@C85297] | [#2166]
### [10.20.0] - 2026-01-28
- Fixed Optical Character Recognition and added tests [@n1474335] | [ab37c1e]
- Fixed JA4 version fallback value [@n1474335] | [7a5225c]
@ -509,6 +538,8 @@ All major and minor version changes will be documented in this file. Details of
## [4.0.0] - 2016-11-28
- Initial open source commit [@n1474335] | [b1d73a72](https://github.com/gchq/CyberChef/commit/b1d73a725dc7ab9fb7eb789296efd2b7e4b08306)
[10.22.0]: https://github.com/gchq/CyberChef/releases/tag/v10.22.0
[10.21.0]: https://github.com/gchq/CyberChef/releases/tag/v10.21.0
[10.20.0]: https://github.com/gchq/CyberChef/releases/tag/v10.20.0
[10.19.0]: https://github.com/gchq/CyberChef/releases/tag/v10.19.0
[10.18.0]: https://github.com/gchq/CyberChef/releases/tag/v10.18.0
@ -754,6 +785,18 @@ All major and minor version changes will be documented in this file. Details of
[@remingtr]: https://github.com/remingtr
[@0xff1ce]: https://github.com/0xff1ce
[@starplanet]: https://github.com/starplanet
[@C85297]: https://github.com/C85297
[@GCHQDeveloper581]: https://github.com/GCHQDeveloper581
[@ThomasNotTom]: https://github.com/ThomasNotTom
[@rbpi]: https://github.com/rbpi
[@AlexGustafsson]: https://github.com/AlexGustafsson
[@tuliperis]: https://github.com/tuliperis
[@thomasxm]: https://github.com/thomasxm
[@twostraws]: https://github.com/twostraws
[@beneri]: https://github.com/beneri
[@t-martine]: https://github.com/t-martine
[@wesinator]: https://github.com/wesinator
[@Raka-loah]: https://github.com/Raka-loah
[8ad18b]: https://github.com/gchq/CyberChef/commit/8ad18bc7db6d9ff184ba3518686293a7685bf7b7
@ -942,3 +985,29 @@ All major and minor version changes will be documented in this file. Details of
[#512]: https://github.com/gchq/CyberChef/issues/512
[#1732]: https://github.com/gchq/CyberChef/issues/1732
[#1789]: https://github.com/gchq/CyberChef/issues/1789
[#1040]: https://github.com/gchq/CyberChef/pull/1040
[#2176]: https://github.com/gchq/CyberChef/pull/2176
[#2177]: https://github.com/gchq/CyberChef/pull/2177
[#2174]: https://github.com/gchq/CyberChef/pull/2174
[#2058]: https://github.com/gchq/CyberChef/pull/2058
[#1861]: https://github.com/gchq/CyberChef/pull/1861
[#2055]: https://github.com/gchq/CyberChef/pull/2055
[#2169]: https://github.com/gchq/CyberChef/pull/2169
[#2175]: https://github.com/gchq/CyberChef/pull/2175
[#2173]: https://github.com/gchq/CyberChef/pull/2173
[#2172]: https://github.com/gchq/CyberChef/pull/2172
[#2136]: https://github.com/gchq/CyberChef/pull/2136
[#2165]: https://github.com/gchq/CyberChef/pull/2165
[#2159]: https://github.com/gchq/CyberChef/pull/2159
[#2086]: https://github.com/gchq/CyberChef/pull/2086
[#2118]: https://github.com/gchq/CyberChef/pull/2118
[#2166]: https://github.com/gchq/CyberChef/pull/2166
[#2188]: https://github.com/gchq/CyberChef/pull/2188
[#2137]: https://github.com/gchq/CyberChef/pull/2137
[#1876]: https://github.com/gchq/CyberChef/pull/1876
[#2186]: https://github.com/gchq/CyberChef/pull/2186
[#1573]: https://github.com/gchq/CyberChef/pull/1573
[#2183]: https://github.com/gchq/CyberChef/pull/2183
[#2182]: https://github.com/gchq/CyberChef/pull/2182
[#2181]: https://github.com/gchq/CyberChef/pull/2181

10
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "cyberchef",
"version": "10.20.0",
"version": "10.22.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cyberchef",
"version": "10.20.0",
"version": "10.22.1",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
@ -6223,9 +6223,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001695",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001695.tgz",
"integrity": "sha512-vHyLade6wTgI2u1ec3WQBxv+2BrTERV28UXQu9LO6lZ9pYeMk34vjXFLOxo1A4UBA8XTL4njRQZdno/yYaSmWw==",
"version": "1.0.30001769",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz",
"integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==",
"dev": true,
"funding": [
{

View file

@ -1,6 +1,6 @@
{
"name": "cyberchef",
"version": "10.20.0",
"version": "10.22.1",
"description": "The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.",
"author": "n1474335 <n1474335@gmail.com>",
"homepage": "https://gchq.github.io/CyberChef",
@ -202,7 +202,8 @@
"lint:grammar": "cspell ./src",
"postinstall": "npx grunt exec:fixCryptoApiImports && npx grunt exec:fixSnackbarMarkup && npx grunt exec:fixJimpModule",
"newop": "node --experimental-modules --experimental-json-modules src/core/config/scripts/newOperation.mjs",
"minor": "node --experimental-modules --experimental-json-modules src/core/config/scripts/newMinorVersion.mjs",
"minor": "node --experimental-modules --experimental-json-modules src/core/config/scripts/newMinorVersion.mjs && npm version minor --git-tag-version=false && echo \"Updated to version v$(npm pkg get version | xargs), please create a pull request and once merged use 'npm run tag'\"",
"tag": "git tag -s \"v$(npm pkg get version | xargs)\" -m \"$(npm pkg get version | xargs)\" && echo \"Created v$(npm pkg get version | xargs), now check and push the tag\"",
"getheapsize": "node -e 'console.log(`node heap limit = ${require(\"v8\").getHeapStatistics().heap_size_limit / (1024 * 1024)} Mb`)'",
"setheapsize": "export NODE_OPTIONS=--max_old_space_size=2048"
}

View file

@ -7,138 +7,197 @@
*/
/* eslint no-console: ["off"] */
/* eslint jsdoc/require-jsdoc: ["off"] */
import prompt from "prompt";
import colors from "colors";
import path from "path";
import fs from "fs";
import fs from "fs";
import process from "process";
import { execSync } from "child_process";
const dir = path.join(process.cwd() + "/src/core/config/");
if (!fs.existsSync(dir)) {
console.log("\nCWD: " + process.cwd());
console.log("Error: newMinorVersion.mjs should be run from the project root");
console.log("Example> node --experimental-modules src/core/config/scripts/newMinorVersion.mjs");
process.exit(1);
}
const ignoredAuthors = ["github-advanced-security[bot]", "dependabot[bot]"];
let changelogData = fs.readFileSync(path.join(process.cwd(), "CHANGELOG.md"), "utf8");
const lastVersion = changelogData.match(/## Details\s+### \[(\d+)\.(\d+)\.(\d+)\]/);
const newVersion = [
parseInt(lastVersion[1], 10),
parseInt(lastVersion[2], 10) + 1,
0
];
async function main() {
const dir = path.join(process.cwd() + "/src/core/config/");
if (!fs.existsSync(dir)) {
console.log("\nCWD: " + process.cwd());
console.log(
"Error: newMinorVersion.mjs should be run from the project root",
);
console.log(
"Example> node --experimental-modules src/core/config/scripts/newMinorVersion.mjs",
);
process.exit(1);
}
let knownContributors = changelogData.match(/^\[@([^\]]+)\]/gm);
knownContributors = knownContributors.map(c => c.slice(2, -1));
let changelogData = fs.readFileSync(
path.join(process.cwd(), "CHANGELOG.md"),
"utf8",
);
const lastVersion = changelogData.match(
/## Details\s+### \[(\d+)\.(\d+)\.(\d+)\]/,
);
const newVersion = [
parseInt(lastVersion[1], 10),
parseInt(lastVersion[2], 10) + 1,
0,
];
const date = (new Date()).toISOString().split("T")[0];
let knownContributors = changelogData.match(/^\[@([^\]]+)\]/gm);
knownContributors = knownContributors.map((c) => c.slice(2, -1));
const schema = {
properties: {
message: {
description: "A short but descriptive summary of a feature in this version",
example: "Added 'Op name' operation",
prompt: "Feature description",
type: "string",
required: true,
const date = new Date().toISOString().split("T")[0];
const lastVersionSha = execSync(
`git rev-list -n 1 v${lastVersion[1]}.${lastVersion[2]}.${lastVersion[3]}`,
{
encoding: "utf8",
},
author: {
description: "The author of the feature (only one supported, edit manually to add more)",
example: "n1474335",
prompt: "Author",
type: "string",
default: "n1474335"
},
id: {
description: "The PR number or full commit hash for this feature.",
example: "1200",
prompt: "Pull request or commit ID",
type: "string"
},
another: {
description: "y/n",
example: "y",
prompt: "Add another feature?",
type: "string",
pattern: /^[yn]$/,
).trim();
if (lastVersionSha.length !== 40) {
throw new Error(
`Unexpected output from git rev-list: ${lastVersionSha}`,
);
}
const features = [];
const commits = await (
await fetch(`https://api.github.com/repos/gchq/cyberchef/commits`)
).json();
let foundLast = false;
for (const commit of commits) {
if (commit.sha === lastVersionSha) {
foundLast = true;
break;
} else {
const feature = {
message: "",
authors: [],
id: "",
};
const msgparts = commit.commit.message.split("\n\n");
feature.message = msgparts[0];
const prIdMatch = feature.message.match(/\(#(\d+)\)$/);
if (prIdMatch !== null) {
feature.message = feature.message
.replace(prIdMatch[0], "")
.trim();
feature.id = prIdMatch[1];
}
if (!ignoredAuthors.includes(commit.author.login)) {
feature.authors.push(commit.author.login);
}
if (msgparts.length > 1) {
msgparts[1]
.split("\n")
.filter((line) => line.startsWith("Co-authored-by: "))
.forEach((line) => {
let coAuthor = line.slice("Co-authored-by: ".length);
if (coAuthor.indexOf(">") !== -1) {
const email = coAuthor.slice(
coAuthor.indexOf("<") + 1,
coAuthor.indexOf(">"),
);
if (email.endsWith("@users.noreply.github.com")) {
coAuthor = email.slice(
email.indexOf("+") + 1,
-"@users.noreply.github.com".length,
);
} else {
throw new Error(
"Could not get ID of co-author: " +
coAuthor,
);
}
} else {
throw new Error(
"Could not get email of co-author: " + coAuthor,
);
}
if (!ignoredAuthors.includes(coAuthor)) {
feature.authors.push(coAuthor);
}
});
}
features.push(feature);
}
}
};
if (!foundLast) {
throw new Error(
`Could not find last version commit: ${lastVersionSha} - need to add paging functionality`,
);
}
// Build schema
for (const prop in schema.properties) {
const p = schema.properties[prop];
p.description = "\n" + colors.white(p.description) + colors.cyan("\nExample: " + p.example) + "\n" + colors.green(p.prompt);
}
let message = `### [${newVersion[0]}.${newVersion[1]}.${newVersion[2]}] - ${date}\n`;
prompt.message = "";
prompt.delimiter = ":".green;
const authors = [];
const prIDs = [];
const commitIDs = [];
const features = [];
const authors = [];
const prIDs = [];
const commitIDs = [];
features.forEach((feature) => {
const id =
feature.id.length > 10 ? feature.id.slice(0, 7) : "#" + feature.id;
message += `- ${feature.message} ${feature.authors.map((a) => `[@${a}]`).join(" ")} | [${id}]\n`;
prompt.start();
feature.authors.forEach((author) => {
if (!knownContributors.includes(author)) {
knownContributors.push(author);
authors.push(`[@${author}]: https://github.com/${author}`);
}
});
const getFeature = function() {
prompt.get(schema, (err, result) => {
if (err) {
console.log("\nExiting script.");
process.exit(0);
}
features.push(result);
if (result.another === "y") {
getFeature();
if (feature.id.length > 10) {
commitIDs.push(
`[${id}]: https://github.com/gchq/CyberChef/commit/${feature.id}`,
);
} else {
let message = `### [${newVersion[0]}.${newVersion[1]}.${newVersion[2]}] - ${date}\n`;
features.forEach(feature => {
const id = feature.id.length > 10 ? feature.id.slice(0, 7) : "#" + feature.id;
message += `- ${feature.message} [@${feature.author}] | [${id}]\n`;
if (!knownContributors.includes(feature.author)) {
authors.push(`[@${feature.author}]: https://github.com/${feature.author}`);
}
if (feature.id.length > 10) {
commitIDs.push(`[${id}]: https://github.com/gchq/CyberChef/commit/${feature.id}`);
} else {
prIDs.push(`[#${feature.id}]: https://github.com/gchq/CyberChef/pull/${feature.id}`);
}
});
// Message
changelogData = changelogData.replace(/## Details\n\n/, "## Details\n\n" + message + "\n");
// Tag
const newTag = `[${newVersion[0]}.${newVersion[1]}.${newVersion[2]}]: https://github.com/gchq/CyberChef/releases/tag/v${newVersion[0]}.${newVersion[1]}.${newVersion[2]}\n`;
changelogData = changelogData.replace(/\n\n(\[\d+\.\d+\.\d+\]: https)/, "\n\n" + newTag + "$1");
// Author
authors.forEach(author => {
changelogData = changelogData.replace(/(\n\[@[^\]]+\]: https:\/\/github\.com\/[^\n]+\n)\n/, "$1" + author + "\n\n");
});
// Commit IDs
commitIDs.forEach(commitID => {
changelogData = changelogData.replace(/(\n\[[^\].]+\]: https:\/\/github.com\/gchq\/CyberChef\/commit\/[^\n]+\n)\n/, "$1" + commitID + "\n\n");
});
// PR IDs
prIDs.forEach(prID => {
changelogData = changelogData.replace(/(\n\[#[^\]]+\]: https:\/\/github.com\/gchq\/CyberChef\/pull\/[^\n]+\n)\n*$/, "$1" + prID + "\n\n");
});
fs.writeFileSync(path.join(process.cwd(), "CHANGELOG.md"), changelogData);
console.log("Written CHANGELOG.md\nCommit changes and then run `npm version minor`.");
prIDs.push(
`[#${feature.id}]: https://github.com/gchq/CyberChef/pull/${feature.id}`,
);
}
});
};
getFeature();
// Message
changelogData = changelogData.replace(
/## Details\n\n/,
"## Details\n\n" + message + "\n",
);
// Tag
const newTag = `[${newVersion[0]}.${newVersion[1]}.${newVersion[2]}]: https://github.com/gchq/CyberChef/releases/tag/v${newVersion[0]}.${newVersion[1]}.${newVersion[2]}\n`;
changelogData = changelogData.replace(
/\n\n(\[\d+\.\d+\.\d+\]: https)/,
"\n\n" + newTag + "$1",
);
// Author
authors.forEach((author) => {
changelogData = changelogData.replace(
/(\n\[@[^\]]+\]: https:\/\/github\.com\/[^\n]+\n)\n/,
"$1" + author + "\n\n",
);
});
// Commit IDs
commitIDs.forEach((commitID) => {
changelogData = changelogData.replace(
/(\n\[[^\].]+\]: https:\/\/github.com\/gchq\/CyberChef\/commit\/[^\n]+\n)\n/,
"$1" + commitID + "\n\n",
);
});
// PR IDs
prIDs.forEach((prID) => {
changelogData = changelogData.replace(
/(\n\[#[^\]]+\]: https:\/\/github.com\/gchq\/CyberChef\/(?:pull|issues)\/[^\n]+\n)\n*$/,
"$1" + prID + "\n\n",
);
});
fs.writeFileSync(path.join(process.cwd(), "CHANGELOG.md"), changelogData);
}
main().catch(console.error);

View file

@ -33,7 +33,7 @@ export function toHex(data, delim=" ", padding=2, extraDelim="", lineSize=0) {
if (data instanceof ArrayBuffer) data = new Uint8Array(data);
let output = "";
const prepend = (delim === "0x" || delim === "\\x");
const prepend = (delim === "0x" || delim === "\\x" || delim === "%");
for (let i = 0; i < data.length; i++) {
const hex = data[i].toString(16).padStart(padding, "0");

View file

@ -23,7 +23,7 @@ class FromQuotedPrintable extends Operation {
this.name = "From Quoted Printable";
this.module = "Default";
this.description = "Converts QP-encoded text back to standard text.<br><br>e.g. The quoted-printable encoded string <code>hello=20world</code> becomes <code>hello world</code>";
this.description = "Converts QP-encoded text back to standard text. This format is a content transfer encoding common in email messages.<br><br>e.g. The quoted-printable encoded string <code>hello=20world</code> becomes <code>hello world</code>";
this.infoURL = "https://wikipedia.org/wiki/Quoted-printable";
this.inputType = "string";
this.outputType = "byteArray";

View file

@ -23,7 +23,7 @@ class ToQuotedPrintable extends Operation {
this.name = "To Quoted Printable";
this.module = "Default";
this.description = "Quoted-Printable, or QP encoding, is an encoding using printable ASCII characters (alphanumeric and the equals sign '=') to transmit 8-bit data over a 7-bit data path or, generally, over a medium which is not 8-bit clean. It is defined as a MIME content transfer encoding for use in e-mail.<br><br>QP works by using the equals sign '=' as an escape character. It also limits line length to 76, as some software has limits on line length.";
this.description = "Quoted-Printable, or QP encoding, is an encoding using printable ASCII characters (alphanumeric and the equals sign '=') to transmit 8-bit data over a 7-bit data path or, generally, over a medium which is not 8-bit clean. It is defined as a MIME content transfer encoding for use in email.<br><br>QP works by using the equals sign '=' as an escape character. It also limits line length to 76, as some software has limits on line length.";
this.infoURL = "https://wikipedia.org/wiki/Quoted-printable";
this.inputType = "ArrayBuffer";
this.outputType = "string";

View file

@ -151,8 +151,20 @@ class InputWaiter {
// Event handlers
EditorView.domEventHandlers({
paste(event, view) {
const clipboardData = event.clipboardData;
const items = clipboardData.items;
const files = [];
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item.kind === "file") {
const file = item.getAsFile();
files.push(file);
event.preventDefault(); // Prevent the default paste behavior
}
}
setTimeout(() => {
self.afterPaste(event);
self.afterPaste(files);
});
}
})
@ -914,9 +926,12 @@ class InputWaiter {
* Handler that fires just after input paste events.
* Checks whether the EOL separator or character encoding should be updated.
*
* @param {event} e
* @param {File[]} files - An array of any files that were included in the paste event
*/
afterPaste(e) {
afterPaste(files) {
if (files.length > 0) {
this.loadUIFiles(files);
}
// If EOL has been fixed, skip this.
if (this.eolState > 1) return;

View file

@ -7,6 +7,8 @@
*/
import Dish from "../../core/Dish.mjs";
import DishError from "../../core/errors/DishError.mjs";
import { CHR_ENC_SIMPLE_REVERSE_LOOKUP } from "../../core/lib/ChrEnc.mjs";
import Utils from "../../core/Utils.mjs";
import cptable from "codepage";
import loglevelMessagePrefix from "loglevel-message-prefix";
@ -98,7 +100,7 @@ async function bufferToStr(data) {
try {
str = cptable.utils.decode(data.encoding, new Uint8Array(data.buffer));
} catch (err) {
str = err;
str = new DishError(`Error decoding buffer with encoding ${CHR_ENC_SIMPLE_REVERSE_LOOKUP[data.encoding]}: ${err.message}`).toString();
}
}

View file

@ -43,6 +43,20 @@ TestRegister.addTests([
}
]
},
{
name: "ASCII to Hex with percent deliminator",
input: "aberystwyth",
expectedOutput: "%61%62%65%72%79%73%74%77%79%74%68",
recipeConfig: [
{
"op": "To Hex",
"args": [
"Percent",
0
]
}
]
},
{
name: "ASCII to 0x Hex with comma and line breaks",
input: "aberystwyth",