Browse Source

Merge branch 'esm' of https://github.com/gchq/CyberChef into ip-convert

Callum Fraser 7 năm trước cách đây
mục cha
commit
ea36687205
75 tập tin đã thay đổi với 4713 bổ sung1315 xóa
  1. 4 4
      Gruntfile.js
  2. 257 257
      package-lock.json
  3. 0 1
      package.json
  4. 6 0
      src/core/Chef.mjs
  5. 2 1
      src/core/Operation.mjs
  6. 20 2
      src/core/Recipe.mjs
  7. 2 0
      src/core/errors/OperationError.mjs
  8. 48 0
      src/core/lib/BCD.mjs
  9. 22 0
      src/core/lib/Base58.mjs
  10. 204 0
      src/core/lib/CanvasComponents.mjs
  11. 41 0
      src/core/lib/Extract.mjs
  12. 116 0
      src/core/lib/PGP.mjs
  13. 6 3
      src/core/operations/AESDecrypt.mjs
  14. 5 2
      src/core/operations/AESEncrypt.mjs
  15. 2 0
      src/core/operations/AffineCipherDecode.mjs
  16. 2 0
      src/core/operations/BifidCipherDecode.mjs
  17. 2 0
      src/core/operations/BifidCipherEncode.mjs
  18. 1 1
      src/core/operations/CartesianProduct.mjs
  19. 53 0
      src/core/operations/ChiSquare.mjs
  20. 133 0
      src/core/operations/DisassembleX86.mjs
  21. 4 1
      src/core/operations/DropBytes.mjs
  22. 96 0
      src/core/operations/Entropy.mjs
  23. 79 0
      src/core/operations/EscapeUnicodeCharacters.mjs
  24. 52 0
      src/core/operations/ExtractDates.mjs
  25. 49 0
      src/core/operations/ExtractDomains.mjs
  26. 49 0
      src/core/operations/ExtractEmailAddresses.mjs
  27. 78 0
      src/core/operations/ExtractFilePaths.mjs
  28. 91 0
      src/core/operations/ExtractIPAddresses.mjs
  29. 49 0
      src/core/operations/ExtractMACAddresses.mjs
  30. 55 0
      src/core/operations/ExtractURLs.mjs
  31. 2 1
      src/core/operations/Filter.mjs
  32. 110 0
      src/core/operations/FrequencyDistribution.mjs
  33. 115 0
      src/core/operations/FromBCD.mjs
  34. 63 0
      src/core/operations/FromBase.mjs
  35. 93 0
      src/core/operations/FromBase58.mjs
  36. 4 1
      src/core/operations/FromCharcode.mjs
  37. 335 0
      src/core/operations/FromHTMLEntity.mjs
  38. 61 0
      src/core/operations/FromQuotedPrintable.mjs
  39. 4 1
      src/core/operations/FromUNIXTimestamp.mjs
  40. 116 0
      src/core/operations/GeneratePGPKeyPair.mjs
  41. 3 2
      src/core/operations/HammingDistance.mjs
  42. 217 0
      src/core/operations/MicrosoftScriptDecoder.mjs
  43. 2 1
      src/core/operations/OffsetChecker.mjs
  44. 86 0
      src/core/operations/PGPDecrypt.mjs
  45. 122 0
      src/core/operations/PGPDecryptAndVerify.mjs
  46. 85 0
      src/core/operations/PGPEncrypt.mjs
  47. 93 0
      src/core/operations/PGPEncryptAndSign.mjs
  48. 195 0
      src/core/operations/ParseColourCode.mjs
  49. 2 1
      src/core/operations/ParseDateTime.mjs
  50. 2 1
      src/core/operations/ParseUNIXFilePermissions.mjs
  51. 69 0
      src/core/operations/ParseURI.mjs
  52. 2 1
      src/core/operations/RawInflate.mjs
  53. 2 1
      src/core/operations/ShowBase64Offsets.mjs
  54. 116 0
      src/core/operations/Strings.mjs
  55. 65 0
      src/core/operations/StripHTMLTags.mjs
  56. 137 0
      src/core/operations/SwapEndianness.mjs
  57. 4 1
      src/core/operations/TakeBytes.mjs
  58. 141 0
      src/core/operations/ToBCD.mjs
  59. 53 0
      src/core/operations/ToBase.mjs
  60. 85 0
      src/core/operations/ToBase58.mjs
  61. 4 1
      src/core/operations/ToCharcode.mjs
  62. 345 0
      src/core/operations/ToHTMLEntity.mjs
  63. 244 0
      src/core/operations/ToQuotedPrintable.mjs
  64. 4 1
      src/core/operations/ToUNIXTimestamp.mjs
  65. 2 1
      src/core/operations/TranslateDateTimeFormat.mjs
  66. 44 0
      src/core/operations/URLDecode.mjs
  67. 68 0
      src/core/operations/URLEncode.mjs
  68. 75 0
      src/core/operations/UnescapeUnicodeCharacters.mjs
  69. 0 333
      src/core/operations/legacy/Extract.js
  70. 0 364
      src/core/operations/legacy/PGP.js
  71. 0 118
      src/core/operations/legacy/URL.js
  72. 13 13
      src/core/vendor/DisassembleX86-64.mjs
  73. 0 186
      src/core/vendor/canvascomponents.js
  74. 1 1
      src/web/index.js
  75. 1 14
      webpack.config.js

+ 4 - 4
Gruntfile.js

@@ -22,11 +22,11 @@ module.exports = function (grunt) {
     // Tasks
     grunt.registerTask("dev",
         "A persistent task which creates a development build whenever source files are modified.",
-        ["clean:dev", "concurrent:dev"]);
+        ["clean:dev", "exec:generateConfig", "concurrent:dev"]);
 
     grunt.registerTask("node",
         "Compiles CyberChef into a single NodeJS module.",
-        ["clean:node", "clean:config", "webpack:node", "chmod:build"]);
+        ["clean:node", "clean:config", "exec:generateConfig", "webpack:node", "chmod:build"]);
 
     grunt.registerTask("test",
         "A task which runs all the tests in test/tests.",
@@ -38,7 +38,7 @@ module.exports = function (grunt) {
 
     grunt.registerTask("prod",
         "Creates a production-ready build. Use the --msg flag to add a compile message.",
-        ["eslint", "clean:prod", "webpack:web", "inline", "chmod"]);
+        ["eslint", "clean:prod", "exec:generateConfig", "webpack:web", "inline", "chmod"]);
 
     grunt.registerTask("default",
         "Lints the code base",
@@ -46,7 +46,7 @@ module.exports = function (grunt) {
 
     grunt.registerTask("inline",
         "Compiles a production build of CyberChef into a single, portable web page.",
-        ["webpack:webInline", "runInliner", "clean:inlineScripts"]);
+        ["exec:generateConfig", "webpack:webInline", "runInliner", "clean:inlineScripts"]);
 
 
     grunt.registerTask("runInliner", runInliner);

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 257 - 257
package-lock.json


+ 0 - 1
package.json

@@ -65,7 +65,6 @@
     "webpack": "^4.6.0",
     "webpack-dev-server": "^3.1.3",
     "webpack-node-externals": "^1.7.2",
-    "webpack-synchronizable-shell-plugin": "0.0.7",
     "worker-loader": "^1.1.1"
   },
   "dependencies": {

+ 6 - 0
src/core/Chef.mjs

@@ -60,6 +60,12 @@ class Chef {
             recipe.setBreakpoint(progress + 1, true);
         }
 
+        // If the previously run operation presented a different value to its
+        // normal output, we need to recalculate it.
+        if (recipe.lastOpPresented(progress)) {
+            progress = 0;
+        }
+
         // If stepping with flow control, we have to start from the beginning
         // but still want to skip all previous breakpoints
         if (progress > 0 && containsFc) {

+ 2 - 1
src/core/Operation.mjs

@@ -81,9 +81,10 @@ class Operation {
      * this behaviour.
      *
      * @param {*} data - The result of the run() function
+     * @param {Object[]} args - The operation's arguments
      * @returns {*} - A human-readable version of the data
      */
-    present(data) {
+    present(data, args) {
         return data;
     }
 

+ 20 - 2
src/core/Recipe.mjs

@@ -177,7 +177,10 @@ class Recipe  {
                 }
             } catch (err) {
                 // Return expected errors as output
-                if (err instanceof OperationError) {
+                if (err instanceof OperationError ||
+                    (err.type && err.type === "OperationError")) {
+                    // Cannot rely on `err instanceof OperationError` here as extending
+                    // native types is not fully supported yet.
                     dish.set(err.message, "string");
                     return i;
                 } else {
@@ -209,7 +212,10 @@ class Recipe  {
     async present(dish) {
         if (!this.lastRunOp) return;
 
-        const output = await this.lastRunOp.present(await dish.get(this.lastRunOp.outputType));
+        const output = await this.lastRunOp.present(
+            await dish.get(this.lastRunOp.outputType),
+            this.lastRunOp.ingValues
+        );
         dish.set(output, this.lastRunOp.presentType);
     }
 
@@ -267,6 +273,18 @@ class Recipe  {
         return highlights;
     }
 
+
+    /**
+     * Determines whether the previous operation has a different presentation type to its normal output.
+     *
+     * @param {number} progress
+     * @returns {boolean}
+     */
+    lastOpPresented(progress) {
+        if (progress < 1) return false;
+        return this.opList[progress-1].presentType !== this.opList[progress-1].outputType;
+    }
+
 }
 
 export default Recipe;

+ 2 - 0
src/core/errors/OperationError.mjs

@@ -15,6 +15,8 @@ class OperationError extends Error {
     constructor(...args) {
         super(...args);
 
+        this.type = "OperationError";
+
         if (Error.captureStackTrace) {
             Error.captureStackTrace(this, OperationError);
         }

+ 48 - 0
src/core/lib/BCD.mjs

@@ -0,0 +1,48 @@
+/**
+ * Binary Code Decimal resources.
+ *
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2017
+ * @license Apache-2.0
+ */
+
+/**
+ * BCD encoding schemes.
+ */
+export const ENCODING_SCHEME = [
+    "8 4 2 1",
+    "7 4 2 1",
+    "4 2 2 1",
+    "2 4 2 1",
+    "8 4 -2 -1",
+    "Excess-3",
+    "IBM 8 4 2 1",
+];
+
+/**
+ * Lookup table for the binary value of each digit representation.
+ *
+ * I wrote a very nice algorithm to generate 8 4 2 1 encoding programatically,
+ * but unfortunately it's much easier (if less elegant) to use lookup tables
+ * when supporting multiple encoding schemes.
+ *
+ * "Practicality beats purity" - PEP 20
+ *
+ * In some schemes it is possible to represent the same value in multiple ways.
+ * For instance, in 4 2 2 1 encoding, 0100 and 0010 both represent 2. Support
+ * has not yet been added for this.
+ */
+export const ENCODING_LOOKUP = {
+    "8 4 2 1":     [0,  1,  2,  3,  4,  5,  6,  7,  8,  9],
+    "7 4 2 1":     [0,  1,  2,  3,  4,  5,  6,  8,  9,  10],
+    "4 2 2 1":     [0,  1,  4,  5,  8,  9,  12, 13, 14, 15],
+    "2 4 2 1":     [0,  1,  2,  3,  4,  11, 12, 13, 14, 15],
+    "8 4 -2 -1":   [0,  7,  6,  5,  4,  11, 10, 9,  8,  15],
+    "Excess-3":    [3,  4,  5,  6,  7,  8,  9,  10, 11, 12],
+    "IBM 8 4 2 1": [10, 1,  2,  3,  4,  5,  6,  7,  8,  9],
+};
+
+/**
+ * BCD formats.
+ */
+export const FORMAT = ["Nibbles", "Bytes", "Raw"];

+ 22 - 0
src/core/lib/Base58.mjs

@@ -0,0 +1,22 @@
+/**
+ * Base58 resources.
+ *
+ * @author tlwr [toby@toby.codes]
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2017
+ * @license Apache-2.0
+ */
+
+/**
+ * Base58 alphabet options.
+ */
+export const ALPHABET_OPTIONS = [
+    {
+        name: "Bitcoin",
+        value: "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz",
+    },
+    {
+        name: "Ripple",
+        value: "rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz",
+    },
+];

+ 204 - 0
src/core/lib/CanvasComponents.mjs

@@ -0,0 +1,204 @@
+/**
+ * Various components for drawing diagrams on an HTML5 canvas.
+ *
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+/**
+ * Draws a line from one point to another
+ *
+ * @param ctx
+ * @param startX
+ * @param startY
+ * @param endX
+ * @param endY
+ */
+export function drawLine(ctx, startX, startY, endX, endY) {
+    ctx.beginPath();
+    ctx.moveTo(startX, startY);
+    ctx.lineTo(endX, endY);
+    ctx.closePath();
+    ctx.stroke();
+}
+
+/**
+ * Draws a bar chart on the canvas.
+ *
+ * @param canvas
+ * @param scores
+ * @param xAxisLabel
+ * @param yAxisLabel
+ * @param numXLabels
+ * @param numYLabels
+ * @param fontSize
+ */
+export function drawBarChart(canvas, scores, xAxisLabel, yAxisLabel, numXLabels, numYLabels, fontSize) {
+    fontSize = fontSize || 15;
+    if (!numXLabels || numXLabels > Math.round(canvas.width / 50)) {
+        numXLabels = Math.round(canvas.width / 50);
+    }
+    if (!numYLabels || numYLabels > Math.round(canvas.width / 50)) {
+        numYLabels = Math.round(canvas.height / 50);
+    }
+
+    // Graph properties
+    const ctx = canvas.getContext("2d"),
+        leftPadding = canvas.width * 0.08,
+        rightPadding = canvas.width * 0.03,
+        topPadding = canvas.height * 0.08,
+        bottomPadding = canvas.height * 0.15,
+        graphHeight = canvas.height - topPadding - bottomPadding,
+        graphWidth = canvas.width - leftPadding - rightPadding,
+        base = topPadding + graphHeight,
+        ceil = topPadding;
+
+    ctx.font = fontSize + "px Arial";
+
+    // Draw axis
+    ctx.lineWidth = "1.0";
+    ctx.strokeStyle = "#444";
+    drawLine(ctx, leftPadding, base, graphWidth + leftPadding, base); // x
+    drawLine(ctx, leftPadding, base, leftPadding, ceil); // y
+
+    // Bar properties
+    const barPadding = graphWidth * 0.003,
+        barWidth = (graphWidth - (barPadding * scores.length)) / scores.length,
+        max = Math.max.apply(Math, scores);
+    let currX = leftPadding + barPadding;
+
+    // Draw bars
+    ctx.fillStyle = "green";
+    for (let i = 0; i < scores.length; i++) {
+        const h = scores[i] / max * graphHeight;
+        ctx.fillRect(currX, base - h, barWidth, h);
+        currX += barWidth + barPadding;
+    }
+
+    // Mark x axis
+    ctx.fillStyle = "black";
+    ctx.textAlign = "center";
+    currX = leftPadding + barPadding;
+    if (numXLabels >= scores.length) {
+        // Mark every score
+        for (let i = 0; i <= scores.length; i++) {
+            ctx.fillText(i, currX, base + (bottomPadding * 0.3));
+            currX += barWidth + barPadding;
+        }
+    } else {
+        // Mark some scores
+        for (let i = 0; i <= numXLabels; i++) {
+            const val = Math.ceil((scores.length / numXLabels) * i);
+            currX = (graphWidth / numXLabels) * i + leftPadding;
+            ctx.fillText(val, currX, base + (bottomPadding * 0.3));
+        }
+    }
+
+    // Mark y axis
+    ctx.textAlign = "right";
+    let currY;
+    if (numYLabels >= max) {
+        // Mark every increment
+        for (let i = 0; i <= max; i++) {
+            currY = base - (i / max * graphHeight) + fontSize / 3;
+            ctx.fillText(i, leftPadding * 0.8, currY);
+        }
+    } else {
+        // Mark some increments
+        for (let i = 0; i <= numYLabels; i++) {
+            const val = Math.ceil((max / numYLabels) * i);
+            currY = base - (val / max * graphHeight) + fontSize / 3;
+            ctx.fillText(val, leftPadding * 0.8, currY);
+        }
+    }
+
+    // Label x axis
+    if (xAxisLabel) {
+        ctx.textAlign = "center";
+        ctx.fillText(xAxisLabel, graphWidth / 2 + leftPadding, base + bottomPadding * 0.8);
+    }
+
+    // Label y axis
+    if (yAxisLabel) {
+        ctx.save();
+        const x = leftPadding * 0.3,
+            y = graphHeight / 2 + topPadding;
+        ctx.translate(x, y);
+        ctx.rotate(-Math.PI / 2);
+        ctx.textAlign = "center";
+        ctx.fillText(yAxisLabel, 0, 0);
+        ctx.restore();
+    }
+}
+
+/**
+ * Draws a scale bar on the canvas.
+ *
+ * @param canvas
+ * @param score
+ * @param max
+ * @param markings
+ */
+export function drawScaleBar(canvas, score, max, markings) {
+    // Bar properties
+    const ctx = canvas.getContext("2d"),
+        leftPadding = canvas.width * 0.01,
+        rightPadding = canvas.width * 0.01,
+        topPadding = canvas.height * 0.1,
+        bottomPadding = canvas.height * 0.3,
+        barHeight = canvas.height - topPadding - bottomPadding,
+        barWidth = canvas.width - leftPadding - rightPadding;
+
+    // Scale properties
+    const proportion = score / max;
+
+    // Draw bar outline
+    ctx.strokeRect(leftPadding, topPadding, barWidth, barHeight);
+
+    // Shade in up to proportion
+    const grad = ctx.createLinearGradient(leftPadding, 0, barWidth + leftPadding, 0);
+    grad.addColorStop(0, "green");
+    grad.addColorStop(0.5, "gold");
+    grad.addColorStop(1, "red");
+    ctx.fillStyle = grad;
+    ctx.fillRect(leftPadding, topPadding, barWidth * proportion, barHeight);
+
+    // Add markings
+    let x0, y0, x1, y1;
+    ctx.fillStyle = "black";
+    ctx.textAlign = "center";
+    ctx.font = "13px Arial";
+    for (let i = 0; i < markings.length; i++) {
+        // Draw min line down
+        x0 = barWidth / max * markings[i].min + leftPadding;
+        y0 = topPadding + barHeight + (bottomPadding * 0.1);
+        x1 = x0;
+        y1 = topPadding + barHeight + (bottomPadding * 0.3);
+        drawLine(ctx, x0, y0, x1, y1);
+
+        // Draw max line down
+        x0 = barWidth / max * markings[i].max + leftPadding;
+        x1 = x0;
+        drawLine(ctx, x0, y0, x1, y1);
+
+        // Join min and max lines
+        x0 = barWidth / max * markings[i].min + leftPadding;
+        y0 = topPadding + barHeight + (bottomPadding * 0.3);
+        x1 = barWidth / max * markings[i].max + leftPadding;
+        y1 = y0;
+        drawLine(ctx, x0, y0, x1, y1);
+
+        // Add label
+        if (markings[i].max >= max * 0.9) {
+            ctx.textAlign = "right";
+            x0 = x1;
+        } else if (markings[i].max <= max * 0.1) {
+            ctx.textAlign = "left";
+        } else {
+            x0 = x0 + (x1 - x0) / 2;
+        }
+        y0 = topPadding + barHeight + (bottomPadding * 0.8);
+        ctx.fillText(markings[i].label, x0, y0);
+    }
+}

+ 41 - 0
src/core/lib/Extract.mjs

@@ -0,0 +1,41 @@
+/**
+ * Identifier extraction functions
+ *
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ *
+ */
+
+/**
+ * Runs search operations across the input data using regular expressions.
+ *
+ * @param {string} input
+ * @param {RegExp} searchRegex
+ * @param {RegExp} removeRegex - A regular expression defining results to remove from the
+ *      final list
+ * @param {boolean} includeTotal - Whether or not to include the total number of results
+ * @returns {string}
+ */
+export function search (input, searchRegex, removeRegex, includeTotal) {
+    let output = "",
+        total = 0,
+        match;
+
+    while ((match = searchRegex.exec(input))) {
+        // Moves pointer when an empty string is matched (prevents infinite loop)
+        if (match.index === searchRegex.lastIndex) {
+            searchRegex.lastIndex++;
+        }
+
+        if (removeRegex && removeRegex.test(match[0]))
+            continue;
+        total++;
+        output += match[0] + "\n";
+    }
+
+    if (includeTotal)
+        output = "Total found: " + total + "\n\n" + output;
+
+    return output;
+}

+ 116 - 0
src/core/lib/PGP.mjs

@@ -0,0 +1,116 @@
+/**
+ * PGP functions.
+ *
+ * @author tlwr [toby@toby.codes]
+ * @author Matt C [matt@artemisbot.uk]
+ * @author n1474335 [n1474335@gmail.com]
+ *
+ * @copyright Crown Copyright 2018
+ * @license Apache-2.0
+ *
+ */
+
+import kbpgp from "kbpgp";
+import promisifyDefault from "es6-promisify";
+const promisify = promisifyDefault.promisify;
+
+/**
+ * Progress callback
+ */
+export const ASP = kbpgp.ASP({
+    "progress_hook": info => {
+        let msg = "";
+
+        switch (info.what) {
+            case "guess":
+                msg = "Guessing a prime";
+                break;
+            case "fermat":
+                msg = "Factoring prime using Fermat's factorization method";
+                break;
+            case "mr":
+                msg = "Performing Miller-Rabin primality test";
+                break;
+            case "passed_mr":
+                msg = "Passed Miller-Rabin primality test";
+                break;
+            case "failed_mr":
+                msg = "Failed Miller-Rabin primality test";
+                break;
+            case "found":
+                msg = "Prime found";
+                break;
+            default:
+                msg = `Stage: ${info.what}`;
+        }
+
+        if (ENVIRONMENT_IS_WORKER())
+            self.sendStatusMessage(msg);
+    }
+});
+
+/**
+ * Get size of subkey
+ *
+ * @param {number} keySize
+ * @returns {number}
+ */
+export function getSubkeySize(keySize) {
+    return {
+        1024: 1024,
+        2048: 1024,
+        4096: 2048,
+        256:   256,
+        384:   256,
+    }[keySize];
+}
+
+/**
+* Import private key and unlock if necessary
+*
+* @param {string} privateKey
+* @param {string} [passphrase]
+* @returns {Object}
+*/
+export async function importPrivateKey(privateKey, passphrase) {
+    try {
+        const key = await promisify(kbpgp.KeyManager.import_from_armored_pgp)({
+            armored: privateKey,
+            opts: {
+                "no_check_keys": true
+            }
+        });
+        if (key.is_pgp_locked()) {
+            if (passphrase) {
+                await promisify(key.unlock_pgp.bind(key))({
+                    passphrase
+                });
+            } else {
+                throw "Did not provide passphrase with locked private key.";
+            }
+        }
+        return key;
+    } catch (err) {
+        throw `Could not import private key: ${err}`;
+    }
+}
+
+/**
+ * Import public key
+ *
+ * @param {string} publicKey
+ * @returns {Object}
+ */
+export async function importPublicKey (publicKey) {
+    try {
+        const key = await promisify(kbpgp.KeyManager.import_from_armored_pgp)({
+            armored: publicKey,
+            opts: {
+                "no_check_keys": true
+            }
+        });
+        return key;
+    } catch (err) {
+        throw `Could not import public key: ${err}`;
+    }
+}

+ 6 - 3
src/core/operations/AESDecrypt.mjs

@@ -7,6 +7,7 @@
 import Operation from "../Operation";
 import Utils from "../Utils";
 import forge from "node-forge/dist/forge.min.js";
+import OperationError from "../errors/OperationError";
 
 /**
  * AES Decrypt operation
@@ -65,6 +66,8 @@ class AESDecrypt extends Operation {
      * @param {string} input
      * @param {Object[]} args
      * @returns {string}
+     *
+     * @throws {OperationError} if cannot decrypt input or invalid key length
      */
     run(input, args) {
         const key = Utils.convertToByteArray(args[0].string, args[0].option),
@@ -75,12 +78,12 @@ class AESDecrypt extends Operation {
             gcmTag = Utils.convertToByteString(args[5].string, args[5].option);
 
         if ([16, 24, 32].indexOf(key.length) < 0) {
-            return `Invalid key length: ${key.length} bytes
+            throw new OperationError(`Invalid key length: ${key.length} bytes
 
 The following algorithms will be used based on the size of the key:
   16 bytes = AES-128
   24 bytes = AES-192
-  32 bytes = AES-256`;
+  32 bytes = AES-256`);
         }
 
         input = Utils.convertToByteString(input, inputType);
@@ -96,7 +99,7 @@ The following algorithms will be used based on the size of the key:
         if (result) {
             return outputType === "Hex" ? decipher.output.toHex() : decipher.output.getBytes();
         } else {
-            return "Unable to decrypt input with these parameters.";
+            throw new OperationError("Unable to decrypt input with these parameters.");
         }
     }
 

+ 5 - 2
src/core/operations/AESEncrypt.mjs

@@ -7,6 +7,7 @@
 import Operation from "../Operation";
 import Utils from "../Utils";
 import forge from "node-forge/dist/forge.min.js";
+import OperationError from "../errors/OperationError";
 
 /**
  * AES Encrypt operation
@@ -59,6 +60,8 @@ class AESEncrypt extends Operation {
      * @param {string} input
      * @param {Object[]} args
      * @returns {string}
+     *
+     * @throws {OperationError} if invalid key length
      */
     run(input, args) {
         const key = Utils.convertToByteArray(args[0].string, args[0].option),
@@ -68,12 +71,12 @@ class AESEncrypt extends Operation {
             outputType = args[4];
 
         if ([16, 24, 32].indexOf(key.length) < 0) {
-            return `Invalid key length: ${key.length} bytes
+            throw new OperationError(`Invalid key length: ${key.length} bytes
 
 The following algorithms will be used based on the size of the key:
   16 bytes = AES-128
   24 bytes = AES-192
-  32 bytes = AES-256`;
+  32 bytes = AES-256`);
         }
 
         input = Utils.convertToByteString(input, inputType);

+ 2 - 0
src/core/operations/AffineCipherDecode.mjs

@@ -42,6 +42,8 @@ class AffineCipherDecode extends Operation {
      * @param {string} input
      * @param {Object[]} args
      * @returns {string}
+     *
+     * @throws {OperationError} if a or b values are invalid
      */
     run(input, args) {
         const alphabet = "abcdefghijklmnopqrstuvwxyz",

+ 2 - 0
src/core/operations/BifidCipherDecode.mjs

@@ -37,6 +37,8 @@ class BifidCipherDecode extends Operation {
      * @param {string} input
      * @param {Object[]} args
      * @returns {string}
+     *
+     * @throws {OperationError} if invalid key
      */
     run(input, args) {
         const keywordStr = args[0].toUpperCase().replace("J", "I"),

+ 2 - 0
src/core/operations/BifidCipherEncode.mjs

@@ -37,6 +37,8 @@ class BifidCipherEncode extends Operation {
      * @param {string} input
      * @param {Object[]} args
      * @returns {string}
+     *
+     * @throws {OperationError} if key is invalid
      */
     run(input, args) {
         const keywordStr = args[0].toUpperCase().replace("J", "I"),

+ 1 - 1
src/core/operations/CartesianProduct.mjs

@@ -41,7 +41,7 @@ class CartesianProduct extends Operation {
      * Validate input length
      *
      * @param {Object[]} sets
-     * @throws {Error} if fewer than 2 sets
+     * @throws {OperationError} if fewer than 2 sets
      */
     validateSampleNumbers(sets) {
         if (!sets || sets.length < 2) {

+ 53 - 0
src/core/operations/ChiSquare.mjs

@@ -0,0 +1,53 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2017
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+
+/**
+ * Chi Square operation
+ */
+class ChiSquare extends Operation {
+
+    /**
+     * ChiSquare constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "Chi Square";
+        this.module = "Default";
+        this.description = "Calculates the Chi Square distribution of values.";
+        this.inputType = "ArrayBuffer";
+        this.outputType = "number";
+        this.args = [];
+    }
+
+    /**
+     * @param {ArrayBuffer} input
+     * @param {Object[]} args
+     * @returns {number}
+     */
+    run(input, args) {
+        const data = new Uint8Array(input);
+        const distArray = new Array(256).fill(0);
+        let total = 0;
+
+        for (let i = 0; i < data.length; i++) {
+            distArray[data[i]]++;
+        }
+
+        for (let i = 0; i < distArray.length; i++) {
+            if (distArray[i] > 0) {
+                total += Math.pow(distArray[i] - data.length / 256, 2) / (data.length / 256);
+            }
+        }
+
+        return total;
+    }
+
+}
+
+export default ChiSquare;

+ 133 - 0
src/core/operations/DisassembleX86.mjs

@@ -0,0 +1,133 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2017
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+import * as disassemble from "../vendor/DisassembleX86-64";
+import OperationError from "../errors/OperationError";
+
+/**
+ * Disassemble x86 operation
+ */
+class DisassembleX86 extends Operation {
+
+    /**
+     * DisassembleX86 constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "Disassemble x86";
+        this.module = "Shellcode";
+        this.description = "Disassembly is the process of translating machine language into assembly language.<br><br>This operation supports 64-bit, 32-bit and 16-bit code written for Intel or AMD x86 processors. It is particularly useful for reverse engineering shellcode.<br><br>Input should be in hexadecimal.";
+        this.inputType = "string";
+        this.outputType = "string";
+        this.args = [
+            {
+                "name": "Bit mode",
+                "type": "option",
+                "value": ["64", "32", "16"]
+            },
+            {
+                "name": "Compatibility",
+                "type": "option",
+                "value": [
+                    "Full x86 architecture",
+                    "Knights Corner",
+                    "Larrabee",
+                    "Cyrix",
+                    "Geode",
+                    "Centaur",
+                    "X86/486"
+                ]
+            },
+            {
+                "name": "Code Segment (CS)",
+                "type": "number",
+                "value": 16
+            },
+            {
+                "name": "Offset (IP)",
+                "type": "number",
+                "value": 0
+            },
+            {
+                "name": "Show instruction hex",
+                "type": "boolean",
+                "value": true
+            },
+            {
+                "name": "Show instruction position",
+                "type": "boolean",
+                "value": true
+            }
+        ];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {string}
+     *
+     * @throws {OperationError} if invalid mode value
+     */
+    run(input, args) {
+        const [
+            mode,
+            compatibility,
+            codeSegment,
+            offset,
+            showInstructionHex,
+            showInstructionPos
+        ] = args;
+
+        switch (mode) {
+            case "64":
+                disassemble.setBitMode(2);
+                break;
+            case "32":
+                disassemble.setBitMode(1);
+                break;
+            case "16":
+                disassemble.setBitMode(0);
+                break;
+            default:
+                throw new OperationError("Invalid mode value");
+        }
+
+        switch (compatibility) {
+            case "Full x86 architecture":
+                disassemble.CompatibilityMode(0);
+                break;
+            case "Knights Corner":
+                disassemble.CompatibilityMode(1);
+                break;
+            case "Larrabee":
+                disassemble.CompatibilityMode(2);
+                break;
+            case "Cyrix":
+                disassemble.CompatibilityMode(3);
+                break;
+            case "Geode":
+                disassemble.CompatibilityMode(4);
+                break;
+            case "Centaur":
+                disassemble.CompatibilityMode(5);
+                break;
+            case "X86/486":
+                disassemble.CompatibilityMode(6);
+                break;
+        }
+
+        disassemble.SetBasePosition(codeSegment + ":" + offset);
+        disassemble.setShowInstructionHex(showInstructionHex);
+        disassemble.setShowInstructionPos(showInstructionPos);
+        disassemble.LoadBinCode(input.replace(/\s/g, ""));
+        return disassemble.LDisassemble();
+    }
+
+}
+
+export default DisassembleX86;

+ 4 - 1
src/core/operations/DropBytes.mjs

@@ -5,6 +5,7 @@
  */
 
 import Operation from "../Operation";
+import OperationError from "../errors/OperationError";
 
 /**
  * Drop bytes operation
@@ -45,6 +46,8 @@ class DropBytes extends Operation {
      * @param {ArrayBuffer} input
      * @param {Object[]} args
      * @returns {ArrayBuffer}
+     *
+     * @throws {OperationError} if invalid input
      */
     run(input, args) {
         const start = args[0],
@@ -52,7 +55,7 @@ class DropBytes extends Operation {
             applyToEachLine = args[2];
 
         if (start < 0 || length < 0)
-            throw "Error: Invalid value";
+            throw new OperationError("Error: Invalid value");
 
         if (!applyToEachLine) {
             const left = input.slice(0, start),

+ 96 - 0
src/core/operations/Entropy.mjs

@@ -0,0 +1,96 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+import Utils from "../Utils";
+
+/**
+ * Entropy operation
+ */
+class Entropy extends Operation {
+
+    /**
+     * Entropy constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "Entropy";
+        this.module = "Default";
+        this.description = "Calculates the Shannon entropy of the input data which gives an idea of its randomness. 8 is the maximum.";
+        this.inputType = "byteArray";
+        this.outputType = "number";
+        this.presentType = "html";
+        this.args = [];
+    }
+
+    /**
+     * @param {byteArray} input
+     * @param {Object[]} args
+     * @returns {number}
+     */
+    run(input, args) {
+        const prob = [],
+            uniques = input.unique(),
+            str = Utils.byteArrayToChars(input);
+        let i;
+
+        for (i = 0; i < uniques.length; i++) {
+            prob.push(str.count(Utils.chr(uniques[i])) / input.length);
+        }
+
+        let entropy = 0,
+            p;
+
+        for (i = 0; i < prob.length; i++) {
+            p = prob[i];
+            entropy += p * Math.log(p) / Math.log(2);
+        }
+
+        return -entropy;
+    }
+
+    /**
+     * Displays the entropy as a scale bar for web apps.
+     *
+     * @param {number} entropy
+     * @returns {html}
+     */
+    present(entropy) {
+        return `Shannon entropy: ${entropy}
+<br><canvas id='chart-area'></canvas><br>
+- 0 represents no randomness (i.e. all the bytes in the data have the same value) whereas 8, the maximum, represents a completely random string.
+- Standard English text usually falls somewhere between 3.5 and 5.
+- Properly encrypted or compressed data of a reasonable length should have an entropy of over 7.5.
+
+The following results show the entropy of chunks of the input data. Chunks with particularly high entropy could suggest encrypted or compressed sections.
+
+<br><script>
+    var canvas = document.getElementById("chart-area"),
+        parentRect = canvas.parentNode.getBoundingClientRect(),
+        entropy = ${entropy},
+        height = parentRect.height * 0.25;
+
+    canvas.width = parentRect.width * 0.95;
+    canvas.height = height > 150 ? 150 : height;
+
+    CanvasComponents.drawScaleBar(canvas, entropy, 8, [
+        {
+            label: "English text",
+            min: 3.5,
+            max: 5
+        },{
+            label: "Encrypted/compressed",
+            min: 7.5,
+            max: 8
+        }
+    ]);
+</script>`;
+    }
+
+}
+
+export default Entropy;

+ 79 - 0
src/core/operations/EscapeUnicodeCharacters.mjs

@@ -0,0 +1,79 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+
+/**
+ * Escape Unicode Characters operation
+ */
+class EscapeUnicodeCharacters extends Operation {
+
+    /**
+     * EscapeUnicodeCharacters constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "Escape Unicode Characters";
+        this.module = "Default";
+        this.description = "Converts characters to their unicode-escaped notations.<br><br>Supports the prefixes:<ul><li><code>\\u</code></li><li><code>%u</code></li><li><code>U+</code></li></ul>e.g. <code>σου</code> becomes <code>\\u03C3\\u03BF\\u03C5</code>";
+        this.inputType = "string";
+        this.outputType = "string";
+        this.args = [
+            {
+                "name": "Prefix",
+                "type": "option",
+                "value": ["\\u", "%u", "U+"]
+            },
+            {
+                "name": "Encode all chars",
+                "type": "boolean",
+                "value": false
+            },
+            {
+                "name": "Padding",
+                "type": "number",
+                "value": 4
+            },
+            {
+                "name": "Uppercase hex",
+                "type": "boolean",
+                "value": true
+            }
+        ];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {string}
+     */
+    run(input, args) {
+        const regexWhitelist = /[ -~]/i,
+            [prefix, encodeAll, padding, uppercaseHex] = args;
+
+        let output = "",
+            character = "";
+
+        for (let i = 0; i < input.length; i++) {
+            character = input[i];
+            if (!encodeAll && regexWhitelist.test(character)) {
+                // It’s a printable ASCII character so don’t escape it.
+                output += character;
+                continue;
+            }
+
+            let cp = character.codePointAt(0).toString(16);
+            if (uppercaseHex) cp = cp.toUpperCase();
+            output += prefix + cp.padStart(padding, "0");
+        }
+
+        return output;
+    }
+
+}
+
+export default EscapeUnicodeCharacters;

+ 52 - 0
src/core/operations/ExtractDates.mjs

@@ -0,0 +1,52 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+import { search } from "../lib/Extract";
+
+/**
+ * Extract dates operation
+ */
+class ExtractDates extends Operation {
+
+    /**
+     * ExtractDates constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "Extract dates";
+        this.module = "Regex";
+        this.description = "Extracts dates in the following formats<ul><li><code>yyyy-mm-dd</code></li><li><code>dd/mm/yyyy</code></li><li><code>mm/dd/yyyy</code></li></ul>Dividers can be any of /, -, . or space";
+        this.inputType = "string";
+        this.outputType = "string";
+        this.args = [
+            {
+                "name": "Display total",
+                "type": "boolean",
+                "value": false
+            }
+        ];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {string}
+     */
+    run(input, args) {
+        const displayTotal = args[0],
+            date1 = "(?:19|20)\\d\\d[- /.](?:0[1-9]|1[012])[- /.](?:0[1-9]|[12][0-9]|3[01])", // yyyy-mm-dd
+            date2 = "(?:0[1-9]|[12][0-9]|3[01])[- /.](?:0[1-9]|1[012])[- /.](?:19|20)\\d\\d", // dd/mm/yyyy
+            date3 = "(?:0[1-9]|1[012])[- /.](?:0[1-9]|[12][0-9]|3[01])[- /.](?:19|20)\\d\\d", // mm/dd/yyyy
+            regex = new RegExp(date1 + "|" + date2 + "|" + date3, "ig");
+
+        return search(input, regex, null, displayTotal);
+    }
+
+}
+
+export default ExtractDates;

+ 49 - 0
src/core/operations/ExtractDomains.mjs

@@ -0,0 +1,49 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+import { search } from "../lib/Extract";
+
+/**
+ * Extract domains operation
+ */
+class ExtractDomains extends Operation {
+
+    /**
+     * ExtractDomains constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "Extract domains";
+        this.module = "Regex";
+        this.description = "Extracts domain names.<br>Note that this will not include paths. Use <strong>Extract URLs</strong> to find entire URLs.";
+        this.inputType = "string";
+        this.outputType = "string";
+        this.args = [
+            {
+                "name": "Display total",
+                "type": "boolean",
+                "value": "Extract.DISPLAY_TOTAL"
+            }
+        ];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {string}
+     */
+    run(input, args) {
+        const displayTotal = args[0],
+            regex = /\b((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63}\b/ig;
+
+        return search(input, regex, null, displayTotal);
+    }
+
+}
+
+export default ExtractDomains;

+ 49 - 0
src/core/operations/ExtractEmailAddresses.mjs

@@ -0,0 +1,49 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+import { search } from "../lib/Extract";
+
+/**
+ * Extract email addresses operation
+ */
+class ExtractEmailAddresses extends Operation {
+
+    /**
+     * ExtractEmailAddresses constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "Extract email addresses";
+        this.module = "Regex";
+        this.description = "Extracts all email addresses from the input.";
+        this.inputType = "string";
+        this.outputType = "string";
+        this.args = [
+            {
+                "name": "Display total",
+                "type": "boolean",
+                "value": false
+            }
+        ];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {string}
+     */
+    run(input, args) {
+        const displayTotal = args[0],
+            regex = /\b\w[-.\w]*@[-\w]+(?:\.[-\w]+)*\.[A-Z]{2,4}\b/ig;
+
+        return search(input, regex, null, displayTotal);
+    }
+
+}
+
+export default ExtractEmailAddresses;

+ 78 - 0
src/core/operations/ExtractFilePaths.mjs

@@ -0,0 +1,78 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+import { search } from "../lib/Extract";
+
+/**
+ * Extract file paths operation
+ */
+class ExtractFilePaths extends Operation {
+
+    /**
+     * ExtractFilePaths constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "Extract file paths";
+        this.module = "Regex";
+        this.description = "Extracts anything that looks like a Windows or UNIX file path.<br><br>Note that if UNIX is selected, there will likely be a lot of false positives.";
+        this.inputType = "string";
+        this.outputType = "string";
+        this.args = [
+            {
+                "name": "Windows",
+                "type": "boolean",
+                "value": true
+            },
+            {
+                "name": "UNIX",
+                "type": "boolean",
+                "value": true
+            },
+            {
+                "name": "Display total",
+                "type": "boolean",
+                "value": false
+            }
+        ];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {string}
+     */
+    run(input, args) {
+        const [includeWinPath, includeUnixPath, displayTotal] = args,
+            winDrive = "[A-Z]:\\\\",
+            winName = "[A-Z\\d][A-Z\\d\\- '_\\(\\)~]{0,61}",
+            winExt = "[A-Z\\d]{1,6}",
+            winPath = winDrive + "(?:" + winName + "\\\\?)*" + winName +
+                "(?:\\." + winExt + ")?",
+            unixPath = "(?:/[A-Z\\d.][A-Z\\d\\-.]{0,61})+";
+        let filePaths = "";
+
+        if (includeWinPath && includeUnixPath) {
+            filePaths = winPath + "|" + unixPath;
+        } else if (includeWinPath) {
+            filePaths = winPath;
+        } else if (includeUnixPath) {
+            filePaths = unixPath;
+        }
+
+        if (filePaths) {
+            const regex = new RegExp(filePaths, "ig");
+            return search(input, regex, null, displayTotal);
+        } else {
+            return "";
+        }
+    }
+
+}
+
+export default ExtractFilePaths;

+ 91 - 0
src/core/operations/ExtractIPAddresses.mjs

@@ -0,0 +1,91 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+import { search } from "../lib/Extract";
+
+/**
+ * Extract IP addresses operation
+ */
+class ExtractIPAddresses extends Operation {
+
+    /**
+     * ExtractIPAddresses constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "Extract IP addresses";
+        this.module = "Regex";
+        this.description = "Extracts all IPv4 and IPv6 addresses.<br><br>Warning: Given a string <code>710.65.0.456</code>, this will match <code>10.65.0.45</code> so always check the original input!";
+        this.inputType = "string";
+        this.outputType = "string";
+        this.args = [
+            {
+                "name": "IPv4",
+                "type": "boolean",
+                "value": true
+            },
+            {
+                "name": "IPv6",
+                "type": "boolean",
+                "value": false
+            },
+            {
+                "name": "Remove local IPv4 addresses",
+                "type": "boolean",
+                "value": false
+            },
+            {
+                "name": "Display total",
+                "type": "boolean",
+                "value": false
+            }
+        ];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {string}
+     */
+    run(input, args) {
+        const [includeIpv4, includeIpv6, removeLocal, displayTotal] = 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})";
+        let ips  = "";
+
+        if (includeIpv4 && includeIpv6) {
+            ips = ipv4 + "|" + ipv6;
+        } else if (includeIpv4) {
+            ips = ipv4;
+        } else if (includeIpv6) {
+            ips = ipv6;
+        }
+
+        if (ips) {
+            const regex = new RegExp(ips, "ig");
+
+            if (removeLocal) {
+                const ten = "10\\..+",
+                    oneninetwo = "192\\.168\\..+",
+                    oneseventwo = "172\\.(?:1[6-9]|2\\d|3[01])\\..+",
+                    onetwoseven = "127\\..+",
+                    removeRegex = new RegExp("^(?:" + ten + "|" + oneninetwo +
+                        "|" + oneseventwo + "|" + onetwoseven + ")");
+
+                return search(input, regex, removeRegex, displayTotal);
+            } else {
+                return search(input, regex, null, displayTotal);
+            }
+        } else {
+            return "";
+        }
+    }
+
+}
+
+export default ExtractIPAddresses;

+ 49 - 0
src/core/operations/ExtractMACAddresses.mjs

@@ -0,0 +1,49 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+import { search } from "../lib/Extract";
+
+/**
+ * Extract MAC addresses operation
+ */
+class ExtractMACAddresses extends Operation {
+
+    /**
+     * ExtractMACAddresses constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "Extract MAC addresses";
+        this.module = "Regex";
+        this.description = "Extracts all Media Access Control (MAC) addresses from the input.";
+        this.inputType = "string";
+        this.outputType = "string";
+        this.args = [
+            {
+                "name": "Display total",
+                "type": "boolean",
+                "value": false
+            }
+        ];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {string}
+     */
+    run(input, args) {
+        const displayTotal = args[0],
+            regex = /[A-F\d]{2}(?:[:-][A-F\d]{2}){5}/ig;
+
+        return search(input, regex, null, displayTotal);
+    }
+
+}
+
+export default ExtractMACAddresses;

+ 55 - 0
src/core/operations/ExtractURLs.mjs

@@ -0,0 +1,55 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+import { search } from "../lib/Extract";
+
+/**
+ * Extract URLs operation
+ */
+class ExtractURLs extends Operation {
+
+    /**
+     * ExtractURLs constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "Extract URLs";
+        this.module = "Regex";
+        this.description = "Extracts Uniform Resource Locators (URLs) from the input. The protocol (http, ftp etc.) is required otherwise there will be far too many false positives.";
+        this.inputType = "string";
+        this.outputType = "string";
+        this.args = [
+            {
+                "name": "Display total",
+                "type": "boolean",
+                "value": false
+            }
+        ];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {string}
+     */
+    run(input, args) {
+        const displayTotal = args[0],
+            protocol = "[A-Z]+://",
+            hostname = "[-\\w]+(?:\\.\\w[-\\w]*)+",
+            port = ":\\d+";
+        let path = "/[^.!,?\"<>\\[\\]{}\\s\\x7F-\\xFF]*";
+
+        path += "(?:[.!,?]+[^.!,?\"<>\\[\\]{}\\s\\x7F-\\xFF]+)*";
+        const regex = new RegExp(protocol + hostname + "(?:" + port +
+            ")?(?:" + path + ")?", "ig");
+        return search(input, regex, null, displayTotal);
+    }
+
+}
+
+export default ExtractURLs;

+ 2 - 1
src/core/operations/Filter.mjs

@@ -7,6 +7,7 @@
 import Operation from "../Operation";
 import Utils from "../Utils";
 import {INPUT_DELIM_OPTIONS} from "../lib/Delim";
+import OperationError from "../errors/OperationError";
 
 /**
  * Filter operation
@@ -56,7 +57,7 @@ class Filter extends Operation {
         try {
             regex = new RegExp(args[1]);
         } catch (err) {
-            return "Invalid regex. Details: " + err.message;
+            throw new OperationError(`Invalid regex. Details: ${err.message}`);
         }
 
         const regexFilter = function(value) {

+ 110 - 0
src/core/operations/FrequencyDistribution.mjs

@@ -0,0 +1,110 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+import Utils from "../Utils";
+import OperationError from "../errors/OperationError";
+
+/**
+ * Frequency distribution operation
+ */
+class FrequencyDistribution extends Operation {
+
+    /**
+     * FrequencyDistribution constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "Frequency distribution";
+        this.module = "Default";
+        this.description = "Displays the distribution of bytes in the data as a graph.";
+        this.inputType = "ArrayBuffer";
+        this.outputType = "json";
+        this.presentType = "html";
+        this.args = [
+            {
+                "name": "Show 0%s",
+                "type": "boolean",
+                "value": "Entropy.FREQ_ZEROS"
+            }
+        ];
+    }
+
+    /**
+     * @param {ArrayBuffer} input
+     * @param {Object[]} args
+     * @returns {json}
+     */
+    run(input, args) {
+        const data = new Uint8Array(input);
+        if (!data.length) throw new OperationError("No data");
+
+        const distrib = new Array(256).fill(0),
+            percentages = new Array(256),
+            len = data.length;
+        let i;
+
+        // Count bytes
+        for (i = 0; i < len; i++) {
+            distrib[data[i]]++;
+        }
+
+        // Calculate percentages
+        let repr = 0;
+        for (i = 0; i < 256; i++) {
+            if (distrib[i] > 0) repr++;
+            percentages[i] = distrib[i] / len * 100;
+        }
+
+        return {
+            "dataLength": len,
+            "percentages": percentages,
+            "distribution": distrib,
+            "bytesRepresented": repr
+        };
+    }
+
+    /**
+     * Displays the frequency distribution as a bar chart for web apps.
+     *
+     * @param {json} freq
+     * @returns {html}
+     */
+    present(freq, args) {
+        const showZeroes = args[0];
+        // Print
+        let output = `<canvas id='chart-area'></canvas><br>
+Total data length: ${freq.dataLength}
+Number of bytes represented: ${freq.bytesRepresented}
+Number of bytes not represented: ${256 - freq.bytesRepresented}
+
+Byte   Percentage
+<script>
+    var canvas = document.getElementById("chart-area"),
+        parentRect = canvas.parentNode.getBoundingClientRect(),
+        scores = ${JSON.stringify(freq.percentages)};
+
+    canvas.width = parentRect.width * 0.95;
+    canvas.height = parentRect.height * 0.9;
+
+    CanvasComponents.drawBarChart(canvas, scores, "Byte", "Frequency %", 16, 6);
+</script>`;
+
+        for (let i = 0; i < 256; i++) {
+            if (freq.distribution[i] || showZeroes) {
+                output += " " + Utils.hex(i, 2) + "    (" +
+                    (freq.percentages[i].toFixed(2).replace(".00", "") + "%)").padEnd(8, " ") +
+                    Array(Math.ceil(freq.percentages[i])+1).join("|") + "\n";
+            }
+        }
+
+        return output;
+    }
+
+}
+
+export default FrequencyDistribution;

+ 115 - 0
src/core/operations/FromBCD.mjs

@@ -0,0 +1,115 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2017
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+import Utils from "../Utils";
+import OperationError from "../errors/OperationError";
+import {ENCODING_SCHEME, ENCODING_LOOKUP, FORMAT} from "../lib/BCD";
+import BigNumber from "bignumber.js";
+
+/**
+ * From BCD operation
+ */
+class FromBCD extends Operation {
+
+    /**
+     * FromBCD constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "From BCD";
+        this.module = "Default";
+        this.description = "Binary-Coded Decimal (BCD) is a class of binary encodings of decimal numbers where each decimal digit is represented by a fixed number of bits, usually four or eight. Special bit patterns are sometimes used for a sign.";
+        this.inputType = "string";
+        this.outputType = "BigNumber";
+        this.args = [
+            {
+                "name": "Scheme",
+                "type": "option",
+                "value": ENCODING_SCHEME
+            },
+            {
+                "name": "Packed",
+                "type": "boolean",
+                "value": true
+            },
+            {
+                "name": "Signed",
+                "type": "boolean",
+                "value": false
+            },
+            {
+                "name": "Input format",
+                "type": "option",
+                "value": FORMAT
+            }
+        ];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {BigNumber}
+     */
+    run(input, args) {
+        const encoding = ENCODING_LOOKUP[args[0]],
+            packed = args[1],
+            signed = args[2],
+            inputFormat = args[3],
+            nibbles = [];
+
+        let output = "",
+            byteArray;
+
+        // Normalise the input
+        switch (inputFormat) {
+            case "Nibbles":
+            case "Bytes":
+                input = input.replace(/\s/g, "");
+                for (let i = 0; i < input.length; i += 4) {
+                    nibbles.push(parseInt(input.substr(i, 4), 2));
+                }
+                break;
+            case "Raw":
+            default:
+                byteArray = Utils.strToByteArray(input);
+                byteArray.forEach(b => {
+                    nibbles.push(b >>> 4);
+                    nibbles.push(b & 15);
+                });
+                break;
+        }
+
+        if (!packed) {
+            // Discard each high nibble
+            for (let i = 0; i < nibbles.length; i++) {
+                nibbles.splice(i, 1);
+            }
+        }
+
+        if (signed) {
+            const sign = nibbles.pop();
+            if (sign === 13 ||
+                sign === 11) {
+                // Negative
+                output += "-";
+            }
+        }
+
+        nibbles.forEach(n => {
+            if (isNaN(n)) throw new OperationError("Invalid input");
+            const val = encoding.indexOf(n);
+            if (val < 0) throw new OperationError(`Value ${Utils.bin(n, 4)} is not in the encoding scheme`);
+            output += val.toString();
+        });
+
+        return new BigNumber(output);
+    }
+
+}
+
+export default FromBCD;

+ 63 - 0
src/core/operations/FromBase.mjs

@@ -0,0 +1,63 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+import BigNumber from "bignumber.js";
+import OperationError from "../errors/OperationError";
+
+/**
+ * From Base operation
+ */
+class FromBase extends Operation {
+
+    /**
+     * FromBase constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "From Base";
+        this.module = "Default";
+        this.description = "Converts a number to decimal from a given numerical base.";
+        this.inputType = "string";
+        this.outputType = "BigNumber";
+        this.args = [
+            {
+                "name": "Radix",
+                "type": "number",
+                "value": 36
+            }
+        ];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {BigNumber}
+     */
+    run(input, args) {
+        const radix = args[0];
+        if (radix < 2 || radix > 36) {
+            throw new OperationError("Error: Radix argument must be between 2 and 36");
+        }
+
+        const number = input.replace(/\s/g, "").split(".");
+        let result = new BigNumber(number[0], radix) || 0;
+
+        if (number.length === 1) return result;
+
+        // Fractional part
+        for (let i = 0; i < number[1].length; i++) {
+            const digit = new BigNumber(number[1][i], radix);
+            result += digit.div(Math.pow(radix, i+1));
+        }
+
+        return result;
+    }
+
+}
+
+export default FromBase;

+ 93 - 0
src/core/operations/FromBase58.mjs

@@ -0,0 +1,93 @@
+/**
+ * @author tlwr [toby@toby.codes]
+ * @copyright Crown Copyright 2017
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+import Utils from "../Utils";
+import OperationError from "../errors/OperationError";
+import {ALPHABET_OPTIONS} from "../lib/Base58";
+
+/**
+ * From Base58 operation
+ */
+class FromBase58 extends Operation {
+
+    /**
+     * FromBase58 constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "From Base58";
+        this.module = "Default";
+        this.description = "Base58 (similar to Base64) is a notation for encoding arbitrary byte data. It differs from Base64 by removing easily misread characters (i.e. l, I, 0 and O) to improve human readability.<br><br>This operation decodes data from an ASCII string (with an alphabet of your choosing, presets included) back into its raw form.<br><br>e.g. <code>StV1DL6CwTryKyV</code> becomes <code>hello world</code><br><br>Base58 is commonly used in cryptocurrencies (Bitcoin, Ripple, etc).";
+        this.inputType = "string";
+        this.outputType = "byteArray";
+        this.args = [
+            {
+                "name": "Alphabet",
+                "type": "editableOption",
+                "value": ALPHABET_OPTIONS
+            },
+            {
+                "name": "Remove non-alphabet chars",
+                "type": "boolean",
+                "value": true
+            }
+        ];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {byteArray}
+     */
+    run(input, args) {
+        let alphabet = args[0] || ALPHABET_OPTIONS[0].value;
+        const removeNonAlphaChars = args[1] === undefined ? true : args[1],
+            result = [0];
+
+        alphabet = Utils.expandAlphRange(alphabet).join("");
+
+        if (alphabet.length !== 58 ||
+            [].unique.call(alphabet).length !== 58) {
+            throw new OperationError("Alphabet must be of length 58");
+        }
+
+        if (input.length === 0) return [];
+
+        [].forEach.call(input, function(c, charIndex) {
+            const index = alphabet.indexOf(c);
+
+            if (index === -1) {
+                if (removeNonAlphaChars) {
+                    return;
+                } else {
+                    throw new OperationError(`Char '${c}' at position ${charIndex} not in alphabet`);
+                }
+            }
+
+            let carry = result[0] * 58 + index;
+            result[0] = carry & 0xFF;
+            carry = carry >> 8;
+
+            for (let i = 1; i < result.length; i++) {
+                carry += result[i] * 58;
+                result[i] = carry & 0xFF;
+                carry = carry >> 8;
+            }
+
+            while (carry > 0) {
+                result.push(carry & 0xFF);
+                carry = carry >> 8;
+            }
+        });
+
+        return result.reverse();
+    }
+
+}
+
+export default FromBase58;

+ 4 - 1
src/core/operations/FromCharcode.mjs

@@ -7,6 +7,7 @@
 import Operation from "../Operation";
 import Utils from "../Utils";
 import {DELIM_OPTIONS} from "../lib/Delim";
+import OperationError from "../errors/OperationError";
 
 /**
  * From Charcode operation
@@ -42,6 +43,8 @@ class FromCharcode extends Operation {
      * @param {string} input
      * @param {Object[]} args
      * @returns {byteArray}
+     *
+     * @throws {OperationError} if base out of range
      */
     run(input, args) {
         const delim = Utils.charRep(args[0] || "Space"),
@@ -50,7 +53,7 @@ class FromCharcode extends Operation {
             i = 0;
 
         if (base < 2 || base > 36) {
-            throw "Error: Base argument must be between 2 and 36";
+            throw new OperationError("Error: Base argument must be between 2 and 36");
         }
 
         if (input.length === 0) {

+ 335 - 0
src/core/operations/FromHTMLEntity.mjs

@@ -0,0 +1,335 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+import Utils from "../Utils";
+
+/**
+ * From HTML Entity operation
+ */
+class FromHTMLEntity extends Operation {
+
+    /**
+     * FromHTMLEntity constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "From HTML Entity";
+        this.module = "Default";
+        this.description = "Converts HTML entities back to characters<br><br>e.g. <code>&amp;<span>amp;</span></code> becomes <code>&amp;</code>";
+        this.inputType = "string";
+        this.outputType = "string";
+        this.args = [];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {string}
+     */
+    run(input, args) {
+        const regex = /&(#?x?[a-zA-Z0-9]{1,8});/g;
+        let output = "",
+            m,
+            i = 0;
+
+        while ((m = regex.exec(input))) {
+            // Add up to match
+            for (; i < m.index;)
+                output += input[i++];
+
+            // Add match
+            const bite = entityToByte[m[1]];
+            if (bite) {
+                output += Utils.chr(bite);
+            } else if (!bite && m[1][0] === "#" && m[1].length > 1 && /^#\d{1,6}$/.test(m[1])) {
+                // Numeric entity (e.g. &#10;)
+                const num = m[1].slice(1, m[1].length);
+                output += Utils.chr(parseInt(num, 10));
+            } else if (!bite && m[1][0] === "#" && m[1].length > 3 && /^#x[\dA-F]{2,8}$/i.test(m[1])) {
+                // Hex entity (e.g. &#x3A;)
+                const hex = m[1].slice(2, m[1].length);
+                output += Utils.chr(parseInt(hex, 16));
+            } else {
+                // Not a valid entity, print as normal
+                for (; i < regex.lastIndex;)
+                    output += input[i++];
+            }
+
+            i = regex.lastIndex;
+        }
+        // Add all after final match
+        for (; i < input.length;)
+            output += input[i++];
+
+        return output;
+    }
+
+}
+
+
+/**
+ * Lookup table to translate HTML entity codes to their byte values.
+ */
+const entityToByte = {
+    "quot": 34,
+    "amp": 38,
+    "apos": 39,
+    "lt": 60,
+    "gt": 62,
+    "nbsp": 160,
+    "iexcl": 161,
+    "cent": 162,
+    "pound": 163,
+    "curren": 164,
+    "yen": 165,
+    "brvbar": 166,
+    "sect": 167,
+    "uml": 168,
+    "copy": 169,
+    "ordf": 170,
+    "laquo": 171,
+    "not": 172,
+    "shy": 173,
+    "reg": 174,
+    "macr": 175,
+    "deg": 176,
+    "plusmn": 177,
+    "sup2": 178,
+    "sup3": 179,
+    "acute": 180,
+    "micro": 181,
+    "para": 182,
+    "middot": 183,
+    "cedil": 184,
+    "sup1": 185,
+    "ordm": 186,
+    "raquo": 187,
+    "frac14": 188,
+    "frac12": 189,
+    "frac34": 190,
+    "iquest": 191,
+    "Agrave": 192,
+    "Aacute": 193,
+    "Acirc": 194,
+    "Atilde": 195,
+    "Auml": 196,
+    "Aring": 197,
+    "AElig": 198,
+    "Ccedil": 199,
+    "Egrave": 200,
+    "Eacute": 201,
+    "Ecirc": 202,
+    "Euml": 203,
+    "Igrave": 204,
+    "Iacute": 205,
+    "Icirc": 206,
+    "Iuml": 207,
+    "ETH": 208,
+    "Ntilde": 209,
+    "Ograve": 210,
+    "Oacute": 211,
+    "Ocirc": 212,
+    "Otilde": 213,
+    "Ouml": 214,
+    "times": 215,
+    "Oslash": 216,
+    "Ugrave": 217,
+    "Uacute": 218,
+    "Ucirc": 219,
+    "Uuml": 220,
+    "Yacute": 221,
+    "THORN": 222,
+    "szlig": 223,
+    "agrave": 224,
+    "aacute": 225,
+    "acirc": 226,
+    "atilde": 227,
+    "auml": 228,
+    "aring": 229,
+    "aelig": 230,
+    "ccedil": 231,
+    "egrave": 232,
+    "eacute": 233,
+    "ecirc": 234,
+    "euml": 235,
+    "igrave": 236,
+    "iacute": 237,
+    "icirc": 238,
+    "iuml": 239,
+    "eth": 240,
+    "ntilde": 241,
+    "ograve": 242,
+    "oacute": 243,
+    "ocirc": 244,
+    "otilde": 245,
+    "ouml": 246,
+    "divide": 247,
+    "oslash": 248,
+    "ugrave": 249,
+    "uacute": 250,
+    "ucirc": 251,
+    "uuml": 252,
+    "yacute": 253,
+    "thorn": 254,
+    "yuml": 255,
+    "OElig": 338,
+    "oelig": 339,
+    "Scaron": 352,
+    "scaron": 353,
+    "Yuml": 376,
+    "fnof": 402,
+    "circ": 710,
+    "tilde": 732,
+    "Alpha": 913,
+    "Beta": 914,
+    "Gamma": 915,
+    "Delta": 916,
+    "Epsilon": 917,
+    "Zeta": 918,
+    "Eta": 919,
+    "Theta": 920,
+    "Iota": 921,
+    "Kappa": 922,
+    "Lambda": 923,
+    "Mu": 924,
+    "Nu": 925,
+    "Xi": 926,
+    "Omicron": 927,
+    "Pi": 928,
+    "Rho": 929,
+    "Sigma": 931,
+    "Tau": 932,
+    "Upsilon": 933,
+    "Phi": 934,
+    "Chi": 935,
+    "Psi": 936,
+    "Omega": 937,
+    "alpha": 945,
+    "beta": 946,
+    "gamma": 947,
+    "delta": 948,
+    "epsilon": 949,
+    "zeta": 950,
+    "eta": 951,
+    "theta": 952,
+    "iota": 953,
+    "kappa": 954,
+    "lambda": 955,
+    "mu": 956,
+    "nu": 957,
+    "xi": 958,
+    "omicron": 959,
+    "pi": 960,
+    "rho": 961,
+    "sigmaf": 962,
+    "sigma": 963,
+    "tau": 964,
+    "upsilon": 965,
+    "phi": 966,
+    "chi": 967,
+    "psi": 968,
+    "omega": 969,
+    "thetasym": 977,
+    "upsih": 978,
+    "piv": 982,
+    "ensp": 8194,
+    "emsp": 8195,
+    "thinsp": 8201,
+    "zwnj": 8204,
+    "zwj": 8205,
+    "lrm": 8206,
+    "rlm": 8207,
+    "ndash": 8211,
+    "mdash": 8212,
+    "lsquo": 8216,
+    "rsquo": 8217,
+    "sbquo": 8218,
+    "ldquo": 8220,
+    "rdquo": 8221,
+    "bdquo": 8222,
+    "dagger": 8224,
+    "Dagger": 8225,
+    "bull": 8226,
+    "hellip": 8230,
+    "permil": 8240,
+    "prime": 8242,
+    "Prime": 8243,
+    "lsaquo": 8249,
+    "rsaquo": 8250,
+    "oline": 8254,
+    "frasl": 8260,
+    "euro": 8364,
+    "image": 8465,
+    "weierp": 8472,
+    "real": 8476,
+    "trade": 8482,
+    "alefsym": 8501,
+    "larr": 8592,
+    "uarr": 8593,
+    "rarr": 8594,
+    "darr": 8595,
+    "harr": 8596,
+    "crarr": 8629,
+    "lArr": 8656,
+    "uArr": 8657,
+    "rArr": 8658,
+    "dArr": 8659,
+    "hArr": 8660,
+    "forall": 8704,
+    "part": 8706,
+    "exist": 8707,
+    "empty": 8709,
+    "nabla": 8711,
+    "isin": 8712,
+    "notin": 8713,
+    "ni": 8715,
+    "prod": 8719,
+    "sum": 8721,
+    "minus": 8722,
+    "lowast": 8727,
+    "radic": 8730,
+    "prop": 8733,
+    "infin": 8734,
+    "ang": 8736,
+    "and": 8743,
+    "or": 8744,
+    "cap": 8745,
+    "cup": 8746,
+    "int": 8747,
+    "there4": 8756,
+    "sim": 8764,
+    "cong": 8773,
+    "asymp": 8776,
+    "ne": 8800,
+    "equiv": 8801,
+    "le": 8804,
+    "ge": 8805,
+    "sub": 8834,
+    "sup": 8835,
+    "nsub": 8836,
+    "sube": 8838,
+    "supe": 8839,
+    "oplus": 8853,
+    "otimes": 8855,
+    "perp": 8869,
+    "sdot": 8901,
+    "vellip": 8942,
+    "lceil": 8968,
+    "rceil": 8969,
+    "lfloor": 8970,
+    "rfloor": 8971,
+    "lang": 9001,
+    "rang": 9002,
+    "loz": 9674,
+    "spades": 9824,
+    "clubs": 9827,
+    "hearts": 9829,
+    "diams": 9830,
+};
+
+export default FromHTMLEntity;

+ 61 - 0
src/core/operations/FromQuotedPrintable.mjs

@@ -0,0 +1,61 @@
+/**
+ * Some parts taken from mimelib (http://github.com/andris9/mimelib)
+ * @author Andris Reinman
+ * @license MIT
+ *
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+
+/**
+ * From Quoted Printable operation
+ */
+class FromQuotedPrintable extends Operation {
+
+    /**
+     * FromQuotedPrintable constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "From Quoted Printable";
+        this.module = "Default";
+        this.description = "Converts QP-encoded text back to standard text.";
+        this.inputType = "string";
+        this.outputType = "byteArray";
+        this.args = [];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {byteArray}
+     */
+    run(input, args) {
+        const str = input.replace(/=(?:\r?\n|$)/g, "");
+
+        const encodedBytesCount = (str.match(/=[\da-fA-F]{2}/g) || []).length,
+            bufferLength = str.length - encodedBytesCount * 2,
+            buffer = new Array(bufferLength);
+        let chr, hex,
+            bufferPos = 0;
+
+        for (let i = 0, len = str.length; i < len; i++) {
+            chr = str.charAt(i);
+            if (chr === "=" && (hex = str.substr(i + 1, 2)) && /[\da-fA-F]{2}/.test(hex)) {
+                buffer[bufferPos++] = parseInt(hex, 16);
+                i += 2;
+                continue;
+            }
+            buffer[bufferPos++] = chr.charCodeAt(0);
+        }
+
+        return buffer;
+    }
+
+}
+
+export default FromQuotedPrintable;

+ 4 - 1
src/core/operations/FromUNIXTimestamp.mjs

@@ -7,6 +7,7 @@
 import Operation from "../Operation";
 import moment from "moment-timezone";
 import {UNITS} from "../lib/DateTime";
+import OperationError from "../errors/OperationError";
 
 /**
  * From UNIX Timestamp operation
@@ -37,6 +38,8 @@ class FromUNIXTimestamp extends Operation {
      * @param {number} input
      * @param {Object[]} args
      * @returns {string}
+     *
+     * @throws {OperationError} if invalid unit
      */
     run(input, args) {
         const units = args[0];
@@ -57,7 +60,7 @@ class FromUNIXTimestamp extends Operation {
             d = moment(input / 1000000);
             return d.tz("UTC").format("ddd D MMMM YYYY HH:mm:ss.SSS") + " UTC";
         } else {
-            throw "Unrecognised unit";
+            throw new OperationError("Unrecognised unit");
         }
     }
 

+ 116 - 0
src/core/operations/GeneratePGPKeyPair.mjs

@@ -0,0 +1,116 @@
+/**
+ * @author tlwr [toby@toby.codes]
+ * @author Matt C [matt@artemisbot.uk]
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2017
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+import kbpgp from "kbpgp";
+import { getSubkeySize, ASP } from "../lib/PGP";
+import promisifyDefault from "es6-promisify";
+const promisify = promisifyDefault.promisify;
+
+/**
+ * Generate PGP Key Pair operation
+ */
+class GeneratePGPKeyPair extends Operation {
+
+    /**
+     * GeneratePGPKeyPair constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "Generate PGP Key Pair";
+        this.module = "PGP";
+        this.description = "Generates a new public/private PGP key pair. Supports RSA and Eliptic Curve (EC) keys.";
+        this.inputType = "string";
+        this.outputType = "string";
+        this.args = [
+            {
+                "name": "Key type",
+                "type": "option",
+                "value": ["RSA-1024", "RSA-2048", "RSA-4096", "ECC-256", "ECC-384"]
+            },
+            {
+                "name": "Password (optional)",
+                "type": "string",
+                "value": ""
+            },
+            {
+                "name": "Name (optional)",
+                "type": "string",
+                "value": ""
+            },
+            {
+                "name": "Email (optional)",
+                "type": "string",
+                "value": ""
+            }
+        ];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {string}
+     */
+    run(input, args) {
+        const [keyType, keySize] = args[0].split("-"),
+            password = args[1],
+            name = args[2],
+            email = args[3];
+        let userIdentifier = "";
+
+        if (name) userIdentifier += name;
+        if (email) userIdentifier += ` <${email}>`;
+
+        let flags = kbpgp.const.openpgp.certify_keys;
+        flags |= kbpgp.const.openpgp.sign_data;
+        flags |= kbpgp.const.openpgp.auth;
+        flags |= kbpgp.const.openpgp.encrypt_comm;
+        flags |= kbpgp.const.openpgp.encrypt_storage;
+
+        const keyGenerationOptions = {
+            userid: userIdentifier,
+            ecc: keyType === "ecc",
+            primary: {
+                "nbits": keySize,
+                "flags": flags,
+                "expire_in": 0
+            },
+            subkeys: [{
+                "nbits": getSubkeySize(keySize),
+                "flags": kbpgp.const.openpgp.sign_data,
+                "expire_in": 86400 * 365 * 8
+            }, {
+                "nbits": getSubkeySize(keySize),
+                "flags": kbpgp.const.openpgp.encrypt_comm | kbpgp.const.openpgp.encrypt_storage,
+                "expire_in": 86400 * 365 * 2
+            }],
+            asp: ASP
+        };
+
+        return new Promise(async (resolve, reject) => {
+            try {
+                const unsignedKey = await promisify(kbpgp.KeyManager.generate)(keyGenerationOptions);
+                await promisify(unsignedKey.sign.bind(unsignedKey))({});
+
+                const signedKey = unsignedKey,
+                    privateKeyExportOptions = {};
+
+                if (password) privateKeyExportOptions.passphrase = password;
+                const privateKey = await promisify(signedKey.export_pgp_private.bind(signedKey))(privateKeyExportOptions);
+                const publicKey = await promisify(signedKey.export_pgp_public.bind(signedKey))({});
+                resolve(privateKey + "\n" + publicKey.trim());
+            } catch (err) {
+                reject(`Error whilst generating key pair: ${err}`);
+            }
+        });
+    }
+
+}
+
+export default GeneratePGPKeyPair;

+ 3 - 2
src/core/operations/HammingDistance.mjs

@@ -7,6 +7,7 @@
 import Operation from "../Operation";
 import Utils from "../Utils";
 import {fromHex} from "../lib/Hex";
+import OperationError from "../errors/OperationError";
 
 /**
  * Hamming Distance operation
@@ -55,11 +56,11 @@ class HammingDistance extends Operation {
             samples = input.split(delim);
 
         if (samples.length !== 2) {
-            return "Error: You can only calculae the edit distance between 2 strings. Please ensure exactly two inputs are provided, separated by the specified delimiter.";
+            throw new OperationError("Error: You can only calculae the edit distance between 2 strings. Please ensure exactly two inputs are provided, separated by the specified delimiter.");
         }
 
         if (samples[0].length !== samples[1].length) {
-            return "Error: Both inputs must be of the same length.";
+            throw new OperationError("Error: Both inputs must be of the same length.");
         }
 
         if (inputType === "Hex") {

+ 217 - 0
src/core/operations/MicrosoftScriptDecoder.mjs

@@ -0,0 +1,217 @@
+/**
+ * @author bmwhitn [brian.m.whitney@outlook.com]
+ * @copyright Crown Copyright 2017
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+
+/**
+ * Microsoft Script Decoder operation
+ */
+class MicrosoftScriptDecoder extends Operation {
+
+    /**
+     * MicrosoftScriptDecoder constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "Microsoft Script Decoder";
+        this.module = "Default";
+        this.description = "Decodes Microsoft Encoded Script files that have been encoded with Microsoft's custom encoding. These are often VBS (Visual Basic Script) files that are encoded and renamed with a '.vbe' extention or JS (JScript) files renamed with a '.jse' extention.<br><br><b>Sample</b><br><br>Encoded:<br><code>#@~^RQAAAA==-mD~sX|:/TP{~J:+dYbxL~@!F@*@!+@*@!&amp;@*eEI@#@&amp;@#@&amp;.jm.raY 214Wv:zms/obI0xEAAA==^#~@</code><br><br>Decoded:<br><code>var my_msg = &#34;Testing <1><2><3>!&#34;;\n\nVScript.Echo(my_msg);</code>";
+        this.inputType = "string";
+        this.outputType = "string";
+        this.args = [];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {string}
+     */
+    run(input, args) {
+        const matcher = /#@~\^.{6}==(.+).{6}==\^#~@/;
+        const encodedData = matcher.exec(input);
+        if (encodedData){
+            return MicrosoftScriptDecoder._decode(encodedData[1]);
+        } else {
+            return "";
+        }
+    }
+
+    /**
+     * Decodes Microsoft Encoded Script files that can be read and executed by cscript.exe/wscript.exe.
+     * This is a conversion of a Python script that was originally created by Didier Stevens
+     * (https://DidierStevens.com).
+     *
+     * @private
+     * @param {string} data
+     * @returns {string}
+     */
+    static _decode(data) {
+        const result = [];
+        let index = -1;
+        data = data.replace(/@&/g, String.fromCharCode(10))
+            .replace(/@#/g, String.fromCharCode(13))
+            .replace(/@\*/g, ">")
+            .replace(/@!/g, "<")
+            .replace(/@\$/g, "@");
+
+        for (let i = 0; i < data.length; i++) {
+            const byte = data.charCodeAt(i);
+            let char = data.charAt(i);
+            if (byte < 128) {
+                index++;
+            }
+
+            if ((byte === 9 || byte > 31 && byte < 128) &&
+                byte !== 60 &&
+                byte !== 62 &&
+                byte !== 64) {
+                char = D_DECODE[byte].charAt(D_COMBINATION[index % 64]);
+            }
+            result.push(char);
+        }
+        return result.join("");
+    }
+
+}
+
+const D_DECODE = [
+    "",
+    "",
+    "",
+    "",
+    "",
+    "",
+    "",
+    "",
+    "",
+    "\x57\x6E\x7B",
+    "\x4A\x4C\x41",
+    "\x0B\x0B\x0B",
+    "\x0C\x0C\x0C",
+    "\x4A\x4C\x41",
+    "\x0E\x0E\x0E",
+    "\x0F\x0F\x0F",
+    "\x10\x10\x10",
+    "\x11\x11\x11",
+    "\x12\x12\x12",
+    "\x13\x13\x13",
+    "\x14\x14\x14",
+    "\x15\x15\x15",
+    "\x16\x16\x16",
+    "\x17\x17\x17",
+    "\x18\x18\x18",
+    "\x19\x19\x19",
+    "\x1A\x1A\x1A",
+    "\x1B\x1B\x1B",
+    "\x1C\x1C\x1C",
+    "\x1D\x1D\x1D",
+    "\x1E\x1E\x1E",
+    "\x1F\x1F\x1F",
+    "\x2E\x2D\x32",
+    "\x47\x75\x30",
+    "\x7A\x52\x21",
+    "\x56\x60\x29",
+    "\x42\x71\x5B",
+    "\x6A\x5E\x38",
+    "\x2F\x49\x33",
+    "\x26\x5C\x3D",
+    "\x49\x62\x58",
+    "\x41\x7D\x3A",
+    "\x34\x29\x35",
+    "\x32\x36\x65",
+    "\x5B\x20\x39",
+    "\x76\x7C\x5C",
+    "\x72\x7A\x56",
+    "\x43\x7F\x73",
+    "\x38\x6B\x66",
+    "\x39\x63\x4E",
+    "\x70\x33\x45",
+    "\x45\x2B\x6B",
+    "\x68\x68\x62",
+    "\x71\x51\x59",
+    "\x4F\x66\x78",
+    "\x09\x76\x5E",
+    "\x62\x31\x7D",
+    "\x44\x64\x4A",
+    "\x23\x54\x6D",
+    "\x75\x43\x71",
+    "\x4A\x4C\x41",
+    "\x7E\x3A\x60",
+    "\x4A\x4C\x41",
+    "\x5E\x7E\x53",
+    "\x40\x4C\x40",
+    "\x77\x45\x42",
+    "\x4A\x2C\x27",
+    "\x61\x2A\x48",
+    "\x5D\x74\x72",
+    "\x22\x27\x75",
+    "\x4B\x37\x31",
+    "\x6F\x44\x37",
+    "\x4E\x79\x4D",
+    "\x3B\x59\x52",
+    "\x4C\x2F\x22",
+    "\x50\x6F\x54",
+    "\x67\x26\x6A",
+    "\x2A\x72\x47",
+    "\x7D\x6A\x64",
+    "\x74\x39\x2D",
+    "\x54\x7B\x20",
+    "\x2B\x3F\x7F",
+    "\x2D\x38\x2E",
+    "\x2C\x77\x4C",
+    "\x30\x67\x5D",
+    "\x6E\x53\x7E",
+    "\x6B\x47\x6C",
+    "\x66\x34\x6F",
+    "\x35\x78\x79",
+    "\x25\x5D\x74",
+    "\x21\x30\x43",
+    "\x64\x23\x26",
+    "\x4D\x5A\x76",
+    "\x52\x5B\x25",
+    "\x63\x6C\x24",
+    "\x3F\x48\x2B",
+    "\x7B\x55\x28",
+    "\x78\x70\x23",
+    "\x29\x69\x41",
+    "\x28\x2E\x34",
+    "\x73\x4C\x09",
+    "\x59\x21\x2A",
+    "\x33\x24\x44",
+    "\x7F\x4E\x3F",
+    "\x6D\x50\x77",
+    "\x55\x09\x3B",
+    "\x53\x56\x55",
+    "\x7C\x73\x69",
+    "\x3A\x35\x61",
+    "\x5F\x61\x63",
+    "\x65\x4B\x50",
+    "\x46\x58\x67",
+    "\x58\x3B\x51",
+    "\x31\x57\x49",
+    "\x69\x22\x4F",
+    "\x6C\x6D\x46",
+    "\x5A\x4D\x68",
+    "\x48\x25\x7C",
+    "\x27\x28\x36",
+    "\x5C\x46\x70",
+    "\x3D\x4A\x6E",
+    "\x24\x32\x7A",
+    "\x79\x41\x2F",
+    "\x37\x3D\x5F",
+    "\x60\x5F\x4B",
+    "\x51\x4F\x5A",
+    "\x20\x42\x2C",
+    "\x36\x65\x57"
+];
+
+const D_COMBINATION = [
+    0, 1, 2, 0, 1, 2, 1, 2, 2, 1, 2, 1, 0, 2, 1, 2, 0, 2, 1, 2, 0, 0, 1, 2, 2, 1, 0, 2, 1, 2, 2, 1,
+    0, 0, 2, 1, 2, 1, 2, 0, 2, 0, 0, 1, 2, 0, 2, 1, 0, 2, 1, 2, 0, 0, 1, 2, 2, 0, 0, 1, 2, 0, 2, 1
+];
+
+export default MicrosoftScriptDecoder;

+ 2 - 1
src/core/operations/OffsetChecker.mjs

@@ -6,6 +6,7 @@
 
 import Operation from "../Operation";
 import Utils from "../Utils";
+import OperationError from "../errors/OperationError";
 
 /**
  * Offset checker operation
@@ -48,7 +49,7 @@ class OffsetChecker extends Operation {
             chr;
 
         if (!samples || samples.length < 2) {
-            return "Not enough samples, perhaps you need to modify the sample delimiter or add more data?";
+            throw new OperationError("Not enough samples, perhaps you need to modify the sample delimiter or add more data?");
         }
 
         // Initialise output strings

+ 86 - 0
src/core/operations/PGPDecrypt.mjs

@@ -0,0 +1,86 @@
+/**
+ * @author tlwr [toby@toby.codes]
+ * @copyright Crown Copyright 2017
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+import kbpgp from "kbpgp";
+import { ASP, importPrivateKey } from "../lib/PGP";
+import OperationError from "../errors/OperationError";
+import promisifyDefault from "es6-promisify";
+const promisify = promisifyDefault.promisify;
+
+/**
+ * PGP Decrypt operation
+ */
+class PGPDecrypt extends Operation {
+
+    /**
+     * PGPDecrypt constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "PGP Decrypt";
+        this.module = "PGP";
+        this.description = [
+            "Input: the ASCII-armoured PGP message you want to decrypt.",
+            "<br><br>",
+            "Arguments: the ASCII-armoured PGP private key of the recipient, ",
+            "(and the private key password if necessary).",
+            "<br><br>",
+            "Pretty Good Privacy is an encryption standard (OpenPGP) used for encrypting, decrypting, and signing messages.",
+            "<br><br>",
+            "This function uses the Keybase implementation of PGP.",
+        ].join("\n");
+        this.inputType = "string";
+        this.outputType = "string";
+        this.args = [
+            {
+                "name": "Private key of recipient",
+                "type": "text",
+                "value": ""
+            },
+            {
+                "name": "Private key passphrase",
+                "type": "string",
+                "value": ""
+            }
+        ];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {string}
+     *
+     * @throws {OperationError} if invalid private key
+     */
+    async run(input, args) {
+        const encryptedMessage = input,
+            [privateKey, passphrase] = args,
+            keyring = new kbpgp.keyring.KeyRing();
+        let plaintextMessage;
+
+        if (!privateKey) throw new OperationError("Enter the private key of the recipient.");
+
+        const key = await importPrivateKey(privateKey, passphrase);
+        keyring.add_key_manager(key);
+
+        try {
+            plaintextMessage = await promisify(kbpgp.unbox)({
+                armored: encryptedMessage,
+                keyfetch: keyring,
+                asp: ASP
+            });
+        } catch (err) {
+            throw new OperationError(`Couldn't decrypt message with provided private key: ${err}`);
+        }
+
+        return plaintextMessage.toString();
+    }
+
+}
+
+export default PGPDecrypt;

+ 122 - 0
src/core/operations/PGPDecryptAndVerify.mjs

@@ -0,0 +1,122 @@
+/**
+ * @author tlwr [toby@toby.codes]
+ * @copyright Crown Copyright 2017
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+import kbpgp from "kbpgp";
+import { ASP, importPrivateKey, importPublicKey } from "../lib/PGP";
+import OperationError from "../errors/OperationError";
+import promisifyDefault from "es6-promisify";
+const promisify = promisifyDefault.promisify;
+
+/**
+ * PGP Decrypt and Verify operation
+ */
+class PGPDecryptAndVerify extends Operation {
+
+    /**
+     * PGPDecryptAndVerify constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "PGP Decrypt and Verify";
+        this.module = "PGP";
+        this.description = [
+            "Input: the ASCII-armoured encrypted PGP message you want to verify.",
+            "<br><br>",
+            "Arguments: the ASCII-armoured PGP public key of the signer, ",
+            "the ASCII-armoured private key of the recipient (and the private key password if necessary).",
+            "<br><br>",
+            "This operation uses PGP to decrypt and verify an encrypted digital signature.",
+            "<br><br>",
+            "Pretty Good Privacy is an encryption standard (OpenPGP) used for encrypting, decrypting, and signing messages.",
+            "<br><br>",
+            "This function uses the Keybase implementation of PGP.",
+        ].join("\n");
+        this.inputType = "string";
+        this.outputType = "string";
+        this.args = [
+            {
+                "name": "Public key of signer",
+                "type": "text",
+                "value": ""
+            },
+            {
+                "name": "Private key of recipient",
+                "type": "text",
+                "value": ""
+            },
+            {
+                "name": "Private key password",
+                "type": "string",
+                "value": ""
+            }
+        ];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {string}
+     */
+    async run(input, args) {
+        const signedMessage = input,
+            [publicKey, privateKey, passphrase] = args,
+            keyring = new kbpgp.keyring.KeyRing();
+        let unboxedLiterals;
+
+        if (!publicKey) throw new OperationError("Enter the public key of the signer.");
+        if (!privateKey) throw new OperationError("Enter the private key of the recipient.");
+        const privKey = await importPrivateKey(privateKey, passphrase);
+        const pubKey = await importPublicKey(publicKey);
+        keyring.add_key_manager(privKey);
+        keyring.add_key_manager(pubKey);
+
+        try {
+            unboxedLiterals = await promisify(kbpgp.unbox)({
+                armored: signedMessage,
+                keyfetch: keyring,
+                asp: ASP
+            });
+            const ds = unboxedLiterals[0].get_data_signer();
+            if (ds) {
+                const km = ds.get_key_manager();
+                if (km) {
+                    const signer = km.get_userids_mark_primary()[0].components;
+                    let text = "Signed by ";
+                    if (signer.email || signer.username || signer.comment) {
+                        if (signer.username) {
+                            text += `${signer.username} `;
+                        }
+                        if (signer.comment) {
+                            text += `${signer.comment} `;
+                        }
+                        if (signer.email) {
+                            text += `<${signer.email}>`;
+                        }
+                        text += "\n";
+                    }
+                    text += [
+                        `PGP fingerprint: ${km.get_pgp_fingerprint().toString("hex")}`,
+                        `Signed on ${new Date(ds.sig.hashed_subpackets[0].time * 1000).toUTCString()}`,
+                        "----------------------------------\n"
+                    ].join("\n");
+                    text += unboxedLiterals.toString();
+                    return text.trim();
+                } else {
+                    throw new OperationError("Could not identify a key manager.");
+                }
+            } else {
+                throw new OperationError("The data does not appear to be signed.");
+            }
+        } catch (err) {
+            throw new OperationError(`Couldn't verify message: ${err}`);
+        }
+    }
+
+}
+
+export default PGPDecryptAndVerify;

+ 85 - 0
src/core/operations/PGPEncrypt.mjs

@@ -0,0 +1,85 @@
+/**
+ * @author tlwr [toby@toby.codes]
+ * @copyright Crown Copyright 2017
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+import kbpgp from "kbpgp";
+import { ASP } from "../lib/PGP";
+import OperationError from "../errors/OperationError";
+import promisifyDefault from "es6-promisify";
+const promisify = promisifyDefault.promisify;
+
+/**
+ * PGP Encrypt operation
+ */
+class PGPEncrypt extends Operation {
+
+    /**
+     * PGPEncrypt constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "PGP Encrypt";
+        this.module = "PGP";
+        this.description = [
+            "Input: the message you want to encrypt.",
+            "<br><br>",
+            "Arguments: the ASCII-armoured PGP public key of the recipient.",
+            "<br><br>",
+            "Pretty Good Privacy is an encryption standard (OpenPGP) used for encrypting, decrypting, and signing messages.",
+            "<br><br>",
+            "This function uses the Keybase implementation of PGP.",
+        ].join("\n");
+        this.inputType = "string";
+        this.outputType = "string";
+        this.args = [
+            {
+                "name": "Public key of recipient",
+                "type": "text",
+                "value": ""
+            }
+        ];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {string}
+     *
+     * @throws {OperationError} if failed private key import or failed encryption
+     */
+    async run(input, args) {
+        const plaintextMessage = input,
+            plainPubKey = args[0];
+        let key,
+            encryptedMessage;
+
+        if (!plainPubKey) throw new OperationError("Enter the public key of the recipient.");
+
+        try {
+            key = await promisify(kbpgp.KeyManager.import_from_armored_pgp)({
+                armored: plainPubKey,
+            });
+        } catch (err) {
+            throw new OperationError(`Could not import public key: ${err}`);
+        }
+
+        try {
+            encryptedMessage = await promisify(kbpgp.box)({
+                "msg": plaintextMessage,
+                "encrypt_for": key,
+                "asp": ASP
+            });
+        } catch (err) {
+            throw new OperationError(`Couldn't encrypt message with provided public key: ${err}`);
+        }
+
+        return encryptedMessage.toString();
+    }
+
+}
+
+export default PGPEncrypt;

+ 93 - 0
src/core/operations/PGPEncryptAndSign.mjs

@@ -0,0 +1,93 @@
+/**
+ * @author tlwr [toby@toby.codes]
+ * @copyright Crown Copyright 2017
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+import kbpgp from "kbpgp";
+import { ASP, importPrivateKey, importPublicKey } from "../lib/PGP";
+import OperationError from "../errors/OperationError";
+import promisifyDefault from "es6-promisify";
+const promisify = promisifyDefault.promisify;
+
+/**
+ * PGP Encrypt and Sign operation
+ */
+class PGPEncryptAndSign extends Operation {
+
+    /**
+     * PGPEncryptAndSign constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "PGP Encrypt and Sign";
+        this.module = "PGP";
+        this.description = [
+            "Input: the cleartext you want to sign.",
+            "<br><br>",
+            "Arguments: the ASCII-armoured private key of the signer (plus the private key password if necessary)",
+            "and the ASCII-armoured PGP public key of the recipient.",
+            "<br><br>",
+            "This operation uses PGP to produce an encrypted digital signature.",
+            "<br><br>",
+            "Pretty Good Privacy is an encryption standard (OpenPGP) used for encrypting, decrypting, and signing messages.",
+            "<br><br>",
+            "This function uses the Keybase implementation of PGP.",
+        ].join("\n");
+        this.inputType = "string";
+        this.outputType = "string";
+        this.args = [
+            {
+                "name": "Private key of signer",
+                "type": "text",
+                "value": ""
+            },
+            {
+                "name": "Private key passphrase",
+                "type": "string",
+                "value": ""
+            },
+            {
+                "name": "Public key of recipient",
+                "type": "text",
+                "value": ""
+            }
+        ];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {string}
+     *
+     * @throws {OperationError} if failure to sign message
+     */
+    async run(input, args) {
+        const message = input,
+            [privateKey, passphrase, publicKey] = args;
+        let signedMessage;
+
+        if (!privateKey) throw new OperationError("Enter the private key of the signer.");
+        if (!publicKey) throw new OperationError("Enter the public key of the recipient.");
+        const privKey = await importPrivateKey(privateKey, passphrase);
+        const pubKey = await importPublicKey(publicKey);
+
+        try {
+            signedMessage = await promisify(kbpgp.box)({
+                "msg": message,
+                "encrypt_for": pubKey,
+                "sign_with": privKey,
+                "asp": ASP
+            });
+        } catch (err) {
+            throw new OperationError(`Couldn't sign message: ${err}`);
+        }
+
+        return signedMessage;
+    }
+
+}
+
+export default PGPEncryptAndSign;

+ 195 - 0
src/core/operations/ParseColourCode.mjs

@@ -0,0 +1,195 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+
+/**
+ * Parse colour code operation
+ */
+class ParseColourCode extends Operation {
+
+    /**
+     * ParseColourCode constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "Parse colour code";
+        this.module = "Default";
+        this.description = "Converts a colour code in a standard format to other standard formats and displays the colour itself.<br><br><strong>Example inputs</strong><ul><li><code>#d9edf7</code></li><li><code>rgba(217,237,247,1)</code></li><li><code>hsla(200,65%,91%,1)</code></li><li><code>cmyk(0.12, 0.04, 0.00, 0.03)</code></li></ul>";
+        this.inputType = "string";
+        this.outputType = "html";
+        this.args = [];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {html}
+     */
+    run(input, args) {
+        let m = null,
+            r = 0, g = 0, b = 0, a = 1;
+
+        // Read in the input
+        if ((m = input.match(/#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})/i))) {
+            // Hex - #d9edf7
+            r = parseInt(m[1], 16);
+            g = parseInt(m[2], 16);
+            b = parseInt(m[3], 16);
+        } else if ((m = input.match(/rgba?\((\d{1,3}(?:\.\d+)?),\s?(\d{1,3}(?:\.\d+)?),\s?(\d{1,3}(?:\.\d+)?)(?:,\s?(\d(?:\.\d+)?))?\)/i))) {
+            // RGB or RGBA - rgb(217,237,247) or rgba(217,237,247,1)
+            r = parseFloat(m[1]);
+            g = parseFloat(m[2]);
+            b = parseFloat(m[3]);
+            a = m[4] ? parseFloat(m[4]) : 1;
+        } else if ((m = input.match(/hsla?\((\d{1,3}(?:\.\d+)?),\s?(\d{1,3}(?:\.\d+)?)%,\s?(\d{1,3}(?:\.\d+)?)%(?:,\s?(\d(?:\.\d+)?))?\)/i))) {
+            // HSL or HSLA - hsl(200, 65%, 91%) or hsla(200, 65%, 91%, 1)
+            const h_ = parseFloat(m[1]) / 360,
+                s_ = parseFloat(m[2]) / 100,
+                l_ = parseFloat(m[3]) / 100,
+                rgb_ = ParseColourCode._hslToRgb(h_, s_, l_);
+
+            r = rgb_[0];
+            g = rgb_[1];
+            b = rgb_[2];
+            a = m[4] ? parseFloat(m[4]) : 1;
+        } else if ((m = input.match(/cmyk\((\d(?:\.\d+)?),\s?(\d(?:\.\d+)?),\s?(\d(?:\.\d+)?),\s?(\d(?:\.\d+)?)\)/i))) {
+            // CMYK - cmyk(0.12, 0.04, 0.00, 0.03)
+            const c_ = parseFloat(m[1]),
+                m_ = parseFloat(m[2]),
+                y_ = parseFloat(m[3]),
+                k_ = parseFloat(m[4]);
+
+            r = Math.round(255 * (1 - c_) * (1 - k_));
+            g = Math.round(255 * (1 - m_) * (1 - k_));
+            b = Math.round(255 * (1 - y_) * (1 - k_));
+        }
+
+        const hsl_ = ParseColourCode._rgbToHsl(r, g, b),
+            h = Math.round(hsl_[0] * 360),
+            s = Math.round(hsl_[1] * 100),
+            l = Math.round(hsl_[2] * 100);
+        let k = 1 - Math.max(r/255, g/255, b/255),
+            c = (1 - r/255 - k) / (1 - k),
+            y = (1 - b/255 - k) / (1 - k);
+
+        m = (1 - g/255 - k) / (1 - k);
+
+        c = isNaN(c) ? "0" : c.toFixed(2);
+        m = isNaN(m) ? "0" : m.toFixed(2);
+        y = isNaN(y) ? "0" : y.toFixed(2);
+        k = k.toFixed(2);
+
+        const hex = "#" +
+                Math.round(r).toString(16).padStart(2, "0") +
+                Math.round(g).toString(16).padStart(2, "0") +
+                Math.round(b).toString(16).padStart(2, "0"),
+            rgb  = "rgb(" + r + ", " + g + ", " + b + ")",
+            rgba = "rgba(" + r + ", " + g + ", " + b + ", " + a + ")",
+            hsl  = "hsl(" + h + ", " + s + "%, " + l + "%)",
+            hsla = "hsla(" + h + ", " + s + "%, " + l + "%, " + a + ")",
+            cmyk = "cmyk(" + c + ", " + m + ", " + y + ", " + k + ")";
+
+        // Generate output
+        return `<div id="colorpicker" style="display: inline-block"></div>
+Hex:  ${hex}
+RGB:  ${rgb}
+RGBA: ${rgba}
+HSL:  ${hsl}
+HSLA: ${hsla}
+CMYK: ${cmyk}
+<script>
+    $('#colorpicker').colorpicker({
+        format: 'rgba',
+        color: '${rgba}',
+        container: true,
+        inline: true,
+    }).on('changeColor', function(e) {
+        var color = e.color.toRGB();
+        document.getElementById('input-text').value = 'rgba(' +
+            color.r + ', ' + color.g + ', ' + color.b + ', ' + color.a + ')';
+        window.app.autoBake();
+    });
+</script>`;
+    }
+
+    /**
+     * Converts an HSL color value to RGB. Conversion formula
+     * adapted from http://en.wikipedia.org/wiki/HSL_colorSpace.
+     * Assumes h, s, and l are contained in the set [0, 1] and
+     * returns r, g, and b in the set [0, 255].
+     *
+     * @author Mohsen (http://stackoverflow.com/a/9493060)
+     *
+     * @param {number} h - The hue
+     * @param {number} s - The saturation
+     * @param {number} l - The lightness
+     * @return {Array} The RGB representation
+     */
+    static _hslToRgb(h, s, l) {
+        let r, g, b;
+
+        if (s === 0){
+            r = g = b = l; // achromatic
+        } else {
+            const hue2rgb = function hue2rgb(p, q, t) {
+                if (t < 0) t += 1;
+                if (t > 1) t -= 1;
+                if (t < 1/6) return p + (q - p) * 6 * t;
+                if (t < 1/2) return q;
+                if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
+                return p;
+            };
+
+            const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
+            const p = 2 * l - q;
+            r = hue2rgb(p, q, h + 1/3);
+            g = hue2rgb(p, q, h);
+            b = hue2rgb(p, q, h - 1/3);
+        }
+
+        return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
+    }
+
+    /**
+     * Converts an RGB color value to HSL. Conversion formula
+     * adapted from http://en.wikipedia.org/wiki/HSL_colorSpace.
+     * Assumes r, g, and b are contained in the set [0, 255] and
+     * returns h, s, and l in the set [0, 1].
+     *
+     * @author Mohsen (http://stackoverflow.com/a/9493060)
+     *
+     * @param {number} r - The red color value
+     * @param {number} g - The green color value
+     * @param {number} b - The blue color value
+     * @return {Array} The HSL representation
+     */
+    static _rgbToHsl(r, g, b) {
+        r /= 255; g /= 255; b /= 255;
+        const max = Math.max(r, g, b),
+            min = Math.min(r, g, b),
+            l = (max + min) / 2;
+        let h, s;
+
+        if (max === min) {
+            h = s = 0; // achromatic
+        } else {
+            const d = max - min;
+            s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
+            switch (max) {
+                case r: h = (g - b) / d + (g < b ? 6 : 0); break;
+                case g: h = (b - r) / d + 2; break;
+                case b: h = (r - g) / d + 4; break;
+            }
+            h /= 6;
+        }
+
+        return [h, s, l];
+    }
+}
+
+export default ParseColourCode;

+ 2 - 1
src/core/operations/ParseDateTime.mjs

@@ -7,6 +7,7 @@
 import Operation from "../Operation";
 import moment from "moment-timezone";
 import {DATETIME_FORMATS, FORMAT_EXAMPLES} from "../lib/DateTime";
+import OperationError from "../errors/OperationError";
 
 /**
  * Parse DateTime operation
@@ -59,7 +60,7 @@ class ParseDateTime extends Operation {
             date = moment.tz(input, inputFormat, inputTimezone);
             if (!date || date.format() === "Invalid date") throw Error;
         } catch (err) {
-            return "Invalid format.\n\n" + FORMAT_EXAMPLES;
+            throw new OperationError(`Invalid format.\n\n${FORMAT_EXAMPLES}`);
         }
 
         output += "Date: " + date.format("dddd Do MMMM YYYY") +

+ 2 - 1
src/core/operations/ParseUNIXFilePermissions.mjs

@@ -5,6 +5,7 @@
  */
 
 import Operation from "../Operation";
+import OperationError from "../errors/OperationError";
 
 /**
  * Parse UNIX file permissions operation
@@ -169,7 +170,7 @@ class ParseUNIXFilePermissions extends Operation {
                 }
             }
         } else {
-            return "Invalid input format.\nPlease enter the permissions in either octal (e.g. 755) or textual (e.g. drwxr-xr-x) format.";
+            throw new OperationError("Invalid input format.\nPlease enter the permissions in either octal (e.g. 755) or textual (e.g. drwxr-xr-x) format.");
         }
 
         output += "Textual representation: " + permsToStr(perms);

+ 69 - 0
src/core/operations/ParseURI.mjs

@@ -0,0 +1,69 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+import url from "url";
+
+/**
+ * Parse URI operation
+ */
+class ParseURI extends Operation {
+
+    /**
+     * ParseURI constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "Parse URI";
+        this.module = "URL";
+        this.description = "Pretty prints complicated Uniform Resource Identifier (URI) strings for ease of reading. Particularly useful for Uniform Resource Locators (URLs) with a lot of arguments.";
+        this.inputType = "string";
+        this.outputType = "string";
+        this.args = [];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {string}
+     */
+    run(input, args) {
+        const uri = url.parse(input, true);
+
+        let output = "";
+
+        if (uri.protocol) output += "Protocol:\t" + uri.protocol + "\n";
+        if (uri.auth) output += "Auth:\t\t" + uri.auth + "\n";
+        if (uri.hostname) output += "Hostname:\t" + uri.hostname + "\n";
+        if (uri.port) output += "Port:\t\t" + uri.port + "\n";
+        if (uri.pathname) output += "Path name:\t" + uri.pathname + "\n";
+        if (uri.query) {
+            const keys = Object.keys(uri.query);
+            let padding = 0;
+
+            keys.forEach(k => {
+                padding = (k.length > padding) ? k.length : padding;
+            });
+
+            output += "Arguments:\n";
+            for (const key in uri.query) {
+                output += "\t" + key.padEnd(padding, " ");
+                if (uri.query[key].length) {
+                    output += " = " + uri.query[key] + "\n";
+                } else {
+                    output += "\n";
+                }
+            }
+        }
+        if (uri.hash) output += "Hash:\t\t" + uri.hash + "\n";
+
+        return output;
+    }
+
+}
+
+export default ParseURI;

+ 2 - 1
src/core/operations/RawInflate.mjs

@@ -7,6 +7,7 @@
 import Operation from "../Operation";
 import {INFLATE_BUFFER_TYPE} from "../lib/Zlib";
 import rawinflate from "zlibjs/bin/rawinflate.min";
+import OperationError from "../errors/OperationError";
 
 const Zlib = rawinflate.Zlib;
 
@@ -90,7 +91,7 @@ class RawInflate extends Operation {
             }
 
             if (!valid) {
-                throw "Error: Unable to inflate data";
+                throw new OperationError("Error: Unable to inflate data");
             }
         }
         // This seems to be the easiest way...

+ 2 - 1
src/core/operations/ShowBase64Offsets.mjs

@@ -7,6 +7,7 @@
 import Operation from "../Operation";
 import Utils from "../Utils";
 import {fromBase64, toBase64} from "../lib/Base64";
+import OperationError from "../errors/OperationError";
 
 /**
  * Show Base64 offsets operation
@@ -58,7 +59,7 @@ class ShowBase64Offsets extends Operation {
             script = "<script type='application/javascript'>$('[data-toggle=\"tooltip\"]').tooltip()</script>";
 
         if (input.length < 1) {
-            return "Please enter a string.";
+            throw new OperationError("Please enter a string.");
         }
 
         // Highlight offset 0

+ 116 - 0
src/core/operations/Strings.mjs

@@ -0,0 +1,116 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+import XRegExp from "xregexp";
+import { search } from "../lib/Extract";
+
+/**
+ * Strings operation
+ */
+class Strings extends Operation {
+
+    /**
+     * Strings constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "Strings";
+        this.module = "Regex";
+        this.description = "Extracts all strings from the input.";
+        this.inputType = "string";
+        this.outputType = "string";
+        this.args = [
+            {
+                "name": "Encoding",
+                "type": "option",
+                "value": ["Single byte", "16-bit littleendian", "16-bit bigendian", "All"]
+            },
+            {
+                "name": "Minimum length",
+                "type": "number",
+                "value": 4
+            },
+            {
+                "name": "Match",
+                "type": "option",
+                "value": [
+                    "[ASCII]", "Alphanumeric + punctuation (A)", "All printable chars (A)", "Null-terminated strings (A)",
+                    "[Unicode]", "Alphanumeric + punctuation (U)", "All printable chars (U)", "Null-terminated strings (U)"
+                ]
+            },
+            {
+                "name": "Display total",
+                "type": "boolean",
+                "value": false
+            }
+        ];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {string}
+     */
+    run(input, args) {
+        const [encoding, minLen, matchType, displayTotal] = args,
+            alphanumeric = "A-Z\\d",
+            punctuation = "/\\-:.,_$%'\"()<>= !\\[\\]{}@",
+            printable = "\x20-\x7e",
+            uniAlphanumeric = "\\pL\\pN",
+            uniPunctuation = "\\pP\\pZ",
+            uniPrintable = "\\pL\\pM\\pZ\\pS\\pN\\pP";
+
+        let strings = "";
+
+        switch (matchType) {
+            case "Alphanumeric + punctuation (A)":
+                strings = `[${alphanumeric + punctuation}]`;
+                break;
+            case "All printable chars (A)":
+            case "Null-terminated strings (A)":
+                strings = `[${printable}]`;
+                break;
+            case "Alphanumeric + punctuation (U)":
+                strings = `[${uniAlphanumeric + uniPunctuation}]`;
+                break;
+            case "All printable chars (U)":
+            case "Null-terminated strings (U)":
+                strings = `[${uniPrintable}]`;
+                break;
+        }
+
+        // UTF-16 support is hacked in by allowing null bytes on either side of the matched chars
+        switch (encoding) {
+            case "All":
+                strings = `(\x00?${strings}\x00?)`;
+                break;
+            case "16-bit littleendian":
+                strings = `(${strings}\x00)`;
+                break;
+            case "16-bit bigendian":
+                strings = `(\x00${strings})`;
+                break;
+            case "Single byte":
+            default:
+                break;
+        }
+
+        strings = `${strings}{${minLen},}`;
+
+        if (matchType.includes("Null-terminated")) {
+            strings += "\x00";
+        }
+
+        const regex = new XRegExp(strings, "ig");
+
+        return search(input, regex, null, displayTotal);
+    }
+
+}
+
+export default Strings;

+ 65 - 0
src/core/operations/StripHTMLTags.mjs

@@ -0,0 +1,65 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+import Utils from "../Utils";
+
+/**
+ * Strip HTML tags operation
+ */
+class StripHTMLTags extends Operation {
+
+    /**
+     * StripHTMLTags constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "Strip HTML tags";
+        this.module = "Default";
+        this.description = "Removes all HTML tags from the input.";
+        this.inputType = "string";
+        this.outputType = "string";
+        this.args = [
+            {
+                "name": "Remove indentation",
+                "type": "boolean",
+                "value": true
+            },
+            {
+                "name": "Remove excess line breaks",
+                "type": "boolean",
+                "value": true
+            }
+        ];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {string}
+     */
+    run(input, args) {
+        const [removeIndentation, removeLineBreaks] = args;
+
+        input = Utils.stripHtmlTags(input);
+
+        if (removeIndentation) {
+            input = input.replace(/\n[ \f\t]+/g, "\n");
+        }
+
+        if (removeLineBreaks) {
+            input = input
+                .replace(/^\s*\n/, "") // first line
+                .replace(/(\n\s*){2,}/g, "\n"); // all others
+        }
+
+        return input;
+    }
+
+}
+
+export default StripHTMLTags;

+ 137 - 0
src/core/operations/SwapEndianness.mjs

@@ -0,0 +1,137 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+import Utils from "../Utils";
+import {toHex, fromHex} from "../lib/Hex";
+import OperationError from "../errors/OperationError";
+
+/**
+ * Swap endianness operation
+ */
+class SwapEndianness extends Operation {
+
+    /**
+     * SwapEndianness constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "Swap endianness";
+        this.module = "Default";
+        this.description = "Switches the data from big-endian to little-endian or vice-versa. Data can be read in as hexadecimal or raw bytes. It will be returned in the same format as it is entered.";
+        this.inputType = "string";
+        this.outputType = "string";
+        this.args = [
+            {
+                "name": "Data format",
+                "type": "option",
+                "value": ["Hex", "Raw"]
+            },
+            {
+                "name": "Word length (bytes)",
+                "type": "number",
+                "value": 4
+            },
+            {
+                "name": "Pad incomplete words",
+                "type": "boolean",
+                "value": true
+            }
+        ];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {string}
+     */
+    run(input, args) {
+        const [dataFormat, wordLength, padIncompleteWords] = args,
+            result = [],
+            words = [];
+        let i = 0,
+            j = 0,
+            data = [];
+
+        if (wordLength <= 0) {
+            throw new OperationError("Word length must be greater than 0");
+        }
+
+        // Convert input to raw data based on specified data format
+        switch (dataFormat) {
+            case "Hex":
+                data = fromHex(input);
+                break;
+            case "Raw":
+                data = Utils.strToByteArray(input);
+                break;
+            default:
+                data = input;
+        }
+
+        // Split up into words
+        for (i = 0; i < data.length; i += wordLength) {
+            const word = data.slice(i, i + wordLength);
+
+            // Pad word if too short
+            if (padIncompleteWords && word.length < wordLength){
+                for (j = word.length; j < wordLength; j++) {
+                    word.push(0);
+                }
+            }
+
+            words.push(word);
+        }
+
+        // Swap endianness and flatten
+        for (i = 0; i < words.length; i++) {
+            j = words[i].length;
+            while (j--) {
+                result.push(words[i][j]);
+            }
+        }
+
+        // Convert data back to specified data format
+        switch (dataFormat) {
+            case "Hex":
+                return toHex(result);
+            case "Raw":
+                return Utils.byteArrayToUtf8(result);
+            default:
+                return result;
+        }
+    }
+
+    /**
+     * Highlight Swap endianness
+     *
+     * @param {Object[]} pos
+     * @param {number} pos[].start
+     * @param {number} pos[].end
+     * @param {Object[]} args
+     * @returns {Object[]} pos
+     */
+    highlight(pos, args) {
+        return pos;
+    }
+
+    /**
+     * Highlight Swap endianness in reverse
+     *
+     * @param {Object[]} pos
+     * @param {number} pos[].start
+     * @param {number} pos[].end
+     * @param {Object[]} args
+     * @returns {Object[]} pos
+     */
+    highlightReverse(pos, args) {
+        return pos;
+    }
+
+}
+
+export default SwapEndianness;

+ 4 - 1
src/core/operations/TakeBytes.mjs

@@ -5,6 +5,7 @@
  */
 
 import Operation from "../Operation";
+import OperationError from "../errors/OperationError";
 
 /**
  * Take bytes operation
@@ -45,6 +46,8 @@ class TakeBytes extends Operation {
      * @param {ArrayBuffer} input
      * @param {Object[]} args
      * @returns {ArrayBuffer}
+     *
+     * @throws {OperationError} if invalid value
      */
     run(input, args) {
         const start = args[0],
@@ -52,7 +55,7 @@ class TakeBytes extends Operation {
             applyToEachLine = args[2];
 
         if (start < 0 || length < 0)
-            throw "Error: Invalid value";
+            throw new OperationError("Error: Invalid value");
 
         if (!applyToEachLine)
             return input.slice(start, start+length);

+ 141 - 0
src/core/operations/ToBCD.mjs

@@ -0,0 +1,141 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2017
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+import Utils from "../Utils";
+import OperationError from "../errors/OperationError";
+import {ENCODING_SCHEME, ENCODING_LOOKUP, FORMAT} from "../lib/BCD";
+import BigNumber from "bignumber.js";
+
+/**
+ * To BCD operation
+ */
+class ToBCD extends Operation {
+
+    /**
+     * ToBCD constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "To BCD";
+        this.module = "Default";
+        this.description = "Binary-Coded Decimal (BCD) is a class of binary encodings of decimal numbers where each decimal digit is represented by a fixed number of bits, usually four or eight. Special bit patterns are sometimes used for a sign";
+        this.inputType = "BigNumber";
+        this.outputType = "string";
+        this.args = [
+            {
+                "name": "Scheme",
+                "type": "option",
+                "value": ENCODING_SCHEME
+            },
+            {
+                "name": "Packed",
+                "type": "boolean",
+                "value": true
+            },
+            {
+                "name": "Signed",
+                "type": "boolean",
+                "value": false
+            },
+            {
+                "name": "Output format",
+                "type": "option",
+                "value": FORMAT
+            }
+        ];
+    }
+
+    /**
+     * @param {BigNumber} input
+     * @param {Object[]} args
+     * @returns {string}
+     */
+    run(input, args) {
+        if (input.isNaN())
+            throw new OperationError("Invalid input");
+        if (!input.integerValue(BigNumber.ROUND_DOWN).isEqualTo(input))
+            throw new OperationError("Fractional values are not supported by BCD");
+
+        const encoding = ENCODING_LOOKUP[args[0]],
+            packed = args[1],
+            signed = args[2],
+            outputFormat = args[3];
+
+        // Split input number up into separate digits
+        const digits = input.toFixed().split("");
+
+        if (digits[0] === "-" || digits[0] === "+") {
+            digits.shift();
+        }
+
+        let nibbles = [];
+
+        digits.forEach(d => {
+            const n = parseInt(d, 10);
+            nibbles.push(encoding[n]);
+        });
+
+        if (signed) {
+            if (packed && digits.length % 2 === 0) {
+                // If there are an even number of digits, we add a leading 0 so
+                // that the sign nibble doesn't sit in its own byte, leading to
+                // ambiguity around whether the number ends with a 0 or not.
+                nibbles.unshift(encoding[0]);
+            }
+
+            nibbles.push(input > 0 ? 12 : 13);
+            // 12 ("C") for + (credit)
+            // 13 ("D") for - (debit)
+        }
+
+        let bytes = [];
+
+        if (packed) {
+            let encoded = 0,
+                little = false;
+
+            nibbles.forEach(n => {
+                encoded ^= little ? n : (n << 4);
+                if (little) {
+                    bytes.push(encoded);
+                    encoded = 0;
+                }
+                little = !little;
+            });
+
+            if (little) bytes.push(encoded);
+        } else {
+            bytes = nibbles;
+
+            // Add null high nibbles
+            nibbles = nibbles.map(n => {
+                return [0, n];
+            }).reduce((a, b) => {
+                return a.concat(b);
+            });
+        }
+
+        // Output
+        switch (outputFormat) {
+            case "Nibbles":
+                return nibbles.map(n => {
+                    return n.toString(2).padStart(4, "0");
+                }).join(" ");
+            case "Bytes":
+                return bytes.map(b => {
+                    return b.toString(2).padStart(8, "0");
+                }).join(" ");
+            case "Raw":
+            default:
+                return Utils.byteArrayToChars(bytes);
+        }
+    }
+
+}
+
+export default ToBCD;

+ 53 - 0
src/core/operations/ToBase.mjs

@@ -0,0 +1,53 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+import OperationError from "../errors/OperationError";
+
+/**
+ * To Base operation
+ */
+class ToBase extends Operation {
+
+    /**
+     * ToBase constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "To Base";
+        this.module = "Default";
+        this.description = "Converts a decimal number to a given numerical base.";
+        this.inputType = "BigNumber";
+        this.outputType = "string";
+        this.args = [
+            {
+                "name": "Radix",
+                "type": "number",
+                "value": 36
+            }
+        ];
+    }
+
+    /**
+     * @param {BigNumber} input
+     * @param {Object[]} args
+     * @returns {string}
+     */
+    run(input, args) {
+        if (!input) {
+            throw new OperationError("Error: Input must be a number");
+        }
+        const radix = args[0];
+        if (radix < 2 || radix > 36) {
+            throw new OperationError("Error: Radix argument must be between 2 and 36");
+        }
+        return input.toString(radix);
+    }
+
+}
+
+export default ToBase;

+ 85 - 0
src/core/operations/ToBase58.mjs

@@ -0,0 +1,85 @@
+/**
+ * @author tlwr [toby@toby.codes]
+ * @copyright Crown Copyright 2017
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+import Utils from "../Utils";
+import OperationError from "../errors/OperationError";
+import {ALPHABET_OPTIONS} from "../lib/Base58";
+
+/**
+ * To Base58 operation
+ */
+class ToBase58 extends Operation {
+
+    /**
+     * ToBase58 constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "To Base58";
+        this.module = "Default";
+        this.description = "Base58 (similar to Base64) is a notation for encoding arbitrary byte data. It differs from Base64 by removing easily misread characters (i.e. l, I, 0 and O) to improve human readability.<br><br>This operation encodes data in an ASCII string (with an alphabet of your choosing, presets included).<br><br>e.g. <code>hello world</code> becomes <code>StV1DL6CwTryKyV</code><br><br>Base58 is commonly used in cryptocurrencies (Bitcoin, Ripple, etc).";
+        this.inputType = "byteArray";
+        this.outputType = "string";
+        this.args = [
+            {
+                "name": "Alphabet",
+                "type": "editableOption",
+                "value": ALPHABET_OPTIONS
+            }
+        ];
+    }
+
+    /**
+     * @param {byteArray} input
+     * @param {Object[]} args
+     * @returns {string}
+     */
+    run(input, args) {
+        let alphabet = args[0] || ALPHABET_OPTIONS[0].value,
+            result = [0];
+
+        alphabet = Utils.expandAlphRange(alphabet).join("");
+
+        if (alphabet.length !== 58 ||
+            [].unique.call(alphabet).length !== 58) {
+            throw new OperationError("Error: alphabet must be of length 58");
+        }
+
+        if (input.length === 0) return "";
+
+        input.forEach(function(b) {
+            let carry = (result[0] << 8) + b;
+            result[0] = carry % 58;
+            carry = (carry / 58) | 0;
+
+            for (let i = 1; i < result.length; i++) {
+                carry += result[i] << 8;
+                result[i] = carry % 58;
+                carry = (carry / 58) | 0;
+            }
+
+            while (carry > 0) {
+                result.push(carry % 58);
+                carry = (carry / 58) | 0;
+            }
+        });
+
+        result = result.map(function(b) {
+            return alphabet[b];
+        }).reverse().join("");
+
+        while (result.length < input.length) {
+            result = alphabet[0] + result;
+        }
+
+        return result;
+    }
+
+}
+
+export default ToBase58;

+ 4 - 1
src/core/operations/ToCharcode.mjs

@@ -7,6 +7,7 @@
 import Operation from "../Operation";
 import Utils from "../Utils";
 import {DELIM_OPTIONS} from "../lib/Delim";
+import OperationError from "../errors/OperationError";
 
 /**
  * To Charcode operation
@@ -42,6 +43,8 @@ class ToCharcode extends Operation {
      * @param {string} input
      * @param {Object[]} args
      * @returns {string}
+     *
+     * @throws {OperationError} if base argument out of range
      */
     run(input, args) {
         const delim = Utils.charRep(args[0] || "Space"),
@@ -51,7 +54,7 @@ class ToCharcode extends Operation {
             ordinal;
 
         if (base < 2 || base > 36) {
-            throw "Error: Base argument must be between 2 and 36";
+            throw new OperationError("Error: Base argument must be between 2 and 36");
         }
 
         const charcode = Utils.strToCharcode(input);

+ 345 - 0
src/core/operations/ToHTMLEntity.mjs

@@ -0,0 +1,345 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+import Utils from "../Utils";
+
+/**
+ * To HTML Entity operation
+ */
+class ToHTMLEntity extends Operation {
+
+    /**
+     * ToHTMLEntity constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "To HTML Entity";
+        this.module = "Default";
+        this.description = "Converts characters to HTML entities<br><br>e.g. <code>&amp;</code> becomes <code>&amp;<span>amp;</span></code>";
+        this.inputType = "string";
+        this.outputType = "string";
+        this.args = [
+            {
+                "name": "Convert all characters",
+                "type": "boolean",
+                "value": false
+            },
+            {
+                "name": "Convert to",
+                "type": "option",
+                "value": ["Named entities where possible", "Numeric entities", "Hex entities"]
+            }
+        ];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {string}
+     */
+    run(input, args) {
+        const convertAll = args[0],
+            numeric = args[1] === "Numeric entities",
+            hexa = args[1] === "Hex entities";
+
+        const charcodes = Utils.strToCharcode(input);
+        let output = "";
+
+        for (let i = 0; i < charcodes.length; i++) {
+            if (convertAll && numeric) {
+                output += "&#" + charcodes[i] + ";";
+            } else if (convertAll && hexa) {
+                output += "&#x" + Utils.hex(charcodes[i]) + ";";
+            } else if (convertAll) {
+                output += byteToEntity[charcodes[i]] || "&#" + charcodes[i] + ";";
+            } else if (numeric) {
+                if (charcodes[i] > 255 || byteToEntity.hasOwnProperty(charcodes[i])) {
+                    output += "&#" + charcodes[i] + ";";
+                } else {
+                    output += Utils.chr(charcodes[i]);
+                }
+            } else if (hexa) {
+                if (charcodes[i] > 255 || byteToEntity.hasOwnProperty(charcodes[i])) {
+                    output += "&#x" + Utils.hex(charcodes[i]) + ";";
+                } else {
+                    output += Utils.chr(charcodes[i]);
+                }
+            } else {
+                output += byteToEntity[charcodes[i]] || (
+                    charcodes[i] > 255 ?
+                        "&#" + charcodes[i] + ";" :
+                        Utils.chr(charcodes[i])
+                );
+            }
+        }
+        return output;
+    }
+
+}
+
+/**
+ * Lookup table to translate byte values to their HTML entity codes.
+ */
+const byteToEntity = {
+    34: "&quot;",
+    38: "&amp;",
+    39: "&apos;",
+    60: "&lt;",
+    62: "&gt;",
+    160: "&nbsp;",
+    161: "&iexcl;",
+    162: "&cent;",
+    163: "&pound;",
+    164: "&curren;",
+    165: "&yen;",
+    166: "&brvbar;",
+    167: "&sect;",
+    168: "&uml;",
+    169: "&copy;",
+    170: "&ordf;",
+    171: "&laquo;",
+    172: "&not;",
+    173: "&shy;",
+    174: "&reg;",
+    175: "&macr;",
+    176: "&deg;",
+    177: "&plusmn;",
+    178: "&sup2;",
+    179: "&sup3;",
+    180: "&acute;",
+    181: "&micro;",
+    182: "&para;",
+    183: "&middot;",
+    184: "&cedil;",
+    185: "&sup1;",
+    186: "&ordm;",
+    187: "&raquo;",
+    188: "&frac14;",
+    189: "&frac12;",
+    190: "&frac34;",
+    191: "&iquest;",
+    192: "&Agrave;",
+    193: "&Aacute;",
+    194: "&Acirc;",
+    195: "&Atilde;",
+    196: "&Auml;",
+    197: "&Aring;",
+    198: "&AElig;",
+    199: "&Ccedil;",
+    200: "&Egrave;",
+    201: "&Eacute;",
+    202: "&Ecirc;",
+    203: "&Euml;",
+    204: "&Igrave;",
+    205: "&Iacute;",
+    206: "&Icirc;",
+    207: "&Iuml;",
+    208: "&ETH;",
+    209: "&Ntilde;",
+    210: "&Ograve;",
+    211: "&Oacute;",
+    212: "&Ocirc;",
+    213: "&Otilde;",
+    214: "&Ouml;",
+    215: "&times;",
+    216: "&Oslash;",
+    217: "&Ugrave;",
+    218: "&Uacute;",
+    219: "&Ucirc;",
+    220: "&Uuml;",
+    221: "&Yacute;",
+    222: "&THORN;",
+    223: "&szlig;",
+    224: "&agrave;",
+    225: "&aacute;",
+    226: "&acirc;",
+    227: "&atilde;",
+    228: "&auml;",
+    229: "&aring;",
+    230: "&aelig;",
+    231: "&ccedil;",
+    232: "&egrave;",
+    233: "&eacute;",
+    234: "&ecirc;",
+    235: "&euml;",
+    236: "&igrave;",
+    237: "&iacute;",
+    238: "&icirc;",
+    239: "&iuml;",
+    240: "&eth;",
+    241: "&ntilde;",
+    242: "&ograve;",
+    243: "&oacute;",
+    244: "&ocirc;",
+    245: "&otilde;",
+    246: "&ouml;",
+    247: "&divide;",
+    248: "&oslash;",
+    249: "&ugrave;",
+    250: "&uacute;",
+    251: "&ucirc;",
+    252: "&uuml;",
+    253: "&yacute;",
+    254: "&thorn;",
+    255: "&yuml;",
+    338: "&OElig;",
+    339: "&oelig;",
+    352: "&Scaron;",
+    353: "&scaron;",
+    376: "&Yuml;",
+    402: "&fnof;",
+    710: "&circ;",
+    732: "&tilde;",
+    913: "&Alpha;",
+    914: "&Beta;",
+    915: "&Gamma;",
+    916: "&Delta;",
+    917: "&Epsilon;",
+    918: "&Zeta;",
+    919: "&Eta;",
+    920: "&Theta;",
+    921: "&Iota;",
+    922: "&Kappa;",
+    923: "&Lambda;",
+    924: "&Mu;",
+    925: "&Nu;",
+    926: "&Xi;",
+    927: "&Omicron;",
+    928: "&Pi;",
+    929: "&Rho;",
+    931: "&Sigma;",
+    932: "&Tau;",
+    933: "&Upsilon;",
+    934: "&Phi;",
+    935: "&Chi;",
+    936: "&Psi;",
+    937: "&Omega;",
+    945: "&alpha;",
+    946: "&beta;",
+    947: "&gamma;",
+    948: "&delta;",
+    949: "&epsilon;",
+    950: "&zeta;",
+    951: "&eta;",
+    952: "&theta;",
+    953: "&iota;",
+    954: "&kappa;",
+    955: "&lambda;",
+    956: "&mu;",
+    957: "&nu;",
+    958: "&xi;",
+    959: "&omicron;",
+    960: "&pi;",
+    961: "&rho;",
+    962: "&sigmaf;",
+    963: "&sigma;",
+    964: "&tau;",
+    965: "&upsilon;",
+    966: "&phi;",
+    967: "&chi;",
+    968: "&psi;",
+    969: "&omega;",
+    977: "&thetasym;",
+    978: "&upsih;",
+    982: "&piv;",
+    8194: "&ensp;",
+    8195: "&emsp;",
+    8201: "&thinsp;",
+    8204: "&zwnj;",
+    8205: "&zwj;",
+    8206: "&lrm;",
+    8207: "&rlm;",
+    8211: "&ndash;",
+    8212: "&mdash;",
+    8216: "&lsquo;",
+    8217: "&rsquo;",
+    8218: "&sbquo;",
+    8220: "&ldquo;",
+    8221: "&rdquo;",
+    8222: "&bdquo;",
+    8224: "&dagger;",
+    8225: "&Dagger;",
+    8226: "&bull;",
+    8230: "&hellip;",
+    8240: "&permil;",
+    8242: "&prime;",
+    8243: "&Prime;",
+    8249: "&lsaquo;",
+    8250: "&rsaquo;",
+    8254: "&oline;",
+    8260: "&frasl;",
+    8364: "&euro;",
+    8465: "&image;",
+    8472: "&weierp;",
+    8476: "&real;",
+    8482: "&trade;",
+    8501: "&alefsym;",
+    8592: "&larr;",
+    8593: "&uarr;",
+    8594: "&rarr;",
+    8595: "&darr;",
+    8596: "&harr;",
+    8629: "&crarr;",
+    8656: "&lArr;",
+    8657: "&uArr;",
+    8658: "&rArr;",
+    8659: "&dArr;",
+    8660: "&hArr;",
+    8704: "&forall;",
+    8706: "&part;",
+    8707: "&exist;",
+    8709: "&empty;",
+    8711: "&nabla;",
+    8712: "&isin;",
+    8713: "&notin;",
+    8715: "&ni;",
+    8719: "&prod;",
+    8721: "&sum;",
+    8722: "&minus;",
+    8727: "&lowast;",
+    8730: "&radic;",
+    8733: "&prop;",
+    8734: "&infin;",
+    8736: "&ang;",
+    8743: "&and;",
+    8744: "&or;",
+    8745: "&cap;",
+    8746: "&cup;",
+    8747: "&int;",
+    8756: "&there4;",
+    8764: "&sim;",
+    8773: "&cong;",
+    8776: "&asymp;",
+    8800: "&ne;",
+    8801: "&equiv;",
+    8804: "&le;",
+    8805: "&ge;",
+    8834: "&sub;",
+    8835: "&sup;",
+    8836: "&nsub;",
+    8838: "&sube;",
+    8839: "&supe;",
+    8853: "&oplus;",
+    8855: "&otimes;",
+    8869: "&perp;",
+    8901: "&sdot;",
+    8942: "&vellip;",
+    8968: "&lceil;",
+    8969: "&rceil;",
+    8970: "&lfloor;",
+    8971: "&rfloor;",
+    9001: "&lang;",
+    9002: "&rang;",
+    9674: "&loz;",
+    9824: "&spades;",
+    9827: "&clubs;",
+    9829: "&hearts;",
+    9830: "&diams;",
+};
+
+export default ToHTMLEntity;

+ 244 - 0
src/core/operations/ToQuotedPrintable.mjs

@@ -0,0 +1,244 @@
+/**
+ * Some parts taken from mimelib (http://github.com/andris9/mimelib)
+ * @author Andris Reinman
+ * @license MIT
+ *
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+
+/**
+ * To Quoted Printable operation
+ */
+class ToQuotedPrintable extends Operation {
+
+    /**
+     * ToQuotedPrintable constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "To Quoted Printable";
+        this.module = "Default";
+        this.description = "Quoted-Printable, or QP encoding, is an encoding using printable ASCII characters (alphanumeric and the equals sign '=') to transmit 8-bit data over a 7-bit data path or, generally, over a medium which is not 8-bit clean. It is defined as a MIME content transfer encoding for use in e-mail.<br><br>QP works by using the equals sign '=' as an escape character. It also limits line length to 76, as some software has limits on line length.";
+        this.inputType = "byteArray";
+        this.outputType = "string";
+        this.args = [];
+    }
+
+    /**
+     * @param {byteArray} input
+     * @param {Object[]} args
+     * @returns {string}
+     */
+    run(input, args) {
+        let mimeEncodedStr = this.mimeEncode(input);
+
+        // fix line breaks
+        mimeEncodedStr = mimeEncodedStr.replace(/\r?\n|\r/g, function() {
+            return "\r\n";
+        }).replace(/[\t ]+$/gm, function(spaces) {
+            return spaces.replace(/ /g, "=20").replace(/\t/g, "=09");
+        });
+
+        return this._addSoftLinebreaks(mimeEncodedStr, "qp");
+    }
+
+
+    /** @license
+    ========================================================================
+      mimelib: http://github.com/andris9/mimelib
+      Copyright (c) 2011-2012 Andris Reinman
+
+      Permission is hereby granted, free of charge, to any person obtaining a copy
+      of this software and associated documentation files (the "Software"), to deal
+      in the Software without restriction, including without limitation the rights
+      to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+      copies of the Software, and to permit persons to whom the Software is
+      furnished to do so, subject to the following conditions:
+
+      THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+      IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+      FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+      AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+      LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+      OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+      SOFTWARE.
+    */
+
+    /**
+     * Encodes mime data.
+     *
+     * @param {byteArray} buffer
+     * @returns {string}
+     */
+    mimeEncode(buffer) {
+        const ranges = [
+            [0x09],
+            [0x0A],
+            [0x0D],
+            [0x20],
+            [0x21],
+            [0x23, 0x3C],
+            [0x3E],
+            [0x40, 0x5E],
+            [0x60, 0x7E]
+        ];
+        let result = "";
+
+        for (let i = 0, len = buffer.length; i < len; i++) {
+            if (this._checkRanges(buffer[i], ranges)) {
+                result += String.fromCharCode(buffer[i]);
+                continue;
+            }
+            result += "=" + (buffer[i] < 0x10 ? "0" : "") + buffer[i].toString(16).toUpperCase();
+        }
+
+        return result;
+    }
+
+    /**
+     * Checks if a given number falls within a given set of ranges.
+     *
+     * @private
+     * @param {number} nr
+     * @param {byteArray[]} ranges
+     * @returns {bolean}
+     */
+    _checkRanges(nr, ranges) {
+        for (let i = ranges.length - 1; i >= 0; i--) {
+            if (!ranges[i].length)
+                continue;
+            if (ranges[i].length === 1 && nr === ranges[i][0])
+                return true;
+            if (ranges[i].length === 2 && nr >= ranges[i][0] && nr <= ranges[i][1])
+                return true;
+        }
+        return false;
+    }
+
+    /**
+     * Adds soft line breaks to a string.
+     * Lines can't be longer that 76 + <CR><LF> = 78 bytes
+     * http://tools.ietf.org/html/rfc2045#section-6.7
+     *
+     * @private
+     * @param {string} str
+     * @param {string} encoding
+     * @returns {string}
+     */
+    _addSoftLinebreaks(str, encoding) {
+        const lineLengthMax = 76;
+
+        encoding = (encoding || "base64").toString().toLowerCase().trim();
+
+        if (encoding === "qp") {
+            return this._addQPSoftLinebreaks(str, lineLengthMax);
+        } else {
+            return this._addBase64SoftLinebreaks(str, lineLengthMax);
+        }
+    }
+
+    /**
+     * Adds soft line breaks to a base64 string.
+     *
+     * @private
+     * @param {string} base64EncodedStr
+     * @param {number} lineLengthMax
+     * @returns {string}
+     */
+    _addBase64SoftLinebreaks(base64EncodedStr, lineLengthMax) {
+        base64EncodedStr = (base64EncodedStr || "").toString().trim();
+        return base64EncodedStr.replace(new RegExp(".{" + lineLengthMax + "}", "g"), "$&\r\n").trim();
+    }
+
+    /**
+     * Adds soft line breaks to a quoted printable string.
+     *
+     * @private
+     * @param {string} mimeEncodedStr
+     * @param {number} lineLengthMax
+     * @returns {string}
+     */
+    _addQPSoftLinebreaks(mimeEncodedStr, lineLengthMax) {
+        const len = mimeEncodedStr.length,
+            lineMargin = Math.floor(lineLengthMax / 3);
+        let pos = 0,
+            match, code, line,
+            result = "";
+
+        // insert soft linebreaks where needed
+        while (pos < len) {
+            line = mimeEncodedStr.substr(pos, lineLengthMax);
+            if ((match = line.match(/\r\n/))) {
+                line = line.substr(0, match.index + match[0].length);
+                result += line;
+                pos += line.length;
+                continue;
+            }
+
+            if (line.substr(-1) === "\n") {
+                // nothing to change here
+                result += line;
+                pos += line.length;
+                continue;
+            } else if ((match = line.substr(-lineMargin).match(/\n.*?$/))) {
+                // truncate to nearest line break
+                line = line.substr(0, line.length - (match[0].length - 1));
+                result += line;
+                pos += line.length;
+                continue;
+            } else if (line.length > lineLengthMax - lineMargin && (match = line.substr(-lineMargin).match(/[ \t.,!?][^ \t.,!?]*$/))) {
+                // truncate to nearest space
+                line = line.substr(0, line.length - (match[0].length - 1));
+            } else if (line.substr(-1) === "\r") {
+                line = line.substr(0, line.length - 1);
+            } else {
+                if (line.match(/=[\da-f]{0,2}$/i)) {
+
+                    // push incomplete encoding sequences to the next line
+                    if ((match = line.match(/=[\da-f]{0,1}$/i))) {
+                        line = line.substr(0, line.length - match[0].length);
+                    }
+
+                    // ensure that utf-8 sequences are not split
+                    while (line.length > 3 && line.length < len - pos && !line.match(/^(?:=[\da-f]{2}){1,4}$/i) && (match = line.match(/=[\da-f]{2}$/ig))) {
+                        code = parseInt(match[0].substr(1, 2), 16);
+                        if (code < 128) {
+                            break;
+                        }
+
+                        line = line.substr(0, line.length - 3);
+
+                        if (code >= 0xC0) {
+                            break;
+                        }
+                    }
+
+                }
+            }
+
+            if (pos + line.length < len && line.substr(-1) !== "\n") {
+                if (line.length === 76 && line.match(/=[\da-f]{2}$/i)) {
+                    line = line.substr(0, line.length - 3);
+                } else if (line.length === 76) {
+                    line = line.substr(0, line.length - 1);
+                }
+                pos += line.length;
+                line += "=\r\n";
+            } else {
+                pos += line.length;
+            }
+
+            result += line;
+        }
+
+        return result;
+    }
+
+}
+
+export default ToQuotedPrintable;

+ 4 - 1
src/core/operations/ToUNIXTimestamp.mjs

@@ -7,6 +7,7 @@
 import Operation from "../Operation";
 import moment from "moment-timezone";
 import {UNITS} from "../lib/DateTime";
+import OperationError from "../errors/OperationError";
 
 /**
  * To UNIX Timestamp operation
@@ -47,6 +48,8 @@ class ToUNIXTimestamp extends Operation {
      * @param {string} input
      * @param {Object[]} args
      * @returns {string}
+     *
+     * @throws {OperationError} if unit unrecognised
      */
     run(input, args) {
         const [units, treatAsUTC, showDateTime] = args,
@@ -63,7 +66,7 @@ class ToUNIXTimestamp extends Operation {
         } else if (units === "Nanoseconds (ns)") {
             result = d.valueOf() * 1000000;
         } else {
-            throw "Unrecognised unit";
+            throw new OperationError("Unrecognised unit");
         }
 
         return showDateTime ? `${result} (${d.tz("UTC").format("ddd D MMMM YYYY HH:mm:ss")} UTC)` : result.toString();

+ 2 - 1
src/core/operations/TranslateDateTimeFormat.mjs

@@ -7,6 +7,7 @@
 import Operation from "../Operation";
 import moment from "moment-timezone";
 import {DATETIME_FORMATS, FORMAT_EXAMPLES} from "../lib/DateTime";
+import OperationError from "../errors/OperationError";
 
 /**
  * Translate DateTime Format operation
@@ -67,7 +68,7 @@ class TranslateDateTimeFormat extends Operation {
             date = moment.tz(input, inputFormat, inputTimezone);
             if (!date || date.format() === "Invalid date") throw Error;
         } catch (err) {
-            return "Invalid format.\n\n" + FORMAT_EXAMPLES;
+            throw new OperationError(`Invalid format.\n\n${FORMAT_EXAMPLES}`);
         }
 
         return date.tz(outputTimezone).format(outputFormat);

+ 44 - 0
src/core/operations/URLDecode.mjs

@@ -0,0 +1,44 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+
+/**
+ * URL Decode operation
+ */
+class URLDecode extends Operation {
+
+    /**
+     * URLDecode constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "URL Decode";
+        this.module = "URL";
+        this.description = "Converts URI/URL percent-encoded characters back to their raw values.<br><br>e.g. <code>%3d</code> becomes <code>=</code>";
+        this.inputType = "string";
+        this.outputType = "string";
+        this.args = [];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {string}
+     */
+    run(input, args) {
+        const data = input.replace(/\+/g, "%20");
+        try {
+            return decodeURIComponent(data);
+        } catch (err) {
+            return unescape(data);
+        }
+    }
+
+}
+
+export default URLDecode;

+ 68 - 0
src/core/operations/URLEncode.mjs

@@ -0,0 +1,68 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+
+/**
+ * URL Encode operation
+ */
+class URLEncode extends Operation {
+
+    /**
+     * URLEncode constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "URL Encode";
+        this.module = "URL";
+        this.description = "Encodes problematic characters into percent-encoding, a format supported by URIs/URLs.<br><br>e.g. <code>=</code> becomes <code>%3d</code>";
+        this.inputType = "string";
+        this.outputType = "string";
+        this.args = [
+            {
+                "name": "Encode all special chars",
+                "type": "boolean",
+                "value": false
+            }
+        ];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {string}
+     */
+    run(input, args) {
+        const encodeAll = args[0];
+        return encodeAll ? this.encodeAllChars(input) : encodeURI(input);
+    }
+
+    /**
+     * Encode characters in URL outside of encodeURI() function spec
+     *
+     * @param {string} str
+     * @returns {string}
+     */
+    encodeAllChars (str) {
+        // TODO Do this programatically
+        return encodeURIComponent(str)
+            .replace(/!/g, "%21")
+            .replace(/#/g, "%23")
+            .replace(/'/g, "%27")
+            .replace(/\(/g, "%28")
+            .replace(/\)/g, "%29")
+            .replace(/\*/g, "%2A")
+            .replace(/-/g, "%2D")
+            .replace(/\./g, "%2E")
+            .replace(/_/g, "%5F")
+            .replace(/~/g, "%7E");
+    }
+
+}
+
+
+export default URLEncode;

+ 75 - 0
src/core/operations/UnescapeUnicodeCharacters.mjs

@@ -0,0 +1,75 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+import Utils from "../Utils";
+
+/**
+ * Unescape Unicode Characters operation
+ */
+class UnescapeUnicodeCharacters extends Operation {
+
+    /**
+     * UnescapeUnicodeCharacters constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "Unescape Unicode Characters";
+        this.module = "Default";
+        this.description = "Converts unicode-escaped character notation back into raw characters.<br><br>Supports the prefixes:<ul><li><code>\\u</code></li><li><code>%u</code></li><li><code>U+</code></li></ul>e.g. <code>\\u03c3\\u03bf\\u03c5</code> becomes <code>σου</code>";
+        this.inputType = "string";
+        this.outputType = "string";
+        this.args = [
+            {
+                "name": "Prefix",
+                "type": "option",
+                "value": ["\\u", "%u", "U+"]
+            }
+        ];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {string}
+     */
+    run(input, args) {
+        const prefix = prefixToRegex[args[0]],
+            regex = new RegExp(prefix+"([a-f\\d]{4})", "ig");
+        let output = "",
+            m,
+            i = 0;
+
+        while ((m = regex.exec(input))) {
+            // Add up to match
+            output += input.slice(i, m.index);
+            i = m.index;
+
+            // Add match
+            output += Utils.chr(parseInt(m[1], 16));
+
+            i = regex.lastIndex;
+        }
+
+        // Add all after final match
+        output += input.slice(i, input.length);
+
+        return output;
+    }
+
+}
+
+/**
+ * Lookup table to add prefixes to unicode delimiters so that they can be used in a regex.
+ */
+const prefixToRegex = {
+    "\\u": "\\\\u",
+    "%u": "%u",
+    "U+": "U\\+"
+};
+
+export default UnescapeUnicodeCharacters;

+ 0 - 333
src/core/operations/legacy/Extract.js

@@ -1,333 +0,0 @@
-import XRegExp from "xregexp";
-
-
-/**
- * Identifier extraction operations.
- *
- * @author n1474335 [n1474335@gmail.com]
- * @copyright Crown Copyright 2016
- * @license Apache-2.0
- *
- * @namespace
- */
-const Extract = {
-
-    /**
-     * Runs search operations across the input data using regular expressions.
-     *
-     * @private
-     * @param {string} input
-     * @param {RegExp} searchRegex
-     * @param {RegExp} removeRegex - A regular expression defining results to remove from the
-     *      final list
-     * @param {boolean} includeTotal - Whether or not to include the total number of results
-     * @returns {string}
-     */
-    _search: function(input, searchRegex, removeRegex, includeTotal) {
-        let output = "",
-            total = 0,
-            match;
-
-        while ((match = searchRegex.exec(input))) {
-            // Moves pointer when an empty string is matched (prevents infinite loop)
-            if (match.index === searchRegex.lastIndex) {
-                searchRegex.lastIndex++;
-            }
-
-            if (removeRegex && removeRegex.test(match[0]))
-                continue;
-            total++;
-            output += match[0] + "\n";
-        }
-
-        if (includeTotal)
-            output = "Total found: " + total + "\n\n" + output;
-
-        return output;
-    },
-
-
-    /**
-     * @constant
-     * @default
-     */
-    MIN_STRING_LEN: 4,
-    /**
-     * @constant
-     * @default
-     */
-    STRING_MATCH_TYPE: [
-        "[ASCII]", "Alphanumeric + punctuation (A)", "All printable chars (A)", "Null-terminated strings (A)",
-        "[Unicode]", "Alphanumeric + punctuation (U)", "All printable chars (U)", "Null-terminated strings (U)"
-    ],
-    /**
-     * @constant
-     * @default
-     */
-    ENCODING_LIST: ["Single byte", "16-bit littleendian", "16-bit bigendian", "All"],
-    /**
-     * @constant
-     * @default
-     */
-    DISPLAY_TOTAL: false,
-
-    /**
-     * Strings operation.
-     *
-     * @param {string} input
-     * @param {Object[]} args
-     * @returns {string}
-     */
-    runStrings: function(input, args) {
-        const encoding = args[0],
-            minLen = args[1],
-            matchType = args[2],
-            displayTotal = args[3],
-            alphanumeric = "A-Z\\d",
-            punctuation = "/\\-:.,_$%'\"()<>= !\\[\\]{}@",
-            printable = "\x20-\x7e",
-            uniAlphanumeric = "\\pL\\pN",
-            uniPunctuation = "\\pP\\pZ",
-            uniPrintable = "\\pL\\pM\\pZ\\pS\\pN\\pP";
-
-        let strings = "";
-
-        switch (matchType) {
-            case "Alphanumeric + punctuation (A)":
-                strings = `[${alphanumeric + punctuation}]`;
-                break;
-            case "All printable chars (A)":
-            case "Null-terminated strings (A)":
-                strings = `[${printable}]`;
-                break;
-            case "Alphanumeric + punctuation (U)":
-                strings = `[${uniAlphanumeric + uniPunctuation}]`;
-                break;
-            case "All printable chars (U)":
-            case "Null-terminated strings (U)":
-                strings = `[${uniPrintable}]`;
-                break;
-        }
-
-        // UTF-16 support is hacked in by allowing null bytes on either side of the matched chars
-        switch (encoding) {
-            case "All":
-                strings = `(\x00?${strings}\x00?)`;
-                break;
-            case "16-bit littleendian":
-                strings = `(${strings}\x00)`;
-                break;
-            case "16-bit bigendian":
-                strings = `(\x00${strings})`;
-                break;
-            case "Single byte":
-            default:
-                break;
-        }
-
-        strings = `${strings}{${minLen},}`;
-
-        if (matchType.includes("Null-terminated")) {
-            strings += "\x00";
-        }
-
-        const regex = new XRegExp(strings, "ig");
-
-        return Extract._search(input, regex, null, displayTotal);
-    },
-
-
-    /**
-     * @constant
-     * @default
-     */
-    INCLUDE_IPV4: true,
-    /**
-     * @constant
-     * @default
-     */
-    INCLUDE_IPV6: false,
-    /**
-     * @constant
-     * @default
-     */
-    REMOVE_LOCAL: false,
-
-    /**
-     * Extract IP addresses operation.
-     *
-     * @param {string} input
-     * @param {Object[]} args
-     * @returns {string}
-     */
-    runIp: function(input, args) {
-        let includeIpv4  = args[0],
-            includeIpv6  = args[1],
-            removeLocal  = args[2],
-            displayTotal = args[3],
-            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})",
-            ips  = "";
-
-        if (includeIpv4 && includeIpv6) {
-            ips = ipv4 + "|" + ipv6;
-        } else if (includeIpv4) {
-            ips = ipv4;
-        } else if (includeIpv6) {
-            ips = ipv6;
-        }
-
-        if (ips) {
-            const regex = new RegExp(ips, "ig");
-
-            if (removeLocal) {
-                let ten = "10\\..+",
-                    oneninetwo = "192\\.168\\..+",
-                    oneseventwo = "172\\.(?:1[6-9]|2\\d|3[01])\\..+",
-                    onetwoseven = "127\\..+",
-                    removeRegex = new RegExp("^(?:" + ten + "|" + oneninetwo +
-                        "|" + oneseventwo + "|" + onetwoseven + ")");
-
-                return Extract._search(input, regex, removeRegex, displayTotal);
-            } else {
-                return Extract._search(input, regex, null, displayTotal);
-            }
-        } else {
-            return "";
-        }
-    },
-
-
-    /**
-     * Extract email addresses operation.
-     *
-     * @param {string} input
-     * @param {Object[]} args
-     * @returns {string}
-     */
-    runEmail: function(input, args) {
-        let displayTotal = args[0],
-            regex = /\b\w[-.\w]*@[-\w]+(?:\.[-\w]+)*\.[A-Z]{2,4}\b/ig;
-
-        return Extract._search(input, regex, null, displayTotal);
-    },
-
-
-    /**
-     * Extract MAC addresses operation.
-     *
-     * @param {string} input
-     * @param {Object[]} args
-     * @returns {string}
-     */
-    runMac: function(input, args) {
-        let displayTotal = args[0],
-            regex = /[A-F\d]{2}(?:[:-][A-F\d]{2}){5}/ig;
-
-        return Extract._search(input, regex, null, displayTotal);
-    },
-
-
-    /**
-     * Extract URLs operation.
-     *
-     * @param {string} input
-     * @param {Object[]} args
-     * @returns {string}
-     */
-    runUrls: function(input, args) {
-        let displayTotal = args[0],
-            protocol = "[A-Z]+://",
-            hostname = "[-\\w]+(?:\\.\\w[-\\w]*)+",
-            port = ":\\d+",
-            path = "/[^.!,?\"<>\\[\\]{}\\s\\x7F-\\xFF]*";
-
-        path += "(?:[.!,?]+[^.!,?\"<>\\[\\]{}\\s\\x7F-\\xFF]+)*";
-        const regex = new RegExp(protocol + hostname + "(?:" + port +
-            ")?(?:" + path + ")?", "ig");
-        return Extract._search(input, regex, null, displayTotal);
-    },
-
-
-    /**
-     * Extract domains operation.
-     *
-     * @param {string} input
-     * @param {Object[]} args
-     * @returns {string}
-     */
-    runDomains: function(input, args) {
-        const displayTotal = args[0],
-            regex = /\b((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63}\b/ig;
-
-        return Extract._search(input, regex, null, displayTotal);
-    },
-
-
-    /**
-     * @constant
-     * @default
-     */
-    INCLUDE_WIN_PATH: true,
-    /**
-     * @constant
-     * @default
-     */
-    INCLUDE_UNIX_PATH: true,
-
-    /**
-     * Extract file paths operation.
-     *
-     * @param {string} input
-     * @param {Object[]} args
-     * @returns {string}
-     */
-    runFilePaths: function(input, args) {
-        let includeWinPath = args[0],
-            includeUnixPath = args[1],
-            displayTotal = args[2],
-            winDrive = "[A-Z]:\\\\",
-            winName = "[A-Z\\d][A-Z\\d\\- '_\\(\\)~]{0,61}",
-            winExt = "[A-Z\\d]{1,6}",
-            winPath = winDrive + "(?:" + winName + "\\\\?)*" + winName +
-                "(?:\\." + winExt + ")?",
-            unixPath = "(?:/[A-Z\\d.][A-Z\\d\\-.]{0,61})+",
-            filePaths = "";
-
-        if (includeWinPath && includeUnixPath) {
-            filePaths = winPath + "|" + unixPath;
-        } else if (includeWinPath) {
-            filePaths = winPath;
-        } else if (includeUnixPath) {
-            filePaths = unixPath;
-        }
-
-        if (filePaths) {
-            const regex = new RegExp(filePaths, "ig");
-            return Extract._search(input, regex, null, displayTotal);
-        } else {
-            return "";
-        }
-    },
-
-
-    /**
-     * Extract dates operation.
-     *
-     * @param {string} input
-     * @param {Object[]} args
-     * @returns {string}
-     */
-    runDates: function(input, args) {
-        let displayTotal = args[0],
-            date1 = "(?:19|20)\\d\\d[- /.](?:0[1-9]|1[012])[- /.](?:0[1-9]|[12][0-9]|3[01])", // yyyy-mm-dd
-            date2 = "(?:0[1-9]|[12][0-9]|3[01])[- /.](?:0[1-9]|1[012])[- /.](?:19|20)\\d\\d", // dd/mm/yyyy
-            date3 = "(?:0[1-9]|1[012])[- /.](?:0[1-9]|[12][0-9]|3[01])[- /.](?:19|20)\\d\\d", // mm/dd/yyyy
-            regex = new RegExp(date1 + "|" + date2 + "|" + date3, "ig");
-
-        return Extract._search(input, regex, null, displayTotal);
-    },
-
-};
-
-export default Extract;

+ 0 - 364
src/core/operations/legacy/PGP.js

@@ -1,364 +0,0 @@
-import * as kbpgp from "kbpgp";
-import {promisify} from "es6-promisify";
-
-
-/**
- * PGP operations.
- *
- * @author tlwr [toby@toby.codes]
- * @author Matt C [matt@artemisbot.uk]
- * @author n1474335 [n1474335@gmail.com]
- * @copyright Crown Copyright 2017
- * @license Apache-2.0
- *
- * @namespace
- */
-const PGP = {
-
-    /**
-     * @constant
-     * @default
-     */
-    KEY_TYPES: ["RSA-1024", "RSA-2048", "RSA-4096", "ECC-256", "ECC-384"],
-
-
-    /**
-     * Get size of subkey
-     *
-     * @private
-     * @param {number} keySize
-     * @returns {number}
-     */
-    _getSubkeySize(keySize) {
-        return {
-            1024: 1024,
-            2048: 1024,
-            4096: 2048,
-            256:   256,
-            384:   256,
-        }[keySize];
-    },
-
-
-    /**
-     * Progress callback
-     *
-     * @private
-     */
-    _ASP: new kbpgp.ASP({
-        "progress_hook": info => {
-            let msg = "";
-
-            switch (info.what) {
-                case "guess":
-                    msg = "Guessing a prime";
-                    break;
-                case "fermat":
-                    msg = "Factoring prime using Fermat's factorization method";
-                    break;
-                case "mr":
-                    msg = "Performing Miller-Rabin primality test";
-                    break;
-                case "passed_mr":
-                    msg = "Passed Miller-Rabin primality test";
-                    break;
-                case "failed_mr":
-                    msg = "Failed Miller-Rabin primality test";
-                    break;
-                case "found":
-                    msg = "Prime found";
-                    break;
-                default:
-                    msg = `Stage: ${info.what}`;
-            }
-
-            if (ENVIRONMENT_IS_WORKER())
-                self.sendStatusMessage(msg);
-        }
-    }),
-
-
-    /**
-     * Import private key and unlock if necessary
-     *
-     * @private
-     * @param {string} privateKey
-     * @param {string} [passphrase]
-     * @returns {Object}
-     */
-    async _importPrivateKey(privateKey, passphrase) {
-        try {
-            const key = await promisify(kbpgp.KeyManager.import_from_armored_pgp)({
-                armored: privateKey,
-                opts: {
-                    "no_check_keys": true
-                }
-            });
-            if (key.is_pgp_locked()) {
-                if (passphrase) {
-                    await promisify(key.unlock_pgp.bind(key))({
-                        passphrase
-                    });
-                } else {
-                    throw "Did not provide passphrase with locked private key.";
-                }
-            }
-            return key;
-        } catch (err) {
-            throw `Could not import private key: ${err}`;
-        }
-    },
-
-
-    /**
-     * Import public key
-     *
-     * @private
-     * @param {string} publicKey
-     * @returns {Object}
-     */
-    async _importPublicKey (publicKey) {
-        try {
-            const key = await promisify(kbpgp.KeyManager.import_from_armored_pgp)({
-                armored: publicKey,
-                opts: {
-                    "no_check_keys": true
-                }
-            });
-            return key;
-        } catch (err) {
-            throw `Could not import public key: ${err}`;
-        }
-    },
-
-
-    /**
-     * Generate PGP Key Pair operation.
-     *
-     * @param {string} input
-     * @param {Object[]} args
-     * @returns {string}
-     */
-    runGenerateKeyPair(input, args) {
-        let [keyType, keySize] = args[0].split("-"),
-            password = args[1],
-            name = args[2],
-            email = args[3],
-            userIdentifier = "";
-
-        if (name) userIdentifier += name;
-        if (email) userIdentifier += ` <${email}>`;
-
-        let flags = kbpgp.const.openpgp.certify_keys;
-        flags |= kbpgp.const.openpgp.sign_data;
-        flags |= kbpgp.const.openpgp.auth;
-        flags |= kbpgp.const.openpgp.encrypt_comm;
-        flags |= kbpgp.const.openpgp.encrypt_storage;
-
-        let keyGenerationOptions = {
-            userid: userIdentifier,
-            ecc: keyType === "ecc",
-            primary: {
-                "nbits": keySize,
-                "flags": flags,
-                "expire_in": 0
-            },
-            subkeys: [{
-                "nbits": PGP._getSubkeySize(keySize),
-                "flags": kbpgp.const.openpgp.sign_data,
-                "expire_in": 86400 * 365 * 8
-            }, {
-                "nbits": PGP._getSubkeySize(keySize),
-                "flags": kbpgp.const.openpgp.encrypt_comm | kbpgp.const.openpgp.encrypt_storage,
-                "expire_in": 86400 * 365 * 2
-            }],
-            asp: PGP._ASP
-        };
-
-        return new Promise(async (resolve, reject) => {
-            try {
-                const unsignedKey = await promisify(kbpgp.KeyManager.generate)(keyGenerationOptions);
-                await promisify(unsignedKey.sign.bind(unsignedKey))({});
-                let signedKey = unsignedKey;
-                let privateKeyExportOptions = {};
-                if (password) privateKeyExportOptions.passphrase = password;
-                const privateKey = await promisify(signedKey.export_pgp_private.bind(signedKey))(privateKeyExportOptions);
-                const publicKey = await promisify(signedKey.export_pgp_public.bind(signedKey))({});
-                resolve(privateKey + "\n" + publicKey.trim());
-            } catch (err) {
-                reject(`Error whilst generating key pair: ${err}`);
-            }
-        });
-    },
-
-
-    /**
-     * PGP Encrypt operation.
-     *
-     * @param {string} input
-     * @param {Object[]} args
-     * @returns {string}
-     */
-    async runEncrypt(input, args) {
-        let plaintextMessage = input,
-            plainPubKey = args[0],
-            key,
-            encryptedMessage;
-
-        if (!plainPubKey) return "Enter the public key of the recipient.";
-
-        try {
-            key = await promisify(kbpgp.KeyManager.import_from_armored_pgp)({
-                armored: plainPubKey,
-            });
-        } catch (err) {
-            throw `Could not import public key: ${err}`;
-        }
-
-        try {
-            encryptedMessage = await promisify(kbpgp.box)({
-                "msg": plaintextMessage,
-                "encrypt_for": key,
-                "asp": PGP._ASP
-            });
-        } catch (err) {
-            throw `Couldn't encrypt message with provided public key: ${err}`;
-        }
-
-        return encryptedMessage.toString();
-    },
-
-
-    /**
-     * PGP Decrypt operation.
-     *
-     * @param {string} input
-     * @param {Object[]} args
-     * @returns {string}
-     */
-    async runDecrypt(input, args) {
-        let encryptedMessage = input,
-            privateKey = args[0],
-            passphrase = args[1],
-            keyring = new kbpgp.keyring.KeyRing(),
-            plaintextMessage;
-
-        if (!privateKey) return "Enter the private key of the recipient.";
-
-        const key = await PGP._importPrivateKey(privateKey, passphrase);
-        keyring.add_key_manager(key);
-
-        try {
-            plaintextMessage = await promisify(kbpgp.unbox)({
-                armored: encryptedMessage,
-                keyfetch: keyring,
-                asp: PGP._ASP
-            });
-        } catch (err) {
-            throw `Couldn't decrypt message with provided private key: ${err}`;
-        }
-
-        return plaintextMessage.toString();
-    },
-
-
-    /**
-     * PGP Sign Message operation.
-     *
-     * @param {string} input
-     * @param {Object[]} args
-     * @returns {string}
-     */
-    async runSign(input, args) {
-        let message = input,
-            privateKey = args[0],
-            passphrase = args[1],
-            publicKey = args[2],
-            signedMessage;
-
-        if (!privateKey) return "Enter the private key of the signer.";
-        if (!publicKey) return "Enter the public key of the recipient.";
-        const privKey = await PGP._importPrivateKey(privateKey, passphrase);
-        const pubKey = await PGP._importPublicKey(publicKey);
-
-        try {
-            signedMessage = await promisify(kbpgp.box)({
-                "msg": message,
-                "encrypt_for": pubKey,
-                "sign_with": privKey,
-                "asp": PGP._ASP
-            });
-        } catch (err) {
-            throw `Couldn't sign message: ${err}`;
-        }
-
-        return signedMessage;
-    },
-
-
-    /**
-     * PGP Verify Message operation.
-     *
-     * @param {string} input
-     * @param {Object[]} args
-     * @returns {string}
-     */
-    async runVerify(input, args) {
-        let signedMessage = input,
-            publicKey = args[0],
-            privateKey = args[1],
-            passphrase = args[2],
-            keyring = new kbpgp.keyring.KeyRing(),
-            unboxedLiterals;
-
-        if (!publicKey) return "Enter the public key of the signer.";
-        if (!privateKey) return "Enter the private key of the recipient.";
-        const privKey = await PGP._importPrivateKey(privateKey, passphrase);
-        const pubKey = await PGP._importPublicKey(publicKey);
-        keyring.add_key_manager(privKey);
-        keyring.add_key_manager(pubKey);
-
-        try {
-            unboxedLiterals = await promisify(kbpgp.unbox)({
-                armored: signedMessage,
-                keyfetch: keyring,
-                asp: PGP._ASP
-            });
-            const ds = unboxedLiterals[0].get_data_signer();
-            if (ds) {
-                const km = ds.get_key_manager();
-                if (km) {
-                    const signer = km.get_userids_mark_primary()[0].components;
-                    let text = "Signed by ";
-                    if (signer.email || signer.username || signer.comment) {
-                        if (signer.username) {
-                            text += `${signer.username} `;
-                        }
-                        if (signer.comment) {
-                            text += `${signer.comment} `;
-                        }
-                        if (signer.email) {
-                            text += `<${signer.email}>`;
-                        }
-                        text += "\n";
-                    }
-                    text += [
-                        `PGP fingerprint: ${km.get_pgp_fingerprint().toString("hex")}`,
-                        `Signed on ${new Date(ds.sig.hashed_subpackets[0].time * 1000).toUTCString()}`,
-                        "----------------------------------\n"
-                    ].join("\n");
-                    text += unboxedLiterals.toString();
-                    return text.trim();
-                } else {
-                    return "Could not identify a key manager.";
-                }
-            } else {
-                return "The data does not appear to be signed.";
-            }
-        } catch (err) {
-            return `Couldn't verify message: ${err}`;
-        }
-    },
-};
-
-export default PGP;

+ 0 - 118
src/core/operations/legacy/URL.js

@@ -1,118 +0,0 @@
-/* globals unescape */
-import url from "url";
-
-
-/**
- * URL operations.
- * Namespace is appended with an underscore to prevent overwriting the global URL object.
- *
- * @author n1474335 [n1474335@gmail.com]
- * @copyright Crown Copyright 2016
- * @license Apache-2.0
- *
- * @namespace
- */
-const URL_ = {
-
-    /**
-     * @constant
-     * @default
-     */
-    ENCODE_ALL: false,
-
-    /**
-     * URL Encode operation.
-     *
-     * @param {string} input
-     * @param {Object[]} args
-     * @returns {string}
-     */
-    runTo: function(input, args) {
-        const encodeAll = args[0];
-        return encodeAll ? URL_._encodeAllChars(input) : encodeURI(input);
-    },
-
-
-    /**
-     * URL Decode operation.
-     *
-     * @param {string} input
-     * @param {Object[]} args
-     * @returns {string}
-     */
-    runFrom: function(input, args) {
-        const data = input.replace(/\+/g, "%20");
-        try {
-            return decodeURIComponent(data);
-        } catch (err) {
-            return unescape(data);
-        }
-    },
-
-
-    /**
-     * Parse URI operation.
-     *
-     * @param {string} input
-     * @param {Object[]} args
-     * @returns {string}
-     */
-    runParse: function(input, args) {
-        const uri = url.parse(input, true);
-
-        let output = "";
-
-        if (uri.protocol) output += "Protocol:\t" + uri.protocol + "\n";
-        if (uri.auth) output += "Auth:\t\t" + uri.auth + "\n";
-        if (uri.hostname) output += "Hostname:\t" + uri.hostname + "\n";
-        if (uri.port) output += "Port:\t\t" + uri.port + "\n";
-        if (uri.pathname) output += "Path name:\t" + uri.pathname + "\n";
-        if (uri.query) {
-            let keys = Object.keys(uri.query),
-                padding = 0;
-
-            keys.forEach(k => {
-                padding = (k.length > padding) ? k.length : padding;
-            });
-
-            output += "Arguments:\n";
-            for (let key in uri.query) {
-                output += "\t" + key.padEnd(padding, " ");
-                if (uri.query[key].length) {
-                    output += " = " + uri.query[key] + "\n";
-                } else {
-                    output += "\n";
-                }
-            }
-        }
-        if (uri.hash) output += "Hash:\t\t" + uri.hash + "\n";
-
-        return output;
-    },
-
-
-    /**
-     * URL encodes additional special characters beyond the standard set.
-     *
-     * @private
-     * @param {string} str
-     * @returns {string}
-     */
-    _encodeAllChars: function(str) {
-        //TODO Do this programatically
-        return encodeURIComponent(str)
-            .replace(/!/g, "%21")
-            .replace(/#/g, "%23")
-            .replace(/'/g, "%27")
-            .replace(/\(/g, "%28")
-            .replace(/\)/g, "%29")
-            .replace(/\*/g, "%2A")
-            .replace(/-/g, "%2D")
-            .replace(/\./g, "%2E")
-            .replace(/_/g, "%5F")
-            .replace(/~/g, "%7E");
-    },
-
-};
-
-export default URL_;

+ 13 - 13
src/core/vendor/DisassembleX86-64.js → src/core/vendor/DisassembleX86-64.mjs

@@ -3316,7 +3316,7 @@ If input "type" is set 5 it will adjust the mnemonic array to decode Centaur ins
 If input "type" is set 6 it will adjust the mnemonic array to decode instruction for the X86/486 CPU which conflict with the vector unit instructions with UMOV.
 -------------------------------------------------------------------------------------------------------------------------*/
 
-function CompatibilityMode( type )
+export function CompatibilityMode( type )
 {
   //Reset the changeable sections of the Mnemonics array, and operand encoding array.
   
@@ -3515,7 +3515,7 @@ The function "GetPosition()" Gives back the current base address in it's proper
 If the hex input is invalid returns false.
 -------------------------------------------------------------------------------------------------------------------------*/
 
-function LoadBinCode( HexStr )
+export function LoadBinCode( HexStr )
 {
   //Clear BinCode, and Reset Code Position in Bin Code array.
 
@@ -3605,7 +3605,7 @@ segment, and offset address. Note that the Code Segment is used in 16 bit code.
 if set 36, or higher. Effects instruction location in memory when decoding a program.
 -------------------------------------------------------------------------------------------------------------------------*/
 
-function SetBasePosition( Address )
+export function SetBasePosition( Address )
 {
   //Split the Segment:offset.
 
@@ -5652,7 +5652,7 @@ function Reset()
 do an linear disassemble.
 -------------------------------------------------------------------------------------------------------------------------*/
 
-function LDisassemble()
+export function LDisassemble()
 {
   var Instruction = ""; //Stores the Decoded instruction.
   var Out = "";  //The Disassemble output
@@ -5709,13 +5709,13 @@ function LDisassemble()
  * The following code has been added to expose public methods for use in CyberChef
  */
 
-export default {
-  LoadBinCode: LoadBinCode,
-  LDisassemble: LDisassemble,
-  SetBasePosition: SetBasePosition,
-  CompatibilityMode: CompatibilityMode,
-
-  setBitMode: val => { BitMode = val; },
-  setShowInstructionHex: val => { ShowInstructionHex = val; },
-  setShowInstructionPos: val => { ShowInstructionPos = val; },
+export function setBitMode (val) {
+  BitMode = val; 
+};
+export function setShowInstructionHex (val) {
+  ShowInstructionHex = val;
 };
+export function setShowInstructionPos (val) {
+  ShowInstructionPos = val; 
+};
+

+ 0 - 186
src/core/vendor/canvascomponents.js

@@ -1,186 +0,0 @@
-"use strict";
-
-/**
- * Various components for drawing diagrams on an HTML5 canvas.
- *
- * @author n1474335 [n1474335@gmail.com]
- * @copyright Crown Copyright 2016
- * @license Apache-2.0
- *
- * @constant
- * @namespace
- */
-const CanvasComponents = {
-
-    drawLine: function(ctx, startX, startY, endX, endY) {
-        ctx.beginPath();
-        ctx.moveTo(startX, startY);
-        ctx.lineTo(endX, endY);
-        ctx.closePath();
-        ctx.stroke();
-    },
-
-    drawBarChart: function(canvas, scores, xAxisLabel, yAxisLabel, numXLabels, numYLabels, fontSize) {
-        fontSize = fontSize || 15;
-        if (!numXLabels || numXLabels > Math.round(canvas.width / 50)) {
-            numXLabels = Math.round(canvas.width / 50);
-        }
-        if (!numYLabels || numYLabels > Math.round(canvas.width / 50)) {
-            numYLabels = Math.round(canvas.height / 50);
-        }
-
-        // Graph properties
-        var ctx = canvas.getContext("2d"),
-            leftPadding = canvas.width * 0.08,
-            rightPadding = canvas.width * 0.03,
-            topPadding = canvas.height * 0.08,
-            bottomPadding = canvas.height * 0.15,
-            graphHeight = canvas.height - topPadding - bottomPadding,
-            graphWidth = canvas.width - leftPadding - rightPadding,
-            base = topPadding + graphHeight,
-            ceil = topPadding;
-
-        ctx.font = fontSize + "px Arial";
-
-        // Draw axis
-        ctx.lineWidth = "1.0";
-        ctx.strokeStyle = "#444";
-        CanvasComponents.drawLine(ctx, leftPadding, base, graphWidth + leftPadding, base); // x
-        CanvasComponents.drawLine(ctx, leftPadding, base, leftPadding, ceil); // y
-
-        // Bar properties
-        var barPadding = graphWidth * 0.003,
-            barWidth = (graphWidth - (barPadding * scores.length)) / scores.length,
-            currX = leftPadding + barPadding,
-            max = Math.max.apply(Math, scores);
-
-        // Draw bars
-        ctx.fillStyle = "green";
-        for (var i = 0; i < scores.length; i++) {
-            var h = scores[i] / max * graphHeight;
-            ctx.fillRect(currX, base - h, barWidth, h);
-            currX += barWidth + barPadding;
-        }
-
-        // Mark x axis
-        ctx.fillStyle = "black";
-        ctx.textAlign = "center";
-        currX = leftPadding + barPadding;
-        if (numXLabels >= scores.length) {
-            // Mark every score
-            for (i = 0; i <= scores.length; i++) {
-                ctx.fillText(i, currX, base + (bottomPadding * 0.3));
-                currX += barWidth + barPadding;
-            }
-        } else {
-            // Mark some scores
-            for (i = 0; i <= numXLabels; i++) {
-                var val = Math.ceil((scores.length / numXLabels) * i);
-                currX = (graphWidth / numXLabels) * i + leftPadding;
-                ctx.fillText(val, currX, base + (bottomPadding * 0.3));
-            }
-        }
-
-        // Mark y axis
-        ctx.textAlign = "right";
-        var currY;
-        if (numYLabels >= max) {
-            // Mark every increment
-            for (i = 0; i <= max; i++) {
-                currY = base - (i / max * graphHeight) + fontSize / 3;
-                ctx.fillText(i, leftPadding * 0.8, currY);
-            }
-        } else {
-            // Mark some increments
-            for (i = 0; i <= numYLabels; i++) {
-                val = Math.ceil((max / numYLabels) * i);
-                currY = base - (val / max * graphHeight) + fontSize / 3;
-                ctx.fillText(val, leftPadding * 0.8, currY);
-            }
-        }
-
-        // Label x axis
-        if (xAxisLabel) {
-            ctx.textAlign = "center";
-            ctx.fillText(xAxisLabel, graphWidth / 2 + leftPadding, base + bottomPadding * 0.8);
-        }
-
-        // Label y axis
-        if (yAxisLabel) {
-            ctx.save();
-            var x = leftPadding * 0.3,
-                y = graphHeight / 2 + topPadding;
-            ctx.translate(x, y);
-            ctx.rotate(-Math.PI / 2);
-            ctx.textAlign = "center";
-            ctx.fillText(yAxisLabel, 0, 0);
-            ctx.restore();
-        }
-    },
-
-    drawScaleBar: function(canvas, score, max, markings) {
-        // Bar properties
-        var ctx = canvas.getContext("2d"),
-            leftPadding = canvas.width * 0.01,
-            rightPadding = canvas.width * 0.01,
-            topPadding = canvas.height * 0.1,
-            bottomPadding = canvas.height * 0.3,
-            barHeight = canvas.height - topPadding - bottomPadding,
-            barWidth = canvas.width - leftPadding - rightPadding;
-
-        // Scale properties
-        var proportion = score / max;
-
-        // Draw bar outline
-        ctx.strokeRect(leftPadding, topPadding, barWidth, barHeight);
-
-        // Shade in up to proportion
-        var grad = ctx.createLinearGradient(leftPadding, 0, barWidth + leftPadding, 0);
-        grad.addColorStop(0, "green");
-        grad.addColorStop(0.5, "gold");
-        grad.addColorStop(1, "red");
-        ctx.fillStyle = grad;
-        ctx.fillRect(leftPadding, topPadding, barWidth * proportion, barHeight);
-
-        // Add markings
-        var x0, y0, x1, y1;
-        ctx.fillStyle = "black";
-        ctx.textAlign = "center";
-        ctx.font = "13px Arial";
-        for (var i = 0; i < markings.length; i++) {
-            // Draw min line down
-            x0 = barWidth / max * markings[i].min + leftPadding;
-            y0 = topPadding + barHeight + (bottomPadding * 0.1);
-            x1 = x0;
-            y1 = topPadding + barHeight + (bottomPadding * 0.3);
-            CanvasComponents.drawLine(ctx, x0, y0, x1, y1);
-
-            // Draw max line down
-            x0 = barWidth / max * markings[i].max + leftPadding;
-            x1 = x0;
-            CanvasComponents.drawLine(ctx, x0, y0, x1, y1);
-
-            // Join min and max lines
-            x0 = barWidth / max * markings[i].min + leftPadding;
-            y0 = topPadding + barHeight + (bottomPadding * 0.3);
-            x1 = barWidth / max * markings[i].max + leftPadding;
-            y1 = y0;
-            CanvasComponents.drawLine(ctx, x0, y0, x1, y1);
-
-            // Add label
-            if (markings[i].max >= max * 0.9) {
-                ctx.textAlign = "right";
-                x0 = x1;
-            } else if (markings[i].max <= max * 0.1) {
-                ctx.textAlign = "left";
-            } else {
-                x0 = x0 + (x1 - x0) / 2;
-            }
-            y0 = topPadding + barHeight + (bottomPadding * 0.8);
-            ctx.fillText(markings[i].label, x0, y0);
-        }
-    },
-
-};
-
-export default CanvasComponents;

+ 1 - 1
src/web/index.js

@@ -13,7 +13,7 @@ import "bootstrap";
 import "bootstrap-switch";
 import "bootstrap-colorpicker";
 import moment from "moment-timezone";
-import CanvasComponents from "../core/vendor/canvascomponents.js";
+import * as CanvasComponents from "../core/lib/CanvasComponents";
 
 // CyberChef
 import App from "./App";

+ 1 - 14
webpack.config.js

@@ -1,6 +1,5 @@
 const webpack = require("webpack");
 const ExtractTextPlugin = require("extract-text-webpack-plugin");
-const WebpackSyncShellPlugin = require("webpack-synchronizable-shell-plugin");
 
 /**
  * Webpack configuration details for use with Grunt.
@@ -43,19 +42,7 @@ module.exports = {
             raw: true,
             entryOnly: true
         }),
-        new ExtractTextPlugin("styles.css"),
-        new WebpackSyncShellPlugin({
-            onBuildStart: {
-                scripts: [
-                    "echo \n--- Generating config files. ---",
-                    "node --experimental-modules src/core/config/scripts/generateOpsIndex.mjs",
-                    "node --experimental-modules src/core/config/scripts/generateConfig.mjs",
-                    "echo --- Config scripts finished. ---\n"
-                ],
-                blocking: true,
-                parallel: false
-            }
-        })
+        new ExtractTextPlugin("styles.css")
     ],
     resolve: {
         alias: {

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác