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.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 = "(? option !== "").map(option => COLOUR_OPTIONS.indexOf(option)),
- parsedImage = await jimp.read(input),
+ 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 7d2fc274..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";
+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);
+ 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/FangURL.mjs b/src/core/operations/FangURL.mjs
new file mode 100644
index 00000000..ad5bf525
--- /dev/null
+++ b/src/core/operations/FangURL.mjs
@@ -0,0 +1,78 @@
+/**
+ * @author arnydo [github@arnydo.com]
+ * @copyright Crown Copyright 2019
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+
+/**
+ * FangURL operation
+ */
+class FangURL extends Operation {
+
+ /**
+ * FangURL constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "Fang URL";
+ this.module = "Default";
+ this.description = "Takes a 'Defanged' Universal Resource Locator (URL) and 'Fangs' it. Meaning, it removes the alterations (defanged) that render it useless so that it can be used again.";
+ this.infoURL = "https://isc.sans.edu/forums/diary/Defang+all+the+things/22744/";
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [
+ {
+ name: "Restore [.]",
+ type: "boolean",
+ value: true
+ },
+ {
+ name: "Restore hxxp",
+ type: "boolean",
+ value: true
+ },
+ {
+ name: "Restore ://",
+ type: "boolean",
+ value: true
+ }
+ ];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ const [dots, http, slashes] = args;
+
+ input = fangURL(input, dots, http, slashes);
+
+ return input;
+ }
+
+}
+
+
+/**
+ * Defangs a given URL
+ *
+ * @param {string} url
+ * @param {boolean} dots
+ * @param {boolean} http
+ * @param {boolean} slashes
+ * @returns {string}
+ */
+function fangURL(url, dots, http, slashes) {
+ if (dots) url = url.replace(/\[\.\]/g, ".");
+ if (http) url = url.replace(/hxxp/g, "http");
+ if (slashes) url = url.replace(/\[:\/\/\]/g, "://");
+
+ return url;
+}
+
+export default FangURL;
diff --git a/src/core/operations/FileTree.mjs b/src/core/operations/FileTree.mjs
index 8321f8f5..9484313f 100644
--- a/src/core/operations/FileTree.mjs
+++ b/src/core/operations/FileTree.mjs
@@ -1,6 +1,6 @@
/**
* @author sw5678
- * @copyright Crown Copyright 2016
+ * @copyright Crown Copyright 2023
* @license Apache-2.0
*/
@@ -21,7 +21,8 @@ class FileTree extends Operation {
this.name = "File Tree";
this.module = "Default";
- this.description = "Creates file tree from list of file paths (similar to the tree command in Linux)";
+ this.description = "Creates a file tree from a list of file paths (similar to the tree command in Linux)";
+ this.infoURL = "https://wikipedia.org/wiki/Tree_(command)";
this.inputType = "string";
this.outputType = "string";
this.args = [
diff --git a/src/core/operations/FlaskSessionDecode.mjs b/src/core/operations/FlaskSessionDecode.mjs
new file mode 100644
index 00000000..5486357e
--- /dev/null
+++ b/src/core/operations/FlaskSessionDecode.mjs
@@ -0,0 +1,80 @@
+/**
+ * @author ThePlayer372-FR []
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+import OperationError from "../errors/OperationError.mjs";
+import { fromBase64 } from "../lib/Base64.mjs";
+
+/**
+ * Flask Session Decode operation
+ */
+class FlaskSessionDecode extends Operation {
+ /**
+ * FlaskSessionDecode constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "Flask Session Decode";
+ this.module = "Crypto";
+ this.description = "Decodes the payload of a Flask session cookie (itsdangerous) into JSON.";
+ this.inputType = "string";
+ this.outputType = "JSON";
+ this.args = [
+ {
+ name: "View TimeStamp",
+ type: "boolean",
+ value: false
+ }
+ ];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {Object[]}
+ */
+ run(input, args) {
+ input = input.trim();
+ const parts = input.split(".");
+ if (parts.length !== 3) {
+ throw new OperationError("Invalid Flask token format. Expected payload.timestamp.signature");
+ }
+
+ const payloadB64 = parts[0];
+ const time = parts[1];
+
+ const timeB64 = time.replace(/-/g, "+").replace(/_/g, "/");
+ const binary = fromBase64(timeB64);
+ const bytes = new Uint8Array(4);
+ for (let i = 0; i < 4; i++) {
+ bytes[i] = binary.charCodeAt(i);
+ }
+ const view = new DataView(bytes.buffer);
+ const timestamp = view.getInt32(0, false);
+
+ const base64 = payloadB64.replace(/-/g, "+").replace(/_/g, "/");
+ const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
+ let payloadJson;
+ try {
+ payloadJson = fromBase64(padded);
+ } catch (e) {
+ throw new OperationError("Invalid Base64 payload");
+ }
+
+ try {
+ let data = JSON.parse(payloadJson);
+
+ if (args[0]) {
+ data = {payload: data, timestamp: timestamp};
+ }
+ return data;
+ } catch (e) {
+ throw new OperationError("Unable to decode JSON payload: " + e.message);
+ }
+ }
+}
+
+export default FlaskSessionDecode;
diff --git a/src/core/operations/FlaskSessionSign.mjs b/src/core/operations/FlaskSessionSign.mjs
new file mode 100644
index 00000000..01ee8b1d
--- /dev/null
+++ b/src/core/operations/FlaskSessionSign.mjs
@@ -0,0 +1,89 @@
+/**
+ * @author ThePlayer372-FR []
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+import CryptoApi from "crypto-api/src/crypto-api.mjs";
+import Utils from "../Utils.mjs";
+import { toBase64 } from "../lib/Base64.mjs";
+import OperationError from "../errors/OperationError.mjs";
+
+/**
+ * Flask Session Sign operation
+ */
+class FlaskSessionSign extends Operation {
+ /**
+ * FlaskSessionSign constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "Flask Session Sign";
+ this.module = "Crypto";
+ this.description = "Signs a JSON payload to produce a Flask session cookie (itsdangerous HMAC).";
+ this.inputType = "JSON";
+ this.outputType = "string";
+ this.args = [
+ {
+ name: "Key",
+ type: "toggleString",
+ value: "",
+ toggleValues: ["Hex", "Decimal", "Binary", "Base64", "UTF8", "Latin1"]
+ },
+ {
+ name: "Salt",
+ type: "toggleString",
+ value: "cookie-session",
+ toggleValues: ["UTF8", "Hex", "Decimal", "Binary", "Base64", "Latin1"]
+ },
+ {
+ name: "Algorithm",
+ type: "option",
+ value: ["sha1", "sha256"],
+ }
+ ];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ if (!args[0].string) {
+ throw new OperationError("Secret key required");
+ }
+ const key = Utils.convertToByteString(args[0].string, args[0].option);
+ const salt = Utils.convertToByteString(args[1].string || "cookie-session", args[1].option);
+ const algorithm = args[2] || "sha1";
+
+ const payloadB64 = toBase64(Utils.strToByteArray(JSON.stringify(input)));
+ const payload = payloadB64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
+
+ const derivedKey = CryptoApi.getHmac(key, CryptoApi.getHasher(algorithm));
+ derivedKey.update(salt);
+
+ const currentTimeStamp = Math.ceil(Date.now() / 1000);
+ const buffer = new ArrayBuffer(4);
+ const view = new DataView(buffer);
+ view.setInt32(0, currentTimeStamp, false);
+ const bytes = new Uint8Array(buffer);
+ let binary = "";
+ bytes.forEach(b => binary += String.fromCharCode(b));
+ const timeB64 = toBase64(Utils.strToByteArray(binary));
+ const time = timeB64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
+
+ const data = Utils.convertToByteString(payload + "." + time, "utf8");
+ const sign = CryptoApi.getHmac(derivedKey.finalize(), CryptoApi.getHasher(algorithm));
+ sign.update(data);
+
+ const signB64 = toBase64(sign.finalize());
+ const sign64 = signB64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
+
+ return payload + "." + time + "." + sign64;
+ }
+}
+
+
+export default FlaskSessionSign;
diff --git a/src/core/operations/FlaskSessionVerify.mjs b/src/core/operations/FlaskSessionVerify.mjs
new file mode 100644
index 00000000..7603ba1f
--- /dev/null
+++ b/src/core/operations/FlaskSessionVerify.mjs
@@ -0,0 +1,136 @@
+/**
+ * @author ThePlayer372-FR []
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+import OperationError from "../errors/OperationError.mjs";
+import CryptoApi from "crypto-api/src/crypto-api.mjs";
+import Utils from "../Utils.mjs";
+import { toBase64, fromBase64 } from "../lib/Base64.mjs";
+
+/**
+ * Flask Session Verify operation
+ */
+class FlaskSessionVerify extends Operation {
+ /**
+ * FlaskSessionVerify constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "Flask Session Verify";
+ this.module = "Crypto";
+ this.description = "Verifies the HMAC signature of a Flask session cookie (itsdangerous) generated.";
+ this.inputType = "string";
+ this.outputType = "JSON";
+ this.args = [
+ {
+ name: "Key",
+ type: "toggleString",
+ value: "",
+ toggleValues: ["Hex", "Decimal", "Binary", "Base64", "UTF8", "Latin1"]
+ },
+ {
+ name: "Salt",
+ type: "toggleString",
+ value: "cookie-session",
+ toggleValues: ["UTF8", "Hex", "Decimal", "Binary", "Base64", "Latin1"]
+ },
+ {
+ name: "Algorithm",
+ type: "option",
+ value: ["sha1", "sha256"],
+ },
+ {
+ name: "View TimeStamp",
+ type: "boolean",
+ value: true
+ }
+ ];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+
+ if (!args[0].string) {
+ throw new OperationError("Secret key required");
+ }
+
+ const key = Utils.convertToByteString(args[0].string, args[0].option);
+ const salt = Utils.convertToByteString(args[1].string || "cookie-session", args[1].option);
+ const algorithm = args[2] || "sha1";
+
+ input = input.trim();
+
+ const parts = input.split(".");
+
+ if (parts.length !== 3) {
+ throw new OperationError("Invalid Flask token format. Expected payload.timestamp.signature");
+ }
+
+ const data = Utils.convertToByteString(parts[0] + "." + parts[1], "utf8");
+
+
+ const derivedKey = CryptoApi.getHmac(key, CryptoApi.getHasher(algorithm));
+ derivedKey.update(salt);
+
+ const sign = CryptoApi.getHmac(derivedKey.finalize(), CryptoApi.getHasher(algorithm));
+ sign.update(data);
+
+ const payloadB64 = parts[0];
+ const base64 = payloadB64.replace(/-/g, "+").replace(/_/g, "/");
+ const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
+
+ const time = parts[1];
+
+ const timeB64 = time.replace(/-/g, "+").replace(/_/g, "/");
+ const binary = fromBase64(timeB64);
+ const bytes = new Uint8Array(4);
+ for (let i = 0; i < 4; i++) {
+ bytes[i] = binary.charCodeAt(i);
+ }
+ const view = new DataView(bytes.buffer);
+ const timestamp = view.getInt32(0, false);
+
+ let payloadJson;
+ try {
+ payloadJson = fromBase64(padded);
+ } catch (e) {
+ throw new OperationError("Invalid Base64 payload");
+ }
+
+ const signB64 = toBase64(sign.finalize());
+ const sign64 = signB64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
+
+ if (sign64 !== parts[2]) {
+ throw new OperationError("Invalid signature!");
+ }
+
+ try {
+ const decoded = JSON.parse(payloadJson);
+ if (!args[3]) {
+ return {
+ valid: true,
+ payload: decoded,
+ };
+ } else {
+ return {
+ valid: true,
+ payload: decoded,
+ timestamp: timestamp
+ };
+ }
+ } catch (e) {
+ throw new OperationError("Unable to decode JSON payload: " + e.message);
+ }
+
+ }
+}
+
+
+export default FlaskSessionVerify;
diff --git a/src/core/operations/FlipImage.mjs b/src/core/operations/FlipImage.mjs
index 30be5a4e..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";
+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"],
+ },
];
}
@@ -51,7 +50,7 @@ class FlipImage extends Operation {
let image;
try {
- image = await jimp.read(input);
+ image = await Jimp.read(input);
} catch (err) {
throw new OperationError(`Error loading image. (${err})`);
}
@@ -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 `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.hello=20world becomes hello world";
this.infoURL = "https://wikipedia.org/wiki/Quoted-printable";
this.inputType = "string";
this.outputType = "byteArray";
diff --git a/src/core/operations/GOSTDecrypt.mjs b/src/core/operations/GOSTDecrypt.mjs
index 8259a0d4..2e7467da 100644
--- a/src/core/operations/GOSTDecrypt.mjs
+++ b/src/core/operations/GOSTDecrypt.mjs
@@ -55,22 +55,19 @@ class GOSTDecrypt extends Operation {
type: "argSelector",
value: [
{
- name: "GOST 28147 (Magma, 1989)",
- off: [5],
- on: [6]
+ name: "GOST 28147 (1989)",
+ on: [5]
+ },
+ {
+ name: "GOST R 34.12 (Magma, 2015)",
+ off: [5]
},
{
name: "GOST R 34.12 (Kuznyechik, 2015)",
- on: [5],
- off: [6]
+ off: [5]
}
]
},
- {
- name: "Block length",
- type: "option",
- value: ["64", "128"]
- },
{
name: "sBox",
type: "option",
@@ -100,14 +97,30 @@ class GOSTDecrypt extends Operation {
* @returns {string}
*/
async run(input, args) {
- const [keyObj, ivObj, inputType, outputType, version, length, sBox, blockMode, keyMeshing, padding] = args;
+ const [keyObj, ivObj, inputType, outputType, version, sBox, blockMode, keyMeshing, padding] = args;
const key = toHexFast(Utils.convertToByteArray(keyObj.string, keyObj.option));
const iv = toHexFast(Utils.convertToByteArray(ivObj.string, ivObj.option));
input = inputType === "Hex" ? input : toHexFast(Utils.strToArrayBuffer(input));
- const versionNum = version === "GOST 28147 (Magma, 1989)" ? 1989 : 2015;
- const blockLength = versionNum === 1989 ? 64 : parseInt(length, 10);
+ let blockLength, versionNum;
+ switch (version) {
+ case "GOST 28147 (1989)":
+ versionNum = 1989;
+ blockLength = 64;
+ break;
+ case "GOST R 34.12 (Magma, 2015)":
+ versionNum = 2015;
+ blockLength = 64;
+ break;
+ case "GOST R 34.12 (Kuznyechik, 2015)":
+ versionNum = 2015;
+ blockLength = 128;
+ break;
+ default:
+ throw new OperationError(`Unknown algorithm version: ${version}`);
+ }
+
const sBoxVal = versionNum === 1989 ? sBox : null;
const algorithm = {
diff --git a/src/core/operations/GOSTEncrypt.mjs b/src/core/operations/GOSTEncrypt.mjs
index ce92ecda..82bdaeda 100644
--- a/src/core/operations/GOSTEncrypt.mjs
+++ b/src/core/operations/GOSTEncrypt.mjs
@@ -55,22 +55,19 @@ class GOSTEncrypt extends Operation {
type: "argSelector",
value: [
{
- name: "GOST 28147 (Magma, 1989)",
- off: [5],
- on: [6]
+ name: "GOST 28147 (1989)",
+ on: [5]
+ },
+ {
+ name: "GOST R 34.12 (Magma, 2015)",
+ off: [5]
},
{
name: "GOST R 34.12 (Kuznyechik, 2015)",
- on: [5],
- off: [6]
+ off: [5]
}
]
},
- {
- name: "Block length",
- type: "option",
- value: ["64", "128"]
- },
{
name: "sBox",
type: "option",
@@ -100,14 +97,30 @@ class GOSTEncrypt extends Operation {
* @returns {string}
*/
async run(input, args) {
- const [keyObj, ivObj, inputType, outputType, version, length, sBox, blockMode, keyMeshing, padding] = args;
+ const [keyObj, ivObj, inputType, outputType, version, sBox, blockMode, keyMeshing, padding] = args;
const key = toHexFast(Utils.convertToByteArray(keyObj.string, keyObj.option));
const iv = toHexFast(Utils.convertToByteArray(ivObj.string, ivObj.option));
input = inputType === "Hex" ? input : toHexFast(Utils.strToArrayBuffer(input));
- const versionNum = version === "GOST 28147 (Magma, 1989)" ? 1989 : 2015;
- const blockLength = versionNum === 1989 ? 64 : parseInt(length, 10);
+ let blockLength, versionNum;
+ switch (version) {
+ case "GOST 28147 (1989)":
+ versionNum = 1989;
+ blockLength = 64;
+ break;
+ case "GOST R 34.12 (Magma, 2015)":
+ versionNum = 2015;
+ blockLength = 64;
+ break;
+ case "GOST R 34.12 (Kuznyechik, 2015)":
+ versionNum = 2015;
+ blockLength = 128;
+ break;
+ default:
+ throw new OperationError(`Unknown algorithm version: ${version}`);
+ }
+
const sBoxVal = versionNum === 1989 ? sBox : null;
const algorithm = {
diff --git a/src/core/operations/GOSTKeyUnwrap.mjs b/src/core/operations/GOSTKeyUnwrap.mjs
index afcd6287..52b0a9ec 100644
--- a/src/core/operations/GOSTKeyUnwrap.mjs
+++ b/src/core/operations/GOSTKeyUnwrap.mjs
@@ -55,22 +55,19 @@ class GOSTKeyUnwrap extends Operation {
type: "argSelector",
value: [
{
- name: "GOST 28147 (Magma, 1989)",
- off: [5],
- on: [6]
+ name: "GOST 28147 (1989)",
+ on: [5]
+ },
+ {
+ name: "GOST R 34.12 (Magma, 2015)",
+ off: [5]
},
{
name: "GOST R 34.12 (Kuznyechik, 2015)",
- on: [5],
- off: [6]
+ off: [5]
}
]
},
- {
- name: "Block length",
- type: "option",
- value: ["64", "128"]
- },
{
name: "sBox",
type: "option",
@@ -90,14 +87,30 @@ class GOSTKeyUnwrap extends Operation {
* @returns {string}
*/
async run(input, args) {
- const [keyObj, ukmObj, inputType, outputType, version, length, sBox, keyWrapping] = args;
+ const [keyObj, ukmObj, inputType, outputType, version, sBox, keyWrapping] = args;
const key = toHexFast(Utils.convertToByteArray(keyObj.string, keyObj.option));
const ukm = toHexFast(Utils.convertToByteArray(ukmObj.string, ukmObj.option));
input = inputType === "Hex" ? input : toHexFast(Utils.strToArrayBuffer(input));
- const versionNum = version === "GOST 28147 (Magma, 1989)" ? 1989 : 2015;
- const blockLength = versionNum === 1989 ? 64 : parseInt(length, 10);
+ let blockLength, versionNum;
+ switch (version) {
+ case "GOST 28147 (1989)":
+ versionNum = 1989;
+ blockLength = 64;
+ break;
+ case "GOST R 34.12 (Magma, 2015)":
+ versionNum = 2015;
+ blockLength = 64;
+ break;
+ case "GOST R 34.12 (Kuznyechik, 2015)":
+ versionNum = 2015;
+ blockLength = 128;
+ break;
+ default:
+ throw new OperationError(`Unknown algorithm version: ${version}`);
+ }
+
const sBoxVal = versionNum === 1989 ? sBox : null;
const algorithm = {
diff --git a/src/core/operations/GOSTKeyWrap.mjs b/src/core/operations/GOSTKeyWrap.mjs
index 5a3fd4e6..443c9ba0 100644
--- a/src/core/operations/GOSTKeyWrap.mjs
+++ b/src/core/operations/GOSTKeyWrap.mjs
@@ -55,22 +55,19 @@ class GOSTKeyWrap extends Operation {
type: "argSelector",
value: [
{
- name: "GOST 28147 (Magma, 1989)",
- off: [5],
- on: [6]
+ name: "GOST 28147 (1989)",
+ on: [5]
+ },
+ {
+ name: "GOST R 34.12 (Magma, 2015)",
+ off: [5]
},
{
name: "GOST R 34.12 (Kuznyechik, 2015)",
- on: [5],
- off: [6]
+ off: [5]
}
]
},
- {
- name: "Block length",
- type: "option",
- value: ["64", "128"]
- },
{
name: "sBox",
type: "option",
@@ -90,14 +87,30 @@ class GOSTKeyWrap extends Operation {
* @returns {string}
*/
async run(input, args) {
- const [keyObj, ukmObj, inputType, outputType, version, length, sBox, keyWrapping] = args;
+ const [keyObj, ukmObj, inputType, outputType, version, sBox, keyWrapping] = args;
const key = toHexFast(Utils.convertToByteArray(keyObj.string, keyObj.option));
const ukm = toHexFast(Utils.convertToByteArray(ukmObj.string, ukmObj.option));
input = inputType === "Hex" ? input : toHexFast(Utils.strToArrayBuffer(input));
- const versionNum = version === "GOST 28147 (Magma, 1989)" ? 1989 : 2015;
- const blockLength = versionNum === 1989 ? 64 : parseInt(length, 10);
+ let blockLength, versionNum;
+ switch (version) {
+ case "GOST 28147 (1989)":
+ versionNum = 1989;
+ blockLength = 64;
+ break;
+ case "GOST R 34.12 (Magma, 2015)":
+ versionNum = 2015;
+ blockLength = 64;
+ break;
+ case "GOST R 34.12 (Kuznyechik, 2015)":
+ versionNum = 2015;
+ blockLength = 128;
+ break;
+ default:
+ throw new OperationError(`Unknown algorithm version: ${version}`);
+ }
+
const sBoxVal = versionNum === 1989 ? sBox : null;
const algorithm = {
diff --git a/src/core/operations/GOSTSign.mjs b/src/core/operations/GOSTSign.mjs
index 9195f469..2af850e7 100644
--- a/src/core/operations/GOSTSign.mjs
+++ b/src/core/operations/GOSTSign.mjs
@@ -55,22 +55,19 @@ class GOSTSign extends Operation {
type: "argSelector",
value: [
{
- name: "GOST 28147 (Magma, 1989)",
- off: [5],
- on: [6]
+ name: "GOST 28147 (1989)",
+ on: [5]
+ },
+ {
+ name: "GOST R 34.12 (Magma, 2015)",
+ off: [5]
},
{
name: "GOST R 34.12 (Kuznyechik, 2015)",
- on: [5],
- off: [6]
+ off: [5]
}
]
},
- {
- name: "Block length",
- type: "option",
- value: ["64", "128"]
- },
{
name: "sBox",
type: "option",
@@ -93,14 +90,30 @@ class GOSTSign extends Operation {
* @returns {string}
*/
async run(input, args) {
- const [keyObj, ivObj, inputType, outputType, version, length, sBox, macLength] = args;
+ const [keyObj, ivObj, inputType, outputType, version, sBox, macLength] = args;
const key = toHexFast(Utils.convertToByteArray(keyObj.string, keyObj.option));
const iv = toHexFast(Utils.convertToByteArray(ivObj.string, ivObj.option));
input = inputType === "Hex" ? input : toHexFast(Utils.strToArrayBuffer(input));
- const versionNum = version === "GOST 28147 (Magma, 1989)" ? 1989 : 2015;
- const blockLength = versionNum === 1989 ? 64 : parseInt(length, 10);
+ let blockLength, versionNum;
+ switch (version) {
+ case "GOST 28147 (1989)":
+ versionNum = 1989;
+ blockLength = 64;
+ break;
+ case "GOST R 34.12 (Magma, 2015)":
+ versionNum = 2015;
+ blockLength = 64;
+ break;
+ case "GOST R 34.12 (Kuznyechik, 2015)":
+ versionNum = 2015;
+ blockLength = 128;
+ break;
+ default:
+ throw new OperationError(`Unknown algorithm version: ${version}`);
+ }
+
const sBoxVal = versionNum === 1989 ? sBox : null;
const algorithm = {
diff --git a/src/core/operations/GOSTVerify.mjs b/src/core/operations/GOSTVerify.mjs
index a270e7c5..2fa362fb 100644
--- a/src/core/operations/GOSTVerify.mjs
+++ b/src/core/operations/GOSTVerify.mjs
@@ -56,22 +56,19 @@ class GOSTVerify extends Operation {
type: "argSelector",
value: [
{
- name: "GOST 28147 (Magma, 1989)",
- off: [5],
- on: [6]
+ name: "GOST 28147 (1989)",
+ on: [5]
+ },
+ {
+ name: "GOST R 34.12 (Magma, 2015)",
+ off: [5]
},
{
name: "GOST R 34.12 (Kuznyechik, 2015)",
- on: [5],
- off: [6]
+ off: [5]
}
]
},
- {
- name: "Block length",
- type: "option",
- value: ["64", "128"]
- },
{
name: "sBox",
type: "option",
@@ -86,15 +83,31 @@ class GOSTVerify extends Operation {
* @returns {string}
*/
async run(input, args) {
- const [keyObj, ivObj, macObj, inputType, version, length, sBox] = args;
+ const [keyObj, ivObj, macObj, inputType, version, sBox] = args;
const key = toHexFast(Utils.convertToByteArray(keyObj.string, keyObj.option));
const iv = toHexFast(Utils.convertToByteArray(ivObj.string, ivObj.option));
const mac = toHexFast(Utils.convertToByteArray(macObj.string, macObj.option));
input = inputType === "Hex" ? input : toHexFast(Utils.strToArrayBuffer(input));
- const versionNum = version === "GOST 28147 (Magma, 1989)" ? 1989 : 2015;
- const blockLength = versionNum === 1989 ? 64 : parseInt(length, 10);
+ let blockLength, versionNum;
+ switch (version) {
+ case "GOST 28147 (1989)":
+ versionNum = 1989;
+ blockLength = 64;
+ break;
+ case "GOST R 34.12 (Magma, 2015)":
+ versionNum = 2015;
+ blockLength = 64;
+ break;
+ case "GOST R 34.12 (Kuznyechik, 2015)":
+ versionNum = 2015;
+ blockLength = 128;
+ break;
+ default:
+ throw new OperationError(`Unknown algorithm version: ${version}`);
+ }
+
const sBoxVal = versionNum === 1989 ? sBox : null;
const algorithm = {
diff --git a/src/core/operations/GenerateAllChecksums.mjs b/src/core/operations/GenerateAllChecksums.mjs
new file mode 100644
index 00000000..b5a3d152
--- /dev/null
+++ b/src/core/operations/GenerateAllChecksums.mjs
@@ -0,0 +1,254 @@
+/**
+ * @author r4mos [2k95ljkhg@mozmail.com]
+ * @copyright Crown Copyright 2025
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+import Adler32Checksum from "./Adler32Checksum.mjs";
+import CRCChecksum from "./CRCChecksum.mjs";
+import Fletcher8Checksum from "./Fletcher8Checksum.mjs";
+import Fletcher16Checksum from "./Fletcher16Checksum.mjs";
+import Fletcher32Checksum from "./Fletcher32Checksum.mjs";
+import Fletcher64Checksum from "./Fletcher64Checksum.mjs";
+
+/**
+ * Generate all checksums operation
+ */
+class GenerateAllChecksums extends Operation {
+
+ /**
+ * GenerateAllChecksums constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "Generate all checksums";
+ this.module = "Crypto";
+ this.description = "Generates all available checksums for the input.";
+ this.infoURL = "https://wikipedia.org/wiki/Checksum";
+ this.inputType = "ArrayBuffer";
+ this.outputType = "string";
+ this.args = [
+ {
+ name: "Length (bits)",
+ type: "option",
+ value: [
+ "All", "3", "4", "5", "6", "7", "8", "10", "11", "12", "13", "14", "15", "16", "17", "21", "24", "30", "31", "32", "40", "64", "82"
+ ]
+ },
+ {
+ name: "Include names",
+ type: "boolean",
+ value: true
+ },
+ ];
+
+ const adler32 = new Adler32Checksum;
+ const crc = new CRCChecksum;
+ const fletcher8 = new Fletcher8Checksum;
+ const fletcher16 = new Fletcher16Checksum;
+ const fletcher32 = new Fletcher32Checksum;
+ const fletcher64 = new Fletcher64Checksum;
+ this.checksums = [
+ {name: "CRC-3/GSM", algo: crc, params: ["CRC-3/GSM"]},
+ {name: "CRC-3/ROHC", algo: crc, params: ["CRC-3/ROHC"]},
+ {name: "CRC-4/G-704", algo: crc, params: ["CRC-4/G-704"]},
+ {name: "CRC-4/INTERLAKEN", algo: crc, params: ["CRC-4/INTERLAKEN"]},
+ {name: "CRC-4/ITU", algo: crc, params: ["CRC-4/ITU"]},
+ {name: "CRC-5/EPC", algo: crc, params: ["CRC-5/EPC"]},
+ {name: "CRC-5/EPC-C1G2", algo: crc, params: ["CRC-5/EPC-C1G2"]},
+ {name: "CRC-5/G-704", algo: crc, params: ["CRC-5/G-704"]},
+ {name: "CRC-5/ITU", algo: crc, params: ["CRC-5/ITU"]},
+ {name: "CRC-5/USB", algo: crc, params: ["CRC-5/USB"]},
+ {name: "CRC-6/CDMA2000-A", algo: crc, params: ["CRC-6/CDMA2000-A"]},
+ {name: "CRC-6/CDMA2000-B", algo: crc, params: ["CRC-6/CDMA2000-B"]},
+ {name: "CRC-6/DARC", algo: crc, params: ["CRC-6/DARC"]},
+ {name: "CRC-6/G-704", algo: crc, params: ["CRC-6/G-704"]},
+ {name: "CRC-6/GSM", algo: crc, params: ["CRC-6/GSM"]},
+ {name: "CRC-6/ITU", algo: crc, params: ["CRC-6/ITU"]},
+ {name: "CRC-7/MMC", algo: crc, params: ["CRC-7/MMC"]},
+ {name: "CRC-7/ROHC", algo: crc, params: ["CRC-7/ROHC"]},
+ {name: "CRC-7/UMTS", algo: crc, params: ["CRC-7/UMTS"]},
+ {name: "CRC-8", algo: crc, params: ["CRC-8"]},
+ {name: "CRC-8/8H2F", algo: crc, params: ["CRC-8/8H2F"]},
+ {name: "CRC-8/AES", algo: crc, params: ["CRC-8/AES"]},
+ {name: "CRC-8/AUTOSAR", algo: crc, params: ["CRC-8/AUTOSAR"]},
+ {name: "CRC-8/BLUETOOTH", algo: crc, params: ["CRC-8/BLUETOOTH"]},
+ {name: "CRC-8/CDMA2000", algo: crc, params: ["CRC-8/CDMA2000"]},
+ {name: "CRC-8/DARC", algo: crc, params: ["CRC-8/DARC"]},
+ {name: "CRC-8/DVB-S2", algo: crc, params: ["CRC-8/DVB-S2"]},
+ {name: "CRC-8/EBU", algo: crc, params: ["CRC-8/EBU"]},
+ {name: "CRC-8/GSM-A", algo: crc, params: ["CRC-8/GSM-A"]},
+ {name: "CRC-8/GSM-B", algo: crc, params: ["CRC-8/GSM-B"]},
+ {name: "CRC-8/HITAG", algo: crc, params: ["CRC-8/HITAG"]},
+ {name: "CRC-8/I-432-1", algo: crc, params: ["CRC-8/I-432-1"]},
+ {name: "CRC-8/I-CODE", algo: crc, params: ["CRC-8/I-CODE"]},
+ {name: "CRC-8/ITU", algo: crc, params: ["CRC-8/ITU"]},
+ {name: "CRC-8/LTE", algo: crc, params: ["CRC-8/LTE"]},
+ {name: "CRC-8/MAXIM", algo: crc, params: ["CRC-8/MAXIM"]},
+ {name: "CRC-8/MAXIM-DOW", algo: crc, params: ["CRC-8/MAXIM-DOW"]},
+ {name: "CRC-8/MIFARE-MAD", algo: crc, params: ["CRC-8/MIFARE-MAD"]},
+ {name: "CRC-8/NRSC-5", algo: crc, params: ["CRC-8/NRSC-5"]},
+ {name: "CRC-8/OPENSAFETY", algo: crc, params: ["CRC-8/OPENSAFETY"]},
+ {name: "CRC-8/ROHC", algo: crc, params: ["CRC-8/ROHC"]},
+ {name: "CRC-8/SAE-J1850", algo: crc, params: ["CRC-8/SAE-J1850"]},
+ {name: "CRC-8/SAE-J1850-ZERO", algo: crc, params: ["CRC-8/SAE-J1850-ZERO"]},
+ {name: "CRC-8/SMBUS", algo: crc, params: ["CRC-8/SMBUS"]},
+ {name: "CRC-8/TECH-3250", algo: crc, params: ["CRC-8/TECH-3250"]},
+ {name: "CRC-8/WCDMA", algo: crc, params: ["CRC-8/WCDMA"]},
+ {name: "Fletcher-8", algo: fletcher8, params: []},
+ {name: "CRC-10/ATM", algo: crc, params: ["CRC-10/ATM"]},
+ {name: "CRC-10/CDMA2000", algo: crc, params: ["CRC-10/CDMA2000"]},
+ {name: "CRC-10/GSM", algo: crc, params: ["CRC-10/GSM"]},
+ {name: "CRC-10/I-610", algo: crc, params: ["CRC-10/I-610"]},
+ {name: "CRC-11/FLEXRAY", algo: crc, params: ["CRC-11/FLEXRAY"]},
+ {name: "CRC-11/UMTS", algo: crc, params: ["CRC-11/UMTS"]},
+ {name: "CRC-12/3GPP", algo: crc, params: ["CRC-12/3GPP"]},
+ {name: "CRC-12/CDMA2000", algo: crc, params: ["CRC-12/CDMA2000"]},
+ {name: "CRC-12/DECT", algo: crc, params: ["CRC-12/DECT"]},
+ {name: "CRC-12/GSM", algo: crc, params: ["CRC-12/GSM"]},
+ {name: "CRC-12/UMTS", algo: crc, params: ["CRC-12/UMTS"]},
+ {name: "CRC-13/BBC", algo: crc, params: ["CRC-13/BBC"]},
+ {name: "CRC-14/DARC", algo: crc, params: ["CRC-14/DARC"]},
+ {name: "CRC-14/GSM", algo: crc, params: ["CRC-14/GSM"]},
+ {name: "CRC-15/CAN", algo: crc, params: ["CRC-15/CAN"]},
+ {name: "CRC-15/MPT1327", algo: crc, params: ["CRC-15/MPT1327"]},
+ {name: "CRC-16", algo: crc, params: ["CRC-16"]},
+ {name: "CRC-16/A", algo: crc, params: ["CRC-16/A"]},
+ {name: "CRC-16/ACORN", algo: crc, params: ["CRC-16/ACORN"]},
+ {name: "CRC-16/ARC", algo: crc, params: ["CRC-16/ARC"]},
+ {name: "CRC-16/AUG-CCITT", algo: crc, params: ["CRC-16/AUG-CCITT"]},
+ {name: "CRC-16/AUTOSAR", algo: crc, params: ["CRC-16/AUTOSAR"]},
+ {name: "CRC-16/B", algo: crc, params: ["CRC-16/B"]},
+ {name: "CRC-16/BLUETOOTH", algo: crc, params: ["CRC-16/BLUETOOTH"]},
+ {name: "CRC-16/BUYPASS", algo: crc, params: ["CRC-16/BUYPASS"]},
+ {name: "CRC-16/CCITT", algo: crc, params: ["CRC-16/CCITT"]},
+ {name: "CRC-16/CCITT-FALSE", algo: crc, params: ["CRC-16/CCITT-FALSE"]},
+ {name: "CRC-16/CCITT-TRUE", algo: crc, params: ["CRC-16/CCITT-TRUE"]},
+ {name: "CRC-16/CCITT-ZERO", algo: crc, params: ["CRC-16/CCITT-ZERO"]},
+ {name: "CRC-16/CDMA2000", algo: crc, params: ["CRC-16/CDMA2000"]},
+ {name: "CRC-16/CMS", algo: crc, params: ["CRC-16/CMS"]},
+ {name: "CRC-16/DARC", algo: crc, params: ["CRC-16/DARC"]},
+ {name: "CRC-16/DDS-110", algo: crc, params: ["CRC-16/DDS-110"]},
+ {name: "CRC-16/DECT-R", algo: crc, params: ["CRC-16/DECT-R"]},
+ {name: "CRC-16/DECT-X", algo: crc, params: ["CRC-16/DECT-X"]},
+ {name: "CRC-16/DNP", algo: crc, params: ["CRC-16/DNP"]},
+ {name: "CRC-16/EN-13757", algo: crc, params: ["CRC-16/EN-13757"]},
+ {name: "CRC-16/EPC", algo: crc, params: ["CRC-16/EPC"]},
+ {name: "CRC-16/EPC-C1G2", algo: crc, params: ["CRC-16/EPC-C1G2"]},
+ {name: "CRC-16/GENIBUS", algo: crc, params: ["CRC-16/GENIBUS"]},
+ {name: "CRC-16/GSM", algo: crc, params: ["CRC-16/GSM"]},
+ {name: "CRC-16/I-CODE", algo: crc, params: ["CRC-16/I-CODE"]},
+ {name: "CRC-16/IBM", algo: crc, params: ["CRC-16/IBM"]},
+ {name: "CRC-16/IBM-3740", algo: crc, params: ["CRC-16/IBM-3740"]},
+ {name: "CRC-16/IBM-SDLC", algo: crc, params: ["CRC-16/IBM-SDLC"]},
+ {name: "CRC-16/IEC-61158-2", algo: crc, params: ["CRC-16/IEC-61158-2"]},
+ {name: "CRC-16/ISO-HDLC", algo: crc, params: ["CRC-16/ISO-HDLC"]},
+ {name: "CRC-16/ISO-IEC-14443-3-A", algo: crc, params: ["CRC-16/ISO-IEC-14443-3-A"]},
+ {name: "CRC-16/ISO-IEC-14443-3-B", algo: crc, params: ["CRC-16/ISO-IEC-14443-3-B"]},
+ {name: "CRC-16/KERMIT", algo: crc, params: ["CRC-16/KERMIT"]},
+ {name: "CRC-16/LHA", algo: crc, params: ["CRC-16/LHA"]},
+ {name: "CRC-16/LJ1200", algo: crc, params: ["CRC-16/LJ1200"]},
+ {name: "CRC-16/LTE", algo: crc, params: ["CRC-16/LTE"]},
+ {name: "CRC-16/M17", algo: crc, params: ["CRC-16/M17"]},
+ {name: "CRC-16/MAXIM", algo: crc, params: ["CRC-16/MAXIM"]},
+ {name: "CRC-16/MAXIM-DOW", algo: crc, params: ["CRC-16/MAXIM-DOW"]},
+ {name: "CRC-16/MCRF4XX", algo: crc, params: ["CRC-16/MCRF4XX"]},
+ {name: "CRC-16/MODBUS", algo: crc, params: ["CRC-16/MODBUS"]},
+ {name: "CRC-16/NRSC-5", algo: crc, params: ["CRC-16/NRSC-5"]},
+ {name: "CRC-16/OPENSAFETY-A", algo: crc, params: ["CRC-16/OPENSAFETY-A"]},
+ {name: "CRC-16/OPENSAFETY-B", algo: crc, params: ["CRC-16/OPENSAFETY-B"]},
+ {name: "CRC-16/PROFIBUS", algo: crc, params: ["CRC-16/PROFIBUS"]},
+ {name: "CRC-16/RIELLO", algo: crc, params: ["CRC-16/RIELLO"]},
+ {name: "CRC-16/SPI-FUJITSU", algo: crc, params: ["CRC-16/SPI-FUJITSU"]},
+ {name: "CRC-16/T10-DIF", algo: crc, params: ["CRC-16/T10-DIF"]},
+ {name: "CRC-16/TELEDISK", algo: crc, params: ["CRC-16/TELEDISK"]},
+ {name: "CRC-16/TMS37157", algo: crc, params: ["CRC-16/TMS37157"]},
+ {name: "CRC-16/UMTS", algo: crc, params: ["CRC-16/UMTS"]},
+ {name: "CRC-16/USB", algo: crc, params: ["CRC-16/USB"]},
+ {name: "CRC-16/V-41-LSB", algo: crc, params: ["CRC-16/V-41-LSB"]},
+ {name: "CRC-16/V-41-MSB", algo: crc, params: ["CRC-16/V-41-MSB"]},
+ {name: "CRC-16/VERIFONE", algo: crc, params: ["CRC-16/VERIFONE"]},
+ {name: "CRC-16/X-25", algo: crc, params: ["CRC-16/X-25"]},
+ {name: "CRC-16/XMODEM", algo: crc, params: ["CRC-16/XMODEM"]},
+ {name: "CRC-16/ZMODEM", algo: crc, params: ["CRC-16/ZMODEM"]},
+ {name: "Fletcher-16", algo: fletcher16, params: []},
+ {name: "CRC-17/CAN-FD", algo: crc, params: ["CRC-17/CAN-FD"]},
+ {name: "CRC-21/CAN-FD", algo: crc, params: ["CRC-21/CAN-FD"]},
+ {name: "CRC-24/BLE", algo: crc, params: ["CRC-24/BLE"]},
+ {name: "CRC-24/FLEXRAY-A", algo: crc, params: ["CRC-24/FLEXRAY-A"]},
+ {name: "CRC-24/FLEXRAY-B", algo: crc, params: ["CRC-24/FLEXRAY-B"]},
+ {name: "CRC-24/INTERLAKEN", algo: crc, params: ["CRC-24/INTERLAKEN"]},
+ {name: "CRC-24/LTE-A", algo: crc, params: ["CRC-24/LTE-A"]},
+ {name: "CRC-24/LTE-B", algo: crc, params: ["CRC-24/LTE-B"]},
+ {name: "CRC-24/OPENPGP", algo: crc, params: ["CRC-24/OPENPGP"]},
+ {name: "CRC-24/OS-9", algo: crc, params: ["CRC-24/OS-9"]},
+ {name: "CRC-30/CDMA", algo: crc, params: ["CRC-30/CDMA"]},
+ {name: "CRC-31/PHILIPS", algo: crc, params: ["CRC-31/PHILIPS"]},
+ {name: "Adler-32", algo: adler32, params: []},
+ {name: "CRC-32", algo: crc, params: ["CRC-32"]},
+ {name: "CRC-32/AAL5", algo: crc, params: ["CRC-32/AAL5"]},
+ {name: "CRC-32/ADCCP", algo: crc, params: ["CRC-32/ADCCP"]},
+ {name: "CRC-32/AIXM", algo: crc, params: ["CRC-32/AIXM"]},
+ {name: "CRC-32/AUTOSAR", algo: crc, params: ["CRC-32/AUTOSAR"]},
+ {name: "CRC-32/BASE91-C", algo: crc, params: ["CRC-32/BASE91-C"]},
+ {name: "CRC-32/BASE91-D", algo: crc, params: ["CRC-32/BASE91-D"]},
+ {name: "CRC-32/BZIP2", algo: crc, params: ["CRC-32/BZIP2"]},
+ {name: "CRC-32/C", algo: crc, params: ["CRC-32/C"]},
+ {name: "CRC-32/CASTAGNOLI", algo: crc, params: ["CRC-32/CASTAGNOLI"]},
+ {name: "CRC-32/CD-ROM-EDC", algo: crc, params: ["CRC-32/CD-ROM-EDC"]},
+ {name: "CRC-32/CKSUM", algo: crc, params: ["CRC-32/CKSUM"]},
+ {name: "CRC-32/D", algo: crc, params: ["CRC-32/D"]},
+ {name: "CRC-32/DECT-B", algo: crc, params: ["CRC-32/DECT-B"]},
+ {name: "CRC-32/INTERLAKEN", algo: crc, params: ["CRC-32/INTERLAKEN"]},
+ {name: "CRC-32/ISCSI", algo: crc, params: ["CRC-32/ISCSI"]},
+ {name: "CRC-32/ISO-HDLC", algo: crc, params: ["CRC-32/ISO-HDLC"]},
+ {name: "CRC-32/JAMCRC", algo: crc, params: ["CRC-32/JAMCRC"]},
+ {name: "CRC-32/MEF", algo: crc, params: ["CRC-32/MEF"]},
+ {name: "CRC-32/MPEG-2", algo: crc, params: ["CRC-32/MPEG-2"]},
+ {name: "CRC-32/NVME", algo: crc, params: ["CRC-32/NVME"]},
+ {name: "CRC-32/PKZIP", algo: crc, params: ["CRC-32/PKZIP"]},
+ {name: "CRC-32/POSIX", algo: crc, params: ["CRC-32/POSIX"]},
+ {name: "CRC-32/Q", algo: crc, params: ["CRC-32/Q"]},
+ {name: "CRC-32/SATA", algo: crc, params: ["CRC-32/SATA"]},
+ {name: "CRC-32/V-42", algo: crc, params: ["CRC-32/V-42"]},
+ {name: "CRC-32/XFER", algo: crc, params: ["CRC-32/XFER"]},
+ {name: "CRC-32/XZ", algo: crc, params: ["CRC-32/XZ"]},
+ {name: "Fletcher-32", algo: fletcher32, params: []},
+ {name: "CRC-40/GSM", algo: crc, params: ["CRC-40/GSM"]},
+ {name: "CRC-64/ECMA-182", algo: crc, params: ["CRC-64/ECMA-182"]},
+ {name: "CRC-64/GO-ECMA", algo: crc, params: ["CRC-64/GO-ECMA"]},
+ {name: "CRC-64/GO-ISO", algo: crc, params: ["CRC-64/GO-ISO"]},
+ {name: "CRC-64/MS", algo: crc, params: ["CRC-64/MS"]},
+ {name: "CRC-64/NVME", algo: crc, params: ["CRC-64/NVME"]},
+ {name: "CRC-64/REDIS", algo: crc, params: ["CRC-64/REDIS"]},
+ {name: "CRC-64/WE", algo: crc, params: ["CRC-64/WE"]},
+ {name: "CRC-64/XZ", algo: crc, params: ["CRC-64/XZ"]},
+ {name: "Fletcher-64", algo: fletcher64, params: []},
+ {name: "CRC-82/DARC", algo: crc, params: ["CRC-82/DARC"]}
+ ];
+ }
+
+ /**
+ * @param {ArrayBuffer} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ const [length, includeNames] = args;
+ let output = "";
+ this.checksums.forEach(checksum => {
+ 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 d9af8065..df09aa85 100644
--- a/src/core/operations/GenerateAllHashes.mjs
+++ b/src/core/operations/GenerateAllHashes.mjs
@@ -22,14 +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 CRC8Checksum from "./CRC8Checksum.mjs";
-import CRC16Checksum from "./CRC16Checksum.mjs";
-import CRC32Checksum from "./CRC32Checksum.mjs";
import BLAKE2b from "./BLAKE2b.mjs";
import BLAKE2s from "./BLAKE2s.mjs";
import Streebog from "./Streebog.mjs";
@@ -114,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 CRC8Checksum), inputType: "arrayBuffer", params: ["CRC-8"]},
- {name: "CRC-16", algo: (new CRC16Checksum), inputType: "arrayBuffer", params: []},
- {name: "CRC-32", algo: (new CRC32Checksum), inputType: "arrayBuffer", params: []}
- ];
}
/**
@@ -144,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/GenerateECDSAKeyPair.mjs b/src/core/operations/GenerateECDSAKeyPair.mjs
new file mode 100644
index 00000000..14714a02
--- /dev/null
+++ b/src/core/operations/GenerateECDSAKeyPair.mjs
@@ -0,0 +1,102 @@
+/**
+ * @author cplussharp
+ * @copyright Crown Copyright 2021
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+import { cryptNotice } from "../lib/Crypt.mjs";
+import r from "jsrsasign";
+
+/**
+ * Generate ECDSA Key Pair operation
+ */
+class GenerateECDSAKeyPair extends Operation {
+
+ /**
+ * GenerateECDSAKeyPair constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "Generate ECDSA Key Pair";
+ this.module = "Ciphers";
+ this.description = `Generate an ECDSA key pair with a given Curve.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).uuid package.object tags.PHP Deserialize.[5,"abc",true]a:3:{i:0;i:5;i:1;s:3:"abc";i:2;b:1;}";
+ this.infoURL = "https://www.phpinternalsbook.com/php5/classes_objects/serialization.html";
+ this.inputType = "JSON";
+ this.outputType = "string";
+ this.args = [];
+ }
+
+ /**
+ * @param {JSON} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ /**
+ * Determines if a number is an integer
+ * @param {number} value
+ * @returns {boolean}
+ */
+ function isInteger(value) {
+ return typeof value === "number" && parseInt(value.toString(), 10) === value;
+ }
+
+ /**
+ * Serialize basic types
+ * @param {string | number | boolean} content
+ * @returns {string}
+ */
+ function serializeBasicTypes(content) {
+ const basicTypes = {
+ "string": "s",
+ "integer": "i",
+ "float": "d",
+ "boolean": "b"
+ };
+ /**
+ * Booleans
+ * cast to 0 or 1
+ */
+ if (typeof content === "boolean") {
+ return `${basicTypes.boolean}:${content ? 1 : 0}`;
+ }
+ /* Numbers */
+ if (typeof content === "number") {
+ if (isInteger(content)) {
+ return `${basicTypes.integer}:${content.toString()}`;
+ } else {
+ return `${basicTypes.float}:${content.toString()}`;
+ }
+ }
+ /* Strings */
+ if (typeof content === "string")
+ return `${basicTypes.string}:${content.length}:"${content}"`;
+
+ /** This should be unreachable */
+ throw new OperationError(`Encountered a non-implemented type: ${typeof content}`);
+ }
+
+ /**
+ * Recursively serialize
+ * @param {*} object
+ * @returns {string}
+ */
+ function serialize(object) {
+ /* Null */
+ if (object == null) {
+ return `N;`;
+ }
+
+ if (typeof object !== "object") {
+ /* Basic types */
+ return `${serializeBasicTypes(object)};`;
+ } else if (object instanceof Array) {
+ /* Arrays */
+ const serializedElements = [];
+
+ for (let i = 0; i < object.length; i++) {
+ serializedElements.push(`${serialize(i)}${serialize(object[i])}`);
+ }
+
+ return `a:${object.length}:{${serializedElements.join("")}}`;
+ } else if (object instanceof Object) {
+ /**
+ * Objects
+ * Note: the output cannot be guaranteed to be in the same order as the input
+ */
+ const serializedElements = [];
+ const keys = Object.keys(object);
+
+ for (const key of keys) {
+ serializedElements.push(`${serialize(key)}${serialize(object[key])}`);
+ }
+
+ return `a:${keys.length}:{${serializedElements.join("")}}`;
+ }
+
+ /** This should be unreachable */
+ throw new OperationError(`Encountered a non-implemented type: ${typeof object}`);
+ }
+
+ return serialize(input);
+ }
+}
+
+export default PHPSerialize;
diff --git a/src/core/operations/ParseCSR.mjs b/src/core/operations/ParseCSR.mjs
new file mode 100644
index 00000000..d3b3c364
--- /dev/null
+++ b/src/core/operations/ParseCSR.mjs
@@ -0,0 +1,390 @@
+/**
+ * @author jkataja
+ * @copyright Crown Copyright 2023
+ * @license Apache-2.0
+ */
+
+import r from "jsrsasign";
+import Operation from "../Operation.mjs";
+import { formatDnObj } from "../lib/PublicKey.mjs";
+import Utils from "../Utils.mjs";
+
+/**
+ * Parse CSR operation
+ */
+class ParseCSR extends Operation {
+
+ /**
+ * ParseCSR constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "Parse CSR";
+ this.module = "PublicKey";
+ this.description = "Parse Certificate Signing Request (CSR) for an X.509 certificate";
+ this.infoURL = "https://wikipedia.org/wiki/Certificate_signing_request";
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [
+ {
+ "name": "Input format",
+ "type": "option",
+ "value": ["PEM"]
+ }
+ ];
+ this.checks = [
+ {
+ "pattern": "^-+BEGIN CERTIFICATE REQUEST-+\\r?\\n[\\da-z+/\\n\\r]+-+END CERTIFICATE REQUEST-+\\r?\\n?$",
+ "flags": "i",
+ "args": ["PEM"]
+ }
+ ];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string} Human-readable description of a Certificate Signing Request (CSR).
+ */
+ run(input, args) {
+ if (!input.length) {
+ return "No input";
+ }
+
+ // Parse the CSR into JSON parameters
+ const csrParam = new r.KJUR.asn1.csr.CSRUtil.getParam(input);
+
+ return `Subject\n${formatDnObj(csrParam.subject, 2)}
+Public Key${formatSubjectPublicKey(csrParam.sbjpubkey)}
+Signature${formatSignature(csrParam.sigalg, csrParam.sighex)}
+Requested Extensions${formatRequestedExtensions(csrParam)}`;
+ }
+}
+
+/**
+ * Format signature of a CSR
+ * @param {*} sigAlg string
+ * @param {*} sigHex string
+ * @returns Multi-line string describing CSR Signature
+ */
+function formatSignature(sigAlg, sigHex) {
+ let out = `\n`;
+
+ out += ` Algorithm: ${sigAlg}\n`;
+
+ if (new RegExp("withdsa", "i").test(sigAlg)) {
+ const d = new r.KJUR.crypto.DSA();
+ const sigParam = d.parseASN1Signature(sigHex);
+ out += ` Signature:
+ R: ${formatHexOntoMultiLine(absBigIntToHex(sigParam[0]))}
+ S: ${formatHexOntoMultiLine(absBigIntToHex(sigParam[1]))}\n`;
+ } else if (new RegExp("withrsa", "i").test(sigAlg)) {
+ out += ` Signature: ${formatHexOntoMultiLine(sigHex)}\n`;
+ } else {
+ out += ` Signature: ${formatHexOntoMultiLine(ensureHexIsPositiveInTwosComplement(sigHex))}\n`;
+ }
+
+ return chop(out);
+}
+
+/**
+ * Format Subject Public Key from PEM encoded public key string
+ * @param {*} publicKeyPEM string
+ * @returns Multi-line string describing Subject Public Key Info
+ */
+function formatSubjectPublicKey(publicKeyPEM) {
+ let out = "\n";
+
+ const publicKey = r.KEYUTIL.getKey(publicKeyPEM);
+ if (publicKey instanceof r.RSAKey) {
+ out += ` Algorithm: RSA
+ Length: ${publicKey.n.bitLength()} bits
+ Modulus: ${formatHexOntoMultiLine(absBigIntToHex(publicKey.n))}
+ Exponent: ${publicKey.e} (0x${Utils.hex(publicKey.e)})\n`;
+ } else if (publicKey instanceof r.KJUR.crypto.ECDSA) {
+ out += ` Algorithm: ECDSA
+ Length: ${publicKey.ecparams.keylen} bits
+ Pub: ${formatHexOntoMultiLine(publicKey.pubKeyHex)}
+ ASN1 OID: ${r.KJUR.crypto.ECDSA.getName(publicKey.getShortNISTPCurveName())}
+ NIST CURVE: ${publicKey.getShortNISTPCurveName()}\n`;
+ } else if (publicKey instanceof r.KJUR.crypto.DSA) {
+ out += ` Algorithm: DSA
+ Length: ${publicKey.p.toString(16).length * 4} bits
+ Pub: ${formatHexOntoMultiLine(absBigIntToHex(publicKey.y))}
+ P: ${formatHexOntoMultiLine(absBigIntToHex(publicKey.p))}
+ Q: ${formatHexOntoMultiLine(absBigIntToHex(publicKey.q))}
+ G: ${formatHexOntoMultiLine(absBigIntToHex(publicKey.g))}\n`;
+ } else {
+ out += `unsupported public key algorithm\n`;
+ }
+
+ return chop(out);
+}
+
+/**
+ * Format known extensions of a CSR
+ * @param {*} csrParam object
+ * @returns Multi-line string describing CSR Requested Extensions
+ */
+function formatRequestedExtensions(csrParam) {
+ const formattedExtensions = new Array(4).fill("");
+
+ if (Object.hasOwn(csrParam, "extreq")) {
+ for (const extension of csrParam.extreq) {
+ let parts = [];
+ switch (extension.extname) {
+ case "basicConstraints" :
+ parts = describeBasicConstraints(extension);
+ formattedExtensions[0] = ` Basic Constraints:${formatExtensionCriticalTag(extension)}\n${indent(4, parts)}`;
+ break;
+ case "keyUsage" :
+ parts = describeKeyUsage(extension);
+ formattedExtensions[1] = ` Key Usage:${formatExtensionCriticalTag(extension)}\n${indent(4, parts)}`;
+ break;
+ case "extKeyUsage" :
+ parts = describeExtendedKeyUsage(extension);
+ formattedExtensions[2] = ` Extended Key Usage:${formatExtensionCriticalTag(extension)}\n${indent(4, parts)}`;
+ break;
+ case "subjectAltName" :
+ parts = describeSubjectAlternativeName(extension);
+ formattedExtensions[3] = ` Subject Alternative Name:${formatExtensionCriticalTag(extension)}\n${indent(4, parts)}`;
+ break;
+ default :
+ parts = ["(unsuported extension)"];
+ formattedExtensions.push(` ${extension.extname}:${formatExtensionCriticalTag(extension)}\n${indent(4, parts)}`);
+ }
+ }
+ }
+
+ let out = "\n";
+
+ formattedExtensions.forEach((formattedExtension) => {
+ if (formattedExtension !== undefined && formattedExtension !== null && formattedExtension.length !== 0) {
+ out += formattedExtension;
+ }
+ });
+
+ return chop(out);
+}
+
+/**
+ * Format extension critical tag
+ * @param {*} extension Object
+ * @returns String describing whether the extension is critical or not
+ */
+function formatExtensionCriticalTag(extension) {
+ return Object.hasOwn(extension, "critical") && extension.critical ? " critical" : "";
+}
+
+/**
+ * Format string input as a comma separated hex string on multiple lines
+ * @param {*} hex String
+ * @returns Multi-line string describing the Hex input
+ */
+function formatHexOntoMultiLine(hex) {
+ if (hex.length % 2 !== 0) {
+ hex = "0" + hex;
+ }
+
+ return formatMultiLine(chop(hex.replace(/(..)/g, "$&:")));
+}
+
+/**
+ * Convert BigInt to abs value in Hex
+ * @param {*} int BigInt
+ * @returns String representing absolute value in Hex
+ */
+function absBigIntToHex(int) {
+ int = int < 0n ? -int : int;
+
+ return ensureHexIsPositiveInTwosComplement(int.toString(16));
+}
+
+/**
+ * Ensure Hex String remains positive in 2's complement
+ * @param {*} hex String
+ * @returns Hex String ensuring value remains positive in 2's complement
+ */
+function ensureHexIsPositiveInTwosComplement(hex) {
+ if (hex.length % 2 !== 0) {
+ return "0" + hex;
+ }
+
+ // prepend 00 if most significant bit is 1 (sign bit)
+ if (hex.length >=2 && (parseInt(hex.substring(0, 2), 16) & 128)) {
+ hex = "00" + hex;
+ }
+
+ return hex;
+}
+
+/**
+ * Format string onto multiple lines
+ * @param {*} longStr
+ * @returns String as a multi-line string
+ */
+function formatMultiLine(longStr) {
+ const lines = [];
+
+ for (let remain = longStr ; remain !== "" ; remain = remain.substring(48)) {
+ lines.push(remain.substring(0, 48));
+ }
+
+ return lines.join("\n ");
+}
+
+/**
+ * Describe Basic Constraints
+ * @see RFC 5280 4.2.1.9. Basic Constraints https://www.ietf.org/rfc/rfc5280.txt
+ * @param {*} extension CSR extension with the name `basicConstraints`
+ * @returns Array of strings describing Basic Constraints
+ */
+function describeBasicConstraints(extension) {
+ const constraints = [];
+
+ constraints.push(`CA = ${Object.hasOwn(extension, "cA") && extension.cA ? "true" : "false"}`);
+ if (Object.hasOwn(extension, "pathLen")) constraints.push(`PathLenConstraint = ${extension.pathLen}`);
+
+ return constraints;
+}
+
+/**
+ * Describe Key Usage extension permitted use cases
+ * @see RFC 5280 4.2.1.3. Key Usage https://www.ietf.org/rfc/rfc5280.txt
+ * @param {*} extension CSR extension with the name `keyUsage`
+ * @returns Array of strings describing Key Usage extension permitted use cases
+ */
+function describeKeyUsage(extension) {
+ const usage = [];
+
+ const kuIdentifierToName = {
+ digitalSignature: "Digital Signature",
+ nonRepudiation: "Non-repudiation",
+ keyEncipherment: "Key encipherment",
+ dataEncipherment: "Data encipherment",
+ keyAgreement: "Key agreement",
+ keyCertSign: "Key certificate signing",
+ cRLSign: "CRL signing",
+ encipherOnly: "Encipher Only",
+ decipherOnly: "Decipher Only",
+ };
+
+ if (Object.hasOwn(extension, "names")) {
+ extension.names.forEach((ku) => {
+ if (Object.hasOwn(kuIdentifierToName, ku)) {
+ usage.push(kuIdentifierToName[ku]);
+ } else {
+ usage.push(`unknown key usage (${ku})`);
+ }
+ });
+ }
+
+ if (usage.length === 0) usage.push("(none)");
+
+ return usage;
+}
+
+/**
+ * Describe Extended Key Usage extension permitted use cases
+ * @see RFC 5280 4.2.1.12. Extended Key Usage https://www.ietf.org/rfc/rfc5280.txt
+ * @param {*} extension CSR extension with the name `extendedKeyUsage`
+ * @returns Array of strings describing Extended Key Usage extension permitted use cases
+ */
+function describeExtendedKeyUsage(extension) {
+ const usage = [];
+
+ const ekuIdentifierToName = {
+ "serverAuth": "TLS Web Server Authentication",
+ "clientAuth": "TLS Web Client Authentication",
+ "codeSigning": "Code signing",
+ "emailProtection": "E-mail Protection (S/MIME)",
+ "timeStamping": "Trusted Timestamping",
+ "1.3.6.1.4.1.311.2.1.21": "Microsoft Individual Code Signing", // msCodeInd
+ "1.3.6.1.4.1.311.2.1.22": "Microsoft Commercial Code Signing", // msCodeCom
+ "1.3.6.1.4.1.311.10.3.1": "Microsoft Trust List Signing", // msCTLSign
+ "1.3.6.1.4.1.311.10.3.3": "Microsoft Server Gated Crypto", // msSGC
+ "1.3.6.1.4.1.311.10.3.4": "Microsoft Encrypted File System", // msEFS
+ "1.3.6.1.4.1.311.20.2.2": "Microsoft Smartcard Login", // msSmartcardLogin
+ "2.16.840.1.113730.4.1": "Netscape Server Gated Crypto", // nsSGC
+ };
+
+ if (Object.hasOwn(extension, "array")) {
+ extension.array.forEach((eku) => {
+ if (Object.hasOwn(ekuIdentifierToName, eku)) {
+ usage.push(ekuIdentifierToName[eku]);
+ } else {
+ usage.push(eku);
+ }
+ });
+ }
+
+ if (usage.length === 0) usage.push("(none)");
+
+ return usage;
+}
+
+/**
+ * Format Subject Alternative Names from the name `subjectAltName` extension
+ * @see RFC 5280 4.2.1.6. Subject Alternative Name https://www.ietf.org/rfc/rfc5280.txt
+ * @param {*} extension object
+ * @returns Array of strings describing Subject Alternative Name extension
+ */
+function describeSubjectAlternativeName(extension) {
+ const names = [];
+
+ if (Object.hasOwn(extension, "extname") && extension.extname === "subjectAltName") {
+ if (Object.hasOwn(extension, "array")) {
+ for (const altName of extension.array) {
+ Object.keys(altName).forEach((key) => {
+ switch (key) {
+ case "rfc822":
+ names.push(`EMAIL: ${altName[key]}`);
+ break;
+ case "dns":
+ names.push(`DNS: ${altName[key]}`);
+ break;
+ case "uri":
+ names.push(`URI: ${altName[key]}`);
+ break;
+ case "ip":
+ names.push(`IP: ${altName[key]}`);
+ break;
+ case "dn":
+ names.push(`DIR: ${altName[key].str}`);
+ break;
+ case "other" :
+ names.push(`Other: ${altName[key].oid}::${altName[key].value.utf8str.str}`);
+ break;
+ default:
+ names.push(`(unable to format SAN '${key}':${altName[key]})\n`);
+ }
+ });
+ }
+ }
+ }
+
+ return names;
+}
+
+/**
+ * Join an array of strings and add leading spaces to each line.
+ * @param {*} n How many leading spaces
+ * @param {*} parts Array of strings
+ * @returns Joined and indented string.
+ */
+function indent(n, parts) {
+ const fluff = " ".repeat(n);
+ return fluff + parts.join("\n" + fluff) + "\n";
+}
+
+/**
+ * Remove last character from a string.
+ * @param {*} s String
+ * @returns Chopped string.
+ */
+function chop(s) {
+ return s.substring(0, s.length - 1);
+}
+
+export default ParseCSR;
diff --git a/src/core/operations/ParseEthernetFrame.mjs b/src/core/operations/ParseEthernetFrame.mjs
new file mode 100644
index 00000000..9dac5d57
--- /dev/null
+++ b/src/core/operations/ParseEthernetFrame.mjs
@@ -0,0 +1,115 @@
+/**
+ * @author tedk [tedk@ted.do]
+ * @copyright Crown Copyright 2024
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+import OperationError from "../errors/OperationError.mjs";
+import Utils from "../Utils.mjs";
+import {fromHex, toHex} from "../lib/Hex.mjs";
+
+/**
+ * Parse Ethernet frame operation
+ */
+class ParseEthernetFrame extends Operation {
+
+ /**
+ * ParseEthernetFrame constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "Parse Ethernet frame";
+ this.module = "Default";
+ this.description = "Parses an Ethernet frame and either shows the deduced values (Source and destination MAC, VLANs) or returns the packet data.
Good for use in conjunction with the Parse IPv4, and Parse TCP/UDP recipes.";
+ this.infoURL = "https://en.wikipedia.org/wiki/Ethernet_frame#Frame_%E2%80%93_data_link_layer";
+ this.inputType = "string";
+ this.outputType = "html";
+ this.args = [
+ {
+ name: "Input type",
+ type: "option",
+ value: [
+ "Raw", "Hex"
+ ],
+ defaultIndex: 0,
+ },
+ {
+ name: "Return type",
+ type: "option",
+ value: [
+ "Text output", "Packet data", "Packet data (hex)",
+ ],
+ defaultIndex: 0,
+ }
+ ];
+ }
+
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {html}
+ */
+ run(input, args) {
+ const format = args[0];
+ const outputFormat = args[1];
+
+ if (format === "Hex") {
+ input = fromHex(input);
+ } else if (format === "Raw") {
+ input = new Uint8Array(Utils.strToArrayBuffer(input));
+ } else {
+ throw new OperationError("Invalid input format selected.");
+ }
+
+ const destinationMac = input.slice(0, 6);
+ const sourceMac = input.slice(6, 12);
+
+ let offset = 12;
+ const vlans = [];
+
+ while (offset < input.length) {
+ const ethType = Utils.byteArrayToChars(input.slice(offset, offset+2));
+ offset += 2;
+
+
+ if (ethType === "\x08\x00") {
+ break;
+ } else if (ethType === "\x81\x00" || ethType === "\x88\xA8") {
+ // Parse the VLAN tag:
+ // [0000] 0000 0000 0000
+ // ^^^ PRIO - Ignored
+ // ^ DEI - Ignored
+ // ^^^^ ^^^^ ^^^^ VLAN ID
+ const vlanTag = input.slice(offset+2, offset+4);
+ vlans.push((vlanTag[0] & 0b00001111) << 4 | vlanTag[1]);
+
+ offset += 2;
+ } else {
+ break;
+ }
+ }
+
+ const packetData = input.slice(offset);
+
+ if (outputFormat === "Packet data") {
+ return Utils.byteArrayToChars(packetData);
+ } else if (outputFormat === "Packet data (hex)") {
+ return toHex(packetData);
+ } else if (outputFormat === "Text output") {
+ let retval = `Source MAC: ${toHex(sourceMac, ":")}\nDestination MAC: ${toHex(destinationMac, ":")}\n`;
+ if (vlans.length > 0) {
+ retval += `VLAN: ${vlans.join(", ")}\n`;
+ }
+ retval += `Data:\n${toHex(packetData)}`;
+ return retval;
+ }
+
+ }
+
+
+}
+
+export default ParseEthernetFrame;
diff --git a/src/core/operations/ParseIPv4Header.mjs b/src/core/operations/ParseIPv4Header.mjs
index 84351cdc..4eed5d46 100644
--- a/src/core/operations/ParseIPv4Header.mjs
+++ b/src/core/operations/ParseIPv4Header.mjs
@@ -33,6 +33,12 @@ class ParseIPv4Header extends Operation {
"name": "Input format",
"type": "option",
"value": ["Hex", "Raw"]
+ },
+ {
+ "name": "Output format",
+ "type": "option",
+ "value": ["Table", "Data (hex)", "Data (raw)"],
+ defaultIndex: 0,
}
];
}
@@ -44,6 +50,8 @@ class ParseIPv4Header extends Operation {
*/
run(input, args) {
const format = args[0];
+ const outputFormat = args[1];
+
let output;
if (format === "Hex") {
@@ -98,7 +106,10 @@ class ParseIPv4Header extends Operation {
checksumResult = givenChecksum + " (incorrect, should be " + correctChecksum + ")";
}
- output = `Field Value
+ const data = input.slice(ihl * 4);
+
+ if (outputFormat === "Table") {
+ output = `Field Value
Version ${version}
Internet Header Length (IHL) ${ihl} (${ihl * 4} bytes)
Differentiated Services Code Point (DSCP) ${dscp}
@@ -116,13 +127,19 @@ class ParseIPv4Header extends Operation {
Protocol ${protocol}, ${protocolInfo.protocol} (${protocolInfo.keyword})
Header checksum ${checksumResult}
Source IP address ${ipv4ToStr(srcIP)}
-Destination IP address ${ipv4ToStr(dstIP)} `;
+Destination IP address ${ipv4ToStr(dstIP)}
+Data (hex) ${toHex(data)} `;
- if (ihl > 5) {
- output += `Options ${toHex(options)} `;
+ if (ihl > 5) {
+ output += `Options ${toHex(options)} `;
+ }
+
+ return output + "
";
+ } else if (outputFormat === "Data (hex)") {
+ return toHex(data);
+ } else if (outputFormat === "Data (raw)") {
+ return Utils.byteArrayToChars(data);
}
-
- return output + "
";
}
}
diff --git a/src/core/operations/ParseQRCode.mjs b/src/core/operations/ParseQRCode.mjs
index 77ab7d21..89eaddee 100644
--- a/src/core/operations/ParseQRCode.mjs
+++ b/src/core/operations/ParseQRCode.mjs
@@ -13,7 +13,6 @@ import { parseQrCode } from "../lib/QRCode.mjs";
* Parse QR Code operation
*/
class ParseQRCode extends Operation {
-
/**
* ParseQRCode constructor
*/
@@ -22,24 +21,26 @@ class ParseQRCode extends Operation {
this.name = "Parse QR Code";
this.module = "Image";
- this.description = "Reads an image file and attempts to detect and read a Quick Response (QR) code from the image.
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.
Normalise Image
Attempts to normalise the image before parsing it to improve detection of a QR code.";
this.infoURL = "https://wikipedia.org/wiki/QR_code";
this.inputType = "ArrayBuffer";
this.outputType = "string";
this.args = [
{
- "name": "Normalise image",
- "type": "boolean",
- "value": false
- }
+ name: "Normalise image",
+ type: "boolean",
+ value: false,
+ },
];
this.checks = [
{
- "pattern": "^(?:\\xff\\xd8\\xff|\\x89\\x50\\x4e\\x47|\\x47\\x49\\x46|.{8}\\x57\\x45\\x42\\x50|\\x42\\x4d)",
- "flags": "",
- "args": [false],
- "useful": true
- }
+ pattern:
+ "^(?:\\xff\\xd8\\xff|\\x89\\x50\\x4e\\x47|\\x47\\x49\\x46|.{8}\\x57\\x45\\x42\\x50|\\x42\\x4d)",
+ flags: "",
+ args: [false],
+ useful: true,
+ },
];
}
@@ -54,9 +55,8 @@ class ParseQRCode extends Operation {
if (!isImage(input)) {
throw new OperationError("Invalid file type.");
}
- return await parseQrCode(input, normalise);
+ return parseQrCode(input, normalise);
}
-
}
export default ParseQRCode;
diff --git a/src/core/operations/ParseTLSRecord.mjs b/src/core/operations/ParseTLSRecord.mjs
new file mode 100644
index 00000000..57a339a8
--- /dev/null
+++ b/src/core/operations/ParseTLSRecord.mjs
@@ -0,0 +1,884 @@
+/**
+ * @author c65722 []
+ * @copyright Crown Copyright 2024
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+import {toHexFast} from "../lib/Hex.mjs";
+import {objToTable} from "../lib/Protocol.mjs";
+import Stream from "../lib/Stream.mjs";
+
+/**
+ * Parse TLS record operation.
+ */
+class ParseTLSRecord extends Operation {
+
+ /**
+ * ParseTLSRecord constructor.
+ */
+ constructor() {
+ super();
+
+ this.name = "Parse TLS record";
+ this.module = "Default";
+ this.description = "Parses one or more TLS records";
+ this.infoURL = "https://wikipedia.org/wiki/Transport_Layer_Security";
+ this.inputType = "ArrayBuffer";
+ this.outputType = "json";
+ this.presentType = "html";
+ this.args = [];
+ this._handshakeParser = new HandshakeParser();
+ this._contentTypes = new Map();
+
+ for (const key in ContentType) {
+ this._contentTypes[ContentType[key]] = key.toString().toLocaleLowerCase();
+ }
+ }
+
+ /**
+ * @param {ArrayBuffer} input - Stream, containing one or more raw TLS Records.
+ * @param {Object[]} args
+ * @returns {Object[]} Array of Object representations of TLS Records contained within input.
+ */
+ run(input, args) {
+ const s = new Stream(new Uint8Array(input));
+
+ const output = [];
+
+ while (s.hasMore()) {
+ const record = this._readRecord(s);
+ if (record) {
+ output.push(record);
+ }
+ }
+
+ return output;
+ }
+
+ /**
+ * Reads a TLS Record from the following bytes in the provided Stream.
+ *
+ * @param {Stream} input - Stream, containing a raw TLS Record.
+ * @returns {Object} Object representation of TLS Record.
+ */
+ _readRecord(input) {
+ const RECORD_HEADER_LEN = 5;
+
+ if (input.position + RECORD_HEADER_LEN > input.length) {
+ input.moveTo(input.length);
+
+ return null;
+ }
+
+ const type = input.readInt(1);
+ const typeString = this._contentTypes[type] ?? type.toString();
+ const version = "0x" + toHexFast(input.getBytes(2));
+ const length = input.readInt(2);
+ const content = input.getBytes(length);
+ const truncated = content.length < length;
+
+ const recordHeader = new RecordHeader(typeString, version, length, truncated);
+
+ if (!content.length) {
+ return {...recordHeader};
+ }
+
+ if (type === ContentType.HANDSHAKE) {
+ return this._handshakeParser.parse(new Stream(content), recordHeader);
+ }
+
+ const record = {...recordHeader};
+ record.value = "0x" + toHexFast(content);
+
+ return record;
+ }
+
+ /**
+ * Displays the parsed TLS Records in a tabular style.
+ *
+ * @param {Object[]} data - Array of Object representations of the TLS Records.
+ * @returns {html} HTML representation of TLS Records contained within data.
+ */
+ present(data) {
+ return data.map(r => objToTable(r)).join("\n\n");
+ }
+}
+
+export default ParseTLSRecord;
+
+/**
+ * Repesents the known values of type field of a TLS Record header.
+ */
+const ContentType = Object.freeze({
+ CHANGE_CIPHER_SPEC: 20,
+ ALERT: 21,
+ HANDSHAKE: 22,
+ APPLICATION_DATA: 23,
+});
+
+/**
+ * Represents a TLS Record header
+ */
+class RecordHeader {
+ /**
+ * RecordHeader cosntructor.
+ *
+ * @param {string} type - String representation of TLS Record type field.
+ * @param {string} version - Hex representation of TLS Record version field.
+ * @param {int} length - Length of TLS Record.
+ * @param {bool} truncated - Is TLS Record truncated.
+ */
+ constructor(type, version, length, truncated) {
+ this.type = type;
+ this.version = version;
+ this.length = length;
+
+ if (truncated) {
+ this.truncated = true;
+ }
+ }
+}
+
+/**
+ * Parses TLS Handshake messages.
+ */
+class HandshakeParser {
+
+ /**
+ * HandshakeParser constructor.
+ */
+ constructor() {
+ this._clientHelloParser = new ClientHelloParser();
+ this._serverHelloParser = new ServerHelloParser();
+ this._newSessionTicketParser = new NewSessionTicketParser();
+ this._certificateParser = new CertificateParser();
+ this._certificateRequestParser = new CertificateRequestParser();
+ this._certificateVerifyParser = new CertificateVerifyParser();
+ this._handshakeTypes = new Map();
+
+ for (const key in HandshakeType) {
+ this._handshakeTypes[HandshakeType[key]] = key.toString().toLowerCase();
+ }
+ }
+
+ /**
+ * Parses a single TLS handshake message.
+ *
+ * @param {Stream} input - Stream, containing a raw Handshake message.
+ * @param {RecordHeader} recordHeader - TLS Record header.
+ * @returns {Object} Object representation of Handshake.
+ */
+ parse(input, recordHeader) {
+ const output = {...recordHeader};
+
+ if (!input.hasMore()) {
+ return output;
+ }
+
+ const handshakeType = input.readInt(1);
+ output.handshakeType = this._handshakeTypes[handshakeType] ?? handshakeType.toString();
+
+ if (input.position + 3 > input.length) {
+ input.moveTo(input.length);
+
+ return output;
+ }
+
+ const handshakeLength = input.readInt(3);
+
+ if (handshakeLength + 4 !== recordHeader.length) {
+ input.moveTo(0);
+
+ output.handshakeType = this._handshakeTypes[HandshakeType.FINISHED];
+ output.handshakeValue = "0x" + toHexFast(input.bytes);
+
+ return output;
+ }
+
+ const content = input.getBytes(handshakeLength);
+ if (!content.length) {
+ return output;
+ }
+
+ switch (handshakeType) {
+ case HandshakeType.CLIENT_HELLO:
+ return {...output, ...this._clientHelloParser.parse(new Stream(content))};
+ case HandshakeType.SERVER_HELLO:
+ return {...output, ...this._serverHelloParser.parse(new Stream(content))};
+ case HandshakeType.NEW_SESSION_TICKET:
+ return {...output, ...this._newSessionTicketParser.parse(new Stream(content))};
+ case HandshakeType.CERTIFICATE:
+ return {...output, ...this._certificateParser.parse(new Stream(content))};
+ case HandshakeType.CERTIFICATE_REQUEST:
+ return {...output, ...this._certificateRequestParser.parse(new Stream(content))};
+ case HandshakeType.CERTIFICATE_VERIFY:
+ return {...output, ...this._certificateVerifyParser.parse(new Stream(content))};
+ default:
+ output.handshakeValue = "0x" + toHexFast(content);
+ }
+
+ return output;
+ }
+}
+
+/**
+ * Represents the known values of the msg_type field of a TLS Handshake message.
+ */
+const HandshakeType = Object.freeze({
+ HELLO_REQUEST: 0,
+ CLIENT_HELLO: 1,
+ SERVER_HELLO: 2,
+ NEW_SESSION_TICKET: 4,
+ CERTIFICATE: 11,
+ SERVER_KEY_EXCHANGE: 12,
+ CERTIFICATE_REQUEST: 13,
+ SERVER_HELLO_DONE: 14,
+ CERTIFICATE_VERIFY: 15,
+ CLIENT_KEY_EXCHANGE: 16,
+ FINISHED: 20,
+});
+
+/**
+ * Parses TLS Handshake ClientHello messages.
+ */
+class ClientHelloParser {
+
+ /**
+ * ClientHelloParser constructor.
+ */
+ constructor() {
+ this._extensionsParser = new ExtensionsParser();
+ }
+
+ /**
+ * Parses a single TLS Handshake ClientHello message.
+ *
+ * @param {Stream} input - Stream, containing a raw ClientHello message.
+ * @returns {Object} Object representation of ClientHello.
+ */
+ parse(input) {
+ const output = {};
+
+ output.clientVersion = this._readClientVersion(input);
+ output.random = this._readRandom(input);
+
+ const sessionID = this._readSessionID(input);
+ if (sessionID) {
+ output.sessionID = sessionID;
+ }
+
+ output.cipherSuites = this._readCipherSuites(input);
+ output.compressionMethods = this._readCompressionMethods(input);
+ output.extensions = this._readExtensions(input);
+
+ return output;
+ }
+
+ /**
+ * Reads the client_version field from the following bytes in the provided Stream.
+ *
+ * @param {Stream} input - Stream, containing a raw ClientHello message, with position before client_version field.
+ * @returns {string} Hex representation of client_version.
+ */
+ _readClientVersion(input) {
+ return readBytesAsHex(input, 2);
+ }
+
+ /**
+ * Reads the random field from the following bytes in the provided Stream.
+ *
+ * @param {Stream} input - Stream, containing a raw ClientHello message, with position before random field.
+ * @returns {string} Hex representation of random.
+ */
+ _readRandom(input) {
+ return readBytesAsHex(input, 32);
+ }
+
+ /**
+ * Reads the session_id field from the following bytes in the provided Stream.
+ *
+ * @param {Stream} input - Stream, containing a raw ClientHello message, with position before session_id length field.
+ * @returns {string} Hex representation of session_id, or empty string if session_id not present.
+ */
+ _readSessionID(input) {
+ return readSizePrefixedBytesAsHex(input, 1);
+ }
+
+ /**
+ * Reads the cipher_suites field from the following bytes in the provided Stream.
+ *
+ * @param {Stream} input - Stream, containing a raw ClientHello message, with position before cipher_suites length field.
+ * @returns {Object} Object represention of cipher_suites field.
+ */
+ _readCipherSuites(input) {
+ const output = {};
+
+ output.length = input.readInt(2);
+ if (!output.length) {
+ return {};
+ }
+
+ const cipherSuites = new Stream(input.getBytes(output.length));
+ if (cipherSuites.length < output.length) {
+ output.truncated = true;
+ }
+
+ output.values = [];
+
+ while (cipherSuites.hasMore()) {
+ const cipherSuite = readBytesAsHex(cipherSuites, 2);
+ if (cipherSuite) {
+ output.values.push(cipherSuite);
+ }
+ }
+
+ return output;
+ }
+
+ /**
+ * Reads the compression_methods field from the following bytes in the provided Stream.
+ *
+ * @param {Stream} input - Stream, containing a raw ClientHello message, with position before compression_methods length field.
+ * @returns {Object} Object representation of compression_methods field.
+ */
+ _readCompressionMethods(input) {
+ const output = {};
+
+ output.length = input.readInt(1);
+ if (!output.length) {
+ return {};
+ }
+
+ const compressionMethods = new Stream(input.getBytes(output.length));
+ if (compressionMethods.length < output.length) {
+ output.truncated = true;
+ }
+
+ output.values = [];
+
+ while (compressionMethods.hasMore()) {
+ const compressionMethod = readBytesAsHex(compressionMethods, 1);
+ if (compressionMethod) {
+ output.values.push(compressionMethod);
+ }
+ }
+
+ return output;
+ }
+
+ /**
+ * Reads the extensions field from the following bytes in the provided Stream.
+ *
+ * @param {Stream} input - Stream, containing a raw ClientHello message, with position before extensions length field.
+ * @returns {Object} Object representations of extensions field.
+ */
+ _readExtensions(input) {
+ const output = {};
+
+ output.length = input.readInt(2);
+ if (!output.length) {
+ return {};
+ }
+
+ const extensions = new Stream(input.getBytes(output.length));
+ if (extensions.length < output.length) {
+ output.truncated = true;
+ }
+
+ output.values = this._extensionsParser.parse(extensions);
+
+ return output;
+ }
+}
+
+/**
+ * Parses TLS Handshake ServeHello messages.
+ */
+class ServerHelloParser {
+
+ /**
+ * ServerHelloParser constructor.
+ */
+ constructor() {
+ this._extensionsParser = new ExtensionsParser();
+ }
+
+ /**
+ * Parses a single TLS Handshake ServerHello message.
+ *
+ * @param {Stream} input - Stream, containing a raw ServerHello message.
+ * @return {Object} Object representation of ServerHello.
+ */
+ parse(input) {
+ const output = {};
+
+ output.serverVersion = this._readServerVersion(input);
+ output.random = this._readRandom(input);
+
+ const sessionID = this._readSessionID(input);
+ if (sessionID) {
+ output.sessionID = sessionID;
+ }
+
+ output.cipherSuite = this._readCipherSuite(input);
+ output.compressionMethod = this._readCompressionMethod(input);
+ output.extensions = this._readExtensions(input);
+
+ return output;
+ }
+
+ /**
+ * Reads the server_version field from the following bytes in the provided Stream.
+ *
+ * @param {Stream} input - Stream, containing a raw ServerHello message, with position before server_version field.
+ * @returns {string} Hex representation of server_version.
+ */
+ _readServerVersion(input) {
+ return readBytesAsHex(input, 2);
+ }
+
+ /**
+ * Reads the random field from the following bytes in the provided Stream.
+ *
+ * @param {Stream} input - Stream, containing a raw ServerHello message, with position before random field.
+ * @returns {string} Hex representation of random.
+ */
+ _readRandom(input) {
+ return readBytesAsHex(input, 32);
+ }
+
+ /**
+ * Reads the session_id field from the following bytes in the provided Stream.
+ *
+ * @param {Stream} input - Stream, containing a raw ServertHello message, with position before session_id length field.
+ * @returns {string} Hex representation of session_id, or empty string if session_id not present.
+ */
+ _readSessionID(input) {
+ return readSizePrefixedBytesAsHex(input, 1);
+ }
+
+ /**
+ * Reads the cipher_suite field from the following bytes in the provided Stream.
+ *
+ * @param {Stream} input - Stream, containing a raw ServerHello message, with position before cipher_suite field.
+ * @returns {string} Hex represention of cipher_suite.
+ */
+ _readCipherSuite(input) {
+ return readBytesAsHex(input, 2);
+ }
+
+ /**
+ * Reads the compression_method field from the following bytes in the provided Stream.
+ *
+ * @param {Stream} input - Stream, containing a raw ServerHello message, with position before compression_method field.
+ * @returns {string} Hex represention of compression_method.
+ */
+ _readCompressionMethod(input) {
+ return readBytesAsHex(input, 1);
+ }
+
+ /**
+ * Reads the extensions field from the following bytes in the provided Stream.
+ *
+ * @param {Stream} input - Stream, containing a raw ServerHello message, with position before extensions length field.
+ * @returns {Object} Object representation of extensions field.
+ */
+ _readExtensions(input) {
+ const output = {};
+
+ output.length = input.readInt(2);
+ if (!output.length) {
+ return {};
+ }
+
+ const extensions = new Stream(input.getBytes(output.length));
+ if (extensions.length < output.length) {
+ output.truncated = true;
+ }
+
+ output.values = this._extensionsParser.parse(extensions);
+
+ return output;
+ }
+}
+
+/**
+ * Parses TLS Handshake Hello Extensions.
+ */
+class ExtensionsParser {
+
+ /**
+ * Parses a stream of TLS Handshake Hello Extensions.
+ *
+ * @param {Stream} input - Stream, containing multiple raw Extensions, with position before first extension length field.
+ * @returns {Object[]} Array of Object representations of Extensions contained within input.
+ */
+ parse(input) {
+ const output = [];
+
+ while (input.hasMore()) {
+ const extension = this._readExtension(input);
+ if (extension) {
+ output.push(extension);
+ }
+ }
+
+ return output;
+ }
+
+ /**
+ * Reads a single Extension from the following bytes in the provided Stream.
+ *
+ * @param {Stream} input - Stream, containing a list of Extensions, with position before the length field of the next Extension.
+ * @returns {Object} Object representation of Extension.
+ */
+ _readExtension(input) {
+ const output = {};
+
+ if (input.position + 4 > input.length) {
+ input.moveTo(input.length);
+ return null;
+ }
+
+ output.type = "0x" + toHexFast(input.getBytes(2));
+ output.length = input.readInt(2);
+ if (!output.length) {
+ return output;
+ }
+
+ const value = input.getBytes(output.length);
+ if (!value || value.length !== output.length) {
+ output.truncated = true;
+ }
+
+ if (value && value.length) {
+ output.value = "0x" + toHexFast(value);
+ }
+
+ return output;
+ }
+}
+
+/**
+ * Parses TLS Handshake NewSessionTicket messages.
+ */
+class NewSessionTicketParser {
+
+ /**
+ * Parses a single TLS Handshake NewSessionTicket message.
+ *
+ * @param {Stream} input - Stream, containing a raw NewSessionTicket message.
+ * @returns {Object} Object representation of NewSessionTicket.
+ */
+ parse(input) {
+ return {
+ ticketLifetimeHint: this._readTicketLifetimeHint(input),
+ ticket: this._readTicket(input),
+ };
+ }
+
+ /**
+ * Reads the ticket_lifetime_hint field from the following bytes in the provided Stream.
+ *
+ * @param {Stream} input - Stream, containing a raw NewSessionTicket message, with position before ticket_lifetime_hint field.
+ * @returns {string} Lifetime hint, in seconds.
+ */
+ _readTicketLifetimeHint(input) {
+ if (input.position + 4 > input.length) {
+ input.moveTo(input.length);
+ return "";
+ }
+
+ return input.readInt(4) + "s";
+ }
+
+ /**
+ * Reads the ticket field fromt the following bytes in the provided Stream.
+ *
+ * @param {Stream} input - Stream, containing a raw NewSessionTicket message, with position before ticket length field.
+ * @returns {string} Hex representation of ticket.
+ */
+ _readTicket(input) {
+ return readSizePrefixedBytesAsHex(input, 2);
+ }
+}
+
+/**
+ * Parses TLS Handshake Certificate messages.
+ */
+class CertificateParser {
+
+ /**
+ * Parses a single TLS Handshake Certificate message.
+ *
+ * @param {Stream} input - Stream, containing a raw Certificate message.
+ * @returns {Object} Object representation of Certificate.
+ */
+ parse(input) {
+ const output = {};
+
+ output.certificateList = this._readCertificateList(input);
+
+ return output;
+ }
+
+ /**
+ * Reads the certificate_list field from the following bytes in the provided Stream.
+ *
+ * @param {Stream} input - Stream, containing a raw Certificate message, with position before certificate_list length field.
+ * @returns {string[]} Array of strings, each containing a hex representation of a value within the certificate_list field.
+ */
+ _readCertificateList(input) {
+ const output = {};
+
+ if (input.position + 3 > input.length) {
+ input.moveTo(input.length);
+ return output;
+ }
+
+ output.length = input.readInt(3);
+ if (!output.length) {
+ return output;
+ }
+
+ const certificates = new Stream(input.getBytes(output.length));
+ if (certificates.length < output.length) {
+ output.truncated = true;
+ }
+
+ output.values = [];
+
+ while (certificates.hasMore()) {
+ const certificate = this._readCertificate(certificates);
+ if (certificate) {
+ output.values.push(certificate);
+ }
+ }
+
+ return output;
+ }
+
+ /**
+ * Reads a single certificate from the following bytes in the provided Stream.
+ *
+ * @param {Stream} input - Stream, containing a list of certificicates, with position before the length field of the next certificate.
+ * @returns {string} Hex representation of certificate.
+ */
+ _readCertificate(input) {
+ return readSizePrefixedBytesAsHex(input, 3);
+ }
+}
+
+/**
+ * Parses TLS Handshake CertificateRequest messages.
+ */
+class CertificateRequestParser {
+
+ /**
+ * Parses a single TLS Handshake CertificateRequest message.
+ *
+ * @param {Stream} input - Stream, containing a raw CertificateRequest message.
+ * @return {Object} Object representation of CertificateRequest.
+ */
+ parse(input) {
+ const output = {};
+
+ output.certificateTypes = this._readCertificateTypes(input);
+ output.supportedSignatureAlgorithms = this._readSupportedSignatureAlgorithms(input);
+
+ const certificateAuthorities = this._readCertificateAuthorities(input);
+ if (certificateAuthorities.length) {
+ output.certificateAuthorities = certificateAuthorities;
+ }
+
+ return output;
+ }
+
+ /**
+ * Reads the certificate_types field from the following bytes in the provided Stream.
+ *
+ * @param {Stream} input - Stream, containing a raw CertificateRequest message, with position before certificate_types length field.
+ * @return {string[]} Array of strings, each containing a hex representation of a value within the certificate_types field.
+ */
+ _readCertificateTypes(input) {
+ const output = {};
+
+ output.length = input.readInt(1);
+ if (!output.length) {
+ return {};
+ }
+
+ const certificateTypes = new Stream(input.getBytes(output.length));
+ if (certificateTypes.length < output.length) {
+ output.truncated = true;
+ }
+
+ output.values = [];
+
+ while (certificateTypes.hasMore()) {
+ const certificateType = readBytesAsHex(certificateTypes, 1);
+ if (certificateType) {
+ output.values.push(certificateType);
+ }
+ }
+
+ return output;
+ }
+
+ /**
+ * Reads the supported_signature_algorithms field from the following bytes in the provided Stream.
+ *
+ * @param {Stream} input - Stream, containing a raw CertificateRequest message, with position before supported_signature_algorithms length field.
+ * @returns {string[]} Array of strings, each containing a hex representation of a value within the supported_signature_algorithms field.
+ */
+ _readSupportedSignatureAlgorithms(input) {
+ const output = {};
+
+ output.length = input.readInt(2);
+ if (!output.length) {
+ return {};
+ }
+
+ const signatureAlgorithms = new Stream(input.getBytes(output.length));
+ if (signatureAlgorithms.length < output.length) {
+ output.truncated = true;
+ }
+
+ output.values = [];
+
+ while (signatureAlgorithms.hasMore()) {
+ const signatureAlgorithm = readBytesAsHex(signatureAlgorithms, 2);
+ if (signatureAlgorithm) {
+ output.values.push(signatureAlgorithm);
+ }
+ }
+
+ return output;
+ }
+
+ /**
+ * Reads the certificate_authorities field from the following bytes in the provided Stream.
+ *
+ * @param {Stream} input - Stream, containing a raw CertificateRequest message, with position before certificate_authorities length field.
+ * @returns {string[]} Array of strings, each containing a hex representation of a value within the certificate_authorities field.
+ */
+ _readCertificateAuthorities(input) {
+ const output = {};
+
+ output.length = input.readInt(2);
+ if (!output.length) {
+ return {};
+ }
+
+ const certificateAuthorities = new Stream(input.getBytes(output.length));
+ if (certificateAuthorities.length < output.length) {
+ output.truncated = true;
+ }
+
+ output.values = [];
+
+ while (certificateAuthorities.hasMore()) {
+ const certificateAuthority = this._readCertificateAuthority(certificateAuthorities);
+ if (certificateAuthority) {
+ output.values.push(certificateAuthority);
+ }
+ }
+
+ return output;
+ }
+
+ /**
+ * Reads a single certificate authority from the following bytes in the provided Stream.
+ *
+ * @param {Stream} input - Stream, containing a list of raw certificate authorities, with position before the length field of the next certificate authority.
+ * @returns {string} Hex representation of certificate authority.
+ */
+ _readCertificateAuthority(input) {
+ return readSizePrefixedBytesAsHex(input, 2);
+ }
+}
+
+/**
+ * Parses TLS Handshake CertificateVerify messages.
+ */
+class CertificateVerifyParser {
+
+ /**
+ * Parses a single CertificateVerify Message.
+ *
+ * @param {Stream} input - Stream, containing a raw CertificateVerify message.
+ * @returns {Object} Object representation of CertificateVerify.
+ */
+ parse(input) {
+ return {
+ algorithmHash: this._readAlgorithmHash(input),
+ algorithmSignature: this._readAlgorithmSignature(input),
+ signature: this._readSignature(input),
+ };
+ }
+
+ /**
+ * Reads the algorithm.hash field from the following bytes in the provided Stream.
+ *
+ * @param {Stream} input - Stream, containing a raw CertificateVerify message, with position before algorithm.hash field.
+ * @return {string} Hex representation of hash algorithm.
+ */
+ _readAlgorithmHash(input) {
+ return readBytesAsHex(input, 1);
+ }
+
+ /**
+ * Reads the algorithm.signature field from the following bytes in the provided Stream.
+ *
+ * @param {Stream} input - Stream, containing a raw CertificateVerify message, with position before algorithm.signature field.
+ * @return {string} Hex representation of signature algorithm.
+ */
+ _readAlgorithmSignature(input) {
+ return readBytesAsHex(input, 1);
+ }
+
+ /**
+ * Reads the signature field from the following bytes in the provided Stream.
+ *
+ * @param {Stream} input - Stream, containing a raw CertificateVerify message, with position before signature field.
+ * @return {string} Hex representation of signature.
+ */
+ _readSignature(input) {
+ return readSizePrefixedBytesAsHex(input, 2);
+ }
+}
+
+/**
+ * Read the following size prefixed bytes from the provided Stream, and reuturn as a hex string.
+ *
+ * @param {Stream} input - Stream to read from.
+ * @param {int} sizePrefixLength - Length of the size prefix field.
+ * @returns {string} Hex representation of bytes read from Stream, empty string is returned if
+ * field cannot be read in full.
+ */
+function readSizePrefixedBytesAsHex(input, sizePrefixLength) {
+ const length = input.readInt(sizePrefixLength);
+ if (!length) {
+ return "";
+ }
+
+ return readBytesAsHex(input, length);
+}
+
+/**
+ * Read n bytes from the provided Stream, and return as a hex string.
+ *
+ * @param {Stream} input - Stream to read from.
+ * @param {int} n - Number of bytes to read.
+ * @returns {string} Hex representation of bytes read from Stream, or empty string if field cannot
+ * be read in full.
+ */
+function readBytesAsHex(input, n) {
+ const bytes = input.getBytes(n);
+ if (!bytes || bytes.length !== n) {
+ return "";
+ }
+
+ return "0x" + toHexFast(bytes);
+}
diff --git a/src/core/operations/ParseX509CRL.mjs b/src/core/operations/ParseX509CRL.mjs
new file mode 100644
index 00000000..f498375d
--- /dev/null
+++ b/src/core/operations/ParseX509CRL.mjs
@@ -0,0 +1,391 @@
+/**
+ * @author robinsandhu
+ * @copyright Crown Copyright 2024
+ * @license Apache-2.0
+ */
+
+import r from "jsrsasign";
+import Operation from "../Operation.mjs";
+import { fromBase64 } from "../lib/Base64.mjs";
+import { toHex } from "../lib/Hex.mjs";
+import { formatDnObj } from "../lib/PublicKey.mjs";
+import OperationError from "../errors/OperationError.mjs";
+import Utils from "../Utils.mjs";
+
+/**
+ * Parse X.509 CRL operation
+ */
+class ParseX509CRL extends Operation {
+
+ /**
+ * ParseX509CRL constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "Parse X.509 CRL";
+ this.module = "PublicKey";
+ this.description = "Parse Certificate Revocation List (CRL)";
+ this.infoURL = "https://wikipedia.org/wiki/Certificate_revocation_list";
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [
+ {
+ "name": "Input format",
+ "type": "option",
+ "value": ["PEM", "DER Hex", "Base64", "Raw"]
+ }
+ ];
+ this.checks = [
+ {
+ "pattern": "^-+BEGIN X509 CRL-+\\r?\\n[\\da-z+/\\n\\r]+-+END X509 CRL-+\\r?\\n?$",
+ "flags": "i",
+ "args": ["PEM"]
+ }
+ ];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string} Human-readable description of a Certificate Revocation List (CRL).
+ */
+ run(input, args) {
+ if (!input.length) {
+ return "No input";
+ }
+
+ const inputFormat = args[0];
+
+ let undefinedInputFormat = false;
+ try {
+ switch (inputFormat) {
+ case "DER Hex":
+ input = input.replace(/\s/g, "").toLowerCase();
+ break;
+ case "PEM":
+ break;
+ case "Base64":
+ input = toHex(fromBase64(input, null, "byteArray"), "");
+ break;
+ case "Raw":
+ input = toHex(Utils.strToArrayBuffer(input), "");
+ break;
+ default:
+ undefinedInputFormat = true;
+ }
+ } catch (e) {
+ throw "Certificate load error (non-certificate input?)";
+ }
+ if (undefinedInputFormat) throw "Undefined input format";
+
+ const crl = new r.X509CRL(input);
+
+ let out = `Certificate Revocation List (CRL):
+ Version: ${crl.getVersion() === null ? "1 (0x0)" : "2 (0x1)"}
+ Signature Algorithm: ${crl.getSignatureAlgorithmField()}
+ Issuer:\n${formatDnObj(crl.getIssuer(), 8)}
+ Last Update: ${generalizedDateTimeToUTC(crl.getThisUpdate())}
+ Next Update: ${generalizedDateTimeToUTC(crl.getNextUpdate())}\n`;
+
+ if (crl.getParam().ext !== undefined) {
+ out += `\tCRL extensions:\n${formatCRLExtensions(crl.getParam().ext, 8)}\n`;
+ }
+
+ out += `Revoked Certificates:\n${formatRevokedCertificates(crl.getRevCertArray(), 4)}
+Signature Value:\n${formatCRLSignature(crl.getSignatureValueHex(), 8)}`;
+
+ return out;
+ }
+}
+
+/**
+ * Generalized date time string to UTC.
+ * @param {string} datetime
+ * @returns UTC datetime string.
+ */
+function generalizedDateTimeToUTC(datetime) {
+ // Ensure the string is in the correct format
+ if (!/^\d{12,14}Z$/.test(datetime)) {
+ throw new OperationError(`failed to format datetime string ${datetime}`);
+ }
+
+ // Extract components
+ let centuary = "20";
+ if (datetime.length === 15) {
+ centuary = datetime.substring(0, 2);
+ datetime = datetime.slice(2);
+ }
+ const year = centuary + datetime.substring(0, 2);
+ const month = datetime.substring(2, 4);
+ const day = datetime.substring(4, 6);
+ const hour = datetime.substring(6, 8);
+ const minute = datetime.substring(8, 10);
+ const second = datetime.substring(10, 12);
+
+ // Construct ISO 8601 format string
+ const isoString = `${year}-${month}-${day}T${hour}:${minute}:${second}Z`;
+
+ // Parse using standard Date object
+ const isoDateTime = new Date(isoString);
+
+ return isoDateTime.toUTCString();
+}
+
+/**
+ * Format CRL extensions.
+ * @param {r.ExtParam[] | undefined} extensions
+ * @param {Number} indent
+ * @returns Formatted string detailing CRL extensions.
+ */
+function formatCRLExtensions(extensions, indent) {
+ if (Array.isArray(extensions) === false || extensions.length === 0) {
+ return indentString(`No CRL extensions.`, indent);
+ }
+
+ let out = ``;
+
+ extensions.sort((a, b) => {
+ if (!Object.hasOwn(a, "extname") || !Object.hasOwn(b, "extname")) {
+ return 0;
+ }
+ if (a.extname < b.extname) {
+ return -1;
+ } else if (a.extname === b.extname) {
+ return 0;
+ } else {
+ return 1;
+ }
+ });
+
+ extensions.forEach((ext) => {
+ if (!Object.hasOwn(ext, "extname")) {
+ throw new OperationError(`CRL entry extension object missing 'extname' key: ${ext}`);
+ }
+ switch (ext.extname) {
+ case "authorityKeyIdentifier":
+ out += `X509v3 Authority Key Identifier:\n`;
+ if (Object.hasOwn(ext, "kid")) {
+ out += `\tkeyid:${colonDelimitedHexFormatString(ext.kid.hex.toUpperCase())}\n`;
+ }
+ if (Object.hasOwn(ext, "issuer")) {
+ out += `\tDirName:${ext.issuer.str}\n`;
+ }
+ if (Object.hasOwn(ext, "sn")) {
+ out += `\tserial:${colonDelimitedHexFormatString(ext.sn.hex.toUpperCase())}\n`;
+ }
+ break;
+ case "cRLDistributionPoints":
+ out += `X509v3 CRL Distribution Points:\n`;
+ ext.array.forEach((distPoint) => {
+ const fullName = `Full Name:\n${formatGeneralNames(distPoint.dpname.full, 4)}`;
+ out += indentString(fullName, 4) + "\n";
+ });
+ break;
+ case "cRLNumber":
+ if (!Object.hasOwn(ext, "num")) {
+ throw new OperationError(`'cRLNumber' CRL entry extension missing 'num' key: ${ext}`);
+ }
+ out += `X509v3 CRL Number:\n\t${ext.num.hex.toUpperCase()}\n`;
+ break;
+ case "issuerAltName":
+ out += `X509v3 Issuer Alternative Name:\n${formatGeneralNames(ext.array, 4)}\n`;
+ break;
+ default:
+ out += `${ext.extname}:\n`;
+ out += `\tUnsupported CRL extension. Try openssl CLI.\n`;
+ break;
+ }
+ });
+
+ return indentString(chop(out), indent);
+}
+
+/**
+ * Format general names array.
+ * @param {Object[]} names
+ * @returns Multi-line formatted string describing all supported general name types.
+ */
+function formatGeneralNames(names, indent) {
+ let out = ``;
+
+ names.forEach((name) => {
+ const key = Object.keys(name)[0];
+
+ switch (key) {
+ case "ip":
+ out += `IP:${name.ip}\n`;
+ break;
+ case "dns":
+ out += `DNS:${name.dns}\n`;
+ break;
+ case "uri":
+ out += `URI:${name.uri}\n`;
+ break;
+ case "rfc822":
+ out += `EMAIL:${name.rfc822}\n`;
+ break;
+ case "dn":
+ out += `DIR:${name.dn.str}\n`;
+ break;
+ case "other":
+ out += `OtherName:${name.other.oid}::${Object.values(name.other.value)[0].str}\n`;
+ break;
+ default:
+ out += `${key}: unsupported general name type`;
+ break;
+ }
+ });
+
+ return indentString(chop(out), indent);
+}
+
+/**
+ * Colon-delimited hex formatted output.
+ * @param {string} hexString Hex String
+ * @returns String representing input hex string with colon delimiter.
+ */
+function colonDelimitedHexFormatString(hexString) {
+ if (hexString.length % 2 !== 0) {
+ hexString = "0" + hexString;
+ }
+
+ return chop(hexString.replace(/(..)/g, "$&:"));
+}
+
+/**
+ * Format revoked certificates array
+ * @param {r.RevokedCertificate[] | null} revokedCertificates
+ * @param {Number} indent
+ * @returns Multi-line formatted string output of revoked certificates array
+ */
+function formatRevokedCertificates(revokedCertificates, indent) {
+ if (Array.isArray(revokedCertificates) === false || revokedCertificates.length === 0) {
+ return indentString("No Revoked Certificates.", indent);
+ }
+
+ let out=``;
+
+ revokedCertificates.forEach((revCert) => {
+ if (!Object.hasOwn(revCert, "sn") || !Object.hasOwn(revCert, "date")) {
+ throw new OperationError("invalid revoked certificate object, missing either serial number or date");
+ }
+
+ out += `Serial Number: ${revCert.sn.hex.toUpperCase()}
+ Revocation Date: ${generalizedDateTimeToUTC(revCert.date)}\n`;
+ if (Object.hasOwn(revCert, "ext") && Array.isArray(revCert.ext) && revCert.ext.length !== 0) {
+ out += `\tCRL entry extensions:\n${indentString(formatCRLEntryExtensions(revCert.ext), 2*indent)}\n`;
+ }
+ });
+
+ return indentString(chop(out), indent);
+}
+
+/**
+ * Format CRL entry extensions.
+ * @param {Object[]} exts
+ * @returns Formatted multi-line string describing CRL entry extensions.
+ */
+function formatCRLEntryExtensions(exts) {
+ let out = ``;
+
+ const crlReasonCodeToReasonMessage = {
+ 0: "Unspecified",
+ 1: "Key Compromise",
+ 2: "CA Compromise",
+ 3: "Affiliation Changed",
+ 4: "Superseded",
+ 5: "Cessation Of Operation",
+ 6: "Certificate Hold",
+ 8: "Remove From CRL",
+ 9: "Privilege Withdrawn",
+ 10: "AA Compromise",
+ };
+
+ const holdInstructionOIDToName = {
+ "1.2.840.10040.2.1": "Hold Instruction None",
+ "1.2.840.10040.2.2": "Hold Instruction Call Issuer",
+ "1.2.840.10040.2.3": "Hold Instruction Reject",
+ };
+
+ exts.forEach((ext) => {
+ if (!Object.hasOwn(ext, "extname")) {
+ throw new OperationError(`CRL entry extension object missing 'extname' key: ${ext}`);
+ }
+ switch (ext.extname) {
+ case "cRLReason":
+ if (!Object.hasOwn(ext, "code")) {
+ throw new OperationError(`'cRLReason' CRL entry extension missing 'code' key: ${ext}`);
+ }
+ out += `X509v3 CRL Reason Code:
+ ${Object.hasOwn(crlReasonCodeToReasonMessage, ext.code) ? crlReasonCodeToReasonMessage[ext.code] : `invalid reason code: ${ext.code}`}\n`;
+ break;
+ case "2.5.29.23": // Hold instruction
+ out += `Hold Instruction Code:\n\t${Object.hasOwn(holdInstructionOIDToName, ext.extn.oid) ? holdInstructionOIDToName[ext.extn.oid] : `${ext.extn.oid}: unknown hold instruction OID`}\n`;
+ break;
+ case "2.5.29.24": // Invalidity Date
+ out += `Invalidity Date:\n\t${generalizedDateTimeToUTC(ext.extn.gentime.str)}\n`;
+ break;
+ default:
+ out += `${ext.extname}:\n`;
+ out += `\tUnsupported CRL entry extension. Try openssl CLI.\n`;
+ break;
+ }
+ });
+
+ return chop(out);
+}
+
+/**
+ * Format CRL signature.
+ * @param {String} sigHex
+ * @param {Number} indent
+ * @returns String representing hex signature value formatted on multiple lines.
+ */
+function formatCRLSignature(sigHex, indent) {
+ if (sigHex.length % 2 !== 0) {
+ sigHex = "0" + sigHex;
+ }
+
+ return indentString(formatMultiLine(chop(sigHex.replace(/(..)/g, "$&:"))), indent);
+}
+
+/**
+ * Format string onto multiple lines.
+ * @param {string} longStr
+ * @returns String as a multi-line string.
+ */
+function formatMultiLine(longStr) {
+ const lines = [];
+
+ for (let remain = longStr ; remain !== "" ; remain = remain.substring(54)) {
+ lines.push(remain.substring(0, 54));
+ }
+
+ return lines.join("\n");
+}
+
+/**
+ * Indent a multi-line string by n spaces.
+ * @param {string} input String
+ * @param {number} spaces How many leading spaces
+ * @returns Indented string.
+ */
+function indentString(input, spaces) {
+ const indent = " ".repeat(spaces);
+ return input.replace(/^/gm, indent);
+}
+
+/**
+ * Remove last character from a string.
+ * @param {string} s String
+ * @returns Chopped string.
+ */
+function chop(s) {
+ if (s.length < 1) {
+ return s;
+ }
+ return s.substring(0, s.length - 1);
+}
+
+export default ParseX509CRL;
diff --git a/src/core/operations/ParseX509Certificate.mjs b/src/core/operations/ParseX509Certificate.mjs
index 11e63424..cdd1e9c7 100644
--- a/src/core/operations/ParseX509Certificate.mjs
+++ b/src/core/operations/ParseX509Certificate.mjs
@@ -6,7 +6,8 @@
import r from "jsrsasign";
import { fromBase64 } from "../lib/Base64.mjs";
-import { toHex } from "../lib/Hex.mjs";
+import { runHash } from "../lib/Hash.mjs";
+import { fromHex, toHex } from "../lib/Hex.mjs";
import { formatByteStr, formatDnObj } from "../lib/PublicKey.mjs";
import Operation from "../Operation.mjs";
import Utils from "../Utils.mjs";
@@ -81,7 +82,8 @@ class ParseX509Certificate extends Operation {
}
if (undefinedInputFormat) throw "Undefined input format";
- const sn = cert.getSerialNumberHex(),
+ const hex = Utils.strToArrayBuffer(Utils.byteArrayToChars(fromHex(cert.hex))),
+ sn = cert.getSerialNumberHex(),
issuer = cert.getIssuer(),
subject = cert.getSubject(),
pk = cert.getPublicKey(),
@@ -191,6 +193,10 @@ Issuer
${issuerStr}
Subject
${subjectStr}
+Fingerprints
+ MD5: ${runHash("md5", hex)}
+ SHA1: ${runHash("sha1", hex)}
+ SHA256: ${runHash("sha256", hex)}
Public Key
${pkStr.slice(0, -1)}
Certificate Signature
diff --git a/src/core/operations/PseudoRandomIntegerGenerator.mjs b/src/core/operations/PseudoRandomIntegerGenerator.mjs
new file mode 100644
index 00000000..a2d83a92
--- /dev/null
+++ b/src/core/operations/PseudoRandomIntegerGenerator.mjs
@@ -0,0 +1,164 @@
+/**
+ * @author cktgh [chankaitung@gmail.com]
+ * @copyright Crown Copyright 2026
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+import OperationError from "../errors/OperationError.mjs";
+import forge from "node-forge";
+import Utils, { isWorkerEnvironment } from "../Utils.mjs";
+import { DELIM_OPTIONS } from "../lib/Delim.mjs";
+
+/**
+ * Pseudo-Random Integer Generator operation
+ */
+class PseudoRandomIntegerGenerator extends Operation {
+
+ // in theory 2**53 is the max range, but we use Number.MAX_SAFE_INTEGER (2**53 - 1) as it is more consistent.
+ static MAX_RANGE = Number.MAX_SAFE_INTEGER;
+ // arbitrary choice
+ static BUFFER_SIZE = 1024;
+
+ /**
+ * PseudoRandomIntegerGenerator constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "Pseudo-Random Integer Generator";
+ this.module = "Ciphers";
+ this.description = "A cryptographically-secure pseudo-random number generator (PRNG).
Generates random integers within a specified range using the browser's built-in crypto.getRandomValues() method if available.
The supported range of integers is from -(2^53 - 1) to (2^53 - 1).";
+ this.infoURL = "https://wikipedia.org/wiki/Pseudorandom_number_generator";
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [
+ {
+ "name": "Number of Integers",
+ "type": "number",
+ "value": 1,
+ "min": 1
+ },
+ {
+ "name": "Min Value",
+ "type": "number",
+ "value": 0,
+ "min": Number.MIN_SAFE_INTEGER,
+ "max": Number.MAX_SAFE_INTEGER
+ },
+ {
+ "name": "Max Value",
+ "type": "number",
+ "value": 99,
+ "min": Number.MIN_SAFE_INTEGER,
+ "max": Number.MAX_SAFE_INTEGER
+ },
+ {
+ "name": "Delimiter",
+ "type": "option",
+ "value": DELIM_OPTIONS
+ },
+ {
+ "name": "Output",
+ "type": "option",
+ "value": ["Raw", "Hex", "Decimal"]
+ }
+ ];
+
+ // not using BigUint64Array to avoid BigInt handling overhead
+ this.randomBuffer = new Uint32Array(PseudoRandomIntegerGenerator.BUFFER_SIZE);
+ this.randomBufferOffset = PseudoRandomIntegerGenerator.BUFFER_SIZE;
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ const [numInts, minInt, maxInt, delimiter, outputType] = args;
+
+ if (minInt === null || maxInt === null) return "";
+
+ const min = Math.ceil(minInt);
+ const max = Math.floor(maxInt);
+ const delim = Utils.charRep(delimiter || "Space");
+
+ if (!Number.isSafeInteger(min) || !Number.isSafeInteger(max)) {
+ throw new OperationError("Min and Max must be between `-(2^53 - 1)` and `2^53 - 1`.");
+ }
+ if (min > max) {
+ throw new OperationError("Min cannot be larger than Max.");
+ }
+ const range = max - min + 1; // inclusive range
+ if (range > PseudoRandomIntegerGenerator.MAX_RANGE) {
+ throw new OperationError("Range between Min and Max cannot be larger than `2^53`");
+ }
+
+ // as large as possible while divisible by range
+ const rejectionThreshold = PseudoRandomIntegerGenerator.MAX_RANGE - (PseudoRandomIntegerGenerator.MAX_RANGE % range);
+ const output = [];
+ for (let i = 0; i < numInts; i++) {
+ const result = this._generateRandomValue(rejectionThreshold);
+ const intValue = min + (result % range);
+
+ switch (outputType) {
+ case "Hex":
+ output.push(intValue.toString(16));
+ break;
+ case "Decimal":
+ output.push(intValue.toString(10));
+ break;
+ case "Raw":
+ default:
+ output.push(Utils.chr(intValue));
+ }
+ }
+
+ if (outputType === "Raw") {
+ return output.join("");
+ }
+ return output.join(delim);
+ }
+
+ /**
+ * Generate a random value, result will be less than the rejection threshold (exclusive).
+ *
+ * @param {number} rejectionThreshold
+ * @returns {number}
+ */
+ _generateRandomValue(rejectionThreshold) {
+ let result;
+ do {
+ if (this.randomBufferOffset + 2 > this.randomBuffer.length) {
+ this._resetRandomBuffer();
+ }
+ // stitching a 53 bit number; not using BigUint64Array to avoid BigInt handling overhead
+ result = (this.randomBuffer[this.randomBufferOffset++] & 0x1f_ffff) * 0x1_0000_0000 +
+ this.randomBuffer[this.randomBufferOffset++];
+ } while (result >= rejectionThreshold);
+
+ return result;
+ }
+
+ /**
+ * Fill random buffer with new random values and rseet the offset.
+ */
+ _resetRandomBuffer() {
+ if (isWorkerEnvironment() && self.crypto) {
+ self.crypto.getRandomValues(this.randomBuffer);
+ } else {
+ const bytes = forge.random.getBytesSync(this.randomBuffer.length * 4);
+ for (let j = 0; j < this.randomBuffer.length; j++) {
+ this.randomBuffer[j] = (bytes.charCodeAt(j * 4) << 24) |
+ (bytes.charCodeAt(j * 4 + 1) << 16) |
+ (bytes.charCodeAt(j * 4 + 2) << 8) |
+ bytes.charCodeAt(j * 4 + 3);
+ }
+ }
+ this.randomBufferOffset = 0;
+ }
+
+}
+
+export default PseudoRandomIntegerGenerator;
diff --git a/src/core/operations/PubKeyFromCert.mjs b/src/core/operations/PubKeyFromCert.mjs
new file mode 100644
index 00000000..0233b04a
--- /dev/null
+++ b/src/core/operations/PubKeyFromCert.mjs
@@ -0,0 +1,68 @@
+/**
+ * @author cplussharp
+ * @copyright Crown Copyright 2023
+ * @license Apache-2.0
+ */
+
+import r from "jsrsasign";
+import Operation from "../Operation.mjs";
+import OperationError from "../errors/OperationError.mjs";
+
+/**
+ * Public Key from Certificate operation
+ */
+class PubKeyFromCert extends Operation {
+
+ /**
+ * PubKeyFromCert constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "Public Key from Certificate";
+ this.module = "PublicKey";
+ this.description = "Extracts the Public Key from a Certificate.";
+ this.infoURL = "https://en.wikipedia.org/wiki/X.509";
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [];
+ this.checks = [];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ let output = "";
+ let match;
+ const regex = /-----BEGIN CERTIFICATE-----/g;
+ while ((match = regex.exec(input)) !== null) {
+ // find corresponding end tag
+ const indexBase64 = match.index + match[0].length;
+ const footer = "-----END CERTIFICATE-----";
+ const indexFooter = input.indexOf(footer, indexBase64);
+ if (indexFooter === -1) {
+ throw new OperationError(`PEM footer '${footer}' not found`);
+ }
+
+ const certPem = input.substring(match.index, indexFooter + footer.length);
+ const cert = new r.X509();
+ cert.readCertPEM(certPem);
+ let pubKey;
+ try {
+ pubKey = cert.getPublicKey();
+ } catch {
+ throw new OperationError("Unsupported public key type");
+ }
+ const pubKeyPem = r.KEYUTIL.getPEM(pubKey);
+
+ // PEM ends with '\n', so a new key always starts on a new line
+ output += pubKeyPem;
+ }
+ return output;
+ }
+}
+
+export default PubKeyFromCert;
diff --git a/src/core/operations/PubKeyFromPrivKey.mjs b/src/core/operations/PubKeyFromPrivKey.mjs
new file mode 100644
index 00000000..5a08882b
--- /dev/null
+++ b/src/core/operations/PubKeyFromPrivKey.mjs
@@ -0,0 +1,82 @@
+/**
+ * @author cplussharp
+ * @copyright Crown Copyright 2023
+ * @license Apache-2.0
+ */
+
+import r from "jsrsasign";
+import Operation from "../Operation.mjs";
+import OperationError from "../errors/OperationError.mjs";
+
+/**
+ * Public Key from Private Key operation
+ */
+class PubKeyFromPrivKey extends Operation {
+
+ /**
+ * PubKeyFromPrivKey constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "Public Key from Private Key";
+ this.module = "PublicKey";
+ this.description = "Extracts the Public Key from a Private Key.";
+ this.infoURL = "https://en.wikipedia.org/wiki/PKCS_8";
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [];
+ this.checks = [];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ let output = "";
+ let match;
+ const regex = /-----BEGIN ((RSA |EC |DSA )?PRIVATE KEY)-----/g;
+ while ((match = regex.exec(input)) !== null) {
+ // find corresponding end tag
+ const indexBase64 = match.index + match[0].length;
+ const footer = `-----END ${match[1]}-----`;
+ const indexFooter = input.indexOf(footer, indexBase64);
+ if (indexFooter === -1) {
+ throw new OperationError(`PEM footer '${footer}' not found`);
+ }
+
+ const privKeyPem = input.substring(match.index, indexFooter + footer.length);
+ let privKey;
+ try {
+ privKey = r.KEYUTIL.getKey(privKeyPem);
+ } catch (err) {
+ throw new OperationError(`Unsupported key type: ${err}`);
+ }
+ let pubKey;
+ if (privKey.type && privKey.type === "EC") {
+ pubKey = new r.KJUR.crypto.ECDSA({ curve: privKey.curve });
+ pubKey.setPublicKeyHex(privKey.generatePublicKeyHex());
+ } else if (privKey.type && privKey.type === "DSA") {
+ if (!privKey.y) {
+ throw new OperationError(`DSA Private Key in PKCS#8 is not supported`);
+ }
+ pubKey = new r.KJUR.crypto.DSA();
+ pubKey.setPublic(privKey.p, privKey.q, privKey.g, privKey.y);
+ } else if (privKey.n && privKey.e) {
+ pubKey = new r.RSAKey();
+ pubKey.setPublic(privKey.n, privKey.e);
+ } else {
+ throw new OperationError(`Unsupported key type`);
+ }
+ const pubKeyPem = r.KEYUTIL.getPEM(pubKey);
+
+ // PEM ends with '\n', so a new key always starts on a new line
+ output += pubKeyPem;
+ }
+ return output;
+ }
+}
+
+export default PubKeyFromPrivKey;
diff --git a/src/core/operations/RAKE.mjs b/src/core/operations/RAKE.mjs
index d1165b51..1470f5f0 100644
--- a/src/core/operations/RAKE.mjs
+++ b/src/core/operations/RAKE.mjs
@@ -101,22 +101,17 @@ class RAKE extends Operation {
phrases = phrases.filter(subArray => subArray.length > 0);
// Remove duplicate phrases
- const uniquePhrases = [...new Set(phrases.map(function (phrase) {
- return phrase.join(" ");
- }))];
- phrases = uniquePhrases.map(function (phrase) {
- return phrase.split(" ");
- });
+ phrases = phrases.unique();
// Generate word_degree_matrix and populate
- const wordDegreeMatrix = Array.from(Array(tokens.length), _ => Array(tokens.length).fill(0));
- phrases.forEach(function (phrase) {
- phrase.forEach(function (word1) {
- phrase.forEach(function (word2) {
+ const wordDegreeMatrix = Array(tokens.length).fill().map(() => Array(tokens.length).fill(0));
+ for (const phrase of phrases) {
+ for (const word1 of phrase) {
+ for (const word2 of phrase) {
wordDegreeMatrix[tokens.indexOf(word1)][tokens.indexOf(word2)]++;
- });
- });
- });
+ }
+ }
+ }
// Calculate degree score for each token
const degreeScores = Array(tokens.length).fill(0);
diff --git a/src/core/operations/RC6Decrypt.mjs b/src/core/operations/RC6Decrypt.mjs
new file mode 100644
index 00000000..3185b632
--- /dev/null
+++ b/src/core/operations/RC6Decrypt.mjs
@@ -0,0 +1,119 @@
+/**
+ * @author Medjedtxm
+ * @copyright Crown Copyright 2026
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+import Utils from "../Utils.mjs";
+import OperationError from "../errors/OperationError.mjs";
+import { toHex } from "../lib/Hex.mjs";
+import { decryptRC6, getBlockSize, getDefaultRounds } from "../lib/RC6.mjs";
+
+/**
+ * RC6 Decrypt operation
+ */
+class RC6Decrypt extends Operation {
+
+ /**
+ * RC6Decrypt constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "RC6 Decrypt";
+ this.module = "Ciphers";
+ this.description = "RC6 is a symmetric key block cipher derived from RC5. It was designed by Ron Rivest, Matt Robshaw, Ray Sidney, and Yiqun Lisa Yin to meet the requirements of the AES competition, and was one of the five finalists.
RC6 is parameterised as RC6-w/r/b where w is word size in bits (any multiple of 8 from 8-256), r is the number of rounds (1-255), and b is the key length in bytes. The standard AES submission uses w=32, r=20. Common word sizes: 8, 16, 32 (standard), 64, 128.
IV: The Initialisation Vector should be 4*w/8 bytes (e.g. 16 bytes for w=32). If not entered, it will default to null bytes.
Padding: In CBC and ECB mode, the PKCS#7 padding scheme is used.";
+ this.infoURL = "https://wikipedia.org/wiki/RC6";
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [
+ {
+ "name": "Key",
+ "type": "toggleString",
+ "value": "",
+ "toggleValues": ["Hex", "UTF8", "Latin1", "Base64"]
+ },
+ {
+ "name": "IV",
+ "type": "toggleString",
+ "value": "",
+ "toggleValues": ["Hex", "UTF8", "Latin1", "Base64"]
+ },
+ {
+ "name": "Mode",
+ "type": "option",
+ "value": ["CBC", "CFB", "OFB", "CTR", "ECB"]
+ },
+ {
+ "name": "Input",
+ "type": "option",
+ "value": ["Hex", "Raw"]
+ },
+ {
+ "name": "Output",
+ "type": "option",
+ "value": ["Raw", "Hex"]
+ },
+ {
+ "name": "Padding",
+ "type": "option",
+ "value": ["PKCS5", "NO", "ZERO", "RANDOM", "BIT"]
+ },
+ {
+ "name": "Word Size",
+ "type": "number",
+ "value": 32,
+ "min": 8,
+ "max": 256,
+ "step": 8
+ },
+ {
+ "name": "Rounds",
+ "type": "number",
+ "value": 20,
+ "min": 1,
+ "max": 255
+ }
+ ];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ const key = Utils.convertToByteArray(args[0].string, args[0].option),
+ iv = Utils.convertToByteArray(args[1].string, args[1].option),
+ [,, mode, inputType, outputType, padding, wordSize, rounds] = args;
+
+ // Validate word size
+ if (!Number.isInteger(wordSize) || wordSize < 8 || wordSize > 256 || wordSize % 8 !== 0)
+ throw new OperationError(`Invalid word size: ${wordSize}. Must be a multiple of 8 between 8 and 256.`);
+
+ const blockSize = getBlockSize(wordSize);
+ const defaultRounds = getDefaultRounds(wordSize);
+
+ if (iv.length !== blockSize && iv.length !== 0 && mode !== "ECB")
+ throw new OperationError(`Invalid IV length: ${iv.length} bytes
+
+RC6-${wordSize} uses an IV length of ${blockSize} bytes (${blockSize * 8} bits).
+Make sure you have specified the type correctly (e.g. Hex vs UTF8).`);
+
+ if (!Number.isInteger(rounds) || rounds < 1 || rounds > 255)
+ throw new OperationError(`Invalid number of rounds: ${rounds}
+
+Rounds must be an integer between 1 and 255. Standard for w=${wordSize} is ${defaultRounds}.`);
+
+ // Default IV to null bytes if empty (like AES)
+ const actualIv = iv.length === 0 ? new Array(blockSize).fill(0) : iv;
+
+ input = Utils.convertToByteArray(input, inputType);
+ const output = decryptRC6(input, key, actualIv, mode, padding, rounds, wordSize);
+ return outputType === "Hex" ? toHex(output, "") : Utils.byteArrayToUtf8(output);
+ }
+
+}
+
+export default RC6Decrypt;
diff --git a/src/core/operations/RC6Encrypt.mjs b/src/core/operations/RC6Encrypt.mjs
new file mode 100644
index 00000000..d5ad6d7f
--- /dev/null
+++ b/src/core/operations/RC6Encrypt.mjs
@@ -0,0 +1,119 @@
+/**
+ * @author Medjedtxm
+ * @copyright Crown Copyright 2026
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+import Utils from "../Utils.mjs";
+import OperationError from "../errors/OperationError.mjs";
+import { toHex } from "../lib/Hex.mjs";
+import { encryptRC6, getBlockSize, getDefaultRounds } from "../lib/RC6.mjs";
+
+/**
+ * RC6 Encrypt operation
+ */
+class RC6Encrypt extends Operation {
+
+ /**
+ * RC6Encrypt constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "RC6 Encrypt";
+ this.module = "Ciphers";
+ this.description = "RC6 is a symmetric key block cipher derived from RC5. It was designed by Ron Rivest, Matt Robshaw, Ray Sidney, and Yiqun Lisa Yin to meet the requirements of the AES competition, and was one of the five finalists.
RC6 is parameterised as RC6-w/r/b where w is word size in bits (any multiple of 8 from 8-256), r is the number of rounds (1-255), and b is the key length in bytes. The standard AES submission uses w=32, r=20. Common word sizes: 8, 16, 32 (standard), 64, 128.
IV: The Initialisation Vector should be 4*w/8 bytes (e.g. 16 bytes for w=32). If not entered, it will default to null bytes.
Padding: In CBC and ECB mode, the PKCS#7 padding scheme is used.";
+ this.infoURL = "https://wikipedia.org/wiki/RC6";
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [
+ {
+ "name": "Key",
+ "type": "toggleString",
+ "value": "",
+ "toggleValues": ["Hex", "UTF8", "Latin1", "Base64"]
+ },
+ {
+ "name": "IV",
+ "type": "toggleString",
+ "value": "",
+ "toggleValues": ["Hex", "UTF8", "Latin1", "Base64"]
+ },
+ {
+ "name": "Mode",
+ "type": "option",
+ "value": ["CBC", "CFB", "OFB", "CTR", "ECB"]
+ },
+ {
+ "name": "Input",
+ "type": "option",
+ "value": ["Raw", "Hex"]
+ },
+ {
+ "name": "Output",
+ "type": "option",
+ "value": ["Hex", "Raw"]
+ },
+ {
+ "name": "Padding",
+ "type": "option",
+ "value": ["PKCS5", "NO", "ZERO", "RANDOM", "BIT"]
+ },
+ {
+ "name": "Word Size",
+ "type": "number",
+ "value": 32,
+ "min": 8,
+ "max": 256,
+ "step": 8
+ },
+ {
+ "name": "Rounds",
+ "type": "number",
+ "value": 20,
+ "min": 1,
+ "max": 255
+ }
+ ];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ const key = Utils.convertToByteArray(args[0].string, args[0].option),
+ iv = Utils.convertToByteArray(args[1].string, args[1].option),
+ [,, mode, inputType, outputType, padding, wordSize, rounds] = args;
+
+ // Validate word size
+ if (!Number.isInteger(wordSize) || wordSize < 8 || wordSize > 256 || wordSize % 8 !== 0)
+ throw new OperationError(`Invalid word size: ${wordSize}. Must be a multiple of 8 between 8 and 256.`);
+
+ const blockSize = getBlockSize(wordSize);
+ const defaultRounds = getDefaultRounds(wordSize);
+
+ if (iv.length !== blockSize && iv.length !== 0 && mode !== "ECB")
+ throw new OperationError(`Invalid IV length: ${iv.length} bytes
+
+RC6-${wordSize} uses an IV length of ${blockSize} bytes (${blockSize * 8} bits).
+Make sure you have specified the type correctly (e.g. Hex vs UTF8).`);
+
+ if (!Number.isInteger(rounds) || rounds < 1 || rounds > 255)
+ throw new OperationError(`Invalid number of rounds: ${rounds}
+
+Rounds must be an integer between 1 and 255. Standard for w=${wordSize} is ${defaultRounds}.`);
+
+ // Default IV to null bytes if empty (like AES)
+ const actualIv = iv.length === 0 ? new Array(blockSize).fill(0) : iv;
+
+ input = Utils.convertToByteArray(input, inputType);
+ const output = encryptRC6(input, key, actualIv, mode, padding, rounds, wordSize);
+ return outputType === "Hex" ? toHex(output, "") : Utils.byteArrayToUtf8(output);
+ }
+
+}
+
+export default RC6Encrypt;
diff --git a/src/core/operations/ROT13.mjs b/src/core/operations/ROT13.mjs
index 1d059565..beec94a4 100644
--- a/src/core/operations/ROT13.mjs
+++ b/src/core/operations/ROT13.mjs
@@ -59,15 +59,16 @@ class ROT13 extends Operation {
rot13Upperacse = args[1],
rotNumbers = args[2];
let amount = args[3],
- chr;
+ amountNumbers = args[3];
if (amount) {
if (amount < 0) {
amount = 26 - (Math.abs(amount) % 26);
+ amountNumbers = 10 - (Math.abs(amountNumbers) % 10);
}
for (let i = 0; i < input.length; i++) {
- chr = input[i];
+ let chr = input[i];
if (rot13Upperacse && chr >= 65 && chr <= 90) { // Upper case
chr = (chr - 65 + amount) % 26;
output[i] = chr + 65;
@@ -75,7 +76,7 @@ class ROT13 extends Operation {
chr = (chr - 97 + amount) % 26;
output[i] = chr + 97;
} else if (rotNumbers && chr >= 48 && chr <= 57) { // Numbers
- chr = (chr - 48 + amount) % 10;
+ chr = (chr - 48 + amountNumbers) % 10;
output[i] = chr + 48;
}
}
diff --git a/src/core/operations/RSASign.mjs b/src/core/operations/RSASign.mjs
index 25160f53..5091549f 100644
--- a/src/core/operations/RSASign.mjs
+++ b/src/core/operations/RSASign.mjs
@@ -60,7 +60,7 @@ class RSASign extends Operation {
const privateKey = forge.pki.decryptRsaPrivateKey(key, password);
// Generate message hash
const md = MD_ALGORITHMS[mdAlgo].create();
- md.update(input, "utf8");
+ md.update(input, "raw");
// Sign message hash
const sig = privateKey.sign(md);
return sig;
diff --git a/src/core/operations/RSAVerify.mjs b/src/core/operations/RSAVerify.mjs
index 89b7d81f..8160438c 100644
--- a/src/core/operations/RSAVerify.mjs
+++ b/src/core/operations/RSAVerify.mjs
@@ -8,6 +8,7 @@ import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs";
import forge from "node-forge";
import { MD_ALGORITHMS } from "../lib/RSA.mjs";
+import Utils from "../Utils.mjs";
/**
* RSA Verify operation
@@ -37,6 +38,11 @@ class RSAVerify extends Operation {
type: "text",
value: ""
},
+ {
+ name: "Message format",
+ type: "option",
+ value: ["Raw", "Hex", "Base64"]
+ },
{
name: "Message Digest Algorithm",
type: "option",
@@ -51,7 +57,7 @@ class RSAVerify extends Operation {
* @returns {string}
*/
run(input, args) {
- const [pemKey, message, mdAlgo] = args;
+ const [pemKey, message, format, mdAlgo] = args;
if (pemKey.replace("-----BEGIN RSA PUBLIC KEY-----", "").length === 0) {
throw new OperationError("Please enter a public key.");
}
@@ -60,7 +66,8 @@ class RSAVerify extends Operation {
const pubKey = forge.pki.publicKeyFromPem(pemKey);
// Generate message digest
const md = MD_ALGORITHMS[mdAlgo].create();
- md.update(message, "utf8");
+ const messageStr = Utils.convertToByteString(message, format);
+ md.update(messageStr, "raw");
// Compare signed message digest and generated message digest
const result = pubKey.verify(md.digest().bytes(), input);
return result ? "Verified OK" : "Verification Failure";
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/RandomizeColourPalette.mjs b/src/core/operations/RandomizeColourPalette.mjs
index e3baf54b..186023c2 100644
--- a/src/core/operations/RandomizeColourPalette.mjs
+++ b/src/core/operations/RandomizeColourPalette.mjs
@@ -10,13 +10,12 @@ import Utils from "../Utils.mjs";
import { isImage } from "../lib/FileType.mjs";
import { runHash } from "../lib/Hash.mjs";
import { toBase64 } from "../lib/Base64.mjs";
-import jimp from "jimp";
+import { Jimp } from "jimp";
/**
* Randomize Colour Palette operation
*/
class RandomizeColourPalette extends Operation {
-
/**
* RandomizeColourPalette constructor
*/
@@ -25,7 +24,8 @@ class RandomizeColourPalette extends Operation {
this.name = "Randomize Colour Palette";
this.module = "Image";
- this.description = "Randomizes each colour in an image's colour palette. This can often reveal text or symbols that were previously a very similar colour to their surroundings, a technique sometimes used in Steganography.";
+ this.description =
+ "Randomizes each colour in an image's colour palette. This can often reveal text or symbols that were previously a very similar colour to their surroundings, a technique sometimes used in Steganography.";
this.infoURL = "https://wikipedia.org/wiki/Indexed_color";
this.inputType = "ArrayBuffer";
this.outputType = "ArrayBuffer";
@@ -34,8 +34,8 @@ class RandomizeColourPalette extends Operation {
{
name: "Seed",
type: "string",
- value: ""
- }
+ value: "",
+ },
];
}
@@ -45,23 +45,24 @@ class RandomizeColourPalette extends Operation {
* @returns {ArrayBuffer}
*/
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 seed = args[0] || (Math.random().toString().substr(2)),
- parsedImage = await jimp.read(input),
+ const seed = args[0] || Math.random().toString().substr(2),
+ parsedImage = await Jimp.read(input),
width = parsedImage.bitmap.width,
height = parsedImage.bitmap.height;
let rgbString, rgbHash, rgbHex;
- parsedImage.scan(0, 0, width, height, function(x, y, idx) {
- rgbString = this.bitmap.data.slice(idx, idx+3).join(".");
+ parsedImage.scan(0, 0, width, height, function (x, y, idx) {
+ rgbString = this.bitmap.data.slice(idx, idx + 3).join(".");
rgbHash = runHash("md5", Utils.strToArrayBuffer(seed + rgbString));
rgbHex = rgbHash.substr(0, 6) + "ff";
parsedImage.setPixelColor(parseInt(rgbHex, 16), x, y);
});
- const imageBuffer = await parsedImage.getBufferAsync(jimp.AUTO);
+ const imageBuffer = await parsedImage.getBuffer(parsedImage.mime);
return new Uint8Array(imageBuffer).buffer;
}
@@ -77,7 +78,6 @@ class RandomizeColourPalette extends Operation {
return `
`;
}
-
}
export default RandomizeColourPalette;
diff --git a/src/core/operations/RegularExpression.mjs b/src/core/operations/RegularExpression.mjs
index 18d3fda9..9ea17e83 100644
--- a/src/core/operations/RegularExpression.mjs
+++ b/src/core/operations/RegularExpression.mjs
@@ -67,6 +67,10 @@ class RegularExpression extends Operation {
name: "MAC address",
value: "[A-Fa-f\\d]{2}(?:[:-][A-Fa-f\\d]{2}){5}"
},
+ {
+ name: "UUID",
+ value: "[0-9a-fA-F]{8}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{12}"
+ },
{
name: "Date (yyyy-mm-dd)",
value: "((?:19|20)\\d\\d)[- /.](0[1-9]|1[012])[- /.](0[1-9]|[12][0-9]|3[01])"
@@ -83,10 +87,6 @@ class RegularExpression extends Operation {
name: "Strings",
value: "[A-Za-z\\d/\\-:.,_$%\\x27\"()<>= !\\[\\]{}@]{4,}"
},
- {
- name: "UUID (any version)",
- value: "[0-9a-fA-F]{8}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{12}"
- },
],
"target": 1
},
diff --git a/src/core/operations/ResizeImage.mjs b/src/core/operations/ResizeImage.mjs
index b2ed3bbf..bec07c4e 100644
--- a/src/core/operations/ResizeImage.mjs
+++ b/src/core/operations/ResizeImage.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";
+import { Jimp, JimpMime, ResizeStrategy } from "jimp";
/**
* Resize Image operation
*/
class ResizeImage extends Operation {
-
/**
* ResizeImage constructor
*/
@@ -24,7 +23,8 @@ class ResizeImage extends Operation {
this.name = "Resize Image";
this.module = "Image";
- this.description = "Resizes an image to the specified width and height values.";
+ this.description =
+ "Resizes an image to the specified width and height values.";
this.infoURL = "https://wikipedia.org/wiki/Image_scaling";
this.inputType = "ArrayBuffer";
this.outputType = "ArrayBuffer";
@@ -34,23 +34,23 @@ class ResizeImage extends Operation {
name: "Width",
type: "number",
value: 100,
- min: 1
+ min: 1,
},
{
name: "Height",
type: "number",
value: 100,
- min: 1
+ min: 1,
},
{
name: "Unit type",
type: "option",
- value: ["Pixels", "Percent"]
+ value: ["Pixels", "Percent"],
},
{
name: "Maintain aspect ratio",
type: "boolean",
- value: false
+ value: false,
},
{
name: "Resizing algorithm",
@@ -60,10 +60,10 @@ class ResizeImage extends Operation {
"Bilinear",
"Bicubic",
"Hermite",
- "Bezier"
+ "Bezier",
],
- defaultIndex: 1
- }
+ defaultIndex: 1,
+ },
];
}
@@ -80,11 +80,11 @@ class ResizeImage extends Operation {
resizeAlg = args[4];
const resizeMap = {
- "Nearest Neighbour": jimp.RESIZE_NEAREST_NEIGHBOR,
- "Bilinear": jimp.RESIZE_BILINEAR,
- "Bicubic": jimp.RESIZE_BICUBIC,
- "Hermite": jimp.RESIZE_HERMITE,
- "Bezier": jimp.RESIZE_BEZIER
+ "Nearest Neighbour": ResizeStrategy.NEAREST_NEIGHBOR,
+ Bilinear: ResizeStrategy.BILINEAR,
+ Bicubic: ResizeStrategy.BICUBIC,
+ Hermite: ResizeStrategy.HERMITE,
+ Bezier: ResizeStrategy.BEZIER,
};
if (!isImage(input)) {
@@ -93,29 +93,37 @@ class ResizeImage extends Operation {
let image;
try {
- image = await jimp.read(input);
+ image = await Jimp.read(input);
} catch (err) {
throw new OperationError(`Error loading image. (${err})`);
}
try {
if (unit === "Percent") {
- width = image.getWidth() * (width / 100);
- height = image.getHeight() * (height / 100);
+ width = image.width * (width / 100);
+ height = image.height * (height / 100);
}
if (isWorkerEnvironment())
self.sendStatusMessage("Resizing image...");
if (aspect) {
- image.scaleToFit(width, height, resizeMap[resizeAlg]);
+ image.scaleToFit({
+ w: width,
+ h: height,
+ mode: resizeMap[resizeAlg],
+ });
} else {
- image.resize(width, height, resizeMap[resizeAlg]);
+ image.resize({
+ w: width,
+ h: height,
+ mode: resizeMap[resizeAlg],
+ });
}
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) {
@@ -139,7 +147,6 @@ class ResizeImage extends Operation {
return `
`;
}
-
}
export default ResizeImage;
diff --git a/src/core/operations/RisonDecode.mjs b/src/core/operations/RisonDecode.mjs
index 1b9741a8..d4e36f80 100644
--- a/src/core/operations/RisonDecode.mjs
+++ b/src/core/operations/RisonDecode.mjs
@@ -20,7 +20,7 @@ class RisonDecode extends Operation {
super();
this.name = "Rison Decode";
- this.module = "Default";
+ this.module = "Encodings";
this.description = "Rison, a data serialization format optimized for compactness in URIs. Rison is a slight variation of JSON that looks vastly superior after URI encoding. Rison still expresses exactly the same set of data structures as JSON, so data can be translated back and forth without loss or guesswork.";
this.infoURL = "https://github.com/Nanonid/rison";
this.inputType = "string";
@@ -29,11 +29,7 @@ class RisonDecode extends Operation {
{
name: "Decode Option",
type: "editableOption",
- value: [
- { name: "Decode", value: "Decode", },
- { name: "Decode Object", value: "Decode Object", },
- { name: "Decode Array", value: "Decode Array", },
- ]
+ value: ["Decode", "Decode Object", "Decode Array"]
},
];
}
@@ -52,8 +48,9 @@ class RisonDecode extends Operation {
return rison.decode_object(input);
case "Decode Array":
return rison.decode_array(input);
+ default:
+ throw new OperationError("Invalid Decode option");
}
- throw new OperationError("Invalid Decode option");
}
}
diff --git a/src/core/operations/RisonEncode.mjs b/src/core/operations/RisonEncode.mjs
index 36a61017..12b13b66 100644
--- a/src/core/operations/RisonEncode.mjs
+++ b/src/core/operations/RisonEncode.mjs
@@ -20,7 +20,7 @@ class RisonEncode extends Operation {
super();
this.name = "Rison Encode";
- this.module = "Default";
+ this.module = "Encodings";
this.description = "Rison, a data serialization format optimized for compactness in URIs. Rison is a slight variation of JSON that looks vastly superior after URI encoding. Rison still expresses exactly the same set of data structures as JSON, so data can be translated back and forth without loss or guesswork.";
this.infoURL = "https://github.com/Nanonid/rison";
this.inputType = "Object";
@@ -28,13 +28,8 @@ class RisonEncode extends Operation {
this.args = [
{
name: "Encode Option",
- type: "editableOption",
- value: [
- { name: "Encode", value: "Encode", },
- { name: "Encode Object", value: "Encode Object", },
- { name: "Encode Array", value: "Encode Array", },
- { name: "Encode URI", value: "Encode URI", }
- ]
+ type: "option",
+ value: ["Encode", "Encode Object", "Encode Array", "Encode URI"]
},
];
}
@@ -55,8 +50,9 @@ class RisonEncode extends Operation {
return rison.encode_array(input);
case "Encode URI":
return rison.encode_uri(input);
+ default:
+ throw new OperationError("Invalid encode option");
}
- throw new OperationError("Invalid encode option");
}
}
diff --git a/src/core/operations/RotateImage.mjs b/src/core/operations/RotateImage.mjs
index a4659b12..5a96ffab 100644
--- a/src/core/operations/RotateImage.mjs
+++ b/src/core/operations/RotateImage.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";
+import { Jimp, JimpMime } from "jimp";
/**
* Rotate Image operation
*/
class RotateImage extends Operation {
-
/**
* RotateImage constructor
*/
@@ -24,7 +23,8 @@ class RotateImage extends Operation {
this.name = "Rotate Image";
this.module = "Image";
- this.description = "Rotates an image by the specified number of degrees.";
+ this.description =
+ "Rotates an image by the specified number of degrees.";
this.infoURL = "";
this.inputType = "ArrayBuffer";
this.outputType = "ArrayBuffer";
@@ -33,8 +33,8 @@ class RotateImage extends Operation {
{
name: "Rotation amount (degrees)",
type: "number",
- value: 90
- }
+ value: 90,
+ },
];
}
@@ -52,7 +52,7 @@ class RotateImage extends Operation {
let image;
try {
- image = await jimp.read(input);
+ image = await Jimp.read(input);
} catch (err) {
throw new OperationError(`Error loading image. (${err})`);
}
@@ -62,10 +62,10 @@ class RotateImage extends Operation {
image.rotate(degrees);
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) {
@@ -89,7 +89,6 @@ class RotateImage extends Operation {
return `
`;
}
-
}
export default RotateImage;
diff --git a/src/core/operations/SIGABA.mjs b/src/core/operations/SIGABA.mjs
index 274f09f6..e3a9b82e 100644
--- a/src/core/operations/SIGABA.mjs
+++ b/src/core/operations/SIGABA.mjs
@@ -40,7 +40,7 @@ class Sigaba extends Operation {
value: false
},
{
- name: "1st cipher rotor intial value",
+ name: "1st cipher rotor initial value",
type: "option",
value: LETTERS
},
@@ -56,7 +56,7 @@ class Sigaba extends Operation {
value: false
},
{
- name: "2nd cipher rotor intial value",
+ name: "2nd cipher rotor initial value",
type: "option",
value: LETTERS
},
@@ -72,7 +72,7 @@ class Sigaba extends Operation {
value: false
},
{
- name: "3rd cipher rotor intial value",
+ name: "3rd cipher rotor initial value",
type: "option",
value: LETTERS
},
@@ -88,7 +88,7 @@ class Sigaba extends Operation {
value: false
},
{
- name: "4th cipher rotor intial value",
+ name: "4th cipher rotor initial value",
type: "option",
value: LETTERS
},
@@ -104,7 +104,7 @@ class Sigaba extends Operation {
value: false
},
{
- name: "5th cipher rotor intial value",
+ name: "5th cipher rotor initial value",
type: "option",
value: LETTERS
},
@@ -120,7 +120,7 @@ class Sigaba extends Operation {
value: false
},
{
- name: "1st control rotor intial value",
+ name: "1st control rotor initial value",
type: "option",
value: LETTERS
},
@@ -136,7 +136,7 @@ class Sigaba extends Operation {
value: false
},
{
- name: "2nd control rotor intial value",
+ name: "2nd control rotor initial value",
type: "option",
value: LETTERS
},
@@ -152,7 +152,7 @@ class Sigaba extends Operation {
value: false
},
{
- name: "3rd control rotor intial value",
+ name: "3rd control rotor initial value",
type: "option",
value: LETTERS
},
@@ -168,7 +168,7 @@ class Sigaba extends Operation {
value: false
},
{
- name: "4th control rotor intial value",
+ name: "4th control rotor initial value",
type: "option",
value: LETTERS
},
@@ -184,7 +184,7 @@ class Sigaba extends Operation {
value: false
},
{
- name: "5th control rotor intial value",
+ name: "5th control rotor initial value",
type: "option",
value: LETTERS
},
@@ -195,7 +195,7 @@ class Sigaba extends Operation {
defaultIndex: 0
},
{
- name: "1st index rotor intial value",
+ name: "1st index rotor initial value",
type: "option",
value: NUMBERS
},
@@ -206,7 +206,7 @@ class Sigaba extends Operation {
defaultIndex: 0
},
{
- name: "2nd index rotor intial value",
+ name: "2nd index rotor initial value",
type: "option",
value: NUMBERS
},
@@ -217,7 +217,7 @@ class Sigaba extends Operation {
defaultIndex: 0
},
{
- name: "3rd index rotor intial value",
+ name: "3rd index rotor initial value",
type: "option",
value: NUMBERS
},
@@ -228,7 +228,7 @@ class Sigaba extends Operation {
defaultIndex: 0
},
{
- name: "4th index rotor intial value",
+ name: "4th index rotor initial value",
type: "option",
value: NUMBERS
},
@@ -239,7 +239,7 @@ class Sigaba extends Operation {
defaultIndex: 0
},
{
- name: "5th index rotor intial value",
+ name: "5th index rotor initial value",
type: "option",
value: NUMBERS
},
diff --git a/src/core/operations/SM2Decrypt.mjs b/src/core/operations/SM2Decrypt.mjs
new file mode 100644
index 00000000..39657110
--- /dev/null
+++ b/src/core/operations/SM2Decrypt.mjs
@@ -0,0 +1,71 @@
+/**
+ * @author flakjacket95 [dflack95@gmail.com]
+ * @copyright Crown Copyright 2024
+ * @license Apache-2.0
+ */
+
+import OperationError from "../errors/OperationError.mjs";
+import Operation from "../Operation.mjs";
+
+import { SM2 } from "../lib/SM2.mjs";
+
+/**
+ * SM2Decrypt operation
+ */
+class SM2Decrypt extends Operation {
+
+ /**
+ * SM2Decrypt constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "SM2 Decrypt";
+ this.module = "Crypto";
+ this.description = "Decrypts a message utilizing the SM2 standard";
+ this.infoURL = ""; // Usually a Wikipedia link. Remember to remove localisation (i.e. https://wikipedia.org/etc rather than https://en.wikipedia.org/etc)
+ this.inputType = "string";
+ this.outputType = "ArrayBuffer";
+ this.args = [
+ {
+ name: "Private Key",
+ type: "string",
+ value: "DEADBEEF"
+ },
+ {
+ "name": "Input Format",
+ "type": "option",
+ "value": ["C1C3C2", "C1C2C3"],
+ "defaultIndex": 0
+ },
+ {
+ name: "Curve",
+ type: "option",
+ "value": ["sm2p256v1"],
+ "defaultIndex": 0
+ }
+ ];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {ArrayBuffer}
+ */
+ run(input, args) {
+ const [privateKey, inputFormat, curveName] = args;
+
+ if (privateKey.length !== 64) {
+ throw new OperationError("Input private key must be in hex; and should be 32 bytes");
+ }
+
+ const sm2 = new SM2(curveName, inputFormat);
+ sm2.setPrivateKey(privateKey);
+
+ const result = sm2.decrypt(input);
+ return result;
+ }
+
+}
+
+export default SM2Decrypt;
diff --git a/src/core/operations/SM2Encrypt.mjs b/src/core/operations/SM2Encrypt.mjs
new file mode 100644
index 00000000..b1e5f901
--- /dev/null
+++ b/src/core/operations/SM2Encrypt.mjs
@@ -0,0 +1,77 @@
+/**
+ * @author flakjacket95 [dflack95@gmail.com]
+ * @copyright Crown Copyright 2024
+ * @license Apache-2.0
+ */
+
+import OperationError from "../errors/OperationError.mjs";
+import Operation from "../Operation.mjs";
+
+import { SM2 } from "../lib/SM2.mjs";
+
+/**
+ * SM2 Encrypt operation
+ */
+class SM2Encrypt extends Operation {
+
+ /**
+ * SM2Encrypt constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "SM2 Encrypt";
+ this.module = "Crypto";
+ this.description = "Encrypts a message utilizing the SM2 standard";
+ this.infoURL = ""; // Usually a Wikipedia link. Remember to remove localisation (i.e. https://wikipedia.org/etc rather than https://en.wikipedia.org/etc)
+ this.inputType = "ArrayBuffer";
+ this.outputType = "string";
+
+ this.args = [
+ {
+ name: "Public Key X",
+ type: "string",
+ value: "DEADBEEF"
+ },
+ {
+ name: "Public Key Y",
+ type: "string",
+ value: "DEADBEEF"
+ },
+ {
+ "name": "Output Format",
+ "type": "option",
+ "value": ["C1C3C2", "C1C2C3"],
+ "defaultIndex": 0
+ },
+ {
+ name: "Curve",
+ type: "option",
+ "value": ["sm2p256v1"],
+ "defaultIndex": 0
+ }
+ ];
+ }
+
+ /**
+ * @param {ArrayBuffer} input
+ * @param {Object[]} args
+ * @returns {byteArray}
+ */
+ run(input, args) {
+ const [publicKeyX, publicKeyY, outputFormat, curveName] = args;
+ this.outputFormat = outputFormat;
+
+ if (publicKeyX.length !== 64 || publicKeyY.length !== 64) {
+ throw new OperationError("Invalid Public Key - Ensure each component is 32 bytes in size and in hex");
+ }
+
+ const sm2 = new SM2(curveName, outputFormat);
+ sm2.setPublicKey(publicKeyX, publicKeyY);
+
+ const result = sm2.encrypt(new Uint8Array(input));
+ return result;
+ }
+}
+
+export default SM2Encrypt;
diff --git a/src/core/operations/SQLBeautify.mjs b/src/core/operations/SQLBeautify.mjs
index 0f3d2e3c..2171f7fc 100644
--- a/src/core/operations/SQLBeautify.mjs
+++ b/src/core/operations/SQLBeautify.mjs
@@ -3,8 +3,7 @@
* @copyright Crown Copyright 2016
* @license Apache-2.0
*/
-
-import vkbeautify from "vkbeautify";
+import { format } from "sql-formatter";
import Operation from "../Operation.mjs";
/**
@@ -39,7 +38,26 @@ class SQLBeautify extends Operation {
*/
run(input, args) {
const indentStr = args[0];
- return vkbeautify.sql(input, indentStr);
+ // Extract and replace bind variables like :Bind1 with __BIND_0__
+ const bindRegex = /:\w+/g;
+ const bindMap = {};
+ let bindCounter=0;
+ const placeholderInput = input.replace(bindRegex, (match) => {
+ const placeholder = `__BIND_${bindCounter++}__`;
+ bindMap[placeholder] = match;
+ return placeholder;
+ });
+ // Format the SQL with chosen options
+ let formatted= format(placeholderInput, {
+ language: "mysql", // Use MySQL as the default dialect for better compatibility with real-world SQL
+ useTabs: indentStr==="\t", // true if tab, false if spaces
+ tabWidth: indentStr.length || 4, // fallback if empty
+ indentStyle: "standard" // fine for most SQL
+ });
+ // Replace placeholders back with original bind variables
+ formatted = formatted.replace(/__BIND_\d+__/g, match => bindMap[match] || match);
+
+ return formatted;
}
}
diff --git a/src/core/operations/Salsa20.mjs b/src/core/operations/Salsa20.mjs
new file mode 100644
index 00000000..7a76cf26
--- /dev/null
+++ b/src/core/operations/Salsa20.mjs
@@ -0,0 +1,154 @@
+/**
+ * @author joostrijneveld [joost@joostrijneveld.nl]
+ * @copyright Crown Copyright 2024
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+import OperationError from "../errors/OperationError.mjs";
+import Utils from "../Utils.mjs";
+import { toHex } from "../lib/Hex.mjs";
+import { salsa20Block } from "../lib/Salsa20.mjs";
+
+/**
+ * Salsa20 operation
+ */
+class Salsa20 extends Operation {
+
+ /**
+ * Salsa20 constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "Salsa20";
+ this.module = "Ciphers";
+ this.description = "Salsa20 is a stream cipher designed by Daniel J. Bernstein and submitted to the eSTREAM project; Salsa20/8 and Salsa20/12 are round-reduced variants. It is closely related to the ChaCha stream cipher.
Key: Salsa20 uses a key of 16 or 32 bytes (128 or 256 bits).
Nonce: Salsa20 uses a nonce of 8 bytes (64 bits).
Counter: Salsa uses a counter of 8 bytes (64 bits). The counter starts at zero at the start of the keystream, and is incremented at every 64 bytes.";
+ this.infoURL = "https://wikipedia.org/wiki/Salsa20";
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [
+ {
+ "name": "Key",
+ "type": "toggleString",
+ "value": "",
+ "toggleValues": ["Hex", "UTF8", "Latin1", "Base64"]
+ },
+ {
+ "name": "Nonce",
+ "type": "toggleString",
+ "value": "",
+ "toggleValues": ["Hex", "UTF8", "Latin1", "Base64", "Integer"]
+ },
+ {
+ "name": "Counter",
+ "type": "number",
+ "value": 0,
+ "min": 0
+ },
+ {
+ "name": "Rounds",
+ "type": "option",
+ "value": ["20", "12", "8"]
+ },
+ {
+ "name": "Input",
+ "type": "option",
+ "value": ["Hex", "Raw"]
+ },
+ {
+ "name": "Output",
+ "type": "option",
+ "value": ["Raw", "Hex"]
+ }
+ ];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ const key = Utils.convertToByteArray(args[0].string, args[0].option),
+ nonceType = args[1].option,
+ rounds = parseInt(args[3], 10),
+ inputType = args[4],
+ outputType = args[5];
+
+ if (key.length !== 16 && key.length !== 32) {
+ throw new OperationError(`Invalid key length: ${key.length} bytes.
+
+Salsa20 uses a key of 16 or 32 bytes (128 or 256 bits).`);
+ }
+
+ let counter, nonce;
+ if (nonceType === "Integer") {
+ nonce = Utils.intToByteArray(parseInt(args[1].string, 10), 8, "little");
+ } else {
+ nonce = Utils.convertToByteArray(args[1].string, args[1].option);
+ if (!(nonce.length === 8)) {
+ throw new OperationError(`Invalid nonce length: ${nonce.length} bytes.
+
+Salsa20 uses a nonce of 8 bytes (64 bits).`);
+ }
+ }
+ counter = Utils.intToByteArray(args[2], 8, "little");
+
+ const output = [];
+ input = Utils.convertToByteArray(input, inputType);
+
+ let counterAsInt = Utils.byteArrayToInt(counter, "little");
+ for (let i = 0; i < input.length; i += 64) {
+ counter = Utils.intToByteArray(counterAsInt, 8, "little");
+ const stream = salsa20Block(key, nonce, counter, rounds);
+ for (let j = 0; j < 64 && i + j < input.length; j++) {
+ output.push(input[i + j] ^ stream[j]);
+ }
+ counterAsInt++;
+ }
+
+ if (outputType === "Hex") {
+ return toHex(output);
+ } else {
+ return Utils.arrayBufferToStr(Uint8Array.from(output).buffer);
+ }
+ }
+
+ /**
+ * Highlight Salsa20
+ *
+ * @param {Object[]} pos
+ * @param {number} pos[].start
+ * @param {number} pos[].end
+ * @param {Object[]} args
+ * @returns {Object[]} pos
+ */
+ highlight(pos, args) {
+ const inputType = args[4],
+ outputType = args[5];
+ if (inputType === "Raw" && outputType === "Raw") {
+ return pos;
+ }
+ }
+
+ /**
+ * Highlight Salsa20 in reverse
+ *
+ * @param {Object[]} pos
+ * @param {number} pos[].start
+ * @param {number} pos[].end
+ * @param {Object[]} args
+ * @returns {Object[]} pos
+ */
+ highlightReverse(pos, args) {
+ const inputType = args[4],
+ outputType = args[5];
+ if (inputType === "Raw" && outputType === "Raw") {
+ return pos;
+ }
+ }
+
+}
+
+export default Salsa20;
diff --git a/src/core/operations/SharpenImage.mjs b/src/core/operations/SharpenImage.mjs
index eb033ad2..1f5461f2 100644
--- a/src/core/operations/SharpenImage.mjs
+++ b/src/core/operations/SharpenImage.mjs
@@ -8,15 +8,13 @@ import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs";
import { isImage } from "../lib/FileType.mjs";
import { toBase64 } from "../lib/Base64.mjs";
-import { gaussianBlur } from "../lib/ImageManipulation.mjs";
import { isWorkerEnvironment } from "../Utils.mjs";
-import jimp from "jimp";
+import { Jimp, JimpMime } from "jimp";
/**
* Sharpen Image operation
*/
class SharpenImage extends Operation {
-
/**
* SharpenImage constructor
*/
@@ -35,22 +33,22 @@ class SharpenImage extends Operation {
name: "Radius",
type: "number",
value: 2,
- min: 1
+ min: 1,
},
{
name: "Amount",
type: "number",
value: 1,
min: 0,
- step: 0.1
+ step: 0.1,
},
{
name: "Threshold",
type: "number",
value: 10,
min: 0,
- max: 100
- }
+ max: 100,
+ },
];
}
@@ -68,7 +66,7 @@ class SharpenImage extends Operation {
let image;
try {
- image = await jimp.read(input);
+ image = await Jimp.read(input);
} catch (err) {
throw new OperationError(`Error loading image. (${err})`);
}
@@ -79,67 +77,102 @@ class SharpenImage extends Operation {
const blurMask = image.clone();
if (isWorkerEnvironment())
- self.sendStatusMessage("Sharpening image... (Blurring cloned image)");
- const blurImage = gaussianBlur(image.clone(), radius);
-
+ self.sendStatusMessage(
+ "Sharpening image... (Blurring cloned image)",
+ );
+ const blurImage = image.clone().gaussian(radius);
if (isWorkerEnvironment())
- self.sendStatusMessage("Sharpening image... (Creating unsharp mask)");
- blurMask.scan(0, 0, blurMask.bitmap.width, blurMask.bitmap.height, function(x, y, idx) {
- const blurRed = blurImage.bitmap.data[idx];
- const blurGreen = blurImage.bitmap.data[idx + 1];
- const blurBlue = blurImage.bitmap.data[idx + 2];
+ self.sendStatusMessage(
+ "Sharpening image... (Creating unsharp mask)",
+ );
+ blurMask.scan(
+ 0,
+ 0,
+ blurMask.bitmap.width,
+ blurMask.bitmap.height,
+ function (x, y, idx) {
+ const blurRed = blurImage.bitmap.data[idx];
+ const blurGreen = blurImage.bitmap.data[idx + 1];
+ const blurBlue = blurImage.bitmap.data[idx + 2];
- const normalRed = this.bitmap.data[idx];
- const normalGreen = this.bitmap.data[idx + 1];
- const normalBlue = this.bitmap.data[idx + 2];
+ const normalRed = this.bitmap.data[idx];
+ const normalGreen = this.bitmap.data[idx + 1];
+ const normalBlue = this.bitmap.data[idx + 2];
- // Subtract blurred pixel value from normal image
- this.bitmap.data[idx] = (normalRed > blurRed) ? normalRed - blurRed : 0;
- this.bitmap.data[idx + 1] = (normalGreen > blurGreen) ? normalGreen - blurGreen : 0;
- this.bitmap.data[idx + 2] = (normalBlue > blurBlue) ? normalBlue - blurBlue : 0;
- });
+ // Subtract blurred pixel value from normal image
+ this.bitmap.data[idx] =
+ normalRed > blurRed ? normalRed - blurRed : 0;
+ this.bitmap.data[idx + 1] =
+ normalGreen > blurGreen ? normalGreen - blurGreen : 0;
+ this.bitmap.data[idx + 2] =
+ normalBlue > blurBlue ? normalBlue - blurBlue : 0;
+ },
+ );
if (isWorkerEnvironment())
- self.sendStatusMessage("Sharpening image... (Merging with unsharp mask)");
- image.scan(0, 0, image.bitmap.width, image.bitmap.height, function(x, y, idx) {
- let maskRed = blurMask.bitmap.data[idx];
- let maskGreen = blurMask.bitmap.data[idx + 1];
- let maskBlue = blurMask.bitmap.data[idx + 2];
+ self.sendStatusMessage(
+ "Sharpening image... (Merging with unsharp mask)",
+ );
+ image.scan(
+ 0,
+ 0,
+ image.bitmap.width,
+ image.bitmap.height,
+ function (x, y, idx) {
+ let maskRed = blurMask.bitmap.data[idx];
+ let maskGreen = blurMask.bitmap.data[idx + 1];
+ let maskBlue = blurMask.bitmap.data[idx + 2];
- const normalRed = this.bitmap.data[idx];
- const normalGreen = this.bitmap.data[idx + 1];
- const normalBlue = this.bitmap.data[idx + 2];
+ const normalRed = this.bitmap.data[idx];
+ const normalGreen = this.bitmap.data[idx + 1];
+ const normalBlue = this.bitmap.data[idx + 2];
- // Calculate luminance
- const maskLuminance = (0.2126 * maskRed + 0.7152 * maskGreen + 0.0722 * maskBlue);
- const normalLuminance = (0.2126 * normalRed + 0.7152 * normalGreen + 0.0722 * normalBlue);
+ // Calculate luminance
+ const maskLuminance =
+ 0.2126 * maskRed +
+ 0.7152 * maskGreen +
+ 0.0722 * maskBlue;
+ const normalLuminance =
+ 0.2126 * normalRed +
+ 0.7152 * normalGreen +
+ 0.0722 * normalBlue;
- let luminanceDiff;
- if (maskLuminance > normalLuminance) {
- luminanceDiff = maskLuminance - normalLuminance;
- } else {
- luminanceDiff = normalLuminance - maskLuminance;
- }
+ let luminanceDiff;
+ if (maskLuminance > normalLuminance) {
+ luminanceDiff = maskLuminance - normalLuminance;
+ } else {
+ luminanceDiff = normalLuminance - maskLuminance;
+ }
- // Scale mask colours by amount
- maskRed = maskRed * amount;
- maskGreen = maskGreen * amount;
- maskBlue = maskBlue * amount;
+ // Scale mask colours by amount
+ maskRed = maskRed * amount;
+ maskGreen = maskGreen * amount;
+ maskBlue = maskBlue * amount;
- // Only change pixel value if the difference is higher than threshold
- if ((luminanceDiff / 255) * 100 >= threshold) {
- this.bitmap.data[idx] = (normalRed + maskRed) <= 255 ? normalRed + maskRed : 255;
- this.bitmap.data[idx + 1] = (normalGreen + maskGreen) <= 255 ? normalGreen + maskGreen : 255;
- this.bitmap.data[idx + 2] = (normalBlue + maskBlue) <= 255 ? normalBlue + maskBlue : 255;
- }
- });
+ // Only change pixel value if the difference is higher than threshold
+ if ((luminanceDiff / 255) * 100 >= threshold) {
+ this.bitmap.data[idx] =
+ normalRed + maskRed <= 255 ?
+ normalRed + maskRed :
+ 255;
+ this.bitmap.data[idx + 1] =
+ normalGreen + maskGreen <= 255 ?
+ normalGreen + maskGreen :
+ 255;
+ this.bitmap.data[idx + 2] =
+ normalBlue + maskBlue <= 255 ?
+ normalBlue + maskBlue :
+ 255;
+ }
+ },
+ );
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) {
@@ -163,7 +196,6 @@ class SharpenImage extends Operation {
return `
`;
}
-
}
export default SharpenImage;
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:- Degrees Minutes Seconds (DMS)
- Degrees Decimal Minutes (DDM)
- Decimal Degrees (DD)
- Geohash
- Military Grid Reference System (MGRS)
- Ordnance Survey National Grid (OSNG)
- Universal Transverse Mercator (UTM)
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 `