浏览代码

Merged upstream master

n1474335 7 年之前
父节点
当前提交
d3246b7c8b
共有 85 个文件被更改,包括 2185 次插入485 次删除
  1. 4 2
      .babelrc
  2. 2 2
      .travis.yml
  3. 28 9
      Gruntfile.js
  4. 7 7
      README.md
  5. 二进制
      docs/favicon.ico
  6. 1 1
      docs/jsdoc.conf.json
  7. 240 235
      package-lock.json
  8. 31 26
      package.json
  9. 135 1
      src/core/Utils.js
  10. 15 6
      src/core/config/Categories.js
  11. 224 40
      src/core/config/OperationConfig.js
  12. 2 0
      src/core/config/modules/Code.js
  13. 10 1
      src/core/config/modules/Default.js
  14. 8 6
      src/core/config/modules/Hashing.js
  15. 40 0
      src/core/operations/BitwiseOp.js
  16. 3 5
      src/core/operations/ByteRepr.js
  17. 13 29
      src/core/operations/Checksum.js
  18. 2 2
      src/core/operations/Cipher.js
  19. 43 0
      src/core/operations/Code.js
  20. 4 7
      src/core/operations/Extract.js
  21. 179 61
      src/core/operations/Hash.js
  22. 213 0
      src/core/operations/MS.js
  23. 55 0
      src/core/operations/OTP.js
  24. 0 2
      src/core/operations/PublicKey.js
  25. 5 5
      src/core/operations/Rotate.js
  26. 75 5
      src/core/operations/StrUtils.js
  27. 14 2
      src/web/App.js
  28. 15 8
      src/web/ControlsWaiter.js
  29. 6 1
      src/web/Manager.js
  30. 29 1
      src/web/OperationsWaiter.js
  31. 5 3
      src/web/RecipeWaiter.js
  32. 34 13
      src/web/html/index.html
  33. 二进制
      src/web/static/images/breakpoint-16x16.png
  34. 二进制
      src/web/static/images/bug-16x16.png
  35. 二进制
      src/web/static/images/clean-16x16.png
  36. 二进制
      src/web/static/images/code-16x16.png
  37. 二进制
      src/web/static/images/cook_female-32x32.png
  38. 二进制
      src/web/static/images/cook_male-32x32.png
  39. 二进制
      src/web/static/images/cyberchef-128x128.png
  40. 二进制
      src/web/static/images/cyberchef-16x16.png
  41. 二进制
      src/web/static/images/cyberchef-256x256.png
  42. 二进制
      src/web/static/images/cyberchef-32x32.png
  43. 二进制
      src/web/static/images/cyberchef-512x512.png
  44. 二进制
      src/web/static/images/cyberchef-64x64.png
  45. 二进制
      src/web/static/images/disable_deselected-16x16.png
  46. 二进制
      src/web/static/images/disable_selected-16x16.png
  47. 二进制
      src/web/static/images/download-24x24.png
  48. 二进制
      src/web/static/images/erase-16x16.png
  49. 二进制
      src/web/static/images/favourite-16x16.png
  50. 二进制
      src/web/static/images/favourite-24x24.png
  51. 二进制
      src/web/static/images/fork_me.png
  52. 二进制
      src/web/static/images/help-16x16.png
  53. 二进制
      src/web/static/images/help-22x22.png
  54. 二进制
      src/web/static/images/info-16x16.png
  55. 二进制
      src/web/static/images/layout-16x16.png
  56. 二进制
      src/web/static/images/mail-16x16.png
  57. 二进制
      src/web/static/images/maximise-16x16.png
  58. 二进制
      src/web/static/images/open_yellow-16x16.png
  59. 二进制
      src/web/static/images/open_yellow-24x24.png
  60. 二进制
      src/web/static/images/recycle-16x16.png
  61. 二进制
      src/web/static/images/remove-16x16.png
  62. 二进制
      src/web/static/images/restore-16x16.png
  63. 二进制
      src/web/static/images/save-16x16.png
  64. 二进制
      src/web/static/images/save-22x22.png
  65. 二进制
      src/web/static/images/save_as-16x16.png
  66. 二进制
      src/web/static/images/settings-22x22.png
  67. 二进制
      src/web/static/images/speech-16x16.png
  68. 二进制
      src/web/static/images/stats-16x16.png
  69. 二进制
      src/web/static/images/step-16x16.png
  70. 二进制
      src/web/static/images/switch-16x16.png
  71. 二进制
      src/web/static/images/thumb_down-16x16.png
  72. 二进制
      src/web/static/images/thumb_up-16x16.png
  73. 二进制
      src/web/static/images/undo-16x16.png
  74. 1 0
      src/web/stylesheets/index.css
  75. 7 1
      src/web/stylesheets/layout/_modals.css
  76. 2 2
      src/web/stylesheets/preloader.css
  77. 115 0
      src/web/stylesheets/themes/_geocities.css
  78. 4 0
      src/web/stylesheets/utils/_overrides.css
  79. 4 0
      test/index.js
  80. 50 0
      test/tests/operations/BitwiseOp.js
  81. 181 0
      test/tests/operations/Code.js
  82. 308 0
      test/tests/operations/Hash.js
  83. 22 0
      test/tests/operations/MS.js
  84. 44 0
      test/tests/operations/StrUtils.js
  85. 5 2
      webpack.config.js

+ 4 - 2
.babelrc

@@ -4,9 +4,11 @@
             "targets": {
                 "chrome": 40,
                 "firefox": 35,
-                "edge": 14
+                "edge": 14,
+                "node": "6.5",
             },
-            "modules": false
+            "modules": false,
+            "useBuiltIns": true
         }]
     ]
 }

+ 2 - 2
.travis.yml

@@ -1,6 +1,6 @@
 language: node_js
 node_js:
-  - node
+  - "8.4"
 install: npm install
 before_script:
   - npm install -g grunt
@@ -22,7 +22,7 @@ deploy:
       repo: gchq/CyberChef
       branch: master
   - provider: releases
-    skip_cleaup: true
+    skip_cleanup: true
     api_key:
       secure: "HV1WSKv4l/0Y2bKKs1iBJocBcmLj08PCRUeEM/jTwA4jqJ8EiLHWiXtER/D5sEg2iibRVKd2OQjfrmS6bo4AiwdeVgAKmv0FtS2Jw+391N8Nd5AkEANHa5Om/IpHLTL2YRAjpJTsDpY72bMUTJIwjQA3TFJkgrpOw6KYfohOcgbxLpZ4XuNJRU3VL4Hsxdv5V9aOVmfFOmMOVPQlakXy7NgtW5POp1f2WJwgcZxylkR1CjwaqMyXmSoVl46pyH3tr5+dptsQoKSGdi6sIHGA60oDotFPcm+0ifa47wZw+vapuuDi4tdNxhrHGaDMG8xiE0WFDHwQUDlk2/+W7j9SEX0H3Em7us371JXRp56EDwEcDa34VpVkC6i8HGcHK55hnxVbMZXGf3qhOFD8wY7qMbjMRvIpucrMHBi86OfkDfv0vDj2LyvIl5APj/AX50BrE0tfH1MZbH26Jkx4NdlkcxQ14GumarmUqfmVvbX/fsoA6oUuAAE9ZgRRi3KHO4wci6KUcRfdm+XOeUkaBFsL86G3EEYIvrtBTuaypdz+Cx7nd1iPZyWMx5Y1gXnVzha4nBdV4+7l9JIsFggD8QVpw2uHXQiS1KXFjOeqA3DBD8tjMB7q26Fl2fD3jkOo4BTbQ2NrRIZUu/iL+fOmMPsyMt2qulB0yaSBCfkbEq8xrUA="
     file:

+ 28 - 9
Gruntfile.js

@@ -1,5 +1,6 @@
 const webpack = require("webpack");
 const HtmlWebpackPlugin = require("html-webpack-plugin");
+const NodeExternals = require("webpack-node-externals");
 const Inliner = require("web-resource-inliner");
 const fs = require("fs");
 
@@ -123,7 +124,7 @@ module.exports = function (grunt) {
             prod: ["build/prod/*", "src/core/config/MetaConfig.js"],
             test: ["build/test/*", "src/core/config/MetaConfig.js"],
             node: ["build/node/*", "src/core/config/MetaConfig.js"],
-            docs: ["docs/*", "!docs/*.conf.json", "!docs/*.ico"],
+            docs: ["docs/*", "!docs/*.conf.json", "!docs/*.ico", "!docs/*.png"],
             inlineScripts: ["build/prod/scripts.js"],
         },
         eslint: {
@@ -180,7 +181,8 @@ module.exports = function (grunt) {
                     library: "MetaConfig",
                     libraryTarget: "commonjs2",
                     libraryExport: "default"
-                }
+                },
+                externals: [NodeExternals()],
             },
             metaConfDev: {
                 target: "node",
@@ -192,6 +194,7 @@ module.exports = function (grunt) {
                     libraryTarget: "commonjs2",
                     libraryExport: "default"
                 },
+                externals: [NodeExternals()],
                 watch: true
             },
             web: {
@@ -269,6 +272,7 @@ module.exports = function (grunt) {
             tests: {
                 target: "node",
                 entry: "./test/index.js",
+                externals: [NodeExternals()],
                 output: {
                     filename: "index.js",
                     path: __dirname + "/build/test"
@@ -277,6 +281,7 @@ module.exports = function (grunt) {
             node: {
                 target: "node",
                 entry: "./src/node/index.js",
+                externals: [NodeExternals()],
                 output: {
                     filename: "CyberChef.js",
                     path: __dirname + "/build/node",
@@ -324,15 +329,29 @@ module.exports = function (grunt) {
         copy: {
             ghPages: {
                 options: {
-                    process: function (content) {
+                    process: function (content, srcpath) {
                         // Add Google Analytics code to index.html
-                        content = content.replace("</body></html>",
-                            grunt.file.read("src/web/static/ga.html") + "</body></html>");
-                        return grunt.template.process(content);
-                    }
+                        if (srcpath.indexOf("index.html") >= 0) {
+                            content = content.replace("</body></html>",
+                                grunt.file.read("src/web/static/ga.html") + "</body></html>");
+                            return grunt.template.process(content, srcpath);
+                        } else {
+                            return content;
+                        }
+                    },
+                    noProcess: ["**", "!**/*.html"]
                 },
-                src: "build/prod/index.html",
-                dest: "build/prod/index.html"
+                files: [
+                    {
+                        src: "build/prod/index.html",
+                        dest: "build/prod/index.html"
+                    },
+                    {
+                        expand: true,
+                        src: "docs/**",
+                        dest: "build/prod/"
+                    }
+                ]
             }
         },
         chmod: {

+ 7 - 7
README.md

@@ -88,10 +88,10 @@ CyberChef is released under the [Apache 2.0 Licence](https://www.apache.org/lice
 
 
   [1]: https://gchq.github.io/CyberChef
-  [2]: https://gchq.github.io/CyberChef/#recipe=%5B%7B%22op%22%3A%22From%20Base64%22%2C%22args%22%3A%5B%22A-Za-z0-9%2B%2F%3D%22%2Ctrue%5D%7D%5D&input=VTI4Z2JHOXVaeUJoYm1RZ2RHaGhibXR6SUdadmNpQmhiR3dnZEdobElHWnBjMmd1
-  [3]: https://gchq.github.io/CyberChef/#recipe=%5B%7B%22op%22%3A%22Translate%20DateTime%20Format%22%2C%22args%22%3A%5B%22Standard%20date%20and%20time%22%2C%22DD%2FMM%2FYYYY%20HH%3Amm%3Ass%22%2C%22UTC%22%2C%22dddd%20Do%20MMMM%20YYYY%20HH%3Amm%3Ass%20Z%20z%22%2C%22Australia%2FQueensland%22%5D%7D%5D&input=MTUvMDYvMjAxNSAyMDo0NTowMA
-  [4]: https://gchq.github.io/CyberChef/#recipe=%5B%7B%22op%22%3A%22Parse%20IPv6%20address%22%2C%22args%22%3A%5B%5D%7D%5D&input=MjAwMTowMDAwOjQxMzY6ZTM3ODo4MDAwOjYzYmY6M2ZmZjpmZGQy
-  [5]: https://gchq.github.io/CyberChef/#recipe=%5B%7B%22op%22%3A%22From%20Hexdump%22%2C%22args%22%3A%5B%5D%7D%2C%7B%22op%22%3A%22Gunzip%22%2C%22args%22%3A%5B%5D%7D%5D&input=MDAwMDAwMDAgIDFmIDhiIDA4IDAwIDEyIGJjIGYzIDU3IDAwIGZmIDBkIGM3IGMxIDA5IDAwIDIwICB8Li4uLi6881cu%2Fy7HwS4uIHwKMDAwMDAwMTAgIDA4IDA1IGQwIDU1IGZlIDA0IDJkIGQzIDA0IDFmIGNhIDhjIDQ0IDIxIDViIGZmICB8Li7QVf4uLdMuLsouRCFb%2F3wKMDAwMDAwMjAgIDYwIGM3IGQ3IDAzIDE2IGJlIDQwIDFmIDc4IDRhIDNmIDA5IDg5IDBiIDlhIDdkICB8YMfXLi6%2BQC54Sj8uLi4ufXwKMDAwMDAwMzAgIDRlIGM4IDRlIDZkIDA1IDFlIDAxIDhiIDRjIDI0IDAwIDAwIDAwICAgICAgICAgICB8TshObS4uLi5MJC4uLnw
-  [6]: https://gchq.github.io/CyberChef/#recipe=%5B%7B%22op%22%3A%22Fork%22%2C%22args%22%3A%5B%22%5C%5Cn%22%2C%22%5C%5Cn%22%5D%7D%2C%7B%22op%22%3A%22From%20UNIX%20Timestamp%22%2C%22args%22%3A%5B%22Seconds%20(s)%22%5D%7D%5D&input=OTc4MzQ2ODAwCjEwMTI2NTEyMDAKMTA0NjY5NjQwMAoxMDgxMDg3MjAwCjExMTUzMDUyMDAKMTE0OTYwOTYwMA
-  [7]: https://gchq.github.io/CyberChef/#recipe=%5B%7B%22op%22%3A%22Fork%22%2C%22args%22%3A%5B%22%5C%5Cn%22%2C%22%5C%5Cn%22%5D%7D%2C%7B%22op%22%3A%22Conditional%20Jump%22%2C%22args%22%3A%5B%221%22%2C%222%22%2C%2210%22%5D%7D%2C%7B%22op%22%3A%22To%20Hex%22%2C%22args%22%3A%5B%22Space%22%5D%7D%2C%7B%22op%22%3A%22Return%22%2C%22args%22%3A%5B%5D%7D%2C%7B%22op%22%3A%22To%20Base64%22%2C%22args%22%3A%5B%22A-Za-z0-9%2B%2F%3D%22%5D%7D%5D&input=U29tZSBkYXRhIHdpdGggYSAxIGluIGl0ClNvbWUgZGF0YSB3aXRoIGEgMiBpbiBpdA
-  [8]: https://gchq.github.io/CyberChef/#recipe=%5B%7B%22op%22%3A%22XOR%22%2C%22args%22%3A%5B%7B%22option%22%3A%22Hex%22%2C%22string%22%3A%223a%22%7D%2Cfalse%2Cfalse%5D%7D%2C%7B%22op%22%3A%22To%20Hexdump%22%2C%22args%22%3A%5B%2216%22%2Cfalse%2Cfalse%5D%7D%5D&input=VGhlIGFuc3dlciB0byB0aGUgdWx0aW1hdGUgcXVlc3Rpb24gb2YgbGlmZSwgdGhlIFVuaXZlcnNlLCBhbmQgZXZlcnl0aGluZyBpcyA0Mi4
+  [2]: https://gchq.github.io/CyberChef/#recipe=From_Base64('A-Za-z0-9%2B/%3D',true)&input=VTI4Z2JHOXVaeUJoYm1RZ2RHaGhibXR6SUdadmNpQmhiR3dnZEdobElHWnBjMmd1
+  [3]: https://gchq.github.io/CyberChef/#recipe=Translate_DateTime_Format('Standard%20date%20and%20time','DD/MM/YYYY%20HH:mm:ss','UTC','dddd%20Do%20MMMM%20YYYY%20HH:mm:ss%20Z%20z','Australia/Queensland')&input=MTUvMDYvMjAxNSAyMDo0NTowMA
+  [4]: https://gchq.github.io/CyberChef/#recipe=Parse_IPv6_address()&input=MjAwMTowMDAwOjQxMzY6ZTM3ODo4MDAwOjYzYmY6M2ZmZjpmZGQy
+  [5]: https://gchq.github.io/CyberChef/#recipe=From_Hexdump()Gunzip()&input=MDAwMDAwMDAgIDFmIDhiIDA4IDAwIDEyIGJjIGYzIDU3IDAwIGZmIDBkIGM3IGMxIDA5IDAwIDIwICB8Li4uLi6881cu/y7HwS4uIHwKMDAwMDAwMTAgIDA4IDA1IGQwIDU1IGZlIDA0IDJkIGQzIDA0IDFmIGNhIDhjIDQ0IDIxIDViIGZmICB8Li7QVf4uLdMuLsouRCFb/3wKMDAwMDAwMjAgIDYwIGM3IGQ3IDAzIDE2IGJlIDQwIDFmIDc4IDRhIDNmIDA5IDg5IDBiIDlhIDdkICB8YMfXLi6%2BQC54Sj8uLi4ufXwKMDAwMDAwMzAgIDRlIGM4IDRlIDZkIDA1IDFlIDAxIDhiIDRjIDI0IDAwIDAwIDAwICAgICAgICAgICB8TshObS4uLi5MJC4uLnw
+  [6]: https://gchq.github.io/CyberChef/#recipe=Fork('%5C%5Cn','%5C%5Cn',false)From_UNIX_Timestamp('Seconds%20(s)')&input=OTc4MzQ2ODAwCjEwMTI2NTEyMDAKMTA0NjY5NjQwMAoxMDgxMDg3MjAwCjExMTUzMDUyMDAKMTE0OTYwOTYwMA
+  [7]: https://gchq.github.io/CyberChef/#recipe=Fork('%5C%5Cn','%5C%5Cn',false)Conditional_Jump('1',2,10)To_Hex('Space')Return()To_Base64('A-Za-z0-9%2B/%3D')&input=U29tZSBkYXRhIHdpdGggYSAxIGluIGl0ClNvbWUgZGF0YSB3aXRoIGEgMiBpbiBpdA
+  [8]: https://gchq.github.io/CyberChef/#recipe=XOR(%7B'option':'Hex','string':'3a'%7D,'',false)To_Hexdump(16,false,false)&input=VGhlIGFuc3dlciB0byB0aGUgdWx0aW1hdGUgcXVlc3Rpb24gb2YgbGlmZSwgdGhlIFVuaXZlcnNlLCBhbmQgZXZlcnl0aGluZyBpcyA0Mi4

二进制
docs/favicon.ico


+ 1 - 1
docs/jsdoc.conf.json

@@ -19,7 +19,7 @@
         "outputSourcePath": true,
         "dateFormat": "ddd MMM Do YYYY",
         "sort": false,
-        "logoFile": "../build/prod/images/cyberchef-32x32.png",
+        "logoFile": "cyberchef-32x32.png",
         "cleverLinks": false,
         "monospaceLinks": false,
         "protocol": "html://",

文件差异内容过多而无法显示
+ 240 - 235
package-lock.json


+ 31 - 26
package.json

@@ -1,6 +1,6 @@
 {
   "name": "cyberchef",
-  "version": "5.12.2",
+  "version": "5.20.0",
   "description": "The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.",
   "author": "n1474335 <n1474335@gmail.com>",
   "homepage": "https://gchq.github.io/CyberChef",
@@ -30,65 +30,70 @@
   "main": "build/node/CyberChef.js",
   "bugs": "https://github.com/gchq/CyberChef/issues",
   "devDependencies": {
-    "babel-core": "^6.24.0",
-    "babel-loader": "^7.1.1",
-    "babel-polyfill": "^6.23.0",
+    "babel-core": "^6.26.0",
+    "babel-loader": "^7.1.2",
     "babel-preset-env": "^1.6.0",
-    "babel-regenerator-runtime": "^6.5.0",
-    "css-loader": "^0.28.4",
+    "css-loader": "^0.28.7",
     "exports-loader": "^0.6.4",
     "extract-text-webpack-plugin": "^3.0.0",
     "file-loader": "^0.11.2",
-    "grunt": ">=0.4.5",
+    "grunt": ">=1.0.1",
     "grunt-accessibility": "~5.0.0",
     "grunt-chmod": "~1.1.1",
     "grunt-concurrent": "^2.3.1",
     "grunt-contrib-clean": "~1.1.0",
     "grunt-contrib-copy": "~1.0.0",
-    "grunt-eslint": "^20.0.0",
-    "grunt-exec": "~2.0.0",
+    "grunt-eslint": "^20.1.0",
+    "grunt-exec": "~3.0.0",
     "grunt-execute": "^0.2.2",
-    "grunt-jsdoc": "^2.1.0",
+    "grunt-jsdoc": "^2.1.1",
     "grunt-webpack": "^3.0.2",
-    "html-webpack-plugin": "^2.29.0",
+    "html-webpack-plugin": "^2.30.1",
     "imports-loader": "^0.7.1",
-    "ink-docstrap": "^1.1.4",
+    "ink-docstrap": "^1.3.0",
     "jsdoc-babel": "^0.3.0",
     "less": "^2.7.2",
     "less-loader": "^4.0.5",
-    "postcss-css-variables": "^0.7.0",
+    "postcss-css-variables": "^0.8.0",
     "postcss-import": "^10.0.0",
-    "postcss-loader": "^2.0.5",
+    "postcss-loader": "^2.0.6",
     "style-loader": "^0.18.2",
-    "url-loader": "^0.5.8",
+    "url-loader": "^0.5.9",
     "val-loader": "^1.0.2",
-    "web-resource-inliner": "^4.1.0",
-    "webpack": "^3.3.0",
+    "web-resource-inliner": "^4.1.1",
+    "webpack": "^3.5.6",
     "webpack-dev-server": "^2.5.0",
+    "webpack-node-externals": "^1.6.0",
     "worker-loader": "^0.8.0"
   },
   "dependencies": {
+    "babel-polyfill": "^6.26.0",
     "bootstrap": "^3.3.7",
     "bootstrap-colorpicker": "^2.5.1",
     "bootstrap-switch": "^3.3.4",
-    "crypto-api": "^0.6.2",
+    "crypto-api": "^0.7.3",
     "crypto-js": "^3.1.9-1",
-    "diff": "^3.3.0",
-    "escodegen": "^1.8.1",
+    "diff": "^3.3.1",
+    "escodegen": "^1.9.0",
     "esmangle": "^1.0.1",
     "esprima": "^4.0.0",
     "exif-parser": "^0.1.12",
     "google-code-prettify": "^1.0.5",
-    "jquery": "^3.1.1",
+    "jquery": "^3.2.1",
+    "js-crc": "^0.2.0",
+    "js-sha3": "^0.6.1",
     "jsbn": "^1.1.0",
+    "jsonpath": "^0.2.12",
     "jsrsasign": "8.0.3",
     "lodash": "^4.17.4",
-    "moment": "^2.17.1",
-    "moment-timezone": "^0.5.11",
+    "moment": "^2.18.1",
+    "moment-timezone": "^0.5.13",
+    "node-md6": "^0.1.0",
+    "otp": "^0.1.3",
     "sladex-blowfish": "^0.8.1",
-    "sortablejs": "^1.5.1",
-    "split.js": "^1.2.0",
-    "vkbeautify": "^0.99.1",
+    "sortablejs": "^1.6.1",
+    "split.js": "^1.3.5",
+    "vkbeautify": "^0.99.3",
     "xmldom": "^0.1.27",
     "xpath": "0.0.24",
     "zlibjs": "^0.3.1"

+ 135 - 1
src/core/Utils.js

@@ -843,6 +843,139 @@ const Utils = {
     },
 
 
+    /**
+     * Encodes a URI fragment (#) or query (?) using a minimal amount of percent-encoding.
+     *
+     * RFC 3986 defines legal characters for the fragment and query parts of a URL to be as follows:
+     *
+     * fragment      = *( pchar / "/" / "?" )
+     * query         = *( pchar / "/" / "?" )
+     * pchar         = unreserved / pct-encoded / sub-delims / ":" / "@" 
+     * unreserved    = ALPHA / DIGIT / "-" / "." / "_" / "~"
+     * pct-encoded   = "%" HEXDIG HEXDIG
+     * sub-delims    = "!" / "$" / "&" / "'" / "(" / ")"
+     *                  / "*" / "+" / "," / ";" / "="
+     *
+     * Meaning that the list of characters that need not be percent-encoded are alphanumeric plus:
+     * -._~!$&'()*+,;=:@/?
+     *
+     * & and = are still escaped as they are used to serialise the key-value pairs in CyberChef
+     * fragments. + is also escaped so as to prevent it being decoded to a space.
+     *
+     * @param {string} str
+     * @returns {string}
+     */
+    encodeURIFragment: function(str) {
+        const LEGAL_CHARS = {
+            "%2D": "-",
+            "%2E": ".",
+            "%5F": "_",
+            "%7E": "~",
+            "%21": "!",
+            "%24": "$",
+            //"%26": "&",
+            "%27": "'",
+            "%28": "(",
+            "%29": ")",
+            "%2A": "*",
+            //"%2B": "+",
+            "%2C": ",",
+            "%3B": ";",
+            //"%3D": "=",
+            "%3A": ":",
+            "%40": "@",
+            "%2F": "/",
+            "%3F": "?"
+        };
+        str = encodeURIComponent(str);
+
+        return str.replace(/%[0-9A-F]{2}/g, function (match) {
+            return LEGAL_CHARS[match] || match;
+        });
+    },
+
+
+    /**
+     * Generates a "pretty" recipe format from a recipeConfig object.
+     *
+     * "Pretty" CyberChef recipe formats are designed to be included in the fragment (#) or query (?)
+     * parts of the URL. They can also be loaded into CyberChef through the 'Load' interface. In order
+     * to make this format as readable as possible, various special characters are used unescaped. This
+     * reduces the amount of percent-encoding included in the URL which is typically difficult to read,
+     * as well as substantially increasing the overall length. These characteristics can be quite
+     * offputting for users.
+     *
+     * @param {Object[]} recipeConfig
+     * @param {boolean} newline - whether to add a newline after each operation
+     * @returns {string}
+     */
+    generatePrettyRecipe: function(recipeConfig, newline) {
+        let prettyConfig = "",
+            name = "",
+            args = "",
+            disabled = "",
+            bp = "";
+
+        recipeConfig.forEach(op => {
+            name = op.op.replace(/ /g, "_");
+            args = JSON.stringify(op.args)
+                .slice(1, -1) // Remove [ and ] as they are implied
+                // We now need to switch double-quoted (") strings to single-quotes (') as these do not
+                // need to be percent-encoded.
+                .replace(/'/g, "\\'") // Escape single quotes
+                .replace(/\\"/g, '"') // Unescape double quotes
+                .replace(/(^|,|{|:)"/g, "$1'") // Replace opening " with '
+                .replace(/"(,|:|}|$)/g, "'$1"); // Replace closing " with '
+
+            disabled = op.disabled ? "/disabled": "";
+            bp = op.breakpoint ? "/breakpoint" : "";
+            prettyConfig += `${name}(${args}${disabled}${bp})`;
+            if (newline) prettyConfig += "\n";
+        });
+        return prettyConfig;
+    },
+
+
+    /**
+     * Converts a recipe string to the JSON representation of the recipe.
+     * Accepts either stringified JSON or bespoke "pretty" recipe format.
+     *
+     * @param {string} recipe
+     * @returns {Object[]}
+     */
+    parseRecipeConfig: function(recipe) {
+        recipe = recipe.trim();
+        if (recipe.length === 0) return [];
+        if (recipe[0] === "[") return JSON.parse(recipe);
+
+        // Parse bespoke recipe format
+        recipe = recipe.replace(/\n/g, "");
+        let m,
+            recipeRegex = /([^(]+)\(((?:'[^'\\]*(?:\\.[^'\\]*)*'|[^)/])*)(\/[^)]+)?\)/g,
+            recipeConfig = [],
+            args;
+
+        while ((m = recipeRegex.exec(recipe))) {
+            // Translate strings in args back to double-quotes
+            args = m[2]
+                .replace(/"/g, '\\"') // Escape double quotes
+                .replace(/(^|,|{|:)'/g, '$1"') // Replace opening ' with "
+                .replace(/([^\\])'(,|:|}|$)/g, '$1"$2') // Replace closing ' with "
+                .replace(/\\'/g, "'"); // Unescape single quotes
+            args = "[" + args + "]";
+
+            let op = {
+                op: m[1].replace(/_/g, " "),
+                args: JSON.parse(args)
+            };
+            if (m[3] && m[3].indexOf("disabled") > 0) op.disabled = true;
+            if (m[3] && m[3].indexOf("breakpoint") > 0) op.breakpoint = true;
+            recipeConfig.push(op);
+        }
+        return recipeConfig;
+    },
+
+
     /**
      * Expresses a number of milliseconds in a human readable format.
      *
@@ -1102,7 +1235,8 @@ const Utils = {
         "Forward slash": /\//g,
         "Backslash":     /\\/g,
         "0x":            /0x/g,
-        "\\x":           /\\x/g
+        "\\x":           /\\x/g,
+        "None":          /\s+/g // Included here to remove whitespace when there shouldn't be any
     },
 
 

+ 15 - 6
src/core/config/Categories.js

@@ -122,6 +122,8 @@ const Categories = [
             "AND",
             "ADD",
             "SUB",
+            "Bit shift left",
+            "Bit shift right",
             "Rotate left",
             "Rotate right",
             "ROT13",
@@ -173,7 +175,6 @@ const Categories = [
             "Tail",
             "Count occurrences",
             "Expand alphabet range",
-            "Parse escaped string",
             "Drop bytes",
             "Take bytes",
             "Pad lines",
@@ -188,6 +189,8 @@ const Categories = [
             "Parse UNIX file permissions",
             "Swap endianness",
             "Parse colour code",
+            "Escape string",
+            "Unescape string",
         ]
     },
     {
@@ -215,6 +218,7 @@ const Categories = [
             "Extract dates",
             "Regular expression",
             "XPath expression",
+            "JPath expression",
             "CSS selector",
             "Extract EXIF",
         ]
@@ -243,20 +247,21 @@ const Categories = [
             "MD2",
             "MD4",
             "MD5",
+            "MD6",
             "SHA0",
             "SHA1",
-            "SHA224",
-            "SHA256",
-            "SHA384",
-            "SHA512",
+            "SHA2",
             "SHA3",
-            "RIPEMD-160",
+            "Keccak",
+            "Shake",
+            "RIPEMD",
             "HMAC",
             "Fletcher-8 Checksum",
             "Fletcher-16 Checksum",
             "Fletcher-32 Checksum",
             "Fletcher-64 Checksum",
             "Adler-32 Checksum",
+            "CRC-16 Checksum",
             "CRC-32 Checksum",
             "TCP/IP Checksum",
         ]
@@ -278,7 +283,9 @@ const Categories = [
             "CSS Beautify",
             "CSS Minify",
             "XPath expression",
+            "JPath expression",
             "CSS selector",
+            "Microsoft Script Decoder",
             "Strip HTML tags",
             "Diff",
             "To Snake case",
@@ -294,6 +301,8 @@ const Categories = [
             "Detect File Type",
             "Scan for Embedded Files",
             "Generate UUID",
+            "Generate TOTP",
+            "Generate HOTP",
             "Render Image",
             "Remove EXIF",
             "Extract EXIF",

+ 224 - 40
src/core/config/OperationConfig.js

@@ -26,9 +26,11 @@ import IP from "../operations/IP.js";
 import JS from "../operations/JS.js";
 import MAC from "../operations/MAC.js";
 import MorseCode from "../operations/MorseCode.js";
+import MS from "../operations/MS.js";
 import NetBIOS from "../operations/NetBIOS.js";
 import Numberwang from "../operations/Numberwang.js";
 import OS from "../operations/OS.js";
+import OTP from "../operations/OTP.js";
 import PublicKey from "../operations/PublicKey.js";
 import Punycode from "../operations/Punycode.js";
 import QuotedPrintable from "../operations/QuotedPrintable.js";
@@ -521,6 +523,7 @@ const OperationConfig = {
             }
         ]
     },
+
     "To Charcode": {
         module: "Default",
         description: "Converts text to its unicode character code equivalent.<br><br>e.g. <code>Γειά σου</code> becomes <code>0393 03b5 03b9 03ac 20 03c3 03bf 03c5</code>",
@@ -1595,41 +1598,41 @@ const OperationConfig = {
     },
     "Rotate right": {
         module: "Default",
-        description: "Rotates each byte to the right by the number of bits specified. Currently only supports 8-bit values.",
+        description: "Rotates each byte to the right by the number of bits specified, optionally carrying the excess bits over to the next byte. Currently only supports 8-bit values.",
         highlight: true,
         highlightReverse: true,
         inputType: "byteArray",
         outputType: "byteArray",
         args: [
             {
-                name: "Number of bits",
+                name: "Amount",
                 type: "number",
                 value: Rotate.ROTATE_AMOUNT
             },
             {
-                name: "Rotate as a whole",
+                name: "Carry through",
                 type: "boolean",
-                value: Rotate.ROTATE_WHOLE
+                value: Rotate.ROTATE_CARRY
             }
         ]
     },
     "Rotate left": {
         module: "Default",
-        description: "Rotates each byte to the left by the number of bits specified. Currently only supports 8-bit values.",
+        description: "Rotates each byte to the left by the number of bits specified, optionally carrying the excess bits over to the next byte. Currently only supports 8-bit values.",
         highlight: true,
         highlightReverse: true,
         inputType: "byteArray",
         outputType: "byteArray",
         args: [
             {
-                name: "Number of bits",
+                name: "Amount",
                 type: "number",
                 value: Rotate.ROTATE_AMOUNT
             },
             {
-                name: "Rotate as a whole",
+                name: "Carry through",
                 type: "boolean",
-                value: Rotate.ROTATE_WHOLE
+                value: Rotate.ROTATE_CARRY
             }
         ]
     },
@@ -2139,7 +2142,7 @@ const OperationConfig = {
     },
     "Extract domains": {
         module: "Default",
-        description: "Extracts domain names with common Top-Level Domains (TLDs).<br>Note that this will not include paths. Use <strong>Extract URLs</strong> to find entire URLs.",
+        description: "Extracts domain names.<br>Note that this will not include paths. Use <strong>Extract URLs</strong> to find entire URLs.",
         inputType: "string",
         outputType: "string",
         args: [
@@ -2244,6 +2247,24 @@ const OperationConfig = {
             }
         ]
     },
+    "JPath expression": {
+        module: "Code",
+        description: "Extract information from a JSON object with a JPath query.",
+        inputType: "string",
+        outputType: "string",
+        args: [
+            {
+                name: "Query",
+                type: "string",
+                value: Code.JPATH_INITIAL
+            },
+            {
+                name: "Result delimiter",
+                type: "binaryShortString",
+                value: Code.JPATH_DELIMITER
+            }
+        ]
+    },
     "CSS selector": {
         module: "Code",
         description: "Extract information from an HTML document with a CSS selector",
@@ -2862,67 +2883,113 @@ const OperationConfig = {
         outputType: "string",
         args: []
     },
-    "SHA0": {
+    "MD6": {
         module: "Hashing",
-        description: "SHA-0 is a retronym applied to the original version of the 160-bit hash function published in 1993 under the name 'SHA'. It was withdrawn shortly after publication due to an undisclosed 'significant flaw' and replaced by the slightly revised version SHA-1.",
+        description: "The MD6 (Message-Digest 6) algorithm is a cryptographic hash function. It uses a Merkle tree-like structure to allow for immense parallel computation of hashes for very long inputs.",
         inputType: "string",
         outputType: "string",
-        args: []
+        args: [
+            {
+                name: "Size",
+                type: "number",
+                value: Hash.MD6_SIZE
+            },
+            {
+                name: "Levels",
+                type: "number",
+                value: Hash.MD6_LEVELS
+            },
+            {
+                name: "Key",
+                type: "string",
+                value: ""
+            }
+        ]
     },
-    "SHA1": {
+    "SHA0": {
         module: "Hashing",
-        description: "The SHA (Secure Hash Algorithm) hash functions were designed by the NSA. SHA-1 is the most established of the existing SHA hash functions and it is used in a variety of security applications and protocols.<br><br>However, SHA-1's collision resistance has been weakening as new attacks are discovered or improved.",
+        description: "SHA-0 is a retronym applied to the original version of the 160-bit hash function published in 1993 under the name 'SHA'. It was withdrawn shortly after publication due to an undisclosed 'significant flaw' and replaced by the slightly revised version SHA-1.",
         inputType: "string",
         outputType: "string",
         args: []
     },
-    "SHA224": {
+    "SHA1": {
         module: "Hashing",
-        description: "SHA-224 is largely identical to SHA-256 but is truncated to 224 bytes.",
+        description: "The SHA (Secure Hash Algorithm) hash functions were designed by the NSA. SHA-1 is the most established of the existing SHA hash functions and it is used in a variety of security applications and protocols.<br><br>However, SHA-1's collision resistance has been weakening as new attacks are discovered or improved.",
         inputType: "string",
         outputType: "string",
         args: []
     },
-    "SHA256": {
+    "SHA2": {
         module: "Hashing",
-        description: "SHA-256 is one of the four variants in the SHA-2 set. It isn't as widely used as SHA-1, though it provides much better security.",
+        description: "The SHA-2 (Secure Hash Algorithm 2) hash functions were designed by the NSA. SHA-2 includes significant changes from its predecessor, SHA-1. The SHA-2 family consists of hash functions with digests (hash values) that are 224, 256, 384 or 512 bits: SHA224, SHA256, SHA384, SHA512.<br><br><ul><li>SHA-512 operates on 64-bit words.</li><li>SHA-256 operates on 32-bit words.</li><li>SHA-384 is largely identical to SHA-512 but is truncated to 384 bytes.</li><li>SHA-224 is largely identical to SHA-256 but is truncated to 224 bytes.</li><li>SHA-512/224 and SHA-512/256 are truncated versions of SHA-512, but the initial values are generated using the method described in Federal Information Processing Standards (FIPS) PUB 180-4.</li></ul>",
         inputType: "string",
         outputType: "string",
-        args: []
+        args: [
+            {
+                name: "Size",
+                type: "option",
+                value: Hash.SHA2_SIZE
+            }
+        ]
     },
-    "SHA384": {
+    "SHA3": {
         module: "Hashing",
-        description: "SHA-384 is largely identical to SHA-512 but is truncated to 384 bytes.",
+        description: "The SHA-3 (Secure Hash Algorithm 3) hash functions were released by NIST on August 5, 2015. Although part of the same series of standards, SHA-3 is internally quite different from the MD5-like structure of SHA-1 and SHA-2.<br><br>SHA-3 is a subset of the broader cryptographic primitive family Keccak designed by Guido Bertoni, Joan Daemen, Michaël Peeters, and Gilles Van Assche, building upon RadioGatún.",
         inputType: "string",
         outputType: "string",
-        args: []
+        args: [
+            {
+                name: "Size",
+                type: "option",
+                value: Hash.SHA3_SIZE
+            }
+        ]
     },
-    "SHA512": {
+    "Keccak": {
         module: "Hashing",
-        description: "SHA-512 is largely identical to SHA-256 but operates on 64-bit words rather than 32.",
+        description: "The Keccak hash algorithm was designed by Guido Bertoni, Joan Daemen, Michaël Peeters, and Gilles Van Assche, building upon RadioGatún. It was selected as the winner of the SHA-3 design competition.<br><br>This version of the algorithm is Keccak[c=2d] and differs from the SHA-3 specification.",
         inputType: "string",
         outputType: "string",
-        args: []
+        args: [
+            {
+                name: "Size",
+                type: "option",
+                value: Hash.KECCAK_SIZE
+            }
+        ]
     },
-    "SHA3": {
+    "Shake": {
         module: "Hashing",
-        description: "This is an implementation of Keccak[c=2d]. SHA3 functions based on different implementations of Keccak will give different results.",
+        description: "Shake is an Extendable Output Function (XOF) of the SHA-3 hash algorithm, part of the Keccak family, allowing for variable output length/size.",
         inputType: "string",
         outputType: "string",
         args: [
             {
-                name: "Output length",
+                name: "Capacity",
                 type: "option",
-                value: Hash.SHA3_LENGTH
+                value: Hash.SHAKE_CAPACITY
+            },
+            {
+                name: "Size",
+                type: "number",
+                value: Hash.SHAKE_SIZE
             }
         ]
+
     },
-    "RIPEMD-160": {
+    "RIPEMD": {
         module: "Hashing",
-        description: "RIPEMD (RACE Integrity Primitives Evaluation Message Digest) is a family of cryptographic hash functions developed in Leuven, Belgium, by Hans Dobbertin, Antoon Bosselaers and Bart Preneel at the COSIC research group at the Katholieke Universiteit Leuven, and first published in 1996.<br><br>RIPEMD was based upon the design principles used in MD4, and is similar in performance to the more popular SHA-1.<br><br>RIPEMD-160 is an improved, 160-bit version of the original RIPEMD, and the most common version in the family.",
+        description: "RIPEMD (RACE Integrity Primitives Evaluation Message Digest) is a family of cryptographic hash functions developed in Leuven, Belgium, by Hans Dobbertin, Antoon Bosselaers and Bart Preneel at the COSIC research group at the Katholieke Universiteit Leuven, and first published in 1996.<br><br>RIPEMD was based upon the design principles used in MD4, and is similar in performance to the more popular SHA-1.<br><br>",
         inputType: "string",
         outputType: "string",
-        args: []
+        args: [
+            {
+                name: "Size",
+                type: "option",
+                value: Hash.RIPEMD_SIZE
+            }
+        ]
     },
     "HMAC": {
         module: "Hashing",
@@ -2980,7 +3047,14 @@ const OperationConfig = {
     "CRC-32 Checksum": {
         module: "Hashing",
         description: "A cyclic redundancy check (CRC) is an error-detecting code commonly used in digital networks and storage devices to detect accidental changes to raw data.<br><br>The CRC was invented by W. Wesley Peterson in 1961; the 32-bit CRC function of Ethernet and many other standards is the work of several researchers and was published in 1975.",
-        inputType: "byteArray",
+        inputType: "string",
+        outputType: "string",
+        args: []
+    },
+    "CRC-16 Checksum": {
+        module: "Hashing",
+        description: "A cyclic redundancy check (CRC) is an error-detecting code commonly used in digital networks and storage devices to detect accidental changes to raw data.<br><br>The CRC was invented by W. Wesley Peterson in 1961.",
+        inputType: "string",
         outputType: "string",
         args: []
     },
@@ -3187,6 +3261,13 @@ const OperationConfig = {
             }
         ]
     },
+    "Microsoft Script Decoder": {
+        module: "Default",
+        description: "Decodes Microsoft Encoded Script files that have been encoded with Microsoft's custom encoding. These are often VBS (Visual Basic Script) files that are encoded and renamed with a '.vbe' extention or JS (JScript) files renamed with a '.jse' extention.<br><br><b>Sample</b><br><br>Encoded:<br><code>#@~^RQAAAA==-mD~sX|:/TP{~J:+dYbxL~@!F@*@!+@*@!&amp;@*eEI@#@&amp;@#@&amp;.jm.raY 214Wv:zms/obI0xEAAA==^#~@</code><br><br>Decoded:<br><code>var my_msg = &#34;Testing <1><2><3>!&#34;;\n\nVScript.Echo(my_msg);</code>",
+        inputType: "string",
+        outputType: "string",
+        args: []
+    },
     "Syntax highlighter": {
         module: "Code",
         description: "Adds syntax highlighting to a range of source code languages. Note that this will not indent the code. Use one of the 'Beautify' operations for that.",
@@ -3207,13 +3288,6 @@ const OperationConfig = {
             }
         ]
     },
-    "Parse escaped string": {
-        module: "Default",
-        description: "Replaces escaped characters with the bytes they represent.<br><br>e.g.<code>Hello\\nWorld</code> becomes <code>Hello<br>World</code>",
-        inputType: "string",
-        outputType: "string",
-        args: []
-    },
     "TCP/IP Checksum": {
         module: "Hashing",
         description: "Calculates the checksum for a TCP (Transport Control Protocol) or IP (Internet Protocol) header from an input of raw bytes.",
@@ -3253,6 +3327,20 @@ const OperationConfig = {
             }
         ]
     },
+    "Escape string": {
+        module: "Default",
+        description: "Escapes special characters in a string so that they do not cause conflicts. For example, <code>Don't stop me now</code> becomes <code>Don\\'t stop me now</code>.",
+        inputType: "string",
+        outputType: "string",
+        args: []
+    },
+    "Unescape string": {
+        module: "Default",
+        description: "Unescapes characters in a string that have been escaped. For example, <code>Don\\'t stop me now</code> becomes <code>Don't stop me now</code>.",
+        inputType: "string",
+        outputType: "string",
+        args: []
+    },
     "To Morse Code": {
         module: "Default",
         description: "Translates alphanumeric characters into International Morse Code.<br><br>Ignores non-Morse characters.<br><br>e.g. <code>SOS</code> becomes <code>... --- ...</code>",
@@ -3567,6 +3655,102 @@ const OperationConfig = {
         ]
 
     },
+    "Bit shift left": {
+        module: "Default",
+        description: "Shifts the bits in each byte towards the left by the specified amount.",
+        inputType: "byteArray",
+        outputType: "byteArray",
+        highlight: true,
+        highlightReverse: true,
+        args: [
+            {
+                name: "Amount",
+                type: "number",
+                value: 1
+            },
+        ]
+    },
+    "Bit shift right": {
+        module: "Default",
+        description: "Shifts the bits in each byte towards the right by the specified amount.<br><br><i>Logical shifts</i> replace the leftmost bits with zeros.<br><i>Arithmetic shifts</i> preserve the most significant bit (MSB) of the original byte keeping the sign the same (positive or negative).",
+        inputType: "byteArray",
+        outputType: "byteArray",
+        highlight: true,
+        highlightReverse: true,
+        args: [
+            {
+                name: "Amount",
+                type: "number",
+                value: 1
+            },
+            {
+                name: "Type",
+                type: "option",
+                value: BitwiseOp.BIT_SHIFT_TYPE
+            }
+        ]
+    },
+    "Generate TOTP": {
+        module: "Default",
+        description: "The Time-based One-Time Password algorithm (TOTP) is an algorithm that computes a one-time password from a shared secret key and the current time. It has been adopted as Internet Engineering Task Force standard RFC 6238, is the cornerstone of Initiative For Open Authentication (OATH), and is used in a number of two-factor authentication systems. A TOTP is an HOTP where the counter is the current time.<br><br>Enter the secret as the input or leave it blank for a random secret to be generated. T0 and T1 are in seconds.",
+        inputType: "byteArray",
+        outputType: "string",
+        args: [
+            {
+                name: "Name",
+                type: "string",
+                value: ""
+            },
+            {
+                name: "Key size",
+                type: "number",
+                value: 32
+            },
+            {
+                name: "Code length",
+                type: "number",
+                value: 6
+            },
+            {
+                name: "Epoch offset (T0)",
+                type: "number",
+                value: 0
+            },
+            {
+                name: "Interval (T1)",
+                type: "number",
+                value: 30
+            }
+        ]
+    },
+    "Generate HOTP": {
+        module: "Default",
+        description: "The HMAC-based One-Time Password algorithm (HOTP) is an algorithm that computes a one-time password from a shared secret key and an incrementing counter. It has been adopted as Internet Engineering Task Force standard RFC 4226, is the cornerstone of Initiative For Open Authentication (OATH), and is used in a number of two-factor authentication systems.<br><br>Enter the secret as the input or leave it blank for a random secret to be generated.",
+        inputType: "string",
+        outputType: "string",
+        args: [
+            {
+                name: "Name",
+                type: "string",
+                value: ""
+            },
+            {
+                name: "Key size",
+                type: "number",
+                value: 32
+            },
+            {
+                name: "Code length",
+                type: "number",
+                value: 6
+            },
+            {
+                name: "Counter",
+                type: "number",
+                value: 0
+            }
+        ]
+    },
 };
 
 

+ 2 - 0
src/core/config/modules/Code.js

@@ -10,6 +10,7 @@ import Code from "../../operations/Code.js";
  *  - vkbeautify
  *  - xmldom
  *  - xpath
+ *  - jpath
  *  - googlecodeprettify
  *
  * @author n1474335 [n1474335@gmail.com]
@@ -37,6 +38,7 @@ OpModules.Code = {
     "To Snake case":         Code.runToSnakeCase,
     "To Camel case":         Code.runToCamelCase,
     "To Kebab case":         Code.runToKebabCase,
+    "JPath expression":      Code.runJpath,
 };
 
 export default OpModules;

+ 10 - 1
src/core/config/modules/Default.js

@@ -15,9 +15,11 @@ import Hexdump from "../../operations/Hexdump.js";
 import HTML from "../../operations/HTML.js";
 import MAC from "../../operations/MAC.js";
 import MorseCode from "../../operations/MorseCode.js";
+import MS from "../../operations/MS.js";
 import NetBIOS from "../../operations/NetBIOS.js";
 import Numberwang from "../../operations/Numberwang.js";
 import OS from "../../operations/OS.js";
+import OTP from "../../operations/OTP.js";
 import QuotedPrintable from "../../operations/QuotedPrintable.js";
 import Rotate from "../../operations/Rotate.js";
 import SeqUtils from "../../operations/SeqUtils.js";
@@ -37,6 +39,7 @@ import UUID from "../../operations/UUID.js";
  * Libraries:
  *  - Utils.js
  *    - CryptoJS
+ *  - otp
  *
  * @author n1474335 [n1474335@gmail.com]
  * @copyright Crown Copyright 2017
@@ -85,6 +88,8 @@ OpModules.Default = {
     "ROT47":                Rotate.runRot47,
     "Rotate left":          Rotate.runRotl,
     "Rotate right":         Rotate.runRotr,
+    "Bit shift left":       BitwiseOp.runBitShiftLeft,
+    "Bit shift right":      BitwiseOp.runBitShiftRight,
     "XOR":                  BitwiseOp.runXor,
     "XOR Brute Force":      BitwiseOp.runXorBrute,
     "OR":                   BitwiseOp.runXor,
@@ -104,7 +109,8 @@ OpModules.Default = {
     "Find / Replace":       StrUtils.runFindReplace,
     "Split":                StrUtils.runSplit,
     "Filter":               StrUtils.runFilter,
-    "Parse escaped string": StrUtils.runParseEscapedString,
+    "Escape string":        StrUtils.runEscape,
+    "Unescape string":      StrUtils.runUnescape,
     "Head":                 StrUtils.runHead,
     "Tail":                 StrUtils.runTail,
     "Remove whitespace":    Tidy.runRemoveWhitespace,
@@ -137,12 +143,15 @@ OpModules.Default = {
     "Extract domains":      Extract.runDomains,
     "Extract file paths":   Extract.runFilePaths,
     "Extract dates":        Extract.runDates,
+    "Microsoft Script Decoder": MS.runDecodeScript,
     "Entropy":              Entropy.runEntropy,
     "Frequency distribution": Entropy.runFreqDistrib,
     "Detect File Type":     FileType.runDetect,
     "Scan for Embedded Files": FileType.runScanForEmbeddedFiles,
     "Generate UUID":        UUID.runGenerateV4,
     "Numberwang":           Numberwang.run,
+    "Generate TOTP":        OTP.runTOTP,
+    "Generate HOTP":        OTP.runHOTP,
     "Fork":                 FlowControl.runFork,
     "Merge":                FlowControl.runMerge,
     "Jump":                 FlowControl.runJump,

+ 8 - 6
src/core/config/modules/Hashing.js

@@ -6,8 +6,9 @@ import Hash from "../../operations/Hash.js";
  * Hashing module.
  *
  * Libraries:
- *  - CryptoJS
  *  - CryptoApi
+ *  - node-md6
+ *  - js-sha3
  *  - ./Checksum.js
  *
  * @author n1474335 [n1474335@gmail.com]
@@ -22,20 +23,21 @@ OpModules.Hashing = {
     "MD2":                  Hash.runMD2,
     "MD4":                  Hash.runMD4,
     "MD5":                  Hash.runMD5,
+    "MD6":                  Hash.runMD6,
     "SHA0":                 Hash.runSHA0,
     "SHA1":                 Hash.runSHA1,
-    "SHA224":               Hash.runSHA224,
-    "SHA256":               Hash.runSHA256,
-    "SHA384":               Hash.runSHA384,
-    "SHA512":               Hash.runSHA512,
+    "SHA2":                 Hash.runSHA2,
     "SHA3":                 Hash.runSHA3,
-    "RIPEMD-160":           Hash.runRIPEMD160,
+    "Keccak":               Hash.runKeccak,
+    "Shake":                Hash.runShake,
+    "RIPEMD":               Hash.runRIPEMD,
     "HMAC":                 Hash.runHMAC,
     "Fletcher-8 Checksum":  Checksum.runFletcher8,
     "Fletcher-16 Checksum": Checksum.runFletcher16,
     "Fletcher-32 Checksum": Checksum.runFletcher32,
     "Fletcher-64 Checksum": Checksum.runFletcher64,
     "Adler-32 Checksum":    Checksum.runAdler32,
+    "CRC-16 Checksum":      Checksum.runCRC16,
     "CRC-32 Checksum":      Checksum.runCRC32,
     "TCP/IP Checksum":      Checksum.runTCPIP,
 };

+ 40 - 0
src/core/operations/BitwiseOp.js

@@ -251,6 +251,46 @@ const BitwiseOp = {
     },
 
 
+    /**
+     * Bit shift left operation.
+     *
+     * @param {byteArray} input
+     * @param {Object[]} args
+     * @returns {byteArray}
+     */
+    runBitShiftLeft: function(input, args) {
+        const amount = args[0];
+
+        return input.map(b => {
+            return (b << amount) & 0xff;
+        });
+    },
+
+
+    /**
+     * @constant
+     * @default
+     */
+    BIT_SHIFT_TYPE: ["Logical shift", "Arithmetic shift"],
+
+    /**
+     * Bit shift right operation.
+     *
+     * @param {byteArray} input
+     * @param {Object[]} args
+     * @returns {byteArray}
+     */
+    runBitShiftRight: function(input, args) {
+        const amount = args[0],
+            type = args[1],
+            mask = type === "Logical shift" ? 0 : 0x80;
+
+        return input.map(b => {
+            return (b >>> amount) ^ (b & mask);
+        });
+    },
+
+
     /**
      * XOR bitwise calculation.
      *

+ 3 - 5
src/core/operations/ByteRepr.js

@@ -196,7 +196,7 @@ const ByteRepr = {
 
 
     /**
-     * Highlight to hex
+     * Highlight from hex
      *
      * @param {Object[]} pos
      * @param {number} pos[].start
@@ -288,10 +288,8 @@ const ByteRepr = {
      * @returns {byteArray}
      */
     runFromBinary: function(input, args) {
-        if (args[0] !== "None") {
-            const delimRegex = Utils.regexRep[args[0] || "Space"];
-            input = input.replace(delimRegex, "");
-        }
+        const delimRegex = Utils.regexRep[args[0] || "Space"];
+        input = input.replace(delimRegex, "");
 
         const output = [];
         const byteLen = 8;

+ 13 - 29
src/core/operations/Checksum.js

@@ -1,3 +1,4 @@
+import * as CRC from "js-crc";
 import Utils from "../Utils.js";
 
 
@@ -119,19 +120,24 @@ const Checksum = {
     /**
      * CRC-32 Checksum operation.
      *
-     * @param {byteArray} input
+     * @param {string} input
      * @param {Object[]} args
      * @returns {string}
      */
     runCRC32: function(input, args) {
-        let crcTable = global.crcTable || (global.crcTable = Checksum._genCRCTable()),
-            crc = 0 ^ (-1);
+        return CRC.crc32(input);
+    },
 
-        for (let i = 0; i < input.length; i++) {
-            crc = (crc >>> 8) ^ crcTable[(crc ^ input[i]) & 0xff];
-        }
 
-        return Utils.hex((crc ^ (-1)) >>> 0);
+    /**
+     * CRC-16 Checksum operation.
+     *
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {string}
+     */
+    runCRC16: function(input, args) {
+        return CRC.crc16(input);
     },
 
 
@@ -168,28 +174,6 @@ const Checksum = {
         return Utils.hex(0xffff - csum);
     },
 
-
-    /**
-     * Generates a CRC table for use with CRC checksums.
-     *
-     * @private
-     * @returns {array}
-     */
-    _genCRCTable: function() {
-        let c,
-            crcTable = [];
-
-        for (let n = 0; n < 256; n++) {
-            c = n;
-            for (let k = 0; k < 8; k++) {
-                c = ((c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1));
-            }
-            crcTable[n] = c;
-        }
-
-        return crcTable;
-    },
-
 };
 
 export default Checksum;

+ 2 - 2
src/core/operations/Cipher.js

@@ -766,8 +766,8 @@ const Cipher = {
      * @returns {string}
      */
     runSubstitute: function (input, args) {
-        let plaintext = Utils.expandAlphRange(args[0]).join(),
-            ciphertext = Utils.expandAlphRange(args[1]).join(),
+        let plaintext = Utils.expandAlphRange(args[0]).join(""),
+            ciphertext = Utils.expandAlphRange(args[1]).join(""),
             output = "",
             index = -1;
 

+ 43 - 0
src/core/operations/Code.js

@@ -4,6 +4,7 @@ import Utils from "../Utils.js";
 import vkbeautify from "vkbeautify";
 import {DOMParser as dom} from "xmldom";
 import xpath from "xpath";
+import jpath from "jsonpath";
 import prettyPrintOne from "imports-loader?window=>global!exports-loader?prettyPrintOne!google-code-prettify/bin/prettify.min.js";
 
 
@@ -355,6 +356,48 @@ const Code = {
     },
 
 
+    /**
+     * @constant
+     * @default
+     */
+    JPATH_INITIAL: "",
+
+    /**
+     * @constant
+     * @default
+     */
+    JPATH_DELIMITER: "\\n",
+
+    /**
+     * JPath expression operation.
+     *
+     * @author Matt C (matt@artemisbot.uk)
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {string}
+     */
+    runJpath: function(input, args) {
+        let query = args[0],
+            delimiter = args[1],
+            results,
+            obj;
+
+        try {
+            obj = JSON.parse(input);
+        } catch (err) {
+            return "Invalid input JSON: " + err.message;
+        }
+
+        try {
+            results = jpath.query(obj, query);
+        } catch (err) {
+            return "Invalid JPath expression: " + err.message;
+        }
+
+        return results.map(result => JSON.stringify(result)).join(delimiter);
+    },
+
+
     /**
      * @constant
      * @default

+ 4 - 7
src/core/operations/Extract.js

@@ -170,9 +170,9 @@ const Extract = {
             protocol = "[A-Z]+://",
             hostname = "[-\\w]+(?:\\.\\w[-\\w]*)+",
             port = ":\\d+",
-            path = "/[^.!,?;\"'<>()\\[\\]{}\\s\\x7F-\\xFF]*";
+            path = "/[^.!,?\"<>\\[\\]{}\\s\\x7F-\\xFF]*";
 
-        path += "(?:[.!,?]+[^.!,?;\"'<>()\\[\\]{}\\s\\x7F-\\xFF]+)*";
+        path += "(?:[.!,?]+[^.!,?\"<>\\[\\]{}\\s\\x7F-\\xFF]+)*";
         const regex = new RegExp(protocol + hostname + "(?:" + port +
             ")?(?:" + path + ")?", "ig");
         return Extract._search(input, regex, null, displayTotal);
@@ -187,11 +187,8 @@ const Extract = {
      * @returns {string}
      */
     runDomains: function(input, args) {
-        let displayTotal = args[0],
-            protocol = "https?://",
-            hostname = "[-\\w\\.]+",
-            tld = "\\.(?:com|net|org|biz|info|co|uk|onion|int|mobi|name|edu|gov|mil|eu|ac|ae|af|de|ca|ch|cn|cy|es|gb|hk|il|in|io|tv|me|nl|no|nz|ro|ru|tr|us|az|ir|kz|uz|pk)+",
-            regex = new RegExp("(?:" + protocol + ")?" + hostname + tld, "ig");
+        const displayTotal = args[0],
+            regex = /\b((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63}\b/ig;
 
         return Extract._search(input, regex, null, displayTotal);
     },

+ 179 - 61
src/core/operations/Hash.js

@@ -1,6 +1,7 @@
 import Utils from "../Utils.js";
-import CryptoJS from "crypto-js";
 import CryptoApi from "crypto-api";
+import MD6 from "node-md6";
+import * as SHA3 from "js-sha3";
 import Checksum from "./Checksum.js";
 
 
@@ -23,7 +24,7 @@ const Hash = {
      * @returns {string}
      */
     runMD2: function (input, args) {
-        return Utils.toHexFast(CryptoApi.hash("md2", input, {}));
+        return CryptoApi.hash("md2", input, {}).stringify("hex");
     },
 
 
@@ -35,7 +36,7 @@ const Hash = {
      * @returns {string}
      */
     runMD4: function (input, args) {
-        return Utils.toHexFast(CryptoApi.hash("md4", input, {}));
+        return CryptoApi.hash("md4", input, {}).stringify("hex");
     },
 
 
@@ -47,85 +48,158 @@ const Hash = {
      * @returns {string}
      */
     runMD5: function (input, args) {
-        input = CryptoJS.enc.Latin1.parse(input); // Cast to WordArray
-        return CryptoJS.MD5(input).toString(CryptoJS.enc.Hex);
+        return CryptoApi.hash("md5", input, {}).stringify("hex");
     },
 
 
     /**
-     * SHA0 operation.
+     * @constant
+     * @default
+     */
+    MD6_SIZE: 256,
+    /**
+     * @constant
+     * @default
+     */
+    MD6_LEVELS: 64,
+
+    /**
+     * MD6 operation.
      *
      * @param {string} input
      * @param {Object[]} args
      * @returns {string}
      */
-    runSHA0: function (input, args) {
-        return Utils.toHexFast(CryptoApi.hash("sha0", input, {}));
+    runMD6: function (input, args) {
+        const size = args[0],
+            levels = args[1],
+            key = args[2];
+
+        if (size < 0 || size > 512)
+            return "Size must be between 0 and 512";
+        if (levels < 0)
+            return "Levels must be greater than 0";
+
+        return MD6.getHashOfText(input, size, key, levels);
     },
 
 
     /**
-     * SHA1 operation.
+     * SHA0 operation.
      *
      * @param {string} input
      * @param {Object[]} args
      * @returns {string}
      */
-    runSHA1: function (input, args) {
-        input = CryptoJS.enc.Latin1.parse(input);
-        return CryptoJS.SHA1(input).toString(CryptoJS.enc.Hex);
+    runSHA0: function (input, args) {
+        return CryptoApi.hash("sha0", input, {}).stringify("hex");
     },
 
 
     /**
-     * SHA224 operation.
+     * SHA1 operation.
      *
      * @param {string} input
      * @param {Object[]} args
      * @returns {string}
      */
-    runSHA224: function (input, args) {
-        input = CryptoJS.enc.Latin1.parse(input);
-        return CryptoJS.SHA224(input).toString(CryptoJS.enc.Hex);
+    runSHA1: function (input, args) {
+        return CryptoApi.hash("sha1", input, {}).stringify("hex");
     },
 
 
     /**
-     * SHA256 operation.
+     * @constant
+     * @default
+     */
+    SHA2_SIZE: ["512", "256", "384", "224", "512/256", "512/224"],
+
+    /**
+     * SHA2 operation.
      *
      * @param {string} input
      * @param {Object[]} args
      * @returns {string}
      */
-    runSHA256: function (input, args) {
-        input = CryptoJS.enc.Latin1.parse(input);
-        return CryptoJS.SHA256(input).toString(CryptoJS.enc.Hex);
+    runSHA2: function (input, args) {
+        const size = args[0];
+        return CryptoApi.hash("sha" + size, input, {}).stringify("hex");
     },
 
 
     /**
-     * SHA384 operation.
+     * @constant
+     * @default
+     */
+    SHA3_SIZE: ["512", "384", "256", "224"],
+
+    /**
+     * SHA3 operation.
      *
      * @param {string} input
      * @param {Object[]} args
      * @returns {string}
      */
-    runSHA384: function (input, args) {
-        input = CryptoJS.enc.Latin1.parse(input);
-        return CryptoJS.SHA384(input).toString(CryptoJS.enc.Hex);
+    runSHA3: function (input, args) {
+        const size = parseInt(args[0], 10);
+        let algo;
+
+        switch (size) {
+            case 224:
+                algo = SHA3.sha3_224;
+                break;
+            case 384:
+                algo = SHA3.sha3_384;
+                break;
+            case 256:
+                algo = SHA3.sha3_256;
+                break;
+            case 512:
+                algo = SHA3.sha3_512;
+                break;
+            default:
+                return "Invalid size";
+        }
+
+        return algo(input);
     },
 
 
     /**
-     * SHA512 operation.
+     * @constant
+     * @default
+     */
+    KECCAK_SIZE: ["512", "384", "256", "224"],
+
+    /**
+     * Keccak operation.
      *
      * @param {string} input
      * @param {Object[]} args
      * @returns {string}
      */
-    runSHA512: function (input, args) {
-        input = CryptoJS.enc.Latin1.parse(input);
-        return CryptoJS.SHA512(input).toString(CryptoJS.enc.Hex);
+    runKeccak: function (input, args) {
+        const size = parseInt(args[0], 10);
+        let algo;
+
+        switch (size) {
+            case 224:
+                algo = SHA3.keccak224;
+                break;
+            case 384:
+                algo = SHA3.keccak384;
+                break;
+            case 256:
+                algo = SHA3.keccak256;
+                break;
+            case 512:
+                algo = SHA3.keccak512;
+                break;
+            default:
+                return "Invalid size";
+        }
+
+        return algo(input);
     },
 
 
@@ -133,35 +207,59 @@ const Hash = {
      * @constant
      * @default
      */
-    SHA3_LENGTH: ["512", "384", "256", "224"],
+    SHAKE_CAPACITY: ["256", "128"],
+    /**
+     * @constant
+     * @default
+     */
+    SHAKE_SIZE: 512,
 
     /**
-     * SHA3 operation.
+     * Shake operation.
      *
      * @param {string} input
      * @param {Object[]} args
      * @returns {string}
      */
-    runSHA3: function (input, args) {
-        input = CryptoJS.enc.Latin1.parse(input);
-        let sha3Length = args[0],
-            options = {
-                outputLength: parseInt(sha3Length, 10)
-            };
-        return CryptoJS.SHA3(input, options).toString(CryptoJS.enc.Hex);
+    runShake: function (input, args) {
+        const capacity = parseInt(args[0], 10),
+            size = args[1];
+        let algo;
+
+        if (size < 0)
+            return "Size must be greater than 0";
+
+        switch (capacity) {
+            case 128:
+                algo = SHA3.shake128;
+                break;
+            case 256:
+                algo = SHA3.shake256;
+                break;
+            default:
+                return "Invalid size";
+        }
+
+        return algo(input, size);
     },
 
 
     /**
-     * RIPEMD-160 operation.
+     * @constant
+     * @default
+     */
+    RIPEMD_SIZE: ["320", "256", "160", "128"],
+
+    /**
+     * RIPEMD operation.
      *
      * @param {string} input
      * @param {Object[]} args
      * @returns {string}
      */
-    runRIPEMD160: function (input, args) {
-        input = CryptoJS.enc.Latin1.parse(input);
-        return CryptoJS.RIPEMD160(input).toString(CryptoJS.enc.Hex);
+    runRIPEMD: function (input, args) {
+        const size = args[0];
+        return CryptoApi.hash("ripemd" + size, input, {}).stringify("hex");
     },
 
 
@@ -169,7 +267,23 @@ const Hash = {
      * @constant
      * @default
      */
-    HMAC_FUNCTIONS: ["MD5", "SHA1", "SHA224", "SHA256", "SHA384", "SHA512", "SHA3", "RIPEMD-160"],
+    HMAC_FUNCTIONS: [
+        "MD2",
+        "MD4",
+        "MD5",
+        "SHA0",
+        "SHA1",
+        "SHA224",
+        "SHA256",
+        "SHA384",
+        "SHA512",
+        "SHA512/224",
+        "SHA512/256",
+        "RIPEMD128",
+        "RIPEMD160",
+        "RIPEMD256",
+        "RIPEMD320",
+    ],
 
     /**
      * HMAC operation.
@@ -179,19 +293,12 @@ const Hash = {
      * @returns {string}
      */
     runHMAC: function (input, args) {
-        const hashFunc = args[1];
-        input = CryptoJS.enc.Latin1.parse(input);
-        const execute = {
-            "MD5": CryptoJS.HmacMD5(input, args[0]),
-            "SHA1": CryptoJS.HmacSHA1(input, args[0]),
-            "SHA224": CryptoJS.HmacSHA224(input, args[0]),
-            "SHA256": CryptoJS.HmacSHA256(input, args[0]),
-            "SHA384": CryptoJS.HmacSHA384(input, args[0]),
-            "SHA512": CryptoJS.HmacSHA512(input, args[0]),
-            "SHA3": CryptoJS.HmacSHA3(input, args[0]),
-            "RIPEMD-160": CryptoJS.HmacRIPEMD160(input, args[0]),
-        };
-        return execute[hashFunc].toString(CryptoJS.enc.Hex);
+        const password = args[0],
+            hashFunc = args[1].toLowerCase(),
+            hmac = CryptoApi.mac("hmac", password, hashFunc, {});
+
+        hmac.update(input);
+        return hmac.finalize().stringify("hex");
     },
 
 
@@ -207,24 +314,35 @@ const Hash = {
             output = "MD2:         " + Hash.runMD2(input, []) +
                 "\nMD4:         " + Hash.runMD4(input, []) +
                 "\nMD5:         " + Hash.runMD5(input, []) +
+                "\nMD6:         " + Hash.runMD6(input, []) +
                 "\nSHA0:        " + Hash.runSHA0(input, []) +
                 "\nSHA1:        " + Hash.runSHA1(input, []) +
-                "\nSHA2 224:    " + Hash.runSHA224(input, []) +
-                "\nSHA2 256:    " + Hash.runSHA256(input, []) +
-                "\nSHA2 384:    " + Hash.runSHA384(input, []) +
-                "\nSHA2 512:    " + Hash.runSHA512(input, []) +
+                "\nSHA2 224:    " + Hash.runSHA2(input, ["224"]) +
+                "\nSHA2 256:    " + Hash.runSHA2(input, ["256"]) +
+                "\nSHA2 384:    " + Hash.runSHA2(input, ["384"]) +
+                "\nSHA2 512:    " + Hash.runSHA2(input, ["512"]) +
                 "\nSHA3 224:    " + Hash.runSHA3(input, ["224"]) +
                 "\nSHA3 256:    " + Hash.runSHA3(input, ["256"]) +
                 "\nSHA3 384:    " + Hash.runSHA3(input, ["384"]) +
                 "\nSHA3 512:    " + Hash.runSHA3(input, ["512"]) +
-                "\nRIPEMD-160:  " + Hash.runRIPEMD160(input, []) +
+                "\nKeccak 224:  " + Hash.runKeccak(input, ["224"]) +
+                "\nKeccak 256:  " + Hash.runKeccak(input, ["256"]) +
+                "\nKeccak 384:  " + Hash.runKeccak(input, ["384"]) +
+                "\nKeccak 512:  " + Hash.runKeccak(input, ["512"]) +
+                "\nShake 128:   " + Hash.runShake(input, ["128", 256]) +
+                "\nShake 256:   " + Hash.runShake(input, ["256", 512]) +
+                "\nRIPEMD-128:  " + Hash.runRIPEMD(input, ["128"]) +
+                "\nRIPEMD-160:  " + Hash.runRIPEMD(input, ["160"]) +
+                "\nRIPEMD-256:  " + Hash.runRIPEMD(input, ["256"]) +
+                "\nRIPEMD-320:  " + Hash.runRIPEMD(input, ["320"]) +
                 "\n\nChecksums:" +
                 "\nFletcher-8:  " + Checksum.runFletcher8(byteArray, []) +
                 "\nFletcher-16: " + Checksum.runFletcher16(byteArray, []) +
                 "\nFletcher-32: " + Checksum.runFletcher32(byteArray, []) +
                 "\nFletcher-64: " + Checksum.runFletcher64(byteArray, []) +
                 "\nAdler-32:    " + Checksum.runAdler32(byteArray, []) +
-                "\nCRC-32:      " + Checksum.runCRC32(byteArray, []);
+                "\nCRC-16:      " + Checksum.runCRC16(input, []) +
+                "\nCRC-32:      " + Checksum.runCRC32(input, []);
 
         return output;
     },

+ 213 - 0
src/core/operations/MS.js

@@ -0,0 +1,213 @@
+/**
+ * Microsoft operations. 
+ *
+ * @author bmwhitn [brian.m.whitney@outlook.com]
+ * @copyright Crown Copyright 2017
+ * @license Apache-2.0
+ *
+ * @namespace
+ */
+const MS = {
+
+    /**
+     * @constant
+     * @default
+     */
+    D_DECODE: [
+        "",
+        "",
+        "",
+        "",
+        "",
+        "",
+        "",
+        "",
+        "",
+        "\x57\x6E\x7B",
+        "\x4A\x4C\x41",
+        "\x0B\x0B\x0B",
+        "\x0C\x0C\x0C",
+        "\x4A\x4C\x41",
+        "\x0E\x0E\x0E",
+        "\x0F\x0F\x0F",
+        "\x10\x10\x10",
+        "\x11\x11\x11",
+        "\x12\x12\x12",
+        "\x13\x13\x13",
+        "\x14\x14\x14",
+        "\x15\x15\x15",
+        "\x16\x16\x16",
+        "\x17\x17\x17",
+        "\x18\x18\x18",
+        "\x19\x19\x19",
+        "\x1A\x1A\x1A",
+        "\x1B\x1B\x1B",
+        "\x1C\x1C\x1C",
+        "\x1D\x1D\x1D",
+        "\x1E\x1E\x1E",
+        "\x1F\x1F\x1F",
+        "\x2E\x2D\x32",
+        "\x47\x75\x30",
+        "\x7A\x52\x21",
+        "\x56\x60\x29",
+        "\x42\x71\x5B",
+        "\x6A\x5E\x38",
+        "\x2F\x49\x33",
+        "\x26\x5C\x3D",
+        "\x49\x62\x58",
+        "\x41\x7D\x3A",
+        "\x34\x29\x35",
+        "\x32\x36\x65",
+        "\x5B\x20\x39",
+        "\x76\x7C\x5C",
+        "\x72\x7A\x56",
+        "\x43\x7F\x73",
+        "\x38\x6B\x66",
+        "\x39\x63\x4E",
+        "\x70\x33\x45",
+        "\x45\x2B\x6B",
+        "\x68\x68\x62",
+        "\x71\x51\x59",
+        "\x4F\x66\x78",
+        "\x09\x76\x5E",
+        "\x62\x31\x7D",
+        "\x44\x64\x4A",
+        "\x23\x54\x6D",
+        "\x75\x43\x71",
+        "\x4A\x4C\x41",
+        "\x7E\x3A\x60",
+        "\x4A\x4C\x41",
+        "\x5E\x7E\x53",
+        "\x40\x4C\x40",
+        "\x77\x45\x42",
+        "\x4A\x2C\x27",
+        "\x61\x2A\x48",
+        "\x5D\x74\x72",
+        "\x22\x27\x75",
+        "\x4B\x37\x31",
+        "\x6F\x44\x37",
+        "\x4E\x79\x4D",
+        "\x3B\x59\x52",
+        "\x4C\x2F\x22",
+        "\x50\x6F\x54",
+        "\x67\x26\x6A",
+        "\x2A\x72\x47",
+        "\x7D\x6A\x64",
+        "\x74\x39\x2D",
+        "\x54\x7B\x20",
+        "\x2B\x3F\x7F",
+        "\x2D\x38\x2E",
+        "\x2C\x77\x4C",
+        "\x30\x67\x5D",
+        "\x6E\x53\x7E",
+        "\x6B\x47\x6C",
+        "\x66\x34\x6F",
+        "\x35\x78\x79",
+        "\x25\x5D\x74",
+        "\x21\x30\x43",
+        "\x64\x23\x26",
+        "\x4D\x5A\x76",
+        "\x52\x5B\x25",
+        "\x63\x6C\x24",
+        "\x3F\x48\x2B",
+        "\x7B\x55\x28",
+        "\x78\x70\x23",
+        "\x29\x69\x41",
+        "\x28\x2E\x34",
+        "\x73\x4C\x09",
+        "\x59\x21\x2A",
+        "\x33\x24\x44",
+        "\x7F\x4E\x3F",
+        "\x6D\x50\x77",
+        "\x55\x09\x3B",
+        "\x53\x56\x55",
+        "\x7C\x73\x69",
+        "\x3A\x35\x61",
+        "\x5F\x61\x63",
+        "\x65\x4B\x50",
+        "\x46\x58\x67",
+        "\x58\x3B\x51",
+        "\x31\x57\x49",
+        "\x69\x22\x4F",
+        "\x6C\x6D\x46",
+        "\x5A\x4D\x68",
+        "\x48\x25\x7C",
+        "\x27\x28\x36",
+        "\x5C\x46\x70",
+        "\x3D\x4A\x6E",
+        "\x24\x32\x7A",
+        "\x79\x41\x2F",
+        "\x37\x3D\x5F",
+        "\x60\x5F\x4B",
+        "\x51\x4F\x5A",
+        "\x20\x42\x2C",
+        "\x36\x65\x57"
+    ],
+
+    /**
+     * @constant
+     * @default
+     */
+    D_COMBINATION: [
+        0, 1, 2, 0, 1, 2, 1, 2, 2, 1, 2, 1, 0, 2, 1, 2, 0, 2, 1, 2, 0, 0, 1, 2, 2, 1, 0, 2, 1, 2, 2, 1,
+        0, 0, 2, 1, 2, 1, 2, 0, 2, 0, 0, 1, 2, 0, 2, 1, 0, 2, 1, 2, 0, 0, 1, 2, 2, 0, 0, 1, 2, 0, 2, 1
+    ],
+
+
+    /**
+     * Decodes Microsoft Encoded Script files that can be read and executed by cscript.exe/wscript.exe.
+     * This is a conversion of a Python script that was originally created by Didier Stevens
+     * (https://DidierStevens.com).
+     *
+     * @private
+     * @param {string} data
+     * @returns {string}
+     */
+    _decode: function (data) {
+        let result = [];
+        let index = -1;
+        data = data.replace(/@&/g, String.fromCharCode(10))
+            .replace(/@#/g, String.fromCharCode(13))
+            .replace(/@\*/g, ">")
+            .replace(/@!/g, "<")
+            .replace(/@\$/g, "@");
+
+        for (let i = 0; i < data.length; i++) {
+            let byte = data.charCodeAt(i);
+            let char = data.charAt(i);
+            if (byte < 128) {
+                index++;
+            }
+
+            if ((byte === 9 || byte > 31 && byte < 128) &&
+                byte !== 60 &&
+                byte !== 62 &&
+                byte !== 64) {
+                char = MS.D_DECODE[byte].charAt(MS.D_COMBINATION[index % 64]);
+            }
+            result.push(char);
+        }
+        return result.join("");
+    },
+
+
+    /**
+     * Microsoft Script Decoder operation.
+     *
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {string}
+     */
+    runDecodeScript: function (input, args) {
+        let matcher = /#@~\^.{6}==(.+).{6}==\^#~@/;
+        let encodedData = matcher.exec(input);
+        if (encodedData){
+            return MS._decode(encodedData[1]);
+        } else {
+            return "";
+        }
+    }
+
+};
+
+export default MS;

+ 55 - 0
src/core/operations/OTP.js

@@ -0,0 +1,55 @@
+import otp from "otp";
+import Base64 from "./Base64.js";
+
+/**
+ * One-Time Password operations.
+ *
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2017
+ * @license Apache-2.0
+ *
+ * @namespace
+ */
+const OTP = {
+
+    /**
+     * Generate TOTP operation.
+     *
+     * @param {byteArray} input
+     * @param {Object[]} args
+     * @returns {string}
+     */
+    runTOTP: function(input, args) {
+        const otpObj = otp({
+            name: args[0],
+            keySize: args[1],
+            codeLength: args[2],
+            secret: Base64.runTo32(input, []),
+            epoch: args[3],
+            timeSlice: args[4]
+        });
+        return `URI: ${otpObj.totpURL}\n\nPassword: ${otpObj.totp()}`;
+    },
+
+
+    /**
+     * Generate HOTP operation.
+     *
+     * @param {byteArray} input
+     * @param {Object[]} args
+     * @returns {string}
+     */
+    runHOTP: function(input, args) {
+        const otpObj = otp({
+            name: args[0],
+            keySize: args[1],
+            codeLength: args[2],
+            secret: Base64.runTo32(input, []),
+        });
+        const counter = args[3];
+        return `URI: ${otpObj.hotpURL}\n\nPassword: ${otpObj.hotp(counter)}`;
+    },
+
+};
+
+export default OTP;

+ 0 - 2
src/core/operations/PublicKey.js

@@ -61,8 +61,6 @@ const PublicKey = {
             sig = cert.getSignatureValueHex(),
             sigStr = "",
             extensions = cert.getInfo().split("X509v3 Extensions:\n")[1].split("signature")[0];
-        window.cert = cert;
-        window.r = r;
 
         // Public Key fields
         pkFields.push({

+ 5 - 5
src/core/operations/Rotate.js

@@ -20,7 +20,7 @@ const Rotate = {
      * @constant
      * @default
      */
-    ROTATE_WHOLE: false,
+    ROTATE_CARRY: false,
 
     /**
      * Runs rotation operations across the input data.
@@ -53,7 +53,7 @@ const Rotate = {
      */
     runRotr: function(input, args) {
         if (args[1]) {
-            return Rotate._rotrWhole(input, args[0]);
+            return Rotate._rotrCarry(input, args[0]);
         } else {
             return Rotate._rot(input, args[0], Rotate._rotr);
         }
@@ -69,7 +69,7 @@ const Rotate = {
      */
     runRotl: function(input, args) {
         if (args[1]) {
-            return Rotate._rotlWhole(input, args[0]);
+            return Rotate._rotlCarry(input, args[0]);
         } else {
             return Rotate._rot(input, args[0], Rotate._rotl);
         }
@@ -197,7 +197,7 @@ const Rotate = {
      * @param {number} amount
      * @returns {byteArray}
      */
-    _rotrWhole: function(data, amount) {
+    _rotrCarry: function(data, amount) {
         let carryBits = 0,
             newByte,
             result = [];
@@ -223,7 +223,7 @@ const Rotate = {
      * @param {number} amount
      * @returns {byteArray}
      */
-    _rotlWhole: function(data, amount) {
+    _rotlCarry: function(data, amount) {
         let carryBits = 0,
             newByte,
             result = [];

+ 75 - 5
src/core/operations/StrUtils.js

@@ -35,11 +35,11 @@ const StrUtils = {
         },
         {
             name: "URL",
-            value: "([A-Za-z]+://)([-\\w]+(?:\\.\\w[-\\w]*)+)(:\\d+)?(/[^.!,?;\"\\x27<>()\\[\\]{}\\s\\x7F-\\xFF]*(?:[.!,?]+[^.!,?;\"\\x27<>()\\[\\]{}\\s\\x7F-\\xFF]+)*)?"
+            value: "([A-Za-z]+://)([-\\w]+(?:\\.\\w[-\\w]*)+)(:\\d+)?(/[^.!,?\"<>\\[\\]{}\\s\\x7F-\\xFF]*(?:[.!,?]+[^.!,?\"<>\\[\\]{}\\s\\x7F-\\xFF]+)*)?"
         },
         {
             name: "Domain",
-            value: "(?:(https?):\\/\\/)?([-\\w.]+)\\.(com|net|org|biz|info|co|uk|onion|int|mobi|name|edu|gov|mil|eu|ac|ae|af|de|ca|ch|cn|cy|es|gb|hk|il|in|io|tv|me|nl|no|nz|ro|ru|tr|us|az|ir|kz|uz|pk)+"
+            value: "\\b((?=[a-z0-9-]{1,63}\\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\\.)+[a-z]{2,63}\\b"
         },
         {
             name: "Windows file path",
@@ -372,14 +372,84 @@ const StrUtils = {
 
 
     /**
-     * Parse escaped string operation.
+     * @constant
+     * @default
+     */
+    ESCAPE_REPLACEMENTS: [
+        {"escaped": "\\\\", "unescaped": "\\"}, // Must be first
+        {"escaped": "\\'", "unescaped": "'"},
+        {"escaped": "\\\"", "unescaped": "\""},
+        {"escaped": "\\n", "unescaped": "\n"},
+        {"escaped": "\\r", "unescaped": "\r"},
+        {"escaped": "\\t", "unescaped": "\t"},
+        {"escaped": "\\b", "unescaped": "\b"},
+        {"escaped": "\\f", "unescaped": "\f"},
+    ],
+
+    /**
+     * Escape string operation.
+     *
+     * @author Vel0x [dalemy@microsoft.com]
      *
      * @param {string} input
      * @param {Object[]} args
      * @returns {string}
+     *
+     * @example
+     * StrUtils.runUnescape("Don't do that", [])
+     * > "Don\'t do that"
+     * StrUtils.runUnescape(`Hello
+     * World`, [])
+     * > "Hello\nWorld"
      */
-    runParseEscapedString: function(input, args) {
-        return Utils.parseEscapedChars(input);
+    runEscape: function(input, args) {
+        return StrUtils._replaceByKeys(input, "unescaped", "escaped");
+    },
+
+
+    /**
+     * Unescape string operation.
+     *
+     * @author Vel0x [dalemy@microsoft.com]
+     *
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {string}
+     *
+     * @example
+     * StrUtils.runUnescape("Don\'t do that", [])
+     * > "Don't do that"
+     * StrUtils.runUnescape("Hello\nWorld", [])
+     * > `Hello
+     * World`
+     */
+    runUnescape: function(input, args) {
+        return StrUtils._replaceByKeys(input, "escaped", "unescaped");
+    },
+
+
+    /**
+     * Replaces all matching tokens in ESCAPE_REPLACEMENTS with the correction. The
+     * ordering is determined by the patternKey and the replacementKey.
+     *
+     * @author Vel0x [dalemy@microsoft.com]
+     * @author Matt C [matt@artemisbot.uk]
+     *
+     * @param {string} input
+     * @param {string} pattern_key
+     * @param {string} replacement_key
+     * @returns {string}
+     */
+    _replaceByKeys: function(input, patternKey, replacementKey) {
+        let output = input;
+
+        // Catch the \\x encoded characters
+        if (patternKey === "escaped") output = Utils.parseEscapedChars(input);
+
+        StrUtils.ESCAPE_REPLACEMENTS.forEach(replacement => {
+            output = output.split(replacement[patternKey]).join(replacement[replacementKey]);
+        });
+        return output;
     },
 
 

+ 14 - 2
src/web/App.js

@@ -76,6 +76,8 @@ App.prototype.registerChefWorker = function() {
 
 /**
  * Fires once all setup activities have completed.
+ *
+ * @fires Manager#apploaded
  */
 App.prototype.loaded = function() {
     // Check that both the app and the worker have loaded successfully, and that
@@ -95,6 +97,8 @@ App.prototype.loaded = function() {
 
     // Clear the loading message interval
     clearInterval(window.loadingMsgsInt);
+
+    document.dispatchEvent(this.manager.apploaded);
 };
 
 
@@ -459,7 +463,7 @@ App.prototype.loadURIParams = function() {
     // Read in recipe from URI params
     if (this.uriParams.recipe) {
         try {
-            const recipeConfig = JSON.parse(this.uriParams.recipe);
+            const recipeConfig = Utils.parseRecipeConfig(this.uriParams.recipe);
             this.setRecipeConfig(recipeConfig);
         } catch (err) {}
     } else if (this.uriParams.op) {
@@ -530,6 +534,7 @@ App.prototype.setRecipeConfig = function(recipeConfig) {
         // Populate arguments
         const args = item.querySelectorAll(".arg");
         for (let j = 0; j < args.length; j++) {
+            if (recipeConfig[i].args[j] === undefined) continue;
             if (args[j].getAttribute("type") === "checkbox") {
                 // checkbox
                 args[j].checked = recipeConfig[i].args[j];
@@ -724,7 +729,14 @@ App.prototype.stateChange = function(e) {
     if (recipeConfig.length === 1) {
         title = `${recipeConfig[0].op} - ${title}`;
     } else if (recipeConfig.length > 1) {
-        title = `${recipeConfig.length} operations - ${title}`;
+        // See how long the full recipe is
+        const ops = recipeConfig.map(op => op.op).join(", ");
+        if (ops.length < 45) {
+            title = `${ops} - ${title}`;
+        } else {
+            // If it's too long, just use the first one and say how many more there are
+            title = `${recipeConfig[0].op}, ${recipeConfig.length - 1} more - ${title}`;
+        }
     }
     document.title = title;
 

+ 15 - 8
src/web/ControlsWaiter.js

@@ -168,7 +168,7 @@ ControlsWaiter.prototype.generateStateUrl = function(includeRecipe, includeInput
     const link = baseURL || window.location.protocol + "//" +
         window.location.host +
         window.location.pathname;
-    const recipeStr = JSON.stringify(recipeConfig);
+    const recipeStr = Utils.generatePrettyRecipe(recipeConfig);
     const inputStr = Utils.toBase64(this.app.getInput(), "A-Za-z0-9+/"); // B64 alphabet with no padding
 
     includeRecipe = includeRecipe && (recipeConfig.length > 0);
@@ -182,7 +182,7 @@ ControlsWaiter.prototype.generateStateUrl = function(includeRecipe, includeInput
 
     const hash = params
         .filter(v => v)
-        .map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
+        .map(([key, value]) => `${key}=${Utils.encodeURIFragment(value)}`)
         .join("&");
 
     if (hash) {
@@ -196,9 +196,9 @@ ControlsWaiter.prototype.generateStateUrl = function(includeRecipe, includeInput
 /**
  * Handler for changes made to the save dialog text area. Re-initialises the save link.
  */
-ControlsWaiter.prototype.saveTextChange = function() {
+ControlsWaiter.prototype.saveTextChange = function(e) {
     try {
-        const recipeConfig = JSON.parse(document.getElementById("save-text").value);
+        const recipeConfig = Utils.parseRecipeConfig(e.target.value);
         this.initialiseSaveLink(recipeConfig);
     } catch (err) {}
 };
@@ -209,9 +209,16 @@ ControlsWaiter.prototype.saveTextChange = function() {
  */
 ControlsWaiter.prototype.saveClick = function() {
     const recipeConfig = this.app.getRecipeConfig();
-    const recipeStr = JSON.stringify(recipeConfig).replace(/},{/g, "},\n{");
+    const recipeStr = JSON.stringify(recipeConfig);
 
-    document.getElementById("save-text").value = recipeStr;
+    document.getElementById("save-text-chef").value = Utils.generatePrettyRecipe(recipeConfig, true);
+    document.getElementById("save-text-clean").value = JSON.stringify(recipeConfig, null, 2)
+        .replace(/{\n\s+"/g, "{ \"")
+        .replace(/\[\n\s{3,}/g, "[")
+        .replace(/\n\s{3,}]/g, "]")
+        .replace(/\s*\n\s*}/g, " }")
+        .replace(/\n\s{6,}/g, " ");
+    document.getElementById("save-text-compact").value = recipeStr;
 
     this.initialiseSaveLink(recipeConfig);
     $("#save-modal").modal();
@@ -248,7 +255,7 @@ ControlsWaiter.prototype.loadClick = function() {
  */
 ControlsWaiter.prototype.saveButtonClick = function() {
     const recipeName = Utils.escapeHtml(document.getElementById("save-name").value);
-    const recipeStr  = document.getElementById("save-text").value;
+    const recipeStr  = document.querySelector("#save-texts .tab-pane.active textarea").value;
 
     if (!recipeName) {
         this.app.alert("Please enter a recipe name", "danger", 2000);
@@ -337,7 +344,7 @@ ControlsWaiter.prototype.loadNameChange = function(e) {
  */
 ControlsWaiter.prototype.loadButtonClick = function() {
     try {
-        const recipeConfig = JSON.parse(document.getElementById("load-text").value);
+        const recipeConfig = Utils.parseRecipeConfig(document.getElementById("load-text").value);
         this.app.setRecipeConfig(recipeConfig);
 
         $("#rec-list [data-toggle=popover]").popover();

+ 6 - 1
src/web/Manager.js

@@ -27,6 +27,10 @@ const Manager = function(app) {
      * @event Manager#appstart
      */
     this.appstart = new CustomEvent("appstart", {bubbles: true});
+    /**
+     * @event Manager#apploaded
+     */
+    this.apploaded = new CustomEvent("apploaded", {bubbles: true});
     /**
      * @event Manager#operationadd
      */
@@ -98,7 +102,7 @@ Manager.prototype.initialiseEventListeners = function() {
     document.getElementById("load-name").addEventListener("change", this.controls.loadNameChange.bind(this.controls));
     document.getElementById("load-button").addEventListener("click", this.controls.loadButtonClick.bind(this.controls));
     document.getElementById("support").addEventListener("click", this.controls.supportButtonClick.bind(this.controls));
-    this.addMultiEventListener("#save-text", "keyup paste", this.controls.saveTextChange, this.controls);
+    this.addMultiEventListeners("#save-texts textarea", "keyup paste", this.controls.saveTextChange, this.controls);
 
     // Operations
     this.addMultiEventListener("#search", "keyup paste search", this.ops.searchOperations, this.ops);
@@ -114,6 +118,7 @@ Manager.prototype.initialiseEventListeners = function() {
     // Recipe
     this.addDynamicListener(".arg", "keyup", this.recipe.ingChange, this.recipe);
     this.addDynamicListener(".arg", "change", this.recipe.ingChange, this.recipe);
+    this.addDynamicListener(".arg", "input", this.recipe.ingChange, this.recipe);
     this.addDynamicListener(".disable-icon", "click", this.recipe.disableClick, this.recipe);
     this.addDynamicListener(".breakpoint", "click", this.recipe.breakpointClick, this.recipe);
     this.addDynamicListener("#rec-list li.operation", "dblclick", this.recipe.operationDblclick, this.recipe);

+ 29 - 1
src/web/OperationsWaiter.js

@@ -155,7 +155,35 @@ OperationsWaiter.prototype.getSelectedOp = function(ops) {
  */
 OperationsWaiter.prototype.opListCreate = function(e) {
     this.manager.recipe.createSortableSeedList(e.target);
-    $("[data-toggle=popover]").popover();
+    this.enableOpsListPopovers(e.target);
+};
+
+
+/**
+ * Sets up popovers, allowing the popover itself to gain focus which enables scrolling
+ * and other interactions.
+ *
+ * @param {Element} el - The element to start selecting from
+ */
+OperationsWaiter.prototype.enableOpsListPopovers = function(el) {
+    $(el).find("[data-toggle=popover]").addBack("[data-toggle=popover]")
+        .popover({trigger: "manual"})
+        .on("mouseenter", function() {
+            const _this = this;
+            $(this).popover("show");
+            $(".popover").on("mouseleave", function () {
+                $(_this).popover("hide");
+            });
+        }).on("mouseleave", function () {
+            const _this = this;
+            setTimeout(function() {
+                // Determine if the popover associated with this element is being hovered over
+                if ($(_this).data("bs.popover") &&
+                    !$(_this).data("bs.popover").$tip.is(":hover")) {
+                    $(_this).popover("hide");
+                }
+            }, 50);
+        });
 };
 
 

+ 5 - 3
src/web/RecipeWaiter.js

@@ -93,7 +93,7 @@ RecipeWaiter.prototype.createSortableSeedList = function(listEl) {
             // Removes popover element and event bindings from the dragged operation but not the
             // event bindings from the one left in the operations list. Without manually removing
             // these bindings, we cannot re-initialise the popover on the stub operation.
-            $(evt.item).popover("destroy");
+            $(evt.item).popover("destroy").removeData("bs.popover").off("mouseenter").off("mouseleave");
             $(evt.clone).off(".popover").removeData("bs.popover");
             evt.item.setAttribute("data-toggle", "popover-disabled");
         },
@@ -120,8 +120,7 @@ RecipeWaiter.prototype.opSortEnd = function(evt) {
 
     // Reinitialise the popover on the original element in the ops list because for some reason it
     // gets destroyed and recreated.
-    $(evt.clone).popover();
-    $(evt.clone).children("[data-toggle=popover]").popover();
+    this.manager.ops.enableOpsListPopovers(evt.clone);
 
     if (evt.item.parentNode.id !== "rec-list") {
         return;
@@ -296,6 +295,9 @@ RecipeWaiter.prototype.getConfig = function() {
                     option: ingList[j].previousSibling.children[0].textContent.slice(0, -1),
                     string: ingList[j].value
                 };
+            } else if (ingList[j].getAttribute("type") === "number") {
+                // number
+                ingredients[j] = parseFloat(ingList[j].value, 10);
             } else {
                 // all others
                 ingredients[j] = ingList[j].value;

+ 34 - 13
src/web/html/index.html

@@ -46,10 +46,15 @@
                 "Initialising Skynet...",
                 "[REDACTED]",
                 "Downloading more RAM...",
-                "Loading more loading messages...",
                 "Ordering 1s and 0s...",
                 "Navigating neural network...",
-                "Importing machine learning..."
+                "Importing machine learning...",
+                "Issuing Alice and Bob one-time pads...",
+                "Mining bitcoin cash...",
+                "Generating key material by trying to escape vim...",
+                "for i in range(additional): Pylon()",
+                "(creating unresolved tension...",
+                "Symlinking emacs and vim to ed...",
             ];
 
             // Shuffle array using Durstenfeld algorithm
@@ -60,20 +65,20 @@
                 loadingMsgs[j] = temp;
             }
 
-            // Show next loading message then move it to the end of the array
+            // Show next loading message and move it to the end of the array
             function changeLoadingMsg() {
                 const msg = loadingMsgs.shift();
+                loadingMsgs.push(msg);
                 try {
                     const el = document.getElementById("preloader-msg");
                     if (!el.classList.contains("loading"))
                         el.classList.add("loading"); // Causes CSS transition on first message
                     el.innerHTML = msg;
                 } catch (err) {} // Ignore errors if DOM not yet ready
-                loadingMsgs.push(msg);
             }
 
             changeLoadingMsg();
-            window.loadingMsgsInt = setInterval(changeLoadingMsg, (Math.random() * 1000) + 1000);
+            window.loadingMsgsInt = setInterval(changeLoadingMsg, (Math.random() * 2000) + 1500);
         </script>
         <% if (!htmlWebpackPlugin.options.inline) { %>
             <script type="application/ld+json">
@@ -212,7 +217,22 @@
                     <div class="modal-body">
                         <div class="form-group">
                             <label for="save-text">Save your recipe to local storage or copy the following string to load later</label>
-                            <textarea class="form-control" id="save-text" rows="5"></textarea>
+                            <ul class="nav nav-tabs" role="tablist">
+                                <li role="presentation" class="active"><a href="#chef-format" role="tab" data-toggle="tab">Chef format</a></li>
+                                <li role="presentation"><a href="#clean-json" role="tab" data-toggle="tab">Clean JSON</a></li>
+                                <li role="presentation"><a href="#compact-json" role="tab" data-toggle="tab">Compact JSON</a></li>
+                            </ul>
+                            <div class="tab-content" id="save-texts">
+                                <div role="tabpanel" class="tab-pane active" id="chef-format">
+                                    <textarea class="form-control" id="save-text-chef" rows="5"></textarea>
+                                </div>
+                                <div role="tabpanel" class="tab-pane" id="clean-json">
+                                    <textarea class="form-control" id="save-text-clean" rows="5"></textarea>
+                                </div>
+                                <div role="tabpanel" class="tab-pane" id="compact-json">
+                                    <textarea class="form-control" id="save-text-compact" rows="5"></textarea>
+                                </div>
+                            </div>
                         </div>
                         <div class="form-group">
                             <label for="save-name">Recipe name</label>
@@ -277,6 +297,7 @@
                             <select option="theme" id="theme">
                                 <option value="classic">Classic</option>
                                 <option value="dark">Dark</option>
+                                <option value="geocities">GeoCities</option>
                             </select>
                             <label for="theme"> Theme (only supported in modern browsers)</label>
                         </div>
@@ -383,12 +404,12 @@
                                     <div class="collapse" id="faq-examples">
                                         <p>There are around 200 operations in CyberChef allowing you to carry out simple and complex tasks easily. Here are some examples:</p>
                                         <ul>
-                                            <li><a href="#recipe=%5B%7B%22op%22%3A%22From%20Base64%22%2C%22args%22%3A%5B%22A-Za-z0-9%2B%2F%3D%22%2Ctrue%5D%7D%5D&input=VTI4Z2JHOXVaeUJoYm1RZ2RHaGhibXR6SUdadmNpQmhiR3dnZEdobElHWnBjMmd1">Decode a Base64-encoded string</a></li>
-                                            <li><a href="#recipe=%5B%7B%22op%22%3A%22Translate%20DateTime%20Format%22%2C%22args%22%3A%5B%22Standard%20date%20and%20time%22%2C%22DD%2FMM%2FYYYY%20HH%3Amm%3Ass%22%2C%22UTC%22%2C%22dddd%20Do%20MMMM%20YYYY%20HH%3Amm%3Ass%20Z%20z%22%2C%22Australia%2FQueensland%22%5D%7D%5D&input=MTUvMDYvMjAxNSAyMDo0NTowMA">Convert a date and time to a different time zone</a></li>
-                                            <li><a href="#recipe=%5B%7B%22op%22%3A%22Parse%20IPv6%20address%22%2C%22args%22%3A%5B%5D%7D%5D&input=MjAwMTowMDAwOjQxMzY6ZTM3ODo4MDAwOjYzYmY6M2ZmZjpmZGQy">Parse a Teredo IPv6 address</a></li>
-                                            <li><a href="#recipe=%5B%7B%22op%22%3A%22From%20Hexdump%22%2C%22args%22%3A%5B%5D%7D%2C%7B%22op%22%3A%22Gunzip%22%2C%22args%22%3A%5B%5D%7D%5D&input=MDAwMDAwMDAgIDFmIDhiIDA4IDAwIDEyIGJjIGYzIDU3IDAwIGZmIDBkIGM3IGMxIDA5IDAwIDIwICB8Li4uLi6881cu%2Fy7HwS4uIHwKMDAwMDAwMTAgIDA4IDA1IGQwIDU1IGZlIDA0IDJkIGQzIDA0IDFmIGNhIDhjIDQ0IDIxIDViIGZmICB8Li7QVf4uLdMuLsouRCFb%2F3wKMDAwMDAwMjAgIDYwIGM3IGQ3IDAzIDE2IGJlIDQwIDFmIDc4IDRhIDNmIDA5IDg5IDBiIDlhIDdkICB8YMfXLi6%2BQC54Sj8uLi4ufXwKMDAwMDAwMzAgIDRlIGM4IDRlIDZkIDA1IDFlIDAxIDhiIDRjIDI0IDAwIDAwIDAwICAgICAgICAgICB8TshObS4uLi5MJC4uLnw">Convert data from a hexdump, then decompress</a></li>
-                                            <li><a href="#recipe=%5B%7B%22op%22%3A%22Fork%22%2C%22args%22%3A%5B%22%5C%5Cn%22%2C%22%5C%5Cn%22%5D%7D%2C%7B%22op%22%3A%22From%20UNIX%20Timestamp%22%2C%22args%22%3A%5B%22Seconds%20(s)%22%5D%7D%5D&input=OTc4MzQ2ODAwCjEwMTI2NTEyMDAKMTA0NjY5NjQwMAoxMDgxMDg3MjAwCjExMTUzMDUyMDAKMTE0OTYwOTYwMA">Display multiple timestamps as full dates</a></li>
-                                            <li><a href="#recipe=%5B%7B%22op%22%3A%22Fork%22%2C%22args%22%3A%5B%22%5C%5Cn%22%2C%22%5C%5Cn%22%5D%7D%2C%7B%22op%22%3A%22Conditional%20Jump%22%2C%22args%22%3A%5B%221%22%2C%222%22%2C%2210%22%5D%7D%2C%7B%22op%22%3A%22To%20Hex%22%2C%22args%22%3A%5B%22Space%22%5D%7D%2C%7B%22op%22%3A%22Return%22%2C%22args%22%3A%5B%5D%7D%2C%7B%22op%22%3A%22To%20Base64%22%2C%22args%22%3A%5B%22A-Za-z0-9%2B%2F%3D%22%5D%7D%5D&input=U29tZSBkYXRhIHdpdGggYSAxIGluIGl0ClNvbWUgZGF0YSB3aXRoIGEgMiBpbiBpdA">Carry out different operations on data of different types</a></li>
+                                            <li><a href="#recipe=From_Base64('A-Za-z0-9%2B/%3D',true)&input=VTI4Z2JHOXVaeUJoYm1RZ2RHaGhibXR6SUdadmNpQmhiR3dnZEdobElHWnBjMmd1">Decode a Base64-encoded string</a></li>
+                                            <li><a href="#recipe=Translate_DateTime_Format('Standard%20date%20and%20time','DD/MM/YYYY%20HH:mm:ss','UTC','dddd%20Do%20MMMM%20YYYY%20HH:mm:ss%20Z%20z','Australia/Queensland')&input=MTUvMDYvMjAxNSAyMDo0NTowMA">Convert a date and time to a different time zone</a></li>
+                                            <li><a href="#recipe=Parse_IPv6_address()&input=MjAwMTowMDAwOjQxMzY6ZTM3ODo4MDAwOjYzYmY6M2ZmZjpmZGQy">Parse a Teredo IPv6 address</a></li>
+                                            <li><a href="#recipe=From_Hexdump()Gunzip()&input=MDAwMDAwMDAgIDFmIDhiIDA4IDAwIDEyIGJjIGYzIDU3IDAwIGZmIDBkIGM3IGMxIDA5IDAwIDIwICB8Li4uLi6881cu/y7HwS4uIHwKMDAwMDAwMTAgIDA4IDA1IGQwIDU1IGZlIDA0IDJkIGQzIDA0IDFmIGNhIDhjIDQ0IDIxIDViIGZmICB8Li7QVf4uLdMuLsouRCFb/3wKMDAwMDAwMjAgIDYwIGM3IGQ3IDAzIDE2IGJlIDQwIDFmIDc4IDRhIDNmIDA5IDg5IDBiIDlhIDdkICB8YMfXLi6%2BQC54Sj8uLi4ufXwKMDAwMDAwMzAgIDRlIGM4IDRlIDZkIDA1IDFlIDAxIDhiIDRjIDI0IDAwIDAwIDAwICAgICAgICAgICB8TshObS4uLi5MJC4uLnw">Convert data from a hexdump, then decompress</a></li>
+                                            <li><a href="#recipe=Fork('%5C%5Cn','%5C%5Cn',false)From_UNIX_Timestamp('Seconds%20(s)')&input=OTc4MzQ2ODAwCjEwMTI2NTEyMDAKMTA0NjY5NjQwMAoxMDgxMDg3MjAwCjExMTUzMDUyMDAKMTE0OTYwOTYwMA">Display multiple timestamps as full dates</a></li>
+                                            <li><a href="#recipe=Fork('%5C%5Cn','%5C%5Cn',false)Conditional_Jump('1',2,10)To_Hex('Space')Return()To_Base64('A-Za-z0-9%2B/%3D')&input=U29tZSBkYXRhIHdpdGggYSAxIGluIGl0ClNvbWUgZGF0YSB3aXRoIGEgMiBpbiBpdA">Carry out different operations on data of different types</a></li>
                                         </ul>
                                     </div>
                                     <blockquote>
@@ -408,7 +429,7 @@
                                     <div class="collapse" id="faq-fork">
                                         <p>Maybe you have 10 timestamps that you want to parse or 16 encoded strings that all have the same key.</p>
                                         <p>The 'Fork' operation (found in the 'Flow control' category) splits up the input line by line and runs all subsequent operations on each line separately. Each output is then displayed on a separate line. These delimiters can be changed, so if your inputs are separated by commas, you can change the split delimiter to a comma instead.</p>
-                                        <p><a href='#recipe=%5B%7B"op"%3A"Fork"%2C"args"%3A%5B"%5C%5Cn"%2C"%5C%5Cn"%5D%7D%2C%7B"op"%3A"From%20UNIX%20Timestamp"%2C"args"%3A%5B"Seconds%20(s)"%5D%7D%5D&input=OTc4MzQ2ODAwCjEwMTI2NTEyMDAKMTA0NjY5NjQwMAoxMDgxMDg3MjAwCjExMTUzMDUyMDAKMTE0OTYwOTYwMA%3D%3D'>Click here</a> for an example.</p>
+                                        <p><a href="#recipe=Fork('%5C%5Cn','%5C%5Cn',false)From_UNIX_Timestamp('Seconds%20(s)')&input=OTc4MzQ2ODAwCjEwMTI2NTEyMDAKMTA0NjY5NjQwMAoxMDgxMDg3MjAwCjExMTUzMDUyMDAKMTE0OTYwOTYwMA">Click here</a> for an example.</p>
                                     </div>
                                 </div>
                                 <div role="tabpanel" class="tab-pane" id="report-bug">

二进制
src/web/static/images/breakpoint-16x16.png


二进制
src/web/static/images/bug-16x16.png


二进制
src/web/static/images/clean-16x16.png


二进制
src/web/static/images/code-16x16.png


二进制
src/web/static/images/cook_female-32x32.png


二进制
src/web/static/images/cook_male-32x32.png


二进制
src/web/static/images/cyberchef-128x128.png


二进制
src/web/static/images/cyberchef-16x16.png


二进制
src/web/static/images/cyberchef-256x256.png


二进制
src/web/static/images/cyberchef-32x32.png


二进制
src/web/static/images/cyberchef-512x512.png


二进制
src/web/static/images/cyberchef-64x64.png


二进制
src/web/static/images/disable_deselected-16x16.png


二进制
src/web/static/images/disable_selected-16x16.png


二进制
src/web/static/images/download-24x24.png


二进制
src/web/static/images/erase-16x16.png


二进制
src/web/static/images/favourite-16x16.png


二进制
src/web/static/images/favourite-24x24.png


二进制
src/web/static/images/fork_me.png


二进制
src/web/static/images/help-16x16.png


二进制
src/web/static/images/help-22x22.png


二进制
src/web/static/images/info-16x16.png


二进制
src/web/static/images/layout-16x16.png


二进制
src/web/static/images/mail-16x16.png


二进制
src/web/static/images/maximise-16x16.png


二进制
src/web/static/images/open_yellow-16x16.png


二进制
src/web/static/images/open_yellow-24x24.png


二进制
src/web/static/images/recycle-16x16.png


二进制
src/web/static/images/remove-16x16.png


二进制
src/web/static/images/restore-16x16.png


二进制
src/web/static/images/save-16x16.png


二进制
src/web/static/images/save-22x22.png


二进制
src/web/static/images/save_as-16x16.png


二进制
src/web/static/images/settings-22x22.png


二进制
src/web/static/images/speech-16x16.png


二进制
src/web/static/images/stats-16x16.png


二进制
src/web/static/images/step-16x16.png


二进制
src/web/static/images/switch-16x16.png


二进制
src/web/static/images/thumb_down-16x16.png


二进制
src/web/static/images/thumb_up-16x16.png


二进制
src/web/static/images/undo-16x16.png


+ 1 - 0
src/web/stylesheets/index.css

@@ -9,6 +9,7 @@
 /* Themes */
 @import "./themes/_classic.css";
 @import "./themes/_dark.css";
+@import "./themes/_geocities.css";
 
 /* Utilities */
 @import "./utils/_overrides.css";

+ 7 - 1
src/web/stylesheets/layout/_modals.css

@@ -78,7 +78,13 @@
     font-family: var(--primary-font-family);
 }
 
-#save-text,
+#save-texts textarea,
 #load-text {
     font-family: var(--fixed-width-font-family);
 }
+
+#save-texts textarea {
+    border-top: none;
+    box-shadow: none;
+    height: 200px;
+}

+ 2 - 2
src/web/stylesheets/preloader.css

@@ -61,8 +61,8 @@
 .loading-msg {
     display: block;
     position: relative;
-    width: 300px;
-    left: calc(50% - 150px);
+    width: 400px;
+    left: calc(50% - 200px);
     top: calc(50% + 50px);
     text-align: center;
     margin-top: 50px;

+ 115 - 0
src/web/stylesheets/themes/_geocities.css

@@ -0,0 +1,115 @@
+/**
+ * GeoCities theme definitions
+ *
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2017
+ * @license Apache-2.0
+ */
+
+:root.geocities {
+    --primary-font-family: "Comic Sans", "Comic Sans MS", "Chalkboard", "ChalkboardSE-Regular", "Marker Felt", "Purisa", "URW Chancery L", cursive, sans-serif;
+    --primary-font-colour: black;
+    --primary-font-size: 14px;
+    --primary-line-height: 20px;
+
+    --fixed-width-font-family: "Courier New", Courier, monospace;
+    --fixed-width-font-colour: yellow;
+    --fixed-width-font-size: inherit;
+
+    --subtext-font-colour: darkgrey;
+    --subtext-font-size: 13px;
+
+    --primary-background-colour: #00f;
+    --secondary-background-colour: #f00;
+
+    --primary-border-colour: pink;
+    --secondary-border-colour: springgreen;
+
+    --title-colour: red;
+    --title-weight: bold;
+    --title-background-colour: yellow;
+
+    --banner-font-colour: white;
+    --banner-bg-colour: maroon;
+
+
+    /* Operation colours */
+    --op-list-operation-font-colour: blue;
+    --op-list-operation-bg-colour: yellow;
+    --op-list-operation-border-colour: green;
+
+    --rec-list-operation-font-colour: white;
+    --rec-list-operation-bg-colour: purple;
+    --rec-list-operation-border-colour: green;
+
+    --selected-operation-font-color: white;
+    --selected-operation-bg-colour: pink;
+    --selected-operation-border-colour: blue;
+
+    --breakpoint-font-colour: white;
+    --breakpoint-bg-colour: red;
+    --breakpoint-border-colour: blue;
+
+    --disabled-font-colour: grey;
+    --disabled-bg-colour: black;
+    --disabled-border-colour: grey;
+
+    --fc-operation-font-colour: sienna;
+    --fc-operation-bg-colour: pink;
+    --fc-operation-border-colour: yellow;
+
+    --fc-breakpoint-operation-font-colour: darkgrey;
+    --fc-breakpoint-operation-bg-colour: deeppink;
+    --fc-breakpoint-operation-border-colour: yellowgreen;
+
+
+    /* Operation arguments */
+    --arg-title-font-weight: bold;
+    --arg-input-height: 34px;
+    --arg-input-line-height: 20px;
+    --arg-input-font-size: 15px;
+    --arg-font-colour: white;
+    --arg-background: black;
+    --arg-border-colour: lime;
+    --arg-disabled-background: grey;
+
+
+    /* Buttons */
+    --btn-default-font-colour: black;
+    --btn-default-bg-colour: white;
+    --btn-default-border-colour: grey;
+
+    --btn-default-hover-font-colour: black;
+    --btn-default-hover-bg-colour: white;
+    --btn-default-hover-border-colour: grey;
+
+    --btn-success-font-colour: white;
+    --btn-success-bg-colour: lawngreen;
+    --btn-success-border-colour: grey;
+
+    --btn-success-hover-font-colour: white;
+    --btn-success-hover-bg-colour: lime;
+    --btn-success-hover-border-colour: grey;
+
+
+    /* Highlighter colours */
+    --hl1: #fff000;
+    --hl2: #95dfff;
+    --hl3: #ffb6b6;
+    --hl4: #fcf8e3;
+    --hl5: #8de768;
+
+
+    /* Scrollbar */
+    --scrollbar-track: lightsteelblue;
+    --scrollbar-thumb: lightslategrey;
+    --scrollbar-hover: grey;
+
+
+    /* Misc. */
+    --drop-file-border-colour: purple;
+    --popover-background: turquoise;
+    --popover-border-colour: violet;
+    --code-background: black;
+    --code-font-colour: limegreen;
+}

+ 4 - 0
src/web/stylesheets/utils/_overrides.css

@@ -142,6 +142,10 @@ optgroup {
     border-color: var(--popover-border-colour);
 }
 
+.popover-content {
+    max-height: 90vh;
+    overflow-y: auto;
+}
 
 .popover.right>.arrow {
     border-right-color: var(--popover-border-colour);

+ 4 - 0
test/index.js

@@ -13,6 +13,7 @@ import "babel-polyfill";
 import TestRegister from "./TestRegister.js";
 import "./tests/operations/Base58.js";
 import "./tests/operations/BCD.js";
+import "./tests/operations/BitwiseOp.js";
 import "./tests/operations/ByteRepr.js";
 import "./tests/operations/CharEnc.js";
 import "./tests/operations/Cipher.js";
@@ -20,11 +21,14 @@ import "./tests/operations/Code.js";
 import "./tests/operations/Compress.js";
 import "./tests/operations/DateTime.js";
 import "./tests/operations/FlowControl.js";
+import "./tests/operations/Hash.js";
 import "./tests/operations/Image.js";
 import "./tests/operations/MorseCode.js";
+import "./tests/operations/MS.js";
 import "./tests/operations/StrUtils.js";
 import "./tests/operations/SeqUtils.js";
 
+
 let allTestsPassing = true;
 const testStatusCounts = {
     total: 0,

+ 50 - 0
test/tests/operations/BitwiseOp.js

@@ -0,0 +1,50 @@
+/**
+ * BitwiseOp tests
+ *
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2017
+ * @license Apache-2.0
+ */
+import TestRegister from "../../TestRegister.js";
+
+TestRegister.addTests([
+    {
+        name: "Bit shift left",
+        input: "01010101 10101010 11111111 00000000 11110000 00001111 00110011 11001100",
+        expectedOutput: "10101010 01010100 11111110 00000000 11100000 00011110 01100110 10011000",
+        recipeConfig: [
+            { "op": "From Binary",
+                "args": ["Space"] },
+            { "op": "Bit shift left",
+                "args": [1] },
+            { "op": "To Binary",
+                "args": ["Space"] }
+        ]
+    },
+    {
+        name: "Bit shift right: Logical shift",
+        input: "01010101 10101010 11111111 00000000 11110000 00001111 00110011 11001100",
+        expectedOutput: "00101010 01010101 01111111 00000000 01111000 00000111 00011001 01100110",
+        recipeConfig: [
+            { "op": "From Binary",
+                "args": ["Space"] },
+            { "op": "Bit shift right",
+                "args": [1, "Logical shift"] },
+            { "op": "To Binary",
+                "args": ["Space"] }
+        ]
+    },
+    {
+        name: "Bit shift right: Arithmetic shift",
+        input: "01010101 10101010 11111111 00000000 11110000 00001111 00110011 11001100",
+        expectedOutput: "00101010 11010101 11111111 00000000 11111000 00000111 00011001 11100110",
+        recipeConfig: [
+            { "op": "From Binary",
+                "args": ["Space"] },
+            { "op": "Bit shift right",
+                "args": [1, "Arithmetic shift"] },
+            { "op": "To Binary",
+                "args": ["Space"] }
+        ]
+    },
+]);

+ 181 - 0
test/tests/operations/Code.js

@@ -2,12 +2,54 @@
  * Code tests.
  *
  * @author tlwr [toby@toby.codes]
+ * @author Matt C [matt@artemisbot.uk]
  *
  * @copyright Crown Copyright 2017
  * @license Apache-2.0
  */
 import TestRegister from "../../TestRegister.js";
 
+const JPATH_TEST_DATA = {
+    "store": {
+        "book": [{
+            "category": "reference",
+            "author": "Nigel Rees",
+            "title": "Sayings of the Century",
+            "price": 8.95
+        }, {
+            "category": "fiction",
+            "author": "Evelyn Waugh",
+            "title": "Sword of Honour",
+            "price": 12.99
+        }, {
+            "category": "fiction",
+            "author": "Herman Melville",
+            "title": "Moby Dick",
+            "isbn": "0-553-21311-3",
+            "price": 8.99
+        }, {
+            "category": "fiction",
+            "author": "J. R. R. Tolkien",
+            "title": "The Lord of the Rings",
+            "isbn": "0-395-19395-8",
+            "price": 22.99
+        }],
+        "bicycle": {
+            "color": "red",
+            "price": 19.95
+        },
+        "newspaper": [{
+            "format": "broadsheet",
+            "title": "Financial Times",
+            "price": 2.75
+        }, {
+            "format": "tabloid",
+            "title": "The Guardian",
+            "price": 2.00
+        }]
+    }
+};
+
 TestRegister.addTests([
     {
         name: "To Camel case (dumb)",
@@ -129,4 +171,143 @@ TestRegister.addTests([
             }
         ],
     },
+    {
+        name: "JPath Expression: Empty JSON",
+        input: "",
+        expectedOutput: "Invalid input JSON: Unexpected end of JSON input",
+        recipeConfig: [
+            {
+                "op": "JPath expression",
+                "args": ["", "\n"]
+            }
+        ],
+    },
+    {
+        name: "JPath Expression: Empty expression",
+        input: JSON.stringify(JPATH_TEST_DATA),
+        expectedOutput: "Invalid JPath expression: we need a path",
+        recipeConfig: [
+            {
+                "op": "JPath expression",
+                "args": ["", "\n"]
+            }
+        ],
+    },
+    {
+        name: "JPath Expression: Fetch of values from specific object",
+        input: JSON.stringify(JPATH_TEST_DATA),
+        expectedOutput: [
+            "\"Nigel Rees\"",
+            "\"Evelyn Waugh\"",
+            "\"Herman Melville\"",
+            "\"J. R. R. Tolkien\""
+        ].join("\n"),
+        recipeConfig: [
+            {
+                "op": "JPath expression",
+                "args": ["$.store.book[*].author", "\n"]
+            }
+        ],
+    },
+    {
+        name: "JPath Expression: Fetch of all values with matching key",
+        input: JSON.stringify(JPATH_TEST_DATA),
+        expectedOutput: [
+            "\"Sayings of the Century\"",
+            "\"Sword of Honour\"",
+            "\"Moby Dick\"",
+            "\"The Lord of the Rings\"",
+            "\"Financial Times\"",
+            "\"The Guardian\""
+        ].join("\n"),
+        recipeConfig: [
+            {
+                "op": "JPath expression",
+                "args": ["$..title", "\n"]
+            }
+        ],
+    },
+    {
+        name: "JPath Expression: All data in object",
+        input: JSON.stringify(JPATH_TEST_DATA),
+        expectedOutput: [
+            "[{\"category\":\"reference\",\"author\":\"Nigel Rees\",\"title\":\"Sayings of the Century\",\"price\":8.95},{\"category\":\"fiction\",\"author\":\"Evelyn Waugh\",\"title\":\"Sword of Honour\",\"price\":12.99},{\"category\":\"fiction\",\"author\":\"Herman Melville\",\"title\":\"Moby Dick\",\"isbn\":\"0-553-21311-3\",\"price\":8.99},{\"category\":\"fiction\",\"author\":\"J. R. R. Tolkien\",\"title\":\"The Lord of the Rings\",\"isbn\":\"0-395-19395-8\",\"price\":22.99}]",
+            "{\"color\":\"red\",\"price\":19.95}",
+            "[{\"format\":\"broadsheet\",\"title\":\"Financial Times\",\"price\":2.75},{\"format\":\"tabloid\",\"title\":\"The Guardian\",\"price\":2}]"
+        ].join("\n"),
+        recipeConfig: [
+            {
+                "op": "JPath expression",
+                "args": ["$.store.*", "\n"]
+            }
+        ],
+    },
+    {
+        name: "JPath Expression: Last element in array",
+        input: JSON.stringify(JPATH_TEST_DATA),
+        expectedOutput: "{\"category\":\"fiction\",\"author\":\"J. R. R. Tolkien\",\"title\":\"The Lord of the Rings\",\"isbn\":\"0-395-19395-8\",\"price\":22.99}",
+        recipeConfig: [
+            {
+                "op": "JPath expression",
+                "args": ["$..book[-1:]", "\n"]
+            }
+        ],
+    },
+    {
+        name: "JPath Expression: First 2 elements in array",
+        input: JSON.stringify(JPATH_TEST_DATA),
+        expectedOutput: [
+            "{\"category\":\"reference\",\"author\":\"Nigel Rees\",\"title\":\"Sayings of the Century\",\"price\":8.95}",
+            "{\"category\":\"fiction\",\"author\":\"Evelyn Waugh\",\"title\":\"Sword of Honour\",\"price\":12.99}"
+        ].join("\n"),
+        recipeConfig: [
+            {
+                "op": "JPath expression",
+                "args": ["$..book[:2]", "\n"]
+            }
+        ],
+    },
+    {
+        name: "JPath Expression: All elements in array with property",
+        input: JSON.stringify(JPATH_TEST_DATA),
+        expectedOutput: [
+            "{\"category\":\"fiction\",\"author\":\"Herman Melville\",\"title\":\"Moby Dick\",\"isbn\":\"0-553-21311-3\",\"price\":8.99}",
+            "{\"category\":\"fiction\",\"author\":\"J. R. R. Tolkien\",\"title\":\"The Lord of the Rings\",\"isbn\":\"0-395-19395-8\",\"price\":22.99}"
+        ].join("\n"),
+        recipeConfig: [
+            {
+                "op": "JPath expression",
+                "args": ["$..book[?(@.isbn)]", "\n"]
+            }
+        ],
+    },
+    {
+        name: "JPath Expression: All elements in array which meet condition",
+        input: JSON.stringify(JPATH_TEST_DATA),
+        expectedOutput: [
+            "{\"category\":\"fiction\",\"author\":\"Evelyn Waugh\",\"title\":\"Sword of Honour\",\"price\":12.99}",
+            "{\"category\":\"fiction\",\"author\":\"Herman Melville\",\"title\":\"Moby Dick\",\"isbn\":\"0-553-21311-3\",\"price\":8.99}",
+            "{\"category\":\"fiction\",\"author\":\"J. R. R. Tolkien\",\"title\":\"The Lord of the Rings\",\"isbn\":\"0-395-19395-8\",\"price\":22.99}"
+        ].join("\n"),
+        recipeConfig: [
+            {
+                "op": "JPath expression",
+                "args": ["$..book[?(@.price<30 && @.category==\"fiction\")]", "\n"]
+            }
+        ],
+    },
+    {
+        name: "JPath Expression: All elements in object",
+        input: JSON.stringify(JPATH_TEST_DATA),
+        expectedOutput: [
+            "{\"category\":\"reference\",\"author\":\"Nigel Rees\",\"title\":\"Sayings of the Century\",\"price\":8.95}",
+            "{\"category\":\"fiction\",\"author\":\"Herman Melville\",\"title\":\"Moby Dick\",\"isbn\":\"0-553-21311-3\",\"price\":8.99}"
+        ].join("\n"),
+        recipeConfig: [
+            {
+                "op": "JPath expression",
+                "args": ["$..book[?(@.price<10)]", "\n"]
+            }
+        ],
+    },
 ]);

+ 308 - 0
test/tests/operations/Hash.js

@@ -0,0 +1,308 @@
+/**
+ * Hash tests.
+ *
+ * @author Matt C [matt@artemisbot.uk]
+ * @copyright Crown Copyright 2017
+ * @license Apache-2.0
+ */
+import TestRegister from "../../TestRegister.js";
+
+TestRegister.addTests([
+    {
+        name: "MD2",
+        input: "Hello, World!",
+        expectedOutput: "1c8f1e6a94aaa7145210bf90bb52871a",
+        recipeConfig: [
+            {
+                "op": "MD2",
+                "args": []
+            }
+        ]
+    },
+    {
+        name: "MD4",
+        input: "Hello, World!",
+        expectedOutput: "94e3cb0fa9aa7a5ee3db74b79e915989",
+        recipeConfig: [
+            {
+                "op": "MD4",
+                "args": []
+            }
+        ]
+    },
+    {
+        name: "MD5",
+        input: "Hello, World!",
+        expectedOutput: "65a8e27d8879283831b664bd8b7f0ad4",
+        recipeConfig: [
+            {
+                "op": "MD5",
+                "args": []
+            }
+        ]
+    },
+    {
+        name: "MD6",
+        input: "Hello, World!",
+        expectedOutput: "ce5effce32637e6b8edaacc9284b873c3fd4e66f9779a79df67eb4a82dda8230",
+        recipeConfig: [
+            {
+                "op": "MD6",
+                "args": [256, 64, ""]
+            }
+        ]
+    },
+    {
+        name: "SHA0",
+        input: "Hello, World!",
+        expectedOutput: "5a5588f0407c6ae9a988758e76965f841b299229",
+        recipeConfig: [
+            {
+                "op": "SHA0",
+                "args": []
+            }
+        ]
+    },
+    {
+        name: "SHA1",
+        input: "Hello, World!",
+        expectedOutput: "0a0a9f2a6772942557ab5355d76af442f8f65e01",
+        recipeConfig: [
+            {
+                "op": "SHA1",
+                "args": []
+            }
+        ]
+    },
+    {
+        name: "SHA2 224",
+        input: "Hello, World!",
+        expectedOutput: "72a23dfa411ba6fde01dbfabf3b00a709c93ebf273dc29e2d8b261ff",
+        recipeConfig: [
+            {
+                "op": "SHA2",
+                "args": ["224"]
+            }
+        ]
+    },
+    {
+        name: "SHA2 384",
+        input: "Hello, World!",
+        expectedOutput: "5485cc9b3365b4305dfb4e8337e0a598a574f8242bf17289e0dd6c20a3cd44a089de16ab4ab308f63e44b1170eb5f515",
+        recipeConfig: [
+            {
+                "op": "SHA2",
+                "args": ["384"]
+            }
+        ]
+    },
+    {
+        name: "SHA2 256",
+        input: "Hello, World!",
+        expectedOutput: "dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986f",
+        recipeConfig: [
+            {
+                "op": "SHA2",
+                "args": ["256"]
+            }
+        ]
+    },
+    {
+        name: "SHA2 512",
+        input: "Hello, World!",
+        expectedOutput: "374d794a95cdcfd8b35993185fef9ba368f160d8daf432d08ba9f1ed1e5abe6cc69291e0fa2fe0006a52570ef18c19def4e617c33ce52ef0a6e5fbe318cb0387",
+        recipeConfig: [
+            {
+                "op": "SHA2",
+                "args": ["512"]
+            }
+        ]
+    },
+    {
+        name: "SHA2 512/224",
+        input: "Hello, World!",
+        expectedOutput: "766745f058e8a0438f19de48ae56ea5f123fe738af39bca050a7547a",
+        recipeConfig: [
+            {
+                "op": "SHA2",
+                "args": ["512/224"]
+            }
+        ]
+    },
+    {
+        name: "SHA2 512/256",
+        input: "Hello, World!",
+        expectedOutput: "0686f0a605973dc1bf035d1e2b9bad1985a0bff712ddd88abd8d2593e5f99030",
+        recipeConfig: [
+            {
+                "op": "SHA2",
+                "args": ["512/256"]
+            }
+        ]
+    },
+    {
+        name: "SHA3 224",
+        input: "Hello, World!",
+        expectedOutput: "853048fb8b11462b6100385633c0cc8dcdc6e2b8e376c28102bc84f2",
+        recipeConfig: [
+            {
+                "op": "SHA3",
+                "args": ["224"]
+            }
+        ]
+    },
+    {
+        name: "SHA3 384",
+        input: "Hello, World!",
+        expectedOutput: "aa9ad8a49f31d2ddcabbb7010a1566417cff803fef50eba239558826f872e468c5743e7f026b0a8e5b2d7a1cc465cdbe",
+        recipeConfig: [
+            {
+                "op": "SHA3",
+                "args": ["384"]
+            }
+        ]
+    },
+    {
+        name: "SHA3 256",
+        input: "Hello, World!",
+        expectedOutput: "1af17a664e3fa8e419b8ba05c2a173169df76162a5a286e0c405b460d478f7ef",
+        recipeConfig: [
+            {
+                "op": "SHA3",
+                "args": ["256"]
+            }
+        ]
+    },
+    {
+        name: "SHA3 512",
+        input: "Hello, World!",
+        expectedOutput: "38e05c33d7b067127f217d8c856e554fcff09c9320b8a5979ce2ff5d95dd27ba35d1fba50c562dfd1d6cc48bc9c5baa4390894418cc942d968f97bcb659419ed",
+        recipeConfig: [
+            {
+                "op": "SHA3",
+                "args": ["512"]
+            }
+        ]
+    },
+    {
+        name: "Keccak 224",
+        input: "Hello, World!",
+        expectedOutput: "4eaaf0e7a1e400efba71130722e1cb4d59b32afb400e654afec4f8ce",
+        recipeConfig: [
+            {
+                "op": "Keccak",
+                "args": ["224"]
+            }
+        ]
+    },
+    {
+        name: "Keccak 384",
+        input: "Hello, World!",
+        expectedOutput: "4d60892fde7f967bcabdc47c73122ae6311fa1f9be90d721da32030f7467a2e3db3f9ccb3c746483f9d2b876e39def17",
+        recipeConfig: [
+            {
+                "op": "Keccak",
+                "args": ["384"]
+            }
+        ]
+    },
+    {
+        name: "Keccak 256",
+        input: "Hello, World!",
+        expectedOutput: "acaf3289d7b601cbd114fb36c4d29c85bbfd5e133f14cb355c3fd8d99367964f",
+        recipeConfig: [
+            {
+                "op": "Keccak",
+                "args": ["256"]
+            }
+        ]
+    },
+    {
+        name: "Keccak 512",
+        input: "Hello, World!",
+        expectedOutput: "eda765576c84c600ed7f5d97510e92703b61f5215def2a161037fd9dd1f5b6ed4f86ce46073c0e3f34b52de0289e9c618798fff9dd4b1bfe035bdb8645fc6e37",
+        recipeConfig: [
+            {
+                "op": "Keccak",
+                "args": ["512"]
+            }
+        ]
+    },
+    {
+        name: "Shake 128",
+        input: "Hello, World!",
+        expectedOutput: "2bf5e6dee6079fad604f573194ba8426bd4d30eb13e8ba2edae70e529b570cbd",
+        recipeConfig: [
+            {
+                "op": "Shake",
+                "args": ["128", 256]
+            }
+        ]
+    },
+    {
+        name: "Shake 256",
+        input: "Hello, World!",
+        expectedOutput: "b3be97bfd978833a65588ceae8a34cf59e95585af62063e6b89d0789f372424e8b0d1be4f21b40ce5a83a438473271e0661854f02d431db74e6904d6c347d757",
+        recipeConfig: [
+            {
+                "op": "Shake",
+                "args": ["256", 512]
+            }
+        ]
+    },
+    {
+        name: "RIPEMD 128",
+        input: "Hello, World!",
+        expectedOutput: "67f9fe75ca2886dc76ad00f7276bdeba",
+        recipeConfig: [
+            {
+                "op": "RIPEMD",
+                "args": ["128"]
+            }
+        ]
+    },
+    {
+        name: "RIPEMD 160",
+        input: "Hello, World!",
+        expectedOutput: "527a6a4b9a6da75607546842e0e00105350b1aaf",
+        recipeConfig: [
+            {
+                "op": "RIPEMD",
+                "args": ["160"]
+            }
+        ]
+    },
+    {
+        name: "RIPEMD 256",
+        input: "Hello, World!",
+        expectedOutput: "567750c6d34dcba7ae038a80016f3ca3260ec25bfdb0b68bbb8e730b00b2447d",
+        recipeConfig: [
+            {
+                "op": "RIPEMD",
+                "args": ["256"]
+            }
+        ]
+    },
+    {
+        name: "RIPEMD 320",
+        input: "Hello, World!",
+        expectedOutput: "f9832e5bb00576fc56c2221f404eb77addeafe49843c773f0df3fc5a996d5934f3c96e94aeb80e89",
+        recipeConfig: [
+            {
+                "op": "RIPEMD",
+                "args": ["320"]
+            }
+        ]
+    },
+    {
+        name: "HMAC SHA256",
+        input: "Hello, World!",
+        expectedOutput: "52589bd80ccfa4acbb3f9512dfaf4f700fa5195008aae0b77a9e47dcca75beac",
+        recipeConfig: [
+            {
+                "op": "HMAC",
+                "args": ["test", "SHA256"]
+            }
+        ]
+    },
+]);

+ 22 - 0
test/tests/operations/MS.js

@@ -0,0 +1,22 @@
+/**
+ * MS tests.
+ *
+ * @author bwhitn [brian.m.whitney@outlook.com]
+ * @copyright Crown Copyright 2017
+ * @license Apache-2.0
+ */
+import TestRegister from "../../TestRegister.js";
+
+TestRegister.addTests([
+    {
+        name: "Microsoft Script Decoder",
+        input: "#@~^RQAAAA==-mD~sX|:/TP{~J:+dYbxL~@!F@*@!+@*@!&@*eEI@#@&@#@&\x7fjm.raY 214Wv:zms/obI0xEAAA==^#~@",
+        expectedOutput: "var my_msg = \"Testing <1><2><3>!\";\r\n\r\nWScript.Echo(my_msg);",
+        recipeConfig: [
+            {
+                "op": "Microsoft Script Decoder",
+                "args": []
+            },
+        ],
+    },
+]);

+ 44 - 0
test/tests/operations/StrUtils.js

@@ -232,4 +232,48 @@ TestRegister.addTests([
             }
         ],
     },
+    {
+        name: "Escape String: quotes",
+        input: "Hello \"World\"! Escape 'these' quotes.",
+        expectedOutput: "Hello \\\"World\\\"! Escape \\'these\\' quotes.",
+        recipeConfig: [
+            {
+                "op": "Escape string",
+                "args": []
+            }
+        ],
+    },
+    {
+        name: "Escape String: special characters",
+        input: "Fizz & buzz\n\ttabbed newline\rcarriage returned line\nbackspace character: \"\" form feed character: \"\"",
+        expectedOutput: "Fizz & buzz\\n\\ttabbed newline\\rcarriage returned line\\nbackspace character: \\\"\\b\\\" form feed character: \\\"\\f\\\"",
+        recipeConfig: [
+            {
+                "op": "Escape string",
+                "args": []
+            }
+        ],
+    },
+    {
+        name: "Unescape String: quotes",
+        input: "Hello \\\"World\\\"! Escape \\'these\\' quotes.",
+        expectedOutput: "Hello \"World\"! Escape 'these' quotes.",
+        recipeConfig: [
+            {
+                "op": "Unescape string",
+                "args": []
+            }
+        ],
+    },
+    {
+        name: "Unescape String: special characters",
+        input: "Fizz \x26 buzz\\n\\ttabbed newline\\rcarriage returned line\\nbackspace character: \\\"\\b\\\" form feed character: \\\"\\f\\\"",
+        expectedOutput: "Fizz & buzz\n\ttabbed newline\rcarriage returned line\nbackspace character: \"\" form feed character: \"\"",
+        recipeConfig: [
+            {
+                "op": "Unescape string",
+                "args": []
+            }
+        ],
+    },
 ]);

+ 5 - 2
webpack.config.js

@@ -12,10 +12,10 @@ const ExtractTextPlugin = require("extract-text-webpack-plugin");
 const banner = `/**
  * CyberChef - The Cyber Swiss Army Knife
  *
- * @copyright Crown Copyright 2016
+ * @copyright Crown Copyright 2017
  * @license Apache-2.0
  *
- *   Copyright 2016 Crown Copyright
+ *   Copyright 2017 Crown Copyright
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -110,4 +110,7 @@ module.exports = {
         modules: false,
         warningsFilter: /source-map/,
     },
+    node: {
+        fs: "empty"
+    }
 };

部分文件因为文件数量过多而无法显示