diff --git a/.github/workflows/releases.yml b/.github/workflows/releases.yml index 20968772..a77f4984 100644 --- a/.github/workflows/releases.yml +++ b/.github/workflows/releases.yml @@ -92,6 +92,5 @@ jobs: - name: Publish to NPM uses: JS-DevTools/npm-publish@v1 - if: false with: - token: ${{ secrets.NPM_TOKEN }} \ No newline at end of file + token: ${{ secrets.NPM_TOKEN }} diff --git a/Dockerfile b/Dockerfile index d63a8ca3..ba605fd7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,8 +29,7 @@ RUN npm run build ######################################### # 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 TARGETARCH ARG TARGETPLATFORM -FROM ${TARGETARCH}/nginx:stable-alpine AS cyberchef +FROM --platform=${TARGETPLATFORM} nginx:stable-alpine AS cyberchef COPY --from=builder /app/build/prod /usr/share/nginx/html/ diff --git a/README.md b/README.md index 5549bda2..89f0371d 100755 --- a/README.md +++ b/README.md @@ -20,21 +20,36 @@ Cryptographic operations in CyberChef should not be relied upon to provide secur [A live demo can be found here][1] - have fun! -## Containers +## Running Locally with Docker -If you would like to try out CyberChef locally you can either build it yourself: +**Prerequisites** +- [Docker](hhttps://www.docker.com/products/docker-desktop/) + - Docker Desktop must be open and running on your machine + + +#### Option 1: Build the Docker Image Yourself + +1. Build the docker image ```bash docker build --tag cyberchef --ulimit nofile=10000 . +``` +2. Run the docker container +```bash docker run -it -p 8080:80 cyberchef ``` +3. Navigate to `http://localhost:8080` in your browser -Or you can use our image directly: +#### Option 2: Use the pre-built Docker Image + +If you prefer to skip the build process, you can use the pre-built image ```bash docker run -it -p 8080:80 ghcr.io/gchq/cyberchef:latest ``` +Just like before, navigate to `http://localhost:8080` in your browser. + This image is built and published through our [GitHub Workflows](.github/workflows/releases.yml) ## How it works diff --git a/package-lock.json b/package-lock.json index ef2da3f0..b374df4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,6 +46,8 @@ "file-saver": "^2.0.5", "flat": "^6.0.1", "geodesy": "1.1.3", + "handlebars": "^4.7.8", + "hash-wasm": "^4.12.0", "highlight.js": "^11.9.0", "ieee754": "^1.2.1", "jimp": "^0.22.12", @@ -54,6 +56,7 @@ "js-sha3": "^0.9.3", "jsesc": "^3.0.2", "json5": "^2.2.3", + "jsonata": "^2.0.3", "jsonpath-plus": "^9.0.0", "jsonwebtoken": "8.5.1", "jsqr": "^1.4.0", @@ -94,6 +97,7 @@ "ua-parser-js": "^1.0.38", "unorm": "^1.6.0", "utf8": "^3.0.0", + "uuid": "^11.1.0", "vkbeautify": "^0.99.3", "xpath": "0.0.34", "xregexp": "^5.1.1", @@ -10842,6 +10846,27 @@ "dev": true, "license": "MIT" }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, "node_modules/has-ansi": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", @@ -10940,6 +10965,11 @@ "node": ">= 0.10" } }, + "node_modules/hash-wasm": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/hash-wasm/-/hash-wasm-4.12.0.tgz", + "integrity": "sha512-+/2B2rYLb48I/evdOIhP+K/DD2ca2fgBjp6O+GBEnCDk2e4rpeXIK8GvIyRPjTezgmWn9gmKwkQjjx6BtqDHVQ==" + }, "node_modules/hash.js": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", @@ -12464,6 +12494,14 @@ "node": ">=6" } }, + "node_modules/jsonata": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/jsonata/-/jsonata-2.0.6.tgz", + "integrity": "sha512-WhQB5tXQ32qjkx2GYHFw2XbL90u+LLzjofAYwi+86g6SyZeXHz9F1Q0amy3dWRYczshOC3Haok9J4pOCgHtwyQ==", + "engines": { + "node": ">= 8" + } + }, "node_modules/jsonpath-plus": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-9.0.0.tgz", @@ -13654,7 +13692,6 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true, "license": "MIT" }, "node_modules/netmask": { @@ -13876,6 +13913,15 @@ "node": ">=8" } }, + "node_modules/nightwatch/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/nightwatch/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -16819,6 +16865,15 @@ "node": ">=0.8.0" } }, + "node_modules/sockjs/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/socks": { "version": "2.8.3", "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", @@ -16859,7 +16914,6 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "devOptional": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -18106,13 +18160,15 @@ } }, "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, - "license": "MIT", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist/esm/bin/uuid" } }, "node_modules/v8flags": { @@ -18877,6 +18933,12 @@ "node": ">=0.10.0" } }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "license": "MIT" + }, "node_modules/worker-loader": { "version": "3.0.8", "resolved": "https://registry.npmjs.org/worker-loader/-/worker-loader-3.0.8.tgz", diff --git a/package.json b/package.json index b3492a8e..9191ab6f 100644 --- a/package.json +++ b/package.json @@ -132,6 +132,8 @@ "file-saver": "^2.0.5", "flat": "^6.0.1", "geodesy": "1.1.3", + "handlebars": "^4.7.8", + "hash-wasm": "^4.12.0", "highlight.js": "^11.9.0", "ieee754": "^1.2.1", "jimp": "^0.22.12", @@ -140,6 +142,7 @@ "js-sha3": "^0.9.3", "jsesc": "^3.0.2", "json5": "^2.2.3", + "jsonata": "^2.0.3", "jsonpath-plus": "^9.0.0", "jsonwebtoken": "8.5.1", "jsqr": "^1.4.0", @@ -180,6 +183,7 @@ "ua-parser-js": "^1.0.38", "unorm": "^1.6.0", "utf8": "^3.0.0", + "uuid": "^11.1.0", "vkbeautify": "^0.99.3", "xpath": "0.0.34", "xregexp": "^5.1.1", diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 29130a27..0f9d1e98 100644 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -294,6 +294,7 @@ "To Upper case", "To Lower case", "Swap case", + "Alternating Caps", "To Case Insensitive Regex", "From Case Insensitive Regex", "Add line numbers", @@ -369,11 +370,13 @@ "Regular expression", "XPath expression", "JPath expression", + "Jsonata Query", "CSS selector", "Extract EXIF", "Extract ID3", "Extract Files", - "RAKE" + "RAKE", + "Template" ] }, { @@ -404,6 +407,7 @@ "name": "Hashing", "ops": [ "Analyse hash", + "Generate all checksums", "Generate all hashes", "MD2", "MD4", @@ -422,6 +426,7 @@ "Snefru", "BLAKE2b", "BLAKE2s", + "BLAKE3", "GOST Hash", "Streebog", "SSDEEP", @@ -446,7 +451,8 @@ "Adler-32 Checksum", "Luhn Checksum", "CRC Checksum", - "TCP/IP Checksum" + "TCP/IP Checksum", + "XOR Checksum" ] }, { @@ -545,6 +551,7 @@ "Pseudo-Random Number Generator", "Generate De Bruijn Sequence", "Generate UUID", + "Analyse UUID", "Generate TOTP", "Generate HOTP", "Generate QR Code", diff --git a/src/core/operations/AlternatingCaps.mjs b/src/core/operations/AlternatingCaps.mjs new file mode 100644 index 00000000..2d54867c --- /dev/null +++ b/src/core/operations/AlternatingCaps.mjs @@ -0,0 +1,53 @@ +/** + * @author sw5678 + * @copyright Crown Copyright 2023 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; + +/** + * Alternating caps operation + */ +class AlternatingCaps extends Operation { + + /** + * AlternatingCaps constructor + */ + constructor() { + super(); + + this.name = "Alternating Caps"; + this.module = "Default"; + this.description = "Alternating caps, also known as studly caps, sticky caps, or spongecase is a form of text notation in which the capitalization of letters varies by some pattern, or arbitrarily. An example of this would be spelling 'alternative caps' as 'aLtErNaTiNg CaPs'."; + this.infoURL = "https://en.wikipedia.org/wiki/Alternating_caps"; + this.inputType = "string"; + this.outputType = "string"; + this.args= []; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + let output = ""; + let previousCaps = true; + for (let i = 0; i < input.length; i++) { + // Check if the element is a letter + if (!RegExp(/^\p{L}/, "u").test(input[i])) { + output += input[i]; + } else if (previousCaps) { + output += input[i].toLowerCase(); + previousCaps = false; + } else { + output += input[i].toUpperCase(); + previousCaps = true; + } + } + return output; + } +} + +export default AlternatingCaps; diff --git a/src/core/operations/AnalyseUUID.mjs b/src/core/operations/AnalyseUUID.mjs new file mode 100644 index 00000000..b3506017 --- /dev/null +++ b/src/core/operations/AnalyseUUID.mjs @@ -0,0 +1,48 @@ +/** + * @author n1474335 [n1474335@gmail.com] + * @copyright Crown Copyright 2023 + * @license Apache-2.0 + */ + +import * as uuid from "uuid"; + +import Operation from "../Operation.mjs"; +import OperationError from "../errors/OperationError.mjs"; + +/** + * Analyse UUID operation + */ +class AnalyseUUID extends Operation { + + /** + * AnalyseUUID constructor + */ + constructor() { + super(); + + this.name = "Analyse UUID"; + this.module = "Crypto"; + this.description = "Tries to determine information about a given UUID and suggests which version may have been used to generate it"; + this.infoURL = "https://wikipedia.org/wiki/Universally_unique_identifier"; + this.inputType = "string"; + this.outputType = "string"; + this.args = []; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + try { + const uuidVersion = uuid.version(input); + return "UUID version: " + uuidVersion; + } catch (error) { + throw new OperationError("Invalid UUID"); + } + } + +} + +export default AnalyseUUID; diff --git a/src/core/operations/BLAKE3.mjs b/src/core/operations/BLAKE3.mjs new file mode 100644 index 00000000..0f686120 --- /dev/null +++ b/src/core/operations/BLAKE3.mjs @@ -0,0 +1,58 @@ +/** + * @author xumptex [xumptex@outlook.fr] + * @copyright Crown Copyright 2025 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import OperationError from "../errors/OperationError.mjs"; +import { blake3 } from "hash-wasm"; +/** + * BLAKE3 operation + */ +class BLAKE3 extends Operation { + + /** + * BLAKE3 constructor + */ + constructor() { + super(); + + this.name = "BLAKE3"; + this.module = "Hashing"; + this.description = "Hashes the input using BLAKE3 (UTF-8 encoded), with an optional key (also UTF-8), and outputs the result in hexadecimal format."; + this.infoURL = "https://en.wikipedia.org/wiki/BLAKE_(hash_function)#BLAKE3"; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { + "name": "Size (bytes)", + "type": "number" + }, { + "name": "Key", + "type": "string", + "value": "" + } + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + const key = args[1]; + const size = args[0]; + // Check if the user want a key hash or not + if (key === "") { + return blake3(input, size*8); + } if (key.length !== 32) { + throw new OperationError("The key must be exactly 32 bytes long"); + } + return blake3(input, size*8, key); + } + +} + +export default BLAKE3; diff --git a/src/core/operations/ECDSAVerify.mjs b/src/core/operations/ECDSAVerify.mjs index 7e46e867..1f8a53ea 100644 --- a/src/core/operations/ECDSAVerify.mjs +++ b/src/core/operations/ECDSAVerify.mjs @@ -9,6 +9,7 @@ import OperationError from "../errors/OperationError.mjs"; import { fromBase64 } from "../lib/Base64.mjs"; import { toHexFast } from "../lib/Hex.mjs"; import r from "jsrsasign"; +import Utils from "../Utils.mjs"; /** * ECDSA Verify operation @@ -59,6 +60,11 @@ class ECDSAVerify extends Operation { name: "Message", type: "text", value: "" + }, + { + name: "Message format", + type: "option", + value: ["Raw", "Hex", "Base64"] } ]; } @@ -70,7 +76,7 @@ class ECDSAVerify extends Operation { */ run(input, args) { let inputFormat = args[0]; - const [, mdAlgo, keyPem, msg] = args; + const [, mdAlgo, keyPem, msg, msgFormat] = args; if (keyPem.replace("-----BEGIN PUBLIC KEY-----", "").length === 0) { throw new OperationError("Please enter a public key."); @@ -145,7 +151,8 @@ class ECDSAVerify extends Operation { throw new OperationError("Provided key is not a public key."); } sig.init(key); - sig.updateString(msg); + const messageStr = Utils.convertToByteString(msg, msgFormat); + sig.updateString(messageStr); const result = sig.verify(signatureASN1Hex); return result ? "Verified OK" : "Verification Failure"; } diff --git a/src/core/operations/ExtractEmailAddresses.mjs b/src/core/operations/ExtractEmailAddresses.mjs index f50e1aaf..34b838ab 100644 --- a/src/core/operations/ExtractEmailAddresses.mjs +++ b/src/core/operations/ExtractEmailAddresses.mjs @@ -51,7 +51,7 @@ class ExtractEmailAddresses extends Operation { run(input, args) { const [displayTotal, sort, unique] = args, // email regex from: https://www.regextester.com/98066 - regex = /(?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9](?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9-]*[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9])?\.)+[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9](?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9-]*[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}\])/ig; + regex = /(?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9](?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9-]*[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9])?\.)+[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9](?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9-]*[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\])/ig; const results = search( input, diff --git a/src/core/operations/ExtractIPAddresses.mjs b/src/core/operations/ExtractIPAddresses.mjs index 97b52478..b74ec8fe 100644 --- a/src/core/operations/ExtractIPAddresses.mjs +++ b/src/core/operations/ExtractIPAddresses.mjs @@ -21,7 +21,7 @@ class ExtractIPAddresses extends Operation { this.name = "Extract IP addresses"; this.module = "Regex"; - this.description = "Extracts all IPv4 and IPv6 addresses.

Warning: Given a string 710.65.0.456, this will match 10.65.0.45 so always check the original input!"; + this.description = "Extracts all IPv4 and IPv6 addresses.

Warning: Given a string 1.2.3.4.5.6.7.8, this will match 1.2.3.4 and 5.6.7.8 so always check the original input!"; this.inputType = "string"; this.outputType = "string"; this.args = [ @@ -65,7 +65,21 @@ class ExtractIPAddresses extends Operation { */ run(input, args) { const [includeIpv4, includeIpv6, removeLocal, displayTotal, sort, unique] = args, - ipv4 = "(?:(?:\\d|[01]?\\d\\d|2[0-4]\\d|25[0-5])\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?\\d\\d|\\d)(?:\\/\\d{1,2})?", + + // IPv4 decimal groups can have values 0 to 255. To construct a regex the following sub-regex is reused: + ipv4DecimalByte = "(?:25[0-5]|2[0-4]\\d|1?[0-9]\\d|\\d)", + ipv4OctalByte = "(?:0[1-3]?[0-7]{1,2})", + + // Look behind and ahead will be used to exclude matches with additional decimal digits left and right of IP address + lookBehind = "(? { + const checksumLength = checksum.name.match(new RegExp("-(\\d{1,2})(\\/|$)"))[1]; + if (length === "All" || length === checksumLength) { + const value = checksum.algo.run(new Uint8Array(input), checksum.params || []); + output += includeNames ? + `${checksum.name}:${" ".repeat(25-checksum.name.length)}${value}\n`: + `${value}\n`; + } + }); + return output; + } +} + +export default GenerateAllChecksums; diff --git a/src/core/operations/GenerateAllHashes.mjs b/src/core/operations/GenerateAllHashes.mjs index 06b5f7d9..df09aa85 100644 --- a/src/core/operations/GenerateAllHashes.mjs +++ b/src/core/operations/GenerateAllHashes.mjs @@ -22,12 +22,6 @@ import HAS160 from "./HAS160.mjs"; import Whirlpool from "./Whirlpool.mjs"; import SSDEEP from "./SSDEEP.mjs"; import CTPH from "./CTPH.mjs"; -import Fletcher8Checksum from "./Fletcher8Checksum.mjs"; -import Fletcher16Checksum from "./Fletcher16Checksum.mjs"; -import Fletcher32Checksum from "./Fletcher32Checksum.mjs"; -import Fletcher64Checksum from "./Fletcher64Checksum.mjs"; -import Adler32Checksum from "./Adler32Checksum.mjs"; -import CRCChecksum from "./CRCChecksum.mjs"; import BLAKE2b from "./BLAKE2b.mjs"; import BLAKE2s from "./BLAKE2s.mjs"; import Streebog from "./Streebog.mjs"; @@ -112,16 +106,6 @@ class GenerateAllHashes extends Operation { {name: "SSDEEP", algo: (new SSDEEP()), inputType: "str"}, {name: "CTPH", algo: (new CTPH()), inputType: "str"} ]; - this.checksums = [ - {name: "Fletcher-8", algo: (new Fletcher8Checksum), inputType: "byteArray", params: []}, - {name: "Fletcher-16", algo: (new Fletcher16Checksum), inputType: "byteArray", params: []}, - {name: "Fletcher-32", algo: (new Fletcher32Checksum), inputType: "byteArray", params: []}, - {name: "Fletcher-64", algo: (new Fletcher64Checksum), inputType: "byteArray", params: []}, - {name: "Adler-32", algo: (new Adler32Checksum), inputType: "byteArray", params: []}, - {name: "CRC-8", algo: (new CRCChecksum), inputType: "arrayBuffer", params: ["CRC-8"]}, - {name: "CRC-16", algo: (new CRCChecksum), inputType: "arrayBuffer", params: ["CRC-16"]}, - {name: "CRC-32", algo: (new CRCChecksum), inputType: "arrayBuffer", params: ["CRC-32"]} - ]; } /** @@ -142,14 +126,6 @@ class GenerateAllHashes extends Operation { output += this.formatDigest(digest, length, includeNames, hash.name); }); - if (length === "All") { - output += "\nChecksums:\n"; - this.checksums.forEach(checksum => { - digest = this.executeAlgo(checksum.algo, checksum.inputType, checksum.params || []); - output += this.formatDigest(digest, length, includeNames, checksum.name); - }); - } - return output; } diff --git a/src/core/operations/GenerateUUID.mjs b/src/core/operations/GenerateUUID.mjs index 1ee0faba..21d063e3 100644 --- a/src/core/operations/GenerateUUID.mjs +++ b/src/core/operations/GenerateUUID.mjs @@ -5,8 +5,8 @@ */ import Operation from "../Operation.mjs"; -import crypto from "crypto"; - +import * as uuid from "uuid"; +import OperationError from "../errors/OperationError.mjs"; /** * Generate UUID operation */ @@ -20,11 +20,38 @@ class GenerateUUID extends Operation { this.name = "Generate UUID"; this.module = "Crypto"; - this.description = "Generates an RFC 4122 version 4 compliant Universally Unique Identifier (UUID), also known as a Globally Unique Identifier (GUID).

A version 4 UUID relies on random numbers, in this case generated using window.crypto if available and falling back to Math.random if not."; + this.description = + "Generates an RFC 9562 (formerly RFC 4122) compliant Universally Unique Identifier (UUID), " + + "also known as a Globally Unique Identifier (GUID).
" + + "
" + + "We currently support generating the following UUID versions:
" + + "" + + "UUIDs are generated using the uuid package.
"; this.infoURL = "https://wikipedia.org/wiki/Universally_unique_identifier"; this.inputType = "string"; this.outputType = "string"; - this.args = []; + this.args = [ + { + name: "Version", + hint: "UUID version", + type: "option", + value: ["v1", "v3", "v4", "v5", "v6", "v7"], + defaultIndex: 2, + }, + { + name: "Namespace", + hint: "UUID namespace (UUID; valid for v3 and v5)", + type: "string", + value: "1b671a64-40d5-491e-99b0-da01ff1f3341" + } + ]; } /** @@ -33,16 +60,17 @@ class GenerateUUID extends Operation { * @returns {string} */ run(input, args) { - const buf = new Uint32Array(4).map(() => { - return crypto.randomBytes(4).readUInt32BE(0, true); - }); - let i = 0; - return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) { - const r = (buf[i >> 3] >> ((i % 8) * 4)) & 0xf, - v = c === "x" ? r : (r & 0x3 | 0x8); - i++; - return v.toString(16); - }); + const [version, namespace] = args; + const hasDesiredVersion = typeof uuid[version] === "function"; + if (!hasDesiredVersion) throw new OperationError("Invalid UUID version"); + + const requiresNamespace = ["v3", "v5"].includes(version); + if (!requiresNamespace) return uuid[version](); + + const hasValidNamespace = typeof namespace === "string" && uuid.validate(namespace); + if (!hasValidNamespace) throw new OperationError("Invalid UUID namespace"); + + return uuid[version](input, namespace); } } diff --git a/src/core/operations/Jsonata.mjs b/src/core/operations/Jsonata.mjs new file mode 100644 index 00000000..82cc4d39 --- /dev/null +++ b/src/core/operations/Jsonata.mjs @@ -0,0 +1,65 @@ +/** + * @author Jon K (jon@ajarsoftware.com) + * @copyright Crown Copyright 2016 + * @license Apache-2.0 + */ + +import jsonata from "jsonata"; +import Operation from "../Operation.mjs"; +import OperationError from "../errors/OperationError.mjs"; + +/** + * Jsonata Query operation + */ +class JsonataQuery extends Operation { + /** + * JsonataQuery constructor + */ + constructor() { + super(); + + this.name = "Jsonata Query"; + this.module = "Code"; + this.description = + "Query and transform JSON data with a jsonata query."; + this.infoURL = "https://docs.jsonata.org/overview.html"; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { + name: "Query", + type: "text", + value: "string", + }, + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + async run(input, args) { + const [query] = args; + let result, jsonObj; + + try { + jsonObj = JSON.parse(input); + } catch (err) { + throw new OperationError(`Invalid input JSON: ${err.message}`); + } + + try { + const expression = jsonata(query); + result = await expression.evaluate(jsonObj); + } catch (err) { + throw new OperationError( + `Invalid Jsonata Expression: ${err.message}` + ); + } + + return JSON.stringify(result === undefined ? "" : result); + } +} + +export default JsonataQuery; diff --git a/src/core/operations/RailFenceCipherDecode.mjs b/src/core/operations/RailFenceCipherDecode.mjs index be54ee12..39795f21 100644 --- a/src/core/operations/RailFenceCipherDecode.mjs +++ b/src/core/operations/RailFenceCipherDecode.mjs @@ -72,7 +72,7 @@ class RailFenceCipherDecode extends Operation { } } - return plaintext.join("").trim(); + return plaintext.join(""); } } diff --git a/src/core/operations/RailFenceCipherEncode.mjs b/src/core/operations/RailFenceCipherEncode.mjs index 03651f85..89eddde7 100644 --- a/src/core/operations/RailFenceCipherEncode.mjs +++ b/src/core/operations/RailFenceCipherEncode.mjs @@ -66,7 +66,7 @@ class RailFenceCipherEncode extends Operation { rows[rowIdx] += plaintext[pos]; } - return rows.join("").trim(); + return rows.join(""); } } diff --git a/src/core/operations/ShowOnMap.mjs b/src/core/operations/ShowOnMap.mjs index c2ac1c6e..d75c2aa6 100644 --- a/src/core/operations/ShowOnMap.mjs +++ b/src/core/operations/ShowOnMap.mjs @@ -1,6 +1,7 @@ /** * @author j433866 [j433866@gmail.com] - * @copyright Crown Copyright 2019 + * @author 0xff1ce [github.com/0xff1ce] + * @copyright Crown Copyright 2024 * @license Apache-2.0 */ @@ -22,7 +23,7 @@ class ShowOnMap extends Operation { this.name = "Show on map"; this.module = "Hashing"; this.description = "Displays co-ordinates on a slippy map.

Co-ordinates will be converted to decimal degrees before being shown on the map.

Supported formats:
This operation will not work offline."; - this.infoURL = "https://foundation.wikimedia.org/wiki/Maps_Terms_of_Use"; + this.infoURL = "https://osmfoundation.org/wiki/Terms_of_Use"; this.inputType = "string"; this.outputType = "string"; this.presentType = "html"; @@ -85,10 +86,10 @@ class ShowOnMap extends Operation { data = "0, 0"; } const zoomLevel = args[0]; - const tileUrl = "https://maps.wikimedia.org/osm-intl/{z}/{x}/{y}.png", - tileAttribution = "
Wikimedia maps | © OpenStreetMap contributors", - leafletUrl = "https://unpkg.com/leaflet@1.5.0/dist/leaflet.js", - leafletCssUrl = "https://unpkg.com/leaflet@1.5.0/dist/leaflet.css"; + const tileUrl = "https://tile.openstreetmap.org/{z}/{x}/{y}.png", + tileAttribution = "© OpenStreetMap contributors", + leafletUrl = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.js", + leafletCssUrl = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"; return `