What
diff --git a/src/web/index.js b/src/web/index.js
index 1d2bc242..736b512f 100755
--- a/src/web/index.js
+++ b/src/web/index.js
@@ -53,7 +53,9 @@ function main() {
logLevel: "info",
autoMagic: true,
imagePreview: true,
- syncTabs: true
+ syncTabs: true,
+ preserveCR: true,
+ userSetCR: false
};
document.removeEventListener("DOMContentLoaded", main, false);
diff --git a/src/web/stylesheets/layout/_io.css b/src/web/stylesheets/layout/_io.css
index d7d628cb..2c8be70e 100755
--- a/src/web/stylesheets/layout/_io.css
+++ b/src/web/stylesheets/layout/_io.css
@@ -337,6 +337,29 @@
fill: var(--primary-font-colour);
}
+.pulse {
+ box-shadow: 0 0 0 0 rgba(90, 153, 212, .3);
+ animation: pulse 1.5s 1;
+}
+
+.pulse:hover {
+ animation-play-state: paused;
+}
+
+@keyframes pulse {
+ 0% {
+ transform: scale(1);
+ }
+ 70% {
+ transform: scale(1.1);
+ box-shadow: 0 0 0 20px rgba(90, 153, 212, 0);
+ }
+ 100% {
+ transform: scale(1);
+ box-shadow: 0 0 0 0 rgba(90, 153, 212, 0);
+ }
+}
+
#input-find-options,
#output-find-options {
display: flex;
diff --git a/src/web/stylesheets/layout/_structure.css b/src/web/stylesheets/layout/_structure.css
index 77902d09..62ea1f9d 100755
--- a/src/web/stylesheets/layout/_structure.css
+++ b/src/web/stylesheets/layout/_structure.css
@@ -39,10 +39,16 @@ div#output {
.split {
box-sizing: border-box;
- overflow: auto;
+ /* overflow: auto;
+ Removed to enable Background Magic button pulse to overflow.
+ Replace this rule if it seems to be causing problems. */
position: relative;
}
+#operations {
+ overflow: auto;
+}
+
.split.split-horizontal, .gutter.gutter-horizontal {
height: 100%;
float: left;
diff --git a/src/web/waiters/InputWaiter.mjs b/src/web/waiters/InputWaiter.mjs
index 40ec412f..e519b963 100644
--- a/src/web/waiters/InputWaiter.mjs
+++ b/src/web/waiters/InputWaiter.mjs
@@ -222,8 +222,6 @@ class InputWaiter {
if (Object.prototype.hasOwnProperty.call(r, "progress") &&
Object.prototype.hasOwnProperty.call(r, "inputNum")) {
this.manager.tabs.updateInputTabProgress(r.inputNum, r.progress, 100);
- } else if (Object.prototype.hasOwnProperty.call(r, "fileBuffer")) {
- this.manager.tabs.updateInputTabProgress(r.inputNum, 100, 100);
}
const transferable = Object.prototype.hasOwnProperty.call(r, "fileBuffer") ? [r.fileBuffer] : undefined;
@@ -305,6 +303,9 @@ class InputWaiter {
case "removeChefWorker":
this.removeChefWorker();
break;
+ case "fileLoaded":
+ this.fileLoaded(r.data.inputNum);
+ break;
default:
log.error(`Unknown action ${r.action}.`);
}
@@ -331,7 +332,7 @@ class InputWaiter {
* @param {number} inputData.size - The size in bytes of the input file
* @param {string} inputData.type - The MIME type of the input file
* @param {number} inputData.progress - The load progress of the input file
- * @param {boolean} [silent=false] - If true, fires the manager statechange event
+ * @param {boolean} [silent=false] - If false, fires the manager statechange event
*/
async set(inputData, silent=false) {
return new Promise(function(resolve, reject) {
@@ -373,7 +374,7 @@ class InputWaiter {
if (!silent) window.dispatchEvent(this.manager.statechange);
} else {
- this.setFile(inputData);
+ this.setFile(inputData, silent);
}
}.bind(this));
@@ -389,8 +390,9 @@ class InputWaiter {
* @param {number} inputData.size - The size in bytes of the input file
* @param {string} inputData.type - The MIME type of the input file
* @param {number} inputData.progress - The load progress of the input file
+ * @param {boolean} [silent=true] - If false, fires the manager statechange event
*/
- setFile(inputData) {
+ setFile(inputData, silent=true) {
const activeTab = this.manager.tabs.getActiveInputTab();
if (inputData.inputNum !== activeTab) return;
@@ -414,6 +416,30 @@ class InputWaiter {
this.setInputInfo(inputData.size, null);
this.displayFilePreview(inputData);
+
+ if (!silent) window.dispatchEvent(this.manager.statechange);
+ }
+
+ /**
+ * Update file details when a file completes loading
+ *
+ * @param {number} inputNum - The inputNum of the input which has finished loading
+ */
+ fileLoaded(inputNum) {
+ this.manager.tabs.updateInputTabProgress(inputNum, 100, 100);
+
+ const activeTab = this.manager.tabs.getActiveInputTab();
+ if (activeTab !== inputNum) return;
+
+ this.inputWorker.postMessage({
+ action: "setInput",
+ data: {
+ inputNum: inputNum,
+ silent: false
+ }
+ });
+
+ this.updateFileProgress(inputNum, 100);
}
/**
@@ -495,19 +521,6 @@ class InputWaiter {
fileLoaded.textContent = progress + "%";
fileLoaded.style.color = "";
}
-
- if (progress === 100 && progress !== oldProgress) {
- // Don't set the input if the progress hasn't changed
- this.inputWorker.postMessage({
- action: "setInput",
- data: {
- inputNum: inputNum,
- silent: false
- }
- });
- window.dispatchEvent(this.manager.statechange);
-
- }
}
/**
@@ -711,33 +724,50 @@ class InputWaiter {
*
* @param {event} e
*/
- inputPaste(e) {
- const pastedData = e.clipboardData.getData("Text");
- if (pastedData.length < (this.app.options.ioDisplayThreshold * 1024)) {
- // Pasting normally fires the inputChange() event before
- // changing the value, so instead change it here ourselves
- // and manually fire inputChange()
- e.preventDefault();
- const inputText = document.getElementById("input-text");
- const selStart = inputText.selectionStart;
- const selEnd = inputText.selectionEnd;
- const startVal = inputText.value.slice(0, selStart);
- const endVal = inputText.value.slice(selEnd);
-
- inputText.value = startVal + pastedData + endVal;
- inputText.setSelectionRange(selStart + pastedData.length, selStart + pastedData.length);
- this.debounceInputChange(e);
- } else {
- e.preventDefault();
- e.stopPropagation();
+ async inputPaste(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ const self = this;
+ /**
+ * Triggers the input file/binary data overlay
+ *
+ * @param {string} pastedData
+ */
+ function triggerOverlay(pastedData) {
const file = new File([pastedData], "PastedData", {
type: "text/plain",
lastModified: Date.now()
});
- this.loadUIFiles([file]);
+ self.loadUIFiles([file]);
+ }
+
+ const pastedData = e.clipboardData.getData("Text");
+ const inputText = document.getElementById("input-text");
+ const selStart = inputText.selectionStart;
+ const selEnd = inputText.selectionEnd;
+ const startVal = inputText.value.slice(0, selStart);
+ const endVal = inputText.value.slice(selEnd);
+ const val = startVal + pastedData + endVal;
+
+ if (val.length >= (this.app.options.ioDisplayThreshold * 1024)) {
+ // Data too large to display, use overlay
+ triggerOverlay(val);
return false;
+ } else if (await this.preserveCarriageReturns(val)) {
+ // Data contains a carriage return and the user doesn't wish to edit it, use overlay
+ // We check this in a separate condition to make sure it is not run unless absolutely
+ // necessary.
+ triggerOverlay(val);
+ return false;
+ } else {
+ // Pasting normally fires the inputChange() event before
+ // changing the value, so instead change it here ourselves
+ // and manually fire inputChange()
+ inputText.value = val;
+ inputText.setSelectionRange(selStart + pastedData.length, selStart + pastedData.length);
+ this.debounceInputChange(e);
}
}
@@ -815,6 +845,46 @@ class InputWaiter {
}
}
+ /**
+ * Checks if an input contains carriage returns.
+ * If a CR is detected, checks if the preserve CR option has been set,
+ * and if not, asks the user for their preference.
+ *
+ * @param {string} input - The input to be checked
+ * @returns {boolean} - If true, the input contains a CR which should be
+ * preserved, so display an overlay so it can't be edited
+ */
+ async preserveCarriageReturns(input) {
+ if (input.indexOf("\r") < 0) return false;
+
+ const optionsStr = "This behaviour can be changed in the
Options pane";
+ if (!this.app.options.userSetCR) {
+ // User has not set a CR preference yet
+ let preserve = await new Promise(function(resolve, reject) {
+ this.app.confirm(
+ "Carriage Return Detected",
+ "A
carriage return (
\\r
,
0x0d
) was detected in your input. As HTML textareas
can't display carriage returns, editing must be turned off to preserve them.
Alternatively, you can enable editing but your carriage returns will not be preserved.
This preference will be saved but can be toggled in the options pane.",
+ "Preserve Carriage Returns",
+ "Enable Editing", resolve, this);
+ }.bind(this));
+ if (preserve === undefined) {
+ // The confirm pane was closed without picking a specific choice
+ this.app.alert(`Not preserving carriage returns.\n${optionsStr}`, 5000);
+ preserve = false;
+ }
+ this.manager.options.updateOption("preserveCR", preserve);
+ this.manager.options.updateOption("userSetCR", true);
+ } else {
+ if (this.app.options.preserveCR) {
+ this.app.alert(`A carriage return (\\r, 0x0d) was detected in your input, so editing has been disabled to preserve it.
${optionsStr}`, 10000);
+ } else {
+ this.app.alert(`A carriage return (\\r, 0x0d) was detected in your input. Editing is remaining enabled, but carriage returns will not be preserved.
${optionsStr}`, 10000);
+ }
+ }
+
+ return this.app.options.preserveCR;
+ }
+
/**
* Load files from the UI into the inputWorker
*
diff --git a/src/web/waiters/OptionsWaiter.mjs b/src/web/waiters/OptionsWaiter.mjs
index eb6bac18..3dde372d 100755
--- a/src/web/waiters/OptionsWaiter.mjs
+++ b/src/web/waiters/OptionsWaiter.mjs
@@ -1,174 +1,180 @@
/**
- * Waiter to handle events related to the CyberChef options.
- *
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2016
* @license Apache-2.0
- *
- * @constructor
- * @param {App} app - The main view object for CyberChef.
*/
-const OptionsWaiter = function(app, manager) {
- this.app = app;
- this.manager = manager;
-};
-
/**
- * Loads options and sets values of switches and inputs to match them.
- *
- * @param {Object} options
+ * Waiter to handle events related to the CyberChef options.
*/
-OptionsWaiter.prototype.load = function(options) {
- for (const option in options) {
- this.app.options[option] = options[option];
+class OptionsWaiter {
+
+ /**
+ * OptionsWaiter 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;
}
- // Set options to match object
- const cboxes = document.querySelectorAll("#options-body input[type=checkbox]");
- let i;
- for (i = 0; i < cboxes.length; i++) {
- cboxes[i].checked = this.app.options[cboxes[i].getAttribute("option")];
- }
+ /**
+ * Loads options and sets values of switches and inputs to match them.
+ *
+ * @param {Object} options
+ */
+ load(options) {
+ for (const option in options) {
+ this.app.options[option] = options[option];
+ }
- const nboxes = document.querySelectorAll("#options-body input[type=number]");
- for (i = 0; i < nboxes.length; i++) {
- nboxes[i].value = this.app.options[nboxes[i].getAttribute("option")];
- nboxes[i].dispatchEvent(new CustomEvent("change", {bubbles: true}));
- }
+ // Set options to match object
+ const cboxes = document.querySelectorAll("#options-body input[type=checkbox]");
+ let i;
+ for (i = 0; i < cboxes.length; i++) {
+ cboxes[i].checked = this.app.options[cboxes[i].getAttribute("option")];
+ }
- const selects = document.querySelectorAll("#options-body select");
- for (i = 0; i < selects.length; i++) {
- const val = this.app.options[selects[i].getAttribute("option")];
- if (val) {
- selects[i].value = val;
- selects[i].dispatchEvent(new CustomEvent("change", {bubbles: true}));
- } else {
- selects[i].selectedIndex = 0;
+ const nboxes = document.querySelectorAll("#options-body input[type=number]");
+ for (i = 0; i < nboxes.length; i++) {
+ nboxes[i].value = this.app.options[nboxes[i].getAttribute("option")];
+ nboxes[i].dispatchEvent(new CustomEvent("change", {bubbles: true}));
+ }
+
+ const selects = document.querySelectorAll("#options-body select");
+ for (i = 0; i < selects.length; i++) {
+ const val = this.app.options[selects[i].getAttribute("option")];
+ if (val) {
+ selects[i].value = val;
+ selects[i].dispatchEvent(new CustomEvent("change", {bubbles: true}));
+ } else {
+ selects[i].selectedIndex = 0;
+ }
}
}
-};
-/**
- * Handler for options click events.
- * Dispays the options pane.
- *
- * @param {event} e
- */
-OptionsWaiter.prototype.optionsClick = function(e) {
- e.preventDefault();
- $("#options-modal").modal();
-};
-
-
-/**
- * Handler for reset options click events.
- * Resets options back to their default values.
- */
-OptionsWaiter.prototype.resetOptionsClick = function() {
- this.load(this.app.doptions);
-};
-
-
-/**
- * Handler for switch change events.
- * Modifies the option state and saves it to local storage.
- *
- * @param {event} e
- */
-OptionsWaiter.prototype.switchChange = function(e) {
- const el = e.target;
- const option = el.getAttribute("option");
- const state = el.checked;
-
- log.debug(`Setting ${option} to ${state}`);
- this.app.options[option] = state;
-
- if (this.app.isLocalStorageAvailable())
- localStorage.setItem("options", JSON.stringify(this.app.options));
-};
-
-
-/**
- * Handler for number change events.
- * Modifies the option value and saves it to local storage.
- *
- * @param {event} e
- */
-OptionsWaiter.prototype.numberChange = function(e) {
- const el = e.target;
- const option = el.getAttribute("option");
- const val = parseInt(el.value, 10);
-
- log.debug(`Setting ${option} to ${val}`);
- this.app.options[option] = val;
-
- if (this.app.isLocalStorageAvailable())
- localStorage.setItem("options", JSON.stringify(this.app.options));
-};
-
-
-/**
- * Handler for select change events.
- * Modifies the option value and saves it to local storage.
- *
- * @param {event} e
- */
-OptionsWaiter.prototype.selectChange = function(e) {
- const el = e.target;
- const option = el.getAttribute("option");
-
- log.debug(`Setting ${option} to ${el.value}`);
- this.app.options[option] = el.value;
-
- if (this.app.isLocalStorageAvailable())
- localStorage.setItem("options", JSON.stringify(this.app.options));
-};
-
-
-/**
- * Sets or unsets word wrap on the input and output depending on the wordWrap option value.
- */
-OptionsWaiter.prototype.setWordWrap = function() {
- document.getElementById("input-text").classList.remove("word-wrap");
- document.getElementById("output-text").classList.remove("word-wrap");
- document.getElementById("output-html").classList.remove("word-wrap");
- document.getElementById("input-highlighter").classList.remove("word-wrap");
- document.getElementById("output-highlighter").classList.remove("word-wrap");
-
- if (!this.app.options.wordWrap) {
- document.getElementById("input-text").classList.add("word-wrap");
- document.getElementById("output-text").classList.add("word-wrap");
- document.getElementById("output-html").classList.add("word-wrap");
- document.getElementById("input-highlighter").classList.add("word-wrap");
- document.getElementById("output-highlighter").classList.add("word-wrap");
+ /**
+ * Handler for options click events.
+ * Dispays the options pane.
+ *
+ * @param {event} e
+ */
+ optionsClick(e) {
+ e.preventDefault();
+ $("#options-modal").modal();
}
-};
-/**
- * Changes the theme by setting the class of the element.
- *
- * @param {Event} e
- */
-OptionsWaiter.prototype.themeChange = function (e) {
- const themeClass = e.target.value;
-
- document.querySelector(":root").className = themeClass;
-};
+ /**
+ * Handler for reset options click events.
+ * Resets options back to their default values.
+ */
+ resetOptionsClick() {
+ this.load(this.app.doptions);
+ }
-/**
- * Changes the console logging level.
- *
- * @param {Event} e
- */
-OptionsWaiter.prototype.logLevelChange = function (e) {
- const level = e.target.value;
- log.setLevel(level, false);
- this.manager.worker.setLogLevel();
- this.manager.input.setLogLevel();
-};
+ /**
+ * Handler for switch change events.
+ *
+ * @param {event} e
+ */
+ switchChange(e) {
+ const el = e.target;
+ const option = el.getAttribute("option");
+ const state = el.checked;
+
+ this.updateOption(option, state);
+ }
+
+
+ /**
+ * Handler for number change events.
+ *
+ * @param {event} e
+ */
+ numberChange(e) {
+ const el = e.target;
+ const option = el.getAttribute("option");
+ const val = parseInt(el.value, 10);
+
+ this.updateOption(option, val);
+ }
+
+
+ /**
+ * Handler for select change events.
+ *
+ * @param {event} e
+ */
+ selectChange(e) {
+ const el = e.target;
+ const option = el.getAttribute("option");
+
+ this.updateOption(option, el.value);
+ }
+
+ /**
+ * Modifies an option value and saves it to local storage.
+ *
+ * @param {string} option - The option to be updated
+ * @param {string|number|boolean} value - The new value of the option
+ */
+ updateOption(option, value) {
+ log.debug(`Setting ${option} to ${value}`);
+ this.app.options[option] = value;
+
+ if (this.app.isLocalStorageAvailable())
+ localStorage.setItem("options", JSON.stringify(this.app.options));
+ }
+
+
+ /**
+ * Sets or unsets word wrap on the input and output depending on the wordWrap option value.
+ */
+ setWordWrap() {
+ document.getElementById("input-text").classList.remove("word-wrap");
+ document.getElementById("output-text").classList.remove("word-wrap");
+ document.getElementById("output-html").classList.remove("word-wrap");
+ document.getElementById("input-highlighter").classList.remove("word-wrap");
+ document.getElementById("output-highlighter").classList.remove("word-wrap");
+
+ if (!this.app.options.wordWrap) {
+ document.getElementById("input-text").classList.add("word-wrap");
+ document.getElementById("output-text").classList.add("word-wrap");
+ document.getElementById("output-html").classList.add("word-wrap");
+ document.getElementById("input-highlighter").classList.add("word-wrap");
+ document.getElementById("output-highlighter").classList.add("word-wrap");
+ }
+ }
+
+
+ /**
+ * Changes the theme by setting the class of the element.
+ *
+ * @param {Event} e
+ */
+ themeChange(e) {
+ const themeClass = e.target.value;
+
+ document.querySelector(":root").className = themeClass;
+ }
+
+
+ /**
+ * Changes the console logging level.
+ *
+ * @param {Event} e
+ */
+ logLevelChange(e) {
+ const level = e.target.value;
+ log.setLevel(level, false);
+ this.manager.worker.setLogLevel();
+ this.manager.input.setLogLevel();
+ }
+}
export default OptionsWaiter;
diff --git a/src/web/waiters/OutputWaiter.mjs b/src/web/waiters/OutputWaiter.mjs
index 005d9533..4a08fe8d 100755
--- a/src/web/waiters/OutputWaiter.mjs
+++ b/src/web/waiters/OutputWaiter.mjs
@@ -217,6 +217,9 @@ class OutputWaiter {
*/
removeAllOutputs() {
this.outputs = {};
+
+ this.resetSwitch();
+
const tabsList = document.getElementById("output-tabs");
const tabsListChildren = tabsList.children;
@@ -516,9 +519,10 @@ class OutputWaiter {
this.app.alert("Could not find any output data to download. Has this output been baked?", 3000);
return;
}
- let fileName = window.prompt("Please enter a filename: ", "download.dat");
+ const fileName = window.prompt("Please enter a filename: ", "download.dat");
- if (fileName === null) fileName = "download.dat";
+ // Assume if the user clicks cancel they don't want to download
+ if (fileName === null) return;
const data = await dish.get(Dish.ARRAY_BUFFER),
file = new File([data], fileName);
@@ -529,12 +533,22 @@ class OutputWaiter {
* Handler for save all click event
* Saves all outputs to a single archvie file
*/
- saveAllClick() {
+ async saveAllClick() {
const downloadButton = document.getElementById("save-all-to-file");
if (downloadButton.firstElementChild.innerHTML === "archive") {
this.downloadAllFiles();
- } else if (window.confirm("Cancel zipping of outputs?")) {
- this.terminateZipWorker();
+ } else {
+ const cancel = await new Promise(function(resolve, reject) {
+ this.app.confirm(
+ "Cancel zipping?",
+ "The outputs are currently being zipped for download.
Cancel zipping?",
+ "Continue zipping",
+ "Cancel zipping",
+ resolve, this);
+ }.bind(this));
+ if (!cancel) {
+ this.terminateZipWorker();
+ }
}
}
@@ -544,57 +558,61 @@ class OutputWaiter {
* be zipped for download
*/
async downloadAllFiles() {
- return new Promise(resolve => {
- const inputNums = Object.keys(this.outputs);
- for (let i = 0; i < inputNums.length; i++) {
- const iNum = inputNums[i];
- if (this.outputs[iNum].status !== "baked" ||
- this.outputs[iNum].bakeId !== this.manager.worker.bakeId) {
- if (window.confirm("Not all outputs have been baked yet. Continue downloading outputs?")) {
- break;
- } else {
- return;
- }
+ const inputNums = Object.keys(this.outputs);
+ for (let i = 0; i < inputNums.length; i++) {
+ const iNum = inputNums[i];
+ if (this.outputs[iNum].status !== "baked" ||
+ this.outputs[iNum].bakeId !== this.manager.worker.bakeId) {
+ const continueDownloading = await new Promise(function(resolve, reject) {
+ this.app.confirm(
+ "Incomplete outputs",
+ "Not all outputs have been baked yet. Continue downloading outputs?",
+ "Download", "Cancel", resolve, this);
+ }.bind(this));
+ if (continueDownloading) {
+ break;
+ } else {
+ return;
}
}
+ }
- let fileName = window.prompt("Please enter a filename: ", "download.zip");
+ let fileName = window.prompt("Please enter a filename: ", "download.zip");
- if (fileName === null || fileName === "") {
- // Don't zip the files if there isn't a filename
- this.app.alert("No filename was specified.", 3000);
- return;
- }
+ if (fileName === null || fileName === "") {
+ // Don't zip the files if there isn't a filename
+ this.app.alert("No filename was specified.", 3000);
+ return;
+ }
- if (!fileName.match(/.zip$/)) {
- fileName += ".zip";
- }
+ if (!fileName.match(/.zip$/)) {
+ fileName += ".zip";
+ }
- let fileExt = window.prompt("Please enter a file extension for the files, or leave blank to detect automatically.", "");
+ let fileExt = window.prompt("Please enter a file extension for the files, or leave blank to detect automatically.", "");
- if (fileExt === null) fileExt = "";
+ if (fileExt === null) fileExt = "";
- if (this.zipWorker !== null) {
- this.terminateZipWorker();
- }
+ if (this.zipWorker !== null) {
+ this.terminateZipWorker();
+ }
- const downloadButton = document.getElementById("save-all-to-file");
+ const downloadButton = document.getElementById("save-all-to-file");
- downloadButton.classList.add("spin");
- downloadButton.title = `Zipping ${inputNums.length} files...`;
- downloadButton.setAttribute("data-original-title", `Zipping ${inputNums.length} files...`);
+ downloadButton.classList.add("spin");
+ downloadButton.title = `Zipping ${inputNums.length} files...`;
+ downloadButton.setAttribute("data-original-title", `Zipping ${inputNums.length} files...`);
- downloadButton.firstElementChild.innerHTML = "autorenew";
+ downloadButton.firstElementChild.innerHTML = "autorenew";
- log.debug("Creating ZipWorker");
- this.zipWorker = new ZipWorker();
- this.zipWorker.postMessage({
- outputs: this.outputs,
- filename: fileName,
- fileExtension: fileExt
- });
- this.zipWorker.addEventListener("message", this.handleZipWorkerMessage.bind(this));
+ log.debug("Creating ZipWorker");
+ this.zipWorker = new ZipWorker();
+ this.zipWorker.postMessage({
+ outputs: this.outputs,
+ filename: fileName,
+ fileExtension: fileExt
});
+ this.zipWorker.addEventListener("message", this.handleZipWorkerMessage.bind(this));
}
/**
@@ -1065,6 +1083,7 @@ class OutputWaiter {
magicButton.setAttribute("data-original-title", `
${opSequence} will produce
"${Utils.escapeHtml(Utils.truncate(result), 30)}"`);
magicButton.setAttribute("data-recipe", JSON.stringify(recipeConfig), null, "");
magicButton.classList.remove("hidden");
+ magicButton.classList.add("pulse");
}
@@ -1074,6 +1093,7 @@ class OutputWaiter {
hideMagicButton() {
const magicButton = document.getElementById("magic");
magicButton.classList.add("hidden");
+ magicButton.classList.remove("pulse");
magicButton.setAttribute("data-recipe", "");
magicButton.setAttribute("data-original-title", "Magic!");
}
@@ -1213,14 +1233,39 @@ class OutputWaiter {
* Moves the current output into the input textarea.
*/
async switchClick() {
- const active = await this.getDishBuffer(this.getOutputDish(this.manager.tabs.getActiveOutputTab()));
+ const activeTab = this.manager.tabs.getActiveOutputTab();
+ const transferable = [];
+
+ const switchButton = document.getElementById("switch");
+ switchButton.classList.add("spin");
+ switchButton.disabled = true;
+ switchButton.firstElementChild.innerHTML = "autorenew";
+ $(switchButton).tooltip("hide");
+
+ let active = await this.getDishBuffer(this.getOutputDish(activeTab));
+
+ if (!this.outputExists(activeTab)) {
+ this.resetSwitchButton();
+ return;
+ }
+
+ if (this.outputs[activeTab].data.type === "string" &&
+ active.byteLength <= this.app.options.ioDisplayThreshold * 1024) {
+ const dishString = await this.getDishStr(this.getOutputDish(activeTab));
+ if (!await this.manager.input.preserveCarriageReturns(dishString)) {
+ active = dishString;
+ }
+ } else {
+ transferable.push(active);
+ }
+
this.manager.input.inputWorker.postMessage({
action: "inputSwitch",
data: {
- inputNum: this.manager.tabs.getActiveInputTab(),
+ inputNum: activeTab,
outputData: active
}
- }, [active]);
+ }, transferable);
}
/**
@@ -1238,6 +1283,9 @@ class OutputWaiter {
inputSwitch(switchData) {
this.switchOrigData = switchData;
document.getElementById("undo-switch").disabled = false;
+
+ this.resetSwitchButton();
+
}
/**
@@ -1246,17 +1294,35 @@ class OutputWaiter {
*/
undoSwitchClick() {
this.manager.input.updateInputObj(this.switchOrigData.inputNum, this.switchOrigData.data);
+
+ this.manager.input.fileLoaded(this.switchOrigData.inputNum);
+
+ this.resetSwitch();
+ }
+
+ /**
+ * Removes the switch data and resets the switch buttons
+ */
+ resetSwitch() {
+ if (this.switchOrigData !== undefined) {
+ delete this.switchOrigData;
+ }
+
const undoSwitch = document.getElementById("undo-switch");
undoSwitch.disabled = true;
$(undoSwitch).tooltip("hide");
- this.manager.input.inputWorker.postMessage({
- action: "setInput",
- data: {
- inputNum: this.switchOrigData.inputNum,
- silent: false
- }
- });
+ this.resetSwitchButton();
+ }
+
+ /**
+ * Resets the switch button to its usual state
+ */
+ resetSwitchButton() {
+ const switchButton = document.getElementById("switch");
+ switchButton.classList.remove("spin");
+ switchButton.disabled = false;
+ switchButton.firstElementChild.innerHTML = "open_in_browser";
}
/**
diff --git a/src/web/workers/InputWorker.mjs b/src/web/workers/InputWorker.mjs
index cec68627..521539c5 100644
--- a/src/web/workers/InputWorker.mjs
+++ b/src/web/workers/InputWorker.mjs
@@ -202,6 +202,7 @@ self.bakeInput = function(inputNum, bakeId) {
if (inputObj === null ||
inputObj === undefined ||
inputObj.status !== "loaded") {
+
self.postMessage({
action: "queueInputError",
data: {
@@ -441,7 +442,7 @@ self.updateTabHeader = function(inputNum) {
*
* @param {object} inputData
* @param {number} inputData.inputNum - The input to get the data for
- * @param {boolean} inputData.silent - If false, the manager statechange event won't be fired
+ * @param {boolean} inputData.silent - If false, the manager statechange event will be fired
*/
self.setInput = function(inputData) {
const inputNum = inputData.inputNum;
@@ -590,7 +591,7 @@ self.updateInputObj = function(inputData) {
const inputNum = inputData.inputNum;
const data = inputData.data;
- if (self.getInputObj(inputNum) === -1) return;
+ if (self.getInputObj(inputNum) === undefined) return;
self.inputs[inputNum].data = data;
};
@@ -663,11 +664,19 @@ self.handleLoaderMessage = function(r) {
if ("fileBuffer" in r) {
log.debug(`Input file ${inputNum} loaded.`);
self.loadingInputs--;
+
self.updateInputValue({
inputNum: inputNum,
value: r.fileBuffer
});
+ self.postMessage({
+ action: "fileLoaded",
+ data: {
+ inputNum: inputNum
+ }
+ });
+
const idx = self.getLoaderWorkerIdx(r.id);
self.loadNextFile(idx);
} else if ("progress" in r) {
@@ -782,7 +791,7 @@ self.loadFiles = function(filesData) {
}
self.getLoadProgress();
- self.setInput({inputNum: activeTab, silent: false});
+ self.setInput({inputNum: activeTab, silent: true});
};
/**
@@ -1025,7 +1034,7 @@ self.inputSwitch = function(switchData) {
const currentData = currentInput.data;
if (currentInput === undefined || currentInput === null) return;
- if (typeof switchData.outputData === "object") {
+ if (typeof switchData.outputData !== "string") {
const output = new Uint8Array(switchData.outputData),
types = detectFileType(output);
let type = "unknown",
@@ -1036,15 +1045,22 @@ self.inputSwitch = function(switchData) {
}
// ArrayBuffer
- currentInput.data = {
- fileBuffer: switchData.outputData,
- name: `output.${ext}`,
- size: switchData.outputData.byteLength.toLocaleString(),
- type: type
- };
+ self.updateInputObj({
+ inputNum: switchData.inputNum,
+ data: {
+ fileBuffer: switchData.outputData,
+ name: `output.${ext}`,
+ size: switchData.outputData.byteLength.toLocaleString(),
+ type: type
+ }
+ });
} else {
// String
- currentInput.data = switchData.outputData;
+ self.updateInputValue({
+ inputNum: switchData.inputNum,
+ value: switchData.outputData,
+ force: true
+ });
}
self.postMessage({
@@ -1055,6 +1071,11 @@ self.inputSwitch = function(switchData) {
}
});
- self.setInput({inputNum: switchData.inputNum, silent: false});
+ self.postMessage({
+ action: "fileLoaded",
+ data: {
+ inputNum: switchData.inputNum
+ }
+ });
};
diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs
index eac6ab9a..696deba0 100644
--- a/tests/operations/index.mjs
+++ b/tests/operations/index.mjs
@@ -26,6 +26,7 @@ import "./tests/BitwiseOp";
import "./tests/ByteRepr";
import "./tests/CartesianProduct";
import "./tests/CharEnc";
+import "./tests/ChangeIPFormat";
import "./tests/Charts";
import "./tests/Checksum";
import "./tests/Ciphers";
@@ -88,6 +89,7 @@ import "./tests/BLAKE2s";
import "./tests/Protobuf";
import "./tests/ParseSSHHostKey";
import "./tests/DefangIP";
+import "./tests/ParseUDP";
// Cannot test operations that use the File type yet
//import "./tests/SplitColourChannels";
diff --git a/tests/operations/tests/ChangeIPFormat.mjs b/tests/operations/tests/ChangeIPFormat.mjs
new file mode 100644
index 00000000..d92ffb79
--- /dev/null
+++ b/tests/operations/tests/ChangeIPFormat.mjs
@@ -0,0 +1,52 @@
+/**
+ * Change IP format tests.
+ *
+ * @author Chris Smith
+ * @copyright Crown Copyright 2019
+ * @license Apache-2.0
+ */
+import TestRegister from "../../lib/TestRegister.mjs";
+
+TestRegister.addTests([
+ {
+ name: "Change IP format: Dotted Decimal to Hex",
+ input: "192.168.1.1",
+ expectedOutput: "c0a80101",
+ recipeConfig: [
+ {
+ op: "Change IP format",
+ args: ["Dotted Decimal", "Hex"],
+ },
+ ],
+ }, {
+ name: "Change IP format: Decimal to Dotted Decimal",
+ input: "3232235777",
+ expectedOutput: "192.168.1.1",
+ recipeConfig: [
+ {
+ op: "Change IP format",
+ args: ["Decimal", "Dotted Decimal"],
+ },
+ ],
+ }, {
+ name: "Change IP format: Hex to Octal",
+ input: "c0a80101",
+ expectedOutput: "030052000401",
+ recipeConfig: [
+ {
+ op: "Change IP format",
+ args: ["Hex", "Octal"],
+ },
+ ],
+ }, {
+ name: "Change IP format: Octal to Decimal",
+ input: "030052000401",
+ expectedOutput: "3232235777",
+ recipeConfig: [
+ {
+ op: "Change IP format",
+ args: ["Octal", "Decimal"],
+ },
+ ],
+ },
+]);
diff --git a/tests/operations/tests/Crypt.mjs b/tests/operations/tests/Crypt.mjs
index 1f92bcb0..1db8093f 100644
--- a/tests/operations/tests/Crypt.mjs
+++ b/tests/operations/tests/Crypt.mjs
@@ -18,6 +18,42 @@ TestRegister.addTests([
*
* All random data blocks (binary input, keys and IVs) were generated from /dev/urandom using dd:
* > dd if=/dev/urandom of=key.txt bs=16 count=1
+ *
+ *
+ * The following is a Python script used to generate the AES-GCM tests.
+ * It uses PyCryptodome (https://www.pycryptodome.org) to handle the AES encryption and decryption.
+ *
+ * from Crypto.Cipher import AES
+ * import binascii
+
+ * input_data = "0123456789ABCDEF"
+ * key = binascii.unhexlify("00112233445566778899aabbccddeeff")
+ * iv = binascii.unhexlify("ffeeddccbbaa99887766554433221100")
+ *
+ * cipher = AES.new(key, AES.MODE_GCM, nonce=iv)
+ * cipher_text, tag = cipher.encrypt_and_digest(binascii.unhexlify(input_data))
+ *
+ * cipher = AES.new(key, AES.MODE_GCM, nonce=iv)
+ * decrypted = cipher.decrypt_and_verify(cipher_text, tag)
+ *
+ * key = binascii.hexlify(key).decode("UTF-8")
+ * iv = binascii.hexlify(iv).decode("UTF-8")
+ * cipher_text = binascii.hexlify(cipher_text).decode("UTF-8")
+ * tag = binascii.hexlify(tag).decode("UTF-8")
+ * decrypted = binascii.hexlify(decrypted).decode("UTF-8")
+ *
+ * print("Key: {}\nIV : {}\nInput data: {}\n\nEncrypted ciphertext: {}\nGCM tag: {}\n\nDecrypted plaintext : {}".format(key, iv, input_data, cipher_text, tag, decrypted))
+ *
+ *
+ * Outputs:
+ * Key: 00112233445566778899aabbccddeeff
+ * IV : ffeeddccbbaa99887766554433221100
+ * Input data: 0123456789ABCDEF
+ *
+ * Encrypted ciphertext: 8feeafedfdb2f6f9
+ * GCM tag: 654ef4957c6e2b0cc6501d8f9bcde032
+ *
+ * Decrypted plaintext : 0123456789abcdef
*/
{
name: "AES Encrypt: no key",
@@ -838,7 +874,7 @@ The following algorithms will be used based on the size of the key:
},
{
name: "AES Decrypt: AES-128-GCM, Binary",
- input: "fa17fcbf5e8763322c1b0c8562e1512ed9d702ef70c1643572b9de3e34ae6b535e6c1b992432aa6d06fb6f80c861262aef66e7c26035afe77bd3861261e4e092b523f058f8ebef2143db21bc16d02f7a011efb07419300cb41c3b884d1d8d6a766b8963c",
+ input: "5a29debb5c5f38cdf8aee421bd94dbbf3399947faddf205f88b3ad8ecb0c51214ec0e28bf78942dfa212d7eb15259bbdcac677b4c05f473eeb9331d74f31d441d97d56eb5c73b586342d72128ca528813543dc0fc7eddb7477172cc9194c18b2e1383e4e",
expectedOutput: "7a0e643132750e96d805d11e9e48e281fa39a41039286423cc1c045e5442b40bf1c3f2822bded3f9c8ef11cb25da64dda9c7ab87c246bd305385150c98f31465c2a6180fe81d31ea289b916504d5a12e1de26cb10adba84a0cb0c86f94bc14bc554f3018",
recipeConfig: [
{
@@ -847,7 +883,7 @@ The following algorithms will be used based on the size of the key:
{"option": "Hex", "string": "51e201d463698ef5f717f71f5b4712af"},
{"option": "Hex", "string": "1748e7179bd56570d51fa4ba287cc3e5"},
"GCM", "Hex", "Hex",
- {"option": "Hex", "string": "fa6bbb34c8cde65a3d7b93fb094fc84f"}
+ {"option": "Hex", "string": "70fad2ca19412c20f40fd06918736e56"}
]
}
],
@@ -934,7 +970,7 @@ The following algorithms will be used based on the size of the key:
},
{
name: "AES Decrypt: AES-192-GCM, Binary",
- input: "ed22946f96964d300b45f5ce2d9601ba87682da1a603c90e6d4f7738729b0602f613ee392c9bfc7792594474f1213fb99185851f02ece4df0e93995e49f97aa4d0a337d7a80d83e4219dae5a3d36658f8659cdd5ed7c32707f98656fab7fb43f7a61e37c",
+ input: "318b479d919d506f0cd904f2676fab263a7921b6d7e0514f36e03ae2333b77fa66ef5600babcb2ee9718aeb71fc357412343c1f2cb351d8715bb0aedae4a6468124f9c4aaf6a721b306beddbe63a978bec8baeeba4b663be33ee5bc982746bd4aed1c38b",
expectedOutput: "7a0e643132750e96d805d11e9e48e281fa39a41039286423cc1c045e5442b40bf1c3f2822bded3f9c8ef11cb25da64dda9c7ab87c246bd305385150c98f31465c2a6180fe81d31ea289b916504d5a12e1de26cb10adba84a0cb0c86f94bc14bc554f3018",
recipeConfig: [
{
@@ -943,7 +979,7 @@ The following algorithms will be used based on the size of the key:
{"option": "Hex", "string": "6801ed503c9d96ee5f9d78b07ab1b295dba3c2adf81c7816"},
{"option": "Hex", "string": "1748e7179bd56570d51fa4ba287cc3e5"},
"GCM", "Hex", "Hex",
- {"option": "Hex", "string": "be17cb31edb77f648b9d1032b235b33d"}
+ {"option": "Hex", "string": "86db597d5302595223cadbd990f1309b"}
]
}
],
@@ -1030,7 +1066,7 @@ The following algorithms will be used based on the size of the key:
},
{
name: "AES Decrypt: AES-256-GCM, Binary",
- input: "e3f1b236eaf3b9df69df8133a1b417fa42b242d8ad49e4d2f3469aca7e2a41737e4f2c8a0d212143287088fad51743577dc6dfa8ed328ca90113cbeb9b137926b2168cc037bdc371777e6ee02b9d9c017b6054fd83d43b4885fbe9c044a8574f1491a893",
+ input: "1287f188ad4d7ab0d9ff69b3c29cb11f861389532d8cb9337181da2e8cfc74a84927e8c0dd7a28a32fd485afe694259a63c199b199b95edd87c7aa95329feac340f2b78b72956a85f367044d821766b1b7135815571df44900695f1518cf3ae38ecb650f",
expectedOutput: "7a0e643132750e96d805d11e9e48e281fa39a41039286423cc1c045e5442b40bf1c3f2822bded3f9c8ef11cb25da64dda9c7ab87c246bd305385150c98f31465c2a6180fe81d31ea289b916504d5a12e1de26cb10adba84a0cb0c86f94bc14bc554f3018",
recipeConfig: [
{
@@ -1039,7 +1075,7 @@ The following algorithms will be used based on the size of the key:
{"option": "Hex", "string": "2d767f6e9333d1c77581946e160b2b7368c2cdd5e2b80f04ca09d64e02afbfe1"},
{"option": "Hex", "string": "1748e7179bd56570d51fa4ba287cc3e5"},
"GCM", "Hex", "Hex",
- {"option": "Hex", "string": "23ddbd3ee4de33f98a9ea9a170bdf268"}
+ {"option": "Hex", "string": "821b1e5f32dad052e502775a523d957a"}
]
}
],
diff --git a/tests/operations/tests/ParseUDP.mjs b/tests/operations/tests/ParseUDP.mjs
new file mode 100644
index 00000000..2c519232
--- /dev/null
+++ b/tests/operations/tests/ParseUDP.mjs
@@ -0,0 +1,68 @@
+/**
+ * Parse UDP tests.
+ *
+ * @author h345983745
+ *
+ * @copyright Crown Copyright 2019
+ * @license Apache-2.0
+ */
+import TestRegister from "../../lib/TestRegister.mjs";
+
+TestRegister.addTests([
+ {
+ name: "Parse UDP: No Data - JSON",
+ input: "04 89 00 35 00 2c 01 01",
+ expectedOutput: "{\"Source port\":1161,\"Destination port\":53,\"Length\":44,\"Checksum\":\"0101\"}",
+ recipeConfig: [
+ {
+ op: "From Hex",
+ args: ["Auto"],
+ },
+ {
+ op: "Parse UDP",
+ args: [],
+ },
+ {
+ op: "JSON Minify",
+ args: [],
+ },
+ ],
+ }, {
+ name: "Parse UDP: With Data - JSON",
+ input: "04 89 00 35 00 2c 01 01 02 02",
+ expectedOutput: "{\"Source port\":1161,\"Destination port\":53,\"Length\":44,\"Checksum\":\"0101\",\"Data\":\"0202\"}",
+ recipeConfig: [
+ {
+ op: "From Hex",
+ args: ["Auto"],
+ },
+ {
+ op: "Parse UDP",
+ args: [],
+ },
+ {
+ op: "JSON Minify",
+ args: [],
+ },
+ ],
+ },
+ {
+ name: "Parse UDP: Not Enough Bytes",
+ input: "04 89 00",
+ expectedOutput: "Need 8 bytes for a UDP Header",
+ recipeConfig: [
+ {
+ op: "From Hex",
+ args: ["Auto"],
+ },
+ {
+ op: "Parse UDP",
+ args: [],
+ },
+ {
+ op: "JSON Minify",
+ args: [],
+ },
+ ],
+ }
+]);