e.g. The quoted-printable encoded string hello=20world becomes hello world";
+
+- this.description = "Converts QP-encoded text back to standard text. This format is a content transfer encoding common in email messages.
e.g. The quoted-printable encoded string hello=20world becomes hello world";
+
+ this.infoURL = "https://wikipedia.org/wiki/Quoted-printable";
+
+ this.inputType = "string";
+
+ this.outputType = "byteArray";
+
+CHANGED: src/core/operations/ToBase85.mjs —
+mkilijanek/src/core/operations/ToBase85.mjs +++
+gchq/src/core/operations/ToBase85.mjs @@ -33,7 +33,7 @@ value:
+ALPHABET_OPTIONS
+
+ },
+
+ {
+
+- name: "Include delimeter",
+
+- name: "Include delimiter",
+
+ type: "boolean",
+
+ value: false
+
+ }
+
+ADDED in gchq (missing in mkilijanek): src/core/operations/ToBech32.mjs
+
+CHANGED: src/core/operations/ToQuotedPrintable.mjs —
+mkilijanek/src/core/operations/ToQuotedPrintable.mjs +++
+gchq/src/core/operations/ToQuotedPrintable.mjs @@ -23,7 +23,7 @@
+
+ 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.
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.
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";
+
+CHANGED: src/core/vendor/gost/gostRandom.mjs —
+mkilijanek/src/core/vendor/gost/gostRandom.mjs +++
+gchq/src/core/vendor/gost/gostRandom.mjs @@ -114,10 +114,7 @@ // Native
+window cryptographic interface
+
+ rootCrypto.getRandomValues(u8);
+
+ } else {
+
+- // Standard Javascript method - WARNING: Not cryptographically secure!
+
+- if (typeof console !== "undefined" && console.warn) {
+
+- console.warn("SECURITY WARNING: crypto.getRandomValues not available, falling back to Math.random() which is NOT cryptographically secure!");
+
+- }
+
+- // Standard Javascript method
+
+ for (var i = 0, n = u8.length; i < n; i++)
+
+ u8[i] = Math.floor(256 * Math.random()) & 255;
+
+ }
+
+CHANGED: src/node/apiUtils.mjs — mkilijanek/src/node/apiUtils.mjs +++
+gchq/src/node/apiUtils.mjs @@ -66,7 +66,7 @@ * @param str
+
+*/
+
+export function sanitise(str) {
+
+- return str.replace(/ /g, ““).toLowerCase();
+
+- return str.replace(/[/.-]/g, ““).toLowerCase();
+
+}
+
+CHANGED: src/web/App.mjs — mkilijanek/src/web/App.mjs +++
+gchq/src/web/App.mjs @@ -650,7 +650,7 @@
+
+ // const compareURL = `https://github.com/gchq/CyberChef/compare/v${prev.join(".")}...v${PKG_VERSION}`;
+
+- let compileInfo = `Last build: ${timeSinceCompile.substr(0, 1).toUpperCase() + timeSinceCompile.substr(1)} ago`;
+
+- let compileInfo = `Last build: ${timeSinceCompile.substring(0, 1).toUpperCase() + timeSinceCompile.substring(1)} ago`;
+
+
+
+ if (window.compileMessage !== "") {
+
+ compileInfo += " - " + window.compileMessage;
+
+CHANGED: src/web/static/sitemap.mjs —
+mkilijanek/src/web/static/sitemap.mjs +++
+gchq/src/web/static/sitemap.mjs @@ -1,6 +1,5 @@ import sm from
+“sitemap”;
+
+-import OperationConfig from “../../core/config/OperationConfig.json”
+assert {type: “json”};
+
+-
+
++import OperationConfig from “../../core/config/OperationConfig.json”
+assert { type: “json” };
+
+/**
+
+- Generates an XML sitemap for all CyberChef operations and a number
+ of recipes.
+
+@@ -10,25 +9,25 @@ * @license Apache-2.0
+
+*/
+
+-const smStream = new sm.SitemapStream({
+
+- hostname: “https://gchq.github.io/CyberChef”,
+
+-});
+
++const baseUrl = “https://gchq.github.io/CyberChef/”;
+
+-
+
++const smStream = new sm.SitemapStream({});
+
+smStream.write({
+
+- url: “/”,
+
+- url: baseUrl,
+
+ changefreq: “weekly”,
+
+- priority: 1.0
+
+- priority: 1.0,
+
+});
+
+for (const op in OperationConfig) {
+
+ smStream.write({
+
+- url: `/?op=${encodeURIComponent(op)}`,
+
+- url: `${baseUrl}?op=${encodeURIComponent(op)}`,
+
+ changeFreq: "yearly",
+
+- priority: 0.5
+
+- priority: 0.5,
+
+ });
+
+}
+
+smStream.end();
+
+sm.streamToPromise(smStream).then(
+
+- buffer => console.log(buffer.toString()) // eslint-disable-line
+ no-console
+
+- (buffer) => console.log(buffer.toString()), // eslint-disable-line
+ no-console
+
+);
+
+CHANGED: src/web/waiters/InputWaiter.mjs —
+mkilijanek/src/web/waiters/InputWaiter.mjs +++
+gchq/src/web/waiters/InputWaiter.mjs @@ -151,8 +151,20 @@ // 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 @@ * Handler that fires just after input paste events.
+
+ * Checks whether the EOL separator or character encoding should be updated.
+
+ *
+
+- * @param {event} e
+
+- */
+
+- afterPaste(e) {
+
+- * @param {File[]} files - An array of any files that were included in the paste event
+
+- */
+
+- afterPaste(files) {
+
+- if (files.length > 0) {
+
+- this.loadUIFiles(files);
+
+- }
+
+ // If EOL has been fixed, skip this.
+
+ if (this.eolState > 1) return;
+
+CHANGED: src/web/workers/DishWorker.mjs —
+mkilijanek/src/web/workers/DishWorker.mjs +++
+gchq/src/web/workers/DishWorker.mjs @@ -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 @@ 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();
+
+ }
+
+ }
+
+CHANGED: tests/node/tests/Utils.mjs —
+mkilijanek/tests/node/tests/Utils.mjs +++
+gchq/tests/node/tests/Utils.mjs @@ -20,4 +20,10 @@
+assert.equal(Utils.parseEscapedChars(“\\\‘“),”\’”);
+
+ }),
+
+- it(“Utils: should replace delete character”, () => {
+
+- assert.equal(
+
+- Utils.printable("\x7e\x7f\x80\xa7", false, true),
+
+- "\x7e...",
+
+- );
+
+- }),
+
+]);
+
+CHANGED: tests/node/tests/nodeApi.mjs —
+mkilijanek/tests/node/tests/nodeApi.mjs +++
+gchq/tests/node/tests/nodeApi.mjs @@ -343,6 +343,42 @@ “args”: [false] }
+
+ ]);
+
+ assert.strictEqual(result.toString(), "begin_something_aaaaaaaaaaaaaa_end_something");
+
+- }),
+
+-
+
+- it(“chef.bake: should accept operation names from Chef Website which
+ contain forward slash”, () => {
+
+- const result = chef.bake("I'll have the test salmon", [
+
+- { "op": "Find / Replace",
+
+- "args": [{ "option": "Regex", "string": "test" }, "good", true, false, true, false]}
+
+- ]);
+
+- assert.strictEqual(result.toString(), "I'll have the good salmon");
+
+- }),
+
+-
+
+- it(“chef.bake: should accept operation names from Chef Website which
+ contain a hyphen”, () => {
+
+- const result = chef.bake("I'll have the test salmon", [
+
+- { "op": "Adler-32 Checksum",
+
+- "args": [] }
+
+- ]);
+
+- assert.strictEqual(result.toString(), "6e4208f8");
+
+- }),
+
+-
+
+- it(“chef.bake: should accept operation names from Chef Website which
+ contain a period”, () => {
+
+- const result = chef.bake("30 13 02 01 05 16 0e 41 6e 79 62 6f 64 79 20 74 68 65 72 65 3f", [
+
+- { "op": "Parse ASN.1 hex string",
+
+- "args": [0, 32] }
+
+- ]);
+
+- assert.strictEqual(result.toString(), `SEQUENCE
+
+- INTEGER 05
+
+- IA5String ‘Anybody there?’
+
++`);
+
+- }),
+
+-
+
+- it(“Excluded operations: throw a sensible error when you try and
+ call one”, () => {
+
+- try {
+
+- chef.fork();
+
+- } catch (e) {
+
+- assert.strictEqual(e.type, "ExcludedOperationError");
+
+- assert.strictEqual(e.message, "Sorry, the Fork operation is not available in the Node.js version of CyberChef.");
+
+- }
+
+ }),
+
+ it(“chef.bake: cannot accept flowControl operations in recipe”, ()
+ => {
+
+CHANGED: tests/operations/index.mjs —
+mkilijanek/tests/operations/index.mjs +++
+gchq/tests/operations/index.mjs @@ -26,6 +26,7 @@ import
+“./tests/Base85.mjs”;
+
+import “./tests/Base92.mjs”;
+
+import “./tests/BCD.mjs”;
+
++import “./tests/Bech32.mjs”;
+
+import “./tests/BitwiseOp.mjs”;
+
+import “./tests/BLAKE2b.mjs”;
+
+import “./tests/BLAKE2s.mjs”;
+
+ADDED in gchq (missing in mkilijanek): tests/operations/tests/Bech32.mjs
+
+CHANGED: tests/operations/tests/Code.mjs —
+mkilijanek/tests/operations/tests/Code.mjs +++
+gchq/tests/operations/tests/Code.mjs @@ -322,8 +322,21 @@ ]
+
+ }
+
+ ],
+
+- expectedMatch: /^Invalid JPath expression: jsonPath: self is not defined:/
+
+- },
+
+- expectedMatch: /^Invalid JPath expression: Unexpected "{" at character 1/
+
+- },
+
+- {
+
+- name: "JPath Expression: Script-based RCE",
+
+- input: "[{}]",
+
+- recipeConfig: [
+
+- {
+
+- "op": "JPath expression",
+
+- "args": [
+
+- "$..[?(p=\"console.log(this.process.mainModule.require('child_process').execSync('id').toString())\";a=''[['constructor']][['constructor']](p);a())]",
+
+- "\n"
+
+- ]
+
+- }
+
+- ],
+
+- expectedMatch: /^Invalid JPath expression: jsonPath: Cannot read properties of {2}\(reading 'constructor'\): / },
+
+ {
+
+ name: "CSS selector",
+
+ input: '
\n
hello
\n
world
\n
again
\n
',
+
+CHANGED: tests/operations/tests/Hex.mjs —
+mkilijanek/tests/operations/tests/Hex.mjs +++
+gchq/tests/operations/tests/Hex.mjs @@ -38,6 +38,20 @@ “op”: “To Hex”,
+
+ "args": [
+
+ "0x with comma",
+
+- 0
+
+- ]
+
+- }
+
+- ]
+
+- },
+
+- {
+
+- 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
+
+ ]
+
+ }
+
+CHANGED: tests/operations/tests/Hexdump.mjs —
+mkilijanek/tests/operations/tests/Hexdump.mjs +++
+gchq/tests/operations/tests/Hexdump.mjs @@ -153,6 +153,17 @@ ],
+
+ },
+
+ {
+
+- name: "From Hexdump: xxd format, odd number of bytes",
+
+- input: "00000000: 6162 6364 65 abcde",
+
+- expectedOutput: "abcde",
+
+- recipeConfig: [
+
+- {
+
+- op: "From Hexdump",
+
+- args: []
+
+- }
+
+- ],
+
+- },
+
+- {
+
+ name: "From Hexdump: Wireshark",
+
+ input: `00000000 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f ........ ........
+
+00000010 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f …….. ……..
+
+CHANGED: tests/operations/tests/JA4.mjs —
+mkilijanek/tests/operations/tests/JA4.mjs +++
+gchq/tests/operations/tests/JA4.mjs @@ -27,6 +27,28 @@ {
+
+ "op": "JA4 Fingerprint",
+
+ "args": ["Hex", "JA4 Original Rendering"]
+
+- }
+
+- ],
+
+- },
+
+- {
+
+- name: "JA4 Fingerprint: TLS 1.3 with whitespace-only ALPN",
+
+- input: "1603010200010001fc0303ed338a18e711d670cdc472ff570a5b59f1ace12e5365918bf68bf845019147b6207e4437bfb062d98a4aeb753be8f09022a9dc9413d7694dad4db57fcdcf076e820024130213031301c02cc030c02bc02fcca9cca8c024c028c023c027009f009e006b006700ff0100018f0000001800160000136465762e636f6e74656e74677261622e6e6574000b000403000102000a00160014001d0017001e00190018010001010102010301040023000000100004000201200016000000170000000d002a0028040305030603080708080809080a080b080408050806040105010601030303010302040205020602002b00050403040303002d00020101003300260024001d00207af053336d5e2c1675aa4c6ce78de5e5fdbd296538113f051ea17ccb64289f22001500d2000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+
+- expectedOutput: "t13d181220_85036bcba153_d41ae481755e",
+
+- recipeConfig: [
+
+- {
+
+- "op": "JA4 Fingerprint",
+
+- "args": ["Hex", "JA4"]
+
+- }
+
+- ],
+
+- },
+
+- {
+
+- name: "JA4 Fingerprint: TLS 1.3 with ALPN containing a whitespace",
+
+- input: "1603010200010001fc0303273682a603be3f64dd025df4ad0f4d2d13043c3a233405a68bb29b865808749a20f4dfc40242b2fce38fae26c516ef9bef20a1b9349eba3c003780168d72471f5c0024130213031301c02cc030c02bc02fcca9cca8c024c028c023c027009f009e006b006700ff0100018f0000001800160000136465762e636f6e74656e74677261622e6e6574000b000403000102000a00160014001d0017001e0019001801000101010201030104002300000010000500030261200016000000170000000d002a0028040305030603080708080809080a080b080408050806040105010601030303010302040205020602002b00050403040303002d00020101003300260024001d0020f4dd1567bd858d3a9f1d88db1fee6a10ab0ea1aa6afe96ffb6a7c4d79dea4075001500d10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+
+- expectedOutput: "t13d181260_85036bcba153_d41ae481755e",
+
+- recipeConfig: [
+
+- {
+
+- "op": "JA4 Fingerprint",
+
+- "args": ["Hex", "JA4"]
+
+ }
+
+ ],
+
+ },
diff --git a/Dockerfile b/Dockerfile
index b82497b1..ced81d78 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -27,10 +27,9 @@ RUN npm run build
#########################################
# Package static build files into nginx #
#########################################
-# We are using Github Actions: redhat-actions/buildah-build@v2 which needs manual selection of arch in base image
-# Remove TARGETARCH if docker buildx is supported in the CI release as --platform=$TARGETPLATFORM will be automatically set
-ARG TARGETPLATFORM
-FROM --platform=${TARGETPLATFORM} nginx:1.27-alpine AS cyberchef
+FROM nginx:stable-alpine AS cyberchef
+
+LABEL maintainer="GCHQ "
COPY --from=builder --chown=nginx:nginx /app/build/prod /usr/share/nginx/html/
diff --git a/Gruntfile.js b/Gruntfile.js
index d3f6b635..a67aa5b8 100755
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -432,18 +432,6 @@ module.exports = function (grunt) {
},
stdout: false
},
- fixJimpModule: {
- command: function () {
- switch (process.platform) {
- case "darwin":
- // Space added before comma to prevent multiple modifications
- return `sed -i '' 's/"es\\/index.js",/"es\\/index.js" ,\\n "type": "module",/' ./node_modules/jimp/package.json`;
- default:
- return `sed -i 's/"es\\/index.js",/"es\\/index.js" ,\\n "type": "module",/' ./node_modules/jimp/package.json`;
- }
- },
- stdout: false
- }
},
});
};
diff --git a/package.json b/package.json
index 66320acc..efdebcfa 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "cyberchef",
- "version": "10.19.4",
+ "version": "10.22.1",
"description": "The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.",
"author": "n1474335 ",
"homepage": "https://gchq.github.io/CyberChef",
@@ -118,7 +118,7 @@
"chi-squared": "^1.1.0",
"codepage": "^1.15.0",
"crypto-api": "^0.8.5",
- "crypto-browserify": "^3.12.0",
+ "crypto-browserify": "^3.12.1",
"crypto-js": "^4.2.0",
"ctph.js": "0.0.5",
"d3": "7.9.0",
@@ -137,7 +137,7 @@
"hash-wasm": "^4.12.0",
"highlight.js": "^11.9.0",
"ieee754": "^1.2.1",
- "jimp": "^0.22.12",
+ "jimp": "^1.6.0",
"jq-web": "^0.5.1",
"jquery": "3.7.1",
"js-sha3": "^0.9.3",
@@ -148,7 +148,7 @@
"jsonwebtoken": "9.0.0",
"jsqr": "^1.4.0",
"jsrsasign": "^11.1.0",
- "kbpgp": "2.1.15",
+ "kbpgp": "^2.1.17",
"libbzip2-wasm": "0.0.4",
"libyara-wasm": "^1.2.1",
"lodash": "^4.17.21",
@@ -198,16 +198,17 @@
"start": "npx grunt dev",
"build": "npx grunt prod",
"node": "npx grunt node",
- "repl": "node --experimental-modules --experimental-json-modules --experimental-specifier-resolution=node --no-experimental-fetch --no-warnings src/node/repl.mjs",
- "test": "npx grunt configTests && node --experimental-modules --experimental-json-modules --no-warnings --no-deprecation --openssl-legacy-provider --no-experimental-fetch tests/node/index.mjs && node --experimental-modules --experimental-json-modules --no-warnings --no-deprecation --openssl-legacy-provider --no-experimental-fetch --trace-uncaught tests/operations/index.mjs",
+ "repl": "node --no-experimental-fetch --no-warnings src/node/repl.mjs",
+ "test": "npx grunt configTests && node --no-warnings --no-deprecation --openssl-legacy-provider --no-experimental-fetch tests/node/index.mjs && node --no-warnings --no-deprecation --openssl-legacy-provider --no-experimental-fetch --trace-uncaught tests/operations/index.mjs",
"testnodeconsumer": "npx grunt testnodeconsumer",
"testui": "npx grunt testui",
"testuidev": "npx nightwatch --env=dev",
"lint": "npx grunt lint",
"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",
+ "newop": "node src/core/config/scripts/newOperation.mjs",
+ "minor": "node src/core/config/scripts/newMinorVersion.mjs",
+ "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",
"security:audit": "npm audit",
@@ -216,5 +217,12 @@
"security:triage": "node scripts/vulnerability-triage.js",
"security:triage:json": "node scripts/vulnerability-triage.js --json",
"security:check": "npm run security:triage && npm run lint"
+ },
+ "overrides": {
+ "pbkdf2": "^3.1.5",
+ "sha.js": "^2.4.12"
+ },
+ "engines": {
+ "node": ">=24"
}
}
diff --git a/src/core/ChefWorker.js b/src/core/ChefWorker.js
index a43993f9..43f595f3 100644
--- a/src/core/ChefWorker.js
+++ b/src/core/ChefWorker.js
@@ -7,7 +7,7 @@
*/
import Chef from "./Chef.mjs";
-import OperationConfig from "./config/OperationConfig.json" assert {type: "json"};
+import OperationConfig from "./config/OperationConfig.json" with { type: "json" };
import OpModules from "./config/modules/OpModules.mjs";
import loglevelMessagePrefix from "loglevel-message-prefix";
diff --git a/src/core/Recipe.mjs b/src/core/Recipe.mjs
index 3ce40aa4..46991190 100755
--- a/src/core/Recipe.mjs
+++ b/src/core/Recipe.mjs
@@ -4,7 +4,7 @@
* @license Apache-2.0
*/
-import OperationConfig from "./config/OperationConfig.json" assert {type: "json"};
+import OperationConfig from "./config/OperationConfig.json" with { type: "json" };
import OperationError from "./errors/OperationError.mjs";
import Operation from "./Operation.mjs";
import DishError from "./errors/DishError.mjs";
@@ -229,6 +229,7 @@ class Recipe {
}
this.lastRunOp = op;
} catch (err) {
+ log.error(err);
// Return expected errors as output
if (err instanceof OperationError || err?.type === "OperationError") {
// Cannot rely on `err instanceof OperationError` here as extending
diff --git a/src/core/Utils.mjs b/src/core/Utils.mjs
index a9c381d7..eae86374 100755
--- a/src/core/Utils.mjs
+++ b/src/core/Utils.mjs
@@ -177,7 +177,7 @@ class Utils {
*/
static printable(str, preserveWs=false, onlyAscii=false) {
if (onlyAscii) {
- return str.replace(/[^\x20-\x7f]/g, ".");
+ return str.replace(/[^\x20-\x7e]/g, ".");
}
// eslint-disable-next-line no-misleading-character-class
diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json
index 434c8bb6..aac00ca1 100644
--- a/src/core/config/Categories.json
+++ b/src/core/config/Categories.json
@@ -26,6 +26,8 @@
"From Base45",
"To Base58",
"From Base58",
+ "To Bech32",
+ "From Bech32",
"To Base62",
"From Base62",
"To Base64",
diff --git a/src/core/config/scripts/generateConfig.mjs b/src/core/config/scripts/generateConfig.mjs
index 64c7cb81..5deac2d8 100644
--- a/src/core/config/scripts/generateConfig.mjs
+++ b/src/core/config/scripts/generateConfig.mjs
@@ -20,7 +20,7 @@ const dir = path.join(process.cwd() + "/src/core/config/");
if (!fs.existsSync(dir)) {
console.log("\nCWD: " + process.cwd());
console.log("Error: generateConfig.mjs should be run from the project root");
- console.log("Example> node --experimental-modules src/core/config/scripts/generateConfig.mjs");
+ console.log("Example> node src/core/config/scripts/generateConfig.mjs");
process.exit(1);
}
diff --git a/src/core/config/scripts/generateOpsIndex.mjs b/src/core/config/scripts/generateOpsIndex.mjs
index d8dd6a70..d58fe32a 100644
--- a/src/core/config/scripts/generateOpsIndex.mjs
+++ b/src/core/config/scripts/generateOpsIndex.mjs
@@ -17,7 +17,7 @@ const dir = path.join(process.cwd() + "/src/core/config/");
if (!fs.existsSync(dir)) {
console.log("\nCWD: " + process.cwd());
console.log("Error: generateOpsIndex.mjs should be run from the project root");
- console.log("Example> node --experimental-modules src/core/config/scripts/generateOpsIndex.mjs");
+ console.log("Example> node src/core/config/scripts/generateOpsIndex.mjs");
process.exit(1);
}
diff --git a/src/core/config/scripts/newMinorVersion.mjs b/src/core/config/scripts/newMinorVersion.mjs
index 67754890..d22ef43b 100644
--- a/src/core/config/scripts/newMinorVersion.mjs
+++ b/src/core/config/scripts/newMinorVersion.mjs
@@ -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);
diff --git a/src/core/config/scripts/newOperation.mjs b/src/core/config/scripts/newOperation.mjs
index 1686f6eb..ffeb3df7 100644
--- a/src/core/config/scripts/newOperation.mjs
+++ b/src/core/config/scripts/newOperation.mjs
@@ -20,7 +20,7 @@ const dir = path.join(process.cwd() + "/src/core/operations/");
if (!fs.existsSync(dir)) {
console.log("\nCWD: " + process.cwd());
console.log("Error: newOperation.mjs should be run from the project root");
- console.log("Example> node --experimental-modules src/core/config/scripts/newOperation.mjs");
+ console.log("Example> node src/core/config/scripts/newOperation.mjs");
process.exit(1);
}
diff --git a/src/core/lib/Bech32.mjs b/src/core/lib/Bech32.mjs
new file mode 100644
index 00000000..6b87a142
--- /dev/null
+++ b/src/core/lib/Bech32.mjs
@@ -0,0 +1,371 @@
+/**
+ * Pure JavaScript implementation of Bech32 and Bech32m encoding.
+ *
+ * Bech32 is defined in BIP-0173: https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki
+ * Bech32m is defined in BIP-0350: https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki
+ *
+ * @author Medjedtxm
+ * @copyright Crown Copyright 2025
+ * @license Apache-2.0
+ */
+
+import OperationError from "../errors/OperationError.mjs";
+
+/** Bech32 character set (32 characters, excludes 1, b, i, o) */
+const CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
+
+/** Reverse lookup table for decoding */
+const CHARSET_REV = {};
+for (let i = 0; i < CHARSET.length; i++) {
+ CHARSET_REV[CHARSET[i]] = i;
+}
+
+/** Checksum constant for Bech32 (BIP-0173) */
+const BECH32_CONST = 1;
+
+/** Checksum constant for Bech32m (BIP-0350) */
+const BECH32M_CONST = 0x2bc830a3;
+
+/** Generator polynomial coefficients for checksum */
+const GENERATOR = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
+
+/**
+ * Compute the polymod checksum
+ * @param {number[]} values - Array of 5-bit values
+ * @returns {number} - Checksum value
+ */
+function polymod(values) {
+ let chk = 1;
+ for (const v of values) {
+ const top = chk >> 25;
+ chk = ((chk & 0x1ffffff) << 5) ^ v;
+ for (let i = 0; i < 5; i++) {
+ if ((top >> i) & 1) {
+ chk ^= GENERATOR[i];
+ }
+ }
+ }
+ return chk;
+}
+
+/**
+ * Expand HRP for checksum computation
+ * @param {string} hrp - Human-readable part (lowercase)
+ * @returns {number[]} - Expanded values
+ */
+function hrpExpand(hrp) {
+ const result = [];
+ for (let i = 0; i < hrp.length; i++) {
+ result.push(hrp.charCodeAt(i) >> 5);
+ }
+ result.push(0);
+ for (let i = 0; i < hrp.length; i++) {
+ result.push(hrp.charCodeAt(i) & 31);
+ }
+ return result;
+}
+
+/**
+ * Verify checksum of a Bech32/Bech32m string
+ * @param {string} hrp - Human-readable part (lowercase)
+ * @param {number[]} data - Data including checksum (5-bit values)
+ * @param {string} encoding - "Bech32" or "Bech32m"
+ * @returns {boolean} - True if checksum is valid
+ */
+function verifyChecksum(hrp, data, encoding) {
+ const constant = encoding === "Bech32m" ? BECH32M_CONST : BECH32_CONST;
+ return polymod(hrpExpand(hrp).concat(data)) === constant;
+}
+
+/**
+ * Create checksum for Bech32/Bech32m encoding
+ * @param {string} hrp - Human-readable part (lowercase)
+ * @param {number[]} data - Data values (5-bit)
+ * @param {string} encoding - "Bech32" or "Bech32m"
+ * @returns {number[]} - 6 checksum values
+ */
+function createChecksum(hrp, data, encoding) {
+ const constant = encoding === "Bech32m" ? BECH32M_CONST : BECH32_CONST;
+ const values = hrpExpand(hrp).concat(data).concat([0, 0, 0, 0, 0, 0]);
+ const mod = polymod(values) ^ constant;
+ const result = [];
+ for (let i = 0; i < 6; i++) {
+ result.push((mod >> (5 * (5 - i))) & 31);
+ }
+ return result;
+}
+
+/**
+ * Convert 8-bit bytes to 5-bit words
+ * @param {number[]|Uint8Array} data - Input bytes
+ * @returns {number[]} - 5-bit words
+ */
+export function toWords(data) {
+ let value = 0;
+ let bits = 0;
+ const result = [];
+
+ for (let i = 0; i < data.length; i++) {
+ value = (value << 8) | data[i];
+ bits += 8;
+
+ while (bits >= 5) {
+ bits -= 5;
+ result.push((value >> bits) & 31);
+ }
+ }
+
+ // Pad remaining bits
+ if (bits > 0) {
+ result.push((value << (5 - bits)) & 31);
+ }
+
+ return result;
+}
+
+/**
+ * Convert 5-bit words to 8-bit bytes
+ * @param {number[]} words - 5-bit words
+ * @returns {number[]} - Output bytes
+ */
+export function fromWords(words) {
+ let value = 0;
+ let bits = 0;
+ const result = [];
+
+ for (let i = 0; i < words.length; i++) {
+ value = (value << 5) | words[i];
+ bits += 5;
+
+ while (bits >= 8) {
+ bits -= 8;
+ result.push((value >> bits) & 255);
+ }
+ }
+
+ // Check for invalid padding per BIP-0173
+ // Condition 1: Cannot have 5+ bits remaining (would indicate incomplete byte)
+ if (bits >= 5) {
+ throw new OperationError("Invalid padding: too many bits remaining");
+ }
+ // Condition 2: Remaining padding bits must all be zero
+ if (bits > 0) {
+ const paddingValue = (value << (8 - bits)) & 255;
+ if (paddingValue !== 0) {
+ throw new OperationError("Invalid padding: non-zero bits in padding");
+ }
+ }
+
+ return result;
+}
+
+/**
+ * Encode data to Bech32/Bech32m string
+ *
+ * @param {string} hrp - Human-readable part
+ * @param {number[]|Uint8Array} data - Data bytes to encode
+ * @param {string} encoding - "Bech32" or "Bech32m"
+ * @param {boolean} segwit - If true, treat first byte as witness version (for Bitcoin SegWit)
+ * @returns {string} - Encoded Bech32/Bech32m string
+ */
+export function encode(hrp, data, encoding = "Bech32", segwit = false) {
+ // Validate HRP
+ if (!hrp || hrp.length === 0) {
+ throw new OperationError("Human-Readable Part (HRP) cannot be empty.");
+ }
+
+ // Check HRP characters (ASCII 33-126)
+ for (let i = 0; i < hrp.length; i++) {
+ const c = hrp.charCodeAt(i);
+ if (c < 33 || c > 126) {
+ throw new OperationError(`HRP contains invalid character at position ${i}. Only printable ASCII characters (33-126) are allowed.`);
+ }
+ }
+
+ // Convert HRP to lowercase
+ const hrpLower = hrp.toLowerCase();
+
+ let words;
+ if (segwit && data.length >= 2) {
+ // SegWit encoding: first byte is witness version (0-16), rest is witness program
+ const witnessVersion = data[0];
+ if (witnessVersion > 16) {
+ throw new OperationError(`Invalid witness version: ${witnessVersion}. Must be 0-16.`);
+ }
+ const witnessProgram = Array.prototype.slice.call(data, 1);
+
+ // Validate witness program length per BIP-0141
+ if (witnessProgram.length < 2 || witnessProgram.length > 40) {
+ throw new OperationError(`Invalid witness program length: ${witnessProgram.length}. Must be 2-40 bytes.`);
+ }
+ if (witnessVersion === 0 && witnessProgram.length !== 20 && witnessProgram.length !== 32) {
+ throw new OperationError(`Invalid witness program length for v0: ${witnessProgram.length}. Must be 20 or 32 bytes.`);
+ }
+
+ // Witness version is kept as single 5-bit value, program is converted
+ words = [witnessVersion].concat(toWords(witnessProgram));
+ } else {
+ // Generic encoding: convert all bytes to 5-bit words
+ words = toWords(data);
+ }
+
+ // Create checksum
+ const checksum = createChecksum(hrpLower, words, encoding);
+
+ // Build result string
+ let result = hrpLower + "1";
+ for (const w of words.concat(checksum)) {
+ result += CHARSET[w];
+ }
+
+ // Check maximum length (90 characters)
+ if (result.length > 90) {
+ throw new OperationError(`Encoded string exceeds maximum length of 90 characters (got ${result.length}). Consider using smaller input data.`);
+ }
+
+ return result;
+}
+
+/**
+ * Decode a Bech32/Bech32m string
+ *
+ * @param {string} str - Bech32/Bech32m encoded string
+ * @param {string} encoding - "Bech32", "Bech32m", or "Auto-detect"
+ * @returns {{hrp: string, data: number[]}} - Decoded HRP and data bytes
+ */
+export function decode(str, encoding = "Auto-detect") {
+ // Check for empty input
+ if (!str || str.length === 0) {
+ throw new OperationError("Input cannot be empty.");
+ }
+
+ // Check maximum length
+ if (str.length > 90) {
+ throw new OperationError(`Invalid Bech32 string: exceeds maximum length of 90 characters (got ${str.length}).`);
+ }
+
+ // Check for mixed case
+ const hasUpper = /[A-Z]/.test(str);
+ const hasLower = /[a-z]/.test(str);
+ if (hasUpper && hasLower) {
+ throw new OperationError("Invalid Bech32 string: mixed case is not allowed. Use all uppercase or all lowercase.");
+ }
+
+ // Convert to lowercase for processing
+ str = str.toLowerCase();
+
+ // Find separator (last occurrence of '1')
+ const sepIndex = str.lastIndexOf("1");
+ if (sepIndex === -1) {
+ throw new OperationError("Invalid Bech32 string: no separator '1' found.");
+ }
+
+ if (sepIndex === 0) {
+ throw new OperationError("Invalid Bech32 string: Human-Readable Part (HRP) cannot be empty.");
+ }
+
+ if (sepIndex + 7 > str.length) {
+ throw new OperationError("Invalid Bech32 string: data part is too short (minimum 6 characters for checksum).");
+ }
+
+ // Extract HRP and data part
+ const hrp = str.substring(0, sepIndex);
+ const dataPart = str.substring(sepIndex + 1);
+
+ // Validate HRP characters
+ for (let i = 0; i < hrp.length; i++) {
+ const c = hrp.charCodeAt(i);
+ if (c < 33 || c > 126) {
+ throw new OperationError(`HRP contains invalid character at position ${i}.`);
+ }
+ }
+
+ // Decode data characters to 5-bit values
+ const data = [];
+ for (let i = 0; i < dataPart.length; i++) {
+ const c = dataPart[i];
+ if (CHARSET_REV[c] === undefined) {
+ throw new OperationError(`Invalid character '${c}' at position ${sepIndex + 1 + i}.`);
+ }
+ data.push(CHARSET_REV[c]);
+ }
+
+ // Verify checksum
+ let usedEncoding;
+ if (encoding === "Bech32") {
+ if (!verifyChecksum(hrp, data, "Bech32")) {
+ throw new OperationError("Invalid Bech32 checksum.");
+ }
+ usedEncoding = "Bech32";
+ } else if (encoding === "Bech32m") {
+ if (!verifyChecksum(hrp, data, "Bech32m")) {
+ throw new OperationError("Invalid Bech32m checksum.");
+ }
+ usedEncoding = "Bech32m";
+ } else {
+ // Auto-detect: try Bech32 first, then Bech32m
+ if (verifyChecksum(hrp, data, "Bech32")) {
+ usedEncoding = "Bech32";
+ } else if (verifyChecksum(hrp, data, "Bech32m")) {
+ usedEncoding = "Bech32m";
+ } else {
+ throw new OperationError("Invalid Bech32/Bech32m string: checksum verification failed.");
+ }
+ }
+
+ // Remove checksum (last 6 values)
+ const words = data.slice(0, data.length - 6);
+
+ // Check if this is likely a SegWit address (Bitcoin, Litecoin, etc.)
+ // For SegWit, the first 5-bit word is the witness version (0-16)
+ // and should be extracted separately, not bit-converted with the rest
+ const segwitHrps = ["bc", "tb", "ltc", "tltc", "bcrt"];
+ const couldBeSegWit = segwitHrps.includes(hrp) && words.length > 0 && words[0] <= 16;
+
+ let bytes;
+ let witnessVersion = null;
+
+ if (couldBeSegWit) {
+ // Try SegWit decode first
+ try {
+ witnessVersion = words[0];
+ const programWords = words.slice(1);
+ const programBytes = fromWords(programWords);
+
+ // Validate SegWit witness program length (20 or 32 bytes for v0, 2-40 for others)
+ const validV0 = witnessVersion === 0 && (programBytes.length === 20 || programBytes.length === 32);
+ const validOther = witnessVersion !== 0 && programBytes.length >= 2 && programBytes.length <= 40;
+
+ if (validV0 || validOther) {
+ // Valid SegWit address
+ bytes = [witnessVersion, ...programBytes];
+ } else {
+ // Not valid SegWit, fall back to generic decode
+ witnessVersion = null;
+ bytes = fromWords(words);
+ }
+ } catch (e) {
+ // SegWit decode failed, try generic decode
+ witnessVersion = null;
+ try {
+ bytes = fromWords(words);
+ } catch (e2) {
+ throw new OperationError(`Failed to decode data: ${e2.message}`);
+ }
+ }
+ } else {
+ // Generic Bech32: convert all words
+ try {
+ bytes = fromWords(words);
+ } catch (e) {
+ throw new OperationError(`Failed to decode data: ${e.message}`);
+ }
+ }
+
+ return {
+ hrp: hrp,
+ data: bytes,
+ encoding: usedEncoding,
+ witnessVersion: witnessVersion
+ };
+}
diff --git a/src/core/lib/Hex.mjs b/src/core/lib/Hex.mjs
index 78e1ad58..6f998e2b 100644
--- a/src/core/lib/Hex.mjs
+++ b/src/core/lib/Hex.mjs
@@ -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");
diff --git a/src/core/lib/ImageManipulation.mjs b/src/core/lib/ImageManipulation.mjs
deleted file mode 100644
index 63a80fe4..00000000
--- a/src/core/lib/ImageManipulation.mjs
+++ /dev/null
@@ -1,251 +0,0 @@
-/**
- * Image manipulation resources
- *
- * @author j433866 [j433866@gmail.com]
- * @copyright Crown Copyright 2019
- * @license Apache-2.0
- */
-
-import OperationError from "../errors/OperationError.mjs";
-
-/**
- * Gaussian blurs an image.
- *
- * @param {jimp} input
- * @param {number} radius
- * @param {boolean} fast
- * @returns {jimp}
- */
-export function gaussianBlur (input, radius) {
- try {
- // From http://blog.ivank.net/fastest-gaussian-blur.html
- const boxes = boxesForGauss(radius, 3);
- for (let i = 0; i < 3; i++) {
- input = boxBlur(input, (boxes[i] - 1) / 2);
- }
- } catch (err) {
- throw new OperationError(`Error blurring image. (${err})`);
- }
-
- return input;
-}
-
-/**
- *
- * @param {number} radius
- * @param {number} numBoxes
- * @returns {Array}
- */
-function boxesForGauss(radius, numBoxes) {
- const idealWidth = Math.sqrt((12 * radius * radius / numBoxes) + 1);
-
- let wl = Math.floor(idealWidth);
-
- if (wl % 2 === 0) {
- wl--;
- }
-
- const wu = wl + 2;
-
- const mIdeal = (12 * radius * radius - numBoxes * wl * wl - 4 * numBoxes * wl - 3 * numBoxes) / (-4 * wl - 4);
- const m = Math.round(mIdeal);
-
- const sizes = [];
- for (let i = 0; i < numBoxes; i++) {
- sizes.push(i < m ? wl : wu);
- }
- return sizes;
-}
-
-/**
- * Applies a box blur effect to the image
- *
- * @param {jimp} source
- * @param {number} radius
- * @returns {jimp}
- */
-function boxBlur (source, radius) {
- const width = source.bitmap.width;
- const height = source.bitmap.height;
- let output = source.clone();
- output = boxBlurH(source, output, width, height, radius);
- source = boxBlurV(output, source, width, height, radius);
-
- return source;
-}
-
-/**
- * Applies the horizontal blur
- *
- * @param {jimp} source
- * @param {jimp} output
- * @param {number} width
- * @param {number} height
- * @param {number} radius
- * @returns {jimp}
- */
-function boxBlurH (source, output, width, height, radius) {
- const iarr = 1 / (radius + radius + 1);
- for (let i = 0; i < height; i++) {
- let ti = 0,
- li = ti,
- ri = ti + radius;
- const idx = source.getPixelIndex(ti, i);
- const firstValRed = source.bitmap.data[idx],
- firstValGreen = source.bitmap.data[idx + 1],
- firstValBlue = source.bitmap.data[idx + 2],
- firstValAlpha = source.bitmap.data[idx + 3];
-
- const lastIdx = source.getPixelIndex(width - 1, i),
- lastValRed = source.bitmap.data[lastIdx],
- lastValGreen = source.bitmap.data[lastIdx + 1],
- lastValBlue = source.bitmap.data[lastIdx + 2],
- lastValAlpha = source.bitmap.data[lastIdx + 3];
-
- let red = (radius + 1) * firstValRed;
- let green = (radius + 1) * firstValGreen;
- let blue = (radius + 1) * firstValBlue;
- let alpha = (radius + 1) * firstValAlpha;
-
- for (let j = 0; j < radius; j++) {
- const jIdx = source.getPixelIndex(ti + j, i);
- red += source.bitmap.data[jIdx];
- green += source.bitmap.data[jIdx + 1];
- blue += source.bitmap.data[jIdx + 2];
- alpha += source.bitmap.data[jIdx + 3];
- }
-
- for (let j = 0; j <= radius; j++) {
- const jIdx = source.getPixelIndex(ri++, i);
- red += source.bitmap.data[jIdx] - firstValRed;
- green += source.bitmap.data[jIdx + 1] - firstValGreen;
- blue += source.bitmap.data[jIdx + 2] - firstValBlue;
- alpha += source.bitmap.data[jIdx + 3] - firstValAlpha;
-
- const tiIdx = source.getPixelIndex(ti++, i);
- output.bitmap.data[tiIdx] = Math.round(red * iarr);
- output.bitmap.data[tiIdx + 1] = Math.round(green * iarr);
- output.bitmap.data[tiIdx + 2] = Math.round(blue * iarr);
- output.bitmap.data[tiIdx + 3] = Math.round(alpha * iarr);
- }
-
- for (let j = radius + 1; j < width - radius; j++) {
- const riIdx = source.getPixelIndex(ri++, i);
- const liIdx = source.getPixelIndex(li++, i);
- red += source.bitmap.data[riIdx] - source.bitmap.data[liIdx];
- green += source.bitmap.data[riIdx + 1] - source.bitmap.data[liIdx + 1];
- blue += source.bitmap.data[riIdx + 2] - source.bitmap.data[liIdx + 2];
- alpha += source.bitmap.data[riIdx + 3] - source.bitmap.data[liIdx + 3];
-
- const tiIdx = source.getPixelIndex(ti++, i);
- output.bitmap.data[tiIdx] = Math.round(red * iarr);
- output.bitmap.data[tiIdx + 1] = Math.round(green * iarr);
- output.bitmap.data[tiIdx + 2] = Math.round(blue * iarr);
- output.bitmap.data[tiIdx + 3] = Math.round(alpha * iarr);
- }
-
- for (let j = width - radius; j < width; j++) {
- const liIdx = source.getPixelIndex(li++, i);
- red += lastValRed - source.bitmap.data[liIdx];
- green += lastValGreen - source.bitmap.data[liIdx + 1];
- blue += lastValBlue - source.bitmap.data[liIdx + 2];
- alpha += lastValAlpha - source.bitmap.data[liIdx + 3];
-
- const tiIdx = source.getPixelIndex(ti++, i);
- output.bitmap.data[tiIdx] = Math.round(red * iarr);
- output.bitmap.data[tiIdx + 1] = Math.round(green * iarr);
- output.bitmap.data[tiIdx + 2] = Math.round(blue * iarr);
- output.bitmap.data[tiIdx + 3] = Math.round(alpha * iarr);
- }
- }
- return output;
-}
-
-/**
- * Applies the vertical blur
- *
- * @param {jimp} source
- * @param {jimp} output
- * @param {number} width
- * @param {number} height
- * @param {number} radius
- * @returns {jimp}
- */
-function boxBlurV (source, output, width, height, radius) {
- const iarr = 1 / (radius + radius + 1);
- for (let i = 0; i < width; i++) {
- let ti = 0,
- li = ti,
- ri = ti + radius;
-
- const idx = source.getPixelIndex(i, ti);
-
- const firstValRed = source.bitmap.data[idx],
- firstValGreen = source.bitmap.data[idx + 1],
- firstValBlue = source.bitmap.data[idx + 2],
- firstValAlpha = source.bitmap.data[idx + 3];
-
- const lastIdx = source.getPixelIndex(i, height - 1),
- lastValRed = source.bitmap.data[lastIdx],
- lastValGreen = source.bitmap.data[lastIdx + 1],
- lastValBlue = source.bitmap.data[lastIdx + 2],
- lastValAlpha = source.bitmap.data[lastIdx + 3];
-
- let red = (radius + 1) * firstValRed;
- let green = (radius + 1) * firstValGreen;
- let blue = (radius + 1) * firstValBlue;
- let alpha = (radius + 1) * firstValAlpha;
-
- for (let j = 0; j < radius; j++) {
- const jIdx = source.getPixelIndex(i, ti + j);
- red += source.bitmap.data[jIdx];
- green += source.bitmap.data[jIdx + 1];
- blue += source.bitmap.data[jIdx + 2];
- alpha += source.bitmap.data[jIdx + 3];
- }
-
- for (let j = 0; j <= radius; j++) {
- const riIdx = source.getPixelIndex(i, ri++);
- red += source.bitmap.data[riIdx] - firstValRed;
- green += source.bitmap.data[riIdx + 1] - firstValGreen;
- blue += source.bitmap.data[riIdx + 2] - firstValBlue;
- alpha += source.bitmap.data[riIdx + 3] - firstValAlpha;
-
- const tiIdx = source.getPixelIndex(i, ti++);
- output.bitmap.data[tiIdx] = Math.round(red * iarr);
- output.bitmap.data[tiIdx + 1] = Math.round(green * iarr);
- output.bitmap.data[tiIdx + 2] = Math.round(blue * iarr);
- output.bitmap.data[tiIdx + 3] = Math.round(alpha * iarr);
- }
-
- for (let j = radius + 1; j < height - radius; j++) {
- const riIdx = source.getPixelIndex(i, ri++);
- const liIdx = source.getPixelIndex(i, li++);
- red += source.bitmap.data[riIdx] - source.bitmap.data[liIdx];
- green += source.bitmap.data[riIdx + 1] - source.bitmap.data[liIdx + 1];
- blue += source.bitmap.data[riIdx + 2] - source.bitmap.data[liIdx + 2];
- alpha += source.bitmap.data[riIdx + 3] - source.bitmap.data[liIdx + 3];
-
- const tiIdx = source.getPixelIndex(i, ti++);
- output.bitmap.data[tiIdx] = Math.round(red * iarr);
- output.bitmap.data[tiIdx + 1] = Math.round(green * iarr);
- output.bitmap.data[tiIdx + 2] = Math.round(blue * iarr);
- output.bitmap.data[tiIdx + 3] = Math.round(alpha * iarr);
- }
-
- for (let j = height - radius; j < height; j++) {
- const liIdx = source.getPixelIndex(i, li++);
- red += lastValRed - source.bitmap.data[liIdx];
- green += lastValGreen - source.bitmap.data[liIdx + 1];
- blue += lastValBlue - source.bitmap.data[liIdx + 2];
- alpha += lastValAlpha - source.bitmap.data[liIdx + 3];
-
- const tiIdx = source.getPixelIndex(i, ti++);
- output.bitmap.data[tiIdx] = Math.round(red * iarr);
- output.bitmap.data[tiIdx + 1] = Math.round(green * iarr);
- output.bitmap.data[tiIdx + 2] = Math.round(blue * iarr);
- output.bitmap.data[tiIdx + 3] = Math.round(alpha * iarr);
- }
- }
- return output;
-}
diff --git a/src/core/lib/JA4.mjs b/src/core/lib/JA4.mjs
index f600f4d8..58422bca 100644
--- a/src/core/lib/JA4.mjs
+++ b/src/core/lib/JA4.mjs
@@ -91,9 +91,7 @@ export function toJA4(bytes) {
let alpn = "00";
for (const ext of tlsr.handshake.value.extensions.value) {
if (ext.type.value === "application_layer_protocol_negotiation") {
- alpn = parseFirstALPNValue(ext.value.data);
- alpn = alpn.charAt(0) + alpn.charAt(alpn.length - 1);
- if (alpn.charCodeAt(0) > 127) alpn = "99";
+ alpn = alpnFingerprint(parseFirstALPNValue(ext.value.data));
break;
}
}
@@ -212,9 +210,7 @@ export function toJA4S(bytes) {
let alpn = "00";
for (const ext of tlsr.handshake.value.extensions.value) {
if (ext.type.value === "application_layer_protocol_negotiation") {
- alpn = parseFirstALPNValue(ext.value.data);
- alpn = alpn.charAt(0) + alpn.charAt(alpn.length - 1);
- if (alpn.charCodeAt(0) > 127) alpn = "99";
+ alpn = alpnFingerprint(parseFirstALPNValue(ext.value.data));
break;
}
}
@@ -262,3 +258,33 @@ function tlsVersionMapper(version) {
default: return "00"; // Unknown
}
}
+
+/**
+ * Checks if a byte is ASCII alphanumeric (0-9, A-Z, a-z).
+ * @param {number} byte
+ * @returns {boolean}
+ */
+function isAlphanumeric(byte) {
+ return (byte >= 0x30 && byte <= 0x39) ||
+ (byte >= 0x41 && byte <= 0x5A) ||
+ (byte >= 0x61 && byte <= 0x7A);
+}
+
+/**
+ * Computes the 2-character ALPN fingerprint from raw ALPN bytes.
+ * If both first and last bytes are ASCII alphanumeric, returns their characters.
+ * Otherwise, returns first hex digit of first byte + last hex digit of last byte.
+ * @param {Uint8Array|null} rawBytes
+ * @returns {string}
+ */
+function alpnFingerprint(rawBytes) {
+ if (!rawBytes || rawBytes.length === 0) return "00";
+ const firstByte = rawBytes[0];
+ const lastByte = rawBytes[rawBytes.length - 1];
+ if (isAlphanumeric(firstByte) && isAlphanumeric(lastByte)) {
+ return String.fromCharCode(firstByte) + String.fromCharCode(lastByte);
+ }
+ const firstHex = firstByte.toString(16).padStart(2, "0");
+ const lastHex = lastByte.toString(16).padStart(2, "0");
+ return firstHex[0] + lastHex[1];
+}
diff --git a/src/core/lib/Magic.mjs b/src/core/lib/Magic.mjs
index 14111ec7..ad407de6 100644
--- a/src/core/lib/Magic.mjs
+++ b/src/core/lib/Magic.mjs
@@ -1,4 +1,4 @@
-import OperationConfig from "../config/OperationConfig.json" assert {type: "json"};
+import OperationConfig from "../config/OperationConfig.json" with { type: "json" };
import Utils, { isWorkerEnvironment } from "../Utils.mjs";
import Recipe from "../Recipe.mjs";
import Dish from "../Dish.mjs";
diff --git a/src/core/lib/QRCode.mjs b/src/core/lib/QRCode.mjs
index ccac4f29..43f9ec9a 100644
--- a/src/core/lib/QRCode.mjs
+++ b/src/core/lib/QRCode.mjs
@@ -10,7 +10,7 @@ import OperationError from "../errors/OperationError.mjs";
import jsQR from "jsqr";
import qr from "qr-image";
import Utils from "../Utils.mjs";
-import Jimp from "jimp/es/index.js";
+import { Jimp, JimpMime } from "jimp";
/**
* Parses a QR code image from an image
@@ -29,18 +29,31 @@ export async function parseQrCode(input, normalise) {
try {
if (normalise) {
- image.rgba(false);
- image.background(0xFFFFFFFF);
- image.normalize();
image.greyscale();
- image = await image.getBufferAsync(Jimp.MIME_JPEG);
- image = await Jimp.read(image);
+ image.normalize();
}
} catch (err) {
throw new OperationError(`Error normalising image. (${err})`);
}
- const qrData = jsQR(image.bitmap.data, image.getWidth(), image.getHeight());
+ // Remove transparency which jsQR cannot handle
+ image.scan((x, y, idx) => {
+ // If pixel is fully transparent, make it opaque white
+ if (image.bitmap.data[idx + 3] === 0x00) {
+ image.bitmap.data[idx + 0] = 0xff;
+ image.bitmap.data[idx + 1] = 0xff;
+ image.bitmap.data[idx + 2] = 0xff;
+ }
+ // Otherwise, make it fully opaque at its existing colour
+ image.bitmap.data[idx + 3] = 0xff;
+ });
+ image = await Jimp.read(await image.getBuffer(JimpMime.jpeg));
+
+ const qrData = jsQR(
+ new Uint8ClampedArray(image.bitmap.data),
+ image.width,
+ image.height,
+ );
if (qrData) {
return qrData.data;
} else {
@@ -58,7 +71,13 @@ export async function parseQrCode(input, normalise) {
* @param {string} errorCorrection
* @returns {ArrayBuffer}
*/
-export function generateQrCode(input, format, moduleSize, margin, errorCorrection) {
+export function generateQrCode(
+ input,
+ format,
+ moduleSize,
+ margin,
+ errorCorrection,
+) {
const formats = ["SVG", "EPS", "PDF", "PNG"];
if (!formats.includes(format.toUpperCase())) {
throw new OperationError("Unsupported QR code format.");
@@ -70,7 +89,8 @@ export function generateQrCode(input, format, moduleSize, margin, errorCorrectio
type: format,
size: moduleSize,
margin: margin,
- "ec_level": errorCorrection.charAt(0).toUpperCase()
+ // eslint-disable-next-line camelcase
+ ec_level: errorCorrection.charAt(0).toUpperCase(),
});
} catch (err) {
throw new OperationError(`Error generating QR code. (${err})`);
@@ -86,7 +106,7 @@ export function generateQrCode(input, format, moduleSize, margin, errorCorrectio
case "PDF":
return Utils.strToArrayBuffer(qrImage);
case "PNG":
- return qrImage.buffer;
+ return qrImage.buffer.slice(qrImage.byteOffset, qrImage.byteLength + qrImage.byteOffset);
default:
throw new OperationError("Unsupported QR code format.");
}
diff --git a/src/core/lib/TLS.mjs b/src/core/lib/TLS.mjs
index dda3d4c2..a1d55e56 100644
--- a/src/core/lib/TLS.mjs
+++ b/src/core/lib/TLS.mjs
@@ -872,15 +872,15 @@ export function parseHighestSupportedVersion(bytes) {
}
/**
- * Parses the application_layer_protocol_negotiation extension and returns the first value.
+ * Parses the application_layer_protocol_negotiation extension and returns the first value as raw bytes.
* @param {Uint8Array} bytes
- * @returns {number}
+ * @returns {Uint8Array|null}
*/
export function parseFirstALPNValue(bytes) {
const s = new Stream(bytes);
const alpnExtLen = s.readInt(2);
- if (alpnExtLen < 3) return "00";
+ if (alpnExtLen < 2) return null;
const strLen = s.readInt(1);
- if (strLen < 2) return "00";
- return s.readString(strLen);
+ if (strLen < 1) return null;
+ return s.getBytes(strLen);
}
diff --git a/src/core/operations/AddTextToImage.mjs b/src/core/operations/AddTextToImage.mjs
index 2eb03e86..c137b492 100644
--- a/src/core/operations/AddTextToImage.mjs
+++ b/src/core/operations/AddTextToImage.mjs
@@ -9,13 +9,19 @@ import OperationError from "../errors/OperationError.mjs";
import { isImage } from "../lib/FileType.mjs";
import { toBase64 } from "../lib/Base64.mjs";
import { isWorkerEnvironment } from "../Utils.mjs";
-import Jimp from "jimp/es/index.js";
+import {
+ Jimp,
+ JimpMime,
+ ResizeStrategy,
+ measureText,
+ measureTextHeight,
+ loadFont,
+} from "jimp";
/**
* Add Text To Image operation
*/
class AddTextToImage extends Operation {
-
/**
* AddTextToImage constructor
*/
@@ -24,7 +30,8 @@ class AddTextToImage extends Operation {
this.name = "Add Text To Image";
this.module = "Image";
- this.description = "Adds text onto an image.
Text can be horizontally or vertically aligned, or the position can be manually specified. Variants of the Roboto font face are available in any size or colour.";
+ this.description =
+ "Adds text onto an image.
Note: GIF files are supported for input, but cannot be outputted.";
+ this.description =
+ "Converts an image between different formats. Supported formats:
Autocrop Automatically crops same-colour borders from the image.
Autocrop tolerance A percentage value for the tolerance of colour difference between pixels.
Only autocrop frames Only crop real frames (all sides must have the same border)
Symmetric autocrop Force autocrop to be symmetric (top/bottom and left/right are cropped by the same amount)
Autocrop keep border The number of pixels of border to leave around the image.";
+ this.description =
+ "Crops an image to the specified region, or automatically crops edges.
Autocrop Automatically crops same-colour borders from the image.
Autocrop tolerance A percentage value for the tolerance of colour difference between pixels.
Only autocrop frames Only crop real frames (all sides must have the same border)
Symmetric autocrop Force autocrop to be symmetric (top/bottom and left/right are cropped by the same amount)
Autocrop keep border The number of pixels of border to leave around the image.";
this.infoURL = "https://wikipedia.org/wiki/Cropping_(image)";
this.inputType = "ArrayBuffer";
this.outputType = "ArrayBuffer";
@@ -34,30 +34,30 @@ class CropImage extends Operation {
name: "X Position",
type: "number",
value: 0,
- min: 0
+ min: 0,
},
{
name: "Y Position",
type: "number",
value: 0,
- min: 0
+ min: 0,
},
{
name: "Width",
type: "number",
value: 10,
- min: 1
+ min: 1,
},
{
name: "Height",
type: "number",
value: 10,
- min: 1
+ min: 1,
},
{
name: "Autocrop",
type: "boolean",
- value: false
+ value: false,
},
{
name: "Autocrop tolerance (%)",
@@ -65,24 +65,24 @@ class CropImage extends Operation {
value: 0.02,
min: 0,
max: 100,
- step: 0.01
+ step: 0.01,
},
{
name: "Only autocrop frames",
type: "boolean",
- value: true
+ value: true,
},
{
name: "Symmetric autocrop",
type: "boolean",
- value: false
+ value: false,
},
{
name: "Autocrop keep border (px)",
type: "number",
value: 0,
- min: 0
- }
+ min: 0,
+ },
];
}
@@ -92,7 +92,17 @@ class CropImage extends Operation {
* @returns {byteArray}
*/
async run(input, args) {
- const [xPos, yPos, width, height, autocrop, autoTolerance, autoFrames, autoSymmetric, autoBorder] = args;
+ const [
+ xPos,
+ yPos,
+ width,
+ height,
+ autocrop,
+ autoTolerance,
+ autoFrames,
+ autoSymmetric,
+ autoBorder,
+ ] = args;
if (!isImage(input)) {
throw new OperationError("Invalid file type.");
}
@@ -108,20 +118,25 @@ class CropImage extends Operation {
self.sendStatusMessage("Cropping image...");
if (autocrop) {
image.autocrop({
- tolerance: (autoTolerance / 100),
+ tolerance: autoTolerance / 100,
cropOnlyFrames: autoFrames,
cropSymmetric: autoSymmetric,
- leaveBorder: autoBorder
+ leaveBorder: autoBorder,
});
} else {
- image.crop(xPos, yPos, width, height);
+ image.crop({
+ x: xPos,
+ y: yPos,
+ w: width,
+ h: height,
+ });
}
let imageBuffer;
- if (image.getMIME() === "image/gif") {
- imageBuffer = await image.getBufferAsync(Jimp.MIME_PNG);
+ if (image.mime === "image/gif") {
+ imageBuffer = await image.getBuffer(JimpMime.png);
} else {
- imageBuffer = await image.getBufferAsync(Jimp.AUTO);
+ imageBuffer = await image.getBuffer(image.mime);
}
return imageBuffer.buffer;
} catch (err) {
@@ -145,7 +160,6 @@ class CropImage extends Operation {
return ``;
}
-
}
export default CropImage;
diff --git a/src/core/operations/DitherImage.mjs b/src/core/operations/DitherImage.mjs
index 17051480..f21c1f88 100644
--- a/src/core/operations/DitherImage.mjs
+++ b/src/core/operations/DitherImage.mjs
@@ -9,13 +9,12 @@ import OperationError from "../errors/OperationError.mjs";
import { isImage } from "../lib/FileType.mjs";
import { toBase64 } from "../lib/Base64.mjs";
import { isWorkerEnvironment } from "../Utils.mjs";
-import Jimp from "jimp/es/index.js";
+import { Jimp, JimpMime } from "jimp";
/**
* Image Dither operation
*/
class DitherImage extends Operation {
-
/**
* DitherImage constructor
*/
@@ -51,17 +50,19 @@ class DitherImage extends Operation {
try {
if (isWorkerEnvironment())
self.sendStatusMessage("Applying dither to image...");
- image.dither565();
+ image.dither();
let imageBuffer;
- if (image.getMIME() === "image/gif") {
- imageBuffer = await image.getBufferAsync(Jimp.MIME_PNG);
+ if (image.mime === "image/gif") {
+ imageBuffer = await image.getBuffer(JimpMime.png);
} else {
- imageBuffer = await image.getBufferAsync(Jimp.AUTO);
+ imageBuffer = await image.getBuffer(image.mime);
}
return imageBuffer.buffer;
} catch (err) {
- throw new OperationError(`Error applying dither to image. (${err})`);
+ throw new OperationError(
+ `Error applying dither to image. (${err})`,
+ );
}
}
@@ -81,7 +82,6 @@ class DitherImage extends Operation {
return ``;
}
-
}
export default DitherImage;
diff --git a/src/core/operations/EscapeUnicodeCharacters.mjs b/src/core/operations/EscapeUnicodeCharacters.mjs
index db2680c0..08d68581 100644
--- a/src/core/operations/EscapeUnicodeCharacters.mjs
+++ b/src/core/operations/EscapeUnicodeCharacters.mjs
@@ -44,23 +44,6 @@ class EscapeUnicodeCharacters extends Operation {
"value": true
}
];
- this.checks = [
- {
- pattern: "\\\\u(?:[\\da-f]{4,6})",
- flags: "i",
- args: ["\\u"]
- },
- {
- pattern: "%u(?:[\\da-f]{4,6})",
- flags: "i",
- args: ["%u"]
- },
- {
- pattern: "U\\+(?:[\\da-f]{4,6})",
- flags: "i",
- args: ["U+"]
- }
- ];
}
/**
diff --git a/src/core/operations/ExtractLSB.mjs b/src/core/operations/ExtractLSB.mjs
index d5c80406..e64831b1 100644
--- a/src/core/operations/ExtractLSB.mjs
+++ b/src/core/operations/ExtractLSB.mjs
@@ -9,13 +9,12 @@ import OperationError from "../errors/OperationError.mjs";
import Utils from "../Utils.mjs";
import { fromBinary } from "../lib/Binary.mjs";
import { isImage } from "../lib/FileType.mjs";
-import Jimp from "jimp/es/index.js";
+import { Jimp } from "jimp";
/**
* Extract LSB operation
*/
class ExtractLSB extends Operation {
-
/**
* ExtractLSB constructor
*/
@@ -24,8 +23,10 @@ class ExtractLSB extends Operation {
this.name = "Extract LSB";
this.module = "Image";
- this.description = "Extracts the Least Significant Bit data from each pixel in an image. This is a common way to hide data in Steganography.";
- this.infoURL = "https://wikipedia.org/wiki/Bit_numbering#Least_significant_bit_in_digital_steganography";
+ this.description =
+ "Extracts the Least Significant Bit data from each pixel in an image. This is a common way to hide data in Steganography.";
+ this.infoURL =
+ "https://wikipedia.org/wiki/Bit_numbering#Least_significant_bit_in_digital_steganography";
this.inputType = "ArrayBuffer";
this.outputType = "byteArray";
this.args = [
@@ -57,8 +58,8 @@ class ExtractLSB extends Operation {
{
name: "Bit",
type: "number",
- value: 0
- }
+ value: 0,
+ },
];
}
@@ -68,21 +69,27 @@ class ExtractLSB extends Operation {
* @returns {byteArray}
*/
async run(input, args) {
- if (!isImage(input)) throw new OperationError("Please enter a valid image file.");
+ if (!isImage(input))
+ throw new OperationError("Please enter a valid image file.");
const bit = 7 - args.pop(),
pixelOrder = args.pop(),
- colours = args.filter(option => option !== "").map(option => COLOUR_OPTIONS.indexOf(option)),
+ colours = args
+ .filter((option) => option !== "")
+ .map((option) => COLOUR_OPTIONS.indexOf(option)),
parsedImage = await Jimp.read(input),
width = parsedImage.bitmap.width,
height = parsedImage.bitmap.height,
rgba = parsedImage.bitmap.data;
if (bit < 0 || bit > 7) {
- throw new OperationError("Error: Bit argument must be between 0 and 7");
+ throw new OperationError(
+ "Error: Bit argument must be between 0 and 7",
+ );
}
- let i, combinedBinary = "";
+ let i,
+ combinedBinary = "";
if (pixelOrder === "Row") {
for (i = 0; i < rgba.length; i += 4) {
@@ -106,7 +113,6 @@ class ExtractLSB extends Operation {
return fromBinary(combinedBinary);
}
-
}
const COLOUR_OPTIONS = ["R", "G", "B", "A"];
diff --git a/src/core/operations/ExtractRGBA.mjs b/src/core/operations/ExtractRGBA.mjs
index 3339a2a7..b0fe3888 100644
--- a/src/core/operations/ExtractRGBA.mjs
+++ b/src/core/operations/ExtractRGBA.mjs
@@ -7,15 +7,14 @@
import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs";
import { isImage } from "../lib/FileType.mjs";
-import Jimp from "jimp/es/index.js";
+import { Jimp } from "jimp";
-import {RGBA_DELIM_OPTIONS} from "../lib/Delim.mjs";
+import { RGBA_DELIM_OPTIONS } from "../lib/Delim.mjs";
/**
* Extract RGBA operation
*/
class ExtractRGBA extends Operation {
-
/**
* ExtractRGBA constructor
*/
@@ -24,7 +23,8 @@ class ExtractRGBA extends Operation {
this.name = "Extract RGBA";
this.module = "Image";
- this.description = "Extracts each pixel's RGBA value in an image. These are sometimes used in Steganography to hide text or data.";
+ this.description =
+ "Extracts each pixel's RGBA value in an image. These are sometimes used in Steganography to hide text or data.";
this.infoURL = "https://wikipedia.org/wiki/RGBA_color_space";
this.inputType = "ArrayBuffer";
this.outputType = "string";
@@ -32,13 +32,13 @@ class ExtractRGBA extends Operation {
{
name: "Delimiter",
type: "editableOption",
- value: RGBA_DELIM_OPTIONS
+ value: RGBA_DELIM_OPTIONS,
},
{
name: "Include Alpha",
type: "boolean",
- value: true
- }
+ value: true,
+ },
];
}
@@ -48,18 +48,20 @@ class ExtractRGBA extends Operation {
* @returns {string}
*/
async run(input, args) {
- if (!isImage(input)) throw new OperationError("Please enter a valid image file.");
+ if (!isImage(input))
+ throw new OperationError("Please enter a valid image file.");
const delimiter = args[0],
includeAlpha = args[1],
parsedImage = await Jimp.read(input);
let bitmap = parsedImage.bitmap.data;
- bitmap = includeAlpha ? bitmap : bitmap.filter((val, idx) => idx % 4 !== 3);
+ bitmap = includeAlpha ?
+ bitmap :
+ bitmap.filter((val, idx) => idx % 4 !== 3);
return bitmap.join(delimiter);
}
-
}
export default ExtractRGBA;
diff --git a/src/core/operations/FlipImage.mjs b/src/core/operations/FlipImage.mjs
index f4b7cba9..cf9c747f 100644
--- a/src/core/operations/FlipImage.mjs
+++ b/src/core/operations/FlipImage.mjs
@@ -9,13 +9,12 @@ import OperationError from "../errors/OperationError.mjs";
import { isImage } from "../lib/FileType.mjs";
import { toBase64 } from "../lib/Base64.mjs";
import { isWorkerEnvironment } from "../Utils.mjs";
-import Jimp from "jimp/es/index.js";
+import { Jimp, JimpMime } from "jimp";
/**
* Flip Image operation
*/
class FlipImage extends Operation {
-
/**
* FlipImage constructor
*/
@@ -33,8 +32,8 @@ class FlipImage extends Operation {
{
name: "Axis",
type: "option",
- value: ["Horizontal", "Vertical"]
- }
+ value: ["Horizontal", "Vertical"],
+ },
];
}
@@ -60,18 +59,24 @@ class FlipImage extends Operation {
self.sendStatusMessage("Flipping image...");
switch (flipAxis) {
case "Horizontal":
- image.flip(true, false);
+ image.flip({
+ horizontal: true,
+ vertical: false,
+ });
break;
case "Vertical":
- image.flip(false, true);
+ image.flip({
+ horizontal: false,
+ vertical: true,
+ });
break;
}
let imageBuffer;
- if (image.getMIME() === "image/gif") {
- imageBuffer = await image.getBufferAsync(Jimp.MIME_PNG);
+ if (image.mime === "image/gif") {
+ imageBuffer = await image.getBuffer(JimpMime.png);
} else {
- imageBuffer = await image.getBufferAsync(Jimp.AUTO);
+ imageBuffer = await image.getBuffer(image.mime);
}
return imageBuffer.buffer;
} catch (err) {
@@ -95,7 +100,6 @@ class FlipImage extends Operation {
return ``;
}
-
}
export default FlipImage;
diff --git a/src/core/operations/FromBech32.mjs b/src/core/operations/FromBech32.mjs
new file mode 100644
index 00000000..8a01d4db
--- /dev/null
+++ b/src/core/operations/FromBech32.mjs
@@ -0,0 +1,149 @@
+/**
+ * @author Medjedtxm
+ * @copyright Crown Copyright 2025
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+import { decode } from "../lib/Bech32.mjs";
+import { toHex } from "../lib/Hex.mjs";
+
+/**
+ * From Bech32 operation
+ */
+class FromBech32 extends Operation {
+
+ /**
+ * FromBech32 constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "From Bech32";
+ this.module = "Default";
+ this.description = "Bech32 is an encoding scheme primarily used for Bitcoin SegWit addresses (BIP-0173). It uses a 32-character alphabet that excludes easily confused characters (1, b, i, o) and includes a checksum for error detection.
Bech32m (BIP-0350) is an updated version used for Bitcoin Taproot addresses.
Auto-detect will attempt Bech32 first, then Bech32m if the checksum fails.
e.g. The quoted-printable encoded string hello=20world becomes hello world";
+ this.description = "Converts QP-encoded text back to standard text. This format is a content transfer encoding common in email messages.
Normalise Image Attempts to normalise the image before parsing it to improve detection of a QR code.";
+ this.description =
+ "Reads an image file and attempts to detect and read a Quick Response (QR) code from the image.
Bech32m (BIP-0350) is an updated version that fixes a weakness in the original Bech32 checksum and is used for Bitcoin Taproot addresses.
The Human-Readable Part (HRP) identifies the network or purpose (e.g., 'bc' for Bitcoin mainnet, 'tb' for testnet, 'age' for AGE encryption keys).
Maximum output length is 90 characters as per specification.";
+ this.infoURL = "https://wikipedia.org/wiki/Bech32";
+ this.inputType = "ArrayBuffer";
+ this.outputType = "string";
+ this.args = [
+ {
+ "name": "Human-Readable Part (HRP)",
+ "type": "string",
+ "value": "bc"
+ },
+ {
+ "name": "Encoding",
+ "type": "option",
+ "value": ["Bech32", "Bech32m"]
+ },
+ {
+ "name": "Input Format",
+ "type": "option",
+ "value": ["Raw bytes", "Hex"]
+ },
+ {
+ "name": "Mode",
+ "type": "option",
+ "value": ["Generic", "Bitcoin SegWit"]
+ },
+ {
+ "name": "Witness Version",
+ "type": "number",
+ "value": 0,
+ "hint": "SegWit witness version (0-16). Only used in Bitcoin SegWit mode."
+ }
+ ];
+ }
+
+ /**
+ * @param {ArrayBuffer} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ const hrp = args[0];
+ const encoding = args[1];
+ const inputFormat = args[2];
+ const mode = args[3];
+ const witnessVersion = args[4];
+
+ let inputArray;
+ if (inputFormat === "Hex") {
+ // Convert hex string to bytes
+ const hexStr = new TextDecoder().decode(new Uint8Array(input)).replace(/\s/g, "");
+ inputArray = fromHex(hexStr);
+ } else {
+ inputArray = new Uint8Array(input);
+ }
+
+ if (mode === "Bitcoin SegWit") {
+ // Prepend witness version to the input data
+ const withVersion = new Uint8Array(inputArray.length + 1);
+ withVersion[0] = witnessVersion;
+ withVersion.set(inputArray, 1);
+ return encode(hrp, withVersion, encoding, true);
+ }
+
+ return encode(hrp, inputArray, encoding, false);
+ }
+
+}
+
+export default ToBech32;
diff --git a/src/core/operations/ToQuotedPrintable.mjs b/src/core/operations/ToQuotedPrintable.mjs
index 9db5c5a5..2ea204f9 100644
--- a/src/core/operations/ToQuotedPrintable.mjs
+++ b/src/core/operations/ToQuotedPrintable.mjs
@@ -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.
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.
',
diff --git a/tests/operations/tests/GenerateQRCode.mjs b/tests/operations/tests/GenerateQRCode.mjs
new file mode 100644
index 00000000..99ed8f9d
--- /dev/null
+++ b/tests/operations/tests/GenerateQRCode.mjs
@@ -0,0 +1,67 @@
+/**
+ * Generate QR Code tests
+ *
+ * @author GCHQDeveloper581
+ * @copyright Crown Copyright 2025
+ * @license Apache-2.0
+ */
+import TestRegister from "../../lib/TestRegister.mjs";
+
+TestRegister.addTests([
+ {
+ name: "Generate QR Code : PNG",
+ input: "Hello world!",
+ expectedOutput: "89 50 4e 47 0d 0a 1a 0a 00 00 00 0d 49 48 44 52 00 00 00 91 00 00 00 91 08 00 00 00 00 e6 b3 05 ff 00 00 01 1a 49 44 41 54 78 da ed da 41 12 83 20 0c 05 50 ef 7f e9 76 dd 05 f4 47 6c c4 ce 63 e5 8c 0c be 4d 24 24 1c af dd c6 41 44 44 44 44 44 44 44 44 44 f4 9f a2 e3 fb 98 cf 2b ad 42 44 d4 2a 1a 07 c3 e7 37 83 a7 d2 37 88 88 1a 44 c3 18 1a 46 e7 ca 2a 44 44 7b 88 4a f3 88 88 1e 23 9a ef 09 44 44 fb 8a 82 b7 c3 3c fe 8e 8c 8d 88 e8 b2 33 6d ba b3 f4 9d b2 89 88 16 eb 90 f3 a5 ef a8 8c 12 11 55 f3 a3 61 a2 93 e6 4c c3 45 89 88 ba 44 a5 e4 e7 64 5d 9d 88 a8 5f 14 74 82 d2 8a 64 5a b4 24 22 6a 10 a5 2d cf 79 3f e9 6c 53 89 88 e8 a7 a2 79 4f a8 b4 b3 ac 57 6b 88 88 ae 15 a5 de b9 bc 14 70 44 44 3f 13 ad d4 21 03 ea d9 6a 0d 11 d1 15 a2 e0 ff 5f 47 07 36 22 a2 06 d1 4a d2 1f 1c 94 89 88 b6 14 95 ee d5 12 11 3d 50 14 74 8c 82 70 24 22 ea 12 05 6f d3 4b 2c 4b d5 1a 22 a2 cb 44 2b 69 7d e9 5e 00 11 51 97 e8 d6 41 44 44 44 44 44 44 44 44 44 f4 7c d1 1b 1c 52 72 cb 26 c8 c7 0b 00 00 00 00 49 45 4e 44 ae 42 60 82",
+ recipeConfig: [
+ {
+ "op": "Generate QR Code",
+ "args": ["PNG", 5, 4, "Medium"]
+ },
+ {
+ "op": "To Hex",
+ "args": ["Space", 0]
+ }
+ ],
+ },
+ {
+ name: "Generate QR Code : SVG",
+ input: "Hello world!",
+ expectedOutput: '',
+ recipeConfig: [
+ {
+ "op": "Generate QR Code",
+ "args": ["SVG", 5, 4, "Medium"]
+ },
+ ],
+ },
+ {
+ name: "Generate QR Code : EPS",
+ input: "Hello world!",
+ expectedOutput: "%!PS-Adobe-3.0 EPSF-3.0%%BoundingBox: 0 0 315 315/h { 0 rlineto } bind def/v { 0 exch neg rlineto } bind def/M { neg 30 add moveto } bind def/z { closepath } bind def9 9 scale5 0 M 7 h 7 v -7 h z13 0 M 1 h 1 v -1 h z16 0 M 2 h 1 v -1 h 1 v 1 h 1 v -2 h -1 v -1 h 2 v -1 h -3 v 2 h z20 0 M 2 h 1 v -2 h z23 0 M 7 h 7 v -7 h z6 1 M 5 v 5 h -5 v z24 1 M 5 v 5 h -5 v z7 2 M 3 h 3 v -3 h z19 2 M 1 h 1 v -1 h z21 2 M 1 h 2 v -1 h z25 2 M 3 h 3 v -3 h z13 4 M 1 h 3 v -1 h z17 4 M 4 h 1 v 1 h 2 v -1 h -1 v -1 h 1 v -1 h -1 v -1 h -1 v -1 h z15 6 M 1 h 1 v -1 h z17 6 M 1 h 1 v -1 h z16 7 M 1 h 1 v -1 h z18 7 M 1 h 1 v -1 h z20 7 M 1 h 2 v 1 h 1 v -2 h -1 v -1 h -1 v 1 h z6 8 M 7 h 1 v 2 h 1 v -2 h 1 v -2 h -1 v 1 h -1 v -4 h 1 v -1 h -1 v -1 h z17 8 M 1 h 1 v -1 h z24 8 M 2 h 1 v -1 h 1 v -1 h z29 8 M 1 h 1 v -1 h z27 9 M 1 h 1 v -1 h z6 10 M 1 h 1 v 1 h 1 v -1 h 1 v -1 h z8 10 M 2 h 5 v 1 h 1 v -3 h 1 v -1 h -4 v 1 h 1 v 1 h -1 v -1 h -1 v 1 h -1 v -1 h z16 10 M 2 h 4 v 1 h -1 v 2 h 1 v 3 h -1 v -3 h -2 v 1 h 1 v 1 h -1 v 1 h 1 v 4 h -2 v 2 h 3 v -2 h 1 v -1 h -1 v -2 h 1 v 2 h 1 v -1 h 1 v 2 h 2 v 1 h -1 v 1 h 3 v -1 h -1 v -2 h -2 v -1 h 2 v 1 h 1 v 2 h 1 v -2 h 2 v -1 h -1 v -1 h -1 v -1 h 1 v -1 h -1 v -1 h 2 v -1 h -1 v -1 h -1 v 1 h -2 v -1 h -1 v -1 h 1 v 1 h 2 v -1 h -1 v -1 h 1 v -1 h -3 v 1 h -1 v -1 h 1 v -1 h -1 v -1 h -1 v 4 h 1 v 1 h -2 v -3 h -1 v -1 h 1 v -1 h -1 v -1 h 2 v -1 h 1 v -2 h -1 v 1 h -1 v -1 h -1 v 1 h -1 v 1 h -1 v 1 h 1 v 1 h -1 v 1 h 1 v 1 h -1 v -1 h z22 10 M 1 h 1 v -1 h z25 10 M 2 h 1 v -2 h z19 11 M 1 h 1 v -1 h z11 12 M 1 h 1 v -1 h z5 13 M 1 h 4 v -1 h z28 14 M 2 h 2 v -1 h -1 v -1 h z21 15 M 1 v 2 h -1 v z13 17 M 1 h 1 v 2 h 1 v -2 h 1 v 1 h 1 v 1 h 1 v -2 h 1 v 2 h 2 v -1 h -1 v -2 h z22 17 M 3 v 3 h -3 v z5 18 M 7 h 7 v -7 h z23 18 M 1 h 1 v -1 h z6 19 M 5 v 5 h -5 v z7 20 M 3 h 3 v -3 h z29 21 M 1 h 4 v -3 h -1 v 2 h z17 22 M 2 h 3 v -2 h -1 v 1 h -1 v -1 h z24 22 M 1 h 1 v 1 h 1 v -1 h 1 v -3 h -1 v 1 h -1 v 1 h z20 23 M 1 h 2 v -1 h zfill%%EOF",
+ recipeConfig: [
+ {
+ "op": "Generate QR Code",
+ "args": ["EPS", 6, 5, "Quartile"]
+ },
+ {
+ "op": "Remove whitespace",
+ "args": [false, true, true, false, false, false]
+ },
+ ],
+ },
+ {
+ name: "Generate QR Code : PDF",
+ input: "Hello world!",
+ expectedOutput: "%PDF-1.01 0 obj << /Type /Catalog /Pages 2 0 R >> endobj2 0 obj << /Type /Pages /Count 1 /Kids [ 3 0 R ] >> endobj3 0 obj << /Type /Page /Parent 2 0 R /Resources <<>> /Contents 4 0 R /MediaBox [ 0 0 261 261 ] >> endobj4 0 obj << /Length 1837 >> stream9 0 0 9 0 0 cm4 25 m 11 25 l 11 18 l 4 18 l h12 25 m 14 25 l 14 23 l 13 23 l 13 24 l 12 24 l h16 25 m 17 25 l 17 22 l 16 22 l h18 25 m 25 25 l 25 18 l 18 18 l h5 24 m 5 19 l 10 19 l 10 24 l h19 24 m 19 19 l 24 19 l 24 24 l h6 23 m 9 23 l 9 20 l 6 20 l h12 23 m 13 23 l 13 21 l 15 21 l 15 20 l 12 20 l h14 23 m 15 23 l 15 22 l 14 22 l h20 23 m 23 23 l 23 20 l 20 20 l h15 22 m 16 22 l 16 21 l 15 21 l h12 19 m 13 19 l 13 18 l 12 18 l h14 19 m 15 19 l 15 13 l 13 13 l 13 11 l 12 11 l 12 14 l 14 14 l 14 15 l 11 15 l 11 16 l 12 16 l 12 17 l 13 17 l 13 16 l 14 16 l 14 17 l 13 17 l 13 18 l 14 18 l h16 19 m 17 19 l 17 18 l 16 18 l h4 17 m 8 17 l 8 16 l 10 16 l 10 15 l 11 15 l 11 14 l 10 14 l 10 13 l 11 13 l 11 12 l 9 12 l 9 15 l 8 15 l 8 13 l 6 13 l 6 15 l 7 15 l 7 16 l 4 16 l h10 17 m 11 17 l 11 16 l 10 16 l h17 17 m 18 17 l 18 16 l 20 16 l 20 17 l 23 17 l 23 15 l 20 15 l 20 13 l 19 13 l 19 15 l 18 15 l 18 14 l 17 14 l 17 13 l 16 13 l 16 16 l 17 16 l h24 17 m 25 17 l 25 14 l 24 14 l 24 13 l 23 13 l 23 15 l 24 15 l h21 14 m 22 14 l 22 13 l 21 13 l h15 13 m 16 13 l 16 11 l 15 11 l h17 13 m 19 13 l 19 12 l 21 12 l 21 10 l 20 10 l 20 9 l 19 9 l 19 10 l 18 10 l 18 9 l 16 9 l 16 8 l 15 8 l 15 10 l 17 10 l 17 11 l 18 11 l 18 12 l 17 12 l h24 13 m 25 13 l 25 11 l 24 11 l h22 12 m 23 12 l 23 11 l 22 11 l h4 11 m 11 11 l 11 4 l 4 4 l h14 11 m 15 11 l 15 10 l 14 10 l h5 10 m 5 5 l 10 5 l 10 10 l h13 10 m 14 10 l 14 9 l 13 9 l h21 10 m 23 10 l 23 9 l 24 9 l 24 7 l 23 7 l 23 6 l 22 6 l 22 7 l 21 7 l h6 9 m 9 9 l 9 6 l 6 6 l h12 8 m 15 8 l 15 7 l 13 7 l 13 6 l 16 6 l 16 4 l 15 4 l 15 5 l 14 5 l 14 4 l 12 4 l h16 8 m 17 8 l 17 6 l 16 6 l h18 8 m 19 8 l 19 7 l 18 7 l h19 7 m 20 7 l 20 6 l 21 6 l 21 5 l 20 5 l 20 4 l 17 4 l 17 6 l 19 6 l h24 6 m 25 6 l 25 5 l 24 5 l h22 5 m 23 5 l 23 4 l 22 4 l hfendstreamendobjxref0 50000000000 65535 f 0000000010 00000 n 0000000059 00000 n 0000000118 00000 n 0000000223 00000 n trailer << /Root 1 0 R /Size 5 >>startxref2111%%EOF",
+ recipeConfig: [
+ {
+ "op": "Generate QR Code",
+ "args": ["PDF", 5, 4, "Low"]
+ },
+ {
+ "op": "Remove whitespace",
+ "args": [false, true, true, false, false, false]
+ },
+ ],
+ },
+]);
diff --git a/tests/operations/tests/Hex.mjs b/tests/operations/tests/Hex.mjs
index 3bb89544..6e49f6f8 100644
--- a/tests/operations/tests/Hex.mjs
+++ b/tests/operations/tests/Hex.mjs
@@ -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",
diff --git a/tests/operations/tests/Hexdump.mjs b/tests/operations/tests/Hexdump.mjs
index 90523a08..6eb486db 100644
--- a/tests/operations/tests/Hexdump.mjs
+++ b/tests/operations/tests/Hexdump.mjs
@@ -152,6 +152,17 @@ TestRegister.addTests([
}
],
},
+ {
+ name: "From Hexdump: xxd format, odd number of bytes",
+ input: "00000000: 6162 6364 65 abcde",
+ expectedOutput: "abcde",
+ recipeConfig: [
+ {
+ op: "From Hexdump",
+ args: []
+ }
+ ],
+ },
{
name: "From Hexdump: Wireshark",
input: `00000000 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f ........ ........
diff --git a/tests/operations/tests/JA4.mjs b/tests/operations/tests/JA4.mjs
index 0fb4624e..699dca40 100644
--- a/tests/operations/tests/JA4.mjs
+++ b/tests/operations/tests/JA4.mjs
@@ -30,6 +30,28 @@ TestRegister.addTests([
}
],
},
+ {
+ name: "JA4 Fingerprint: TLS 1.3 with whitespace-only ALPN",
+ input: "1603010200010001fc0303ed338a18e711d670cdc472ff570a5b59f1ace12e5365918bf68bf845019147b6207e4437bfb062d98a4aeb753be8f09022a9dc9413d7694dad4db57fcdcf076e820024130213031301c02cc030c02bc02fcca9cca8c024c028c023c027009f009e006b006700ff0100018f0000001800160000136465762e636f6e74656e74677261622e6e6574000b000403000102000a00160014001d0017001e00190018010001010102010301040023000000100004000201200016000000170000000d002a0028040305030603080708080809080a080b080408050806040105010601030303010302040205020602002b00050403040303002d00020101003300260024001d00207af053336d5e2c1675aa4c6ce78de5e5fdbd296538113f051ea17ccb64289f22001500d2000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ expectedOutput: "t13d181220_85036bcba153_d41ae481755e",
+ recipeConfig: [
+ {
+ "op": "JA4 Fingerprint",
+ "args": ["Hex", "JA4"]
+ }
+ ],
+ },
+ {
+ name: "JA4 Fingerprint: TLS 1.3 with ALPN containing a whitespace",
+ input: "1603010200010001fc0303273682a603be3f64dd025df4ad0f4d2d13043c3a233405a68bb29b865808749a20f4dfc40242b2fce38fae26c516ef9bef20a1b9349eba3c003780168d72471f5c0024130213031301c02cc030c02bc02fcca9cca8c024c028c023c027009f009e006b006700ff0100018f0000001800160000136465762e636f6e74656e74677261622e6e6574000b000403000102000a00160014001d0017001e0019001801000101010201030104002300000010000500030261200016000000170000000d002a0028040305030603080708080809080a080b080408050806040105010601030303010302040205020602002b00050403040303002d00020101003300260024001d0020f4dd1567bd858d3a9f1d88db1fee6a10ab0ea1aa6afe96ffb6a7c4d79dea4075001500d10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ expectedOutput: "t13d181260_85036bcba153_d41ae481755e",
+ recipeConfig: [
+ {
+ "op": "JA4 Fingerprint",
+ "args": ["Hex", "JA4"]
+ }
+ ],
+ },
{
name: "JA4 Fingerprint: TLS 1.2",
input: "1603010200010001fc0303ecb2691addb2bf6c599c7aaae23de5f42561cc04eb41029acc6fc050a16ac1d22046f8617b580ac9358e2aa44e306d52466bcc989c87c8ca64309f5faf50ba7b4d0022130113031302c02bc02fcca9cca8c02cc030c00ac009c013c014009c009d002f00350100019100000021001f00001c636f6e74696c652e73657276696365732e6d6f7a696c6c612e636f6d00170000ff01000100000a000e000c001d00170018001901000101000b00020100002300000010000e000c02683208687474702f312e310005000501000000000022000a000804030503060302030033006b0069001d00208909858fbeb6ed2f1248ba5b9e2978bead0e840110192c61daed0096798b184400170041044d183d91f5eed35791fa982464e3b0214aaa5f5d1b78616d9b9fbebc22d11f535b2f94c686143136aa795e6e5a875d6c08064ad5b76d44caad766e2483012748002b00050403040303000d0018001604030503060308040805080604010501060102030201002d00020101001c000240010015007a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
diff --git a/tests/operations/tests/JWTSign.mjs b/tests/operations/tests/JWTSign.mjs
index a7752138..395a8196 100644
--- a/tests/operations/tests/JWTSign.mjs
+++ b/tests/operations/tests/JWTSign.mjs
@@ -15,26 +15,52 @@ const inputObject = JSON.stringify({
}, null, 4);
const hsKey = "secret_cat";
-const rsKey = `-----BEGIN RSA PRIVATE KEY-----
-MIICWwIBAAKBgQDdlatRjRjogo3WojgGHFHYLugdUWAY9iR3fy4arWNA1KoS8kVw
-33cJibXr8bvwUAUparCwlvdbH6dvEOfou0/gCFQsHUfQrSDv+MuSUMAe8jzKE4qW
-+jK+xQU9a03GUnKHkkle+Q0pX/g6jXZ7r1/xAK5Do2kQ+X5xK9cipRgEKwIDAQAB
-AoGAD+onAtVye4ic7VR7V50DF9bOnwRwNXrARcDhq9LWNRrRGElESYYTQ6EbatXS
-3MCyjjX2eMhu/aF5YhXBwkppwxg+EOmXeh+MzL7Zh284OuPbkglAaGhV9bb6/5Cp
-uGb1esyPbYW+Ty2PC0GSZfIXkXs76jXAu9TOBvD0ybc2YlkCQQDywg2R/7t3Q2OE
-2+yo382CLJdrlSLVROWKwb4tb2PjhY4XAwV8d1vy0RenxTB+K5Mu57uVSTHtrMK0
-GAtFr833AkEA6avx20OHo61Yela/4k5kQDtjEf1N0LfI+BcWZtxsS3jDM3i1Hp0K
-Su5rsCPb8acJo5RO26gGVrfAsDcIXKC+bQJAZZ2XIpsitLyPpuiMOvBbzPavd4gY
-6Z8KWrfYzJoI/Q9FuBo6rKwl4BFoToD7WIUS+hpkagwWiz+6zLoX1dbOZwJACmH5
-fSSjAkLRi54PKJ8TFUeOP15h9sQzydI8zJU+upvDEKZsZc/UhT/SySDOxQ4G/523
-Y0sz/OZtSWcol/UMgQJALesy++GdvoIDLfJX5GBQpuFgFenRiRDabxrE9MNUZ2aP
-FaFp+DyAe+b4nDwuJaW2LURbr8AEZga7oQj0uYxcYw==
------END RSA PRIVATE KEY-----`;
-const esKey = `-----BEGIN PRIVATE KEY-----
-MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgevZzL1gdAFr88hb2
-OF/2NxApJCzGCEDdfSp6VQO30hyhRANCAAQRWz+jn65BtOMvdyHKcvjBeBSDZH2r
-1RTwjmYSi9R/zpBnuQ4EiMnCqfMPWiZqB4QdbAd0E7oH50VpuZ1P087G
+const rsKey = `-----BEGIN PRIVATE KEY-----
+MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDnBZHfKzx27jc+
+9cFPj3b42HO8auxjtlpJpiWgMluSJi8ECrN3iIODf5H9xf1Np61+4xtz3SDKibdA
+XgB9rhXRz6LcRNN962QtIvKriI+Mr9EEaAwNFYziZ8bhDVgawheWCqSJO0DjDDDk
+0CTbfl7SeIhcTuoPXeWFQICUsyc6w3uE+KWnnPZnK9l3R02ssBjsqQhf3LZ9bz04
+52ahhuBg1BgBOIeu8Y8hzTAjsOuMDZ7XV8MbEpcmOYRy46vEjRTl3ZIpYoS0v5st
+M60swjnPiv43oBAsziVddIJWnh+TnBacnuhHl7amuGCLspogftJGefuGDNxIJOLH
+mPaO5R7vAgMBAAECggEADZn32SZQBIaI7SWF8Ju3OvZvdffrnAFH9o8YJwLf/k5O
+NVQ19cMtTwgrPcAy5igJoG9Zlew+en46Mkl2iO+/bB9n7MUGmKLLvpaQqAW9weA2
+E6bWksyig0/t1yE0fzrPLa/JuSSqcNOua0JP8TZS+dxL1vd0c1wpX7uI9nhHxn9Q
+OJNY6N+ebeIjEugYJaeaLWj6cQ1x9V0+JealQtlra4Xex/CwczqPLKD01bdPe/Fn
+5iLG8/dIofHz+615UTC52vwKaL2JiWUHX9gBlahrZYOuhxJQ1RRAf03+Ij2hyUwa
+tOKglPZIhDajALfU3qSfOfRHnMKpakdA9tiZ1TTIvQKBgQD5lUvlZOfY/WdkHl3u
+RnF3g/lWT4dRzdcezeYkLAdYcu5Q1ExVlYPezA9tYD2uR7cziJy/em1v0tPXzCSL
++BGB8/Ds4wg3QYnV8RXgI/4XxXf2iNbfJEfJ74nxza1/9L9Bnvg1OZLcdagu+Ria
+p8y3hWzbBie52o0T8s/KDo2/PQKBgQDs9hseyloiDqsmW7hfLw+AiH1IGwXgK53b
+ZeBwtASr7SYD1Ej0xFZ5SZ/OukjdDAQoyrivpjPcCjXxR1eoCA857S/+sSRPk+Gw
+3+9lWys7jd4VEWvQgV7PKwGGVc1Ku/aKjNbNnu4HxbQfb7bJZnvoW8WqsqHhT/2z
+bFnuYVD5mwKBgQC88adpXECg5wX0p4CYuD+CKSkDjGV3KoumyF1oGOTesvNzwaSg
+TfZtHrK3LNrFK4mnu85erwJWW5cAkY1BYWVvqgtEaoN3wWflzQOwkc70lAvDWcjB
+WSf32h3mLr0gV1rLBNwG/zUNLQ1LskxMGKhEbv//t+MvMiMHbRSddPMeSQKBgQDm
+H8QKzP1nodM4905AshVeAC+a/RNhtzogvfmPumPnC/IlOd54RsysEYIvY94rPeY0
+L1vYyZIHmar1XRGVz+3plZ1MvX/EAJvoCDIXvshnl8kbsMWBwoHus5dRfLZYY950
+g36ARl5oEepxtS5QvUSMTcPTmJN5mxOJUiqsRLo9DQKBgQDyOatwmZLrWs8Sipet
+pdJpPnBJ1TE+Dm1WmaUfmjoXJxi07Bfikex5ybqqed49s5TSb2OBU4eG/yVLQcix
+ik0m4tG0NSEJ/EgfwUxdteHSjGmWnuiK0r3oAz6ffE32RfqvEoFRcCh/H8MdkVtg
+ErVnxmpzv5elRSZ/DUn2uygxgA==
-----END PRIVATE KEY-----`;
+const es256Key = `-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEIA3ISxaDm/NdIVmkuAWR5VTNNj2PTpe+eWhv2rUhNE9joAoGCCqGSM49
+AwEHoUQDQgAEY5Iad2fwtKm40eqg+hx4sxwsZwKm0WP/GDNGAPUisD3H2AlLedYE
++xZ8cehEKjddb6Bftv0I3PLGFMBQIyE0pA==
+-----END EC PRIVATE KEY-----`;
+const es384Key = `-----BEGIN EC PRIVATE KEY-----
+MIGkAgEBBDAp0jx0wSk6L2od/DbovYvrhG/AKXAFpqaQ7/u8tgg7w0lBHWrA9yHF
+YG1Z0k5lEACgBwYFK4EEACKhZANiAASYGNRZMaBebhOkwML7V4JJgiXqukoopqun
+xXWTCEw4qCEqydsXE6TFVB2RJ0+hpjoK00XqVVvyv8fw6ZYH9y5dHmj0RsnBMrAm
+oOhhUpXlZWsfiY97db80mKCp7n2MkNw=
+-----END EC PRIVATE KEY-----`;
+const es512Key = `-----BEGIN EC PRIVATE KEY-----
+MIHcAgEBBEIARI9C6QrByhpJ8paMAgKyz6piOIEF2wDxlPEFcVC3ZHSo4crE2Uc5
+E94EOuLNnKs/TinGMiTUhbefpfETI5SML4qgBwYFK4EEACOhgYkDgYYABADka9q3
+ZXy2j1ml6fQqFbG6ryD2+2Cu43xxDeEoec9PW4Dfe/hmkluYwSv15X5+VhBx36sh
+Jeh34sXjACyMVXfLGQGS3Puxol0c1nyC1uv9ecxLJmWUtUmPm9Uz87tb3vhI636x
+8YT3MpqHkOZnDm4CiG09QIS9ROUHKH/BGQ7QtH/9yA==
+-----END EC PRIVATE KEY-----`;
TestRegister.addTests([
{
@@ -88,7 +114,7 @@ TestRegister.addTests([
recipeConfig: [
{
op: "JWT Sign",
- args: [esKey, "ES256", "{}"],
+ args: [es256Key, "ES256", "{}"],
},
{
op: "JWT Decode",
@@ -103,7 +129,7 @@ TestRegister.addTests([
recipeConfig: [
{
op: "JWT Sign",
- args: [esKey, "ES384", "{}"],
+ args: [es384Key, "ES384", "{}"],
},
{
op: "JWT Decode",
@@ -118,7 +144,7 @@ TestRegister.addTests([
recipeConfig: [
{
op: "JWT Sign",
- args: [esKey, "ES512", "{}"],
+ args: [es512Key, "ES512", "{}"],
},
{
op: "JWT Decode",
@@ -163,7 +189,7 @@ TestRegister.addTests([
recipeConfig: [
{
op: "JWT Sign",
- args: [esKey, "RS512", "{}"],
+ args: [rsKey, "RS512", "{}"],
},
{
op: "JWT Decode",
diff --git a/tests/samples/Images.mjs b/tests/samples/Images.mjs
index 7ee39643..663fa201 100644
--- a/tests/samples/Images.mjs
+++ b/tests/samples/Images.mjs
@@ -18,18 +18,6 @@ export const GIF_ANIMATED_HEX = "4749463839610f000f00b30b00424242ffe700ffef00ffc
*/
export const PNG_HEX = "89504e470d0a1a0a0000000d4948445200000020000000200806000000737a7af400000006624b474400ff00ff00ffa0bda793000000097048597300000dd700000dd70142289b78000005184944415458c3c5575d6c145514feeeccecccacdbddb6e096a5dbcdb6d06d80d06090466d6953454ab52ad0a65589840ac1d02a313c989af062820fa66210130d9a68b0363c34610135690b188b7183c13f44506c8115ba535ab6ddd2617f667f66ae0fb41596ddee2eadf13c4de69e7bcf77cff9cecf25b83f613b3b3b975b2c96f25028c47a3c9e1f5a5a5a7e05a0016000d0c9ef9442d23448a60edeb973a769c78e1d077272721a65594620106000505996bf1a1f1f3f67369bebc2e1f0ef6bd7aedd0a409d2d00e2743a1f2929296915046199a66901007aa3d1580600131313da24000000a594124288aaaab72a2b2bed1d1d1d8f8ba2386fc3860d9f25f3c84c0088cbe56a2d2c2cdc4708d12552880770a7288a3228088215003c1ecfd68d1b377e9e488f4b66dde974aeb2dbed498da71251146d538ed1b4e4746092dddee170b4300ca3c32c251c0edfd8bc79f3d164de4e0680110461794a02119292c482202c387efcf86f3d3d3d7b13814816024a2955e62a8b4451b4abaafad8e485d5743ca005028153699c4dd30c83140a857e4c9409c900a0bbbbfbc368343a34a3754a693a1c58b76eddf2dadada5d89002705b07bf7eee13367ce3cab284aff6c482808425e6767e70bc9ea0033d3e6c6c6c65fd6ac5953a1695a3453c3a150c84d295529a59aa669914cd3705adc6eb7926eaca74455d5605555d5c3030303f59224bd525f5f7f30992e87ff40344d5328a5caa64d9bbe4ca5cbe07f1666ae522dae40a5dd8ed30941c8e5727d63341a9f8a5f181a1ac2f0f07022029e02109d2b00bae2e26207cbb2f72cf03c8f9c9c9c441c580c804dc70b330258b6c020beb87ac9abecb59f8b087377b4f4f30a68b6de482549a29224ddb5168bc51cd5d5d54ff6f5f575cfa69633edeb971c78e2d195db055e77cfb6a2eaadb816e5b59ffafb19a7d3095555e3ab64341a8d96f6f6f6fe755f247c69d542abd9c0bd3c70f90a628c30fd5f56542c5c550fc3837600406e6e2eca9e2e433837fcefc0c8b2e079fe7b9fcfe7aba9a9296613c52f55084acc864a027013b28c828a2d30e805bcbe670fac4b5740f5a9285b18c6a0db4da8c180fdc6fdb035d850c555a174a4148410b85cae7293c97442a7d395363434347775757d91b6075a2a6c45d66ce18369258685de644659d96af45ff80345f9f908c932821313c4eff7639b6d1b06838358242c82d96c86288abe582ce6e6797e052184701c9797910796e61976b10c991fff7f7b5313b6373541d5340426d36f747414e5c67294679503a1e90634e6f57adbac56ebb14020f0e9a14387decf84038c8e232b53b45888dc6dec63636389d290c9caca5a3d09a6a2a6a6a628130054d33092a2c52272bbe4515996113f16288ab2c86432bd01001cc72db5582caf651202eaf5473e7e80d7af270409d9cb320c0c66331ca5a5602c1624180d492412392bcbf2db46a3f1394992f665c481b77a2f9f78e719476b5e16ff2e00d31dae8524cb30e8f560390ee72e5e243d7d7d34168bc16030a87575752ccbb20400a2d1e8b7478e1c390ce0f0fd5442fae6d7039f343d643956345f5fcbf1fafd00b219868145afc78d4b97101a1b833a32426d361bcdcfcf87cd6663a7a6649ee70725497a6faede86e4c2c993cf171716eee5753aeb9d0b7f5ebfae5df67a99b86164e8e6cd9badcdcdcdc7d27ae5a6a3f45147c7794dd30e2e59bcf896c0f3851ccbe602c0a8df4fc783413269d8130c06f79d3e7d7a4b5b5bdbd9b45b77c60304c3f0df75752db31714acf8dbe7cbbee2f5fafd7efff9f6f6f6b357af5e8d647ade3fa1780bad734c65970000000049454e44ae426082";
-/**
- * The CyberChef logo with 'chef'
- * 32x32
- */
-export const PNG_CHEF_B64 = "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";
-
-/**
- * The CyberChef logo with blur
- * 32x32
- */
-export const PNG_BLUR_B64 = "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";
-
/**
* Sunglasses smiley
* 32x32