瀏覽代碼

Merge branch 'master' of github.com:gchq/CyberChef into node-lib

d98762625 6 年之前
父節點
當前提交
0a3d87dfe3

+ 2 - 1
.travis.yml

@@ -32,8 +32,9 @@ deploy:
     skip_cleanup: true
     skip_cleanup: true
     api_key:
     api_key:
       secure: "HV1WSKv4l/0Y2bKKs1iBJocBcmLj08PCRUeEM/jTwA4jqJ8EiLHWiXtER/D5sEg2iibRVKd2OQjfrmS6bo4AiwdeVgAKmv0FtS2Jw+391N8Nd5AkEANHa5Om/IpHLTL2YRAjpJTsDpY72bMUTJIwjQA3TFJkgrpOw6KYfohOcgbxLpZ4XuNJRU3VL4Hsxdv5V9aOVmfFOmMOVPQlakXy7NgtW5POp1f2WJwgcZxylkR1CjwaqMyXmSoVl46pyH3tr5+dptsQoKSGdi6sIHGA60oDotFPcm+0ifa47wZw+vapuuDi4tdNxhrHGaDMG8xiE0WFDHwQUDlk2/+W7j9SEX0H3Em7us371JXRp56EDwEcDa34VpVkC6i8HGcHK55hnxVbMZXGf3qhOFD8wY7qMbjMRvIpucrMHBi86OfkDfv0vDj2LyvIl5APj/AX50BrE0tfH1MZbH26Jkx4NdlkcxQ14GumarmUqfmVvbX/fsoA6oUuAAE9ZgRRi3KHO4wci6KUcRfdm+XOeUkaBFsL86G3EEYIvrtBTuaypdz+Cx7nd1iPZyWMx5Y1gXnVzha4nBdV4+7l9JIsFggD8QVpw2uHXQiS1KXFjOeqA3DBD8tjMB7q26Fl2fD3jkOo4BTbQ2NrRIZUu/iL+fOmMPsyMt2qulB0yaSBCfkbEq8xrUA="
       secure: "HV1WSKv4l/0Y2bKKs1iBJocBcmLj08PCRUeEM/jTwA4jqJ8EiLHWiXtER/D5sEg2iibRVKd2OQjfrmS6bo4AiwdeVgAKmv0FtS2Jw+391N8Nd5AkEANHa5Om/IpHLTL2YRAjpJTsDpY72bMUTJIwjQA3TFJkgrpOw6KYfohOcgbxLpZ4XuNJRU3VL4Hsxdv5V9aOVmfFOmMOVPQlakXy7NgtW5POp1f2WJwgcZxylkR1CjwaqMyXmSoVl46pyH3tr5+dptsQoKSGdi6sIHGA60oDotFPcm+0ifa47wZw+vapuuDi4tdNxhrHGaDMG8xiE0WFDHwQUDlk2/+W7j9SEX0H3Em7us371JXRp56EDwEcDa34VpVkC6i8HGcHK55hnxVbMZXGf3qhOFD8wY7qMbjMRvIpucrMHBi86OfkDfv0vDj2LyvIl5APj/AX50BrE0tfH1MZbH26Jkx4NdlkcxQ14GumarmUqfmVvbX/fsoA6oUuAAE9ZgRRi3KHO4wci6KUcRfdm+XOeUkaBFsL86G3EEYIvrtBTuaypdz+Cx7nd1iPZyWMx5Y1gXnVzha4nBdV4+7l9JIsFggD8QVpw2uHXQiS1KXFjOeqA3DBD8tjMB7q26Fl2fD3jkOo4BTbQ2NrRIZUu/iL+fOmMPsyMt2qulB0yaSBCfkbEq8xrUA="
+    file_glob: true
     file:
     file:
-      - build/prod/cyberchef.htm
+      - build/prod/*.zip
       - build/node/CyberChef.js
       - build/node/CyberChef.js
     on:
     on:
       repo: gchq/CyberChef
       repo: gchq/CyberChef

+ 9 - 0
CHANGELOG.md

@@ -2,6 +2,12 @@
 All major and minor version changes will be documented in this file. Details of patch-level version changes can be found in [commit messages](https://github.com/gchq/CyberChef/commits/master).
 All major and minor version changes will be documented in this file. Details of patch-level version changes can be found in [commit messages](https://github.com/gchq/CyberChef/commits/master).
 
 
 
 
+### [8.31.0] - 2019-04-12
+- The downloadable version of CyberChef is now a .zip file containing separate modules rather than a single .htm file. It is still completely standalone and will not make any external network requests. This change reduces the complexity of the build process significantly. [@n1474335]
+
+### [8.30.0] - 2019-04-12
+- 'Decode Protobuf' operation added [@n1474335] | [#533]
+
 ### [8.29.0] - 2019-03-31
 ### [8.29.0] - 2019-03-31
 - 'BLAKE2s' and 'BLAKE2b' hashing operations added [@h345983745] | [#525]
 - 'BLAKE2s' and 'BLAKE2b' hashing operations added [@h345983745] | [#525]
 
 
@@ -124,6 +130,8 @@ All major and minor version changes will be documented in this file. Details of
 
 
 
 
 
 
+[8.31.0]: https://github.com/gchq/CyberChef/releases/tag/v8.31.0
+[8.30.0]: https://github.com/gchq/CyberChef/releases/tag/v8.30.0
 [8.29.0]: https://github.com/gchq/CyberChef/releases/tag/v8.29.0
 [8.29.0]: https://github.com/gchq/CyberChef/releases/tag/v8.29.0
 [8.28.0]: https://github.com/gchq/CyberChef/releases/tag/v8.28.0
 [8.28.0]: https://github.com/gchq/CyberChef/releases/tag/v8.28.0
 [8.27.0]: https://github.com/gchq/CyberChef/releases/tag/v8.27.0
 [8.27.0]: https://github.com/gchq/CyberChef/releases/tag/v8.27.0
@@ -223,3 +231,4 @@ All major and minor version changes will be documented in this file. Details of
 [#506]: https://github.com/gchq/CyberChef/pull/506
 [#506]: https://github.com/gchq/CyberChef/pull/506
 [#516]: https://github.com/gchq/CyberChef/pull/516
 [#516]: https://github.com/gchq/CyberChef/pull/516
 [#525]: https://github.com/gchq/CyberChef/pull/525
 [#525]: https://github.com/gchq/CyberChef/pull/525
+[#533]: https://github.com/gchq/CyberChef/pull/533

+ 53 - 78
Gruntfile.js

@@ -4,7 +4,6 @@ const webpack = require("webpack");
 const HtmlWebpackPlugin = require("html-webpack-plugin");
 const HtmlWebpackPlugin = require("html-webpack-plugin");
 const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
 const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
 const NodeExternals = require("webpack-node-externals");
 const NodeExternals = require("webpack-node-externals");
-const Inliner = require("web-resource-inliner");
 const glob = require("glob");
 const glob = require("glob");
 const path = require("path");
 const path = require("path");
 const UglifyJSWebpackPlugin = require("uglifyjs-webpack-plugin");
 const UglifyJSWebpackPlugin = require("uglifyjs-webpack-plugin");
@@ -50,18 +49,16 @@ module.exports = function (grunt) {
 
 
     grunt.registerTask("prod",
     grunt.registerTask("prod",
         "Creates a production-ready build. Use the --msg flag to add a compile message.",
         "Creates a production-ready build. Use the --msg flag to add a compile message.",
-        ["eslint", "clean:prod", "clean:config", "exec:generateConfig", "webpack:web", "inline", "chmod"]);
+        [
+            "eslint", "clean:prod", "clean:config", "exec:generateConfig", "webpack:web",
+            "copy:standalone", "zip:standalone", "clean:standalone", "chmod"
+        ]);
 
 
     grunt.registerTask("default",
     grunt.registerTask("default",
         "Lints the code base",
         "Lints the code base",
         ["eslint", "exec:repoSize"]);
         ["eslint", "exec:repoSize"]);
 
 
-    grunt.registerTask("inline",
-        "Compiles a production build of CyberChef into a single, portable web page.",
-        ["exec:generateConfig", "webpack:webInline", "runInliner", "clean:inlineScripts"]);
 
 
-
-    grunt.registerTask("runInliner", runInliner);
     grunt.registerTask("doc", "docs");
     grunt.registerTask("doc", "docs");
     grunt.registerTask("tests", "test");
     grunt.registerTask("tests", "test");
     grunt.registerTask("lint", "eslint");
     grunt.registerTask("lint", "eslint");
@@ -79,6 +76,7 @@ module.exports = function (grunt) {
     grunt.loadNpmTasks("grunt-accessibility");
     grunt.loadNpmTasks("grunt-accessibility");
     grunt.loadNpmTasks("grunt-concurrent");
     grunt.loadNpmTasks("grunt-concurrent");
     grunt.loadNpmTasks("grunt-contrib-connect");
     grunt.loadNpmTasks("grunt-contrib-connect");
+    grunt.loadNpmTasks("grunt-zip");
 
 
 
 
     // Project configuration
     // Project configuration
@@ -101,32 +99,6 @@ module.exports = function (grunt) {
         },
         },
         moduleEntryPoints = listEntryModules();
         moduleEntryPoints = listEntryModules();
 
 
-    /**
-     * Compiles a production build of CyberChef into a single, portable web page.
-     */
-    function runInliner() {
-        const done = this.async();
-        Inliner.html({
-            relativeTo: "build/prod/",
-            fileContent: grunt.file.read("build/prod/cyberchef.htm"),
-            images: true,
-            svgs: true,
-            scripts: true,
-            links: true,
-            strict: true
-        }, function(error, result) {
-            if (error) {
-                if (error instanceof Error) {
-                    done(error);
-                } else {
-                    done(new Error(error));
-                }
-            } else {
-                grunt.file.write("build/prod/cyberchef.htm", result);
-                done(true);
-            }
-        });
-    }
 
 
     /**
     /**
      * Generates an entry list for all the modules.
      * Generates an entry list for all the modules.
@@ -137,7 +109,7 @@ module.exports = function (grunt) {
         glob.sync("./src/core/config/modules/*.mjs").forEach(file => {
         glob.sync("./src/core/config/modules/*.mjs").forEach(file => {
             const basename = path.basename(file);
             const basename = path.basename(file);
             if (basename !== "Default.mjs" && basename !== "OpModules.mjs")
             if (basename !== "Default.mjs" && basename !== "OpModules.mjs")
-                entryModules[basename.split(".mjs")[0]] = path.resolve(file);
+                entryModules["modules/" + basename.split(".mjs")[0]] = path.resolve(file);
         });
         });
 
 
         return entryModules;
         return entryModules;
@@ -150,7 +122,7 @@ module.exports = function (grunt) {
             node: ["build/node/*"],
             node: ["build/node/*"],
             config: ["src/core/config/OperationConfig.json", "src/core/config/modules/*", "src/code/operations/index.mjs", "src/node/index.mjs", "src/node/config/OperationConfig.json"],
             config: ["src/core/config/OperationConfig.json", "src/core/config/modules/*", "src/code/operations/index.mjs", "src/node/index.mjs", "src/node/config/OperationConfig.json"],
             docs: ["docs/*", "!docs/*.conf.json", "!docs/*.ico", "!docs/*.png"],
             docs: ["docs/*", "!docs/*.conf.json", "!docs/*.ico", "!docs/*.png"],
-            inlineScripts: ["build/prod/scripts.js"],
+            standalone: ["build/prod/CyberChef*.html"]
         },
         },
         eslint: {
         eslint: {
             options: {
             options: {
@@ -202,7 +174,10 @@ module.exports = function (grunt) {
                     }, moduleEntryPoints),
                     }, moduleEntryPoints),
                     output: {
                     output: {
                         path: __dirname + "/build/prod",
                         path: __dirname + "/build/prod",
-                        // globalObject: "this"
+                        filename: chunkData => {
+                            return chunkData.chunk.name === "main" ? "assets/[name].js": "[name].js";
+                        },
+                        globalObject: "this"
                     },
                     },
                     resolve: {
                     resolve: {
                         alias: {
                         alias: {
@@ -232,46 +207,6 @@ module.exports = function (grunt) {
                     ]
                     ]
                 };
                 };
             },
             },
-            webInline: {
-                mode: "production",
-                target: "web",
-                entry: "./src/web/index.js",
-                output: {
-                    filename: "scripts.js",
-                    path: __dirname + "/build/prod"
-                },
-                plugins: [
-                    new webpack.DefinePlugin(Object.assign({}, BUILD_CONSTANTS, {
-                        INLINE: "true"
-                    })),
-                    new HtmlWebpackPlugin({
-                        filename: "cyberchef.htm",
-                        template: "./src/web/html/index.html",
-                        compileTime: compileTime,
-                        version: pkg.version + "s",
-                        inline: true,
-                        minify: {
-                            removeComments: true,
-                            collapseWhitespace: true,
-                            minifyJS: true,
-                            minifyCSS: true
-                        }
-                    }),
-                ]
-            },
-            tests: {
-                mode: "development",
-                target: "node",
-                entry: "./test/index.mjs",
-                externals: [NodeExternals()],
-                output: {
-                    filename: "index.js",
-                    path: __dirname + "/build/test/node"
-                },
-                plugins: [
-                    new webpack.DefinePlugin(BUILD_CONSTANTS),
-                ]
-            },
             node: {
             node: {
                 mode: NODE_PROD ? "production" : "development",
                 mode: NODE_PROD ? "production" : "development",
                 target: "node",
                 target: "node",
@@ -381,6 +316,18 @@ module.exports = function (grunt) {
                 }
                 }
             }
             }
         },
         },
+        zip: {
+            standalone: {
+                cwd: "build/prod/",
+                src: [
+                    "build/prod/**/*",
+                    "!build/prod/index.html",
+                    "!build/prod/BundleAnalyzerReport.html",
+                    "!build/prod/sitemap.js"
+                ],
+                dest: `build/prod/CyberChef_v${pkg.version}.zip`
+            }
+        },
         connect: {
         connect: {
             prod: {
             prod: {
                 options: {
                 options: {
@@ -393,10 +340,16 @@ module.exports = function (grunt) {
             ghPages: {
             ghPages: {
                 options: {
                 options: {
                     process: function (content, srcpath) {
                     process: function (content, srcpath) {
-                        // Add Google Analytics code to index.html
                         if (srcpath.indexOf("index.html") >= 0) {
                         if (srcpath.indexOf("index.html") >= 0) {
+                            // Add Google Analytics code to index.html
                             content = content.replace("</body></html>",
                             content = content.replace("</body></html>",
                                 grunt.file.read("src/web/static/ga.html") + "</body></html>");
                                 grunt.file.read("src/web/static/ga.html") + "</body></html>");
+
+                            // Add Structured Data for SEO
+                            content = content.replace("</head>",
+                                "<script type='application/ld+json'>" +
+                                JSON.stringify(JSON.parse(grunt.file.read("src/web/static/structuredData.json"))) +
+                                "</script></head>");
                             return grunt.template.process(content, srcpath);
                             return grunt.template.process(content, srcpath);
                         } else {
                         } else {
                             return content;
                             return content;
@@ -415,6 +368,28 @@ module.exports = function (grunt) {
                         dest: "build/prod/"
                         dest: "build/prod/"
                     },
                     },
                 ]
                 ]
+            },
+            standalone: {
+                options: {
+                    process: function (content, srcpath) {
+                        if (srcpath.indexOf("index.html") >= 0) {
+                            // Replace download link with version number
+                            content = content.replace(/<a [^>]+>Download CyberChef.+?<\/a>/,
+                                `<span>Version ${pkg.version}</span>`);
+
+                            return grunt.template.process(content, srcpath);
+                        } else {
+                            return content;
+                        }
+                    },
+                    noProcess: ["**", "!**/*.html"]
+                },
+                files: [
+                    {
+                        src: "build/prod/index.html",
+                        dest: `build/prod/CyberChef_v${pkg.version}.html`
+                    }
+                ]
             }
             }
         },
         },
         chmod: {
         chmod: {
@@ -477,7 +452,7 @@ module.exports = function (grunt) {
                 command: "node --experimental-modules --no-warnings --no-deprecation tests/operations/index.mjs"
                 command: "node --experimental-modules --no-warnings --no-deprecation tests/operations/index.mjs"
             },
             },
             browserTests: {
             browserTests: {
-                command: "./node_modules/.bin/nightwatch --env prod,inline"
+                command: "./node_modules/.bin/nightwatch --env prod"
             },
             },
             nodeTests: {
             nodeTests: {
                 command: "node --experimental-modules --no-warnings --no-deprecation tests/node/index.mjs"
                 command: "node --experimental-modules --no-warnings --no-deprecation tests/node/index.mjs"

+ 0 - 4
nightwatch.json

@@ -23,10 +23,6 @@
 
 
     "prod": {
     "prod": {
       "launch_url": "http://localhost:8000/index.html"
       "launch_url": "http://localhost:8000/index.html"
-    },
-
-    "inline": {
-      "launch_url": "http://localhost:8000/cyberchef.htm"
     }
     }
 
 
   }
   }

File diff suppressed because it is too large
+ 300 - 288
package-lock.json


+ 17 - 16
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "cyberchef",
   "name": "cyberchef",
-  "version": "8.29.1",
+  "version": "8.31.1",
   "description": "The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.",
   "description": "The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.",
   "author": "n1474335 <n1474335@gmail.com>",
   "author": "n1474335 <n1474335@gmail.com>",
   "homepage": "https://gchq.github.io/CyberChef",
   "homepage": "https://gchq.github.io/CyberChef",
@@ -31,17 +31,17 @@
   "module": "src/node/index.mjs",
   "module": "src/node/index.mjs",
   "bugs": "https://github.com/gchq/CyberChef/issues",
   "bugs": "https://github.com/gchq/CyberChef/issues",
   "devDependencies": {
   "devDependencies": {
-    "@babel/core": "^7.4.0",
-    "@babel/plugin-transform-runtime": "^7.4.0",
-    "@babel/preset-env": "^7.4.2",
-    "autoprefixer": "^9.5.0",
+    "@babel/core": "^7.4.3",
+    "@babel/plugin-transform-runtime": "^7.4.3",
+    "@babel/preset-env": "^7.4.3",
+    "autoprefixer": "^9.5.1",
     "babel-eslint": "^10.0.1",
     "babel-eslint": "^10.0.1",
     "babel-loader": "^8.0.5",
     "babel-loader": "^8.0.5",
     "babel-plugin-dynamic-import-node": "^2.2.0",
     "babel-plugin-dynamic-import-node": "^2.2.0",
     "chromedriver": "^2.46.0",
     "chromedriver": "^2.46.0",
     "colors": "^1.3.3",
     "colors": "^1.3.3",
     "css-loader": "^2.1.1",
     "css-loader": "^2.1.1",
-    "eslint": "^5.15.3",
+    "eslint": "^5.16.0",
     "exports-loader": "^0.7.0",
     "exports-loader": "^0.7.0",
     "file-loader": "^3.0.1",
     "file-loader": "^3.0.1",
     "grunt": "^1.0.4",
     "grunt": "^1.0.4",
@@ -54,13 +54,14 @@
     "grunt-contrib-watch": "^1.1.0",
     "grunt-contrib-watch": "^1.1.0",
     "grunt-eslint": "^21.0.0",
     "grunt-eslint": "^21.0.0",
     "grunt-exec": "~3.0.0",
     "grunt-exec": "~3.0.0",
-    "grunt-jsdoc": "^2.3.0",
+    "grunt-jsdoc": "^2.3.1",
     "grunt-webpack": "^3.1.3",
     "grunt-webpack": "^3.1.3",
+    "grunt-zip": "^0.18.2",
     "html-webpack-plugin": "^3.2.0",
     "html-webpack-plugin": "^3.2.0",
     "imports-loader": "^0.8.0",
     "imports-loader": "^0.8.0",
     "ink-docstrap": "^1.3.2",
     "ink-docstrap": "^1.3.2",
     "jsdoc-babel": "^0.5.0",
     "jsdoc-babel": "^0.5.0",
-    "mini-css-extract-plugin": "^0.5.0",
+    "mini-css-extract-plugin": "^0.6.0",
     "nightwatch": "^1.0.19",
     "nightwatch": "^1.0.19",
     "node-sass": "^4.11.0",
     "node-sass": "^4.11.0",
     "postcss-css-variables": "^0.12.0",
     "postcss-css-variables": "^0.12.0",
@@ -73,15 +74,15 @@
     "svg-url-loader": "^2.3.2",
     "svg-url-loader": "^2.3.2",
     "uglifyjs-webpack-plugin": "^2.0.1",
     "uglifyjs-webpack-plugin": "^2.0.1",
     "url-loader": "^1.1.2",
     "url-loader": "^1.1.2",
-    "web-resource-inliner": "^4.3.1",
     "webpack": "^4.29.6",
     "webpack": "^4.29.6",
-    "webpack-bundle-analyzer": "^3.1.0",
-    "webpack-dev-server": "^3.2.1",
+    "webpack-bundle-analyzer": "^3.3.2",
+    "webpack-dev-server": "^3.3.1",
     "webpack-node-externals": "^1.7.2",
     "webpack-node-externals": "^1.7.2",
     "worker-loader": "^2.0.0"
     "worker-loader": "^2.0.0"
   },
   },
   "dependencies": {
   "dependencies": {
-    "@babel/runtime": "^7.4.2",
+    "@babel/polyfill": "^7.4.3",
+    "@babel/runtime": "^7.4.3",
     "arrive": "^2.4.1",
     "arrive": "^2.4.1",
     "babel-plugin-transform-builtin-extend": "1.1.2",
     "babel-plugin-transform-builtin-extend": "1.1.2",
     "bcryptjs": "^2.4.3",
     "bcryptjs": "^2.4.3",
@@ -93,7 +94,7 @@
     "bson": "^4.0.2",
     "bson": "^4.0.2",
     "chi-squared": "^1.1.0",
     "chi-squared": "^1.1.0",
     "clippyjs": "0.0.3",
     "clippyjs": "0.0.3",
-    "core-js": "^3.0.0",
+    "core-js": "^3.0.1",
     "crypto-api": "^0.8.3",
     "crypto-api": "^0.8.3",
     "crypto-js": "^3.1.9-1",
     "crypto-js": "^3.1.9-1",
     "ctph.js": "0.0.5",
     "ctph.js": "0.0.5",
@@ -108,8 +109,8 @@
     "file-saver": "^2.0.1",
     "file-saver": "^2.0.1",
     "geodesy": "^1.1.3",
     "geodesy": "^1.1.3",
     "highlight.js": "^9.15.6",
     "highlight.js": "^9.15.6",
-    "jimp": "^0.6.0",
-    "jquery": "^3.3.1",
+    "jimp": "^0.6.1",
+    "jquery": "3.3.1",
     "js-crc": "^0.2.0",
     "js-crc": "^0.2.0",
     "js-sha3": "^0.8.0",
     "js-sha3": "^0.8.0",
     "jsesc": "^2.5.2",
     "jsesc": "^2.5.2",
@@ -131,7 +132,7 @@
     "notepack.io": "^2.2.0",
     "notepack.io": "^2.2.0",
     "nwmatcher": "^1.4.4",
     "nwmatcher": "^1.4.4",
     "otp": "^0.1.3",
     "otp": "^0.1.3",
-    "popper.js": "^1.14.7",
+    "popper.js": "^1.15.0",
     "qr-image": "^3.2.0",
     "qr-image": "^3.2.0",
     "scryptsy": "^2.0.0",
     "scryptsy": "^2.0.0",
     "snackbarjs": "^1.1.0",
     "snackbarjs": "^1.1.0",

+ 1 - 1
src/core/ChefWorker.js

@@ -178,7 +178,7 @@ self.loadRequiredModules = function(recipeConfig) {
         if (!OpModules.hasOwnProperty(module)) {
         if (!OpModules.hasOwnProperty(module)) {
             log.info(`Loading ${module} module`);
             log.info(`Loading ${module} module`);
             self.sendStatusMessage(`Loading ${module} module`);
             self.sendStatusMessage(`Loading ${module} module`);
-            self.importScripts(`${self.docURL}/${module}.js`);
+            self.importScripts(`${self.docURL}/modules/${module}.js`);
             self.sendStatusMessage("");
             self.sendStatusMessage("");
         }
         }
     });
     });

+ 3 - 0
src/core/config/Categories.json

@@ -169,6 +169,9 @@
             "Parse URI",
             "Parse URI",
             "URL Encode",
             "URL Encode",
             "URL Decode",
             "URL Decode",
+            "Protobuf Decode",
+            "VarInt Encode",
+            "VarInt Decode",
             "Format MAC addresses",
             "Format MAC addresses",
             "Change IP format",
             "Change IP format",
             "Group IP addresses",
             "Group IP addresses",

+ 285 - 0
src/core/lib/Protobuf.mjs

@@ -0,0 +1,285 @@
+import Utils from "../Utils";
+
+/**
+ * Protobuf lib. Contains functions to decode protobuf serialised
+ * data without a schema or .proto file.
+ *
+ * Provides utility functions to encode and decode variable length
+ * integers (varint).
+ *
+ * @author GCHQ Contributor [3]
+ * @copyright Crown Copyright 2019
+ * @license Apache-2.0
+ */
+class Protobuf {
+
+    /**
+     * Protobuf constructor
+     *
+     * @param {byteArray} data
+     */
+    constructor(data) {
+        // Check we have a byteArray
+        if (data instanceof Array) {
+            this.data = data;
+        } else {
+            throw new Error("Protobuf input must be a byteArray");
+        }
+
+        // Set up masks
+        this.TYPE = 0x07;
+        this.NUMBER = 0x78;
+        this.MSB = 0x80;
+        this.VALUE = 0x7f;
+
+        // Declare offset and length
+        this.offset = 0;
+        this.LENGTH = data.length;
+    }
+
+    // Public Functions
+
+    /**
+     * Encode a varint from a number
+     *
+     * @param {number} number
+     * @returns {byteArray}
+     */
+    static varIntEncode(number) {
+        const MSB = 0x80,
+            VALUE = 0x7f,
+            MSBALL = ~VALUE,
+            INT = Math.pow(2, 31);
+        const out = [];
+        let offset = 0;
+
+        while (number >= INT) {
+            out[offset++] = (number & 0xff) | MSB;
+            number /= 128;
+        }
+        while (number & MSBALL) {
+            out[offset++] = (number & 0xff) | MSB;
+            number >>>= 7;
+        }
+        out[offset] = number | 0;
+        return out;
+    }
+
+    /**
+     * Decode a varint from the byteArray
+     *
+     * @param {byteArray} input
+     * @returns {number}
+     */
+    static varIntDecode(input) {
+        const pb = new Protobuf(input);
+        return pb._varInt();
+    }
+
+    /**
+     * Parse Protobuf data
+     *
+     * @param {byteArray} input
+     * @returns {Object}
+     */
+    static decode(input) {
+        const pb = new Protobuf(input);
+        return pb._parse();
+    }
+
+    // Private Class Functions
+
+    /**
+     * Main private parsing function
+     *
+     * @private
+     * @returns {Object}
+     */
+    _parse() {
+        let object = {};
+        // Continue reading whilst we still have data
+        while (this.offset < this.LENGTH) {
+            const field = this._parseField();
+            object = this._addField(field, object);
+        }
+        // Throw an error if we have gone beyond the end of the data
+        if (this.offset > this.LENGTH) {
+            throw new Error("Exhausted Buffer");
+        }
+        return object;
+    }
+
+    /**
+     * Add a field read from the protobuf data into the Object. As
+     * protobuf fields can appear multiple times, if the field already
+     * exists we need to add the new field into an array of fields
+     * for that key.
+     *
+     * @private
+     * @param {Object} field
+     * @param {Object} object
+     * @returns {Object}
+     */
+    _addField(field, object) {
+        // Get the field key/values
+        const key = field.key;
+        const value = field.value;
+        object[key] = object.hasOwnProperty(key) ?
+            object[key] instanceof Array ?
+                object[key].concat([value]) :
+                [object[key], value] :
+            value;
+        return object;
+    }
+
+    /**
+     * Parse a field and return the Object read from the record
+     *
+     * @private
+     * @returns {Object}
+     */
+    _parseField() {
+        // Get the field headers
+        const header = this._fieldHeader();
+        const type = header.type;
+        const key = header.key;
+        switch (type) {
+            // varint
+            case 0:
+                return { "key": key, "value": this._varInt() };
+            // fixed 64
+            case 1:
+                return { "key": key, "value": this._uint64() };
+            // length delimited
+            case 2:
+                return { "key": key, "value": this._lenDelim() };
+            // fixed 32
+            case 5:
+                return { "key": key, "value": this._uint32() };
+            // unknown type
+            default:
+                throw new Error("Unknown type 0x" + type.toString(16));
+        }
+    }
+
+    /**
+     * Parse the field header and return the type and key
+     *
+     * @private
+     * @returns {Object}
+     */
+    _fieldHeader() {
+        // Make sure we call type then number to preserve offset
+        return { "type": this._fieldType(), "key": this._fieldNumber() };
+    }
+
+    /**
+     * Parse the field type from the field header. Type is stored in the
+     * lower 3 bits of the tag byte. This does not move the offset on as
+     * we need to read the field number from the tag byte too.
+     *
+     * @private
+     * @returns {number}
+     */
+    _fieldType() {
+        // Field type stored in lower 3 bits of tag byte
+        return this.data[this.offset] & this.TYPE;
+    }
+
+    /**
+     * Parse the field number (i.e. the key) from the field header. The
+     * field number is stored in the upper 5 bits of the tag byte - but
+     * is also varint encoded so the follow on bytes may need to be read
+     * when field numbers are > 15.
+     *
+     * @private
+     * @returns {number}
+     */
+    _fieldNumber() {
+        let shift = -3;
+        let fieldNumber = 0;
+        do {
+            fieldNumber += shift < 28 ?
+                shift === -3 ?
+                    (this.data[this.offset] & this.NUMBER) >> -shift :
+                    (this.data[this.offset] & this.VALUE) << shift :
+                (this.data[this.offset] & this.VALUE) * Math.pow(2, shift);
+            shift += 7;
+        } while ((this.data[this.offset++] & this.MSD) === this.MSB);
+        return fieldNumber;
+    }
+
+    // Field Parsing Functions
+
+    /**
+     * Read off a varint from the data
+     *
+     * @private
+     * @returns {number}
+     */
+    _varInt() {
+        let value = 0;
+        let shift = 0;
+        // Keep reading while upper bit set
+        do {
+            value += shift < 28 ?
+                (this.data[this.offset] & this.VALUE) << shift :
+                (this.data[this.offset] & this.VALUE) * Math.pow(2, shift);
+            shift += 7;
+        } while ((this.data[this.offset++] & this.MSB) === this.MSB);
+        return value;
+    }
+
+    /**
+     * Read off a 64 bit unsigned integer from the data
+     *
+     * @private
+     * @returns {number}
+     */
+    _uint64() {
+        // Read off a Uint64
+        let num = this.data[this.offset++] * 0x1000000 + (this.data[this.offset++] << 16) + (this.data[this.offset++] << 8) + this.data[this.offset++];
+        num = num * 0x100000000 + this.data[this.offset++] * 0x1000000 + (this.data[this.offset++] << 16) + (this.data[this.offset++] << 8) + this.data[this.offset++];
+        return num;
+    }
+
+    /**
+     * Read off a length delimited field from the data
+     *
+     * @private
+     * @returns {Object|string}
+     */
+    _lenDelim() {
+        // Read off the field length
+        const length = this._varInt();
+        const fieldBytes = this.data.slice(this.offset, this.offset + length);
+        let field;
+        try {
+            // Attempt to parse as a new Protobuf Object
+            const pbObject = new Protobuf(fieldBytes);
+            field = pbObject._parse();
+        } catch (err) {
+            // Otherwise treat as bytes
+            field = Utils.byteArrayToChars(fieldBytes);
+        }
+        // Move the offset and return the field
+        this.offset += length;
+        return field;
+    }
+
+    /**
+     * Read a 32 bit unsigned integer from the data
+     *
+     * @private
+     * @returns {number}
+     */
+    _uint32() {
+        // Use a dataview to read off the integer
+        const dataview = new DataView(new Uint8Array(this.data.slice(this.offset, this.offset + 4)).buffer);
+        const value = dataview.getUint32(0);
+        this.offset += 4;
+        return value;
+    }
+}
+
+export default Protobuf;

+ 46 - 0
src/core/operations/ProtobufDecode.mjs

@@ -0,0 +1,46 @@
+/**
+ * @author GCHQ Contributor [3]
+ * @copyright Crown Copyright 2019
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+import OperationError from "../errors/OperationError";
+import Protobuf from "../lib/Protobuf";
+
+/**
+ * Protobuf Decode operation
+ */
+class ProtobufDecode extends Operation {
+
+    /**
+     * ProtobufDecode constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "Protobuf Decode";
+        this.module = "Default";
+        this.description = "Decodes any Protobuf encoded data to a JSON representation of the data using the field number as the field key.";
+        this.infoURL = "https://wikipedia.org/wiki/Protocol_Buffers";
+        this.inputType = "byteArray";
+        this.outputType = "JSON";
+        this.args = [];
+    }
+
+    /**
+     * @param {byteArray} input
+     * @param {Object[]} args
+     * @returns {JSON}
+     */
+    run(input, args) {
+        try {
+            return Protobuf.decode(input);
+        } catch (err) {
+            throw new OperationError(err);
+        }
+    }
+
+}
+
+export default ProtobufDecode;

+ 9 - 2
src/core/operations/RegularExpression.mjs

@@ -230,6 +230,7 @@ function regexHighlight (input, regex, displayTotal) {
         title = "",
         title = "",
         hl = 1,
         hl = 1,
         total = 0;
         total = 0;
+    const captureGroups = [];
 
 
     output = input.replace(regex, (match, ...args) => {
     output = input.replace(regex, (match, ...args) => {
         args.pop(); // Throw away full string
         args.pop(); // Throw away full string
@@ -247,9 +248,15 @@ function regexHighlight (input, regex, displayTotal) {
         // Switch highlight
         // Switch highlight
         hl = hl === 1 ? 2 : 1;
         hl = hl === 1 ? 2 : 1;
 
 
-        total++;
+        // Store highlighted match and replace with a placeholder
+        captureGroups.push(`<span class='hl${hl}' title='${title}'>${Utils.escapeHtml(match)}</span>`);
+        return `[cc_capture_group_${total++}]`;
+    });
 
 
-        return `<span class='hl${hl}' title='${title}'>${Utils.escapeHtml(match)}</span>`;
+    // Safely escape all remaining text, then replace placeholders
+    output = Utils.escapeHtml(output);
+    output = output.replace(/\[cc_capture_group_(\d+)\]/g, (_, i) => {
+        return captureGroups[i];
     });
     });
 
 
     if (displayTotal)
     if (displayTotal)

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

@@ -79,7 +79,7 @@ class TextEncodingBruteForce extends Operation {
         let table = "<table class='table table-hover table-sm table-bordered table-nonfluid'><tr><th>Encoding</th><th>Value</th></tr>";
         let table = "<table class='table table-hover table-sm table-bordered table-nonfluid'><tr><th>Encoding</th><th>Value</th></tr>";
 
 
         for (const enc in encodings) {
         for (const enc in encodings) {
-            const value = Utils.printable(encodings[enc], true);
+            const value = Utils.escapeHtml(Utils.printable(encodings[enc], true));
             table += `<tr><td>${enc}</td><td>${value}</td></tr>`;
             table += `<tr><td>${enc}</td><td>${value}</td></tr>`;
         }
         }
 
 

+ 46 - 0
src/core/operations/VarIntDecode.mjs

@@ -0,0 +1,46 @@
+/**
+ * @author GCHQ Contributor [3]
+ * @copyright Crown Copyright 2019
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+import OperationError from "../errors/OperationError";
+import Protobuf from "../lib/Protobuf";
+
+/**
+ * VarInt Decode operation
+ */
+class VarIntDecode extends Operation {
+
+    /**
+     * VarIntDecode constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "VarInt Decode";
+        this.module = "Default";
+        this.description = "Decodes a VarInt encoded integer. VarInt is an efficient way of encoding variable length integers and is commonly used with Protobuf.";
+        this.infoURL = "https://developers.google.com/protocol-buffers/docs/encoding#varints";
+        this.inputType = "byteArray";
+        this.outputType = "number";
+        this.args = [];
+    }
+
+    /**
+     * @param {byteArray} input
+     * @param {Object[]} args
+     * @returns {number}
+     */
+    run(input, args) {
+        try {
+            return Protobuf.varIntDecode(input);
+        } catch (err) {
+            throw new OperationError(err);
+        }
+    }
+
+}
+
+export default VarIntDecode;

+ 46 - 0
src/core/operations/VarIntEncode.mjs

@@ -0,0 +1,46 @@
+/**
+ * @author GCHQ Contributor [3]
+ * @copyright Crown Copyright 2019
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+import OperationError from "../errors/OperationError";
+import Protobuf from "../lib/Protobuf";
+
+/**
+ * VarInt Encode operation
+ */
+class VarIntEncode extends Operation {
+
+    /**
+     * VarIntEncode constructor
+     */
+    constructor() {
+        super();
+
+        this.name = "VarInt Encode";
+        this.module = "Default";
+        this.description = "Encodes a Vn integer as a VarInt. VarInt is an efficient way of encoding variable length integers and is commonly used with Protobuf.";
+        this.infoURL = "https://developers.google.com/protocol-buffers/docs/encoding#varints";
+        this.inputType = "number";
+        this.outputType = "byteArray";
+        this.args = [];
+    }
+
+    /**
+     * @param {number} input
+     * @param {Object[]} args
+     * @returns {byteArray}
+     */
+    run(input, args) {
+        try {
+            return Protobuf.varIntEncode(input);
+        } catch (err) {
+            throw new OperationError(err);
+        }
+    }
+
+}
+
+export default VarIntEncode;

+ 1 - 1
src/web/ControlsWaiter.mjs

@@ -338,7 +338,7 @@ class ControlsWaiter {
         const saveLink = this.generateStateUrl(true, true, null, "https://gchq.github.io/CyberChef/");
         const saveLink = this.generateStateUrl(true, true, null, "https://gchq.github.io/CyberChef/");
 
 
         if (reportBugInfo) {
         if (reportBugInfo) {
-            reportBugInfo.innerHTML = `* Version: ${PKG_VERSION + (typeof INLINE === "undefined" ? "" : "s")}
+            reportBugInfo.innerHTML = `* Version: ${PKG_VERSION}
 * Compile time: ${COMPILE_TIME}
 * Compile time: ${COMPILE_TIME}
 * User-Agent:
 * User-Agent:
 ${navigator.userAgent}
 ${navigator.userAgent}

+ 1 - 12
src/web/html/index.html

@@ -131,13 +131,6 @@
             };
             };
             window.addEventListener("error", loadingErrorHandler);
             window.addEventListener("error", loadingErrorHandler);
         </script>
         </script>
-        <% if (htmlWebpackPlugin.options.inline) { %>
-            <meta name="robots" content="noindex" />
-        <% } else { %>
-            <script type="application/ld+json">
-                <% print(JSON.stringify(require("../static/structuredData.json"))); %>
-            </script>
-        <% } %>
     </head>
     </head>
     <body>
     <body>
         <!-- Preloader overlay -->
         <!-- Preloader overlay -->
@@ -153,11 +146,7 @@
         <div id="content-wrapper">
         <div id="content-wrapper">
             <div id="banner" class="row">
             <div id="banner" class="row">
                 <div class="col" style="text-align: left; padding-left: 10px;">
                 <div class="col" style="text-align: left; padding-left: 10px;">
-                    <% if (htmlWebpackPlugin.options.inline) { %>
-                        <span>Version <%= htmlWebpackPlugin.options.version %></span>
-                    <% } else { %>
-                        <a href="cyberchef.htm" download>Download CyberChef <i class="material-icons">file_download</i></a>
-                    <% } %>
+                    <a href="CyberChef_v<%= htmlWebpackPlugin.options.version %>.zip" download>Download CyberChef <i class="material-icons">file_download</i></a>
                 </div>
                 </div>
                 <div class="col-md-6" id="notice-wrapper">
                 <div class="col-md-6" id="notice-wrapper">
                     <span id="notice">
                     <span id="notice">

+ 1 - 0
tests/operations/index.mjs

@@ -94,6 +94,7 @@ import "./tests/MultipleBombe";
 import "./tests/Typex";
 import "./tests/Typex";
 import "./tests/BLAKE2b";
 import "./tests/BLAKE2b";
 import "./tests/BLAKE2s";
 import "./tests/BLAKE2s";
+import "./tests/Protobuf";
 
 
 // Cannot test operations that use the File type yet
 // Cannot test operations that use the File type yet
 //import "./tests/SplitColourChannels";
 //import "./tests/SplitColourChannels";

+ 36 - 0
tests/operations/tests/Protobuf.mjs

@@ -0,0 +1,36 @@
+/**
+ * Protobuf tests.
+ *
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2019
+ * @license Apache-2.0
+ */
+
+import TestRegister from "../TestRegister";
+
+TestRegister.addTests([
+    {
+        name: "Protobuf Decode",
+        input: "0d1c0000001203596f751a024d65202b2a0a0a066162633132331200",
+        expectedOutput: JSON.stringify({
+            "1": 469762048,
+            "2": "You",
+            "3": "Me",
+            "4": 43,
+            "5": {
+                "1": "abc123",
+                "2": {}
+            }
+        }, null, 4),
+        recipeConfig: [
+            {
+                "op": "From Hex",
+                "args": ["Auto"]
+            },
+            {
+                "op": "Protobuf Decode",
+                "args": []
+            }
+        ]
+    },
+]);

+ 19 - 5
webpack.config.js

@@ -48,7 +48,7 @@ module.exports = {
             "process.browser": "true"
             "process.browser": "true"
         }),
         }),
         new MiniCssExtractPlugin({
         new MiniCssExtractPlugin({
-            filename: "[name].css"
+            filename: "assets/[name].css"
         }),
         }),
     ],
     ],
     resolve: {
     resolve: {
@@ -80,7 +80,12 @@ module.exports = {
             {
             {
                 test: /\.css$/,
                 test: /\.css$/,
                 use: [
                 use: [
-                    MiniCssExtractPlugin.loader,
+                    {
+                        loader: MiniCssExtractPlugin.loader,
+                        options: {
+                            publicPath: "../"
+                        }
+                    },
                     "css-loader",
                     "css-loader",
                     "postcss-loader",
                     "postcss-loader",
                 ]
                 ]
@@ -88,7 +93,12 @@ module.exports = {
             {
             {
                 test: /\.scss$/,
                 test: /\.scss$/,
                 use: [
                 use: [
-                    MiniCssExtractPlugin.loader,
+                    {
+                        loader: MiniCssExtractPlugin.loader,
+                        options: {
+                            publicPath: "../"
+                        }
+                    },
                     "css-loader",
                     "css-loader",
                     "sass-loader",
                     "sass-loader",
                 ]
                 ]
@@ -97,7 +107,9 @@ module.exports = {
                 test: /\.(ico|eot|ttf|woff|woff2)$/,
                 test: /\.(ico|eot|ttf|woff|woff2)$/,
                 loader: "url-loader",
                 loader: "url-loader",
                 options: {
                 options: {
-                    limit: 10000
+                    limit: 10000,
+                    name: "[hash].[ext]",
+                    outputPath: "assets"
                 }
                 }
             },
             },
             {
             {
@@ -120,7 +132,9 @@ module.exports = {
                 exclude: /web\/static/,
                 exclude: /web\/static/,
                 loader: "url-loader",
                 loader: "url-loader",
                 options: {
                 options: {
-                    limit: 10000
+                    limit: 10000,
+                    name: "[hash].[ext]",
+                    outputPath: "assets"
                 }
                 }
             },
             },
         ]
         ]

Some files were not shown because too many files changed in this diff