${key} `;
if (typeof obj[key] === "object")
html += `${objToTable(obj[key], true)} `;
diff --git a/src/core/lib/QRCode.mjs b/src/core/lib/QRCode.mjs
index 842ebd55f..ccac4f295 100644
--- a/src/core/lib/QRCode.mjs
+++ b/src/core/lib/QRCode.mjs
@@ -10,7 +10,7 @@ import OperationError from "../errors/OperationError.mjs";
import jsQR from "jsqr";
import qr from "qr-image";
import Utils from "../Utils.mjs";
-import jimp from "jimp";
+import Jimp from "jimp/es/index.js";
/**
* Parses a QR code image from an image
@@ -22,7 +22,7 @@ import jimp from "jimp";
export async function parseQrCode(input, normalise) {
let image;
try {
- image = await jimp.read(input);
+ image = await Jimp.read(input);
} catch (err) {
throw new OperationError(`Error opening image. (${err})`);
}
@@ -33,8 +33,8 @@ export async function parseQrCode(input, normalise) {
image.background(0xFFFFFFFF);
image.normalize();
image.greyscale();
- image = await image.getBufferAsync(jimp.MIME_JPEG);
- image = await jimp.read(image);
+ image = await image.getBufferAsync(Jimp.MIME_JPEG);
+ image = await Jimp.read(image);
}
} catch (err) {
throw new OperationError(`Error normalising image. (${err})`);
diff --git a/src/core/lib/SM2.mjs b/src/core/lib/SM2.mjs
new file mode 100644
index 000000000..e81564106
--- /dev/null
+++ b/src/core/lib/SM2.mjs
@@ -0,0 +1,258 @@
+/**
+ * Utilities and operations utilized for SM2 encryption and decryption
+ * @author flakjacket95 [dflack95@gmail.com]
+ * @copyright Crown Copyright 2024
+ * @license Apache-2.0
+ */
+
+import OperationError from "../errors/OperationError.mjs";
+import { fromHex } from "../lib/Hex.mjs";
+import Utils from "../Utils.mjs";
+import Sm3 from "crypto-api/src/hasher/sm3.mjs";
+import {toHex} from "crypto-api/src/encoder/hex.mjs";
+import r from "jsrsasign";
+
+/**
+ * SM2 Class for encryption and decryption operations
+ */
+export class SM2 {
+ /**
+ * Constructor for SM2 class; sets up with the curve and the output format as specified in user args
+ *
+ * @param {*} curve
+ * @param {*} format
+ */
+ constructor(curve, format) {
+ this.ecParams = null;
+ this.rng = new r.SecureRandom();
+ /*
+ For any additional curve definitions utilized by SM2, add another block like the below for that curve, then add the curve name to the Curve selection dropdown
+ */
+ r.crypto.ECParameterDB.regist(
+ "sm2p256v1", // name / p = 2**256 - 2**224 - 2**96 + 2**64 - 1
+ 256,
+ "FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF", // p
+ "FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFC", // a
+ "28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93", // b
+ "FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123", // n
+ "1", // h
+ "32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7", // gx
+ "BC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0", // gy
+ []
+ ); // alias
+ this.ecParams = r.crypto.ECParameterDB.getByName(curve);
+
+ this.format = format;
+ }
+
+ /**
+ * Set the public key coordinates for the SM2 class
+ *
+ * @param {string} publicKeyX
+ * @param {string} publicKeyY
+ */
+ setPublicKey(publicKeyX, publicKeyY) {
+ /*
+ * TODO: This needs some additional length validation; and checking for errors in the decoding process
+ * TODO: Can probably support other public key encoding methods here as well in the future
+ */
+ this.publicKey = this.ecParams.curve.decodePointHex("04" + publicKeyX + publicKeyY);
+
+ if (this.publicKey.isInfinity()) {
+ throw new OperationError("Invalid Public Key");
+ }
+ }
+
+ /**
+ * Set the private key value for the SM2 class
+ *
+ * @param {string} privateKey
+ */
+ setPrivateKey(privateKeyHex) {
+ this.privateKey = new r.BigInteger(privateKeyHex, 16);
+ }
+
+ /**
+ * Main encryption function; takes user input, processes encryption and returns the result in hex (with the components arranged as configured by the user args)
+ *
+ * @param {*} input
+ * @returns {string}
+ */
+ encrypt(input) {
+ const G = this.ecParams.G;
+
+ /*
+ * Compute a new, random public key along the same elliptic curve to form the starting point for our encryption process (record the resulting X and Y as hex to provide as part of the operation output)
+ * k: Randomly generated BigInteger
+ * c1: Result of dotting our curve generator point `G` with the value of `k`
+ */
+ const k = this.generatePublicKey();
+ const c1 = G.multiply(k);
+ const [hexC1X, hexC1Y] = this.getPointAsHex(c1);
+
+ /*
+ * Compute p2 (secret) using the public key, and the chosen k value above
+ */
+ const p2 = this.publicKey.multiply(k);
+
+ /*
+ * Compute the C3 SM3 hash before we transform the array
+ */
+ const c3 = this.c3(p2, input);
+
+ /*
+ * Genreate a proper length encryption key, XOR iteratively, and convert newly encrypted data to hex
+ */
+ const key = this.kdf(p2, input.byteLength);
+ for (let i = 0; i < input.byteLength; i++) {
+ input[i] ^= Utils.ord(key[i]);
+ }
+ const c2 = Buffer.from(input).toString("hex");
+
+ /*
+ * Check user input specs; order the output components as selected
+ */
+ if (this.format === "C1C3C2") {
+ return hexC1X + hexC1Y + c3 + c2;
+ } else {
+ return hexC1X + hexC1Y + c2 + c3;
+ }
+ }
+ /**
+ * Function to decrypt an SM2 encrypted message
+ *
+ * @param {*} input
+ */
+ decrypt(input) {
+ const c1X = input.slice(0, 64);
+ const c1Y = input.slice(64, 128);
+
+ let c3 = "";
+ let c2 = "";
+
+ if (this.format === "C1C3C2") {
+ c3 = input.slice(128, 192);
+ c2 = input.slice(192);
+ } else {
+ c2 = input.slice(128, -64);
+ c3 = input.slice(-64);
+ }
+ c2 = Uint8Array.from(fromHex(c2));
+ const c1 = this.ecParams.curve.decodePointHex("04" + c1X + c1Y);
+
+ /*
+ * Compute the p2 (secret) value by taking the C1 point provided in the encrypted package, and multiplying by the private k value
+ */
+ const p2 = c1.multiply(this.privateKey);
+
+ /*
+ * Similar to encryption; compute sufficient length key material and XOR the input data to recover the original message
+ */
+ const key = this.kdf(p2, c2.byteLength);
+
+ for (let i = 0; i < c2.byteLength; i++) {
+ c2[i] ^= Utils.ord(key[i]);
+ }
+
+ const check = this.c3(p2, c2);
+ if (check === c3) {
+ return c2.buffer;
+ } else {
+ throw new OperationError("Decryption Error -- Computed Hashes Do Not Match");
+ }
+ }
+
+
+ /**
+ * Generates a large random number
+ *
+ * @param {*} limit
+ * @returns
+ */
+ getBigRandom(limit) {
+ return new r.BigInteger(limit.bitLength(), this.rng)
+ .mod(limit.subtract(r.BigInteger.ONE))
+ .add(r.BigInteger.ONE);
+ }
+
+ /**
+ * Helper function for generating a large random K number; utilized for generating our initial C1 point
+ * TODO: Do we need to do any sort of validation on the resulting k values?
+ *
+ * @returns {BigInteger}
+ */
+ generatePublicKey() {
+ const n = this.ecParams.n;
+ const k = this.getBigRandom(n);
+ return k;
+ }
+
+ /**
+ * SM2 Key Derivation Function (KDF); Takes P2 point, and generates a key material stream large enough to encrypt all of the input data
+ *
+ * @param {*} p2
+ * @param {*} len
+ * @returns {string}
+ */
+ kdf(p2, len) {
+ const [hX, hY] = this.getPointAsHex(p2);
+
+ const total = Math.ceil(len / 32) + 1;
+ let cnt = 1;
+
+ let keyMaterial = "";
+
+ while (cnt < total) {
+ const num = Utils.intToByteArray(cnt, 4, "big");
+ const overall = fromHex(hX).concat(fromHex(hY)).concat(num);
+ keyMaterial += this.sm3(overall);
+ cnt++;
+ }
+ return keyMaterial;
+ }
+
+ /**
+ * Calculates the C3 component of our final encrypted payload; which is the SM3 hash of the P2 point and the original, unencrypted input data
+ *
+ * @param {*} p2
+ * @param {*} input
+ * @returns {string}
+ */
+ c3(p2, input) {
+ const [hX, hY] = this.getPointAsHex(p2);
+
+ const overall = fromHex(hX).concat(Array.from(input)).concat(fromHex(hY));
+
+ return toHex(this.sm3(overall));
+
+ }
+
+ /**
+ * SM3 setup helper function; takes input data as an array, processes the hash and returns the result
+ *
+ * @param {*} data
+ * @returns {string}
+ */
+ sm3(data) {
+ const hashData = Utils.arrayBufferToStr(Uint8Array.from(data).buffer, false);
+ const hasher = new Sm3();
+ hasher.update(hashData);
+ return hasher.finalize();
+ }
+
+ /**
+ * Utility function, returns an elliptic curve points X and Y values as hex;
+ *
+ * @param {EcPointFp} point
+ * @returns {[]}
+ */
+ getPointAsHex(point) {
+ const biX = point.getX().toBigInteger();
+ const biY = point.getY().toBigInteger();
+
+ const charlen = this.ecParams.keycharlen;
+ const hX = ("0000000000" + biX.toString(16)).slice(- charlen);
+ const hY = ("0000000000" + biY.toString(16)).slice(- charlen);
+ return [hX, hY];
+ }
+}
diff --git a/src/core/lib/Salsa20.mjs b/src/core/lib/Salsa20.mjs
new file mode 100644
index 000000000..d72831bfd
--- /dev/null
+++ b/src/core/lib/Salsa20.mjs
@@ -0,0 +1,144 @@
+/**
+ * @author joostrijneveld [joost@joostrijneveld.nl]
+ * @copyright Crown Copyright 2024
+ * @license Apache-2.0
+ */
+
+import Utils from "../Utils.mjs";
+
+/**
+ * Computes the Salsa20 permute function
+ *
+ * @param {byteArray} x
+ * @param {integer} rounds
+ */
+function salsa20Permute(x, rounds) {
+ /**
+ * Macro to compute a 32-bit rotate-left operation
+ *
+ * @param {integer} x
+ * @param {integer} n
+ * @returns {integer}
+ */
+ function ROL32(x, n) {
+ return ((x << n) & 0xFFFFFFFF) | (x >>> (32 - n));
+ }
+
+ /**
+ * Macro to compute a single Salsa20 quarterround operation
+ *
+ * @param {integer} x
+ * @param {integer} a
+ * @param {integer} b
+ * @param {integer} c
+ * @param {integer} d
+ * @returns {integer}
+ */
+ function quarterround(x, a, b, c, d) {
+ x[b] ^= ROL32((x[a] + x[d]) & 0xFFFFFFFF, 7);
+ x[c] ^= ROL32((x[b] + x[a]) & 0xFFFFFFFF, 9);
+ x[d] ^= ROL32((x[c] + x[b]) & 0xFFFFFFFF, 13);
+ x[a] ^= ROL32((x[d] + x[c]) & 0xFFFFFFFF, 18);
+ }
+
+ for (let i = 0; i < rounds / 2; i++) {
+ quarterround(x, 0, 4, 8, 12);
+ quarterround(x, 5, 9, 13, 1);
+ quarterround(x, 10, 14, 2, 6);
+ quarterround(x, 15, 3, 7, 11);
+ quarterround(x, 0, 1, 2, 3);
+ quarterround(x, 5, 6, 7, 4);
+ quarterround(x, 10, 11, 8, 9);
+ quarterround(x, 15, 12, 13, 14);
+ }
+}
+
+/**
+ * Computes the Salsa20 block function
+ *
+ * @param {byteArray} key
+ * @param {byteArray} nonce
+ * @param {byteArray} counter
+ * @param {integer} rounds
+ * @returns {byteArray}
+ */
+export function salsa20Block(key, nonce, counter, rounds) {
+ const tau = "expand 16-byte k";
+ const sigma = "expand 32-byte k";
+ let state, c;
+ if (key.length === 16) {
+ c = Utils.strToByteArray(tau);
+ key = key.concat(key);
+ } else {
+ c = Utils.strToByteArray(sigma);
+ }
+
+ state = c.slice(0, 4);
+ state = state.concat(key.slice(0, 16));
+ state = state.concat(c.slice(4, 8));
+ state = state.concat(nonce);
+ state = state.concat(counter);
+ state = state.concat(c.slice(8, 12));
+ state = state.concat(key.slice(16, 32));
+ state = state.concat(c.slice(12, 16));
+
+ const x = Array();
+ for (let i = 0; i < 64; i += 4) {
+ x.push(Utils.byteArrayToInt(state.slice(i, i + 4), "little"));
+ }
+ const a = [...x];
+
+ salsa20Permute(x, rounds);
+
+ for (let i = 0; i < 16; i++) {
+ x[i] = (x[i] + a[i]) & 0xFFFFFFFF;
+ }
+
+ let output = Array();
+ for (let i = 0; i < 16; i++) {
+ output = output.concat(Utils.intToByteArray(x[i], 4, "little"));
+ }
+ return output;
+}
+
+/**
+ * Computes the hSalsa20 function
+ *
+ * @param {byteArray} key
+ * @param {byteArray} nonce
+ * @param {integer} rounds
+ * @returns {byteArray}
+ */
+export function hsalsa20(key, nonce, rounds) {
+ const tau = "expand 16-byte k";
+ const sigma = "expand 32-byte k";
+ let state, c;
+ if (key.length === 16) {
+ c = Utils.strToByteArray(tau);
+ key = key.concat(key);
+ } else {
+ c = Utils.strToByteArray(sigma);
+ }
+
+ state = c.slice(0, 4);
+ state = state.concat(key.slice(0, 16));
+ state = state.concat(c.slice(4, 8));
+ state = state.concat(nonce);
+ state = state.concat(c.slice(8, 12));
+ state = state.concat(key.slice(16, 32));
+ state = state.concat(c.slice(12, 16));
+
+ const x = Array();
+ for (let i = 0; i < 64; i += 4) {
+ x.push(Utils.byteArrayToInt(state.slice(i, i + 4), "little"));
+ }
+
+ salsa20Permute(x, rounds);
+
+ let output = Array();
+ const idx = [0, 5, 10, 15, 6, 7, 8, 9];
+ for (let i = 0; i < 8; i++) {
+ output = output.concat(Utils.intToByteArray(x[idx[i]], 4, "little"));
+ }
+ return output;
+}
diff --git a/src/core/lib/Stream.mjs b/src/core/lib/Stream.mjs
index 18ce71c30..8253c4cf0 100644
--- a/src/core/lib/Stream.mjs
+++ b/src/core/lib/Stream.mjs
@@ -18,12 +18,23 @@ export default class Stream {
* Stream constructor.
*
* @param {Uint8Array} input
+ * @param {number} pos
+ * @param {number} bitPos
*/
- constructor(input) {
+ constructor(input, pos=0, bitPos=0) {
this.bytes = input;
this.length = this.bytes.length;
- this.position = 0;
- this.bitPos = 0;
+ this.position = pos;
+ this.bitPos = bitPos;
+ }
+
+ /**
+ * Clone this Stream returning a new identical Stream.
+ *
+ * @returns {Stream}
+ */
+ clone() {
+ return new Stream(this.bytes, this.position, this.bitPos);
}
/**
diff --git a/src/core/lib/TLS.mjs b/src/core/lib/TLS.mjs
new file mode 100644
index 000000000..eaf661a89
--- /dev/null
+++ b/src/core/lib/TLS.mjs
@@ -0,0 +1,877 @@
+/**
+ * TLS resources.
+ *
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2024
+ * @license Apache-2.0
+ */
+
+import OperationError from "../errors/OperationError.mjs";
+import Stream from "../lib/Stream.mjs";
+
+/**
+ * Parse a TLS Record
+ * @param {Uint8Array} bytes
+ * @returns {JSON}
+ */
+export function parseTLSRecord(bytes) {
+ const s = new Stream(bytes);
+ const b = s.clone();
+ const r = {};
+
+ // Content type
+ r.contentType = {
+ description: "Content Type",
+ length: 1,
+ data: b.getBytes(1),
+ value: s.readInt(1)
+ };
+ if (r.contentType.value !== 0x16)
+ throw new OperationError("Not handshake data.");
+
+ // Version
+ r.version = {
+ description: "Protocol Version",
+ length: 2,
+ data: b.getBytes(2),
+ value: s.readInt(2)
+ };
+
+ // Length
+ r.length = {
+ description: "Record Length",
+ length: 2,
+ data: b.getBytes(2),
+ value: s.readInt(2)
+ };
+ if (s.length !== r.length.value + 5)
+ throw new OperationError("Incorrect handshake length.");
+
+ // Handshake
+ r.handshake = {
+ description: "Handshake",
+ length: r.length.value,
+ data: b.getBytes(r.length.value),
+ value: parseHandshake(s.getBytes(r.length.value))
+ };
+
+ return r;
+}
+
+/**
+ * Parse a TLS Handshake
+ * @param {Uint8Array} bytes
+ * @returns {JSON}
+ */
+function parseHandshake(bytes) {
+ const s = new Stream(bytes);
+ const b = s.clone();
+ const h = {};
+
+ // Handshake type
+ h.handshakeType = {
+ description: "Handshake Type",
+ length: 1,
+ data: b.getBytes(1),
+ value: s.readInt(1)
+ };
+
+ // Handshake length
+ h.handshakeLength = {
+ description: "Handshake Length",
+ length: 3,
+ data: b.getBytes(3),
+ value: s.readInt(3)
+ };
+ if (s.length !== h.handshakeLength.value + 4)
+ throw new OperationError("Not enough data in Handshake message.");
+
+
+ switch (h.handshakeType.value) {
+ case 0x01:
+ h.handshakeType.description = "Client Hello";
+ parseClientHello(s, b, h);
+ break;
+ case 0x02:
+ h.handshakeType.description = "Server Hello";
+ parseServerHello(s, b, h);
+ break;
+ default:
+ throw new OperationError("Not a known handshake message.");
+ }
+
+ return h;
+}
+
+/**
+ * Parse a TLS Client Hello
+ * @param {Stream} s
+ * @param {Stream} b
+ * @param {Object} h
+ * @returns {JSON}
+ */
+function parseClientHello(s, b, h) {
+ // Hello version
+ h.helloVersion = {
+ description: "Client Hello Version",
+ length: 2,
+ data: b.getBytes(2),
+ value: s.readInt(2)
+ };
+
+ // Random
+ h.random = {
+ description: "Client Random",
+ length: 32,
+ data: b.getBytes(32),
+ value: s.getBytes(32)
+ };
+
+ // Session ID Length
+ h.sessionIDLength = {
+ description: "Session ID Length",
+ length: 1,
+ data: b.getBytes(1),
+ value: s.readInt(1)
+ };
+
+ // Session ID
+ h.sessionID = {
+ description: "Session ID",
+ length: h.sessionIDLength.value,
+ data: b.getBytes(h.sessionIDLength.value),
+ value: s.getBytes(h.sessionIDLength.value)
+ };
+
+ // Cipher Suites Length
+ h.cipherSuitesLength = {
+ description: "Cipher Suites Length",
+ length: 2,
+ data: b.getBytes(2),
+ value: s.readInt(2)
+ };
+
+ // Cipher Suites
+ h.cipherSuites = {
+ description: "Cipher Suites",
+ length: h.cipherSuitesLength.value,
+ data: b.getBytes(h.cipherSuitesLength.value),
+ value: parseCipherSuites(s.getBytes(h.cipherSuitesLength.value))
+ };
+
+ // Compression Methods Length
+ h.compressionMethodsLength = {
+ description: "Compression Methods Length",
+ length: 1,
+ data: b.getBytes(1),
+ value: s.readInt(1)
+ };
+
+ // Compression Methods
+ h.compressionMethods = {
+ description: "Compression Methods",
+ length: h.compressionMethodsLength.value,
+ data: b.getBytes(h.compressionMethodsLength.value),
+ value: parseCompressionMethods(s.getBytes(h.compressionMethodsLength.value))
+ };
+
+ // Extensions Length
+ h.extensionsLength = {
+ description: "Extensions Length",
+ length: 2,
+ data: b.getBytes(2),
+ value: s.readInt(2)
+ };
+
+ // Extensions
+ h.extensions = {
+ description: "Extensions",
+ length: h.extensionsLength.value,
+ data: b.getBytes(h.extensionsLength.value),
+ value: parseExtensions(s.getBytes(h.extensionsLength.value))
+ };
+
+ return h;
+}
+
+/**
+ * Parse a TLS Server Hello
+ * @param {Stream} s
+ * @param {Stream} b
+ * @param {Object} h
+ * @returns {JSON}
+ */
+function parseServerHello(s, b, h) {
+ // Hello version
+ h.helloVersion = {
+ description: "Server Hello Version",
+ length: 2,
+ data: b.getBytes(2),
+ value: s.readInt(2)
+ };
+
+ // Random
+ h.random = {
+ description: "Server Random",
+ length: 32,
+ data: b.getBytes(32),
+ value: s.getBytes(32)
+ };
+
+ // Session ID Length
+ h.sessionIDLength = {
+ description: "Session ID Length",
+ length: 1,
+ data: b.getBytes(1),
+ value: s.readInt(1)
+ };
+
+ // Session ID
+ h.sessionID = {
+ description: "Session ID",
+ length: h.sessionIDLength.value,
+ data: b.getBytes(h.sessionIDLength.value),
+ value: s.getBytes(h.sessionIDLength.value)
+ };
+
+ // Cipher Suite
+ h.cipherSuite = {
+ description: "Selected Cipher Suite",
+ length: 2,
+ data: b.getBytes(2),
+ value: CIPHER_SUITES_LOOKUP[s.readInt(2)] || "Unknown"
+ };
+
+ // Compression Method
+ h.compressionMethod = {
+ description: "Selected Compression Method",
+ length: 1,
+ data: b.getBytes(1),
+ value: s.readInt(1) // TODO: Compression method name here
+ };
+
+ // Extensions Length
+ h.extensionsLength = {
+ description: "Extensions Length",
+ length: 2,
+ data: b.getBytes(2),
+ value: s.readInt(2)
+ };
+
+ // Extensions
+ h.extensions = {
+ description: "Extensions",
+ length: h.extensionsLength.value,
+ data: b.getBytes(h.extensionsLength.value),
+ value: parseExtensions(s.getBytes(h.extensionsLength.value))
+ };
+}
+
+/**
+ * Parse Cipher Suites
+ * @param {Uint8Array} bytes
+ * @returns {JSON}
+ */
+function parseCipherSuites(bytes) {
+ const s = new Stream(bytes);
+ const b = s.clone();
+ const cs = [];
+
+ while (s.hasMore()) {
+ cs.push({
+ description: "Cipher Suite",
+ length: 2,
+ data: b.getBytes(2),
+ value: CIPHER_SUITES_LOOKUP[s.readInt(2)] || "Unknown"
+ });
+ }
+ return cs;
+}
+
+/**
+ * Parse Compression Methods
+ * @param {Uint8Array} bytes
+ * @returns {JSON}
+ */
+function parseCompressionMethods(bytes) {
+ const s = new Stream(bytes);
+ const b = s.clone();
+ const cm = [];
+
+ while (s.hasMore()) {
+ cm.push({
+ description: "Compression Method",
+ length: 1,
+ data: b.getBytes(1),
+ value: s.readInt(1) // TODO: Compression method name here
+ });
+ }
+ return cm;
+}
+
+/**
+ * Parse Extensions
+ * @param {Uint8Array} bytes
+ * @returns {JSON}
+ */
+function parseExtensions(bytes) {
+ const s = new Stream(bytes);
+ const b = s.clone();
+
+ const exts = [];
+ while (s.hasMore()) {
+ const ext = {};
+
+ // Type
+ ext.type = {
+ description: "Extension Type",
+ length: 2,
+ data: b.getBytes(2),
+ value: EXTENSION_LOOKUP[s.readInt(2)] || "unknown"
+ };
+
+ // Length
+ ext.length = {
+ description: "Extension Length",
+ length: 2,
+ data: b.getBytes(2),
+ value: s.readInt(2)
+ };
+
+ // Value
+ ext.value = {
+ description: "Extension Value",
+ length: ext.length.value,
+ data: b.getBytes(ext.length.value),
+ value: s.getBytes(ext.length.value)
+ };
+
+ exts.push(ext);
+ }
+
+ return exts;
+}
+
+/**
+ * Extension type lookup table
+ */
+const EXTENSION_LOOKUP = {
+ 0: "server_name",
+ 1: "max_fragment_length",
+ 2: "client_certificate_url",
+ 3: "trusted_ca_keys",
+ 4: "truncated_hmac",
+ 5: "status_request",
+ 6: "user_mapping",
+ 7: "client_authz",
+ 8: "server_authz",
+ 9: "cert_type",
+ 10: "supported_groups",
+ 11: "ec_point_formats",
+ 12: "srp",
+ 13: "signature_algorithms",
+ 14: "use_srtp",
+ 15: "heartbeat",
+ 16: "application_layer_protocol_negotiation",
+ 17: "status_request_v2",
+ 18: "signed_certificate_timestamp",
+ 19: "client_certificate_type",
+ 20: "server_certificate_type",
+ 21: "padding",
+ 22: "encrypt_then_mac",
+ 23: "extended_master_secret",
+ 24: "token_binding",
+ 25: "cached_info",
+ 26: "tls_lts",
+ 27: "compress_certificate",
+ 28: "record_size_limit",
+ 29: "pwd_protect",
+ 30: "pwd_clear",
+ 31: "password_salt",
+ 32: "ticket_pinning",
+ 33: "tls_cert_with_extern_psk",
+ 34: "delegated_credential",
+ 35: "session_ticket",
+ 36: "TLMSP",
+ 37: "TLMSP_proxying",
+ 38: "TLMSP_delegate",
+ 39: "supported_ekt_ciphers",
+ 40: "Reserved",
+ 41: "pre_shared_key",
+ 42: "early_data",
+ 43: "supported_versions",
+ 44: "cookie",
+ 45: "psk_key_exchange_modes",
+ 46: "Reserved",
+ 47: "certificate_authorities",
+ 48: "oid_filters",
+ 49: "post_handshake_auth",
+ 50: "signature_algorithms_cert",
+ 51: "key_share",
+ 52: "transparency_info",
+ 53: "connection_id (deprecated)",
+ 54: "connection_id",
+ 55: "external_id_hash",
+ 56: "external_session_id",
+ 57: "quic_transport_parameters",
+ 58: "ticket_request",
+ 59: "dnssec_chain",
+ 60: "sequence_number_encryption_algorithms",
+ 61: "rrc",
+ 2570: "GREASE",
+ 6682: "GREASE",
+ 10794: "GREASE",
+ 14906: "GREASE",
+ 17513: "application_settings",
+ 19018: "GREASE",
+ 23130: "GREASE",
+ 27242: "GREASE",
+ 31354: "GREASE",
+ 35466: "GREASE",
+ 39578: "GREASE",
+ 43690: "GREASE",
+ 47802: "GREASE",
+ 51914: "GREASE",
+ 56026: "GREASE",
+ 60138: "GREASE",
+ 64250: "GREASE",
+ 64768: "ech_outer_extensions",
+ 65037: "encrypted_client_hello",
+ 65281: "renegotiation_info"
+};
+
+/**
+ * Cipher suites lookup table
+ */
+const CIPHER_SUITES_LOOKUP = {
+ 0x0000: "TLS_NULL_WITH_NULL_NULL",
+ 0x0001: "TLS_RSA_WITH_NULL_MD5",
+ 0x0002: "TLS_RSA_WITH_NULL_SHA",
+ 0x0003: "TLS_RSA_EXPORT_WITH_RC4_40_MD5",
+ 0x0004: "TLS_RSA_WITH_RC4_128_MD5",
+ 0x0005: "TLS_RSA_WITH_RC4_128_SHA",
+ 0x0006: "TLS_RSA_EXPORT_WITH_RC2_CBC_40_MD5",
+ 0x0007: "TLS_RSA_WITH_IDEA_CBC_SHA",
+ 0x0008: "TLS_RSA_EXPORT_WITH_DES40_CBC_SHA",
+ 0x0009: "TLS_RSA_WITH_DES_CBC_SHA",
+ 0x000A: "TLS_RSA_WITH_3DES_EDE_CBC_SHA",
+ 0x000B: "TLS_DH_DSS_EXPORT_WITH_DES40_CBC_SHA",
+ 0x000C: "TLS_DH_DSS_WITH_DES_CBC_SHA",
+ 0x000D: "TLS_DH_DSS_WITH_3DES_EDE_CBC_SHA",
+ 0x000E: "TLS_DH_RSA_EXPORT_WITH_DES40_CBC_SHA",
+ 0x000F: "TLS_DH_RSA_WITH_DES_CBC_SHA",
+ 0x0010: "TLS_DH_RSA_WITH_3DES_EDE_CBC_SHA",
+ 0x0011: "TLS_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA",
+ 0x0012: "TLS_DHE_DSS_WITH_DES_CBC_SHA",
+ 0x0013: "TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA",
+ 0x0014: "TLS_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA",
+ 0x0015: "TLS_DHE_RSA_WITH_DES_CBC_SHA",
+ 0x0016: "TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA",
+ 0x0017: "TLS_DH_anon_EXPORT_WITH_RC4_40_MD5",
+ 0x0018: "TLS_DH_anon_WITH_RC4_128_MD5",
+ 0x0019: "TLS_DH_anon_EXPORT_WITH_DES40_CBC_SHA",
+ 0x001A: "TLS_DH_anon_WITH_DES_CBC_SHA",
+ 0x001B: "TLS_DH_anon_WITH_3DES_EDE_CBC_SHA",
+ 0x001E: "TLS_KRB5_WITH_DES_CBC_SHA",
+ 0x001F: "TLS_KRB5_WITH_3DES_EDE_CBC_SHA",
+ 0x0020: "TLS_KRB5_WITH_RC4_128_SHA",
+ 0x0021: "TLS_KRB5_WITH_IDEA_CBC_SHA",
+ 0x0022: "TLS_KRB5_WITH_DES_CBC_MD5",
+ 0x0023: "TLS_KRB5_WITH_3DES_EDE_CBC_MD5",
+ 0x0024: "TLS_KRB5_WITH_RC4_128_MD5",
+ 0x0025: "TLS_KRB5_WITH_IDEA_CBC_MD5",
+ 0x0026: "TLS_KRB5_EXPORT_WITH_DES_CBC_40_SHA",
+ 0x0027: "TLS_KRB5_EXPORT_WITH_RC2_CBC_40_SHA",
+ 0x0028: "TLS_KRB5_EXPORT_WITH_RC4_40_SHA",
+ 0x0029: "TLS_KRB5_EXPORT_WITH_DES_CBC_40_MD5",
+ 0x002A: "TLS_KRB5_EXPORT_WITH_RC2_CBC_40_MD5",
+ 0x002B: "TLS_KRB5_EXPORT_WITH_RC4_40_MD5",
+ 0x002C: "TLS_PSK_WITH_NULL_SHA",
+ 0x002D: "TLS_DHE_PSK_WITH_NULL_SHA",
+ 0x002E: "TLS_RSA_PSK_WITH_NULL_SHA",
+ 0x002F: "TLS_RSA_WITH_AES_128_CBC_SHA",
+ 0x0030: "TLS_DH_DSS_WITH_AES_128_CBC_SHA",
+ 0x0031: "TLS_DH_RSA_WITH_AES_128_CBC_SHA",
+ 0x0032: "TLS_DHE_DSS_WITH_AES_128_CBC_SHA",
+ 0x0033: "TLS_DHE_RSA_WITH_AES_128_CBC_SHA",
+ 0x0034: "TLS_DH_anon_WITH_AES_128_CBC_SHA",
+ 0x0035: "TLS_RSA_WITH_AES_256_CBC_SHA",
+ 0x0036: "TLS_DH_DSS_WITH_AES_256_CBC_SHA",
+ 0x0037: "TLS_DH_RSA_WITH_AES_256_CBC_SHA",
+ 0x0038: "TLS_DHE_DSS_WITH_AES_256_CBC_SHA",
+ 0x0039: "TLS_DHE_RSA_WITH_AES_256_CBC_SHA",
+ 0x003A: "TLS_DH_anon_WITH_AES_256_CBC_SHA",
+ 0x003B: "TLS_RSA_WITH_NULL_SHA256",
+ 0x003C: "TLS_RSA_WITH_AES_128_CBC_SHA256",
+ 0x003D: "TLS_RSA_WITH_AES_256_CBC_SHA256",
+ 0x003E: "TLS_DH_DSS_WITH_AES_128_CBC_SHA256",
+ 0x003F: "TLS_DH_RSA_WITH_AES_128_CBC_SHA256",
+ 0x0040: "TLS_DHE_DSS_WITH_AES_128_CBC_SHA256",
+ 0x0041: "TLS_RSA_WITH_CAMELLIA_128_CBC_SHA",
+ 0x0042: "TLS_DH_DSS_WITH_CAMELLIA_128_CBC_SHA",
+ 0x0043: "TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA",
+ 0x0044: "TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA",
+ 0x0045: "TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA",
+ 0x0046: "TLS_DH_anon_WITH_CAMELLIA_128_CBC_SHA",
+ 0x0067: "TLS_DHE_RSA_WITH_AES_128_CBC_SHA256",
+ 0x0068: "TLS_DH_DSS_WITH_AES_256_CBC_SHA256",
+ 0x0069: "TLS_DH_RSA_WITH_AES_256_CBC_SHA256",
+ 0x006A: "TLS_DHE_DSS_WITH_AES_256_CBC_SHA256",
+ 0x006B: "TLS_DHE_RSA_WITH_AES_256_CBC_SHA256",
+ 0x006C: "TLS_DH_anon_WITH_AES_128_CBC_SHA256",
+ 0x006D: "TLS_DH_anon_WITH_AES_256_CBC_SHA256",
+ 0x0084: "TLS_RSA_WITH_CAMELLIA_256_CBC_SHA",
+ 0x0085: "TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA",
+ 0x0086: "TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA",
+ 0x0087: "TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA",
+ 0x0088: "TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA",
+ 0x0089: "TLS_DH_anon_WITH_CAMELLIA_256_CBC_SHA",
+ 0x008A: "TLS_PSK_WITH_RC4_128_SHA",
+ 0x008B: "TLS_PSK_WITH_3DES_EDE_CBC_SHA",
+ 0x008C: "TLS_PSK_WITH_AES_128_CBC_SHA",
+ 0x008D: "TLS_PSK_WITH_AES_256_CBC_SHA",
+ 0x008E: "TLS_DHE_PSK_WITH_RC4_128_SHA",
+ 0x008F: "TLS_DHE_PSK_WITH_3DES_EDE_CBC_SHA",
+ 0x0090: "TLS_DHE_PSK_WITH_AES_128_CBC_SHA",
+ 0x0091: "TLS_DHE_PSK_WITH_AES_256_CBC_SHA",
+ 0x0092: "TLS_RSA_PSK_WITH_RC4_128_SHA",
+ 0x0093: "TLS_RSA_PSK_WITH_3DES_EDE_CBC_SHA",
+ 0x0094: "TLS_RSA_PSK_WITH_AES_128_CBC_SHA",
+ 0x0095: "TLS_RSA_PSK_WITH_AES_256_CBC_SHA",
+ 0x0096: "TLS_RSA_WITH_SEED_CBC_SHA",
+ 0x0097: "TLS_DH_DSS_WITH_SEED_CBC_SHA",
+ 0x0098: "TLS_DH_RSA_WITH_SEED_CBC_SHA",
+ 0x0099: "TLS_DHE_DSS_WITH_SEED_CBC_SHA",
+ 0x009A: "TLS_DHE_RSA_WITH_SEED_CBC_SHA",
+ 0x009B: "TLS_DH_anon_WITH_SEED_CBC_SHA",
+ 0x009C: "TLS_RSA_WITH_AES_128_GCM_SHA256",
+ 0x009D: "TLS_RSA_WITH_AES_256_GCM_SHA384",
+ 0x009E: "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256",
+ 0x009F: "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384",
+ 0x00A0: "TLS_DH_RSA_WITH_AES_128_GCM_SHA256",
+ 0x00A1: "TLS_DH_RSA_WITH_AES_256_GCM_SHA384",
+ 0x00A2: "TLS_DHE_DSS_WITH_AES_128_GCM_SHA256",
+ 0x00A3: "TLS_DHE_DSS_WITH_AES_256_GCM_SHA384",
+ 0x00A4: "TLS_DH_DSS_WITH_AES_128_GCM_SHA256",
+ 0x00A5: "TLS_DH_DSS_WITH_AES_256_GCM_SHA384",
+ 0x00A6: "TLS_DH_anon_WITH_AES_128_GCM_SHA256",
+ 0x00A7: "TLS_DH_anon_WITH_AES_256_GCM_SHA384",
+ 0x00A8: "TLS_PSK_WITH_AES_128_GCM_SHA256",
+ 0x00A9: "TLS_PSK_WITH_AES_256_GCM_SHA384",
+ 0x00AA: "TLS_DHE_PSK_WITH_AES_128_GCM_SHA256",
+ 0x00AB: "TLS_DHE_PSK_WITH_AES_256_GCM_SHA384",
+ 0x00AC: "TLS_RSA_PSK_WITH_AES_128_GCM_SHA256",
+ 0x00AD: "TLS_RSA_PSK_WITH_AES_256_GCM_SHA384",
+ 0x00AE: "TLS_PSK_WITH_AES_128_CBC_SHA256",
+ 0x00AF: "TLS_PSK_WITH_AES_256_CBC_SHA384",
+ 0x00B0: "TLS_PSK_WITH_NULL_SHA256",
+ 0x00B1: "TLS_PSK_WITH_NULL_SHA384",
+ 0x00B2: "TLS_DHE_PSK_WITH_AES_128_CBC_SHA256",
+ 0x00B3: "TLS_DHE_PSK_WITH_AES_256_CBC_SHA384",
+ 0x00B4: "TLS_DHE_PSK_WITH_NULL_SHA256",
+ 0x00B5: "TLS_DHE_PSK_WITH_NULL_SHA384",
+ 0x00B6: "TLS_RSA_PSK_WITH_AES_128_CBC_SHA256",
+ 0x00B7: "TLS_RSA_PSK_WITH_AES_256_CBC_SHA384",
+ 0x00B8: "TLS_RSA_PSK_WITH_NULL_SHA256",
+ 0x00B9: "TLS_RSA_PSK_WITH_NULL_SHA384",
+ 0x00BA: "TLS_RSA_WITH_CAMELLIA_128_CBC_SHA256",
+ 0x00BB: "TLS_DH_DSS_WITH_CAMELLIA_128_CBC_SHA256",
+ 0x00BC: "TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA256",
+ 0x00BD: "TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA256",
+ 0x00BE: "TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA256",
+ 0x00BF: "TLS_DH_anon_WITH_CAMELLIA_128_CBC_SHA256",
+ 0x00C0: "TLS_RSA_WITH_CAMELLIA_256_CBC_SHA256",
+ 0x00C1: "TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA256",
+ 0x00C2: "TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA256",
+ 0x00C3: "TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA256",
+ 0x00C4: "TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA256",
+ 0x00C5: "TLS_DH_anon_WITH_CAMELLIA_256_CBC_SHA256",
+ 0x00C6: "TLS_SM4_GCM_SM3",
+ 0x00C7: "TLS_SM4_CCM_SM3",
+ 0x00FF: "TLS_EMPTY_RENEGOTIATION_INFO_SCSV",
+ 0x0A0A: "GREASE",
+ 0x1301: "TLS_AES_128_GCM_SHA256",
+ 0x1302: "TLS_AES_256_GCM_SHA384",
+ 0x1303: "TLS_CHACHA20_POLY1305_SHA256",
+ 0x1304: "TLS_AES_128_CCM_SHA256",
+ 0x1305: "TLS_AES_128_CCM_8_SHA256",
+ 0x1306: "TLS_AEGIS_256_SHA512",
+ 0x1307: "TLS_AEGIS_128L_SHA256",
+ 0x1A1A: "GREASE",
+ 0x2A2A: "GREASE",
+ 0x3A3A: "GREASE",
+ 0x4A4A: "GREASE",
+ 0x5600: "TLS_FALLBACK_SCSV",
+ 0x5A5A: "GREASE",
+ 0x6A6A: "GREASE",
+ 0x7A7A: "GREASE",
+ 0x8A8A: "GREASE",
+ 0x9A9A: "GREASE",
+ 0xAAAA: "GREASE",
+ 0xBABA: "GREASE",
+ 0xC001: "TLS_ECDH_ECDSA_WITH_NULL_SHA",
+ 0xC002: "TLS_ECDH_ECDSA_WITH_RC4_128_SHA",
+ 0xC003: "TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA",
+ 0xC004: "TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA",
+ 0xC005: "TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA",
+ 0xC006: "TLS_ECDHE_ECDSA_WITH_NULL_SHA",
+ 0xC007: "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA",
+ 0xC008: "TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA",
+ 0xC009: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
+ 0xC00A: "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA",
+ 0xC00B: "TLS_ECDH_RSA_WITH_NULL_SHA",
+ 0xC00C: "TLS_ECDH_RSA_WITH_RC4_128_SHA",
+ 0xC00D: "TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA",
+ 0xC00E: "TLS_ECDH_RSA_WITH_AES_128_CBC_SHA",
+ 0xC00F: "TLS_ECDH_RSA_WITH_AES_256_CBC_SHA",
+ 0xC010: "TLS_ECDHE_RSA_WITH_NULL_SHA",
+ 0xC011: "TLS_ECDHE_RSA_WITH_RC4_128_SHA",
+ 0xC012: "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA",
+ 0xC013: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
+ 0xC014: "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
+ 0xC015: "TLS_ECDH_anon_WITH_NULL_SHA",
+ 0xC016: "TLS_ECDH_anon_WITH_RC4_128_SHA",
+ 0xC017: "TLS_ECDH_anon_WITH_3DES_EDE_CBC_SHA",
+ 0xC018: "TLS_ECDH_anon_WITH_AES_128_CBC_SHA",
+ 0xC019: "TLS_ECDH_anon_WITH_AES_256_CBC_SHA",
+ 0xC01A: "TLS_SRP_SHA_WITH_3DES_EDE_CBC_SHA",
+ 0xC01B: "TLS_SRP_SHA_RSA_WITH_3DES_EDE_CBC_SHA",
+ 0xC01C: "TLS_SRP_SHA_DSS_WITH_3DES_EDE_CBC_SHA",
+ 0xC01D: "TLS_SRP_SHA_WITH_AES_128_CBC_SHA",
+ 0xC01E: "TLS_SRP_SHA_RSA_WITH_AES_128_CBC_SHA",
+ 0xC01F: "TLS_SRP_SHA_DSS_WITH_AES_128_CBC_SHA",
+ 0xC020: "TLS_SRP_SHA_WITH_AES_256_CBC_SHA",
+ 0xC021: "TLS_SRP_SHA_RSA_WITH_AES_256_CBC_SHA",
+ 0xC022: "TLS_SRP_SHA_DSS_WITH_AES_256_CBC_SHA",
+ 0xC023: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
+ 0xC024: "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384",
+ 0xC025: "TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256",
+ 0xC026: "TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384",
+ 0xC027: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256",
+ 0xC028: "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384",
+ 0xC029: "TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256",
+ 0xC02A: "TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384",
+ 0xC02B: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
+ 0xC02C: "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
+ 0xC02D: "TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256",
+ 0xC02E: "TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384",
+ 0xC02F: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
+ 0xC030: "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
+ 0xC031: "TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256",
+ 0xC032: "TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384",
+ 0xC033: "TLS_ECDHE_PSK_WITH_RC4_128_SHA",
+ 0xC034: "TLS_ECDHE_PSK_WITH_3DES_EDE_CBC_SHA",
+ 0xC035: "TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA",
+ 0xC036: "TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA",
+ 0xC037: "TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256",
+ 0xC038: "TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA384",
+ 0xC039: "TLS_ECDHE_PSK_WITH_NULL_SHA",
+ 0xC03A: "TLS_ECDHE_PSK_WITH_NULL_SHA256",
+ 0xC03B: "TLS_ECDHE_PSK_WITH_NULL_SHA384",
+ 0xC03C: "TLS_RSA_WITH_ARIA_128_CBC_SHA256",
+ 0xC03D: "TLS_RSA_WITH_ARIA_256_CBC_SHA384",
+ 0xC03E: "TLS_DH_DSS_WITH_ARIA_128_CBC_SHA256",
+ 0xC03F: "TLS_DH_DSS_WITH_ARIA_256_CBC_SHA384",
+ 0xC040: "TLS_DH_RSA_WITH_ARIA_128_CBC_SHA256",
+ 0xC041: "TLS_DH_RSA_WITH_ARIA_256_CBC_SHA384",
+ 0xC042: "TLS_DHE_DSS_WITH_ARIA_128_CBC_SHA256",
+ 0xC043: "TLS_DHE_DSS_WITH_ARIA_256_CBC_SHA384",
+ 0xC044: "TLS_DHE_RSA_WITH_ARIA_128_CBC_SHA256",
+ 0xC045: "TLS_DHE_RSA_WITH_ARIA_256_CBC_SHA384",
+ 0xC046: "TLS_DH_anon_WITH_ARIA_128_CBC_SHA256",
+ 0xC047: "TLS_DH_anon_WITH_ARIA_256_CBC_SHA384",
+ 0xC048: "TLS_ECDHE_ECDSA_WITH_ARIA_128_CBC_SHA256",
+ 0xC049: "TLS_ECDHE_ECDSA_WITH_ARIA_256_CBC_SHA384",
+ 0xC04A: "TLS_ECDH_ECDSA_WITH_ARIA_128_CBC_SHA256",
+ 0xC04B: "TLS_ECDH_ECDSA_WITH_ARIA_256_CBC_SHA384",
+ 0xC04C: "TLS_ECDHE_RSA_WITH_ARIA_128_CBC_SHA256",
+ 0xC04D: "TLS_ECDHE_RSA_WITH_ARIA_256_CBC_SHA384",
+ 0xC04E: "TLS_ECDH_RSA_WITH_ARIA_128_CBC_SHA256",
+ 0xC04F: "TLS_ECDH_RSA_WITH_ARIA_256_CBC_SHA384",
+ 0xC050: "TLS_RSA_WITH_ARIA_128_GCM_SHA256",
+ 0xC051: "TLS_RSA_WITH_ARIA_256_GCM_SHA384",
+ 0xC052: "TLS_DHE_RSA_WITH_ARIA_128_GCM_SHA256",
+ 0xC053: "TLS_DHE_RSA_WITH_ARIA_256_GCM_SHA384",
+ 0xC054: "TLS_DH_RSA_WITH_ARIA_128_GCM_SHA256",
+ 0xC055: "TLS_DH_RSA_WITH_ARIA_256_GCM_SHA384",
+ 0xC056: "TLS_DHE_DSS_WITH_ARIA_128_GCM_SHA256",
+ 0xC057: "TLS_DHE_DSS_WITH_ARIA_256_GCM_SHA384",
+ 0xC058: "TLS_DH_DSS_WITH_ARIA_128_GCM_SHA256",
+ 0xC059: "TLS_DH_DSS_WITH_ARIA_256_GCM_SHA384",
+ 0xC05A: "TLS_DH_anon_WITH_ARIA_128_GCM_SHA256",
+ 0xC05B: "TLS_DH_anon_WITH_ARIA_256_GCM_SHA384",
+ 0xC05C: "TLS_ECDHE_ECDSA_WITH_ARIA_128_GCM_SHA256",
+ 0xC05D: "TLS_ECDHE_ECDSA_WITH_ARIA_256_GCM_SHA384",
+ 0xC05E: "TLS_ECDH_ECDSA_WITH_ARIA_128_GCM_SHA256",
+ 0xC05F: "TLS_ECDH_ECDSA_WITH_ARIA_256_GCM_SHA384",
+ 0xC060: "TLS_ECDHE_RSA_WITH_ARIA_128_GCM_SHA256",
+ 0xC061: "TLS_ECDHE_RSA_WITH_ARIA_256_GCM_SHA384",
+ 0xC062: "TLS_ECDH_RSA_WITH_ARIA_128_GCM_SHA256",
+ 0xC063: "TLS_ECDH_RSA_WITH_ARIA_256_GCM_SHA384",
+ 0xC064: "TLS_PSK_WITH_ARIA_128_CBC_SHA256",
+ 0xC065: "TLS_PSK_WITH_ARIA_256_CBC_SHA384",
+ 0xC066: "TLS_DHE_PSK_WITH_ARIA_128_CBC_SHA256",
+ 0xC067: "TLS_DHE_PSK_WITH_ARIA_256_CBC_SHA384",
+ 0xC068: "TLS_RSA_PSK_WITH_ARIA_128_CBC_SHA256",
+ 0xC069: "TLS_RSA_PSK_WITH_ARIA_256_CBC_SHA384",
+ 0xC06A: "TLS_PSK_WITH_ARIA_128_GCM_SHA256",
+ 0xC06B: "TLS_PSK_WITH_ARIA_256_GCM_SHA384",
+ 0xC06C: "TLS_DHE_PSK_WITH_ARIA_128_GCM_SHA256",
+ 0xC06D: "TLS_DHE_PSK_WITH_ARIA_256_GCM_SHA384",
+ 0xC06E: "TLS_RSA_PSK_WITH_ARIA_128_GCM_SHA256",
+ 0xC06F: "TLS_RSA_PSK_WITH_ARIA_256_GCM_SHA384",
+ 0xC070: "TLS_ECDHE_PSK_WITH_ARIA_128_CBC_SHA256",
+ 0xC071: "TLS_ECDHE_PSK_WITH_ARIA_256_CBC_SHA384",
+ 0xC072: "TLS_ECDHE_ECDSA_WITH_CAMELLIA_128_CBC_SHA256",
+ 0xC073: "TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_CBC_SHA384",
+ 0xC074: "TLS_ECDH_ECDSA_WITH_CAMELLIA_128_CBC_SHA256",
+ 0xC075: "TLS_ECDH_ECDSA_WITH_CAMELLIA_256_CBC_SHA384",
+ 0xC076: "TLS_ECDHE_RSA_WITH_CAMELLIA_128_CBC_SHA256",
+ 0xC077: "TLS_ECDHE_RSA_WITH_CAMELLIA_256_CBC_SHA384",
+ 0xC078: "TLS_ECDH_RSA_WITH_CAMELLIA_128_CBC_SHA256",
+ 0xC079: "TLS_ECDH_RSA_WITH_CAMELLIA_256_CBC_SHA384",
+ 0xC07A: "TLS_RSA_WITH_CAMELLIA_128_GCM_SHA256",
+ 0xC07B: "TLS_RSA_WITH_CAMELLIA_256_GCM_SHA384",
+ 0xC07C: "TLS_DHE_RSA_WITH_CAMELLIA_128_GCM_SHA256",
+ 0xC07D: "TLS_DHE_RSA_WITH_CAMELLIA_256_GCM_SHA384",
+ 0xC07E: "TLS_DH_RSA_WITH_CAMELLIA_128_GCM_SHA256",
+ 0xC07F: "TLS_DH_RSA_WITH_CAMELLIA_256_GCM_SHA384",
+ 0xC080: "TLS_DHE_DSS_WITH_CAMELLIA_128_GCM_SHA256",
+ 0xC081: "TLS_DHE_DSS_WITH_CAMELLIA_256_GCM_SHA384",
+ 0xC082: "TLS_DH_DSS_WITH_CAMELLIA_128_GCM_SHA256",
+ 0xC083: "TLS_DH_DSS_WITH_CAMELLIA_256_GCM_SHA384",
+ 0xC084: "TLS_DH_anon_WITH_CAMELLIA_128_GCM_SHA256",
+ 0xC085: "TLS_DH_anon_WITH_CAMELLIA_256_GCM_SHA384",
+ 0xC086: "TLS_ECDHE_ECDSA_WITH_CAMELLIA_128_GCM_SHA256",
+ 0xC087: "TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_GCM_SHA384",
+ 0xC088: "TLS_ECDH_ECDSA_WITH_CAMELLIA_128_GCM_SHA256",
+ 0xC089: "TLS_ECDH_ECDSA_WITH_CAMELLIA_256_GCM_SHA384",
+ 0xC08A: "TLS_ECDHE_RSA_WITH_CAMELLIA_128_GCM_SHA256",
+ 0xC08B: "TLS_ECDHE_RSA_WITH_CAMELLIA_256_GCM_SHA384",
+ 0xC08C: "TLS_ECDH_RSA_WITH_CAMELLIA_128_GCM_SHA256",
+ 0xC08D: "TLS_ECDH_RSA_WITH_CAMELLIA_256_GCM_SHA384",
+ 0xC08E: "TLS_PSK_WITH_CAMELLIA_128_GCM_SHA256",
+ 0xC08F: "TLS_PSK_WITH_CAMELLIA_256_GCM_SHA384",
+ 0xC090: "TLS_DHE_PSK_WITH_CAMELLIA_128_GCM_SHA256",
+ 0xC091: "TLS_DHE_PSK_WITH_CAMELLIA_256_GCM_SHA384",
+ 0xC092: "TLS_RSA_PSK_WITH_CAMELLIA_128_GCM_SHA256",
+ 0xC093: "TLS_RSA_PSK_WITH_CAMELLIA_256_GCM_SHA384",
+ 0xC094: "TLS_PSK_WITH_CAMELLIA_128_CBC_SHA256",
+ 0xC095: "TLS_PSK_WITH_CAMELLIA_256_CBC_SHA384",
+ 0xC096: "TLS_DHE_PSK_WITH_CAMELLIA_128_CBC_SHA256",
+ 0xC097: "TLS_DHE_PSK_WITH_CAMELLIA_256_CBC_SHA384",
+ 0xC098: "TLS_RSA_PSK_WITH_CAMELLIA_128_CBC_SHA256",
+ 0xC099: "TLS_RSA_PSK_WITH_CAMELLIA_256_CBC_SHA384",
+ 0xC09A: "TLS_ECDHE_PSK_WITH_CAMELLIA_128_CBC_SHA256",
+ 0xC09B: "TLS_ECDHE_PSK_WITH_CAMELLIA_256_CBC_SHA384",
+ 0xC09C: "TLS_RSA_WITH_AES_128_CCM",
+ 0xC09D: "TLS_RSA_WITH_AES_256_CCM",
+ 0xC09E: "TLS_DHE_RSA_WITH_AES_128_CCM",
+ 0xC09F: "TLS_DHE_RSA_WITH_AES_256_CCM",
+ 0xC0A0: "TLS_RSA_WITH_AES_128_CCM_8",
+ 0xC0A1: "TLS_RSA_WITH_AES_256_CCM_8",
+ 0xC0A2: "TLS_DHE_RSA_WITH_AES_128_CCM_8",
+ 0xC0A3: "TLS_DHE_RSA_WITH_AES_256_CCM_8",
+ 0xC0A4: "TLS_PSK_WITH_AES_128_CCM",
+ 0xC0A5: "TLS_PSK_WITH_AES_256_CCM",
+ 0xC0A6: "TLS_DHE_PSK_WITH_AES_128_CCM",
+ 0xC0A7: "TLS_DHE_PSK_WITH_AES_256_CCM",
+ 0xC0A8: "TLS_PSK_WITH_AES_128_CCM_8",
+ 0xC0A9: "TLS_PSK_WITH_AES_256_CCM_8",
+ 0xC0AA: "TLS_PSK_DHE_WITH_AES_128_CCM_8",
+ 0xC0AB: "TLS_PSK_DHE_WITH_AES_256_CCM_8",
+ 0xC0AC: "TLS_ECDHE_ECDSA_WITH_AES_128_CCM",
+ 0xC0AD: "TLS_ECDHE_ECDSA_WITH_AES_256_CCM",
+ 0xC0AE: "TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8",
+ 0xC0AF: "TLS_ECDHE_ECDSA_WITH_AES_256_CCM_8",
+ 0xC0B0: "TLS_ECCPWD_WITH_AES_128_GCM_SHA256",
+ 0xC0B1: "TLS_ECCPWD_WITH_AES_256_GCM_SHA384",
+ 0xC0B2: "TLS_ECCPWD_WITH_AES_128_CCM_SHA256",
+ 0xC0B3: "TLS_ECCPWD_WITH_AES_256_CCM_SHA384",
+ 0xC0B4: "TLS_SHA256_SHA256",
+ 0xC0B5: "TLS_SHA384_SHA384",
+ 0xC100: "TLS_GOSTR341112_256_WITH_KUZNYECHIK_CTR_OMAC",
+ 0xC101: "TLS_GOSTR341112_256_WITH_MAGMA_CTR_OMAC",
+ 0xC102: "TLS_GOSTR341112_256_WITH_28147_CNT_IMIT",
+ 0xC103: "TLS_GOSTR341112_256_WITH_KUZNYECHIK_MGM_L",
+ 0xC104: "TLS_GOSTR341112_256_WITH_MAGMA_MGM_L",
+ 0xC105: "TLS_GOSTR341112_256_WITH_KUZNYECHIK_MGM_S",
+ 0xC106: "TLS_GOSTR341112_256_WITH_MAGMA_MGM_S",
+ 0xCACA: "GREASE",
+ 0xCCA8: "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
+ 0xCCA9: "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
+ 0xCCAA: "TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
+ 0xCCAB: "TLS_PSK_WITH_CHACHA20_POLY1305_SHA256",
+ 0xCCAC: "TLS_ECDHE_PSK_WITH_CHACHA20_POLY1305_SHA256",
+ 0xCCAD: "TLS_DHE_PSK_WITH_CHACHA20_POLY1305_SHA256",
+ 0xCCAE: "TLS_RSA_PSK_WITH_CHACHA20_POLY1305_SHA256",
+ 0xD001: "TLS_ECDHE_PSK_WITH_AES_128_GCM_SHA256",
+ 0xD002: "TLS_ECDHE_PSK_WITH_AES_256_GCM_SHA384",
+ 0xD003: "TLS_ECDHE_PSK_WITH_AES_128_CCM_8_SHA256",
+ 0xD005: "TLS_ECDHE_PSK_WITH_AES_128_CCM_SHA256",
+ 0xDADA: "GREASE",
+ 0xEAEA: "GREASE",
+ 0xFAFA: "GREASE",
+};
+
+/**
+ * GREASE values
+ */
+export const GREASE_VALUES = [
+ 0x0a0a,
+ 0x1a1a,
+ 0x2a2a,
+ 0x3a3a,
+ 0x4a4a,
+ 0x5a5a,
+ 0x6a6a,
+ 0x7a7a,
+ 0x8a8a,
+ 0x9a9a,
+ 0xaaaa,
+ 0xbaba,
+ 0xcaca,
+ 0xdada,
+ 0xeaea,
+ 0xfafa
+];
+
+/**
+ * Parses the supported_versions extension and returns the highest supported version.
+ * @param {Uint8Array} bytes
+ * @returns {number}
+ */
+export function parseHighestSupportedVersion(bytes) {
+ const s = new Stream(bytes);
+
+ // The Server Hello supported_versions extension simply contains the chosen version
+ if (s.length === 2) {
+ return s.readInt(2);
+ }
+
+ // Length
+ let i = s.readInt(1);
+
+ let highestVersion = 0;
+ while (s.hasMore() && i-- > 0) {
+ const v = s.readInt(2);
+ if (GREASE_VALUES.includes(v)) continue;
+ if (v > highestVersion) highestVersion = v;
+ }
+
+ return highestVersion;
+}
+
+/**
+ * Parses the application_layer_protocol_negotiation extension and returns the first value as raw bytes.
+ * @param {Uint8Array} bytes
+ * @returns {Uint8Array|null}
+ */
+export function parseFirstALPNValue(bytes) {
+ const s = new Stream(bytes);
+ const alpnExtLen = s.readInt(2);
+ if (alpnExtLen < 2) return null;
+ const strLen = s.readInt(1);
+ if (strLen < 1) return null;
+ return s.getBytes(strLen);
+}
diff --git a/src/core/lib/XXTEA.mjs b/src/core/lib/XXTEA.mjs
new file mode 100644
index 000000000..6d556fc5e
--- /dev/null
+++ b/src/core/lib/XXTEA.mjs
@@ -0,0 +1,174 @@
+/**
+ * XXTEA library
+ *
+ * Encryption Algorithm Authors:
+ * David J. Wheeler
+ * Roger M. Needham
+ *
+ * @author Ma Bingyao [mabingyao@gmail.com]
+ * @author n1474335 [n1474335@gmail.com]
+ * @license MIT
+ */
+
+const DELTA = 0x9E3779B9;
+
+/**
+ * Convert a buffer to a Uint8Array
+ * @param {Uint32Array} v
+ * @param {boolean} includeLength
+ * @returns {Uint8Array}
+ */
+function toUint8Array(v, includeLength) {
+ const length = v.length;
+ let n = length << 2;
+ if (includeLength) {
+ const m = v[length - 1];
+ n -= 4;
+ if ((m < n - 3) || (m > n)) {
+ return null;
+ }
+ n = m;
+ }
+ const bytes = new Uint8Array(n);
+ for (let i = 0; i < n; i++) {
+ bytes[i] = v[i >> 2] >> ((i & 3) << 3);
+ }
+ return bytes;
+}
+
+/**
+ * Convert a buffer to a Uint32Array
+ * @param {TypedArray} bs
+ * @param {boolean} includeLength
+ * @returns {Uint32Array}
+ */
+function toUint32Array(bs, includeLength) {
+ const length = bs.length;
+ let n = length >> 2;
+ if ((length & 3) !== 0) {
+ ++n;
+ }
+ let v;
+ if (includeLength) {
+ v = new Uint32Array(n + 1);
+ v[n] = length;
+ } else {
+ v = new Uint32Array(n);
+ }
+ for (let i = 0; i < length; ++i) {
+ v[i >> 2] |= bs[i] << ((i & 3) << 3);
+ }
+ return v;
+}
+
+/**
+ * Mask an int to 32 bits
+ * @param {number} i
+ * @returns {number}
+ */
+function int32(i) {
+ return i & 0xFFFFFFFF;
+}
+
+/**
+ * MX function for data randomisation
+ * @param {number} sum
+ * @param {number} y
+ * @param {number} z
+ * @param {number} p
+ * @param {number} e
+ * @param {number} k
+ * @returns {number}
+ */
+function mx(sum, y, z, p, e, k) {
+ return ((z >>> 5 ^ y << 2) + (y >>> 3 ^ z << 4)) ^ ((sum ^ y) + (k[p & 3 ^ e] ^ z));
+}
+
+/**
+ * Ensure an array is a multiple of 16 bits
+ * @param {TypedArray} k
+ * @returns {TypedArray}
+ */
+function fixk(k) {
+ if (k.length < 16) {
+ const key = new Uint8Array(16);
+ key.set(k);
+ return key;
+ }
+ return k;
+}
+
+/**
+ * Performs XXTEA encryption on a Uint32Array
+ * @param {Uint32Array} v
+ * @param {Uint32Array} k
+ * @returns {Uint32Array}
+ */
+function encryptUint32Array(v, k) {
+ const length = v.length;
+ const n = length - 1;
+ let y, z, sum, e, p, q;
+ z = v[n];
+ sum = 0;
+ for (q = Math.floor(6 + 52 / length) | 0; q > 0; --q) {
+ sum = int32(sum + DELTA);
+ e = sum >>> 2 & 3;
+ for (p = 0; p < n; ++p) {
+ y = v[p + 1];
+ z = v[p] = int32(v[p] + mx(sum, y, z, p, e, k));
+ }
+ y = v[0];
+ z = v[n] = int32(v[n] + mx(sum, y, z, n, e, k));
+ }
+ return v;
+}
+
+/**
+ * Performs XXTEA decryption on a Uint32Array
+ * @param {Uint32Array} v
+ * @param {Uint32Array} k
+ * @returns {Uint32Array}
+ */
+function decryptUint32Array(v, k) {
+ const length = v.length;
+ const n = length - 1;
+ let y, z, sum, e, p;
+ y = v[0];
+ const q = Math.floor(6 + 52 / length);
+ for (sum = int32(q * DELTA); sum !== 0; sum = int32(sum - DELTA)) {
+ e = sum >>> 2 & 3;
+ for (p = n; p > 0; --p) {
+ z = v[p - 1];
+ y = v[p] = int32(v[p] - mx(sum, y, z, p, e, k));
+ }
+ z = v[n];
+ y = v[0] = int32(v[0] - mx(sum, y, z, 0, e, k));
+ }
+ return v;
+}
+
+/**
+ * Encrypt function
+ * @param {TypedArray} data
+ * @param {TypedArray} key
+ * @returns {Uint8Array}
+ */
+export function encrypt(data, key) {
+ if (data === undefined || data === null || data.length === 0) {
+ return data;
+ }
+ return toUint8Array(encryptUint32Array(toUint32Array(data, true), toUint32Array(fixk(key), false)), false);
+}
+
+/**
+ * Decrypt function
+ * @param {TypedArray} data
+ * @param {TypedArray} key
+ * @returns {Uint8Array}
+ */
+export function decrypt(data, key) {
+ if (data === undefined || data === null || data.length === 0) {
+ return data;
+ }
+ return toUint8Array(decryptUint32Array(toUint32Array(data, false), toUint32Array(fixk(key), false)), true);
+}
diff --git a/src/core/operations/AESDecrypt.mjs b/src/core/operations/AESDecrypt.mjs
index e24a5119b..5e6cec264 100644
--- a/src/core/operations/AESDecrypt.mjs
+++ b/src/core/operations/AESDecrypt.mjs
@@ -112,7 +112,7 @@ class AESDecrypt extends Operation {
run(input, args) {
const key = Utils.convertToByteString(args[0].string, args[0].option),
iv = Utils.convertToByteString(args[1].string, args[1].option),
- mode = args[2].substring(0, 3),
+ mode = args[2].split("/")[0],
noPadding = args[2].endsWith("NoPadding"),
inputType = args[3],
outputType = args[4],
diff --git a/src/core/operations/AESEncrypt.mjs b/src/core/operations/AESEncrypt.mjs
index 7b52ff039..84e1c5405 100644
--- a/src/core/operations/AESEncrypt.mjs
+++ b/src/core/operations/AESEncrypt.mjs
@@ -66,6 +66,14 @@ class AESEncrypt extends Operation {
{
name: "ECB",
off: [5]
+ },
+ {
+ name: "CBC/NoPadding",
+ off: [5]
+ },
+ {
+ name: "ECB/NoPadding",
+ off: [5]
}
]
},
@@ -98,7 +106,8 @@ class AESEncrypt extends Operation {
run(input, args) {
const key = Utils.convertToByteString(args[0].string, args[0].option),
iv = Utils.convertToByteString(args[1].string, args[1].option),
- mode = args[2],
+ mode = args[2].split("/")[0],
+ noPadding = args[2].endsWith("NoPadding"),
inputType = args[3],
outputType = args[4],
aad = Utils.convertToByteString(args[5].string, args[5].option);
@@ -114,11 +123,20 @@ The following algorithms will be used based on the size of the key:
input = Utils.convertToByteString(input, inputType);
+ // Handle NoPadding modes
+ if (noPadding && input.length % 16 !== 0) {
+ throw new OperationError("Input length must be a multiple of 16 bytes for NoPadding modes.");
+ }
const cipher = forge.cipher.createCipher("AES-" + mode, key);
cipher.start({
iv: iv,
additionalData: mode === "GCM" ? aad : undefined
});
+ if (noPadding) {
+ cipher.mode.pad = function(output, options) {
+ return true;
+ };
+ }
cipher.update(forge.util.createBuffer(input));
cipher.finish();
diff --git a/src/core/operations/AddLineNumbers.mjs b/src/core/operations/AddLineNumbers.mjs
index c1c6159af..3eee65444 100644
--- a/src/core/operations/AddLineNumbers.mjs
+++ b/src/core/operations/AddLineNumbers.mjs
@@ -22,7 +22,13 @@ class AddLineNumbers extends Operation {
this.description = "Adds line numbers to the output.";
this.inputType = "string";
this.outputType = "string";
- this.args = [];
+ this.args = [
+ {
+ "name": "Offset",
+ "type": "number",
+ "value": 0
+ }
+ ];
}
/**
@@ -33,10 +39,11 @@ class AddLineNumbers extends Operation {
run(input, args) {
const lines = input.split("\n"),
width = lines.length.toString().length;
+ const offset = args[0] ? parseInt(args[0], 10) : 0;
let output = "";
for (let n = 0; n < lines.length; n++) {
- output += (n+1).toString().padStart(width, " ") + " " + lines[n] + "\n";
+ output += (n+1+offset).toString().padStart(width, " ") + " " + lines[n] + "\n";
}
return output.slice(0, output.length-1);
}
diff --git a/src/core/operations/AddTextToImage.mjs b/src/core/operations/AddTextToImage.mjs
index 084c92ec6..2eb03e860 100644
--- a/src/core/operations/AddTextToImage.mjs
+++ b/src/core/operations/AddTextToImage.mjs
@@ -9,7 +9,7 @@ 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 from "jimp/es/index.js";
/**
* Add Text To Image operation
@@ -127,7 +127,7 @@ class AddTextToImage extends Operation {
let image;
try {
- image = await jimp.read(input);
+ image = await Jimp.read(input);
} catch (err) {
throw new OperationError(`Error loading image. (${err})`);
}
@@ -163,7 +163,7 @@ class AddTextToImage extends Operation {
const font = fontsMap[fontFace];
// LoadFont needs an absolute url, so append the font name to self.docURL
- const jimpFont = await jimp.loadFont(self.docURL + "/" + font.default);
+ const jimpFont = await Jimp.loadFont(self.docURL + "/" + font.default);
jimpFont.pages.forEach(function(page) {
if (page.bitmap) {
@@ -190,7 +190,7 @@ class AddTextToImage extends Operation {
});
// Create a temporary image to hold the rendered text
- const textImage = new jimp(jimp.measureText(jimpFont, text), jimp.measureTextHeight(jimpFont, text));
+ const textImage = new Jimp(Jimp.measureText(jimpFont, text), Jimp.measureTextHeight(jimpFont, text));
textImage.print(jimpFont, 0, 0, text);
// Scale the rendered text image to the correct size
@@ -198,9 +198,9 @@ class AddTextToImage extends Operation {
if (size !== 1) {
// Use bicubic for decreasing size
if (size > 1) {
- textImage.scale(scaleFactor, jimp.RESIZE_BICUBIC);
+ textImage.scale(scaleFactor, Jimp.RESIZE_BICUBIC);
} else {
- textImage.scale(scaleFactor, jimp.RESIZE_BILINEAR);
+ textImage.scale(scaleFactor, Jimp.RESIZE_BILINEAR);
}
}
@@ -234,9 +234,9 @@ class AddTextToImage extends Operation {
let imageBuffer;
if (image.getMIME() === "image/gif") {
- imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
+ imageBuffer = await image.getBufferAsync(Jimp.MIME_PNG);
} else {
- imageBuffer = await image.getBufferAsync(jimp.AUTO);
+ imageBuffer = await image.getBufferAsync(Jimp.AUTO);
}
return imageBuffer.buffer;
} catch (err) {
diff --git a/src/core/operations/AlternatingCaps.mjs b/src/core/operations/AlternatingCaps.mjs
new file mode 100644
index 000000000..2d54867c4
--- /dev/null
+++ b/src/core/operations/AlternatingCaps.mjs
@@ -0,0 +1,53 @@
+/**
+ * @author sw5678
+ * @copyright Crown Copyright 2023
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+
+/**
+ * Alternating caps operation
+ */
+class AlternatingCaps extends Operation {
+
+ /**
+ * AlternatingCaps constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "Alternating Caps";
+ this.module = "Default";
+ this.description = "Alternating caps, also known as studly caps, sticky caps, or spongecase is a form of text notation in which the capitalization of letters varies by some pattern, or arbitrarily. An example of this would be spelling 'alternative caps' as 'aLtErNaTiNg CaPs'.";
+ this.infoURL = "https://en.wikipedia.org/wiki/Alternating_caps";
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args= [];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ let output = "";
+ let previousCaps = true;
+ for (let i = 0; i < input.length; i++) {
+ // Check if the element is a letter
+ if (!RegExp(/^\p{L}/, "u").test(input[i])) {
+ output += input[i];
+ } else if (previousCaps) {
+ output += input[i].toLowerCase();
+ previousCaps = false;
+ } else {
+ output += input[i].toUpperCase();
+ previousCaps = true;
+ }
+ }
+ return output;
+ }
+}
+
+export default AlternatingCaps;
diff --git a/src/core/operations/AnalyseUUID.mjs b/src/core/operations/AnalyseUUID.mjs
new file mode 100644
index 000000000..b35060179
--- /dev/null
+++ b/src/core/operations/AnalyseUUID.mjs
@@ -0,0 +1,48 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2023
+ * @license Apache-2.0
+ */
+
+import * as uuid from "uuid";
+
+import Operation from "../Operation.mjs";
+import OperationError from "../errors/OperationError.mjs";
+
+/**
+ * Analyse UUID operation
+ */
+class AnalyseUUID extends Operation {
+
+ /**
+ * AnalyseUUID constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "Analyse UUID";
+ this.module = "Crypto";
+ this.description = "Tries to determine information about a given UUID and suggests which version may have been used to generate it";
+ this.infoURL = "https://wikipedia.org/wiki/Universally_unique_identifier";
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ try {
+ const uuidVersion = uuid.version(input);
+ return "UUID version: " + uuidVersion;
+ } catch (error) {
+ throw new OperationError("Invalid UUID");
+ }
+ }
+
+}
+
+export default AnalyseUUID;
diff --git a/src/core/operations/BLAKE3.mjs b/src/core/operations/BLAKE3.mjs
new file mode 100644
index 000000000..0f686120a
--- /dev/null
+++ b/src/core/operations/BLAKE3.mjs
@@ -0,0 +1,58 @@
+/**
+ * @author xumptex [xumptex@outlook.fr]
+ * @copyright Crown Copyright 2025
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+import OperationError from "../errors/OperationError.mjs";
+import { blake3 } from "hash-wasm";
+/**
+ * BLAKE3 operation
+ */
+class BLAKE3 extends Operation {
+
+ /**
+ * BLAKE3 constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "BLAKE3";
+ this.module = "Hashing";
+ this.description = "Hashes the input using BLAKE3 (UTF-8 encoded), with an optional key (also UTF-8), and outputs the result in hexadecimal format.";
+ this.infoURL = "https://en.wikipedia.org/wiki/BLAKE_(hash_function)#BLAKE3";
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [
+ {
+ "name": "Size (bytes)",
+ "type": "number"
+ }, {
+ "name": "Key",
+ "type": "string",
+ "value": ""
+ }
+ ];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ const key = args[1];
+ const size = args[0];
+ // Check if the user want a key hash or not
+ if (key === "") {
+ return blake3(input, size*8);
+ } if (key.length !== 32) {
+ throw new OperationError("The key must be exactly 32 bytes long");
+ }
+ return blake3(input, size*8, key);
+ }
+
+}
+
+export default BLAKE3;
diff --git a/src/core/operations/BlowfishDecrypt.mjs b/src/core/operations/BlowfishDecrypt.mjs
index f7dc8d176..afb260073 100644
--- a/src/core/operations/BlowfishDecrypt.mjs
+++ b/src/core/operations/BlowfishDecrypt.mjs
@@ -70,10 +70,14 @@ class BlowfishDecrypt extends Operation {
inputType = args[3],
outputType = args[4];
- if (key.length !== 8) {
+ if (key.length < 4 || key.length > 56) {
throw new OperationError(`Invalid key length: ${key.length} bytes
-Blowfish uses a key length of 8 bytes (64 bits).`);
+Blowfish's key length needs to be between 4 and 56 bytes (32-448 bits).`);
+ }
+
+ if (mode !== "ECB" && iv.length !== 8) {
+ throw new OperationError(`Invalid IV length: ${iv.length} bytes. Expected 8 bytes.`);
}
input = Utils.convertToByteString(input, inputType);
diff --git a/src/core/operations/BlowfishEncrypt.mjs b/src/core/operations/BlowfishEncrypt.mjs
index 2cf3672bf..1d5dcf020 100644
--- a/src/core/operations/BlowfishEncrypt.mjs
+++ b/src/core/operations/BlowfishEncrypt.mjs
@@ -70,10 +70,14 @@ class BlowfishEncrypt extends Operation {
inputType = args[3],
outputType = args[4];
- if (key.length !== 8) {
+ if (key.length < 4 || key.length > 56) {
throw new OperationError(`Invalid key length: ${key.length} bytes
-Blowfish uses a key length of 8 bytes (64 bits).`);
+Blowfish's key length needs to be between 4 and 56 bytes (32-448 bits).`);
+ }
+
+ if (mode !== "ECB" && iv.length !== 8) {
+ throw new OperationError(`Invalid IV length: ${iv.length} bytes. Expected 8 bytes.`);
}
input = Utils.convertToByteString(input, inputType);
diff --git a/src/core/operations/BlurImage.mjs b/src/core/operations/BlurImage.mjs
index 51082e549..179041808 100644
--- a/src/core/operations/BlurImage.mjs
+++ b/src/core/operations/BlurImage.mjs
@@ -10,7 +10,7 @@ import { isWorkerEnvironment } from "../Utils.mjs";
import { isImage } from "../lib/FileType.mjs";
import { toBase64 } from "../lib/Base64.mjs";
import { gaussianBlur } from "../lib/ImageManipulation.mjs";
-import jimp from "jimp";
+import Jimp from "jimp/es/index.js";
/**
* Blur Image operation
@@ -59,7 +59,7 @@ class BlurImage 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,9 +79,9 @@ class BlurImage extends Operation {
let imageBuffer;
if (image.getMIME() === "image/gif") {
- imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
+ imageBuffer = await image.getBufferAsync(Jimp.MIME_PNG);
} else {
- imageBuffer = await image.getBufferAsync(jimp.AUTO);
+ imageBuffer = await image.getBufferAsync(Jimp.AUTO);
}
return imageBuffer.buffer;
} catch (err) {
diff --git a/src/core/operations/CRC16Checksum.mjs b/src/core/operations/CRC16Checksum.mjs
deleted file mode 100644
index 035ee04b3..000000000
--- a/src/core/operations/CRC16Checksum.mjs
+++ /dev/null
@@ -1,41 +0,0 @@
-/**
- * @author n1474335 [n1474335@gmail.com]
- * @copyright Crown Copyright 2016
- * @license Apache-2.0
- */
-
-import Operation from "../Operation.mjs";
-import JSCRC from "js-crc";
-
-/**
- * CRC-16 Checksum operation
- */
-class CRC16Checksum extends Operation {
-
- /**
- * CRC16Checksum constructor
- */
- constructor() {
- super();
-
- this.name = "CRC-16 Checksum";
- this.module = "Crypto";
- this.description = "A cyclic redundancy check (CRC) is an error-detecting code commonly used in digital networks and storage devices to detect accidental changes to raw data. The CRC was invented by W. Wesley Peterson in 1961.";
- this.infoURL = "https://wikipedia.org/wiki/Cyclic_redundancy_check";
- this.inputType = "ArrayBuffer";
- this.outputType = "string";
- this.args = [];
- }
-
- /**
- * @param {ArrayBuffer} input
- * @param {Object[]} args
- * @returns {string}
- */
- run(input, args) {
- return JSCRC.crc16(input);
- }
-
-}
-
-export default CRC16Checksum;
diff --git a/src/core/operations/CRC32Checksum.mjs b/src/core/operations/CRC32Checksum.mjs
deleted file mode 100644
index cfe846438..000000000
--- a/src/core/operations/CRC32Checksum.mjs
+++ /dev/null
@@ -1,41 +0,0 @@
-/**
- * @author n1474335 [n1474335@gmail.com]
- * @copyright Crown Copyright 2016
- * @license Apache-2.0
- */
-
-import Operation from "../Operation.mjs";
-import JSCRC from "js-crc";
-
-/**
- * CRC-32 Checksum operation
- */
-class CRC32Checksum extends Operation {
-
- /**
- * CRC32Checksum constructor
- */
- constructor() {
- super();
-
- this.name = "CRC-32 Checksum";
- this.module = "Crypto";
- this.description = "A cyclic redundancy check (CRC) is an error-detecting code commonly used in digital networks and storage devices to detect accidental changes to raw data. The CRC was invented by W. Wesley Peterson in 1961; the 32-bit CRC function of Ethernet and many other standards is the work of several researchers and was published in 1975.";
- this.infoURL = "https://wikipedia.org/wiki/Cyclic_redundancy_check";
- this.inputType = "ArrayBuffer";
- this.outputType = "string";
- this.args = [];
- }
-
- /**
- * @param {ArrayBuffer} input
- * @param {Object[]} args
- * @returns {string}
- */
- run(input, args) {
- return JSCRC.crc32(input);
- }
-
-}
-
-export default CRC32Checksum;
diff --git a/src/core/operations/CRC8Checksum.mjs b/src/core/operations/CRC8Checksum.mjs
deleted file mode 100644
index 193cadf98..000000000
--- a/src/core/operations/CRC8Checksum.mjs
+++ /dev/null
@@ -1,157 +0,0 @@
-/**
- * @author mshwed [m@ttshwed.com]
- * @copyright Crown Copyright 2019
- * @license Apache-2.0
- */
-
-import Operation from "../Operation.mjs";
-import OperationError from "../errors/OperationError.mjs";
-
-import { toHexFast } from "../lib/Hex.mjs";
-
-/**
- * CRC-8 Checksum operation
- */
-class CRC8Checksum extends Operation {
-
- /**
- * CRC8Checksum constructor
- */
- constructor() {
- super();
-
- this.name = "CRC-8 Checksum";
- this.module = "Crypto";
- this.description = "A cyclic redundancy check (CRC) is an error-detecting code commonly used in digital networks and storage devices to detect accidental changes to raw data. The CRC was invented by W. Wesley Peterson in 1961.";
- this.infoURL = "https://wikipedia.org/wiki/Cyclic_redundancy_check";
- this.inputType = "ArrayBuffer";
- this.outputType = "string";
- this.args = [
- {
- "name": "Algorithm",
- "type": "option",
- "value": [
- "CRC-8",
- "CRC-8/CDMA2000",
- "CRC-8/DARC",
- "CRC-8/DVB-S2",
- "CRC-8/EBU",
- "CRC-8/I-CODE",
- "CRC-8/ITU",
- "CRC-8/MAXIM",
- "CRC-8/ROHC",
- "CRC-8/WCDMA"
- ]
- }
- ];
- }
-
- /**
- * Generates the pre-computed lookup table for byte division
- *
- * @param polynomial
- */
- calculateCRC8LookupTable(polynomial) {
- const crc8Table = new Uint8Array(256);
-
- let currentByte;
- for (let i = 0; i < 256; i++) {
- currentByte = i;
- for (let bit = 0; bit < 8; bit++) {
- if ((currentByte & 0x80) !== 0) {
- currentByte <<= 1;
- currentByte ^= polynomial;
- } else {
- currentByte <<= 1;
- }
- }
-
- crc8Table[i] = currentByte;
- }
-
- return crc8Table;
- }
-
- /**
- * Calculates the CRC-8 Checksum from an input
- *
- * @param {ArrayBuffer} input
- * @param {number} polynomial
- * @param {number} initializationValue
- * @param {boolean} inputReflection
- * @param {boolean} outputReflection
- * @param {number} xorOut
- */
- calculateCRC8(input, polynomial, initializationValue, inputReflection, outputReflection, xorOut) {
- const crcSize = 8;
- const crcTable = this.calculateCRC8LookupTable(polynomial);
-
- let crc = initializationValue !== 0 ? initializationValue : 0;
- let currentByte, position;
-
- input = new Uint8Array(input);
- for (const inputByte of input) {
- currentByte = inputReflection ? this.reverseBits(inputByte, crcSize) : inputByte;
-
- position = (currentByte ^ crc) & 255;
- crc = crcTable[position];
- }
-
- crc = outputReflection ? this.reverseBits(crc, crcSize) : crc;
-
- if (xorOut !== 0) crc = crc ^ xorOut;
-
- return toHexFast(new Uint8Array([crc]));
- }
-
- /**
- * Reverse the bits for a given input byte.
- *
- * @param {number} input
- */
- reverseBits(input, hashSize) {
- let reversedByte = 0;
- for (let i = hashSize - 1; i >= 0; i--) {
- reversedByte |= ((input & 1) << i);
- input >>= 1;
- }
-
- return reversedByte;
- }
-
- /**
- * @param {ArrayBuffer} input
- * @param {Object[]} args
- * @returns {string}
- */
- run(input, args) {
- const algorithm = args[0];
-
- switch (algorithm) {
- case "CRC-8":
- return this.calculateCRC8(input, 0x7, 0x0, false, false, 0x0);
- case "CRC-8/CDMA2000":
- return this.calculateCRC8(input, 0x9B, 0xFF, false, false, 0x0);
- case "CRC-8/DARC":
- return this.calculateCRC8(input, 0x39, 0x0, true, true, 0x0);
- case "CRC-8/DVB-S2":
- return this.calculateCRC8(input, 0xD5, 0x0, false, false, 0x0);
- case "CRC-8/EBU":
- return this.calculateCRC8(input, 0x1D, 0xFF, true, true, 0x0);
- case "CRC-8/I-CODE":
- return this.calculateCRC8(input, 0x1D, 0xFD, false, false, 0x0);
- case "CRC-8/ITU":
- return this.calculateCRC8(input, 0x7, 0x0, false, false, 0x55);
- case "CRC-8/MAXIM":
- return this.calculateCRC8(input, 0x31, 0x0, true, true, 0x0);
- case "CRC-8/ROHC":
- return this.calculateCRC8(input, 0x7, 0xFF, true, true, 0x0);
- case "CRC-8/WCDMA":
- return this.calculateCRC8(input, 0x9B, 0x0, true, true, 0x0);
- default:
- throw new OperationError("Unknown checksum algorithm");
- }
- }
-}
-
-export default CRC8Checksum;
diff --git a/src/core/operations/CRCChecksum.mjs b/src/core/operations/CRCChecksum.mjs
new file mode 100644
index 000000000..88da2fa5a
--- /dev/null
+++ b/src/core/operations/CRCChecksum.mjs
@@ -0,0 +1,1110 @@
+/**
+ * @author r4mos [2k95ljkhg@mozmail.com]
+ * @copyright Crown Copyright 2025
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+import OperationError from "../errors/OperationError.mjs";
+
+/**
+ * CRC Checksum operation
+ */
+class CRCChecksum extends Operation {
+
+ /**
+ * CRCChecksum constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "CRC Checksum";
+ this.module = "Default";
+ this.description = "A Cyclic Redundancy Check (CRC ) is an error-detecting code commonly used in digital networks and storage devices to detect accidental changes to raw data.";
+ this.infoURL = "https://wikipedia.org/wiki/Cyclic_redundancy_check";
+ this.inputType = "ArrayBuffer";
+ this.outputType = "string";
+ this.args = [
+ {
+ name: "Algorithm",
+ type: "argSelector",
+ value: [
+ {
+ name: "Custom",
+ on: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-3/GSM",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-3/ROHC",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-4/G-704",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-4/INTERLAKEN",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-4/ITU",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-5/EPC",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-5/EPC-C1G2",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-5/G-704",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-5/ITU",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-5/USB",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-6/CDMA2000-A",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-6/CDMA2000-B",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-6/DARC",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-6/G-704",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-6/GSM",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-6/ITU",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-7/MMC",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-7/ROHC",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-7/UMTS",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-8",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-8/8H2F",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-8/AES",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-8/AUTOSAR",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-8/BLUETOOTH",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-8/CDMA2000",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-8/DARC",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-8/DVB-S2",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-8/EBU",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-8/GSM-A",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-8/GSM-B",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-8/HITAG",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-8/I-432-1",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-8/I-CODE",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-8/ITU",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-8/LTE",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-8/MAXIM",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-8/MAXIM-DOW",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-8/MIFARE-MAD",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-8/NRSC-5",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-8/OPENSAFETY",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-8/ROHC",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-8/SAE-J1850",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-8/SAE-J1850-ZERO",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-8/SMBUS",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-8/TECH-3250",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-8/WCDMA",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-10/ATM",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-10/CDMA2000",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-10/GSM",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-10/I-610",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-11/FLEXRAY",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-11/UMTS",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-12/3GPP",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-12/CDMA2000",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-12/DECT",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-12/GSM",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-12/UMTS",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-13/BBC",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-14/DARC",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-14/GSM",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-15/CAN",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-15/MPT1327",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/A",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/ACORN",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/ARC",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/AUG-CCITT",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/AUTOSAR",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/B",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/BLUETOOTH",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/BUYPASS",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/CCITT",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/CCITT-FALSE",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/CCITT-TRUE",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/CCITT-ZERO",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/CDMA2000",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/CMS",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/DARC",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/DDS-110",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/DECT-R",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/DECT-X",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/DNP",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/EN-13757",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/EPC",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/EPC-C1G2",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/GENIBUS",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/GSM",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/I-CODE",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/IBM",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/IBM-3740",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/IBM-SDLC",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/IEC-61158-2",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/ISO-HDLC",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/ISO-IEC-14443-3-A",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/ISO-IEC-14443-3-B",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/KERMIT",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/LHA",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/LJ1200",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/LTE",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/M17",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/MAXIM",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/MAXIM-DOW",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/MCRF4XX",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/MODBUS",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/NRSC-5",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/OPENSAFETY-A",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/OPENSAFETY-B",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/PROFIBUS",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/RIELLO",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/SPI-FUJITSU",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/T10-DIF",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/TELEDISK",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/TMS37157",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/UMTS",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/USB",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/V-41-LSB",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/V-41-MSB",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/VERIFONE",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/X-25",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/XMODEM",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-16/ZMODEM",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-17/CAN-FD",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-21/CAN-FD",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-24/BLE",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-24/FLEXRAY-A",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-24/FLEXRAY-B",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-24/INTERLAKEN",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-24/LTE-A",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-24/LTE-B",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-24/OPENPGP",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-24/OS-9",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-30/CDMA",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-31/PHILIPS",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-32",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-32/AAL5",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-32/ADCCP",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-32/AIXM",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-32/AUTOSAR",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-32/BASE91-C",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-32/BASE91-D",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-32/BZIP2",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-32/C",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-32/CASTAGNOLI",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-32/CD-ROM-EDC",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-32/CKSUM",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-32/D",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-32/DECT-B",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-32/INTERLAKEN",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-32/ISCSI",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-32/ISO-HDLC",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-32/JAMCRC",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-32/MEF",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-32/MPEG-2",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-32/NVME",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-32/PKZIP",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-32/POSIX",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-32/Q",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-32/SATA",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-32/V-42",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-32/XFER",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-32/XZ",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-40/GSM",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-64/ECMA-182",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-64/GO-ECMA",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-64/GO-ISO",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-64/MS",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-64/NVME",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-64/REDIS",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-64/WE",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-64/XZ",
+ off: [1, 2, 3, 4, 5, 6]
+ },
+ {
+ name: "CRC-82/DARC",
+ off: [1, 2, 3, 4, 5, 6]
+ }
+ ]
+ },
+ {
+ name: "Width (bits)",
+ type: "toggleString",
+ value: "0",
+ toggleValues: ["Decimal"]
+ },
+ {
+ name: "Polynomial",
+ type: "toggleString",
+ value: "0",
+ toggleValues: ["Hex"]
+ },
+ {
+ name: "Initialization",
+ type: "toggleString",
+ value: "0",
+ toggleValues: ["Hex"]
+ },
+ {
+ name: "Reflect input",
+ type: "option",
+ value: ["True", "False"]
+ },
+ {
+ name: "Reflect output",
+ type: "option",
+ value: ["True", "False"]
+ },
+ {
+ name: "Xor Output",
+ type: "toggleString",
+ value: "0",
+ toggleValues: ["Hex"]
+ }
+ ];
+ }
+
+ /**
+ * Reverse the order of bits in a number
+ *
+ * @param {BigInt} data
+ * @param {BigInt} reflect
+ */
+ reflectData(data, reflect) {
+ let value = 0n;
+ for (let bit = 0n; bit < reflect; bit++) {
+ if ((data & 1n) === 1n) {
+ value |= (1n << ((reflect - 1n) - bit));
+ }
+ data >>= 1n;
+ }
+ return value;
+ }
+
+ /**
+ * Performs the CRC Checksum calculation bit per bit without acceleration
+ *
+ * @param {BigInt} width
+ * @param {ArrayBuffer} input
+ * @param {BigInt} poly
+ * @param {BigInt} remainder
+ * @param {boolean} reflectIn
+ * @param {boolean} reflectOut
+ * @param {BigInt} xorOut
+ */
+ calculateCrcBitPerBit(width, input, poly, remainder, reflectIn, reflectOut, xorOut) {
+ const TOP_BIT = 1n << (width - 1n);
+ const MASK = (1n << width) - 1n;
+
+ for (let byte of input) {
+ byte = BigInt(byte);
+ if (reflectIn) {
+ byte = this.reflectData(byte, 8n);
+ }
+
+ for (let i = 0x80n; i !== 0n; i >>= 1n) {
+ let bit = remainder & TOP_BIT;
+
+ remainder = (remainder << 1n) & MASK;
+
+ if ((byte & i) !== 0n) {
+ bit ^= TOP_BIT;
+ }
+
+ if (bit !== 0n) {
+ remainder ^= poly;
+ }
+ }
+ }
+
+ if (reflectOut) {
+ remainder = this.reflectData(remainder, width);
+ }
+
+ return remainder ^ xorOut;
+ }
+
+ /**
+ * Generates the necessary table to speed up the calculation
+ *
+ * @param {BigInt} width
+ * @param {BigInt} poly
+ * @param {BigInt} MASK
+ * @param {BigInt} TOP_BIT
+ */
+ generateTable(width, poly, MASK, TOP_BIT) {
+ const table = new Array(256n);
+ for (let byte = 0n; byte < 256n; byte++) {
+ let value = ((byte << width - 8n) & MASK);
+ for (let bit = 0n; bit < 8n; bit++) {
+ value = (value & TOP_BIT) === 0n ?
+ ((value << 1n) & MASK) :
+ ((value << 1n) & MASK) ^ poly;
+ }
+ table[byte] = value;
+ }
+ return table;
+ }
+
+ /**
+ * Performs the CRC Checksum calculation byte per byte using a computed table to accelerate it
+ *
+ * @param {BigInt} width
+ * @param {ArrayBuffer} input
+ * @param {BigInt} poly
+ * @param {BigInt} remainder
+ * @param {boolean} reflectIn
+ * @param {boolean} reflectOut
+ * @param {BigInt} xorOut
+ */
+ calculateCrcBytePerByte(width, input, poly, remainder, reflectIn, reflectOut, xorOut) {
+ const TOP_BIT = 1n << (width - 1n);
+ const MASK = (1n << width) - 1n;
+ const TABLE = this.generateTable(width, poly, MASK, TOP_BIT);
+
+ for (let byte of input) {
+ byte = BigInt(byte);
+ if (reflectIn) {
+ byte = this.reflectData(byte, 8n);
+ }
+ remainder ^= (byte << width - 8n) & MASK;
+
+ const INDEX = remainder >> width - 8n;
+ remainder = (remainder << 8n) & MASK;
+ remainder ^= TABLE[INDEX];
+ }
+
+ if (reflectOut) {
+ remainder = this.reflectData(remainder, width);
+ }
+ return remainder ^ xorOut;
+ }
+
+ /**
+ * Calculates the CRC Checksum using Bigint (https://developer.mozilla.org/en-US/docs/Glossary/BigInt)
+ *
+ * @param {BigInt} width
+ * @param {ArrayBuffer} input
+ * @param {BigInt} poly
+ * @param {BigInt} init
+ * @param {boolean} reflectIn
+ * @param {boolean} reflectOut
+ * @param {BigInt} xorOut
+ */
+ crc(width, input, poly, init, reflectIn, reflectOut, xorOut) {
+ const VALUE = width < 8n ?
+ this.calculateCrcBitPerBit(width, input, poly, init, reflectIn, reflectOut, xorOut) :
+ this.calculateCrcBytePerByte(width, input, poly, init, reflectIn, reflectOut, xorOut);
+
+ return VALUE.toString(16).padStart(Math.ceil(Number(width) / 4), "0");
+ }
+
+ /**
+ * Validates user input to perform a custom CRC
+ *
+ * @param {Object} widthObject
+ * @param {ArrayBuffer} input
+ * @param {Object} polyObject
+ * @param {Object} initObject
+ * @param {Object} reflectInObject
+ * @param {Object} reflectOutObject
+ * @param {Object} xorOutObject
+ */
+ custom(widthObject, input, polyObject, initObject, reflectInObject, reflectOutObject, xorOutObject) {
+ try {
+ const width = BigInt(widthObject.string);
+ const poly = BigInt("0x" + polyObject.string);
+ const init = BigInt("0x" + initObject.string);
+ const reflectIn = reflectInObject === "True";
+ const reflectOut = reflectOutObject === "True";
+ const xorOut = BigInt("0x" + xorOutObject.string);
+
+ return this.crc(width, input, poly, init, reflectIn, reflectOut, xorOut);
+ } catch (error) {
+ throw new OperationError("Invalid custom CRC arguments");
+ }
+ }
+
+ /**
+ * Calculation of all known CRCs. Names and constants extracted from https://reveng.sourceforge.io/crc-catalogue/all.htm
+ *
+ * @param {ArrayBuffer} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ const algorithm = args[0];
+ input = new Uint8Array(input);
+
+ switch (algorithm) {
+ case "Custom": return this.custom(args[1], input, args[2], args[3], args[4], args[5], args[6]);
+ case "CRC-3/GSM": return this.crc(3n, input, 0x3n, 0x0n, false, false, 0x7n);
+ case "CRC-3/ROHC": return this.crc(3n, input, 0x3n, 0x7n, true, true, 0x0n);
+ case "CRC-4/G-704": return this.crc(4n, input, 0x3n, 0x0n, true, true, 0x0n);
+ case "CRC-4/INTERLAKEN": return this.crc(4n, input, 0x3n, 0xFn, false, false, 0xFn);
+ case "CRC-4/ITU": return this.crc(4n, input, 0x3n, 0x0n, true, true, 0x0n);
+ case "CRC-5/EPC": return this.crc(5n, input, 0x09n, 0x09n, false, false, 0x00n);
+ case "CRC-5/EPC-C1G2": return this.crc(5n, input, 0x09n, 0x09n, false, false, 0x00n);
+ case "CRC-5/G-704": return this.crc(5n, input, 0x15n, 0x00n, true, true, 0x00n);
+ case "CRC-5/ITU": return this.crc(5n, input, 0x15n, 0x00n, true, true, 0x00n);
+ case "CRC-5/USB": return this.crc(5n, input, 0x05n, 0x1Fn, true, true, 0x1Fn);
+ case "CRC-6/CDMA2000-A": return this.crc(6n, input, 0x27n, 0x3Fn, false, false, 0x00n);
+ case "CRC-6/CDMA2000-B": return this.crc(6n, input, 0x07n, 0x3Fn, false, false, 0x00n);
+ case "CRC-6/DARC": return this.crc(6n, input, 0x19n, 0x00n, true, true, 0x00n);
+ case "CRC-6/G-704": return this.crc(6n, input, 0x03n, 0x00n, true, true, 0x00n);
+ case "CRC-6/GSM": return this.crc(6n, input, 0x2Fn, 0x00n, false, false, 0x3Fn);
+ case "CRC-6/ITU": return this.crc(6n, input, 0x03n, 0x00n, true, true, 0x00n);
+ case "CRC-7/MMC": return this.crc(7n, input, 0x09n, 0x00n, false, false, 0x00n);
+ case "CRC-7/ROHC": return this.crc(7n, input, 0x4Fn, 0x7Fn, true, true, 0x00n);
+ case "CRC-7/UMTS": return this.crc(7n, input, 0x45n, 0x00n, false, false, 0x00n);
+ case "CRC-8": return this.crc(8n, input, 0x07n, 0x00n, false, false, 0x00n);
+ case "CRC-8/8H2F": return this.crc(8n, input, 0x2Fn, 0xFFn, false, false, 0xFFn);
+ case "CRC-8/AES": return this.crc(8n, input, 0x1Dn, 0xFFn, true, true, 0x00n);
+ case "CRC-8/AUTOSAR": return this.crc(8n, input, 0x2Fn, 0xFFn, false, false, 0xFFn);
+ case "CRC-8/BLUETOOTH": return this.crc(8n, input, 0xA7n, 0x00n, true, true, 0x00n);
+ case "CRC-8/CDMA2000": return this.crc(8n, input, 0x9Bn, 0xFFn, false, false, 0x00n);
+ case "CRC-8/DARC": return this.crc(8n, input, 0x39n, 0x00n, true, true, 0x00n);
+ case "CRC-8/DVB-S2": return this.crc(8n, input, 0xD5n, 0x00n, false, false, 0x00n);
+ case "CRC-8/EBU": return this.crc(8n, input, 0x1Dn, 0xFFn, true, true, 0x00n);
+ case "CRC-8/GSM-A": return this.crc(8n, input, 0x1Dn, 0x00n, false, false, 0x00n);
+ case "CRC-8/GSM-B": return this.crc(8n, input, 0x49n, 0x00n, false, false, 0xFFn);
+ case "CRC-8/HITAG": return this.crc(8n, input, 0x1Dn, 0xFFn, false, false, 0x00n);
+ case "CRC-8/I-432-1": return this.crc(8n, input, 0x07n, 0x00n, false, false, 0x55n);
+ case "CRC-8/I-CODE": return this.crc(8n, input, 0x1Dn, 0xFDn, false, false, 0x00n);
+ case "CRC-8/ITU": return this.crc(8n, input, 0x07n, 0x00n, false, false, 0x55n);
+ case "CRC-8/LTE": return this.crc(8n, input, 0x9Bn, 0x00n, false, false, 0x00n);
+ case "CRC-8/MAXIM": return this.crc(8n, input, 0x31n, 0x00n, true, true, 0x00n);
+ case "CRC-8/MAXIM-DOW": return this.crc(8n, input, 0x31n, 0x00n, true, true, 0x00n);
+ case "CRC-8/MIFARE-MAD": return this.crc(8n, input, 0x1Dn, 0xC7n, false, false, 0x00n);
+ case "CRC-8/NRSC-5": return this.crc(8n, input, 0x31n, 0xFFn, false, false, 0x00n);
+ case "CRC-8/OPENSAFETY": return this.crc(8n, input, 0x2Fn, 0x00n, false, false, 0x00n);
+ case "CRC-8/ROHC": return this.crc(8n, input, 0x07n, 0xFFn, true, true, 0x00n);
+ case "CRC-8/SAE-J1850": return this.crc(8n, input, 0x1Dn, 0xFFn, false, false, 0xFFn);
+ case "CRC-8/SAE-J1850-ZERO": return this.crc(8n, input, 0x1Dn, 0x00n, false, false, 0x00n);
+ case "CRC-8/SMBUS": return this.crc(8n, input, 0x07n, 0x00n, false, false, 0x00n);
+ case "CRC-8/TECH-3250": return this.crc(8n, input, 0x1Dn, 0xFFn, true, true, 0x00n);
+ case "CRC-8/WCDMA": return this.crc(8n, input, 0x9Bn, 0x00n, true, true, 0x00n);
+ case "CRC-10/ATM": return this.crc(10n, input, 0x233n, 0x000n, false, false, 0x000n);
+ case "CRC-10/CDMA2000": return this.crc(10n, input, 0x3D9n, 0x3FFn, false, false, 0x000n);
+ case "CRC-10/GSM": return this.crc(10n, input, 0x175n, 0x000n, false, false, 0x3FFn);
+ case "CRC-10/I-610": return this.crc(10n, input, 0x233n, 0x000n, false, false, 0x000n);
+ case "CRC-11/FLEXRAY": return this.crc(11n, input, 0x385n, 0x01An, false, false, 0x000n);
+ case "CRC-11/UMTS": return this.crc(11n, input, 0x307n, 0x000n, false, false, 0x000n);
+ case "CRC-12/3GPP": return this.crc(12n, input, 0x80Fn, 0x000n, false, true, 0x000n);
+ case "CRC-12/CDMA2000": return this.crc(12n, input, 0xF13n, 0xFFFn, false, false, 0x000n);
+ case "CRC-12/DECT": return this.crc(12n, input, 0x80Fn, 0x000n, false, false, 0x000n);
+ case "CRC-12/GSM": return this.crc(12n, input, 0xD31n, 0x000n, false, false, 0xFFFn);
+ case "CRC-12/UMTS": return this.crc(12n, input, 0x80Fn, 0x000n, false, true, 0x000n);
+ case "CRC-13/BBC": return this.crc(13n, input, 0x1CF5n, 0x0000n, false, false, 0x0000n);
+ case "CRC-14/DARC": return this.crc(14n, input, 0x0805n, 0x0000n, true, true, 0x0000n);
+ case "CRC-14/GSM": return this.crc(14n, input, 0x202Dn, 0x0000n, false, false, 0x3FFFn);
+ case "CRC-15/CAN": return this.crc(15n, input, 0x4599n, 0x0000n, false, false, 0x0000n);
+ case "CRC-15/MPT1327": return this.crc(15n, input, 0x6815n, 0x0000n, false, false, 0x0001n);
+ case "CRC-16": return this.crc(16n, input, 0x8005n, 0x0000n, true, true, 0x0000n);
+ case "CRC-16/A": return this.crc(16n, input, 0x1021n, 0xC6C6n, true, true, 0x0000n);
+ case "CRC-16/ACORN": return this.crc(16n, input, 0x1021n, 0x0000n, false, false, 0x0000n);
+ case "CRC-16/ARC": return this.crc(16n, input, 0x8005n, 0x0000n, true, true, 0x0000n);
+ case "CRC-16/AUG-CCITT": return this.crc(16n, input, 0x1021n, 0x1D0Fn, false, false, 0x0000n);
+ case "CRC-16/AUTOSAR": return this.crc(16n, input, 0x1021n, 0xFFFFn, false, false, 0x0000n);
+ case "CRC-16/B": return this.crc(16n, input, 0x1021n, 0xFFFFn, true, true, 0xFFFFn);
+ case "CRC-16/BLUETOOTH": return this.crc(16n, input, 0x1021n, 0x0000n, true, true, 0x0000n);
+ case "CRC-16/BUYPASS": return this.crc(16n, input, 0x8005n, 0x0000n, false, false, 0x0000n);
+ case "CRC-16/CCITT": return this.crc(16n, input, 0x1021n, 0x0000n, true, true, 0x0000n);
+ case "CRC-16/CCITT-FALSE": return this.crc(16n, input, 0x1021n, 0xFFFFn, false, false, 0x0000n);
+ case "CRC-16/CCITT-TRUE": return this.crc(16n, input, 0x1021n, 0x0000n, true, true, 0x0000n);
+ case "CRC-16/CCITT-ZERO": return this.crc(16n, input, 0x1021n, 0x0000n, false, false, 0x0000n);
+ case "CRC-16/CDMA2000": return this.crc(16n, input, 0xC867n, 0xFFFFn, false, false, 0x0000n);
+ case "CRC-16/CMS": return this.crc(16n, input, 0x8005n, 0xFFFFn, false, false, 0x0000n);
+ case "CRC-16/DARC": return this.crc(16n, input, 0x1021n, 0xFFFFn, false, false, 0xFFFFn);
+ case "CRC-16/DDS-110": return this.crc(16n, input, 0x8005n, 0x800Dn, false, false, 0x0000n);
+ case "CRC-16/DECT-R": return this.crc(16n, input, 0x0589n, 0x0000n, false, false, 0x0001n);
+ case "CRC-16/DECT-X": return this.crc(16n, input, 0x0589n, 0x0000n, false, false, 0x0000n);
+ case "CRC-16/DNP": return this.crc(16n, input, 0x3D65n, 0x0000n, true, true, 0xFFFFn);
+ case "CRC-16/EN-13757": return this.crc(16n, input, 0x3D65n, 0x0000n, false, false, 0xFFFFn);
+ case "CRC-16/EPC": return this.crc(16n, input, 0x1021n, 0xFFFFn, false, false, 0xFFFFn);
+ case "CRC-16/EPC-C1G2": return this.crc(16n, input, 0x1021n, 0xFFFFn, false, false, 0xFFFFn);
+ case "CRC-16/GENIBUS": return this.crc(16n, input, 0x1021n, 0xFFFFn, false, false, 0xFFFFn);
+ case "CRC-16/GSM": return this.crc(16n, input, 0x1021n, 0x0000n, false, false, 0xFFFFn);
+ case "CRC-16/I-CODE": return this.crc(16n, input, 0x1021n, 0xFFFFn, false, false, 0xFFFFn);
+ case "CRC-16/IBM": return this.crc(16n, input, 0x8005n, 0x0000n, true, true, 0x0000n);
+ case "CRC-16/IBM-3740": return this.crc(16n, input, 0x1021n, 0xFFFFn, false, false, 0x0000n);
+ case "CRC-16/IBM-SDLC": return this.crc(16n, input, 0x1021n, 0xFFFFn, true, true, 0xFFFFn);
+ case "CRC-16/IEC-61158-2": return this.crc(16n, input, 0x1DCFn, 0xFFFFn, false, false, 0xFFFFn);
+ case "CRC-16/ISO-HDLC": return this.crc(16n, input, 0x1021n, 0xFFFFn, true, true, 0xFFFFn);
+ case "CRC-16/ISO-IEC-14443-3-A": return this.crc(16n, input, 0x1021n, 0xC6C6n, true, true, 0x0000n);
+ case "CRC-16/ISO-IEC-14443-3-B": return this.crc(16n, input, 0x1021n, 0xFFFFn, true, true, 0xFFFFn);
+ case "CRC-16/KERMIT": return this.crc(16n, input, 0x1021n, 0x0000n, true, true, 0x0000n);
+ case "CRC-16/LHA": return this.crc(16n, input, 0x8005n, 0x0000n, true, true, 0x0000n);
+ case "CRC-16/LJ1200": return this.crc(16n, input, 0x6F63n, 0x0000n, false, false, 0x0000n);
+ case "CRC-16/LTE": return this.crc(16n, input, 0x1021n, 0x0000n, false, false, 0x0000n);
+ case "CRC-16/M17": return this.crc(16n, input, 0x5935n, 0xFFFFn, false, false, 0x0000n);
+ case "CRC-16/MAXIM": return this.crc(16n, input, 0x8005n, 0x0000n, true, true, 0xFFFFn);
+ case "CRC-16/MAXIM-DOW": return this.crc(16n, input, 0x8005n, 0x0000n, true, true, 0xFFFFn);
+ case "CRC-16/MCRF4XX": return this.crc(16n, input, 0x1021n, 0xFFFFn, true, true, 0x0000n);
+ case "CRC-16/MODBUS": return this.crc(16n, input, 0x8005n, 0xFFFFn, true, true, 0x0000n);
+ case "CRC-16/NRSC-5": return this.crc(16n, input, 0x080Bn, 0xFFFFn, true, true, 0x0000n);
+ case "CRC-16/OPENSAFETY-A": return this.crc(16n, input, 0x5935n, 0x0000n, false, false, 0x0000n);
+ case "CRC-16/OPENSAFETY-B": return this.crc(16n, input, 0x755Bn, 0x0000n, false, false, 0x0000n);
+ case "CRC-16/PROFIBUS": return this.crc(16n, input, 0x1DCFn, 0xFFFFn, false, false, 0xFFFFn);
+ case "CRC-16/RIELLO": return this.crc(16n, input, 0x1021n, 0xB2AAn, true, true, 0x0000n);
+ case "CRC-16/SPI-FUJITSU": return this.crc(16n, input, 0x1021n, 0x1D0Fn, false, false, 0x0000n);
+ case "CRC-16/T10-DIF": return this.crc(16n, input, 0x8BB7n, 0x0000n, false, false, 0x0000n);
+ case "CRC-16/TELEDISK": return this.crc(16n, input, 0xA097n, 0x0000n, false, false, 0x0000n);
+ case "CRC-16/TMS37157": return this.crc(16n, input, 0x1021n, 0x89ECn, true, true, 0x0000n);
+ case "CRC-16/UMTS": return this.crc(16n, input, 0x8005n, 0x0000n, false, false, 0x0000n);
+ case "CRC-16/USB": return this.crc(16n, input, 0x8005n, 0xFFFFn, true, true, 0xFFFFn);
+ case "CRC-16/V-41-LSB": return this.crc(16n, input, 0x1021n, 0x0000n, true, true, 0x0000n);
+ case "CRC-16/V-41-MSB": return this.crc(16n, input, 0x1021n, 0x0000n, false, false, 0x0000n);
+ case "CRC-16/VERIFONE": return this.crc(16n, input, 0x8005n, 0x0000n, false, false, 0x0000n);
+ case "CRC-16/X-25": return this.crc(16n, input, 0x1021n, 0xFFFFn, true, true, 0xFFFFn);
+ case "CRC-16/XMODEM": return this.crc(16n, input, 0x1021n, 0x0000n, false, false, 0x0000n);
+ case "CRC-16/ZMODEM": return this.crc(16n, input, 0x1021n, 0x0000n, false, false, 0x0000n);
+ case "CRC-17/CAN-FD": return this.crc(17n, input, 0x1685Bn, 0x00000n, false, false, 0x00000n);
+ case "CRC-21/CAN-FD": return this.crc(21n, input, 0x102899n, 0x000000n, false, false, 0x000000n);
+ case "CRC-24/BLE": return this.crc(24n, input, 0x00065Bn, 0x555555n, true, true, 0x000000n);
+ case "CRC-24/FLEXRAY-A": return this.crc(24n, input, 0x5D6DCBn, 0xFEDCBAn, false, false, 0x000000n);
+ case "CRC-24/FLEXRAY-B": return this.crc(24n, input, 0x5D6DCBn, 0xABCDEFn, false, false, 0x000000n);
+ case "CRC-24/INTERLAKEN": return this.crc(24n, input, 0x328B63n, 0xFFFFFFn, false, false, 0xFFFFFFn);
+ case "CRC-24/LTE-A": return this.crc(24n, input, 0x864CFBn, 0x000000n, false, false, 0x000000n);
+ case "CRC-24/LTE-B": return this.crc(24n, input, 0x800063n, 0x000000n, false, false, 0x000000n);
+ case "CRC-24/OPENPGP": return this.crc(24n, input, 0x864CFBn, 0xB704CEn, false, false, 0x000000n);
+ case "CRC-24/OS-9": return this.crc(24n, input, 0x800063n, 0xFFFFFFn, false, false, 0xFFFFFFn);
+ case "CRC-30/CDMA": return this.crc(30n, input, 0x2030B9C7n, 0x3FFFFFFFn, false, false, 0x3FFFFFFFn);
+ case "CRC-31/PHILIPS": return this.crc(31n, input, 0x04C11DB7n, 0x7FFFFFFFn, false, false, 0x7FFFFFFFn);
+ case "CRC-32": return this.crc(32n, input, 0x04C11DB7n, 0xFFFFFFFFn, true, true, 0xFFFFFFFFn);
+ case "CRC-32/AAL5": return this.crc(32n, input, 0x04C11DB7n, 0xFFFFFFFFn, false, false, 0xFFFFFFFFn);
+ case "CRC-32/ADCCP": return this.crc(32n, input, 0x04C11DB7n, 0xFFFFFFFFn, true, true, 0xFFFFFFFFn);
+ case "CRC-32/AIXM": return this.crc(32n, input, 0x814141ABn, 0x00000000n, false, false, 0x00000000n);
+ case "CRC-32/AUTOSAR": return this.crc(32n, input, 0xF4ACFB13n, 0xFFFFFFFFn, true, true, 0xFFFFFFFFn);
+ case "CRC-32/BASE91-C": return this.crc(32n, input, 0x1EDC6F41n, 0xFFFFFFFFn, true, true, 0xFFFFFFFFn);
+ case "CRC-32/BASE91-D": return this.crc(32n, input, 0xA833982Bn, 0xFFFFFFFFn, true, true, 0xFFFFFFFFn);
+ case "CRC-32/BZIP2": return this.crc(32n, input, 0x04C11DB7n, 0xFFFFFFFFn, false, false, 0xFFFFFFFFn);
+ case "CRC-32/C": return this.crc(32n, input, 0x1EDC6F41n, 0xFFFFFFFFn, true, true, 0xFFFFFFFFn);
+ case "CRC-32/CASTAGNOLI": return this.crc(32n, input, 0x1EDC6F41n, 0xFFFFFFFFn, true, true, 0xFFFFFFFFn);
+ case "CRC-32/CD-ROM-EDC": return this.crc(32n, input, 0x8001801Bn, 0x00000000n, true, true, 0x00000000n);
+ case "CRC-32/CKSUM": return this.crc(32n, input, 0x04C11DB7n, 0x00000000n, false, false, 0xFFFFFFFFn);
+ case "CRC-32/D": return this.crc(32n, input, 0xA833982Bn, 0xFFFFFFFFn, true, true, 0xFFFFFFFFn);
+ case "CRC-32/DECT-B": return this.crc(32n, input, 0x04C11DB7n, 0xFFFFFFFFn, false, false, 0xFFFFFFFFn);
+ case "CRC-32/INTERLAKEN": return this.crc(32n, input, 0x1EDC6F41n, 0xFFFFFFFFn, true, true, 0xFFFFFFFFn);
+ case "CRC-32/ISCSI": return this.crc(32n, input, 0x1EDC6F41n, 0xFFFFFFFFn, true, true, 0xFFFFFFFFn);
+ case "CRC-32/ISO-HDLC": return this.crc(32n, input, 0x04C11DB7n, 0xFFFFFFFFn, true, true, 0xFFFFFFFFn);
+ case "CRC-32/JAMCRC": return this.crc(32n, input, 0x04C11DB7n, 0xFFFFFFFFn, true, true, 0x00000000n);
+ case "CRC-32/MEF": return this.crc(32n, input, 0x741B8CD7n, 0xFFFFFFFFn, true, true, 0x00000000n);
+ case "CRC-32/MPEG-2": return this.crc(32n, input, 0x04C11DB7n, 0xFFFFFFFFn, false, false, 0x00000000n);
+ case "CRC-32/NVME": return this.crc(32n, input, 0x1EDC6F41n, 0xFFFFFFFFn, true, true, 0xFFFFFFFFn);
+ case "CRC-32/PKZIP": return this.crc(32n, input, 0x04C11DB7n, 0xFFFFFFFFn, true, true, 0xFFFFFFFFn);
+ case "CRC-32/POSIX": return this.crc(32n, input, 0x04C11DB7n, 0x00000000n, false, false, 0xFFFFFFFFn);
+ case "CRC-32/Q": return this.crc(32n, input, 0x814141ABn, 0x00000000n, false, false, 0x00000000n);
+ case "CRC-32/SATA": return this.crc(32n, input, 0x04C11DB7n, 0x52325032n, false, false, 0x00000000n);
+ case "CRC-32/V-42": return this.crc(32n, input, 0x04C11DB7n, 0xFFFFFFFFn, true, true, 0xFFFFFFFFn);
+ case "CRC-32/XFER": return this.crc(32n, input, 0x000000AFn, 0x00000000n, false, false, 0x00000000n);
+ case "CRC-32/XZ": return this.crc(32n, input, 0x04C11DB7n, 0xFFFFFFFFn, true, true, 0xFFFFFFFFn);
+ case "CRC-40/GSM": return this.crc(40n, input, 0x0004820009n, 0x0000000000n, false, false, 0xFFFFFFFFFFn);
+ case "CRC-64/ECMA-182": return this.crc(64n, input, 0x42F0E1EBA9EA3693n, 0x0000000000000000n, false, false, 0x0000000000000000n);
+ case "CRC-64/GO-ECMA": return this.crc(64n, input, 0x42F0E1EBA9EA3693n, 0xFFFFFFFFFFFFFFFFn, true, true, 0xFFFFFFFFFFFFFFFFn);
+ case "CRC-64/GO-ISO": return this.crc(64n, input, 0x000000000000001Bn, 0xFFFFFFFFFFFFFFFFn, true, true, 0xFFFFFFFFFFFFFFFFn);
+ case "CRC-64/MS": return this.crc(64n, input, 0x259C84CBA6426349n, 0xFFFFFFFFFFFFFFFFn, true, true, 0x0000000000000000n);
+ case "CRC-64/NVME": return this.crc(64n, input, 0xAD93D23594C93659n, 0xFFFFFFFFFFFFFFFFn, true, true, 0xFFFFFFFFFFFFFFFFn);
+ case "CRC-64/REDIS": return this.crc(64n, input, 0xAD93D23594C935A9n, 0x0000000000000000n, true, true, 0x0000000000000000n);
+ case "CRC-64/WE": return this.crc(64n, input, 0x42F0E1EBA9EA3693n, 0xFFFFFFFFFFFFFFFFn, false, false, 0xFFFFFFFFFFFFFFFFn);
+ case "CRC-64/XZ": return this.crc(64n, input, 0x42F0E1EBA9EA3693n, 0xFFFFFFFFFFFFFFFFn, true, true, 0xFFFFFFFFFFFFFFFFn);
+ case "CRC-82/DARC": return this.crc(82n, input, 0x0308C0111011401440411n, 0x000000000000000000000n, true, true, 0x000000000000000000000n);
+ default: throw new OperationError("Unknown checksum algorithm");
+ }
+ }
+
+}
+
+export default CRCChecksum;
diff --git a/src/core/operations/CSSSelector.mjs b/src/core/operations/CSSSelector.mjs
index d6b8da110..639726c49 100644
--- a/src/core/operations/CSSSelector.mjs
+++ b/src/core/operations/CSSSelector.mjs
@@ -6,7 +6,7 @@
import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs";
-import xmldom from "xmldom";
+import xmldom from "@xmldom/xmldom";
import nwmatcher from "nwmatcher";
/**
diff --git a/src/core/operations/CTPH.mjs b/src/core/operations/CTPH.mjs
index 7394faaae..6b6a487d8 100644
--- a/src/core/operations/CTPH.mjs
+++ b/src/core/operations/CTPH.mjs
@@ -21,7 +21,7 @@ class CTPH extends Operation {
this.name = "CTPH";
this.module = "Crypto";
this.description = "Context Triggered Piecewise Hashing, also called Fuzzy Hashing, can match inputs that have homologies. Such inputs have sequences of identical bytes in the same order, although bytes in between these sequences may be different in both content and length. CTPH was originally based on the work of Dr. Andrew Tridgell and a spam email detector called SpamSum. This method was adapted by Jesse Kornblum and published at the DFRWS conference in 2006 in a paper 'Identifying Almost Identical Files Using Context Triggered Piecewise Hashing'.";
- this.infoURL = "https://forensicswiki.xyz/wiki/index.php?title=Context_Triggered_Piecewise_Hashing";
+ this.infoURL = "https://forensics.wiki/context_triggered_piecewise_hashing/";
this.inputType = "string";
this.outputType = "string";
this.args = [];
diff --git a/src/core/operations/CaretMdecode.mjs b/src/core/operations/CaretMdecode.mjs
new file mode 100644
index 000000000..68c6dacba
--- /dev/null
+++ b/src/core/operations/CaretMdecode.mjs
@@ -0,0 +1,98 @@
+/**
+ * @author tedk [tedk@ted.do]
+ * @copyright Crown Copyright 2024
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+
+/**
+ * Caret/M-decode operation
+ *
+ * https://gist.githubusercontent.com/JaHIY/3c91bbf7bea5661e6abfbd1349ee81a2/raw/c7b480e9ff24bcb8f5287a8a8a2dcb9bf5628506/decode_m_notation.cpp
+ */
+class CaretMdecode extends Operation {
+
+ /**
+ * CaretMdecode constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "Caret/M-decode";
+ this.module = "Default";
+ this.description = "Decodes caret or M-encoded strings, i.e. ^M turns into a newline, M-^] turns into 0x9d. Sources such as `cat -v`.\n\nPlease be aware that when using `cat -v` ^_ (caret-underscore) will not be encoded, but represents a valid encoding (namely that of 0x1f).";
+ this.infoURL = "https://en.wikipedia.org/wiki/Caret_notation";
+ this.inputType = "string";
+ this.outputType = "byteArray";
+ this.args = [];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {byteArray}
+ */
+ run(input, args) {
+
+ const bytes = [];
+
+ let prev = "";
+
+ for (let i = 0; i < input.length; i++) {
+
+ const charCode = input.charCodeAt(i);
+ const curChar = input.charAt(i);
+
+ if (prev === "M-^") {
+ if (charCode > 63 && charCode <= 95) {
+ bytes.push(charCode + 64);
+ } else if (charCode === 63) {
+ bytes.push(255);
+ } else {
+ bytes.push(77, 45, 94, charCode);
+ }
+ prev = "";
+ } else if (prev === "M-") {
+ if (curChar === "^") {
+ prev = prev + "^";
+ } else if (charCode >= 32 && charCode <= 126) {
+ bytes.push(charCode + 128);
+ prev = "";
+ } else {
+ bytes.push(77, 45, charCode);
+ prev = "";
+ }
+ } else if (prev === "M") {
+ if (curChar === "-") {
+ prev = prev + "-";
+ } else {
+ bytes.push(77, charCode);
+ prev = "";
+ }
+ } else if (prev === "^") {
+ if (charCode > 63 && charCode <= 126) {
+ bytes.push(charCode - 64);
+ } else if (charCode === 63) {
+ bytes.push(127);
+ } else {
+ bytes.push(94, charCode);
+ }
+ prev = "";
+ } else {
+ if (curChar === "M") {
+ prev = "M";
+ } else if (curChar === "^") {
+ prev = "^";
+ } else {
+ bytes.push(charCode);
+ }
+ }
+
+ }
+ return bytes;
+ }
+
+}
+
+export default CaretMdecode;
diff --git a/src/core/operations/ChaCha.mjs b/src/core/operations/ChaCha.mjs
index 166c1663d..2d186d358 100644
--- a/src/core/operations/ChaCha.mjs
+++ b/src/core/operations/ChaCha.mjs
@@ -100,7 +100,7 @@ class ChaCha extends Operation {
super();
this.name = "ChaCha";
- this.module = "Default";
+ this.module = "Ciphers";
this.description = "ChaCha is a stream cipher designed by Daniel J. Bernstein. It is a variant of the Salsa stream cipher. Several parameterizations exist; 'ChaCha' may refer to the original construction, or to the variant as described in RFC-8439. ChaCha is often used with Poly1305, in the ChaCha20-Poly1305 AEAD construction.Key: ChaCha uses a key of 16 or 32 bytes (128 or 256 bits).Nonce: ChaCha uses a nonce of 8 or 12 bytes (64 or 96 bits).Counter: ChaCha uses a counter of 4 or 8 bytes (32 or 64 bits); together, the nonce and counter must add up to 16 bytes. 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#ChaCha_variant";
this.inputType = "string";
@@ -191,7 +191,7 @@ ChaCha uses a nonce of 8 or 12 bytes (64 or 96 bits).`);
if (outputType === "Hex") {
return toHex(output);
} else {
- return Utils.arrayBufferToStr(output);
+ return Utils.arrayBufferToStr(Uint8Array.from(output).buffer);
}
}
diff --git a/src/core/operations/CompareCTPHHashes.mjs b/src/core/operations/CompareCTPHHashes.mjs
index 40fec4720..91956220e 100644
--- a/src/core/operations/CompareCTPHHashes.mjs
+++ b/src/core/operations/CompareCTPHHashes.mjs
@@ -24,7 +24,7 @@ class CompareCTPHHashes extends Operation {
this.name = "Compare CTPH hashes";
this.module = "Crypto";
this.description = "Compares two Context Triggered Piecewise Hashing (CTPH) fuzzy hashes to determine the similarity between them on a scale of 0 to 100.";
- this.infoURL = "https://forensicswiki.xyz/wiki/index.php?title=Context_Triggered_Piecewise_Hashing";
+ this.infoURL = "https://forensics.wiki/context_triggered_piecewise_hashing/";
this.inputType = "string";
this.outputType = "Number";
this.args = [
diff --git a/src/core/operations/CompareSSDEEPHashes.mjs b/src/core/operations/CompareSSDEEPHashes.mjs
index 806b048ef..9937d7e6d 100644
--- a/src/core/operations/CompareSSDEEPHashes.mjs
+++ b/src/core/operations/CompareSSDEEPHashes.mjs
@@ -24,7 +24,7 @@ class CompareSSDEEPHashes extends Operation {
this.name = "Compare SSDEEP hashes";
this.module = "Crypto";
this.description = "Compares two SSDEEP fuzzy hashes to determine the similarity between them on a scale of 0 to 100.";
- this.infoURL = "https://forensicswiki.xyz/wiki/index.php?title=Ssdeep";
+ this.infoURL = "https://forensics.wiki/ssdeep/";
this.inputType = "string";
this.outputType = "Number";
this.args = [
diff --git a/src/core/operations/ContainImage.mjs b/src/core/operations/ContainImage.mjs
index 09717a28e..853021e1d 100644
--- a/src/core/operations/ContainImage.mjs
+++ b/src/core/operations/ContainImage.mjs
@@ -9,7 +9,7 @@ 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 from "jimp/es/index.js";
/**
* Contain Image operation
@@ -91,20 +91,20 @@ class ContainImage extends Operation {
const [width, height, hAlign, vAlign, alg, opaqueBg] = args;
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": Jimp.RESIZE_NEAREST_NEIGHBOR,
+ "Bilinear": Jimp.RESIZE_BILINEAR,
+ "Bicubic": Jimp.RESIZE_BICUBIC,
+ "Hermite": Jimp.RESIZE_HERMITE,
+ "Bezier": Jimp.RESIZE_BEZIER
};
const alignMap = {
- "Left": jimp.HORIZONTAL_ALIGN_LEFT,
- "Center": jimp.HORIZONTAL_ALIGN_CENTER,
- "Right": jimp.HORIZONTAL_ALIGN_RIGHT,
- "Top": jimp.VERTICAL_ALIGN_TOP,
- "Middle": jimp.VERTICAL_ALIGN_MIDDLE,
- "Bottom": jimp.VERTICAL_ALIGN_BOTTOM
+ "Left": Jimp.HORIZONTAL_ALIGN_LEFT,
+ "Center": Jimp.HORIZONTAL_ALIGN_CENTER,
+ "Right": Jimp.HORIZONTAL_ALIGN_RIGHT,
+ "Top": Jimp.VERTICAL_ALIGN_TOP,
+ "Middle": Jimp.VERTICAL_ALIGN_MIDDLE,
+ "Bottom": Jimp.VERTICAL_ALIGN_BOTTOM
};
if (!isImage(input)) {
@@ -113,7 +113,7 @@ class ContainImage extends Operation {
let image;
try {
- image = await jimp.read(input);
+ image = await Jimp.read(input);
} catch (err) {
throw new OperationError(`Error loading image. (${err})`);
}
@@ -123,16 +123,16 @@ class ContainImage extends Operation {
image.contain(width, height, alignMap[hAlign] | alignMap[vAlign], resizeMap[alg]);
if (opaqueBg) {
- const newImage = await jimp.read(width, height, 0x000000FF);
+ const newImage = await Jimp.read(width, height, 0x000000FF);
newImage.blit(image, 0, 0);
image = newImage;
}
let imageBuffer;
if (image.getMIME() === "image/gif") {
- imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
+ imageBuffer = await image.getBufferAsync(Jimp.MIME_PNG);
} else {
- imageBuffer = await image.getBufferAsync(jimp.AUTO);
+ imageBuffer = await image.getBufferAsync(Jimp.AUTO);
}
return imageBuffer.buffer;
} catch (err) {
diff --git a/src/core/operations/ConvertImageFormat.mjs b/src/core/operations/ConvertImageFormat.mjs
index 9f8cdd913..7d57f33ba 100644
--- a/src/core/operations/ConvertImageFormat.mjs
+++ b/src/core/operations/ConvertImageFormat.mjs
@@ -8,7 +8,7 @@ import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs";
import { isImage } from "../lib/FileType.mjs";
import { toBase64 } from "../lib/Base64.mjs";
-import jimp from "jimp";
+import Jimp from "jimp/es/index.js";
/**
* Convert Image Format operation
@@ -76,19 +76,19 @@ class ConvertImageFormat extends Operation {
async run(input, args) {
const [format, jpegQuality, pngFilterType, pngDeflateLevel] = args;
const formatMap = {
- "JPEG": jimp.MIME_JPEG,
- "PNG": jimp.MIME_PNG,
- "BMP": jimp.MIME_BMP,
- "TIFF": jimp.MIME_TIFF
+ "JPEG": Jimp.MIME_JPEG,
+ "PNG": Jimp.MIME_PNG,
+ "BMP": Jimp.MIME_BMP,
+ "TIFF": Jimp.MIME_TIFF
};
const pngFilterMap = {
- "Auto": jimp.PNG_FILTER_AUTO,
- "None": jimp.PNG_FILTER_NONE,
- "Sub": jimp.PNG_FILTER_SUB,
- "Up": jimp.PNG_FILTER_UP,
- "Average": jimp.PNG_FILTER_AVERAGE,
- "Paeth": jimp.PNG_FILTER_PATH
+ "Auto": Jimp.PNG_FILTER_AUTO,
+ "None": Jimp.PNG_FILTER_NONE,
+ "Sub": Jimp.PNG_FILTER_SUB,
+ "Up": Jimp.PNG_FILTER_UP,
+ "Average": Jimp.PNG_FILTER_AVERAGE,
+ "Paeth": Jimp.PNG_FILTER_PATH
};
const mime = formatMap[format];
@@ -98,7 +98,7 @@ class ConvertImageFormat extends Operation {
}
let image;
try {
- image = await jimp.read(input);
+ image = await Jimp.read(input);
} catch (err) {
throw new OperationError(`Error opening image file. (${err})`);
}
diff --git a/src/core/operations/ConvertLeetSpeak.mjs b/src/core/operations/ConvertLeetSpeak.mjs
new file mode 100644
index 000000000..1a7cb2fcd
--- /dev/null
+++ b/src/core/operations/ConvertLeetSpeak.mjs
@@ -0,0 +1,116 @@
+/**
+ * @author bartblaze []
+ * @copyright Crown Copyright 2025
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+
+/**
+ * Convert Leet Speak operation
+ */
+class ConvertLeetSpeak extends Operation {
+ /**
+ * ConvertLeetSpeak constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "Convert Leet Speak";
+ this.module = "Default";
+ this.description = "Converts to and from Leet Speak.";
+ this.infoURL = "https://wikipedia.org/wiki/Leet";
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [
+ {
+ name: "Direction",
+ type: "option",
+ value: ["To Leet Speak", "From Leet Speak"],
+ defaultIndex: 0
+ }
+ ];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ const direction = args[0];
+
+ if (direction === "To Leet Speak") {
+ return input.replace(/[a-z]/gi, char => {
+ const leetChar = toLeetMap[char.toLowerCase()] || char;
+ return char === char.toUpperCase() ? leetChar.toUpperCase() : leetChar;
+ });
+ } else if (direction === "From Leet Speak") {
+ return input.replace(/[48cd3f6h1jklmn0pqr57uvwxyz]/gi, char => {
+ const normalChar = fromLeetMap[char] || char;
+ return normalChar;
+ });
+ }
+ }
+}
+
+const toLeetMap = {
+ "a": "4",
+ "b": "b",
+ "c": "c",
+ "d": "d",
+ "e": "3",
+ "f": "f",
+ "g": "g",
+ "h": "h",
+ "i": "1",
+ "j": "j",
+ "k": "k",
+ "l": "l",
+ "m": "m",
+ "n": "n",
+ "o": "0",
+ "p": "p",
+ "q": "q",
+ "r": "r",
+ "s": "5",
+ "t": "7",
+ "u": "u",
+ "v": "v",
+ "w": "w",
+ "x": "x",
+ "y": "y",
+ "z": "z"
+};
+
+const fromLeetMap = {
+ "4": "a",
+ "b": "b",
+ "c": "c",
+ "d": "d",
+ "3": "e",
+ "f": "f",
+ "g": "g",
+ "h": "h",
+ "1": "i",
+ "j": "j",
+ "k": "k",
+ "l": "l",
+ "m": "m",
+ "n": "n",
+ "0": "o",
+ "p": "p",
+ "q": "q",
+ "r": "r",
+ "5": "s",
+ "7": "t",
+ "u": "u",
+ "v": "v",
+ "w": "w",
+ "x": "x",
+ "y": "y",
+ "z": "z"
+};
+
+export default ConvertLeetSpeak;
+
diff --git a/src/core/operations/CoverImage.mjs b/src/core/operations/CoverImage.mjs
index 07838ecfd..7d4d07b98 100644
--- a/src/core/operations/CoverImage.mjs
+++ b/src/core/operations/CoverImage.mjs
@@ -9,7 +9,7 @@ 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 from "jimp/es/index.js";
/**
* Cover Image operation
diff --git a/src/core/operations/CropImage.mjs b/src/core/operations/CropImage.mjs
index 8b4800807..11a85e37b 100644
--- a/src/core/operations/CropImage.mjs
+++ b/src/core/operations/CropImage.mjs
@@ -9,7 +9,7 @@ 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 from "jimp/es/index.js";
/**
* Crop Image operation
@@ -99,7 +99,7 @@ class CropImage extends Operation {
let image;
try {
- image = await jimp.read(input);
+ image = await Jimp.read(input);
} catch (err) {
throw new OperationError(`Error loading image. (${err})`);
}
@@ -119,9 +119,9 @@ class CropImage extends Operation {
let imageBuffer;
if (image.getMIME() === "image/gif") {
- imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
+ imageBuffer = await image.getBufferAsync(Jimp.MIME_PNG);
} else {
- imageBuffer = await image.getBufferAsync(jimp.AUTO);
+ imageBuffer = await image.getBufferAsync(Jimp.AUTO);
}
return imageBuffer.buffer;
} catch (err) {
diff --git a/src/core/operations/DESDecrypt.mjs b/src/core/operations/DESDecrypt.mjs
index 856aa0658..4b1ab40ee 100644
--- a/src/core/operations/DESDecrypt.mjs
+++ b/src/core/operations/DESDecrypt.mjs
@@ -22,7 +22,7 @@ class DESDecrypt extends Operation {
this.name = "DES Decrypt";
this.module = "Ciphers";
- this.description = "DES is a previously dominant algorithm for encryption, and was published as an official U.S. Federal Information Processing Standard (FIPS). It is now considered to be insecure due to its small key size.Key: DES uses a key length of 8 bytes (64 bits). Triple DES uses a key length of 24 bytes (192 bits).IV: The Initialization Vector should be 8 bytes long. If not entered, it will default to 8 null bytes.Padding: In CBC and ECB mode, PKCS#7 padding will be used as a default.";
+ this.description = "DES is a previously dominant algorithm for encryption, and was published as an official U.S. Federal Information Processing Standard (FIPS). It is now considered to be insecure due to its small key size.Key: DES uses a key length of 8 bytes (64 bits).IV: The Initialization Vector should be 8 bytes long. If not entered, it will default to 8 null bytes.Padding: In CBC and ECB mode, PKCS#7 padding will be used as a default.";
this.infoURL = "https://wikipedia.org/wiki/Data_Encryption_Standard";
this.inputType = "string";
this.outputType = "string";
@@ -72,8 +72,7 @@ class DESDecrypt extends Operation {
if (key.length !== 8) {
throw new OperationError(`Invalid key length: ${key.length} bytes
-DES uses a key length of 8 bytes (64 bits).
-Triple DES uses a key length of 24 bytes (192 bits).`);
+DES uses a key length of 8 bytes (64 bits).`);
}
if (iv.length !== 8 && mode !== "ECB") {
throw new OperationError(`Invalid IV length: ${iv.length} bytes
diff --git a/src/core/operations/DESEncrypt.mjs b/src/core/operations/DESEncrypt.mjs
index 9472abe87..28d6202a6 100644
--- a/src/core/operations/DESEncrypt.mjs
+++ b/src/core/operations/DESEncrypt.mjs
@@ -22,7 +22,7 @@ class DESEncrypt extends Operation {
this.name = "DES Encrypt";
this.module = "Ciphers";
- this.description = "DES is a previously dominant algorithm for encryption, and was published as an official U.S. Federal Information Processing Standard (FIPS). It is now considered to be insecure due to its small key size.Key: DES uses a key length of 8 bytes (64 bits). Triple DES uses a key length of 24 bytes (192 bits). You can generate a password-based key using one of the KDF operations.IV: The Initialization Vector should be 8 bytes long. If not entered, it will default to 8 null bytes.Padding: In CBC and ECB mode, PKCS#7 padding will be used.";
+ this.description = "DES is a previously dominant algorithm for encryption, and was published as an official U.S. Federal Information Processing Standard (FIPS). It is now considered to be insecure due to its small key size.Key: DES uses a key length of 8 bytes (64 bits). You can generate a password-based key using one of the KDF operations.IV: The Initialization Vector should be 8 bytes long. If not entered, it will default to 8 null bytes.Padding: In CBC and ECB mode, PKCS#7 padding will be used.";
this.infoURL = "https://wikipedia.org/wiki/Data_Encryption_Standard";
this.inputType = "string";
this.outputType = "string";
@@ -70,8 +70,7 @@ class DESEncrypt extends Operation {
if (key.length !== 8) {
throw new OperationError(`Invalid key length: ${key.length} bytes
-DES uses a key length of 8 bytes (64 bits).
-Triple DES uses a key length of 24 bytes (192 bits).`);
+DES uses a key length of 8 bytes (64 bits).`);
}
if (iv.length !== 8 && mode !== "ECB") {
throw new OperationError(`Invalid IV length: ${iv.length} bytes
diff --git a/src/core/operations/DateTimeDelta.mjs b/src/core/operations/DateTimeDelta.mjs
new file mode 100644
index 000000000..7f82cf01f
--- /dev/null
+++ b/src/core/operations/DateTimeDelta.mjs
@@ -0,0 +1,107 @@
+/**
+ * @author tomgond [tom.gonda@gmail.com]
+ * @copyright Crown Copyright 2024
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+import moment from "moment-timezone";
+import {DATETIME_FORMATS, FORMAT_EXAMPLES} from "../lib/DateTime.mjs";
+
+/**
+ * DateTime Delta operation
+ */
+class DateTimeDelta extends Operation {
+
+ /**
+ * DateTimeDelta constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "DateTime Delta";
+ this.module = "Default";
+ this.description = "Calculates a new DateTime value given an input DateTime value and a time difference (delta) from the input DateTime value.";
+ this.inputType = "string";
+ this.outputType = "html";
+ this.args = [
+ {
+ "name": "Built in formats",
+ "type": "populateOption",
+ "value": DATETIME_FORMATS,
+ "target": 1
+ },
+ {
+ "name": "Input format string",
+ "type": "binaryString",
+ "value": "DD/MM/YYYY HH:mm:ss"
+ },
+ {
+ "name": "Time Operation",
+ "type": "option",
+ "value": ["Add", "Subtract"]
+ },
+ {
+ "name": "Days",
+ "type": "number",
+ "value": 0
+ },
+ {
+ "name": "Hours",
+ "type": "number",
+ "value": 0
+ },
+ {
+ "name": "Minutes",
+ "type": "number",
+ "value": 0
+ },
+ {
+ "name": "Seconds",
+ "type": "number",
+ "value": 0
+ }
+
+ ];
+ }
+
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ const inputTimezone = "UTC";
+ const inputFormat = args[1];
+ const operationType = args[2];
+ const daysDelta = args[3];
+ const hoursDelta = args[4];
+ const minutesDelta = args[5];
+ const secondsDelta = args[6];
+ let date = "";
+
+ try {
+ date = moment.tz(input, inputFormat, inputTimezone);
+ if (!date || date.format() === "Invalid date") throw Error;
+ } catch (err) {
+ return `Invalid format.\n\n${FORMAT_EXAMPLES}`;
+ }
+ let newDate;
+ if (operationType === "Add") {
+ newDate = date.add(daysDelta, "days")
+ .add(hoursDelta, "hours")
+ .add(minutesDelta, "minutes")
+ .add(secondsDelta, "seconds");
+
+ } else {
+ newDate = date.add(-daysDelta, "days")
+ .add(-hoursDelta, "hours")
+ .add(-minutesDelta, "minutes")
+ .add(-secondsDelta, "seconds");
+ }
+ return newDate.tz(inputTimezone).format(inputFormat.replace(/[<>]/g, ""));
+ }
+}
+
+export default DateTimeDelta;
diff --git a/src/core/operations/DeriveEVPKey.mjs b/src/core/operations/DeriveEVPKey.mjs
index 5885f892f..3d67aa512 100644
--- a/src/core/operations/DeriveEVPKey.mjs
+++ b/src/core/operations/DeriveEVPKey.mjs
@@ -62,11 +62,13 @@ class DeriveEVPKey extends Operation {
* @returns {string}
*/
run(input, args) {
- const passphrase = Utils.convertToByteString(args[0].string, args[0].option),
+ const passphrase = CryptoJS.enc.Latin1.parse(
+ Utils.convertToByteString(args[0].string, args[0].option)),
keySize = args[1] / 32,
iterations = args[2],
hasher = args[3],
- salt = Utils.convertToByteString(args[4].string, args[4].option),
+ salt = CryptoJS.enc.Latin1.parse(
+ Utils.convertToByteString(args[4].string, args[4].option)),
key = CryptoJS.EvpKDF(passphrase, salt, { // lgtm [js/insufficient-password-hash]
keySize: keySize,
hasher: CryptoJS.algo[hasher],
diff --git a/src/core/operations/Diff.mjs b/src/core/operations/Diff.mjs
index 841072457..9f180f861 100644
--- a/src/core/operations/Diff.mjs
+++ b/src/core/operations/Diff.mjs
@@ -119,9 +119,9 @@ class Diff extends Operation {
for (let i = 0; i < diff.length; i++) {
if (diff[i].added) {
- if (showAdded) output += "" + Utils.escapeHtml(diff[i].value) + " ";
+ if (showAdded) output += "" + Utils.escapeHtml(diff[i].value) + " ";
} else if (diff[i].removed) {
- if (showRemoved) output += "" + Utils.escapeHtml(diff[i].value) + " ";
+ if (showRemoved) output += "" + Utils.escapeHtml(diff[i].value) + "";
} else if (!showSubtraction) {
output += Utils.escapeHtml(diff[i].value);
}
diff --git a/src/core/operations/DitherImage.mjs b/src/core/operations/DitherImage.mjs
index 6aef72dc5..170514801 100644
--- a/src/core/operations/DitherImage.mjs
+++ b/src/core/operations/DitherImage.mjs
@@ -9,7 +9,7 @@ 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 from "jimp/es/index.js";
/**
* Image Dither operation
@@ -44,7 +44,7 @@ class DitherImage extends Operation {
let image;
try {
- image = await jimp.read(input);
+ image = await Jimp.read(input);
} catch (err) {
throw new OperationError(`Error loading image. (${err})`);
}
@@ -55,9 +55,9 @@ class DitherImage extends Operation {
let imageBuffer;
if (image.getMIME() === "image/gif") {
- imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
+ imageBuffer = await image.getBufferAsync(Jimp.MIME_PNG);
} else {
- imageBuffer = await image.getBufferAsync(jimp.AUTO);
+ imageBuffer = await image.getBufferAsync(Jimp.AUTO);
}
return imageBuffer.buffer;
} catch (err) {
diff --git a/src/core/operations/DropNthBytes.mjs b/src/core/operations/DropNthBytes.mjs
new file mode 100644
index 000000000..e6bac1cd2
--- /dev/null
+++ b/src/core/operations/DropNthBytes.mjs
@@ -0,0 +1,79 @@
+/**
+ * @author Oshawk [oshawk@protonmail.com]
+ * @copyright Crown Copyright 2019
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+import OperationError from "../errors/OperationError.mjs";
+
+/**
+ * Drop nth bytes operation
+ */
+class DropNthBytes extends Operation {
+
+ /**
+ * DropNthBytes constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "Drop nth bytes";
+ this.module = "Default";
+ this.description = "Drops every nth byte starting with a given byte.";
+ this.infoURL = "";
+ this.inputType = "byteArray";
+ this.outputType = "byteArray";
+ this.args = [
+ {
+ name: "Drop every",
+ type: "number",
+ value: 4
+ },
+ {
+ name: "Starting at",
+ type: "number",
+ value: 0
+ },
+ {
+ name: "Apply to each line",
+ type: "boolean",
+ value: false
+ }
+ ];
+ }
+
+ /**
+ * @param {byteArray} input
+ * @param {Object[]} args
+ * @returns {byteArray}
+ */
+ run(input, args) {
+ const n = args[0];
+ const start = args[1];
+ const eachLine = args[2];
+
+ if (parseInt(n, 10) !== n || n <= 0) {
+ throw new OperationError("'Drop every' must be a positive integer.");
+ }
+ if (parseInt(start, 10) !== start || start < 0) {
+ throw new OperationError("'Starting at' must be a positive or zero integer.");
+ }
+
+ let offset = 0;
+ const output = [];
+ for (let i = 0; i < input.length; i++) {
+ if (eachLine && input[i] === 0x0a) {
+ output.push(0x0a);
+ offset = i + 1;
+ } else if (i - offset < start || (i - (start + offset)) % n !== 0) {
+ output.push(input[i]);
+ }
+ }
+
+ return output;
+ }
+
+}
+
+export default DropNthBytes;
diff --git a/src/core/operations/ECDSASign.mjs b/src/core/operations/ECDSASign.mjs
new file mode 100644
index 000000000..7b8f57f18
--- /dev/null
+++ b/src/core/operations/ECDSASign.mjs
@@ -0,0 +1,107 @@
+/**
+ * @author cplussharp
+ * @copyright Crown Copyright 2021
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+import OperationError from "../errors/OperationError.mjs";
+import { fromHex } from "../lib/Hex.mjs";
+import { toBase64 } from "../lib/Base64.mjs";
+import r from "jsrsasign";
+
+/**
+ * ECDSA Sign operation
+ */
+class ECDSASign extends Operation {
+
+ /**
+ * ECDSASign constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "ECDSA Sign";
+ this.module = "Ciphers";
+ this.description = "Sign a plaintext message with a PEM encoded EC key.";
+ this.infoURL = "https://wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm";
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [
+ {
+ name: "ECDSA Private Key (PEM)",
+ type: "text",
+ value: "-----BEGIN EC PRIVATE KEY-----"
+ },
+ {
+ name: "Message Digest Algorithm",
+ type: "option",
+ value: [
+ "SHA-256",
+ "SHA-384",
+ "SHA-512",
+ "SHA-1",
+ "MD5"
+ ]
+ },
+ {
+ name: "Output Format",
+ type: "option",
+ value: [
+ "ASN.1 HEX",
+ "P1363 HEX",
+ "JSON Web Signature",
+ "Raw JSON"
+ ]
+ }
+ ];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ const [keyPem, mdAlgo, outputFormat] = args;
+
+ if (keyPem.replace("-----BEGIN EC PRIVATE KEY-----", "").length === 0) {
+ throw new OperationError("Please enter a private key.");
+ }
+
+ const internalAlgorithmName = mdAlgo.replace("-", "") + "withECDSA";
+ const sig = new r.KJUR.crypto.Signature({ alg: internalAlgorithmName });
+ const key = r.KEYUTIL.getKey(keyPem);
+ if (key.type !== "EC") {
+ throw new OperationError("Provided key is not an EC key.");
+ }
+ if (!key.isPrivate) {
+ throw new OperationError("Provided key is not a private key.");
+ }
+ sig.init(key);
+ const signatureASN1Hex = sig.signString(input);
+
+ let result;
+ switch (outputFormat) {
+ case "ASN.1 HEX":
+ result = signatureASN1Hex;
+ break;
+ case "P1363 HEX":
+ result = r.KJUR.crypto.ECDSA.asn1SigToConcatSig(signatureASN1Hex);
+ break;
+ case "JSON Web Signature":
+ result = r.KJUR.crypto.ECDSA.asn1SigToConcatSig(signatureASN1Hex);
+ result = toBase64(fromHex(result), "A-Za-z0-9-_"); // base64url
+ break;
+ case "Raw JSON": {
+ const signatureRS = r.KJUR.crypto.ECDSA.parseSigHexInHexRS(signatureASN1Hex);
+ result = JSON.stringify(signatureRS);
+ break;
+ }
+ }
+
+ return result;
+ }
+}
+
+export default ECDSASign;
diff --git a/src/core/operations/ECDSASignatureConversion.mjs b/src/core/operations/ECDSASignatureConversion.mjs
new file mode 100644
index 000000000..3f6c6bfb0
--- /dev/null
+++ b/src/core/operations/ECDSASignatureConversion.mjs
@@ -0,0 +1,146 @@
+/**
+ * @author cplussharp
+ * @copyright Crown Copyright 2021
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+import OperationError from "../errors/OperationError.mjs";
+import { fromBase64, toBase64 } from "../lib/Base64.mjs";
+import { fromHex, toHexFast } from "../lib/Hex.mjs";
+import r from "jsrsasign";
+
+/**
+ * ECDSA Sign operation
+ */
+class ECDSASignatureConversion extends Operation {
+
+ /**
+ * ECDSASignatureConversion constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "ECDSA Signature Conversion";
+ this.module = "Ciphers";
+ this.description = "Convert an ECDSA signature between hex, asn1 and json.";
+ this.infoURL = "https://wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm";
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [
+ {
+ name: "Input Format",
+ type: "option",
+ value: [
+ "Auto",
+ "ASN.1 HEX",
+ "P1363 HEX",
+ "JSON Web Signature",
+ "Raw JSON"
+ ]
+ },
+ {
+ name: "Output Format",
+ type: "option",
+ value: [
+ "ASN.1 HEX",
+ "P1363 HEX",
+ "JSON Web Signature",
+ "Raw JSON"
+ ]
+ }
+ ];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ let inputFormat = args[0];
+ const outputFormat = args[1];
+
+ // detect input format
+ let inputJson;
+ if (inputFormat === "Auto") {
+ try {
+ inputJson = JSON.parse(input);
+ if (typeof(inputJson) === "object") {
+ inputFormat = "Raw JSON";
+ }
+ } catch {}
+ }
+
+ if (inputFormat === "Auto") {
+ const hexRegex = /^[a-f\d]{2,}$/gi;
+ if (hexRegex.test(input)) {
+ if (input.substring(0, 2) === "30" && r.ASN1HEX.isASN1HEX(input)) {
+ inputFormat = "ASN.1 HEX";
+ } else {
+ inputFormat = "P1363 HEX";
+ }
+ }
+ }
+
+ let inputBase64;
+ if (inputFormat === "Auto") {
+ try {
+ inputBase64 = fromBase64(input, "A-Za-z0-9-_", false);
+ inputFormat = "JSON Web Signature";
+ } catch {}
+ }
+
+ // convert input to ASN.1 hex
+ let signatureASN1Hex;
+ switch (inputFormat) {
+ case "Auto":
+ throw new OperationError("Signature format could not be detected");
+ case "ASN.1 HEX":
+ signatureASN1Hex = input;
+ break;
+ case "P1363 HEX":
+ signatureASN1Hex = r.KJUR.crypto.ECDSA.concatSigToASN1Sig(input);
+ break;
+ case "JSON Web Signature":
+ if (!inputBase64) inputBase64 = fromBase64(input, "A-Za-z0-9-_");
+ signatureASN1Hex = r.KJUR.crypto.ECDSA.concatSigToASN1Sig(toHexFast(inputBase64));
+ break;
+ case "Raw JSON": {
+ if (!inputJson) inputJson = JSON.parse(input);
+ if (!inputJson.r) {
+ throw new OperationError('No "r" value in the signature JSON');
+ }
+ if (!inputJson.s) {
+ throw new OperationError('No "s" value in the signature JSON');
+ }
+ signatureASN1Hex = r.KJUR.crypto.ECDSA.hexRSSigToASN1Sig(inputJson.r, inputJson.s);
+ break;
+ }
+ }
+
+ // convert ASN.1 hex to output format
+ let result;
+ switch (outputFormat) {
+ case "ASN.1 HEX":
+ result = signatureASN1Hex;
+ break;
+ case "P1363 HEX":
+ result = r.KJUR.crypto.ECDSA.asn1SigToConcatSig(signatureASN1Hex);
+ break;
+ case "JSON Web Signature":
+ result = r.KJUR.crypto.ECDSA.asn1SigToConcatSig(signatureASN1Hex);
+ result = toBase64(fromHex(result), "A-Za-z0-9-_"); // base64url
+ break;
+ case "Raw JSON": {
+ const signatureRS = r.KJUR.crypto.ECDSA.parseSigHexInHexRS(signatureASN1Hex);
+ result = JSON.stringify(signatureRS);
+ break;
+ }
+ }
+
+ return result;
+ }
+}
+
+export default ECDSASignatureConversion;
diff --git a/src/core/operations/ECDSAVerify.mjs b/src/core/operations/ECDSAVerify.mjs
new file mode 100644
index 000000000..1f8a53ea6
--- /dev/null
+++ b/src/core/operations/ECDSAVerify.mjs
@@ -0,0 +1,161 @@
+/**
+ * @author cplussharp
+ * @copyright Crown Copyright 2021
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+import OperationError from "../errors/OperationError.mjs";
+import { fromBase64 } from "../lib/Base64.mjs";
+import { toHexFast } from "../lib/Hex.mjs";
+import r from "jsrsasign";
+import Utils from "../Utils.mjs";
+
+/**
+ * ECDSA Verify operation
+ */
+class ECDSAVerify extends Operation {
+
+ /**
+ * ECDSAVerify constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "ECDSA Verify";
+ this.module = "Ciphers";
+ this.description = "Verify a message against a signature and a public PEM encoded EC key.";
+ this.infoURL = "https://wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm";
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [
+ {
+ name: "Input Format",
+ type: "option",
+ value: [
+ "Auto",
+ "ASN.1 HEX",
+ "P1363 HEX",
+ "JSON Web Signature",
+ "Raw JSON"
+ ]
+ },
+ {
+ name: "Message Digest Algorithm",
+ type: "option",
+ value: [
+ "SHA-256",
+ "SHA-384",
+ "SHA-512",
+ "SHA-1",
+ "MD5"
+ ]
+ },
+ {
+ name: "ECDSA Public Key (PEM)",
+ type: "text",
+ value: "-----BEGIN PUBLIC KEY-----"
+ },
+ {
+ name: "Message",
+ type: "text",
+ value: ""
+ },
+ {
+ name: "Message format",
+ type: "option",
+ value: ["Raw", "Hex", "Base64"]
+ }
+ ];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ let inputFormat = args[0];
+ const [, mdAlgo, keyPem, msg, msgFormat] = args;
+
+ if (keyPem.replace("-----BEGIN PUBLIC KEY-----", "").length === 0) {
+ throw new OperationError("Please enter a public key.");
+ }
+
+ // detect input format
+ let inputJson;
+ if (inputFormat === "Auto") {
+ try {
+ inputJson = JSON.parse(input);
+ if (typeof(inputJson) === "object") {
+ inputFormat = "Raw JSON";
+ }
+ } catch {}
+ }
+
+ if (inputFormat === "Auto") {
+ const hexRegex = /^[a-f\d]{2,}$/gi;
+ if (hexRegex.test(input)) {
+ if (input.substring(0, 2) === "30" && r.ASN1HEX.isASN1HEX(input)) {
+ inputFormat = "ASN.1 HEX";
+ } else {
+ inputFormat = "P1363 HEX";
+ }
+ }
+ }
+
+ let inputBase64;
+ if (inputFormat === "Auto") {
+ try {
+ inputBase64 = fromBase64(input, "A-Za-z0-9-_", false);
+ inputFormat = "JSON Web Signature";
+ } catch {}
+ }
+
+ // convert to ASN.1 signature
+ let signatureASN1Hex;
+ switch (inputFormat) {
+ case "Auto":
+ throw new OperationError("Signature format could not be detected");
+ case "ASN.1 HEX":
+ signatureASN1Hex = input;
+ break;
+ case "P1363 HEX":
+ signatureASN1Hex = r.KJUR.crypto.ECDSA.concatSigToASN1Sig(input);
+ break;
+ case "JSON Web Signature":
+ if (!inputBase64) inputBase64 = fromBase64(input, "A-Za-z0-9-_");
+ signatureASN1Hex = r.KJUR.crypto.ECDSA.concatSigToASN1Sig(toHexFast(inputBase64));
+ break;
+ case "Raw JSON": {
+ if (!inputJson) inputJson = JSON.parse(input);
+ if (!inputJson.r) {
+ throw new OperationError('No "r" value in the signature JSON');
+ }
+ if (!inputJson.s) {
+ throw new OperationError('No "s" value in the signature JSON');
+ }
+ signatureASN1Hex = r.KJUR.crypto.ECDSA.hexRSSigToASN1Sig(inputJson.r, inputJson.s);
+ break;
+ }
+ }
+
+ // verify signature
+ const internalAlgorithmName = mdAlgo.replace("-", "") + "withECDSA";
+ const sig = new r.KJUR.crypto.Signature({ alg: internalAlgorithmName });
+ const key = r.KEYUTIL.getKey(keyPem);
+ if (key.type !== "EC") {
+ throw new OperationError("Provided key is not an EC key.");
+ }
+ if (!key.isPublic) {
+ throw new OperationError("Provided key is not a public key.");
+ }
+ sig.init(key);
+ const messageStr = Utils.convertToByteString(msg, msgFormat);
+ sig.updateString(messageStr);
+ const result = sig.verify(signatureASN1Hex);
+ return result ? "Verified OK" : "Verification Failure";
+ }
+}
+
+export default ECDSAVerify;
diff --git a/src/core/operations/ExtractDomains.mjs b/src/core/operations/ExtractDomains.mjs
index c28efbb5b..9e78bf3a2 100644
--- a/src/core/operations/ExtractDomains.mjs
+++ b/src/core/operations/ExtractDomains.mjs
@@ -5,7 +5,7 @@
*/
import Operation from "../Operation.mjs";
-import { search, DOMAIN_REGEX } from "../lib/Extract.mjs";
+import { search, DOMAIN_REGEX, DMARC_DOMAIN_REGEX } from "../lib/Extract.mjs";
import { caseInsensitiveSort } from "../lib/Sort.mjs";
/**
@@ -39,6 +39,11 @@ class ExtractDomains extends Operation {
name: "Unique",
type: "boolean",
value: false
+ },
+ {
+ name: "Underscore (DMARC, DKIM, etc)",
+ type: "boolean",
+ value: false
}
];
}
@@ -49,11 +54,11 @@ class ExtractDomains extends Operation {
* @returns {string}
*/
run(input, args) {
- const [displayTotal, sort, unique] = args;
+ const [displayTotal, sort, unique, dmarc] = args;
const results = search(
input,
- DOMAIN_REGEX,
+ dmarc ? DMARC_DOMAIN_REGEX : DOMAIN_REGEX,
null,
sort ? caseInsensitiveSort : null,
unique
diff --git a/src/core/operations/ExtractEmailAddresses.mjs b/src/core/operations/ExtractEmailAddresses.mjs
index f50e1aaf5..34b838ab3 100644
--- a/src/core/operations/ExtractEmailAddresses.mjs
+++ b/src/core/operations/ExtractEmailAddresses.mjs
@@ -51,7 +51,7 @@ class ExtractEmailAddresses extends Operation {
run(input, args) {
const [displayTotal, sort, unique] = args,
// email regex from: https://www.regextester.com/98066
- regex = /(?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9](?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9-]*[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9])?\.)+[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9](?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9-]*[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}\])/ig;
+ regex = /(?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9](?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9-]*[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9])?\.)+[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9](?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9-]*[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\])/ig;
const results = search(
input,
diff --git a/src/core/operations/ExtractFiles.mjs b/src/core/operations/ExtractFiles.mjs
index 4c6fd1dfd..3ff8ffb6c 100644
--- a/src/core/operations/ExtractFiles.mjs
+++ b/src/core/operations/ExtractFiles.mjs
@@ -39,7 +39,7 @@ class ExtractFiles extends Operation {
${supportedExts.join("")}
Minimum File Size can be used to prune small false positives.`;
- this.infoURL = "https://forensicswiki.xyz/wiki/index.php?title=File_Carving";
+ this.infoURL = "https://forensics.wiki/file_carving";
this.inputType = "ArrayBuffer";
this.outputType = "List";
this.presentType = "html";
diff --git a/src/core/operations/ExtractHashes.mjs b/src/core/operations/ExtractHashes.mjs
new file mode 100644
index 000000000..fd50089f5
--- /dev/null
+++ b/src/core/operations/ExtractHashes.mjs
@@ -0,0 +1,84 @@
+/**
+ * @author mshwed [m@ttshwed.com]
+ * @copyright Crown Copyright 2019
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+import { search } from "../lib/Extract.mjs";
+
+/**
+ * Extract Hash Values operation
+ */
+class ExtractHashes extends Operation {
+
+ /**
+ * ExtractHashValues constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "Extract hashes";
+ this.module = "Regex";
+ this.description = "Extracts potential hashes based on hash character length";
+ this.infoURL = "https://wikipedia.org/wiki/Comparison_of_cryptographic_hash_functions";
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [
+ {
+ name: "Hash character length",
+ type: "number",
+ value: 40
+ },
+ {
+ name: "All hashes",
+ type: "boolean",
+ value: false
+ },
+ {
+ name: "Display Total",
+ type: "boolean",
+ value: false
+ }
+ ];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ const results = [];
+ let hashCount = 0;
+
+ const [hashLength, searchAllHashes, showDisplayTotal] = args;
+
+ // Convert character length to bit length
+ let hashBitLengths = [(hashLength / 2) * 8];
+
+ if (searchAllHashes) hashBitLengths = [4, 8, 16, 32, 64, 128, 160, 192, 224, 256, 320, 384, 512, 1024];
+
+ for (const hashBitLength of hashBitLengths) {
+ // Convert bit length to character length
+ const hashCharacterLength = (hashBitLength / 8) * 2;
+
+ const regex = new RegExp(`(\\b|^)[a-f0-9]{${hashCharacterLength}}(\\b|$)`, "g");
+ const searchResults = search(input, regex, null, false);
+
+ hashCount += searchResults.length;
+ results.push(...searchResults);
+ }
+
+ let output = "";
+ if (showDisplayTotal) {
+ output = `Total Results: ${hashCount}\n\n`;
+ }
+
+ output = output + results.join("\n");
+ return output;
+ }
+
+}
+
+export default ExtractHashes;
diff --git a/src/core/operations/ExtractIPAddresses.mjs b/src/core/operations/ExtractIPAddresses.mjs
index 95e0a50fd..b74ec8fe2 100644
--- a/src/core/operations/ExtractIPAddresses.mjs
+++ b/src/core/operations/ExtractIPAddresses.mjs
@@ -21,7 +21,7 @@ class ExtractIPAddresses extends Operation {
this.name = "Extract IP addresses";
this.module = "Regex";
- this.description = "Extracts all IPv4 and IPv6 addresses. Warning: Given a string 710.65.0.456, this will match 10.65.0.45 so always check the original input!";
+ this.description = "Extracts all IPv4 and IPv6 addresses. Warning: Given a string 1.2.3.4.5.6.7.8, this will match 1.2.3.4 and 5.6.7.8 so always check the original input!";
this.inputType = "string";
this.outputType = "string";
this.args = [
@@ -65,8 +65,22 @@ 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})?",
- ipv6 = "((?=.*::)(?!.*::.+::)(::)?([\\dA-F]{1,4}:(:|\\b)|){5}|([\\dA-F]{1,4}:){6})((([\\dA-F]{1,4}((?!\\3)::|:\\b|(?![\\dA-F])))|(?!\\2\\3)){2}|(((2[0-4]|1\\d|[1-9])?\\d|25[0-5])\\.?\\b){4})";
+
+ // 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),
+ parsedImage = await Jimp.read(input),
width = parsedImage.bitmap.width,
height = parsedImage.bitmap.height,
rgba = parsedImage.bitmap.data;
diff --git a/src/core/operations/ExtractRGBA.mjs b/src/core/operations/ExtractRGBA.mjs
index 7d2fc2749..3339a2a74 100644
--- a/src/core/operations/ExtractRGBA.mjs
+++ b/src/core/operations/ExtractRGBA.mjs
@@ -7,7 +7,7 @@
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/es/index.js";
import {RGBA_DELIM_OPTIONS} from "../lib/Delim.mjs";
@@ -52,7 +52,7 @@ class ExtractRGBA extends Operation {
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);
diff --git a/src/core/operations/FangURL.mjs b/src/core/operations/FangURL.mjs
new file mode 100644
index 000000000..ad5bf525a
--- /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/FernetDecrypt.mjs b/src/core/operations/FernetDecrypt.mjs
new file mode 100644
index 000000000..78d6efb97
--- /dev/null
+++ b/src/core/operations/FernetDecrypt.mjs
@@ -0,0 +1,63 @@
+/**
+ * @author Karsten Silkenbäumer [github.com/kassi]
+ * @copyright Karsten Silkenbäumer 2019
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+import OperationError from "../errors/OperationError.mjs";
+import fernet from "fernet";
+
+/**
+ * FernetDecrypt operation
+ */
+class FernetDecrypt extends Operation {
+ /**
+ * FernetDecrypt constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "Fernet Decrypt";
+ this.module = "Default";
+ this.description = "Fernet is a symmetric encryption method which makes sure that the message encrypted cannot be manipulated/read without the key. It uses URL safe encoding for the keys. Fernet uses 128-bit AES in CBC mode and PKCS7 padding, with HMAC using SHA256 for authentication. The IV is created from os.random().Key: The key must be 32 bytes (256 bits) encoded with Base64.";
+ this.infoURL = "https://asecuritysite.com/encryption/fer";
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [
+ {
+ "name": "Key",
+ "type": "string",
+ "value": ""
+ },
+ ];
+ this.patterns = [
+ {
+ match: "^[A-Z\\d\\-_=]{20,}$",
+ flags: "i",
+ args: []
+ },
+ ];
+ }
+ /**
+ * @param {String} input
+ * @param {Object[]} args
+ * @returns {String}
+ */
+ run(input, args) {
+ const [secretInput] = args;
+ try {
+ const secret = new fernet.Secret(secretInput);
+ const token = new fernet.Token({
+ secret: secret,
+ token: input,
+ ttl: 0
+ });
+ return token.decode();
+ } catch (err) {
+ throw new OperationError(err);
+ }
+ }
+}
+
+export default FernetDecrypt;
diff --git a/src/core/operations/FernetEncrypt.mjs b/src/core/operations/FernetEncrypt.mjs
new file mode 100644
index 000000000..84778486f
--- /dev/null
+++ b/src/core/operations/FernetEncrypt.mjs
@@ -0,0 +1,54 @@
+/**
+ * @author Karsten Silkenbäumer [github.com/kassi]
+ * @copyright Karsten Silkenbäumer 2019
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+import OperationError from "../errors/OperationError.mjs";
+import fernet from "fernet";
+
+/**
+ * FernetEncrypt operation
+ */
+class FernetEncrypt extends Operation {
+ /**
+ * FernetEncrypt constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "Fernet Encrypt";
+ this.module = "Default";
+ this.description = "Fernet is a symmetric encryption method which makes sure that the message encrypted cannot be manipulated/read without the key. It uses URL safe encoding for the keys. Fernet uses 128-bit AES in CBC mode and PKCS7 padding, with HMAC using SHA256 for authentication. The IV is created from os.random().Key: The key must be 32 bytes (256 bits) encoded with Base64.";
+ this.infoURL = "https://asecuritysite.com/encryption/fer";
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [
+ {
+ "name": "Key",
+ "type": "string",
+ "value": ""
+ },
+ ];
+ }
+ /**
+ * @param {String} input
+ * @param {Object[]} args
+ * @returns {String}
+ */
+ run(input, args) {
+ const [secretInput] = args;
+ try {
+ const secret = new fernet.Secret(secretInput);
+ const token = new fernet.Token({
+ secret: secret,
+ });
+ return token.encode(input);
+ } catch (err) {
+ throw new OperationError(err);
+ }
+ }
+}
+
+export default FernetEncrypt;
diff --git a/src/core/operations/FileTree.mjs b/src/core/operations/FileTree.mjs
new file mode 100644
index 000000000..9484313fe
--- /dev/null
+++ b/src/core/operations/FileTree.mjs
@@ -0,0 +1,94 @@
+/**
+ * @author sw5678
+ * @copyright Crown Copyright 2023
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+import Utils from "../Utils.mjs";
+import {INPUT_DELIM_OPTIONS} from "../lib/Delim.mjs";
+
+/**
+ * Unique operation
+ */
+class FileTree extends Operation {
+
+ /**
+ * Unique constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "File Tree";
+ this.module = "Default";
+ 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 = [
+ {
+ name: "File Path Delimiter",
+ type: "binaryString",
+ value: "/"
+ },
+ {
+ name: "Delimiter",
+ type: "option",
+ value: INPUT_DELIM_OPTIONS
+ }
+ ];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+
+ // Set up arrow and pipe for nice output display
+ const ARROW = "|---";
+ const PIPE = "| ";
+
+ // Get args from input
+ const fileDelim = args[0];
+ const entryDelim = Utils.charRep(args[1]);
+
+ // Store path to print
+ const completedList = [];
+ const printList = [];
+
+ // Loop through all entries
+ const filePaths = input.split(entryDelim).unique().sort();
+ for (let i = 0; i < filePaths.length; i++) {
+ // Split by file delimiter
+ let path = filePaths[i].split(fileDelim);
+
+ if (path[0] === "") {
+ path = path.slice(1, path.length);
+ }
+
+ for (let j = 0; j < path.length; j++) {
+ let printLine;
+ let key;
+ if (j === 0) {
+ printLine = path[j];
+ key = path[j];
+ } else {
+ printLine = PIPE.repeat(j-1) + ARROW + path[j];
+ key = path.slice(0, j+1).join("/");
+ }
+
+ // Check to see we have already added that path
+ if (!completedList.includes(key)) {
+ completedList.push(key);
+ printList.push(printLine);
+ }
+ }
+ }
+ return printList.join("\n");
+ }
+
+}
+
+export default FileTree;
diff --git a/src/core/operations/FlipImage.mjs b/src/core/operations/FlipImage.mjs
index 30be5a4e7..f4b7cba93 100644
--- a/src/core/operations/FlipImage.mjs
+++ b/src/core/operations/FlipImage.mjs
@@ -9,7 +9,7 @@ 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 from "jimp/es/index.js";
/**
* Flip Image operation
@@ -51,7 +51,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})`);
}
@@ -69,9 +69,9 @@ class FlipImage extends Operation {
let imageBuffer;
if (image.getMIME() === "image/gif") {
- imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
+ imageBuffer = await image.getBufferAsync(Jimp.MIME_PNG);
} else {
- imageBuffer = await image.getBufferAsync(jimp.AUTO);
+ imageBuffer = await image.getBufferAsync(Jimp.AUTO);
}
return imageBuffer.buffer;
} catch (err) {
diff --git a/src/core/operations/FromBase32.mjs b/src/core/operations/FromBase32.mjs
index 73added68..8ee0f1f87 100644
--- a/src/core/operations/FromBase32.mjs
+++ b/src/core/operations/FromBase32.mjs
@@ -6,6 +6,8 @@
import Operation from "../Operation.mjs";
import Utils from "../Utils.mjs";
+import {ALPHABET_OPTIONS} from "../lib/Base32.mjs";
+
/**
* From Base32 operation
@@ -27,8 +29,8 @@ class FromBase32 extends Operation {
this.args = [
{
name: "Alphabet",
- type: "binaryString",
- value: "A-Z2-7="
+ type: "editableOption",
+ value: ALPHABET_OPTIONS
},
{
name: "Remove non-alphabet chars",
@@ -41,6 +43,11 @@ class FromBase32 extends Operation {
pattern: "^(?:[A-Z2-7]{8})+(?:[A-Z2-7]{2}={6}|[A-Z2-7]{4}={4}|[A-Z2-7]{5}={3}|[A-Z2-7]{7}={1})?$",
flags: "",
args: ["A-Z2-7=", false]
+ },
+ {
+ pattern: "^(?:[0-9A-V]{8})+(?:[0-9A-V]{2}={6}|[0-9A-V]{4}={4}|[0-9A-V]{5}={3}|[0-9A-V]{7}={1})?$",
+ flags: "",
+ args: ["0-9A-V=", false]
}
];
}
@@ -96,3 +103,4 @@ class FromBase32 extends Operation {
}
export default FromBase32;
+
diff --git a/src/core/operations/FromBase58.mjs b/src/core/operations/FromBase58.mjs
index f5a9ac3de..cb4911595 100644
--- a/src/core/operations/FromBase58.mjs
+++ b/src/core/operations/FromBase58.mjs
@@ -60,7 +60,7 @@ class FromBase58 extends Operation {
run(input, args) {
let alphabet = args[0] || ALPHABET_OPTIONS[0].value;
const removeNonAlphaChars = args[1] === undefined ? true : args[1],
- result = [0];
+ result = [];
alphabet = Utils.expandAlphRange(alphabet).join("");
@@ -87,11 +87,9 @@ class FromBase58 extends Operation {
}
}
- let carry = result[0] * 58 + index;
- result[0] = carry & 0xFF;
- carry = carry >> 8;
+ let carry = index;
- for (let i = 1; i < result.length; i++) {
+ for (let i = 0; i < result.length; i++) {
carry += result[i] * 58;
result[i] = carry & 0xFF;
carry = carry >> 8;
diff --git a/src/core/operations/FromBase92.mjs b/src/core/operations/FromBase92.mjs
new file mode 100644
index 000000000..8315a51cf
--- /dev/null
+++ b/src/core/operations/FromBase92.mjs
@@ -0,0 +1,55 @@
+/**
+ * @author sg5506844 [sg5506844@gmail.com]
+ * @copyright Crown Copyright 2021
+ * @license Apache-2.0
+ */
+
+import { base92Ord } from "../lib/Base92.mjs";
+import Operation from "../Operation.mjs";
+
+/**
+ * From Base92 operation
+ */
+class FromBase92 extends Operation {
+ /**
+ * FromBase92 constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "From Base92";
+ this.module = "Default";
+ this.description = "Base92 is a notation for encoding arbitrary byte data using a restricted set of symbols that can be conveniently used by humans and processed by computers.";
+ this.infoURL = "https://wikipedia.org/wiki/List_of_numeral_systems";
+ this.inputType = "string";
+ this.outputType = "byteArray";
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {byteArray}
+ */
+ run(input, args) {
+ const res = [];
+ let bitString = "";
+
+ for (let i = 0; i < input.length; i += 2) {
+ if (i + 1 !== input.length) {
+ const x = base92Ord(input[i]) * 91 + base92Ord(input[i + 1]);
+ bitString += x.toString(2).padStart(13, "0");
+ } else {
+ const x = base92Ord(input[i]);
+ bitString += x.toString(2).padStart(6, "0");
+ }
+ while (bitString.length >= 8) {
+ res.push(parseInt(bitString.slice(0, 8), 2));
+ bitString = bitString.slice(8);
+ }
+ }
+
+ return res;
+ }
+}
+
+export default FromBase92;
diff --git a/src/core/operations/FromBech32.mjs b/src/core/operations/FromBech32.mjs
new file mode 100644
index 000000000..8a01d4db5
--- /dev/null
+++ b/src/core/operations/FromBech32.mjs
@@ -0,0 +1,149 @@
+/**
+ * @author Medjedtxm
+ * @copyright Crown Copyright 2025
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+import { decode } from "../lib/Bech32.mjs";
+import { toHex } from "../lib/Hex.mjs";
+
+/**
+ * From Bech32 operation
+ */
+class FromBech32 extends Operation {
+
+ /**
+ * FromBech32 constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "From Bech32";
+ this.module = "Default";
+ this.description = "Bech32 is an encoding scheme primarily used for Bitcoin SegWit addresses (BIP-0173). It uses a 32-character alphabet that excludes easily confused characters (1, b, i, o) and includes a checksum for error detection. Bech32m (BIP-0350) is an updated version used for Bitcoin Taproot addresses. Auto-detect will attempt Bech32 first, then Bech32m if the checksum fails. Output format options allow you to see the Human-Readable Part (HRP) along with the decoded data.";
+ this.infoURL = "https://wikipedia.org/wiki/Bech32";
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [
+ {
+ "name": "Encoding",
+ "type": "option",
+ "value": ["Auto-detect", "Bech32", "Bech32m"]
+ },
+ {
+ "name": "Output Format",
+ "type": "option",
+ "value": ["Raw", "Hex", "Bitcoin scriptPubKey", "HRP: Hex", "JSON"]
+ }
+ ];
+ this.checks = [
+ {
+ // Bitcoin mainnet SegWit/Taproot addresses
+ pattern: "^bc1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{6,87}$",
+ flags: "i",
+ args: ["Auto-detect", "Hex"]
+ },
+ {
+ // Bitcoin testnet addresses
+ pattern: "^tb1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{6,87}$",
+ flags: "i",
+ args: ["Auto-detect", "Hex"]
+ },
+ {
+ // AGE public keys
+ pattern: "^age1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{6,87}$",
+ flags: "i",
+ args: ["Auto-detect", "HRP: Hex"]
+ },
+ {
+ // AGE secret keys
+ pattern: "^AGE-SECRET-KEY-1[QPZRY9X8GF2TVDW0S3JN54KHCE6MUA7L]{6,87}$",
+ flags: "",
+ args: ["Auto-detect", "HRP: Hex"]
+ },
+ {
+ // Litecoin mainnet addresses
+ pattern: "^ltc1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{6,87}$",
+ flags: "i",
+ args: ["Auto-detect", "Hex"]
+ },
+ {
+ // Generic bech32 pattern
+ pattern: "^[a-z]{1,83}1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{6,}$",
+ flags: "i",
+ args: ["Auto-detect", "Hex"]
+ }
+ ];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ const encoding = args[0];
+ const outputFormat = args[1];
+
+ input = input.trim();
+
+ if (input.length === 0) {
+ return "";
+ }
+
+ const decoded = decode(input, encoding);
+
+ // Format output based on selected option
+ switch (outputFormat) {
+ case "Raw":
+ return decoded.data.map(b => String.fromCharCode(b)).join("");
+
+ case "Hex":
+ return toHex(decoded.data, "");
+
+ case "Bitcoin scriptPubKey": {
+ // Convert to Bitcoin scriptPubKey format as shown in BIP-0173/BIP-0350
+ // Format: [OP_version][length][witness_program]
+ // OP_0 = 0x00, OP_1-OP_16 = 0x51-0x60
+ if (decoded.witnessVersion === null || decoded.data.length < 2) {
+ // Not a SegWit address, fall back to hex
+ return toHex(decoded.data, "");
+ }
+ const witnessVersion = decoded.data[0];
+ const witnessProgram = decoded.data.slice(1);
+
+ // Convert witness version to OP code
+ let opCode;
+ if (witnessVersion === 0) {
+ opCode = 0x00; // OP_0
+ } else if (witnessVersion >= 1 && witnessVersion <= 16) {
+ opCode = 0x50 + witnessVersion; // OP_1 = 0x51, ..., OP_16 = 0x60
+ } else {
+ // Invalid witness version, fall back to hex
+ return toHex(decoded.data, "");
+ }
+
+ // Build scriptPubKey: [OP_version][length][program]
+ const scriptPubKey = [opCode, witnessProgram.length, ...witnessProgram];
+ return toHex(scriptPubKey, "");
+ }
+
+ case "HRP: Hex":
+ return `${decoded.hrp}: ${toHex(decoded.data, "")}`;
+
+ case "JSON":
+ return JSON.stringify({
+ hrp: decoded.hrp,
+ encoding: decoded.encoding,
+ data: toHex(decoded.data, "")
+ }, null, 2);
+
+ default:
+ return toHex(decoded.data, "");
+ }
+ }
+
+}
+
+export default FromBech32;
diff --git a/src/core/operations/FromFloat.mjs b/src/core/operations/FromFloat.mjs
new file mode 100644
index 000000000..8bf56d814
--- /dev/null
+++ b/src/core/operations/FromFloat.mjs
@@ -0,0 +1,78 @@
+/**
+ * @author tcode2k16 [tcode2k16@gmail.com]
+ * @copyright Crown Copyright 2019
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+import Utils from "../Utils.mjs";
+import ieee754 from "ieee754";
+import {DELIM_OPTIONS} from "../lib/Delim.mjs";
+
+/**
+ * From Float operation
+ */
+class FromFloat extends Operation {
+
+ /**
+ * FromFloat constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "From Float";
+ this.module = "Default";
+ this.description = "Convert from IEEE754 Floating Point Numbers";
+ this.infoURL = "https://wikipedia.org/wiki/IEEE_754";
+ this.inputType = "string";
+ this.outputType = "byteArray";
+ this.args = [
+ {
+ "name": "Endianness",
+ "type": "option",
+ "value": [
+ "Big Endian",
+ "Little Endian"
+ ]
+ },
+ {
+ "name": "Size",
+ "type": "option",
+ "value": [
+ "Float (4 bytes)",
+ "Double (8 bytes)"
+ ]
+ },
+ {
+ "name": "Delimiter",
+ "type": "option",
+ "value": DELIM_OPTIONS
+ }
+ ];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {byteArray}
+ */
+ run(input, args) {
+ if (input.length === 0) return [];
+
+ const [endianness, size, delimiterName] = args;
+ const delim = Utils.charRep(delimiterName || "Space");
+ const byteSize = size === "Double (8 bytes)" ? 8 : 4;
+ const isLE = endianness === "Little Endian";
+ const mLen = byteSize === 4 ? 23 : 52;
+ const floats = input.split(delim);
+
+ const output = new Array(floats.length*byteSize);
+ for (let i = 0; i < floats.length; i++) {
+ ieee754.write(output, parseFloat(floats[i]), i*byteSize, isLE, mLen, byteSize);
+ }
+ return output;
+ }
+
+}
+
+export default FromFloat;
diff --git a/src/core/operations/FromHexdump.mjs b/src/core/operations/FromHexdump.mjs
index e8c25441f..6fd3c1dc7 100644
--- a/src/core/operations/FromHexdump.mjs
+++ b/src/core/operations/FromHexdump.mjs
@@ -43,7 +43,7 @@ class FromHexdump extends Operation {
*/
run(input, args) {
const output = [],
- regex = /^\s*(?:[\dA-F]{4,16}h?:?)?[ \t]+((?:[\dA-F]{2} ){1,8}(?:[ \t]|[\dA-F]{2}-)(?:[\dA-F]{2} ){1,8}|(?:[\dA-F]{4} )*[\dA-F]{4}|(?:[\dA-F]{2} )*[\dA-F]{2})/igm;
+ regex = /^\s*(?:[\dA-F]{4,16}h?:?)?[ \t]+((?:[\dA-F]{2} ){1,8}(?:[ \t]|[\dA-F]{2}-)(?:[\dA-F]{2} ){1,8}|(?:[\dA-F]{4} )+(?:[\dA-F]{2})?|(?:[\dA-F]{2} )*[\dA-F]{2})/igm;
let block, line;
while ((block = regex.exec(input))) {
diff --git a/src/core/operations/FromModhex.mjs b/src/core/operations/FromModhex.mjs
new file mode 100644
index 000000000..029d95d8e
--- /dev/null
+++ b/src/core/operations/FromModhex.mjs
@@ -0,0 +1,84 @@
+/**
+ * @author linuxgemini [ilteris@asenkron.com.tr]
+ * @copyright Crown Copyright 2024
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+import { FROM_MODHEX_DELIM_OPTIONS, fromModhex } from "../lib/Modhex.mjs";
+
+/**
+ * From Modhex operation
+ */
+class FromModhex extends Operation {
+
+ /**
+ * FromModhex constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "From Modhex";
+ this.module = "Default";
+ this.description = "Converts a modhex byte string back into its raw value.";
+ this.infoURL = "https://en.wikipedia.org/wiki/YubiKey#ModHex";
+ this.inputType = "string";
+ this.outputType = "byteArray";
+ this.args = [
+ {
+ name: "Delimiter",
+ type: "option",
+ value: FROM_MODHEX_DELIM_OPTIONS
+ }
+ ];
+ this.checks = [
+ {
+ pattern: "^(?:[cbdefghijklnrtuv]{2})+$",
+ flags: "i",
+ args: ["None"]
+ },
+ {
+ pattern: "^[cbdefghijklnrtuv]{2}(?: [cbdefghijklnrtuv]{2})*$",
+ flags: "i",
+ args: ["Space"]
+ },
+ {
+ pattern: "^[cbdefghijklnrtuv]{2}(?:,[cbdefghijklnrtuv]{2})*$",
+ flags: "i",
+ args: ["Comma"]
+ },
+ {
+ pattern: "^[cbdefghijklnrtuv]{2}(?:;[cbdefghijklnrtuv]{2})*$",
+ flags: "i",
+ args: ["Semi-colon"]
+ },
+ {
+ pattern: "^[cbdefghijklnrtuv]{2}(?::[cbdefghijklnrtuv]{2})*$",
+ flags: "i",
+ args: ["Colon"]
+ },
+ {
+ pattern: "^[cbdefghijklnrtuv]{2}(?:\\n[cbdefghijklnrtuv]{2})*$",
+ flags: "i",
+ args: ["Line feed"]
+ },
+ {
+ pattern: "^[cbdefghijklnrtuv]{2}(?:\\r\\n[cbdefghijklnrtuv]{2})*$",
+ flags: "i",
+ args: ["CRLF"]
+ }
+ ];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {byteArray}
+ */
+ run(input, args) {
+ const delim = args[0] || "Auto";
+ return fromModhex(input, delim, 2);
+ }
+}
+
+export default FromModhex;
diff --git a/src/core/operations/GOSTDecrypt.mjs b/src/core/operations/GOSTDecrypt.mjs
new file mode 100644
index 000000000..2e7467dae
--- /dev/null
+++ b/src/core/operations/GOSTDecrypt.mjs
@@ -0,0 +1,151 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2023
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+import OperationError from "../errors/OperationError.mjs";
+import Utils from "../Utils.mjs";
+import { toHexFast, fromHex } from "../lib/Hex.mjs";
+import { CryptoGost, GostEngine } from "@wavesenterprise/crypto-gost-js/index.js";
+
+/**
+ * GOST Decrypt operation
+ */
+class GOSTDecrypt extends Operation {
+
+ /**
+ * GOSTDecrypt constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "GOST Decrypt";
+ this.module = "Ciphers";
+ this.description = "The GOST block cipher (Magma), defined in the standard GOST 28147-89 (RFC 5830), is a Soviet and Russian government standard symmetric key block cipher with a block size of 64 bits. The original standard, published in 1989, did not give the cipher any name, but the most recent revision of the standard, GOST R 34.12-2015 (RFC 7801, RFC 8891), specifies that it may be referred to as Magma. The GOST hash function is based on this cipher. The new standard also specifies a new 128-bit block cipher called Kuznyechik. Developed in the 1970s, the standard had been marked 'Top Secret' and then downgraded to 'Secret' in 1990. Shortly after the dissolution of the USSR, it was declassified and it was released to the public in 1994. GOST 28147 was a Soviet alternative to the United States standard algorithm, DES. Thus, the two are very similar in structure.";
+ this.infoURL = "https://wikipedia.org/wiki/GOST_(block_cipher)";
+ 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: "Input type",
+ type: "option",
+ value: ["Hex", "Raw"]
+ },
+ {
+ name: "Output type",
+ type: "option",
+ value: ["Raw", "Hex"]
+ },
+ {
+ name: "Algorithm",
+ type: "argSelector",
+ value: [
+ {
+ name: "GOST 28147 (1989)",
+ on: [5]
+ },
+ {
+ name: "GOST R 34.12 (Magma, 2015)",
+ off: [5]
+ },
+ {
+ name: "GOST R 34.12 (Kuznyechik, 2015)",
+ off: [5]
+ }
+ ]
+ },
+ {
+ name: "sBox",
+ type: "option",
+ value: ["E-TEST", "E-A", "E-B", "E-C", "E-D", "E-SC", "E-Z", "D-TEST", "D-A", "D-SC"]
+ },
+ {
+ name: "Block mode",
+ type: "option",
+ value: ["ECB", "CFB", "OFB", "CTR", "CBC"]
+ },
+ {
+ name: "Key meshing mode",
+ type: "option",
+ value: ["NO", "CP"]
+ },
+ {
+ name: "Padding",
+ type: "option",
+ value: ["NO", "PKCS5", "ZERO", "RANDOM", "BIT"]
+ }
+ ];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ async run(input, 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));
+
+ 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 = {
+ version: versionNum,
+ length: blockLength,
+ mode: "ES",
+ sBox: sBoxVal,
+ block: blockMode,
+ keyMeshing: keyMeshing,
+ padding: padding
+ };
+
+ try {
+ const Hex = CryptoGost.coding.Hex;
+ if (iv) algorithm.iv = Hex.decode(iv);
+
+ const cipher = GostEngine.getGostCipher(algorithm);
+ const out = Hex.encode(cipher.decrypt(Hex.decode(key), Hex.decode(input)));
+
+ return outputType === "Hex" ? out : Utils.byteArrayToChars(fromHex(out));
+ } catch (err) {
+ throw new OperationError(err);
+ }
+ }
+
+}
+
+export default GOSTDecrypt;
diff --git a/src/core/operations/GOSTEncrypt.mjs b/src/core/operations/GOSTEncrypt.mjs
new file mode 100644
index 000000000..82bdaeda0
--- /dev/null
+++ b/src/core/operations/GOSTEncrypt.mjs
@@ -0,0 +1,151 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2023
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+import OperationError from "../errors/OperationError.mjs";
+import Utils from "../Utils.mjs";
+import { toHexFast, fromHex } from "../lib/Hex.mjs";
+import { CryptoGost, GostEngine } from "@wavesenterprise/crypto-gost-js/index.js";
+
+/**
+ * GOST Encrypt operation
+ */
+class GOSTEncrypt extends Operation {
+
+ /**
+ * GOSTEncrypt constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "GOST Encrypt";
+ this.module = "Ciphers";
+ this.description = "The GOST block cipher (Magma), defined in the standard GOST 28147-89 (RFC 5830), is a Soviet and Russian government standard symmetric key block cipher with a block size of 64 bits. The original standard, published in 1989, did not give the cipher any name, but the most recent revision of the standard, GOST R 34.12-2015 (RFC 7801, RFC 8891), specifies that it may be referred to as Magma. The GOST hash function is based on this cipher. The new standard also specifies a new 128-bit block cipher called Kuznyechik. Developed in the 1970s, the standard had been marked 'Top Secret' and then downgraded to 'Secret' in 1990. Shortly after the dissolution of the USSR, it was declassified and it was released to the public in 1994. GOST 28147 was a Soviet alternative to the United States standard algorithm, DES. Thus, the two are very similar in structure.";
+ this.infoURL = "https://wikipedia.org/wiki/GOST_(block_cipher)";
+ 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: "Input type",
+ type: "option",
+ value: ["Raw", "Hex"]
+ },
+ {
+ name: "Output type",
+ type: "option",
+ value: ["Hex", "Raw"]
+ },
+ {
+ name: "Algorithm",
+ type: "argSelector",
+ value: [
+ {
+ name: "GOST 28147 (1989)",
+ on: [5]
+ },
+ {
+ name: "GOST R 34.12 (Magma, 2015)",
+ off: [5]
+ },
+ {
+ name: "GOST R 34.12 (Kuznyechik, 2015)",
+ off: [5]
+ }
+ ]
+ },
+ {
+ name: "sBox",
+ type: "option",
+ value: ["E-TEST", "E-A", "E-B", "E-C", "E-D", "E-SC", "E-Z", "D-TEST", "D-A", "D-SC"]
+ },
+ {
+ name: "Block mode",
+ type: "option",
+ value: ["ECB", "CFB", "OFB", "CTR", "CBC"]
+ },
+ {
+ name: "Key meshing mode",
+ type: "option",
+ value: ["NO", "CP"]
+ },
+ {
+ name: "Padding",
+ type: "option",
+ value: ["NO", "PKCS5", "ZERO", "RANDOM", "BIT"]
+ }
+ ];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ async run(input, 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));
+
+ 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 = {
+ version: versionNum,
+ length: blockLength,
+ mode: "ES",
+ sBox: sBoxVal,
+ block: blockMode,
+ keyMeshing: keyMeshing,
+ padding: padding
+ };
+
+ try {
+ const Hex = CryptoGost.coding.Hex;
+ if (iv) algorithm.iv = Hex.decode(iv);
+
+ const cipher = GostEngine.getGostCipher(algorithm);
+ const out = Hex.encode(cipher.encrypt(Hex.decode(key), Hex.decode(input)));
+
+ return outputType === "Hex" ? out : Utils.byteArrayToChars(fromHex(out));
+ } catch (err) {
+ throw new OperationError(err);
+ }
+ }
+
+}
+
+export default GOSTEncrypt;
diff --git a/src/core/operations/GOSTHash.mjs b/src/core/operations/GOSTHash.mjs
index d67a594cc..5c8cc6f75 100644
--- a/src/core/operations/GOSTHash.mjs
+++ b/src/core/operations/GOSTHash.mjs
@@ -7,7 +7,7 @@
import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs";
import GostDigest from "../vendor/gost/gostDigest.mjs";
-import {toHexFast} from "../lib/Hex.mjs";
+import { toHexFast } from "../lib/Hex.mjs";
/**
* GOST hash operation
@@ -20,7 +20,7 @@ class GOSTHash extends Operation {
constructor() {
super();
- this.name = "GOST hash";
+ this.name = "GOST Hash";
this.module = "Hashing";
this.description = "The GOST hash function, defined in the standards GOST R 34.11-94 and GOST 34.311-95 is a 256-bit cryptographic hash function. It was initially defined in the Russian national standard GOST R 34.11-94 Information Technology – Cryptographic Information Security – Hash Function . The equivalent standard used by other member-states of the CIS is GOST 34.311-95. This function must not be confused with a different Streebog hash function, which is defined in the new revision of the standard GOST R 34.11-2012. The GOST hash function is based on the GOST block cipher.";
this.infoURL = "https://wikipedia.org/wiki/GOST_(hash_function)";
@@ -28,20 +28,30 @@ class GOSTHash extends Operation {
this.outputType = "string";
this.args = [
{
- "name": "S-Box",
- "type": "option",
- "value": [
- "D-A",
- "D-SC",
- "E-TEST",
- "E-A",
- "E-B",
- "E-C",
- "E-D",
- "E-SC",
- "E-Z",
- "D-TEST"
+ name: "Algorithm",
+ type: "argSelector",
+ value: [
+ {
+ name: "GOST 28147 (1994)",
+ off: [1],
+ on: [2]
+ },
+ {
+ name: "GOST R 34.11 (Streebog, 2012)",
+ on: [1],
+ off: [2]
+ }
]
+ },
+ {
+ name: "Digest length",
+ type: "option",
+ value: ["256", "512"]
+ },
+ {
+ name: "sBox",
+ type: "option",
+ value: ["E-TEST", "E-A", "E-B", "E-C", "E-D", "E-SC", "E-Z", "D-TEST", "D-A", "D-SC"]
}
];
}
@@ -52,13 +62,23 @@ class GOSTHash extends Operation {
* @returns {string}
*/
run(input, args) {
+ const [version, length, sBox] = args;
+
+ const versionNum = version === "GOST 28147 (1994)" ? 1994 : 2012;
+ const algorithm = {
+ name: versionNum === 1994 ? "GOST 28147" : "GOST R 34.10",
+ version: versionNum,
+ mode: "HASH"
+ };
+
+ if (versionNum === 1994) {
+ algorithm.sBox = sBox;
+ } else {
+ algorithm.length = parseInt(length, 10);
+ }
+
try {
- const sBox = args[1];
- const gostDigest = new GostDigest({
- name: "GOST R 34.11",
- version: 1994,
- sBox: sBox
- });
+ const gostDigest = new GostDigest(algorithm);
return toHexFast(gostDigest.digest(input));
} catch (err) {
diff --git a/src/core/operations/GOSTKeyUnwrap.mjs b/src/core/operations/GOSTKeyUnwrap.mjs
new file mode 100644
index 000000000..52b0a9ecb
--- /dev/null
+++ b/src/core/operations/GOSTKeyUnwrap.mjs
@@ -0,0 +1,142 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2023
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+import OperationError from "../errors/OperationError.mjs";
+import Utils from "../Utils.mjs";
+import { toHexFast, fromHex } from "../lib/Hex.mjs";
+import { CryptoGost, GostEngine } from "@wavesenterprise/crypto-gost-js/index.js";
+
+/**
+ * GOST Key Unwrap operation
+ */
+class GOSTKeyUnwrap extends Operation {
+
+ /**
+ * GOSTKeyUnwrap constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "GOST Key Unwrap";
+ this.module = "Ciphers";
+ this.description = "A decryptor for keys wrapped using one of the GOST block ciphers.";
+ this.infoURL = "https://wikipedia.org/wiki/GOST_(block_cipher)";
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [
+ {
+ name: "Key",
+ type: "toggleString",
+ value: "",
+ toggleValues: ["Hex", "UTF8", "Latin1", "Base64"]
+ },
+ {
+ name: "User Key Material",
+ type: "toggleString",
+ value: "",
+ toggleValues: ["Hex", "UTF8", "Latin1", "Base64"]
+ },
+ {
+ name: "Input type",
+ type: "option",
+ value: ["Hex", "Raw"]
+ },
+ {
+ name: "Output type",
+ type: "option",
+ value: ["Raw", "Hex"]
+ },
+ {
+ name: "Algorithm",
+ type: "argSelector",
+ value: [
+ {
+ name: "GOST 28147 (1989)",
+ on: [5]
+ },
+ {
+ name: "GOST R 34.12 (Magma, 2015)",
+ off: [5]
+ },
+ {
+ name: "GOST R 34.12 (Kuznyechik, 2015)",
+ off: [5]
+ }
+ ]
+ },
+ {
+ name: "sBox",
+ type: "option",
+ value: ["E-TEST", "E-A", "E-B", "E-C", "E-D", "E-SC", "E-Z", "D-TEST", "D-A", "D-SC"]
+ },
+ {
+ name: "Key wrapping",
+ type: "option",
+ value: ["NO", "CP", "SC"]
+ }
+ ];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ async run(input, 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));
+
+ 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 = {
+ version: versionNum,
+ length: blockLength,
+ mode: "KW",
+ sBox: sBoxVal,
+ keyWrapping: keyWrapping
+ };
+
+ try {
+ const Hex = CryptoGost.coding.Hex;
+ algorithm.ukm = Hex.decode(ukm);
+
+ const cipher = GostEngine.getGostCipher(algorithm);
+ const out = Hex.encode(cipher.unwrapKey(Hex.decode(key), Hex.decode(input)));
+
+ return outputType === "Hex" ? out : Utils.byteArrayToChars(fromHex(out));
+ } catch (err) {
+ if (err.toString().includes("Invalid typed array length")) {
+ throw new OperationError("Incorrect input length. Must be a multiple of the block size.");
+ }
+ throw new OperationError(err);
+ }
+ }
+
+}
+
+export default GOSTKeyUnwrap;
diff --git a/src/core/operations/GOSTKeyWrap.mjs b/src/core/operations/GOSTKeyWrap.mjs
new file mode 100644
index 000000000..443c9ba04
--- /dev/null
+++ b/src/core/operations/GOSTKeyWrap.mjs
@@ -0,0 +1,142 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2023
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+import OperationError from "../errors/OperationError.mjs";
+import Utils from "../Utils.mjs";
+import { toHexFast, fromHex } from "../lib/Hex.mjs";
+import { CryptoGost, GostEngine } from "@wavesenterprise/crypto-gost-js/index.js";
+
+/**
+ * GOST Key Wrap operation
+ */
+class GOSTKeyWrap extends Operation {
+
+ /**
+ * GOSTKeyWrap constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "GOST Key Wrap";
+ this.module = "Ciphers";
+ this.description = "A key wrapping algorithm for protecting keys in untrusted storage using one of the GOST block cipers.";
+ this.infoURL = "https://wikipedia.org/wiki/GOST_(block_cipher)";
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [
+ {
+ name: "Key",
+ type: "toggleString",
+ value: "",
+ toggleValues: ["Hex", "UTF8", "Latin1", "Base64"]
+ },
+ {
+ name: "User Key Material",
+ type: "toggleString",
+ value: "",
+ toggleValues: ["Hex", "UTF8", "Latin1", "Base64"]
+ },
+ {
+ name: "Input type",
+ type: "option",
+ value: ["Raw", "Hex"]
+ },
+ {
+ name: "Output type",
+ type: "option",
+ value: ["Hex", "Raw"]
+ },
+ {
+ name: "Algorithm",
+ type: "argSelector",
+ value: [
+ {
+ name: "GOST 28147 (1989)",
+ on: [5]
+ },
+ {
+ name: "GOST R 34.12 (Magma, 2015)",
+ off: [5]
+ },
+ {
+ name: "GOST R 34.12 (Kuznyechik, 2015)",
+ off: [5]
+ }
+ ]
+ },
+ {
+ name: "sBox",
+ type: "option",
+ value: ["E-TEST", "E-A", "E-B", "E-C", "E-D", "E-SC", "E-Z", "D-TEST", "D-A", "D-SC"]
+ },
+ {
+ name: "Key wrapping",
+ type: "option",
+ value: ["NO", "CP", "SC"]
+ }
+ ];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ async run(input, 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));
+
+ 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 = {
+ version: versionNum,
+ length: blockLength,
+ mode: "KW",
+ sBox: sBoxVal,
+ keyWrapping: keyWrapping
+ };
+
+ try {
+ const Hex = CryptoGost.coding.Hex;
+ algorithm.ukm = Hex.decode(ukm);
+
+ const cipher = GostEngine.getGostCipher(algorithm);
+ const out = Hex.encode(cipher.wrapKey(Hex.decode(key), Hex.decode(input)));
+
+ return outputType === "Hex" ? out : Utils.byteArrayToChars(fromHex(out));
+ } catch (err) {
+ if (err.toString().includes("Invalid typed array length")) {
+ throw new OperationError("Incorrect input length. Must be a multiple of the block size.");
+ }
+ throw new OperationError(err);
+ }
+ }
+
+}
+
+export default GOSTKeyWrap;
diff --git a/src/core/operations/GOSTSign.mjs b/src/core/operations/GOSTSign.mjs
new file mode 100644
index 000000000..2af850e79
--- /dev/null
+++ b/src/core/operations/GOSTSign.mjs
@@ -0,0 +1,142 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2023
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+import OperationError from "../errors/OperationError.mjs";
+import Utils from "../Utils.mjs";
+import { toHexFast, fromHex } from "../lib/Hex.mjs";
+import { CryptoGost, GostEngine } from "@wavesenterprise/crypto-gost-js/index.js";
+
+/**
+ * GOST Sign operation
+ */
+class GOSTSign extends Operation {
+
+ /**
+ * GOSTSign constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "GOST Sign";
+ this.module = "Ciphers";
+ this.description = "Sign a plaintext message using one of the GOST block ciphers.";
+ this.infoURL = "https://wikipedia.org/wiki/GOST_(block_cipher)";
+ 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: "Input type",
+ type: "option",
+ value: ["Raw", "Hex"]
+ },
+ {
+ name: "Output type",
+ type: "option",
+ value: ["Hex", "Raw"]
+ },
+ {
+ name: "Algorithm",
+ type: "argSelector",
+ value: [
+ {
+ name: "GOST 28147 (1989)",
+ on: [5]
+ },
+ {
+ name: "GOST R 34.12 (Magma, 2015)",
+ off: [5]
+ },
+ {
+ name: "GOST R 34.12 (Kuznyechik, 2015)",
+ off: [5]
+ }
+ ]
+ },
+ {
+ name: "sBox",
+ type: "option",
+ value: ["E-TEST", "E-A", "E-B", "E-C", "E-D", "E-SC", "E-Z", "D-TEST", "D-A", "D-SC"]
+ },
+ {
+ name: "MAC length",
+ type: "number",
+ value: 32,
+ min: 8,
+ max: 64,
+ step: 8
+ }
+ ];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ async run(input, 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));
+
+ 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 = {
+ version: versionNum,
+ length: blockLength,
+ mode: "MAC",
+ sBox: sBoxVal,
+ macLength: macLength
+ };
+
+ try {
+ const Hex = CryptoGost.coding.Hex;
+ if (iv) algorithm.iv = Hex.decode(iv);
+
+ const cipher = GostEngine.getGostCipher(algorithm);
+ const out = Hex.encode(cipher.sign(Hex.decode(key), Hex.decode(input)));
+
+ return outputType === "Hex" ? out : Utils.byteArrayToChars(fromHex(out));
+ } catch (err) {
+ throw new OperationError(err);
+ }
+ }
+
+}
+
+export default GOSTSign;
diff --git a/src/core/operations/GOSTVerify.mjs b/src/core/operations/GOSTVerify.mjs
new file mode 100644
index 000000000..2fa362fba
--- /dev/null
+++ b/src/core/operations/GOSTVerify.mjs
@@ -0,0 +1,136 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2023
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+import OperationError from "../errors/OperationError.mjs";
+import Utils from "../Utils.mjs";
+import { toHexFast } from "../lib/Hex.mjs";
+import { CryptoGost, GostEngine } from "@wavesenterprise/crypto-gost-js/index.js";
+
+/**
+ * GOST Verify operation
+ */
+class GOSTVerify extends Operation {
+
+ /**
+ * GOSTVerify constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "GOST Verify";
+ this.module = "Ciphers";
+ this.description = "Verify the signature of a plaintext message using one of the GOST block ciphers. Enter the signature in the MAC field.";
+ this.infoURL = "https://wikipedia.org/wiki/GOST_(block_cipher)";
+ 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: "MAC",
+ type: "toggleString",
+ value: "",
+ toggleValues: ["Hex", "UTF8", "Latin1", "Base64"]
+ },
+ {
+ name: "Input type",
+ type: "option",
+ value: ["Raw", "Hex"]
+ },
+ {
+ name: "Algorithm",
+ type: "argSelector",
+ value: [
+ {
+ name: "GOST 28147 (1989)",
+ on: [5]
+ },
+ {
+ name: "GOST R 34.12 (Magma, 2015)",
+ off: [5]
+ },
+ {
+ name: "GOST R 34.12 (Kuznyechik, 2015)",
+ off: [5]
+ }
+ ]
+ },
+ {
+ name: "sBox",
+ type: "option",
+ value: ["E-TEST", "E-A", "E-B", "E-C", "E-D", "E-SC", "E-Z", "D-TEST", "D-A", "D-SC"]
+ }
+ ];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ async run(input, 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));
+
+ 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 = {
+ version: versionNum,
+ length: blockLength,
+ mode: "MAC",
+ sBox: sBoxVal,
+ macLength: mac.length * 4
+ };
+
+ try {
+ const Hex = CryptoGost.coding.Hex;
+ if (iv) algorithm.iv = Hex.decode(iv);
+
+ const cipher = GostEngine.getGostCipher(algorithm);
+ const out = cipher.verify(Hex.decode(key), Hex.decode(mac), Hex.decode(input));
+
+ return out ? "The signature matches" : "The signature does not match";
+ } catch (err) {
+ throw new OperationError(err);
+ }
+ }
+
+}
+
+export default GOSTVerify;
diff --git a/src/core/operations/GenerateAllChecksums.mjs b/src/core/operations/GenerateAllChecksums.mjs
new file mode 100644
index 000000000..b5a3d1525
--- /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 2a4a2b1a5..df09aa858 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";
@@ -108,22 +100,12 @@ class GenerateAllHashes extends Operation {
{name: "BLAKE2s-256", algo: (new BLAKE2s), inputType: "arrayBuffer", params: ["256", "Hex", {string: "", option: "UTF8"}]},
{name: "Streebog-256", algo: (new Streebog), inputType: "arrayBuffer", params: ["256"]},
{name: "Streebog-512", algo: (new Streebog), inputType: "arrayBuffer", params: ["512"]},
- {name: "GOST", algo: (new GOSTHash), inputType: "arrayBuffer", params: ["D-A"]},
+ {name: "GOST", algo: (new GOSTHash), inputType: "arrayBuffer", params: ["GOST 28147 (1994)", "256", "D-A"]},
{name: "LM Hash", algo: (new LMHash), inputType: "str", params: []},
{name: "NT Hash", algo: (new NTHash), inputType: "str", params: []},
{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 000000000..14714a02e
--- /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. ${cryptNotice}`;
+ this.infoURL = "https://wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm";
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [
+ {
+ name: "Elliptic Curve",
+ type: "option",
+ value: [
+ "P-256",
+ "P-384",
+ "P-521"
+ ]
+ },
+ {
+ name: "Output Format",
+ type: "option",
+ value: [
+ "PEM",
+ "DER",
+ "JWK"
+ ]
+ }
+ ];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ async run(input, args) {
+ const [curveName, outputFormat] = args;
+
+ return new Promise((resolve, reject) => {
+ let internalCurveName;
+ switch (curveName) {
+ case "P-256":
+ internalCurveName = "secp256r1";
+ break;
+ case "P-384":
+ internalCurveName = "secp384r1";
+ break;
+ case "P-521":
+ internalCurveName = "secp521r1";
+ break;
+ }
+ const keyPair = r.KEYUTIL.generateKeypair("EC", internalCurveName);
+
+ let pubKey;
+ let privKey;
+ let result;
+ switch (outputFormat) {
+ case "PEM":
+ pubKey = r.KEYUTIL.getPEM(keyPair.pubKeyObj).replace(/\r/g, "");
+ privKey = r.KEYUTIL.getPEM(keyPair.prvKeyObj, "PKCS8PRV").replace(/\r/g, "");
+ result = pubKey + "\n" + privKey;
+ break;
+ case "DER":
+ result = keyPair.prvKeyObj.prvKeyHex;
+ break;
+ case "JWK":
+ pubKey = r.KEYUTIL.getJWKFromKey(keyPair.pubKeyObj);
+ pubKey.key_ops = ["verify"]; // eslint-disable-line camelcase
+ pubKey.kid = "PublicKey";
+ privKey = r.KEYUTIL.getJWKFromKey(keyPair.prvKeyObj);
+ privKey.key_ops = ["sign"]; // eslint-disable-line camelcase
+ privKey.kid = "PrivateKey";
+ result = JSON.stringify({keys: [privKey, pubKey]}, null, 4);
+ break;
+ }
+
+ resolve(result);
+ });
+ }
+
+}
+
+export default GenerateECDSAKeyPair;
diff --git a/src/core/operations/GenerateHOTP.mjs b/src/core/operations/GenerateHOTP.mjs
index a800df6b8..75f5329f9 100644
--- a/src/core/operations/GenerateHOTP.mjs
+++ b/src/core/operations/GenerateHOTP.mjs
@@ -5,16 +5,14 @@
*/
import Operation from "../Operation.mjs";
-import otp from "otp";
-import ToBase32 from "./ToBase32.mjs";
+import * as OTPAuth from "otpauth";
/**
* Generate HOTP operation
*/
class GenerateHOTP extends Operation {
-
/**
- * GenerateHOTP constructor
+ *
*/
constructor() {
super();
@@ -31,11 +29,6 @@ class GenerateHOTP extends Operation {
"type": "string",
"value": ""
},
- {
- "name": "Key size",
- "type": "number",
- "value": 32
- },
{
"name": "Code length",
"type": "number",
@@ -50,21 +43,26 @@ class GenerateHOTP extends Operation {
}
/**
- * @param {ArrayBuffer} input
- * @param {Object[]} args
- * @returns {string}
+ *
*/
run(input, args) {
- const otpObj = otp({
- name: args[0],
- keySize: args[1],
- codeLength: args[2],
- secret: (new ToBase32).run(input, []).split("=")[0],
- });
- const counter = args[3];
- return `URI: ${otpObj.hotpURL}\n\nPassword: ${otpObj.hotp(counter)}`;
- }
+ const secretStr = new TextDecoder("utf-8").decode(input).trim();
+ const secret = secretStr ? secretStr.toUpperCase().replace(/\s+/g, "") : "";
+ const hotp = new OTPAuth.HOTP({
+ issuer: "",
+ label: args[0],
+ algorithm: "SHA1",
+ digits: args[1],
+ counter: args[2],
+ secret: OTPAuth.Secret.fromBase32(secret)
+ });
+
+ const uri = hotp.toString();
+ const code = hotp.generate();
+
+ return `URI: ${uri}\n\nPassword: ${code}`;
+ }
}
export default GenerateHOTP;
diff --git a/src/core/operations/GenerateImage.mjs b/src/core/operations/GenerateImage.mjs
index 66e762b5e..118e3a32b 100644
--- a/src/core/operations/GenerateImage.mjs
+++ b/src/core/operations/GenerateImage.mjs
@@ -10,7 +10,7 @@ import Utils from "../Utils.mjs";
import {isImage} from "../lib/FileType.mjs";
import {toBase64} from "../lib/Base64.mjs";
import {isWorkerEnvironment} from "../Utils.mjs";
-import jimp from "jimp";
+import Jimp from "jimp/es/index.js";
/**
* Generate Image operation
@@ -81,7 +81,7 @@ class GenerateImage extends Operation {
}
const height = Math.ceil(input.length / bytesPerPixel / width);
- const image = await new jimp(width, height, (err, image) => {});
+ const image = await new Jimp(width, height, (err, image) => {});
if (isWorkerEnvironment())
self.sendStatusMessage("Generating image from data...");
@@ -95,7 +95,7 @@ class GenerateImage extends Operation {
const y = Math.floor(index / width);
const value = curByte[k] === "0" ? 0xFF : 0x00;
- const pixel = jimp.rgbaToInt(value, value, value, 0xFF);
+ const pixel = Jimp.rgbaToInt(value, value, value, 0xFF);
image.setPixelColor(pixel, x, y);
}
}
@@ -139,7 +139,7 @@ class GenerateImage extends Operation {
}
try {
- const pixel = jimp.rgbaToInt(red, green, blue, alpha);
+ const pixel = Jimp.rgbaToInt(red, green, blue, alpha);
image.setPixelColor(pixel, x, y);
} catch (err) {
throw new OperationError(`Error while generating image from pixel values. (${err})`);
@@ -151,11 +151,11 @@ class GenerateImage extends Operation {
if (isWorkerEnvironment())
self.sendStatusMessage("Scaling image...");
- image.scaleToFit(width*scale, height*scale, jimp.RESIZE_NEAREST_NEIGHBOR);
+ image.scaleToFit(width*scale, height*scale, Jimp.RESIZE_NEAREST_NEIGHBOR);
}
try {
- const imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
+ const imageBuffer = await image.getBufferAsync(Jimp.MIME_PNG);
return imageBuffer.buffer;
} catch (err) {
throw new OperationError(`Error generating image. (${err})`);
diff --git a/src/core/operations/GenerateTOTP.mjs b/src/core/operations/GenerateTOTP.mjs
index 5c52721d9..187f8418f 100644
--- a/src/core/operations/GenerateTOTP.mjs
+++ b/src/core/operations/GenerateTOTP.mjs
@@ -5,20 +5,17 @@
*/
import Operation from "../Operation.mjs";
-import otp from "otp";
-import ToBase32 from "./ToBase32.mjs";
+import * as OTPAuth from "otpauth";
/**
* Generate TOTP operation
*/
class GenerateTOTP extends Operation {
-
/**
- * GenerateTOTP constructor
+ *
*/
constructor() {
super();
-
this.name = "Generate TOTP";
this.module = "Default";
this.description = "The Time-based One-Time Password algorithm (TOTP) is an algorithm that computes a one-time password from a shared secret key and the current time. It has been adopted as Internet Engineering Task Force standard RFC 6238, is the cornerstone of Initiative For Open Authentication (OAUTH), and is used in a number of two-factor authentication systems. A TOTP is an HOTP where the counter is the current time. Enter the secret as the input or leave it blank for a random secret to be generated. T0 and T1 are in seconds.";
@@ -31,11 +28,6 @@ class GenerateTOTP extends Operation {
"type": "string",
"value": ""
},
- {
- "name": "Key size",
- "type": "number",
- "value": 32
- },
{
"name": "Code length",
"type": "number",
@@ -55,22 +47,27 @@ class GenerateTOTP extends Operation {
}
/**
- * @param {ArrayBuffer} input
- * @param {Object[]} args
- * @returns {string}
+ *
*/
run(input, args) {
- const otpObj = otp({
- name: args[0],
- keySize: args[1],
- codeLength: args[2],
- secret: (new ToBase32).run(input, []).split("=")[0],
- epoch: args[3],
- timeSlice: args[4]
- });
- return `URI: ${otpObj.totpURL}\n\nPassword: ${otpObj.totp()}`;
- }
+ const secretStr = new TextDecoder("utf-8").decode(input).trim();
+ const secret = secretStr ? secretStr.toUpperCase().replace(/\s+/g, "") : "";
+ const totp = new OTPAuth.TOTP({
+ issuer: "",
+ label: args[0],
+ algorithm: "SHA1",
+ digits: args[1],
+ period: args[3],
+ epoch: args[2] * 1000, // Convert seconds to milliseconds
+ secret: OTPAuth.Secret.fromBase32(secret)
+ });
+
+ const uri = totp.toString();
+ const code = totp.generate();
+
+ return `URI: ${uri}\n\nPassword: ${code}`;
+ }
}
export default GenerateTOTP;
diff --git a/src/core/operations/GenerateUUID.mjs b/src/core/operations/GenerateUUID.mjs
index 1ee0faba6..21d063e3e 100644
--- a/src/core/operations/GenerateUUID.mjs
+++ b/src/core/operations/GenerateUUID.mjs
@@ -5,8 +5,8 @@
*/
import Operation from "../Operation.mjs";
-import crypto from "crypto";
-
+import * as uuid from "uuid";
+import OperationError from "../errors/OperationError.mjs";
/**
* Generate UUID operation
*/
@@ -20,11 +20,38 @@ class GenerateUUID extends Operation {
this.name = "Generate UUID";
this.module = "Crypto";
- this.description = "Generates an RFC 4122 version 4 compliant Universally Unique Identifier (UUID), also known as a Globally Unique Identifier (GUID). A version 4 UUID relies on random numbers, in this case generated using window.crypto if available and falling back to Math.random if not.";
+ this.description =
+ "Generates an RFC 9562 (formerly RFC 4122) compliant Universally Unique Identifier (UUID), " +
+ "also known as a Globally Unique Identifier (GUID). " +
+ " " +
+ "We currently support generating the following UUID versions: " +
+ "" +
+ "v1 : Timestamp-based " +
+ "v3 : Namespace w/ MD5 " +
+ "v4 : Random (default) " +
+ "v5 : Namespace w/ SHA-1 " +
+ "v6 : Timestamp, reordered " +
+ "v7 : Unix Epoch time-based " +
+ " " +
+ "UUIDs are generated using the uuid package. ";
this.infoURL = "https://wikipedia.org/wiki/Universally_unique_identifier";
this.inputType = "string";
this.outputType = "string";
- this.args = [];
+ this.args = [
+ {
+ name: "Version",
+ hint: "UUID version",
+ type: "option",
+ value: ["v1", "v3", "v4", "v5", "v6", "v7"],
+ defaultIndex: 2,
+ },
+ {
+ name: "Namespace",
+ hint: "UUID namespace (UUID; valid for v3 and v5)",
+ type: "string",
+ value: "1b671a64-40d5-491e-99b0-da01ff1f3341"
+ }
+ ];
}
/**
@@ -33,16 +60,17 @@ class GenerateUUID extends Operation {
* @returns {string}
*/
run(input, args) {
- const buf = new Uint32Array(4).map(() => {
- return crypto.randomBytes(4).readUInt32BE(0, true);
- });
- let i = 0;
- return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) {
- const r = (buf[i >> 3] >> ((i % 8) * 4)) & 0xf,
- v = c === "x" ? r : (r & 0x3 | 0x8);
- i++;
- return v.toString(16);
- });
+ const [version, namespace] = args;
+ const hasDesiredVersion = typeof uuid[version] === "function";
+ if (!hasDesiredVersion) throw new OperationError("Invalid UUID version");
+
+ const requiresNamespace = ["v3", "v5"].includes(version);
+ if (!requiresNamespace) return uuid[version]();
+
+ const hasValidNamespace = typeof namespace === "string" && uuid.validate(namespace);
+ if (!hasValidNamespace) throw new OperationError("Invalid UUID namespace");
+
+ return uuid[version](input, namespace);
}
}
diff --git a/src/core/operations/IPv6TransitionAddresses.mjs b/src/core/operations/IPv6TransitionAddresses.mjs
new file mode 100644
index 000000000..80d03fd0f
--- /dev/null
+++ b/src/core/operations/IPv6TransitionAddresses.mjs
@@ -0,0 +1,209 @@
+/**
+ * @author jb30795 [jb30795@proton.me]
+ * @copyright Crown Copyright 2024
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+
+/**
+ * IPv6 Transition Addresses operation
+ */
+class IPv6TransitionAddresses extends Operation {
+
+ /**
+ * IPv6TransitionAddresses constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "IPv6 Transition Addresses";
+ this.module = "Default";
+ this.description = "Converts IPv4 addresses to their IPv6 Transition addresses. IPv6 Transition addresses can also be converted back into their original IPv4 address. MAC addresses can also be converted into the EUI-64 format, this can them be appended to your IPv6 /64 range to obtain a full /128 address. Transition technologies enable translation between IPv4 and IPv6 addresses or tunneling to allow traffic to pass through the incompatible network, allowing the two standards to coexist. Only /24 ranges and currently handled. Remove headers to easily copy out results.";
+ this.infoURL = "https://wikipedia.org/wiki/IPv6_transition_mechanism";
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [
+ {
+ "name": "Ignore ranges",
+ "type": "boolean",
+ "value": true
+ },
+ {
+ "name": "Remove headers",
+ "type": "boolean",
+ "value": false
+ }
+ ];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ const XOR = {"0": "2", "1": "3", "2": "0", "3": "1", "4": "6", "5": "7", "6": "4", "7": "5", "8": "a", "9": "b", "a": "8", "b": "9", "c": "e", "d": "f", "e": "c", "f": "d"};
+
+ /**
+ * Function to convert to hex
+ */
+ function hexify(octet) {
+ return Number(octet).toString(16).padStart(2, "0");
+ }
+
+ /**
+ * Function to convert Hex to Int
+ */
+ function intify(hex) {
+ return parseInt(hex, 16);
+ }
+
+ /**
+ * Function converts IPv4 to IPv6 Transtion address
+ */
+ function ipTransition(input, range) {
+ let output = "";
+ const HEXIP = input.split(".");
+
+ /**
+ * 6to4
+ */
+ if (!args[1]) {
+ output += "6to4: ";
+ }
+ output += "2002:" + hexify(HEXIP[0]) + hexify(HEXIP[1]) + ":" + hexify(HEXIP[2]);
+ if (range) {
+ output += "00::/40\n";
+ } else {
+ output += hexify(HEXIP[3]) + "::/48\n";
+ }
+
+ /**
+ * Mapped
+ */
+ if (!args[1]) {
+ output += "IPv4 Mapped: ";
+ }
+ output += "::ffff:" + hexify(HEXIP[0]) + hexify(HEXIP[1]) + ":" + hexify(HEXIP[2]);
+ if (range) {
+ output += "00/120\n";
+ } else {
+ output += hexify(HEXIP[3]) + "\n";
+ }
+
+ /**
+ * Translated
+ */
+ if (!args[1]) {
+ output += "IPv4 Translated: ";
+ }
+ output += "::ffff:0:" + hexify(HEXIP[0]) + hexify(HEXIP[1]) + ":" + hexify(HEXIP[2]);
+ if (range) {
+ output += "00/120\n";
+ } else {
+ output += hexify(HEXIP[3]) + "\n";
+ }
+
+ /**
+ * Nat64
+ */
+ if (!args[1]) {
+ output += "Nat 64: ";
+ }
+ output += "64:ff9b::" + hexify(HEXIP[0]) + hexify(HEXIP[1]) + ":" + hexify(HEXIP[2]);
+ if (range) {
+ output += "00/120\n";
+ } else {
+ output += hexify(HEXIP[3]) + "\n";
+ }
+
+ return output;
+ }
+
+ /**
+ * Convert MAC to EUI-64
+ */
+ function macTransition(input) {
+ let output = "";
+ const MACPARTS = input.split(":");
+ if (!args[1]) {
+ output += "EUI-64 Interface ID: ";
+ }
+ const MAC = MACPARTS[0] + MACPARTS[1] + ":" + MACPARTS[2] + "ff:fe" + MACPARTS[3] + ":" + MACPARTS[4] + MACPARTS[5];
+ output += MAC.slice(0, 1) + XOR[MAC.slice(1, 2)] + MAC.slice(2);
+
+ return output;
+ }
+
+
+ /**
+ * Convert IPv6 address to its original IPv4 or MAC address
+ */
+ function unTransition(input) {
+ let output = "";
+ let hextets = "";
+
+ /**
+ * 6to4
+ */
+ if (input.startsWith("2002:")) {
+ if (!args[1]) {
+ output += "IPv4: ";
+ }
+ output += String(intify(input.slice(5, 7))) + "." + String(intify(input.slice(7, 9)))+ "." + String(intify(input.slice(10, 12)))+ "." + String(intify(input.slice(12, 14))) + "\n";
+ } else if (input.startsWith("::ffff:") || input.startsWith("0000:0000:0000:0000:0000:ffff:") || input.startsWith("::ffff:0000:") || input.startsWith("0000:0000:0000:0000:ffff:0000:") || input.startsWith("64:ff9b::") || input.startsWith("0064:ff9b:0000:0000:0000:0000:")) {
+ /**
+ * Mapped/Translated/Nat64
+ */
+ hextets = /:([0-9a-z]{1,4}):[0-9a-z]{1,4}$/.exec(input)[1].padStart(4, "0") + /:([0-9a-z]{1,4})$/.exec(input)[1].padStart(4, "0");
+ if (!args[1]) {
+ output += "IPv4: ";
+ }
+ output += intify(hextets.slice(-8, -7) + hextets.slice(-7, -6)) + "." +intify(hextets.slice(-6, -5) + hextets.slice(-5, -4)) + "." +intify(hextets.slice(-4, -3) + hextets.slice(-3, -2)) + "." +intify(hextets.slice(-2, -1) + hextets.slice(-1,)) + "\n";
+ } else if (input.slice(-12, -7).toUpperCase() === "FF:FE") {
+ /**
+ * EUI-64
+ */
+ if (!args[1]) {
+ output += "Mac Address: ";
+ }
+ const MAC = (input.slice(-19, -17) + ":" + input.slice(-17, -15) + ":" + input.slice(-14, -12) + ":" + input.slice(-7, -5) + ":" + input.slice(-4, -2) + ":" + input.slice(-2,)).toUpperCase();
+ output += MAC.slice(0, 1) + XOR[MAC.slice(1, 2)] + MAC.slice(2) + "\n";
+ }
+
+ return output;
+ }
+
+
+ /**
+ * Main
+ */
+ let output = "";
+ let inputs = input.split("\n");
+ // Remove blank rows
+ inputs = inputs.filter(Boolean);
+
+ for (let i = 0; i < inputs.length; i++) {
+ // if ignore ranges is checked and input is a range, skip
+ if ((args[0] && !inputs[i].includes("/")) || (!args[0])) {
+ if (/^[0-9]{1,3}(?:\.[0-9]{1,3}){3}$/.test(inputs[i])) {
+ output += ipTransition(inputs[i], false);
+ } else if (/\/24$/.test(inputs[i])) {
+ output += ipTransition(inputs[i], true);
+ } else if (/^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$/.test(inputs[i])) {
+ output += macTransition(inputs[i]);
+ } else if (/^((?:[0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,7}:|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?:(?::[0-9a-fA-F]{1,4}){1,6})|:(?:(?::[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(?::[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(?:ffff(?::0{1,4}){0,1}:){0,1}(?:(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])|(?:[0-9a-fA-F]{1,4}:){1,4}:(?:(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/.test(inputs[i])) {
+ output += unTransition(inputs[i]);
+ } else {
+ output = "Enter compressed or expanded IPv6 address, IPv4 address or MAC Address.";
+ }
+ }
+ }
+
+ return output;
+ }
+
+}
+
+export default IPv6TransitionAddresses;
diff --git a/src/core/operations/ImageBrightnessContrast.mjs b/src/core/operations/ImageBrightnessContrast.mjs
index 3845bebb7..bb0541aba 100644
--- a/src/core/operations/ImageBrightnessContrast.mjs
+++ b/src/core/operations/ImageBrightnessContrast.mjs
@@ -9,7 +9,7 @@ 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 from "jimp/es/index.js";
/**
* Image Brightness / Contrast operation
@@ -60,7 +60,7 @@ class ImageBrightnessContrast extends Operation {
let image;
try {
- image = await jimp.read(input);
+ image = await Jimp.read(input);
} catch (err) {
throw new OperationError(`Error loading image. (${err})`);
}
@@ -78,9 +78,9 @@ class ImageBrightnessContrast extends Operation {
let imageBuffer;
if (image.getMIME() === "image/gif") {
- imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
+ imageBuffer = await image.getBufferAsync(Jimp.MIME_PNG);
} else {
- imageBuffer = await image.getBufferAsync(jimp.AUTO);
+ imageBuffer = await image.getBufferAsync(Jimp.AUTO);
}
return imageBuffer.buffer;
} catch (err) {
diff --git a/src/core/operations/ImageFilter.mjs b/src/core/operations/ImageFilter.mjs
index 137dd0fc3..ed47bd2a0 100644
--- a/src/core/operations/ImageFilter.mjs
+++ b/src/core/operations/ImageFilter.mjs
@@ -9,7 +9,7 @@ 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 from "jimp/es/index.js";
/**
* Image Filter operation
@@ -54,7 +54,7 @@ class ImageFilter extends Operation {
let image;
try {
- image = await jimp.read(input);
+ image = await Jimp.read(input);
} catch (err) {
throw new OperationError(`Error loading image. (${err})`);
}
@@ -69,9 +69,9 @@ class ImageFilter extends Operation {
let imageBuffer;
if (image.getMIME() === "image/gif") {
- imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
+ imageBuffer = await image.getBufferAsync(Jimp.MIME_PNG);
} else {
- imageBuffer = await image.getBufferAsync(jimp.AUTO);
+ imageBuffer = await image.getBufferAsync(Jimp.AUTO);
}
return imageBuffer.buffer;
} catch (err) {
diff --git a/src/core/operations/ImageHueSaturationLightness.mjs b/src/core/operations/ImageHueSaturationLightness.mjs
index b0bad682e..a008c8f3e 100644
--- a/src/core/operations/ImageHueSaturationLightness.mjs
+++ b/src/core/operations/ImageHueSaturationLightness.mjs
@@ -9,7 +9,7 @@ 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 from "jimp/es/index.js";
/**
* Image Hue/Saturation/Lightness operation
@@ -68,7 +68,7 @@ class ImageHueSaturationLightness extends Operation {
let image;
try {
- image = await jimp.read(input);
+ image = await Jimp.read(input);
} catch (err) {
throw new OperationError(`Error loading image. (${err})`);
}
@@ -106,9 +106,9 @@ class ImageHueSaturationLightness extends Operation {
let imageBuffer;
if (image.getMIME() === "image/gif") {
- imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
+ imageBuffer = await image.getBufferAsync(Jimp.MIME_PNG);
} else {
- imageBuffer = await image.getBufferAsync(jimp.AUTO);
+ imageBuffer = await image.getBufferAsync(Jimp.AUTO);
}
return imageBuffer.buffer;
} catch (err) {
diff --git a/src/core/operations/ImageOpacity.mjs b/src/core/operations/ImageOpacity.mjs
index 622ee96cb..fc0a23e78 100644
--- a/src/core/operations/ImageOpacity.mjs
+++ b/src/core/operations/ImageOpacity.mjs
@@ -9,7 +9,7 @@ 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 from "jimp/es/index.js";
/**
* Image Opacity operation
@@ -53,7 +53,7 @@ class ImageOpacity extends Operation {
let image;
try {
- image = await jimp.read(input);
+ image = await Jimp.read(input);
} catch (err) {
throw new OperationError(`Error loading image. (${err})`);
}
@@ -64,9 +64,9 @@ class ImageOpacity extends Operation {
let imageBuffer;
if (image.getMIME() === "image/gif") {
- imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
+ imageBuffer = await image.getBufferAsync(Jimp.MIME_PNG);
} else {
- imageBuffer = await image.getBufferAsync(jimp.AUTO);
+ imageBuffer = await image.getBufferAsync(Jimp.AUTO);
}
return imageBuffer.buffer;
} catch (err) {
diff --git a/src/core/operations/InvertImage.mjs b/src/core/operations/InvertImage.mjs
index 6b62dea72..0036564f1 100644
--- a/src/core/operations/InvertImage.mjs
+++ b/src/core/operations/InvertImage.mjs
@@ -9,7 +9,7 @@ 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 from "jimp/es/index.js";
/**
* Invert Image operation
@@ -44,7 +44,7 @@ class InvertImage extends Operation {
let image;
try {
- image = await jimp.read(input);
+ image = await Jimp.read(input);
} catch (err) {
throw new OperationError(`Error loading image. (${err})`);
}
@@ -55,9 +55,9 @@ class InvertImage extends Operation {
let imageBuffer;
if (image.getMIME() === "image/gif") {
- imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
+ imageBuffer = await image.getBufferAsync(Jimp.MIME_PNG);
} else {
- imageBuffer = await image.getBufferAsync(jimp.AUTO);
+ imageBuffer = await image.getBufferAsync(Jimp.AUTO);
}
return imageBuffer.buffer;
} catch (err) {
diff --git a/src/core/operations/JA4Fingerprint.mjs b/src/core/operations/JA4Fingerprint.mjs
new file mode 100644
index 000000000..31f57525f
--- /dev/null
+++ b/src/core/operations/JA4Fingerprint.mjs
@@ -0,0 +1,73 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2024
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+import Utils from "../Utils.mjs";
+import {toJA4} from "../lib/JA4.mjs";
+
+/**
+ * JA4 Fingerprint operation
+ */
+class JA4Fingerprint extends Operation {
+
+ /**
+ * JA4Fingerprint constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "JA4 Fingerprint";
+ this.module = "Crypto";
+ this.description = "Generates a JA4 fingerprint to help identify TLS clients based on hashing together values from the Client Hello. Input: A hex stream of the TLS or QUIC Client Hello packet application layer.";
+ this.infoURL = "https://medium.com/foxio/ja4-network-fingerprinting-9376fe9ca637";
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [
+ {
+ name: "Input format",
+ type: "option",
+ value: ["Hex", "Base64", "Raw"]
+ },
+ {
+ name: "Output format",
+ type: "option",
+ value: ["JA4", "JA4 Original Rendering", "JA4 Raw", "JA4 Raw Original Rendering", "All"]
+ }
+ ];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ const [inputFormat, outputFormat] = args;
+ input = Utils.convertToByteArray(input, inputFormat);
+ const ja4 = toJA4(new Uint8Array(input));
+
+ // Output
+ switch (outputFormat) {
+ case "JA4":
+ return ja4.JA4;
+ case "JA4 Original Rendering":
+ return ja4.JA4_o;
+ case "JA4 Raw":
+ return ja4.JA4_r;
+ case "JA4 Raw Original Rendering":
+ return ja4.JA4_ro;
+ case "All":
+ default:
+ return `JA4: ${ja4.JA4}
+JA4_o: ${ja4.JA4_o}
+JA4_r: ${ja4.JA4_r}
+JA4_ro: ${ja4.JA4_ro}`;
+ }
+ }
+
+}
+
+export default JA4Fingerprint;
diff --git a/src/core/operations/JA4ServerFingerprint.mjs b/src/core/operations/JA4ServerFingerprint.mjs
new file mode 100644
index 000000000..662285a8f
--- /dev/null
+++ b/src/core/operations/JA4ServerFingerprint.mjs
@@ -0,0 +1,66 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2024
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+import Utils from "../Utils.mjs";
+import {toJA4S} from "../lib/JA4.mjs";
+
+/**
+ * JA4Server Fingerprint operation
+ */
+class JA4ServerFingerprint extends Operation {
+
+ /**
+ * JA4ServerFingerprint constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "JA4Server Fingerprint";
+ this.module = "Crypto";
+ this.description = "Generates a JA4Server Fingerprint (JA4S) to help identify TLS servers or sessions based on hashing together values from the Server Hello. Input: A hex stream of the TLS or QUIC Server Hello packet application layer.";
+ this.infoURL = "https://medium.com/foxio/ja4-network-fingerprinting-9376fe9ca637";
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [
+ {
+ name: "Input format",
+ type: "option",
+ value: ["Hex", "Base64", "Raw"]
+ },
+ {
+ name: "Output format",
+ type: "option",
+ value: ["JA4S", "JA4S Raw", "Both"]
+ }
+ ];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ const [inputFormat, outputFormat] = args;
+ input = Utils.convertToByteArray(input, inputFormat);
+ const ja4s = toJA4S(new Uint8Array(input));
+
+ // Output
+ switch (outputFormat) {
+ case "JA4S":
+ return ja4s.JA4S;
+ case "JA4S Raw":
+ return ja4s.JA4S_r;
+ case "Both":
+ default:
+ return `JA4S: ${ja4s.JA4S}\nJA4S_r: ${ja4s.JA4S_r}`;
+ }
+ }
+
+}
+
+export default JA4ServerFingerprint;
diff --git a/src/core/operations/JPathExpression.mjs b/src/core/operations/JPathExpression.mjs
index 73a27433e..c7f515d5d 100644
--- a/src/core/operations/JPathExpression.mjs
+++ b/src/core/operations/JPathExpression.mjs
@@ -35,12 +35,6 @@ class JPathExpression extends Operation {
name: "Result delimiter",
type: "binaryShortString",
value: "\\n"
- },
- {
- name: "Prevent eval",
- type: "boolean",
- value: true,
- description: "Evaluated expressions are disabled by default for security reasons"
}
];
}
@@ -51,7 +45,7 @@ class JPathExpression extends Operation {
* @returns {string}
*/
run(input, args) {
- const [query, delimiter, preventEval] = args;
+ const [query, delimiter] = args;
let results, jsonObj;
try {
@@ -63,8 +57,7 @@ class JPathExpression extends Operation {
try {
results = JSONPath({
path: query,
- json: jsonObj,
- preventEval: preventEval
+ json: jsonObj
});
} catch (err) {
throw new OperationError(`Invalid JPath expression: ${err.message}`);
diff --git a/src/core/operations/JSONtoYAML.mjs b/src/core/operations/JSONtoYAML.mjs
new file mode 100644
index 000000000..0d4cc626b
--- /dev/null
+++ b/src/core/operations/JSONtoYAML.mjs
@@ -0,0 +1,46 @@
+/**
+ * @author ccarpo [ccarpo@gmx.net]
+ * @copyright Crown Copyright 2021
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+import OperationError from "../errors/OperationError.mjs";
+import YAML from "yaml";
+
+/**
+ * JSON to YAML operation
+ */
+class JSONtoYAML extends Operation {
+
+ /**
+ * JSONtoYAML constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "JSON to YAML";
+ this.module = "Default";
+ this.description = "Format a JSON object into YAML";
+ this.infoURL = "https://en.wikipedia.org/wiki/YAML";
+ this.inputType = "JSON";
+ this.outputType = "string";
+ this.args = [];
+ }
+
+ /**
+ * @param {JSON} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ try {
+ return YAML.stringify(input);
+ } catch (err) {
+ throw new OperationError("Test");
+ }
+ }
+
+}
+
+export default JSONtoYAML;
diff --git a/src/core/operations/JWKToPem.mjs b/src/core/operations/JWKToPem.mjs
new file mode 100644
index 000000000..c8c002702
--- /dev/null
+++ b/src/core/operations/JWKToPem.mjs
@@ -0,0 +1,80 @@
+/**
+ * @author cplussharp
+ * @copyright Crown Copyright 2021
+ * @license Apache-2.0
+ */
+
+import r from "jsrsasign";
+import Operation from "../Operation.mjs";
+import OperationError from "../errors/OperationError.mjs";
+
+/**
+ * PEM to JWK operation
+ */
+class PEMToJWK extends Operation {
+
+ /**
+ * PEMToJWK constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "JWK to PEM";
+ this.module = "PublicKey";
+ this.description = "Converts Keys in JSON Web Key format to PEM format (PKCS#8).";
+ this.infoURL = "https://datatracker.ietf.org/doc/html/rfc7517";
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [];
+ this.checks = [
+ {
+ "pattern": "\"kty\":\\s*\"(EC|RSA)\"",
+ "flags": "gm",
+ "args": []
+ }
+ ];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ const inputJson = JSON.parse(input);
+
+ let keys = [];
+ if (Array.isArray(inputJson)) {
+ // list of keys => transform all keys
+ keys = inputJson;
+ } else if (Array.isArray(inputJson.keys)) {
+ // JSON Web Key Set => transform all keys
+ keys = inputJson.keys;
+ } else if (typeof inputJson === "object") {
+ // single key
+ keys.push(inputJson);
+ } else {
+ throw new OperationError("Input is not a JSON Web Key");
+ }
+
+ let output = "";
+ for (let i=0; i {
- // Convert element to integer.
- let temp = parseInt(elem, 10);
+ // Convert element to an integer based on the provided radix.
+ let temp = parseInt(elem, radix);
- // If element is not an integer.
- if (isNaN(temp))
- throw new OperationError("Character: " + elem + " is not a digit.");
+ // If element is not a valid number in the given radix.
+ if (isNaN(temp)) {
+ throw new Error("Character: " + elem + " is not valid in radix " + radix + ".");
+ }
// If element is in an even position
if (even) {
- // Double the element and add the quotient and remainder together.
- temp = 2 * elem;
- temp = Math.floor(temp/10) + (temp % 10);
+ // Double the element and sum the quotient and remainder.
+ temp = 2 * temp;
+ temp = Math.floor(temp / radix) + (temp % radix);
}
even = !even;
return acc + temp;
- }, 0) % 10;
+ }, 0) % radix; // Use radix as the modulus base
}
/**
@@ -63,9 +71,20 @@ class LuhnChecksum extends Operation {
run(input, args) {
if (!input) return "";
- const checkSum = this.checksum(input);
- let checkDigit = this.checksum(input + "0");
- checkDigit = checkDigit === 0 ? 0 : (10-checkDigit);
+ const radix = args[0];
+
+ if (radix < 2 || radix > 36) {
+ throw new OperationError("Error: Radix argument must be between 2 and 36");
+ }
+
+ if (radix % 2 !== 0) {
+ throw new OperationError("Error: Radix argument must be divisible by 2");
+ }
+
+ const checkSum = this.checksum(input, radix).toString(radix);
+ let checkDigit = this.checksum(input + "0", radix);
+ checkDigit = checkDigit === 0 ? 0 : (radix - checkDigit);
+ checkDigit = checkDigit.toString(radix);
return `Checksum: ${checkSum}
Checkdigit: ${checkDigit}
diff --git a/src/core/operations/MIMEDecoding.mjs b/src/core/operations/MIMEDecoding.mjs
new file mode 100644
index 000000000..7b52fbdd2
--- /dev/null
+++ b/src/core/operations/MIMEDecoding.mjs
@@ -0,0 +1,171 @@
+/**
+ * @author mshwed [m@ttshwed.com]
+ * @copyright Crown Copyright 2019
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+import OperationError from "../errors/OperationError.mjs";
+import Utils from "../Utils.mjs";
+import { fromHex } from "../lib/Hex.mjs";
+import { fromBase64 } from "../lib/Base64.mjs";
+import cptable from "codepage";
+
+/**
+ * MIME Decoding operation
+ */
+class MIMEDecoding extends Operation {
+
+ /**
+ * MIMEDecoding constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "MIME Decoding";
+ this.module = "Default";
+ this.description = "Enables the decoding of MIME message header extensions for non-ASCII text";
+ this.infoURL = "https://tools.ietf.org/html/rfc2047";
+ this.inputType = "byteArray";
+ this.outputType = "string";
+ this.args = [];
+ }
+
+ /**
+ * @param {byteArray} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ const mimeEncodedText = Utils.byteArrayToUtf8(input);
+ const encodedHeaders = mimeEncodedText.replace(/\r\n/g, "\n");
+
+ const decodedHeader = this.decodeHeaders(encodedHeaders);
+
+ return decodedHeader;
+ }
+
+ /**
+ * Decode MIME header strings
+ *
+ * @param headerString
+ */
+ decodeHeaders(headerString) {
+ // No encoded words detected
+ let i = headerString.indexOf("=?");
+ if (i === -1) return headerString;
+
+ let decodedHeaders = headerString.slice(0, i);
+ let header = headerString.slice(i);
+
+ let isBetweenWords = false;
+ let start, cur, charset, encoding, j, end, text;
+ while (header.length > -1) {
+ start = header.indexOf("=?");
+ if (start === -1) break;
+ cur = start + "=?".length;
+
+ i = header.slice(cur).indexOf("?");
+ if (i === -1) break;
+
+ charset = header.slice(cur, cur + i);
+ cur += i + "?".length;
+
+ if (header.length < cur + "Q??=".length) break;
+
+ encoding = header[cur];
+ cur += 1;
+
+ if (header[cur] !== "?") break;
+
+ cur += 1;
+
+ j = header.slice(cur).indexOf("?=");
+ if (j === -1) break;
+
+ text = header.slice(cur, cur + j);
+ end = cur + j + "?=".length;
+
+ if (encoding.toLowerCase() === "b") {
+ text = fromBase64(text);
+ } else if (encoding.toLowerCase() === "q") {
+ text = this.parseQEncodedWord(text);
+ } else {
+ isBetweenWords = false;
+ decodedHeaders += header.slice(0, start + 2);
+ header = header.slice(start + 2);
+ }
+
+ if (start > 0 && (!isBetweenWords || header.slice(0, start).search(/\S/g) > -1)) {
+ decodedHeaders += header.slice(0, start);
+ }
+
+ decodedHeaders += this.convertFromCharset(charset, text);
+
+ header = header.slice(end);
+ isBetweenWords = true;
+ }
+
+ if (header.length > 0) {
+ decodedHeaders += header;
+ }
+
+ return decodedHeaders;
+ }
+
+ /**
+ * Converts decoded text for supported charsets.
+ * Supports UTF-8, US-ASCII, ISO-8859-*
+ *
+ * @param encodedWord
+ */
+ convertFromCharset(charset, encodedText) {
+ charset = charset.toLowerCase();
+ const parsedCharset = charset.split("-");
+
+ if (parsedCharset.length === 2 && parsedCharset[0] === "utf" && charset === "utf-8") {
+ return cptable.utils.decode(65001, encodedText);
+ } else if (parsedCharset.length === 2 && charset === "us-ascii") {
+ return cptable.utils.decode(20127, encodedText);
+ } else if (parsedCharset.length === 3 && parsedCharset[0] === "iso" && parsedCharset[1] === "8859") {
+ const isoCharset = parseInt(parsedCharset[2], 10);
+ if (isoCharset >= 1 && isoCharset <= 16) {
+ return cptable.utils.decode(28590 + isoCharset, encodedText);
+ }
+ }
+
+ throw new OperationError("Unhandled Charset");
+ }
+
+ /**
+ * Parses a Q encoded word
+ *
+ * @param encodedWord
+ */
+ parseQEncodedWord(encodedWord) {
+ let decodedWord = "";
+ for (let i = 0; i < encodedWord.length; i++) {
+ if (encodedWord[i] === "_") {
+ decodedWord += " ";
+ // Parse hex encoding
+ } else if (encodedWord[i] === "=") {
+ if ((i + 2) >= encodedWord.length) throw new OperationError("Incorrectly Encoded Word");
+ const decodedHex = Utils.byteArrayToChars(fromHex(encodedWord.substring(i + 1, i + 3)));
+ decodedWord += decodedHex;
+ i += 2;
+ } else if (
+ (encodedWord[i].charCodeAt(0) >= " ".charCodeAt(0) && encodedWord[i].charCodeAt(0) <= "~".charCodeAt(0)) ||
+ encodedWord[i] === "\n" ||
+ encodedWord[i] === "\r" ||
+ encodedWord[i] === "\t") {
+ decodedWord += encodedWord[i];
+ } else {
+ throw new OperationError("Incorrectly Encoded Word");
+ }
+ }
+
+ return decodedWord;
+ }
+}
+
+export default MIMEDecoding;
diff --git a/src/core/operations/MurmurHash3.mjs b/src/core/operations/MurmurHash3.mjs
new file mode 100644
index 000000000..7921633f3
--- /dev/null
+++ b/src/core/operations/MurmurHash3.mjs
@@ -0,0 +1,139 @@
+/**
+ * Based on murmurhash-js (https://github.com/garycourt/murmurhash-js)
+ * @author Gary Court
+ * @license MIT
+ *
+ * @author AliceGrey [alice@grey.systems]
+ * @copyright Crown Copyright 2024
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+
+/**
+ * MurmurHash3 operation
+ */
+class MurmurHash3 extends Operation {
+
+ /**
+ * MurmurHash3 constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "MurmurHash3";
+ this.module = "Hashing";
+ this.description = "Generates a MurmurHash v3 for a string input and an optional seed input";
+ this.infoURL = "https://wikipedia.org/wiki/MurmurHash";
+ this.inputType = "string";
+ this.outputType = "number";
+ this.args = [
+ {
+ name: "Seed",
+ type: "number",
+ value: 0
+ },
+ {
+ name: "Convert to Signed",
+ type: "boolean",
+ value: false
+ }
+ ];
+ }
+
+ /**
+ * Calculates the MurmurHash3 hash of the input.
+ * Based on Gary Court's JS MurmurHash implementation
+ * @see http://github.com/garycourt/murmurhash-js
+ * @author AliceGrey [alice@grey.systems]
+ * @param {string} input ASCII only
+ * @param {number} seed Positive integer only
+ * @return {number} 32-bit positive integer hash
+ */
+ mmh3(input, seed) {
+ let h1b;
+ let k1;
+ const remainder = input.length & 3; // input.length % 4
+ const bytes = input.length - remainder;
+ let h1 = seed;
+ const c1 = 0xcc9e2d51;
+ const c2 = 0x1b873593;
+ let i = 0;
+
+ while (i < bytes) {
+ k1 =
+ ((input.charCodeAt(i) & 0xff)) |
+ ((input.charCodeAt(++i) & 0xff) << 8) |
+ ((input.charCodeAt(++i) & 0xff) << 16) |
+ ((input.charCodeAt(++i) & 0xff) << 24);
+ ++i;
+
+ k1 = ((((k1 & 0xffff) * c1) + ((((k1 >>> 16) * c1) & 0xffff) << 16))) & 0xffffffff;
+ k1 = (k1 << 15) | (k1 >>> 17);
+ k1 = ((((k1 & 0xffff) * c2) + ((((k1 >>> 16) * c2) & 0xffff) << 16))) & 0xffffffff;
+
+ h1 ^= k1;
+ h1 = (h1 << 13) | (h1 >>> 19);
+ h1b = ((((h1 & 0xffff) * 5) + ((((h1 >>> 16) * 5) & 0xffff) << 16))) & 0xffffffff;
+ h1 = (((h1b & 0xffff) + 0x6b64) + ((((h1b >>> 16) + 0xe654) & 0xffff) << 16));
+ }
+
+ k1 = 0;
+
+ if (remainder === 3) {
+ k1 ^= (input.charCodeAt(i + 2) & 0xff) << 16;
+ }
+
+ if (remainder === 3 || remainder === 2) {
+ k1 ^= (input.charCodeAt(i + 1) & 0xff) << 8;
+ }
+
+ if (remainder === 3 || remainder === 2 || remainder === 1) {
+ k1 ^= (input.charCodeAt(i) & 0xff);
+
+ k1 = (((k1 & 0xffff) * c1) + ((((k1 >>> 16) * c1) & 0xffff) << 16)) & 0xffffffff;
+ k1 = (k1 << 15) | (k1 >>> 17);
+ k1 = (((k1 & 0xffff) * c2) + ((((k1 >>> 16) * c2) & 0xffff) << 16)) & 0xffffffff;
+ h1 ^= k1;
+ }
+
+ h1 ^= input.length;
+
+ h1 ^= h1 >>> 16;
+ h1 = (((h1 & 0xffff) * 0x85ebca6b) + ((((h1 >>> 16) * 0x85ebca6b) & 0xffff) << 16)) & 0xffffffff;
+ h1 ^= h1 >>> 13;
+ h1 = ((((h1 & 0xffff) * 0xc2b2ae35) + ((((h1 >>> 16) * 0xc2b2ae35) & 0xffff) << 16))) & 0xffffffff;
+ h1 ^= h1 >>> 16;
+
+ return h1 >>> 0;
+ }
+
+ /**
+ * Converts an unsigned 32-bit integer to a signed 32-bit integer
+ * @author AliceGrey [alice@grey.systems]
+ * @param {value} 32-bit unsigned integer
+ * @return {number} 32-bit signed integer
+ */
+ unsignedToSigned(value) {
+ return value & 0x80000000 ? -0x100000000 + value : value;
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {number}
+ */
+ run(input, args) {
+ if (args && args.length >= 1) {
+ const seed = args[0];
+ const hash = this.mmh3(input, seed);
+ if (args.length > 1 && args[1]) {
+ return this.unsignedToSigned(hash);
+ }
+ return hash;
+ }
+ return this.mmh3(input);
+ }
+}
+
+export default MurmurHash3;
diff --git a/src/core/operations/NormaliseImage.mjs b/src/core/operations/NormaliseImage.mjs
index 08de9ed5b..e67570bc1 100644
--- a/src/core/operations/NormaliseImage.mjs
+++ b/src/core/operations/NormaliseImage.mjs
@@ -8,7 +8,7 @@ import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs";
import { isImage } from "../lib/FileType.mjs";
import { toBase64 } from "../lib/Base64.mjs";
-import jimp from "jimp";
+import Jimp from "jimp/es/index.js";
/**
* Normalise Image operation
@@ -43,7 +43,7 @@ class NormaliseImage extends Operation {
let image;
try {
- image = await jimp.read(input);
+ image = await Jimp.read(input);
} catch (err) {
throw new OperationError(`Error opening image file. (${err})`);
}
@@ -53,9 +53,9 @@ class NormaliseImage extends Operation {
let imageBuffer;
if (image.getMIME() === "image/gif") {
- imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
+ imageBuffer = await image.getBufferAsync(Jimp.MIME_PNG);
} else {
- imageBuffer = await image.getBufferAsync(jimp.AUTO);
+ imageBuffer = await image.getBufferAsync(Jimp.AUTO);
}
return imageBuffer.buffer;
} catch (err) {
diff --git a/src/core/operations/OpticalCharacterRecognition.mjs b/src/core/operations/OpticalCharacterRecognition.mjs
index 6262df7b4..dfcff9654 100644
--- a/src/core/operations/OpticalCharacterRecognition.mjs
+++ b/src/core/operations/OpticalCharacterRecognition.mjs
@@ -12,9 +12,10 @@ import { isImage } from "../lib/FileType.mjs";
import { toBase64 } from "../lib/Base64.mjs";
import { isWorkerEnvironment } from "../Utils.mjs";
-import process from "process";
import { createWorker } from "tesseract.js";
+const OEM_MODES = ["Tesseract only", "LSTM only", "Tesseract/LSTM Combined"];
+
/**
* Optical Character Recognition operation
*/
@@ -37,6 +38,12 @@ class OpticalCharacterRecognition extends Operation {
name: "Show confidence",
type: "boolean",
value: true
+ },
+ {
+ name: "OCR Engine Mode",
+ type: "option",
+ value: OEM_MODES,
+ defaultIndex: 1
}
];
}
@@ -47,7 +54,7 @@ class OpticalCharacterRecognition extends Operation {
* @returns {string}
*/
async run(input, args) {
- const [showConfidence] = args;
+ const [showConfidence, oemChoice] = args;
if (!isWorkerEnvironment()) throw new OperationError("This operation only works in a browser");
@@ -56,12 +63,13 @@ class OpticalCharacterRecognition extends Operation {
throw new OperationError("Unsupported file type (supported: jpg,png,pbm,bmp) or no file provided");
}
- const assetDir = isWorkerEnvironment() ? `${self.docURL}/assets/` : `${process.cwd()}/src/core/vendor/`;
+ const assetDir = `${self.docURL}/assets/`;
+ const oem = OEM_MODES.indexOf(oemChoice);
try {
self.sendStatusMessage("Spinning up Tesseract worker...");
const image = `data:${type};base64,${toBase64(input)}`;
- const worker = createWorker({
+ const worker = await createWorker("eng", oem, {
workerPath: `${assetDir}tesseract/worker.min.js`,
langPath: `${assetDir}tesseract/lang-data`,
corePath: `${assetDir}tesseract/tesseract-core.wasm.js`,
@@ -71,11 +79,6 @@ class OpticalCharacterRecognition extends Operation {
}
}
});
- await worker.load();
- self.sendStatusMessage(`Loading English language pack...`);
- await worker.loadLanguage("eng");
- self.sendStatusMessage("Intialising Tesseract API...");
- await worker.initialize("eng");
self.sendStatusMessage("Finding text...");
const result = await worker.recognize(image);
diff --git a/src/core/operations/PEMToJWK.mjs b/src/core/operations/PEMToJWK.mjs
new file mode 100644
index 000000000..61fde3713
--- /dev/null
+++ b/src/core/operations/PEMToJWK.mjs
@@ -0,0 +1,88 @@
+/**
+ * @author cplussharp
+ * @copyright Crown Copyright 2021
+ * @license Apache-2.0
+ */
+
+import r from "jsrsasign";
+import Operation from "../Operation.mjs";
+import OperationError from "../errors/OperationError.mjs";
+
+/**
+ * PEM to JWK operation
+ */
+class PEMToJWK extends Operation {
+
+ /**
+ * PEMToJWK constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "PEM to JWK";
+ this.module = "PublicKey";
+ this.description = "Converts Keys in PEM format to a JSON Web Key format.";
+ this.infoURL = "https://datatracker.ietf.org/doc/html/rfc7517";
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [];
+ this.checks = [
+ {
+ "pattern": "-----BEGIN ((RSA |EC )?(PRIVATE|PUBLIC) KEY|CERTIFICATE)-----",
+ "args": []
+ }
+ ];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ let output = "";
+ let match;
+ const regex = /-----BEGIN ([A-Z][A-Z ]+[A-Z])-----/g;
+ while ((match = regex.exec(input)) !== null) {
+ // find corresponding end tag
+ const indexBase64 = match.index + match[0].length;
+ const header = input.substring(match.index, indexBase64);
+ const footer = `-----END ${match[1]}-----`;
+ const indexFooter = input.indexOf(footer, indexBase64);
+ if (indexFooter === -1) {
+ throw new OperationError(`PEM footer '${footer}' not found`);
+ }
+
+ const pem = input.substring(match.index, indexFooter + footer.length);
+ if (match[1].indexOf("KEY") !== -1) {
+ if (header === "-----BEGIN RSA PUBLIC KEY-----") {
+ throw new OperationError("Unsupported RSA public key format. Only PKCS#8 is supported.");
+ }
+
+ const key = r.KEYUTIL.getKey(pem);
+ if (key.type === "DSA") {
+ throw new OperationError("DSA keys are not supported for JWK");
+ }
+ const jwk = r.KEYUTIL.getJWKFromKey(key);
+ if (output.length > 0) {
+ output += "\n";
+ }
+ output += JSON.stringify(jwk);
+ } else if (match[1] === "CERTIFICATE") {
+ const cert = new r.X509();
+ cert.readCertPEM(pem);
+ const key = cert.getPublicKey();
+ const jwk = r.KEYUTIL.getJWKFromKey(key);
+ if (output.length > 0) {
+ output += "\n";
+ }
+ output += JSON.stringify(jwk);
+ } else {
+ throw new OperationError(`Unsupported PEM type '${match[1]}'`);
+ }
+ }
+ return output;
+ }
+}
+
+export default PEMToJWK;
diff --git a/src/core/operations/PHPSerialize.mjs b/src/core/operations/PHPSerialize.mjs
new file mode 100644
index 000000000..00fb1380b
--- /dev/null
+++ b/src/core/operations/PHPSerialize.mjs
@@ -0,0 +1,126 @@
+/**
+ * @author brun0ne [brunonblok@gmail.com]
+ * @copyright Crown Copyright 2023
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+import OperationError from "../errors/OperationError.mjs";
+
+/**
+ * PHP Serialize operation
+ */
+class PHPSerialize extends Operation {
+
+ /**
+ * PHPSerialize constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "PHP Serialize";
+ this.module = "Default";
+ this.description = "Performs PHP serialization on JSON data. This function does not support object tags. Since PHP doesn't distinguish dicts and arrays, this operation is not always symmetric to PHP Deserialize. Example:[5,"abc",true] becomesa: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/ParseASN1HexString.mjs b/src/core/operations/ParseASN1HexString.mjs
index 14890186c..35fd5ba46 100644
--- a/src/core/operations/ParseASN1HexString.mjs
+++ b/src/core/operations/ParseASN1HexString.mjs
@@ -20,7 +20,7 @@ class ParseASN1HexString extends Operation {
this.name = "Parse ASN.1 hex string";
this.module = "PublicKey";
- this.description = "Abstract Syntax Notation One (ASN.1) is a standard and notation that describes rules and structures for representing, encoding, transmitting, and decoding data in telecommunications and computer networking. This operation parses arbitrary ASN.1 data and presents the resulting tree.";
+ this.description = "Abstract Syntax Notation One (ASN.1) is a standard and notation that describes rules and structures for representing, encoding, transmitting, and decoding data in telecommunications and computer networking. This operation parses arbitrary ASN.1 data (encoded as an hex string: use the 'To Hex' operation if necessary) and presents the resulting tree.";
this.infoURL = "https://wikipedia.org/wiki/Abstract_Syntax_Notation_One";
this.inputType = "string";
this.outputType = "string";
diff --git a/src/core/operations/ParseCSR.mjs b/src/core/operations/ParseCSR.mjs
new file mode 100644
index 000000000..d3b3c364a
--- /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/ParseTLSRecord.mjs b/src/core/operations/ParseTLSRecord.mjs
new file mode 100644
index 000000000..57a339a84
--- /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 000000000..f498375d5
--- /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 11e63424d..cdd1e9c7c 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/PubKeyFromCert.mjs b/src/core/operations/PubKeyFromCert.mjs
new file mode 100644
index 000000000..0233b04aa
--- /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 000000000..5a08882b6
--- /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
new file mode 100644
index 000000000..1470f5f0c
--- /dev/null
+++ b/src/core/operations/RAKE.mjs
@@ -0,0 +1,144 @@
+/**
+ * @author sw5678
+ * @copyright Crown Copyright 2024
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+
+/**
+ * RAKE operation
+ */
+class RAKE extends Operation {
+
+ /**
+ * RAKE constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "RAKE";
+ this.module = "Default";
+ this.description = [
+ "Rapid Keyword Extraction (RAKE)",
+ " ",
+ "RAKE is a domain-independent keyword extraction algorithm in Natural Language Processing.",
+ " ",
+ "The list of stop words are from the NLTK python package",
+ ].join("\n");
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [
+ {
+ name: "Word Delimiter (Regex)",
+ type: "text",
+ value: "\\s"
+ },
+ {
+ name: "Sentence Delimiter (Regex)",
+ type: "text",
+ value: "\\.\\s|\\n"
+ },
+ {
+ name: "Stop Words",
+ type: "text",
+ value: "i,me,my,myself,we,our,ours,ourselves,you,you're,you've,you'll,you'd,your,yours,yourself,yourselves,he,him,his,himself,she,she's,her,hers,herself,it,it's,its,itsef,they,them,their,theirs,themselves,what,which,who,whom,this,that,that'll,these,those,am,is,are,was,were,be,been,being,have,has,had,having,do,does',did,doing,a,an,the,and,but,if,or,because,as,until,while,of,at,by,for,with,about,against,between,into,through,during,before,after,above,below,to,from,up,down,in,out,on,off,over,under,again,further,then,once,here,there,when,where,why,how,all,any,both,each,few,more,most,other,some,such,no,nor,not,only,own,same,so,than,too,very,s,t,can,will,just,don,don't,should,should've,now,d,ll,m,o,re,ve,y,ain,aren,aren't,couldn,couldn't,didn,didn't,doesn,doesn't,hadn,hadn't,hasn,hasn't,haven,haven't,isn,isn't,ma,mightn,mightn't,mustn,mustn't,needn,needn't,shan,shan't,shouldn,shouldn't,wasn,wasn't,weren,weren't,won,won't,wouldn,wouldn't"
+ }
+ ];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+
+ // Get delimiter regexs
+ const wordDelim = new RegExp(args[0], "g");
+ const sentDelim = new RegExp(args[1], "g");
+
+ // Deduplicate the stop words and add the empty string
+ const stopWords = args[2].toLowerCase().replace(/ /g, "").split(",").unique();
+ stopWords.push("");
+
+ // Lower case input and remove start and ending whitespace
+ input = input.toLowerCase().trim();
+
+ // Get tokens, token count, and phrases
+ const tokens = [];
+ const wordFrequencies = [];
+ let phrases = [];
+
+ // Build up list of phrases and token counts
+ const sentences = input.split(sentDelim);
+ for (const sent of sentences) {
+
+ // Split sentence into words
+ const splitSent = sent.split(wordDelim);
+ let startIndex = 0;
+
+ for (let i = 0; i < splitSent.length; i++) {
+ const token = splitSent[i];
+ if (stopWords.includes(token)) {
+ // If token is stop word then split to create phrase
+ phrases.push(splitSent.slice(startIndex, i));
+ startIndex = i + 1;
+ } else {
+ // If token is not a stop word add to the count of the list of words
+ if (tokens.includes(token)) {
+ wordFrequencies[tokens.indexOf(token)]+=1;
+ } else {
+ tokens.push(token);
+ wordFrequencies.push(1);
+ }
+ }
+ }
+ phrases.push(splitSent.slice(startIndex));
+ }
+
+ // remove empty phrases
+ phrases = phrases.filter(subArray => subArray.length > 0);
+
+ // Remove duplicate phrases
+ phrases = phrases.unique();
+
+ // Generate word_degree_matrix and populate
+ 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);
+ for (let i=0; i b[0] - a[0]);
+ scores.unshift(new Array("Scores: ", "Keywords: "));
+
+ // Output works with the 'To Table' functionality already built into CC
+ return scores.map(function (score) {
+ return score.join(", ");
+ }).join("\n");
+ }
+}
+
+export default RAKE;
diff --git a/src/core/operations/ROT13.mjs b/src/core/operations/ROT13.mjs
index 1d0595658..beec94a45 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 25160f53c..5091549f0 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 89b7d81f0..8160438c3 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 be54ee124..39795f215 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 03651f857..89eddde75 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 e3baf54b9..fa8fa59ec 100644
--- a/src/core/operations/RandomizeColourPalette.mjs
+++ b/src/core/operations/RandomizeColourPalette.mjs
@@ -10,7 +10,7 @@ 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/es/index.js";
/**
* Randomize Colour Palette operation
@@ -48,7 +48,7 @@ class RandomizeColourPalette extends Operation {
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),
+ parsedImage = await Jimp.read(input),
width = parsedImage.bitmap.width,
height = parsedImage.bitmap.height;
@@ -61,7 +61,7 @@ class RandomizeColourPalette extends Operation {
parsedImage.setPixelColor(parseInt(rgbHex, 16), x, y);
});
- const imageBuffer = await parsedImage.getBufferAsync(jimp.AUTO);
+ const imageBuffer = await parsedImage.getBufferAsync(Jimp.AUTO);
return new Uint8Array(imageBuffer).buffer;
}
diff --git a/src/core/operations/RegularExpression.mjs b/src/core/operations/RegularExpression.mjs
index 1d8de9c4e..9ea17e837 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])"
diff --git a/src/core/operations/ResizeImage.mjs b/src/core/operations/ResizeImage.mjs
index b2ed3bbfd..2d2af0450 100644
--- a/src/core/operations/ResizeImage.mjs
+++ b/src/core/operations/ResizeImage.mjs
@@ -9,7 +9,7 @@ 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 from "jimp/es/index.js";
/**
* Resize Image operation
@@ -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": Jimp.RESIZE_NEAREST_NEIGHBOR,
+ "Bilinear": Jimp.RESIZE_BILINEAR,
+ "Bicubic": Jimp.RESIZE_BICUBIC,
+ "Hermite": Jimp.RESIZE_HERMITE,
+ "Bezier": Jimp.RESIZE_BEZIER
};
if (!isImage(input)) {
@@ -93,7 +93,7 @@ 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})`);
}
@@ -113,9 +113,9 @@ class ResizeImage extends Operation {
let imageBuffer;
if (image.getMIME() === "image/gif") {
- imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
+ imageBuffer = await image.getBufferAsync(Jimp.MIME_PNG);
} else {
- imageBuffer = await image.getBufferAsync(jimp.AUTO);
+ imageBuffer = await image.getBufferAsync(Jimp.AUTO);
}
return imageBuffer.buffer;
} catch (err) {
diff --git a/src/core/operations/RisonDecode.mjs b/src/core/operations/RisonDecode.mjs
new file mode 100644
index 000000000..d4e36f806
--- /dev/null
+++ b/src/core/operations/RisonDecode.mjs
@@ -0,0 +1,57 @@
+/**
+ * @author sg5506844 [sg5506844@gmail.com]
+ * @copyright Crown Copyright 2021
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+import OperationError from "../errors/OperationError.mjs";
+import rison from "rison";
+
+/**
+ * Rison Decode operation
+ */
+class RisonDecode extends Operation {
+
+ /**
+ * RisonDecode constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "Rison Decode";
+ 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";
+ this.outputType = "Object";
+ this.args = [
+ {
+ name: "Decode Option",
+ type: "editableOption",
+ value: ["Decode", "Decode Object", "Decode Array"]
+ },
+ ];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {Object}
+ */
+ run(input, args) {
+ const [decodeOption] = args;
+ switch (decodeOption) {
+ case "Decode":
+ return rison.decode(input);
+ case "Decode Object":
+ return rison.decode_object(input);
+ case "Decode Array":
+ return rison.decode_array(input);
+ default:
+ throw new OperationError("Invalid Decode option");
+ }
+ }
+}
+
+export default RisonDecode;
diff --git a/src/core/operations/RisonEncode.mjs b/src/core/operations/RisonEncode.mjs
new file mode 100644
index 000000000..12b13b668
--- /dev/null
+++ b/src/core/operations/RisonEncode.mjs
@@ -0,0 +1,59 @@
+/**
+ * @author sg5506844 [sg5506844@gmail.com]
+ * @copyright Crown Copyright 2021
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+import OperationError from "../errors/OperationError.mjs";
+import rison from "rison";
+
+/**
+ * Rison Encode operation
+ */
+class RisonEncode extends Operation {
+
+ /**
+ * RisonEncode constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "Rison Encode";
+ 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";
+ this.outputType = "string";
+ this.args = [
+ {
+ name: "Encode Option",
+ type: "option",
+ value: ["Encode", "Encode Object", "Encode Array", "Encode URI"]
+ },
+ ];
+ }
+
+ /**
+ * @param {Object} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ const [encodeOption] = args;
+ switch (encodeOption) {
+ case "Encode":
+ return rison.encode(input);
+ case "Encode Object":
+ return rison.encode_object(input);
+ case "Encode Array":
+ return rison.encode_array(input);
+ case "Encode URI":
+ return rison.encode_uri(input);
+ default:
+ throw new OperationError("Invalid encode option");
+ }
+ }
+}
+
+export default RisonEncode;
diff --git a/src/core/operations/RotateImage.mjs b/src/core/operations/RotateImage.mjs
index a4659b12c..894ec7855 100644
--- a/src/core/operations/RotateImage.mjs
+++ b/src/core/operations/RotateImage.mjs
@@ -9,7 +9,7 @@ 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 from "jimp/es/index.js";
/**
* Rotate Image operation
@@ -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})`);
}
@@ -63,9 +63,9 @@ class RotateImage extends Operation {
let imageBuffer;
if (image.getMIME() === "image/gif") {
- imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
+ imageBuffer = await image.getBufferAsync(Jimp.MIME_PNG);
} else {
- imageBuffer = await image.getBufferAsync(jimp.AUTO);
+ imageBuffer = await image.getBufferAsync(Jimp.AUTO);
}
return imageBuffer.buffer;
} catch (err) {
diff --git a/src/core/operations/SIGABA.mjs b/src/core/operations/SIGABA.mjs
index 274f09f65..e3a9b82ef 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 000000000..396571105
--- /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 000000000..b1e5f9018
--- /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/SSDEEP.mjs b/src/core/operations/SSDEEP.mjs
index df1557d51..6a76a68be 100644
--- a/src/core/operations/SSDEEP.mjs
+++ b/src/core/operations/SSDEEP.mjs
@@ -21,7 +21,7 @@ class SSDEEP extends Operation {
this.name = "SSDEEP";
this.module = "Crypto";
this.description = "SSDEEP is a program for computing context triggered piecewise hashes (CTPH). Also called fuzzy hashes, CTPH can match inputs that have homologies. Such inputs have sequences of identical bytes in the same order, although bytes in between these sequences may be different in both content and length. SSDEEP hashes are now widely used for simple identification purposes (e.g. the 'Basic Properties' section in VirusTotal). Although 'better' fuzzy hashes are available, SSDEEP is still one of the primary choices because of its speed and being a de facto standard. This operation is fundamentally the same as the CTPH operation, however their outputs differ in format.";
- this.infoURL = "https://forensicswiki.xyz/wiki/index.php?title=Ssdeep";
+ this.infoURL = "https://forensics.wiki/ssdeep";
this.inputType = "string";
this.outputType = "string";
this.args = [];
diff --git a/src/core/operations/Salsa20.mjs b/src/core/operations/Salsa20.mjs
new file mode 100644
index 000000000..7a76cf262
--- /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 eb033ad2c..5cf6b606d 100644
--- a/src/core/operations/SharpenImage.mjs
+++ b/src/core/operations/SharpenImage.mjs
@@ -10,7 +10,7 @@ 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 from "jimp/es/index.js";
/**
* Sharpen Image operation
@@ -68,7 +68,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})`);
}
@@ -137,9 +137,9 @@ class SharpenImage extends Operation {
let imageBuffer;
if (image.getMIME() === "image/gif") {
- imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
+ imageBuffer = await image.getBufferAsync(Jimp.MIME_PNG);
} else {
- imageBuffer = await image.getBufferAsync(jimp.AUTO);
+ imageBuffer = await image.getBufferAsync(Jimp.AUTO);
}
return imageBuffer.buffer;
} catch (err) {
diff --git a/src/core/operations/ShowOnMap.mjs b/src/core/operations/ShowOnMap.mjs
index c2ac1c6e3..d75c2aa6a 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 `