diff --git a/src/core/ChefWorker.js b/src/core/ChefWorker.js index 9b3a8a41..91f9955d 100644 --- a/src/core/ChefWorker.js +++ b/src/core/ChefWorker.js @@ -104,12 +104,16 @@ async function bake(data) { self.postMessage({ action: "bakeComplete", - data: response + data: Object.assign(response, { + id: data.id + }) }); } catch (err) { self.postMessage({ action: "bakeError", - data: err + data: Object.assign(err, { + id: data.id + }) }); } } diff --git a/src/core/lib/Magic.mjs b/src/core/lib/Magic.mjs index 43e9dfe4..61e113b3 100644 --- a/src/core/lib/Magic.mjs +++ b/src/core/lib/Magic.mjs @@ -229,17 +229,22 @@ class Magic { const testEnc = async op => { for (let i = 0; i < encodings.length; i++) { const conf = { - op: op, - args: [encodings[i]] - }, - data = await this._runRecipe([conf], sample.buffer); + op: op, + args: [encodings[i]] + }; - // Only add to the results if it changed the data - if (!_buffersEqual(data, sample.buffer)) { - results.push({ - data: data, - conf: conf - }); + try { + const data = await this._runRecipe([conf], sample.buffer); + + // Only add to the results if it changed the data + if (!_buffersEqual(data, sample.buffer)) { + results.push({ + data: data, + conf: conf + }); + } + } catch (err) { + continue; } } }; @@ -344,6 +349,11 @@ class Magic { aScore += a.entropy; bScore += b.entropy; + // A result with no recipe but matching ops suggests there are better options + if ((!a.recipe.length && a.matchingOps.length) && + b.recipe.length) + return 1; + return aScore - bScore; }); } @@ -356,8 +366,9 @@ class Magic { * @returns {ArrayBuffer} */ async _runRecipe(recipeConfig, input=this.inputBuffer) { + input = input instanceof ArrayBuffer ? input : input.buffer; const dish = new Dish(); - dish.set(input.buffer, Dish.ARRAY_BUFFER); + dish.set(input, Dish.ARRAY_BUFFER); if (ENVIRONMENT_IS_WORKER()) self.loadRequiredModules(recipeConfig); diff --git a/src/core/operations/Magic.mjs b/src/core/operations/Magic.mjs index 849fbc8e..b44b7ccc 100644 --- a/src/core/operations/Magic.mjs +++ b/src/core/operations/Magic.mjs @@ -25,7 +25,8 @@ class Magic extends Operation { this.module = "Default"; this.description = "The Magic operation attempts to detect various properties of the input data and suggests which operations could help to make more sense of it.

Options
Depth: If an operation appears to match the data, it will be run and the result will be analysed further. This argument controls the maximum number of levels of recursion.

Intensive mode: When this is turned on, various operations like XOR, bit rotates, and character encodings are brute-forced to attempt to detect valid data underneath. To improve performance, only the first 100 bytes of the data is brute-forced.

Extensive language support: At each stage, the relative byte frequencies of the data will be compared to average frequencies for a number of languages. The default set consists of ~40 of the most commonly used languages on the Internet. The extensive list consists of 284 languages and can result in many languages matching the data if their byte frequencies are similar."; this.inputType = "ArrayBuffer"; - this.outputType = "html"; + this.outputType = "JSON"; + this.presentType = "html"; this.args = [ { "name": "Depth", @@ -56,10 +57,25 @@ class Magic extends Operation { const ings = state.opList[state.progress].ingValues, [depth, intensive, extLang] = ings, dish = state.dish, - currentRecipeConfig = state.opList.map(op => op.config), magic = new MagicLib(await dish.get(Dish.ARRAY_BUFFER)), options = await magic.speculativeExecution(depth, extLang, intensive); + // Record the current state for use when presenting + this.state = state; + + dish.set(options, Dish.JSON); + return state; + } + + /** + * Displays Magic results in HTML for web apps. + * + * @param {JSON} options + * @returns {html} + */ + present(options) { + const currentRecipeConfig = this.state.opList.map(op => op.config); + let output = ` @@ -84,9 +100,9 @@ class Magic extends Operation { options.forEach(option => { // Construct recipe URL // Replace this Magic op with the generated recipe - const recipeConfig = currentRecipeConfig.slice(0, state.progress) + const recipeConfig = currentRecipeConfig.slice(0, this.state.progress) .concat(option.recipe) - .concat(currentRecipeConfig.slice(state.progress + 1)), + .concat(currentRecipeConfig.slice(this.state.progress + 1)), recipeURL = "recipe=" + Utils.encodeURIFragment(Utils.generatePrettyRecipe(recipeConfig)); let language = "", @@ -131,8 +147,8 @@ class Magic extends Operation { if (!options.length) { output = "Nothing of interest could be detected about the input data.\nHave you tried modifying the operation arguments?"; } - dish.set(output, Dish.HTML); - return state; + + return output; } } diff --git a/src/web/BackgroundWorkerWaiter.mjs b/src/web/BackgroundWorkerWaiter.mjs new file mode 100644 index 00000000..340b9e76 --- /dev/null +++ b/src/web/BackgroundWorkerWaiter.mjs @@ -0,0 +1,156 @@ +/** + * @author n1474335 [n1474335@gmail.com] + * @copyright Crown Copyright 2018 + * @license Apache-2.0 + */ + +import ChefWorker from "worker-loader?inline&fallback=false!../core/ChefWorker"; + +/** + * Waiter to handle conversations with a ChefWorker in the background. + */ +class BackgroundWorkerWaiter { + + /** + * BackgroundWorkerWaiter constructor. + * + * @param {App} app - The main view object for CyberChef. + * @param {Manager} manager - The CyberChef event manager. + */ + constructor(app, manager) { + this.app = app; + this.manager = manager; + + this.callbacks = {}; + this.callbackID = 0; + this.completedCallback = -1; + this.timeout = null; + } + + + /** + * Sets up the ChefWorker and associated listeners. + */ + registerChefWorker() { + log.debug("Registering new background ChefWorker"); + this.chefWorker = new ChefWorker(); + this.chefWorker.addEventListener("message", this.handleChefMessage.bind(this)); + + let docURL = document.location.href.split(/[#?]/)[0]; + const index = docURL.lastIndexOf("/"); + if (index > 0) { + docURL = docURL.substring(0, index); + } + this.chefWorker.postMessage({"action": "docURL", "data": docURL}); + } + + + /** + * Handler for messages sent back by the ChefWorker. + * + * @param {MessageEvent} e + */ + handleChefMessage(e) { + const r = e.data; + log.debug("Receiving '" + r.action + "' from ChefWorker in the background"); + + switch (r.action) { + case "bakeComplete": + case "bakeError": + if (typeof r.data.id !== "undefined") { + clearTimeout(this.timeout); + this.callbacks[r.data.id].bind(this)(r.data); + this.completedCallback = r.data.id; + } + break; + case "workerLoaded": + log.debug("Background ChefWorker loaded"); + break; + case "optionUpdate": + // Ignore these messages + break; + default: + log.error("Unrecognised message from background ChefWorker", e); + break; + } + } + + + /** + * Cancels the current bake by terminating the ChefWorker and creating a new one. + */ + cancelBake() { + if (this.chefWorker) + this.chefWorker.terminate(); + this.registerChefWorker(); + } + + + /** + * Asks the ChefWorker to bake the input using the specified recipe. + * + * @param {string} input + * @param {Object[]} recipeConfig + * @param {Object} options + * @param {number} progress + * @param {boolean} step + * @param {Function} callback + */ + bake(input, recipeConfig, options, progress, step, callback) { + const id = this.callbackID++; + this.callbacks[id] = callback; + + this.chefWorker.postMessage({ + action: "bake", + data: { + input: input, + recipeConfig: recipeConfig, + options: options, + progress: progress, + step: step, + id: id + } + }); + } + + + /** + * Asks the Magic operation what it can do with the input data. + * + * @param {string|ArrayBuffer} input + */ + magic(input) { + // If we're still working on the previous bake, cancel it before stating a new one. + if (this.completedCallback + 1 < this.callbackID) { + clearTimeout(this.timeout); + this.cancelBake(); + } + + this.bake(input, [ + { + "op": "Magic", + "args": [3, false, false] + } + ], {}, 0, false, this.magicComplete); + + // Cancel this bake if it takes too long. + this.timeout = setTimeout(this.cancelBake.bind(this), 3000); + } + + + /** + * Handler for completed Magic bakes. + * + * @param {Object} response + */ + magicComplete(response) { + log.debug("--- Background Magic Bake complete ---"); + if (!response || response.error) return; + + this.manager.output.backgroundMagicResult(response.dish.value); + } + +} + + +export default BackgroundWorkerWaiter; diff --git a/src/web/Manager.mjs b/src/web/Manager.mjs index 4f004edc..4ef7de07 100755 --- a/src/web/Manager.mjs +++ b/src/web/Manager.mjs @@ -15,6 +15,7 @@ import OptionsWaiter from "./OptionsWaiter"; import HighlighterWaiter from "./HighlighterWaiter"; import SeasonalWaiter from "./SeasonalWaiter"; import BindingsWaiter from "./BindingsWaiter"; +import BackgroundWorkerWaiter from "./BackgroundWorkerWaiter"; /** @@ -68,6 +69,7 @@ class Manager { this.highlighter = new HighlighterWaiter(this.app, this); this.seasonal = new SeasonalWaiter(this.app, this); this.bindings = new BindingsWaiter(this.app, this); + this.background = new BackgroundWorkerWaiter(this.app, this); // Object to store dynamic handlers to fire on elements that may not exist yet this.dynamicHandlers = {}; @@ -84,6 +86,7 @@ class Manager { this.recipe.initialiseOperationDragNDrop(); this.controls.autoBakeChange(); this.bindings.updateKeybList(); + this.background.registerChefWorker(); this.seasonal.load(); } diff --git a/src/web/OutputWaiter.mjs b/src/web/OutputWaiter.mjs index 166dde0f..1b6a5878 100755 --- a/src/web/OutputWaiter.mjs +++ b/src/web/OutputWaiter.mjs @@ -117,6 +117,7 @@ class OutputWaiter { this.manager.highlighter.removeHighlights(); this.setOutputInfo(length, lines, duration); + this.backgroundMagic(); } @@ -444,6 +445,42 @@ class OutputWaiter { return this.dishBuffer; } + + /** + * Triggers the BackgroundWorker to attempt Magic on the current output. + */ + backgroundMagic() { + const sample = this.dishStr ? this.dishStr.slice(0, 1000) : + this.dishBuffer ? this.dishBuffer.slice(0, 1000) : ""; + + if (sample.length) { + this.manager.background.magic(sample); + } + } + + + /** + * Handles the results of a background Magic call. + * + * @param {Object[]} options + */ + backgroundMagicResult(options) { + if (!options.length || + !options[0].recipe.length) + return; + + //console.log(options); + + const currentRecipeConfig = this.app.getRecipeConfig(); + const newRecipeConfig = currentRecipeConfig.concat(options[0].recipe); + const recipeURL = "#recipe=" + Utils.encodeURIFragment(Utils.generatePrettyRecipe(newRecipeConfig)); + const opSequence = options[0].recipe.map(o => o.op).join(", "); + + log.log(`Running ${opSequence} will result in "${Utils.truncate(options[0].data, 20)}"`); + //this.app.setRecipeConfig(newRecipeConfig); + //this.app.autoBake(); + } + } export default OutputWaiter;