Merge remote-tracking branch 'origin/master' into 181-responsive-ui

This commit is contained in:
Matt C 2026-02-06 10:54:55 -05:00
commit 9d3567e0e6
135 changed files with 12226 additions and 12211 deletions

View File

@ -0,0 +1,41 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/javascript-node
{
"name": "CyberChef",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/javascript-node:1-18-bookworm",
// Features to add to the dev container. More info: https://containers.dev/features.
"features": {
"ghcr.io/devcontainers/features/github-cli": "latest"
},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [8080],
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": {
"npm": "bash -c \"sudo chown node node_modules && npm install\""
},
"containerEnv": {
"DISPLAY": ":99"
},
"mounts": [
"source=${localWorkspaceFolderBasename}-node_modules,target=${containerWorkspaceFolder}/node_modules,type=volume"
],
// Configure tool-specific properties.
"customizations": {
"vscode": {
"extensions": [
"dbaeumer.vscode-eslint",
"GitHub.vscode-github-actions"
]
}
}
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}

2
.dockerignore Normal file
View File

@ -0,0 +1,2 @@
node_modules
build

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
* text=auto eol=lf

View File

@ -33,6 +33,20 @@ jobs:
if: success() if: success()
run: npx grunt prod run: npx grunt prod
- name: Production Image Build
if: success()
id: build-image
uses: redhat-actions/buildah-build@v2
with:
# Not being uploaded to any registry, use a simple name to allow Buildah to build correctly.
image: cyberchef
containerfiles: ./Dockerfile
platforms: linux/amd64
oci: true
# Webpack seems to use a lot of open files, increase the max open file limit to accomodate.
extra-args: |
--ulimit nofile=10000
- name: UI Tests - name: UI Tests
if: success() if: success()
run: | run: |

View File

@ -6,6 +6,12 @@ on:
tags: tags:
- 'v*' - 'v*'
env:
REGISTRY: ghcr.io
REGISTRY_USER: ${{ github.actor }}
REGISTRY_PASSWORD: ${{ github.token }}
IMAGE_NAME: ${{ github.repository }}
jobs: jobs:
main: main:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -19,7 +25,7 @@ jobs:
- name: Install - name: Install
run: | run: |
npm install npm ci
npm run setheapsize npm run setheapsize
- name: Lint - name: Lint
@ -31,17 +37,38 @@ jobs:
npm run testnodeconsumer npm run testnodeconsumer
- name: Production Build - name: Production Build
if: success()
run: npx grunt prod run: npx grunt prod
- name: UI Tests - name: UI Tests
if: success()
run: | run: |
sudo apt-get install xvfb sudo apt-get install xvfb
xvfb-run --server-args="-screen 0 1200x800x24" npx grunt testui xvfb-run --server-args="-screen 0 1200x800x24" npx grunt testui
- name: Image Metadata
id: image-metadata
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=semver,pattern={{major}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{version}}
- name: Production Image Build
id: build-image
uses: redhat-actions/buildah-build@v2
with:
tags: ${{ steps.image-metadata.outputs.tags }}
labels: ${{ steps.image-metadata.outputs.labels }}
containerfiles: ./Dockerfile
platforms: linux/amd64
oci: true
# Webpack seems to use a lot of open files, increase the max open file limit to accomodate.
extra-args: |
--ulimit nofile=10000
- name: Upload Release Assets - name: Upload Release Assets
if: success()
id: upload-release-assets id: upload-release-assets
uses: svenstaro/upload-release-action@v2 uses: svenstaro/upload-release-action@v2
with: with:
@ -53,7 +80,14 @@ jobs:
body: "See the [CHANGELOG](https://github.com/gchq/CyberChef/blob/master/CHANGELOG.md) and [commit messages](https://github.com/gchq/CyberChef/commits/master) for details." body: "See the [CHANGELOG](https://github.com/gchq/CyberChef/blob/master/CHANGELOG.md) and [commit messages](https://github.com/gchq/CyberChef/commits/master) for details."
- name: Publish to NPM - name: Publish to NPM
if: success()
uses: JS-DevTools/npm-publish@v1 uses: JS-DevTools/npm-publish@v1
with: with:
token: ${{ secrets.NPM_TOKEN }} token: ${{ secrets.NPM_TOKEN }}
- name: Publish to GHCR
uses: redhat-actions/push-to-registry@v2
with:
tags: ${{ steps.build-image.outputs.tags }}
registry: ${{ env.REGISTRY }}
username: ${{ env.REGISTRY_USER }}
password: ${{ env.REGISTRY_PASSWORD }}

1
.gitignore vendored
View File

@ -3,6 +3,7 @@ npm-debug.log
travis.log travis.log
build build
.vscode .vscode
.idea
.*.swp .*.swp
src/core/config/modules/* src/core/config/modules/*
src/core/config/OperationConfig.json src/core/config/OperationConfig.json

View File

@ -13,6 +13,70 @@ All major and minor version changes will be documented in this file. Details of
## Details ## Details
### [10.18.0] - 2024-04-24
- Added 'XXTEA Encrypt' and 'XXTEA Decrypt' operations [@n1474335] | [0a353ee]
### [10.17.0] - 2024-04-13
- Fix unit test 'expectOutput' implementation [@zb3] | [#1783]
- Add accessibility labels for icons [@e218736] | [#1743]
- Add focus styling for keyboard navigation [@e218736] | [#1739]
- Add support for operation option hiding [@TheZ3ro] | [#541]
- Improve efficiency of RAKE implementation [@sw5678] | [#1751]
- Require (a, 26) to be coprime in 'Affine Encode' [@EvieHarv] | [#1788]
- Added 'JWK to PEM' operation [@cplussharp] | [#1277]
- Added 'PEM to JWK' operation [@cplussharp] | [#1277]
- Added 'Public Key from Certificate' operation [@cplussharp] | [#1642]
- Added 'Public Key from Private Key' operation [@cplussharp] | [#1642]
### [10.16.0] - 2024-04-12
- Added 'JA4Server Fingerprint' operation [@n1474335] | [#1789]
### [10.15.0] - 2024-04-02
- Fix Ciphersaber2 key concatenation [@zb3] | [#1765]
- Fix DeriveEVPKey's array parsing [@zb3] | [#1767]
- Fix JWT operations [@a3957273] | [#1769]
- Added 'Parse Certificate Signing Request' operation [@jkataja] | [#1504]
- Added 'Extract Hash Values' operation [@MShwed] | [#512]
- Added 'DateTime Delta' operation [@tomgond] | [#1732]
### [10.14.0] - 2024-03-31
- Added 'To Float' and 'From Float' operations [@tcode2k16] | [#1762]
- Fix ChaCha raw export option [@joostrijneveld] | [#1606]
- Update x86 disassembler vendor library [@evanreichard] | [#1197]
- Allow variable Blowfish key sizes [@cbeuw] | [#933]
- Added 'XXTEA' operation [@devcydo] | [#1361]
### [10.13.0] - 2024-03-30
- Added 'FangURL' operation [@breakersall] [@arnydo] | [#1591] [#654]
### [10.12.0] - 2024-03-29
- Added 'Salsa20' and 'XSalsa20' operation [@joostrijneveld] | [#1750]
### [10.11.0] - 2024-03-29
- Add HEIC/HEIF file signatures [@simonw] | [#1757]
- Update xmldom to fix medium security vulnerability [@chriswhite199] | [#1752]
- Update JSONWebToken to fix medium security vulnerability [@chriswhite199] | [#1753]
### [10.10.0] - 2024-03-27
- Added 'JA4 Fingerprint' operation [@n1474335] | [#1759]
### [10.9.0] - 2024-03-26
- Line ending sequences and UTF-8 character encoding are now detected automatically [@n1474335] | [65ffd8d]
### [10.8.0] - 2024-02-13
- Add official Docker images [@AshCorr] | [#1699]
### [10.7.0] - 2024-02-09
- Added 'File Tree' operation [@sw5678] | [#1667]
- Added 'RISON' operation [@sg5506844] | [#1555]
- Added 'MurmurHash3' operation [@AliceGrey] | [#1694]
### [10.6.0] - 2024-02-03
- Updated 'Forensics Wiki' URLs to new domain [@a3957273] | [#1703]
- Added 'LZNT1 Decompress' operation [@0xThiebaut] | [#1675]
- Updated 'Regex Expression' UUID matcher [@cnotin] | [#1678]
- Removed duplicate 'hover' message within baking info [@KevinSJ] | [#1541]
### [10.5.0] - 2023-07-14 ### [10.5.0] - 2023-07-14
- Added GOST Encrypt, Decrypt, Sign, Verify, Key Wrap, and Key Unwrap operations [@n1474335] | [#592] - Added GOST Encrypt, Decrypt, Sign, Verify, Key Wrap, and Key Unwrap operations [@n1474335] | [#592]
@ -372,8 +436,19 @@ All major and minor version changes will be documented in this file. Details of
## [4.0.0] - 2016-11-28 ## [4.0.0] - 2016-11-28
- Initial open source commit [@n1474335] | [b1d73a72](https://github.com/gchq/CyberChef/commit/b1d73a725dc7ab9fb7eb789296efd2b7e4b08306) - Initial open source commit [@n1474335] | [b1d73a72](https://github.com/gchq/CyberChef/commit/b1d73a725dc7ab9fb7eb789296efd2b7e4b08306)
[10.18.0]: https://github.com/gchq/CyberChef/releases/tag/v10.18.0
[10.17.0]: https://github.com/gchq/CyberChef/releases/tag/v10.17.0
[10.16.0]: https://github.com/gchq/CyberChef/releases/tag/v10.16.0
[10.15.0]: https://github.com/gchq/CyberChef/releases/tag/v10.15.0
[10.14.0]: https://github.com/gchq/CyberChef/releases/tag/v10.14.0
[10.13.0]: https://github.com/gchq/CyberChef/releases/tag/v10.13.0
[10.12.0]: https://github.com/gchq/CyberChef/releases/tag/v10.12.0
[10.11.0]: https://github.com/gchq/CyberChef/releases/tag/v10.11.0
[10.10.0]: https://github.com/gchq/CyberChef/releases/tag/v10.10.0
[10.9.0]: https://github.com/gchq/CyberChef/releases/tag/v10.9.0
[10.8.0]: https://github.com/gchq/CyberChef/releases/tag/v10.7.0
[10.7.0]: https://github.com/gchq/CyberChef/releases/tag/v10.7.0
[10.6.0]: https://github.com/gchq/CyberChef/releases/tag/v10.6.0
[10.5.0]: https://github.com/gchq/CyberChef/releases/tag/v10.5.0 [10.5.0]: https://github.com/gchq/CyberChef/releases/tag/v10.5.0
[10.4.0]: https://github.com/gchq/CyberChef/releases/tag/v10.4.0 [10.4.0]: https://github.com/gchq/CyberChef/releases/tag/v10.4.0
[10.3.0]: https://github.com/gchq/CyberChef/releases/tag/v10.3.0 [10.3.0]: https://github.com/gchq/CyberChef/releases/tag/v10.3.0
@ -528,6 +603,27 @@ All major and minor version changes will be documented in this file. Details of
[@joostrijneveld]: https://github.com/joostrijneveld [@joostrijneveld]: https://github.com/joostrijneveld
[@Xenonym]: https://github.com/Xenonym [@Xenonym]: https://github.com/Xenonym
[@gchq77703]: https://github.com/gchq77703 [@gchq77703]: https://github.com/gchq77703
[@a3957273]: https://github.com/a3957273
[@0xThiebaut]: https://github.com/0xThiebaut
[@cnotin]: https://github.com/cnotin
[@KevinSJ]: https://github.com/KevinSJ
[@sw5678]: https://github.com/sw5678
[@sg5506844]: https://github.com/sg5506844
[@AliceGrey]: https://github.com/AliceGrey
[@AshCorr]: https://github.com/AshCorr
[@simonw]: https://github.com/simonw
[@chriswhite199]: https://github.com/chriswhite199
[@breakersall]: https://github.com/breakersall
[@evanreichard]: https://github.com/evanreichard
[@devcydo]: https://github.com/devcydo
[@zb3]: https://github.com/zb3
[@jkataja]: https://github.com/jkataja
[@tomgond]: https://github.com/tomgond
[@e218736]: https://github.com/e218736
[@TheZ3ro]: https://github.com/TheZ3ro
[@EvieHarv]: https://github.com/EvieHarv
[@cplussharp]: https://github.com/cplussharp
[8ad18b]: https://github.com/gchq/CyberChef/commit/8ad18bc7db6d9ff184ba3518686293a7685bf7b7 [8ad18b]: https://github.com/gchq/CyberChef/commit/8ad18bc7db6d9ff184ba3518686293a7685bf7b7
[9a33498]: https://github.com/gchq/CyberChef/commit/9a33498fed26a8df9c9f35f39a78a174bf50a513 [9a33498]: https://github.com/gchq/CyberChef/commit/9a33498fed26a8df9c9f35f39a78a174bf50a513
@ -537,6 +633,8 @@ All major and minor version changes will be documented in this file. Details of
[a895d1d]: https://github.com/gchq/CyberChef/commit/a895d1d82a2f92d440a0c5eca2bc7c898107b737 [a895d1d]: https://github.com/gchq/CyberChef/commit/a895d1d82a2f92d440a0c5eca2bc7c898107b737
[31a7f83]: https://github.com/gchq/CyberChef/commit/31a7f83b82e78927f89689f323fcb9185144d6ff [31a7f83]: https://github.com/gchq/CyberChef/commit/31a7f83b82e78927f89689f323fcb9185144d6ff
[760eff4]: https://github.com/gchq/CyberChef/commit/760eff49b5307aaa3104c5e5b437ffe62299acd1 [760eff4]: https://github.com/gchq/CyberChef/commit/760eff49b5307aaa3104c5e5b437ffe62299acd1
[65ffd8d]: https://github.com/gchq/CyberChef/commit/65ffd8d65d88eb369f6f61a5d1d0f807179bffb7
[0a353ee]: https://github.com/gchq/CyberChef/commit/0a353eeb378b9ca5d49e23c7dfc175ae07107b08
[#95]: https://github.com/gchq/CyberChef/pull/299 [#95]: https://github.com/gchq/CyberChef/pull/299
[#173]: https://github.com/gchq/CyberChef/pull/173 [#173]: https://github.com/gchq/CyberChef/pull/173
@ -646,4 +744,31 @@ All major and minor version changes will be documented in this file. Details of
[#661]: https://github.com/gchq/CyberChef/pull/661 [#661]: https://github.com/gchq/CyberChef/pull/661
[#493]: https://github.com/gchq/CyberChef/pull/493 [#493]: https://github.com/gchq/CyberChef/pull/493
[#592]: https://github.com/gchq/CyberChef/issues/592 [#592]: https://github.com/gchq/CyberChef/issues/592
[#1703]: https://github.com/gchq/CyberChef/issues/1703
[#1675]: https://github.com/gchq/CyberChef/issues/1675
[#1678]: https://github.com/gchq/CyberChef/issues/1678
[#1541]: https://github.com/gchq/CyberChef/issues/1541
[#1667]: https://github.com/gchq/CyberChef/issues/1667
[#1555]: https://github.com/gchq/CyberChef/issues/1555
[#1694]: https://github.com/gchq/CyberChef/issues/1694
[#1699]: https://github.com/gchq/CyberChef/issues/1699
[#1757]: https://github.com/gchq/CyberChef/issues/1757
[#1752]: https://github.com/gchq/CyberChef/issues/1752
[#1753]: https://github.com/gchq/CyberChef/issues/1753
[#1750]: https://github.com/gchq/CyberChef/issues/1750
[#1591]: https://github.com/gchq/CyberChef/issues/1591
[#654]: https://github.com/gchq/CyberChef/issues/654
[#1762]: https://github.com/gchq/CyberChef/issues/1762
[#1606]: https://github.com/gchq/CyberChef/issues/1606
[#1197]: https://github.com/gchq/CyberChef/issues/1197
[#933]: https://github.com/gchq/CyberChef/issues/933
[#1361]: https://github.com/gchq/CyberChef/issues/1361
[#1765]: https://github.com/gchq/CyberChef/issues/1765
[#1767]: https://github.com/gchq/CyberChef/issues/1767
[#1769]: https://github.com/gchq/CyberChef/issues/1769
[#1759]: https://github.com/gchq/CyberChef/issues/1759
[#1504]: https://github.com/gchq/CyberChef/issues/1504
[#512]: https://github.com/gchq/CyberChef/issues/512
[#1732]: https://github.com/gchq/CyberChef/issues/1732
[#1789]: https://github.com/gchq/CyberChef/issues/1789

9
Dockerfile Normal file
View File

@ -0,0 +1,9 @@
FROM node:18-alpine AS build
COPY . .
RUN npm ci
RUN npm run build
FROM nginx:1.25-alpine3.18 AS cyberchef
COPY --from=build ./build/prod /usr/share/nginx/html/

View File

@ -86,10 +86,12 @@ module.exports = function (grunt) {
// Project configuration // Project configuration
const compileTime = grunt.template.today("UTC:dd/mm/yyyy HH:MM:ss") + " UTC", const compileYear = grunt.template.today("UTC:yyyy"),
compileTime = grunt.template.today("UTC:dd/mm/yyyy HH:MM:ss") + " UTC",
pkg = grunt.file.readJSON("package.json"), pkg = grunt.file.readJSON("package.json"),
webpackConfig = require("./webpack.config.js"), webpackConfig = require("./webpack.config.js"),
BUILD_CONSTANTS = { BUILD_CONSTANTS = {
COMPILE_YEAR: JSON.stringify(compileYear),
COMPILE_TIME: JSON.stringify(compileTime), COMPILE_TIME: JSON.stringify(compileTime),
COMPILE_MSG: JSON.stringify(grunt.option("compile-msg") || grunt.option("msg") || ""), COMPILE_MSG: JSON.stringify(grunt.option("compile-msg") || grunt.option("msg") || ""),
PKG_VERSION: JSON.stringify(pkg.version), PKG_VERSION: JSON.stringify(pkg.version),
@ -125,6 +127,7 @@ module.exports = function (grunt) {
filename: "index.html", filename: "index.html",
template: "./src/web/html/index.html", template: "./src/web/html/index.html",
chunks: ["main"], chunks: ["main"],
compileYear: compileYear,
compileTime: compileTime, compileTime: compileTime,
version: pkg.version, version: pkg.version,
minify: { minify: {
@ -228,6 +231,7 @@ module.exports = function (grunt) {
filename: "index.html", filename: "index.html",
template: "./src/web/html/index.html", template: "./src/web/html/index.html",
chunks: ["main"], chunks: ["main"],
compileYear: compileYear,
compileTime: compileTime, compileTime: compileTime,
version: pkg.version, version: pkg.version,
}) })

View File

@ -20,6 +20,22 @@ Cryptographic operations in CyberChef should not be relied upon to provide secur
[A live demo can be found here][1] - have fun! [A live demo can be found here][1] - have fun!
## Containers
If you would like to try out CyberChef locally you can either build it yourself:
```bash
docker build --tag cyberchef --ulimit nofile=10000 .
docker run -it -p 8080:80 cyberchef
```
Or you can use our image directly:
```bash
docker run -it -p 8080:80 ghcr.io/gchq/cyberchef:latest
```
This image is built and published through our [GitHub Workflows](.github/workflows/releases.yml)
## How it works ## How it works

15236
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "cyberchef", "name": "cyberchef",
"version": "10.5.2", "version": "10.18.6",
"description": "The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.", "description": "The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.",
"author": "n1474335 <n1474335@gmail.com>", "author": "n1474335 <n1474335@gmail.com>",
"homepage": "https://gchq.github.io/CyberChef", "homepage": "https://gchq.github.io/CyberChef",
@ -39,55 +39,55 @@
"node >= 16" "node >= 16"
], ],
"devDependencies": { "devDependencies": {
"@babel/core": "^7.21.0", "@babel/core": "^7.23.9",
"@babel/eslint-parser": "^7.19.1", "@babel/eslint-parser": "^7.23.10",
"@babel/plugin-syntax-import-assertions": "^7.20.0", "@babel/plugin-syntax-import-assertions": "^7.23.3",
"@babel/plugin-transform-runtime": "^7.21.0", "@babel/plugin-transform-runtime": "^7.23.9",
"@babel/preset-env": "^7.20.2", "@babel/preset-env": "^7.23.9",
"@babel/runtime": "^7.21.0", "@babel/runtime": "^7.23.9",
"@codemirror/commands": "^6.2.1", "@codemirror/commands": "^6.3.3",
"@codemirror/language": "^6.6.0", "@codemirror/language": "^6.10.1",
"@codemirror/search": "^6.2.3", "@codemirror/search": "^6.5.5",
"@codemirror/state": "^6.2.0", "@codemirror/state": "^6.4.0",
"@codemirror/view": "^6.9.2", "@codemirror/view": "^6.23.1",
"autoprefixer": "^10.4.13", "autoprefixer": "^10.4.17",
"babel-loader": "^9.1.2", "babel-loader": "^9.1.3",
"babel-plugin-dynamic-import-node": "^2.3.3", "babel-plugin-dynamic-import-node": "^2.3.3",
"babel-plugin-transform-builtin-extend": "1.1.2", "babel-plugin-transform-builtin-extend": "1.1.2",
"base64-loader": "^1.0.0", "base64-loader": "^1.0.0",
"chromedriver": "^121.0.0", "chromedriver": "^123.0.4",
"cli-progress": "^3.12.0", "cli-progress": "^3.12.0",
"colors": "^1.4.0", "colors": "^1.4.0",
"copy-webpack-plugin": "^11.0.0", "copy-webpack-plugin": "^12.0.2",
"core-js": "^3.29.0", "core-js": "^3.35.1",
"css-loader": "6.7.3", "css-loader": "6.10.0",
"eslint": "^8.35.0", "eslint": "^8.56.0",
"grunt": "^1.6.1", "grunt": "^1.6.1",
"grunt-chmod": "~1.1.1", "grunt-chmod": "~1.1.1",
"grunt-concurrent": "^3.0.0", "grunt-concurrent": "^3.0.0",
"grunt-contrib-clean": "~2.0.1", "grunt-contrib-clean": "~2.0.1",
"grunt-contrib-connect": "^3.0.0", "grunt-contrib-connect": "^4.0.0",
"grunt-contrib-copy": "~1.0.0", "grunt-contrib-copy": "~1.0.0",
"grunt-contrib-watch": "^1.1.0", "grunt-contrib-watch": "^1.1.0",
"grunt-eslint": "^24.0.1", "grunt-eslint": "^24.3.0",
"grunt-exec": "~3.0.0", "grunt-exec": "~3.0.0",
"grunt-webpack": "^5.0.0", "grunt-webpack": "^6.0.0",
"grunt-zip": "^0.20.0", "grunt-zip": "^1.0.0",
"html-webpack-plugin": "^5.5.0", "html-webpack-plugin": "^5.6.0",
"imports-loader": "^4.0.1", "imports-loader": "^5.0.0",
"mini-css-extract-plugin": "2.7.3", "mini-css-extract-plugin": "2.8.0",
"modify-source-webpack-plugin": "^3.0.0", "modify-source-webpack-plugin": "^3.0.0",
"nightwatch": "^2.6.16", "nightwatch": "^3.4.0",
"postcss": "^8.4.21", "postcss": "^8.4.33",
"postcss-css-variables": "^0.18.0", "postcss-css-variables": "^0.19.0",
"postcss-import": "^15.1.0", "postcss-import": "^16.0.0",
"postcss-loader": "^7.0.2", "postcss-loader": "^8.1.0",
"prompt": "^1.3.0", "prompt": "^1.3.0",
"sitemap": "^7.1.1", "sitemap": "^7.1.1",
"terser": "^5.16.6", "terser": "^5.27.0",
"webpack": "^5.76.0", "webpack": "^5.90.1",
"webpack-bundle-analyzer": "^4.8.0", "webpack-bundle-analyzer": "^4.10.1",
"webpack-dev-server": "4.11.1", "webpack-dev-server": "4.15.1",
"webpack-node-externals": "^3.0.0", "webpack-node-externals": "^3.0.0",
"worker-loader": "^3.0.8" "worker-loader": "^3.0.8"
}, },
@ -96,11 +96,12 @@
"@babel/polyfill": "^7.12.1", "@babel/polyfill": "^7.12.1",
"@blu3r4y/lzma": "^2.3.3", "@blu3r4y/lzma": "^2.3.3",
"@wavesenterprise/crypto-gost-js": "^2.1.0-RC1", "@wavesenterprise/crypto-gost-js": "^2.1.0-RC1",
"@xmldom/xmldom": "^0.8.0",
"argon2-browser": "^1.18.0", "argon2-browser": "^1.18.0",
"arrive": "^2.4.1", "arrive": "^2.4.1",
"avsc": "^5.7.7", "avsc": "^5.7.7",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"bignumber.js": "^9.1.1", "bignumber.js": "^9.1.2",
"blakejs": "^1.2.1", "blakejs": "^1.2.1",
"bootstrap": "4.6.2", "bootstrap": "4.6.2",
"bootstrap-colorpicker": "^3.4.0", "bootstrap-colorpicker": "^3.4.0",
@ -108,45 +109,47 @@
"browserify-zlib": "^0.2.0", "browserify-zlib": "^0.2.0",
"bson": "^4.7.2", "bson": "^4.7.2",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"cbor": "8.1.0", "cbor": "9.0.2",
"chi-squared": "^1.1.0", "chi-squared": "^1.1.0",
"codepage": "^1.15.0", "codepage": "^1.15.0",
"crypto-api": "^0.8.5", "crypto-api": "^0.8.5",
"crypto-browserify": "^3.12.0", "crypto-browserify": "^3.12.0",
"crypto-js": "^4.1.1", "crypto-js": "^4.2.0",
"ctph.js": "0.0.5", "ctph.js": "0.0.5",
"d3": "7.8.2", "d3": "7.8.5",
"d3-hexbin": "^0.2.2", "d3-hexbin": "^0.2.2",
"diff": "^5.1.0", "diff": "^5.1.0",
"es6-promisify": "^7.0.0", "es6-promisify": "^7.0.0",
"escodegen": "^2.0.0", "escodegen": "^2.1.0",
"esprima": "^4.0.1", "esprima": "^4.0.1",
"exif-parser": "^0.1.12", "exif-parser": "^0.1.12",
"fernet": "^0.3.2",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"flat": "^5.0.2", "flat": "^6.0.1",
"geodesy": "1.1.3", "geodesy": "1.1.3",
"highlight.js": "^11.7.0", "highlight.js": "^11.9.0",
"ieee754": "^1.1.13",
"jimp": "^0.16.13", "jimp": "^0.16.13",
"jquery": "3.6.4", "jquery": "3.7.1",
"js-crc": "^0.2.0", "js-crc": "^0.2.0",
"js-sha3": "^0.8.0", "js-sha3": "^0.9.3",
"jsesc": "^3.0.2", "jsesc": "^3.0.2",
"json5": "^2.2.3", "json5": "^2.2.3",
"jsonpath-plus": "^7.2.0", "jsonpath-plus": "^8.0.0",
"jsonwebtoken": "8.5.1", "jsonwebtoken": "8.5.1",
"jsqr": "^1.4.0", "jsqr": "^1.4.0",
"jsrsasign": "^10.6.1", "jsrsasign": "^11.1.0",
"kbpgp": "2.1.15", "kbpgp": "2.1.15",
"libbzip2-wasm": "0.0.4", "libbzip2-wasm": "0.0.4",
"libyara-wasm": "^1.2.1", "libyara-wasm": "^1.2.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"loglevel": "^1.8.1", "loglevel": "^1.9.1",
"loglevel-message-prefix": "^3.0.0", "loglevel-message-prefix": "^3.0.0",
"lz-string": "^1.5.0", "lz-string": "^1.5.0",
"lz4js": "^0.2.0", "lz4js": "^0.2.0",
"markdown-it": "^13.0.1", "markdown-it": "^14.0.0",
"moment": "^2.29.4", "moment": "^2.30.1",
"moment-timezone": "^0.5.41", "moment-timezone": "^0.5.44",
"ngeohash": "^0.6.3", "ngeohash": "^0.6.3",
"node-forge": "^1.3.1", "node-forge": "^1.3.1",
"node-md6": "^0.1.0", "node-md6": "^0.1.0",
@ -158,22 +161,22 @@
"path": "^0.12.7", "path": "^0.12.7",
"popper.js": "^1.16.1", "popper.js": "^1.16.1",
"process": "^0.11.10", "process": "^0.11.10",
"protobufjs": "^7.2.2", "protobufjs": "^7.2.6",
"qr-image": "^3.2.0", "qr-image": "^3.2.0",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.2.1",
"rison": "^0.1.1",
"scryptsy": "^2.1.0", "scryptsy": "^2.1.0",
"snackbarjs": "^1.1.0", "snackbarjs": "^1.1.0",
"sortablejs": "^1.15.0", "sortablejs": "^1.15.2",
"split.js": "^1.6.5", "split.js": "^1.6.5",
"ssdeep.js": "0.0.3", "ssdeep.js": "0.0.3",
"stream-browserify": "^3.0.0", "stream-browserify": "^3.0.0",
"tesseract.js": "3.0.3", "tesseract.js": "5.0.4",
"ua-parser-js": "^1.0.34", "ua-parser-js": "^1.0.37",
"unorm": "^1.6.0", "unorm": "^1.6.0",
"utf8": "^3.0.0", "utf8": "^3.0.0",
"vkbeautify": "^0.99.3", "vkbeautify": "^0.99.3",
"xmldom": "^0.6.0", "xpath": "0.0.34",
"xpath": "0.0.32",
"xregexp": "^5.1.1", "xregexp": "^5.1.1",
"zlibjs": "^0.3.1" "zlibjs": "^0.3.1"
}, },

View File

@ -892,6 +892,23 @@ class Utils {
} }
/**
* Converts a string to its title case equivalent.
*
* @param {string} str
* @returns string
*
* @example
* // return "A Tiny String"
* Utils.toTitleCase("a tIny String");
*/
static toTitleCase(str) {
return str.replace(/\w\S*/g, function(txt) {
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
});
}
/** /**
* Encodes a URI fragment (#) or query (?) using a minimal amount of percent-encoding. * Encodes a URI fragment (#) or query (?) using a minimal amount of percent-encoding.
* *

View File

@ -14,6 +14,8 @@
"From Charcode", "From Charcode",
"To Decimal", "To Decimal",
"From Decimal", "From Decimal",
"To Float",
"From Float",
"To Binary", "To Binary",
"From Binary", "From Binary",
"To Octal", "To Octal",
@ -29,6 +31,8 @@
"To Base64", "To Base64",
"From Base64", "From Base64",
"Show Base64 offsets", "Show Base64 offsets",
"To Base92",
"From Base92",
"To Base85", "To Base85",
"From Base85", "From Base85",
"To Base", "To Base",
@ -67,7 +71,10 @@
"JSON to CSV", "JSON to CSV",
"Avro to JSON", "Avro to JSON",
"CBOR Encode", "CBOR Encode",
"CBOR Decode" "CBOR Decode",
"Caret/M-decode",
"Rison Encode",
"Rison Decode"
] ]
}, },
{ {
@ -81,6 +88,8 @@
"DES Decrypt", "DES Decrypt",
"Triple DES Encrypt", "Triple DES Encrypt",
"Triple DES Decrypt", "Triple DES Decrypt",
"Fernet Encrypt",
"Fernet Decrypt",
"LS47 Encrypt", "LS47 Encrypt",
"LS47 Decrypt", "LS47 Decrypt",
"RC2 Encrypt", "RC2 Encrypt",
@ -88,6 +97,8 @@
"RC4", "RC4",
"RC4 Drop", "RC4 Drop",
"ChaCha", "ChaCha",
"Salsa20",
"XSalsa20",
"Rabbit", "Rabbit",
"SM4 Encrypt", "SM4 Encrypt",
"SM4 Decrypt", "SM4 Decrypt",
@ -106,6 +117,8 @@
"XOR Brute Force", "XOR Brute Force",
"Vigenère Encode", "Vigenère Encode",
"Vigenère Decode", "Vigenère Decode",
"XXTEA Encrypt",
"XXTEA Decrypt",
"To Morse Code", "To Morse Code",
"From Morse Code", "From Morse Code",
"Bacon Cipher Encode", "Bacon Cipher Encode",
@ -156,6 +169,8 @@
"Hex to PEM", "Hex to PEM",
"Hex to Object Identifier", "Hex to Object Identifier",
"Object Identifier to Hex", "Object Identifier to Hex",
"PEM to JWK",
"JWK to PEM",
"Generate PGP Key Pair", "Generate PGP Key Pair",
"PGP Encrypt", "PGP Encrypt",
"PGP Decrypt", "PGP Decrypt",
@ -167,7 +182,14 @@
"RSA Verify", "RSA Verify",
"RSA Encrypt", "RSA Encrypt",
"RSA Decrypt", "RSA Decrypt",
"Parse SSH Host Key" "Generate ECDSA Key Pair",
"ECDSA Signature Conversion",
"ECDSA Sign",
"ECDSA Verify",
"Parse SSH Host Key",
"Parse CSR",
"Public Key from Certificate",
"Public Key from Private Key"
] ]
}, },
{ {
@ -224,6 +246,8 @@
"VarInt Decode", "VarInt Decode",
"JA3 Fingerprint", "JA3 Fingerprint",
"JA3S Fingerprint", "JA3S Fingerprint",
"JA4 Fingerprint",
"JA4Server Fingerprint",
"HASSH Client Fingerprint", "HASSH Client Fingerprint",
"HASSH Server Fingerprint", "HASSH Server Fingerprint",
"Format MAC addresses", "Format MAC addresses",
@ -232,6 +256,7 @@
"Encode NetBIOS Name", "Encode NetBIOS Name",
"Decode NetBIOS Name", "Decode NetBIOS Name",
"Defang URL", "Defang URL",
"Fang URL",
"Defang IP Addresses" "Defang IP Addresses"
] ]
}, },
@ -294,7 +319,8 @@
"Escape string", "Escape string",
"Unescape string", "Unescape string",
"Pseudo-Random Number Generator", "Pseudo-Random Number Generator",
"Sleep" "Sleep",
"File Tree"
] ]
}, },
{ {
@ -306,6 +332,7 @@
"To UNIX Timestamp", "To UNIX Timestamp",
"Windows Filetime to UNIX Timestamp", "Windows Filetime to UNIX Timestamp",
"UNIX Timestamp to Windows Filetime", "UNIX Timestamp to Windows Filetime",
"DateTime Delta",
"Extract dates", "Extract dates",
"Get Time", "Get Time",
"Sleep" "Sleep"
@ -322,13 +349,15 @@
"Extract domains", "Extract domains",
"Extract file paths", "Extract file paths",
"Extract dates", "Extract dates",
"Extract hashes",
"Regular expression", "Regular expression",
"XPath expression", "XPath expression",
"JPath expression", "JPath expression",
"CSS selector", "CSS selector",
"Extract EXIF", "Extract EXIF",
"Extract ID3", "Extract ID3",
"Extract Files" "Extract Files",
"RAKE"
] ]
}, },
{ {
@ -351,7 +380,8 @@
"LZMA Decompress", "LZMA Decompress",
"LZMA Compress", "LZMA Compress",
"LZ4 Decompress", "LZ4 Decompress",
"LZ4 Compress" "LZ4 Compress",
"LZNT1 Decompress"
] ]
}, },
{ {
@ -392,6 +422,7 @@
"Scrypt", "Scrypt",
"NT Hash", "NT Hash",
"LM Hash", "LM Hash",
"MurmurHash3",
"Fletcher-8 Checksum", "Fletcher-8 Checksum",
"Fletcher-16 Checksum", "Fletcher-16 Checksum",
"Fletcher-32 Checksum", "Fletcher-32 Checksum",

View File

@ -147,7 +147,7 @@ class ${moduleName} extends Operation {
this.name = "${result.opName}"; this.name = "${result.opName}";
this.module = "${result.module}"; this.module = "${result.module}";
this.description = "${(new EscapeString).run(result.description, ["Special chars", "Double"])}"; this.description = "${(new EscapeString).run(result.description, ["Special chars", "Double"])}";
this.infoURL = "${result.infoURL}"; this.infoURL = "${result.infoURL}"; // Usually a Wikipedia link. Remember to remove localisation (i.e. https://wikipedia.org/etc rather than https://en.wikipedia.org/etc)
this.inputType = "${result.inputType}"; this.inputType = "${result.inputType}";
this.outputType = "${result.outputType}"; this.outputType = "${result.outputType}";
this.args = [ this.args = [

44
src/core/lib/Base92.mjs Normal file
View File

@ -0,0 +1,44 @@
/**
* Base92 resources.
*
* @author sg5506844 [sg5506844@gmail.com]
* @copyright Crown Copyright 2021
* @license Apache-2.0
*/
import OperationError from "../errors/OperationError.mjs";
/**
* Base92 alphabet char
*
* @param {number} val
* @returns {number}
*/
export function base92Chr(val) {
if (val < 0 || val >= 91) {
throw new OperationError("Invalid value");
}
if (val === 0)
return "!".charCodeAt(0);
else if (val <= 61)
return "#".charCodeAt(0) + val - 1;
else
return "a".charCodeAt(0) + val - 62;
}
/**
* Base92 alphabet ord
*
* @param {string} val
* @returns {number}
*/
export function base92Ord(val) {
if (val === "!")
return 0;
else if ("#" <= val && val <= "_")
return val.charCodeAt(0) - "#".charCodeAt(0) + 1;
else if ("a" <= val && val <= "}")
return val.charCodeAt(0) - "a".charCodeAt(0) + 62;
throw new OperationError(`${val} is not a base92 character`);
}

View File

@ -224,8 +224,85 @@ export function chrEncWidth(page) {
* @copyright Crown Copyright 2019 * @copyright Crown Copyright 2019
* @license Apache-2.0 * @license Apache-2.0
*/ */
export const UNICODE_NORMALISATION_FORMS = ["NFD", "NFC", "NFKD", "NFKC"];
/** /**
* Character encoding format mappings. * Detects whether the input buffer is valid UTF8.
*
* @param {ArrayBuffer} data
* @returns {number} - 0 = not UTF8, 1 = ASCII, 2 = UTF8
*/ */
export const UNICODE_NORMALISATION_FORMS = ["NFD", "NFC", "NFKD", "NFKC"]; export function isUTF8(data) {
const bytes = new Uint8Array(data);
let i = 0;
let onlyASCII = true;
while (i < bytes.length) {
if (( // ASCII
bytes[i] === 0x09 ||
bytes[i] === 0x0A ||
bytes[i] === 0x0D ||
(0x20 <= bytes[i] && bytes[i] <= 0x7E)
)) {
i += 1;
continue;
}
onlyASCII = false;
if (( // non-overlong 2-byte
(0xC2 <= bytes[i] && bytes[i] <= 0xDF) &&
(0x80 <= bytes[i+1] && bytes[i+1] <= 0xBF)
)) {
i += 2;
continue;
}
if (( // excluding overlongs
bytes[i] === 0xE0 &&
(0xA0 <= bytes[i + 1] && bytes[i + 1] <= 0xBF) &&
(0x80 <= bytes[i + 2] && bytes[i + 2] <= 0xBF)
) ||
( // straight 3-byte
((0xE1 <= bytes[i] && bytes[i] <= 0xEC) ||
bytes[i] === 0xEE ||
bytes[i] === 0xEF) &&
(0x80 <= bytes[i + 1] && bytes[i+1] <= 0xBF) &&
(0x80 <= bytes[i+2] && bytes[i+2] <= 0xBF)
) ||
( // excluding surrogates
bytes[i] === 0xED &&
(0x80 <= bytes[i+1] && bytes[i+1] <= 0x9F) &&
(0x80 <= bytes[i+2] && bytes[i+2] <= 0xBF)
)) {
i += 3;
continue;
}
if (( // planes 1-3
bytes[i] === 0xF0 &&
(0x90 <= bytes[i + 1] && bytes[i + 1] <= 0xBF) &&
(0x80 <= bytes[i + 2] && bytes[i + 2] <= 0xBF) &&
(0x80 <= bytes[i + 3] && bytes[i + 3] <= 0xBF)
) ||
( // planes 4-15
(0xF1 <= bytes[i] && bytes[i] <= 0xF3) &&
(0x80 <= bytes[i + 1] && bytes[i + 1] <= 0xBF) &&
(0x80 <= bytes[i + 2] && bytes[i + 2] <= 0xBF) &&
(0x80 <= bytes[i + 3] && bytes[i + 3] <= 0xBF)
) ||
( // plane 16
bytes[i] === 0xF4 &&
(0x80 <= bytes[i + 1] && bytes[i + 1] <= 0x8F) &&
(0x80 <= bytes[i + 2] && bytes[i + 2] <= 0xBF) &&
(0x80 <= bytes[i + 3] && bytes[i + 3] <= 0xBF)
)) {
i += 4;
continue;
}
return 0;
}
return onlyASCII ? 1 : 2;
}

View File

@ -4,7 +4,7 @@
* @license Apache-2.0 * @license Apache-2.0
*/ */
export function encode(tempIVP, key, rounds, input) { export function encode(tempIVP, key, rounds, input) {
const ivp = new Uint8Array(key.concat(tempIVP)); const ivp = new Uint8Array([...key, ...tempIVP]);
const state = new Array(256).fill(0); const state = new Array(256).fill(0);
let j = 0, i = 0; let j = 0, i = 0;
const result = []; const result = [];

View File

@ -3,6 +3,7 @@
* *
* @author Matt C [matt@artemisbot.uk] * @author Matt C [matt@artemisbot.uk]
* @author n1474335 [n1474335@gmail.com] * @author n1474335 [n1474335@gmail.com]
* @author Evie H [evie@evie.sh]
* *
* @copyright Crown Copyright 2018 * @copyright Crown Copyright 2018
* @license Apache-2.0 * @license Apache-2.0
@ -10,6 +11,7 @@
*/ */
import OperationError from "../errors/OperationError.mjs"; import OperationError from "../errors/OperationError.mjs";
import Utils from "../Utils.mjs";
import CryptoJS from "crypto-js"; import CryptoJS from "crypto-js";
/** /**
@ -30,6 +32,10 @@ export function affineEncode(input, args) {
throw new OperationError("The values of a and b can only be integers."); throw new OperationError("The values of a and b can only be integers.");
} }
if (Utils.gcd(a, 26) !== 1) {
throw new OperationError("The value of `a` must be coprime to 26.");
}
for (let i = 0; i < input.length; i++) { for (let i = 0; i < input.length; i++) {
if (alphabet.indexOf(input[i]) >= 0) { if (alphabet.indexOf(input[i]) >= 0) {
// Uses the affine function ax+b % m = y (where m is length of the alphabet) // Uses the affine function ax+b % m = y (where m is length of the alphabet)

View File

@ -72,6 +72,27 @@ export const FILE_SIGNATURES = {
}, },
extractor: extractWEBP extractor: extractWEBP
}, },
{
name: "High Efficiency Image File Format",
extension: "heic,heif",
mime: "image/heif",
description: "",
signature: {
0: 0x00,
1: 0x00,
2: 0x00,
3: [0x24, 0x18],
4: 0x66, // ftypheic
5: 0x74,
6: 0x79,
7: 0x70,
8: 0x68,
9: 0x65,
10: 0x69,
11: 0x63
},
extractor: null
},
{ {
name: "Camera Image File Format", name: "Camera Image File Format",
extension: "crw", extension: "crw",
@ -2727,7 +2748,7 @@ export function extractGIF(bytes, offset) {
stream.moveForwardsBy(11); stream.moveForwardsBy(11);
// Loop until next Graphic Control Extension. // Loop until next Graphic Control Extension.
while (stream.getBytes(2) !== [0x21, 0xf9]) { while (!Array.from(stream.getBytes(2)).equals([0x21, 0xf9])) {
stream.moveBackwardsBy(2); stream.moveBackwardsBy(2);
stream.moveForwardsBy(stream.readInt(1)); stream.moveForwardsBy(stream.readInt(1));
if (!stream.readInt(1)) if (!stream.readInt(1))

264
src/core/lib/JA4.mjs Normal file
View File

@ -0,0 +1,264 @@
/**
* JA4 resources.
*
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2024
* @license Apache-2.0
*
* JA4 Copyright 2023 FoxIO, LLC.
* @license BSD-3-Clause
*/
import OperationError from "../errors/OperationError.mjs";
import { parseTLSRecord, parseHighestSupportedVersion, parseFirstALPNValue } from "./TLS.mjs";
import { toHexFast } from "./Hex.mjs";
import { runHash } from "./Hash.mjs";
import Utils from "../Utils.mjs";
/**
* Calculate the JA4 from a given TLS Client Hello Stream
* @param {Uint8Array} bytes
* @returns {string}
*/
export function toJA4(bytes) {
let tlsr = {};
try {
tlsr = parseTLSRecord(bytes);
if (tlsr.handshake.value.handshakeType.value !== 0x01) {
throw new Error();
}
} catch (err) {
throw new OperationError("Data is not a valid TLS Client Hello. QUIC is not yet supported.\n" + err);
}
/* QUIC
q or t, which denotes whether the hello packet is for QUIC or TCP.
TODO: Implement QUIC
*/
const ptype = "t";
/* TLS Version
TLS version is shown in 3 different places. If extension 0x002b exists (supported_versions), then the version
is the highest value in the extension. Remember to ignore GREASE values. If the extension doesnt exist, then
the TLS version is the value of the Protocol Version. Handshake version (located at the top of the packet)
should be ignored.
*/
let version = tlsr.version.value;
for (const ext of tlsr.handshake.value.extensions.value) {
if (ext.type.value === "supported_versions") {
version = parseHighestSupportedVersion(ext.value.data);
break;
}
}
version = tlsVersionMapper(version);
/* SNI
If the SNI extension (0x0000) exists, then the destination of the connection is a domain, or d in the fingerprint.
If the SNI does not exist, then the destination is an IP address, or i.
*/
let sni = "i";
for (const ext of tlsr.handshake.value.extensions.value) {
if (ext.type.value === "server_name") {
sni = "d";
break;
}
}
/* Number of Ciphers
2 character number of cipher suites, so if theres 6 cipher suites in the hello packet, then the value should be 06.
If theres > 99, which there should never be, then output 99. Remember, ignore GREASE values. They dont count.
*/
let cipherLen = 0;
for (const cs of tlsr.handshake.value.cipherSuites.value) {
if (cs.value !== "GREASE") cipherLen++;
}
cipherLen = cipherLen > 99 ? "99" : cipherLen.toString().padStart(2, "0");
/* Number of Extensions
Same as counting ciphers. Ignore GREASE. Include SNI and ALPN.
*/
let extLen = 0;
for (const ext of tlsr.handshake.value.extensions.value) {
if (ext.type.value !== "GREASE") extLen++;
}
extLen = extLen > 99 ? "99" : extLen.toString().padStart(2, "0");
/* ALPN Extension Value
The first and last characters of the ALPN (Application-Layer Protocol Negotiation) first value.
If there are no ALPN values or no ALPN extension then we print 00 as the value in the fingerprint.
*/
let alpn = "00";
for (const ext of tlsr.handshake.value.extensions.value) {
if (ext.type.value === "application_layer_protocol_negotiation") {
alpn = parseFirstALPNValue(ext.value.data);
alpn = alpn.charAt(0) + alpn.charAt(alpn.length - 1);
if (alpn.charCodeAt(0) > 127) alpn = "99";
break;
}
}
/* Cipher hash
A 12 character truncated sha256 hash of the list of ciphers sorted in hex order, first 12 characters.
The list is created using the 4 character hex values of the ciphers, lower case, comma delimited, ignoring GREASE.
*/
const originalCiphersList = [];
for (const cs of tlsr.handshake.value.cipherSuites.value) {
if (cs.value !== "GREASE") {
originalCiphersList.push(toHexFast(cs.data));
}
}
const sortedCiphersList = [...originalCiphersList].sort();
const sortedCiphersRaw = sortedCiphersList.join(",");
const originalCiphersRaw = originalCiphersList.join(",");
const sortedCiphers = runHash(
"sha256",
Utils.strToArrayBuffer(sortedCiphersRaw)
).substring(0, 12);
const originalCiphers = runHash(
"sha256",
Utils.strToArrayBuffer(originalCiphersRaw)
).substring(0, 12);
/* Extension hash
A 12 character truncated sha256 hash of the list of extensions, sorted by hex value, followed by the list of signature
algorithms, in the order that they appear (not sorted).
The extension list is created using the 4 character hex values of the extensions, lower case, comma delimited, sorted
(not in the order they appear). Ignore the SNI extension (0000) and the ALPN extension (0010) as weve already captured
them in the a section of the fingerprint. These values are omitted so that the same application would have the same b
section of the fingerprint regardless of if it were going to a domain, IP, or changing ALPNs.
*/
const originalExtensionsList = [];
let signatureAlgorithms = "";
for (const ext of tlsr.handshake.value.extensions.value) {
if (ext.type.value !== "GREASE") {
originalExtensionsList.push(toHexFast(ext.type.data));
}
if (ext.type.value === "signature_algorithms") {
signatureAlgorithms = toHexFast(ext.value.data.slice(2));
signatureAlgorithms = signatureAlgorithms.replace(/(.{4})/g, "$1,");
signatureAlgorithms = signatureAlgorithms.substring(0, signatureAlgorithms.length - 1);
}
}
const sortedExtensionsList = [...originalExtensionsList].filter(e => e !== "0000" && e !== "0010").sort();
const sortedExtensionsRaw = sortedExtensionsList.join(",") + "_" + signatureAlgorithms;
const originalExtensionsRaw = originalExtensionsList.join(",") + "_" + signatureAlgorithms;
const sortedExtensions = runHash(
"sha256",
Utils.strToArrayBuffer(sortedExtensionsRaw)
).substring(0, 12);
const originalExtensions = runHash(
"sha256",
Utils.strToArrayBuffer(originalExtensionsRaw)
).substring(0, 12);
return {
"JA4": `${ptype}${version}${sni}${cipherLen}${extLen}${alpn}_${sortedCiphers}_${sortedExtensions}`,
"JA4_o": `${ptype}${version}${sni}${cipherLen}${extLen}${alpn}_${originalCiphers}_${originalExtensions}`,
"JA4_r": `${ptype}${version}${sni}${cipherLen}${extLen}${alpn}_${sortedCiphersRaw}_${sortedExtensionsRaw}`,
"JA4_ro": `${ptype}${version}${sni}${cipherLen}${extLen}${alpn}_${originalCiphersRaw}_${originalExtensionsRaw}`,
};
}
/**
* Calculate the JA4Server from a given TLS Server Hello Stream
* @param {Uint8Array} bytes
* @returns {string}
*/
export function toJA4S(bytes) {
let tlsr = {};
try {
tlsr = parseTLSRecord(bytes);
if (tlsr.handshake.value.handshakeType.value !== 0x02) {
throw new Error();
}
} catch (err) {
throw new OperationError("Data is not a valid TLS Server Hello. QUIC is not yet supported.\n" + err);
}
/* QUIC
q or t, which denotes whether the hello packet is for QUIC or TCP.
TODO: Implement QUIC
*/
const ptype = "t";
/* TLS Version
TLS version is shown in 3 different places. If extension 0x002b exists (supported_versions), then the version
is the highest value in the extension. Remember to ignore GREASE values. If the extension doesnt exist, then
the TLS version is the value of the Protocol Version. Handshake version (located at the top of the packet)
should be ignored.
*/
let version = tlsr.version.value;
for (const ext of tlsr.handshake.value.extensions.value) {
if (ext.type.value === "supported_versions") {
version = parseHighestSupportedVersion(ext.value.data);
break;
}
}
version = tlsVersionMapper(version);
/* Number of Extensions
2 character number of cipher suites, so if theres 6 cipher suites in the hello packet, then the value should be 06.
If theres > 99, which there should never be, then output 99.
*/
let extLen = tlsr.handshake.value.extensions.value.length;
extLen = extLen > 99 ? "99" : extLen.toString().padStart(2, "0");
/* ALPN Extension Chosen Value
The first and last characters of the ALPN (Application-Layer Protocol Negotiation) first value.
If there are no ALPN values or no ALPN extension then we print 00 as the value in the fingerprint.
*/
let alpn = "00";
for (const ext of tlsr.handshake.value.extensions.value) {
if (ext.type.value === "application_layer_protocol_negotiation") {
alpn = parseFirstALPNValue(ext.value.data);
alpn = alpn.charAt(0) + alpn.charAt(alpn.length - 1);
if (alpn.charCodeAt(0) > 127) alpn = "99";
break;
}
}
/* Chosen Cipher
The hex value of the chosen cipher suite
*/
const cipher = toHexFast(tlsr.handshake.value.cipherSuite.data);
/* Extension hash
A 12 character truncated sha256 hash of the list of extensions.
The extension list is created using the 4 character hex values of the extensions, lower case, comma delimited.
*/
const extensionsList = [];
for (const ext of tlsr.handshake.value.extensions.value) {
extensionsList.push(toHexFast(ext.type.data));
}
const extensionsRaw = extensionsList.join(",");
const extensionsHash = runHash(
"sha256",
Utils.strToArrayBuffer(extensionsRaw)
).substring(0, 12);
return {
"JA4S": `${ptype}${version}${extLen}${alpn}_${cipher}_${extensionsHash}`,
"JA4S_r": `${ptype}${version}${extLen}${alpn}_${cipher}_${extensionsRaw}`,
};
}
/**
* Takes a TLS version value and returns a JA4 TLS version string
* @param {Uint8Array} version - Two byte array of version number
* @returns {string}
*/
function tlsVersionMapper(version) {
switch (version) {
case 0x0304: return "13"; // TLS 1.3
case 0x0303: return "12"; // TLS 1.2
case 0x0302: return "11"; // TLS 1.1
case 0x0301: return "10"; // TLS 1.0
case 0x0300: return "s3"; // SSL 3.0
case 0x0200: return "s2"; // SSL 2.0
case 0x0100: return "s1"; // SSL 1.0
default: return "00"; // Unknown
}
}

88
src/core/lib/LZNT1.mjs Normal file
View File

@ -0,0 +1,88 @@
/**
*
* LZNT1 Decompress.
*
* @author 0xThiebaut [thiebaut.dev]
* @copyright Crown Copyright 2023
* @license Apache-2.0
*
* https://github.com/Velocidex/go-ntfs/blob/master/parser%2Flznt1.go
*/
import Utils from "../Utils.mjs";
import OperationError from "../errors/OperationError.mjs";
const COMPRESSED_MASK = 1 << 15,
SIZE_MASK = (1 << 12) - 1;
/**
* @param {number} offset
* @returns {number}
*/
function getDisplacement(offset) {
let result = 0;
while (offset >= 0x10) {
offset >>= 1;
result += 1;
}
return result;
}
/**
* @param {byteArray} compressed
* @returns {byteArray}
*/
export function decompress(compressed) {
const decompressed = Array();
let coffset = 0;
while (coffset + 2 <= compressed.length) {
const doffset = decompressed.length;
const blockHeader = Utils.byteArrayToInt(compressed.slice(coffset, coffset + 2), "little");
coffset += 2;
const size = blockHeader & SIZE_MASK;
const blockEnd = coffset + size + 1;
if (size === 0) {
break;
} else if (compressed.length < coffset + size) {
throw new OperationError("Malformed LZNT1 stream: Block too small! Has the stream been truncated?");
}
if ((blockHeader & COMPRESSED_MASK) !== 0) {
while (coffset < blockEnd) {
let header = compressed[coffset++];
for (let i = 0; i < 8 && coffset < blockEnd; i++) {
if ((header & 1) === 0) {
decompressed.push(compressed[coffset++]);
} else {
const pointer = Utils.byteArrayToInt(compressed.slice(coffset, coffset + 2), "little");
coffset += 2;
const displacement = getDisplacement(decompressed.length - doffset - 1);
const symbolOffset = (pointer >> (12 - displacement)) + 1;
const symbolLength = (pointer & (0xFFF >> displacement)) + 2;
const shiftOffset = decompressed.length - symbolOffset;
for (let shiftDelta = 0; shiftDelta < symbolLength + 1; shiftDelta++) {
const shift = shiftOffset + shiftDelta;
if (shift < 0 || decompressed.length <= shift) {
throw new OperationError("Malformed LZNT1 stream: Invalid shift!");
}
decompressed.push(decompressed[shift]);
}
}
header >>= 1;
}
}
} else {
decompressed.push(...compressed.slice(coffset, coffset + size + 1));
coffset += size + 1;
}
}
return decompressed;
}

View File

@ -3,6 +3,7 @@ import Utils, { isWorkerEnvironment } from "../Utils.mjs";
import Recipe from "../Recipe.mjs"; import Recipe from "../Recipe.mjs";
import Dish from "../Dish.mjs"; import Dish from "../Dish.mjs";
import {detectFileType, isType} from "./FileType.mjs"; import {detectFileType, isType} from "./FileType.mjs";
import {isUTF8} from "./ChrEnc.mjs";
import chiSquared from "chi-squared"; import chiSquared from "chi-squared";
/** /**
@ -111,82 +112,6 @@ class Magic {
}; };
} }
/**
* Detects whether the input buffer is valid UTF8.
*
* @returns {boolean}
*/
isUTF8() {
const bytes = new Uint8Array(this.inputBuffer);
let i = 0;
while (i < bytes.length) {
if (( // ASCII
bytes[i] === 0x09 ||
bytes[i] === 0x0A ||
bytes[i] === 0x0D ||
(0x20 <= bytes[i] && bytes[i] <= 0x7E)
)) {
i += 1;
continue;
}
if (( // non-overlong 2-byte
(0xC2 <= bytes[i] && bytes[i] <= 0xDF) &&
(0x80 <= bytes[i+1] && bytes[i+1] <= 0xBF)
)) {
i += 2;
continue;
}
if (( // excluding overlongs
bytes[i] === 0xE0 &&
(0xA0 <= bytes[i + 1] && bytes[i + 1] <= 0xBF) &&
(0x80 <= bytes[i + 2] && bytes[i + 2] <= 0xBF)
) ||
( // straight 3-byte
((0xE1 <= bytes[i] && bytes[i] <= 0xEC) ||
bytes[i] === 0xEE ||
bytes[i] === 0xEF) &&
(0x80 <= bytes[i + 1] && bytes[i+1] <= 0xBF) &&
(0x80 <= bytes[i+2] && bytes[i+2] <= 0xBF)
) ||
( // excluding surrogates
bytes[i] === 0xED &&
(0x80 <= bytes[i+1] && bytes[i+1] <= 0x9F) &&
(0x80 <= bytes[i+2] && bytes[i+2] <= 0xBF)
)) {
i += 3;
continue;
}
if (( // planes 1-3
bytes[i] === 0xF0 &&
(0x90 <= bytes[i + 1] && bytes[i + 1] <= 0xBF) &&
(0x80 <= bytes[i + 2] && bytes[i + 2] <= 0xBF) &&
(0x80 <= bytes[i + 3] && bytes[i + 3] <= 0xBF)
) ||
( // planes 4-15
(0xF1 <= bytes[i] && bytes[i] <= 0xF3) &&
(0x80 <= bytes[i + 1] && bytes[i + 1] <= 0xBF) &&
(0x80 <= bytes[i + 2] && bytes[i + 2] <= 0xBF) &&
(0x80 <= bytes[i + 3] && bytes[i + 3] <= 0xBF)
) ||
( // plane 16
bytes[i] === 0xF4 &&
(0x80 <= bytes[i + 1] && bytes[i + 1] <= 0x8F) &&
(0x80 <= bytes[i + 2] && bytes[i + 2] <= 0xBF) &&
(0x80 <= bytes[i + 3] && bytes[i + 3] <= 0xBF)
)) {
i += 4;
continue;
}
return false;
}
return true;
}
/** /**
* Calculates the Shannon entropy of the input data. * Calculates the Shannon entropy of the input data.
* *
@ -336,7 +261,7 @@ class Magic {
data: this.inputStr.slice(0, 100), data: this.inputStr.slice(0, 100),
languageScores: this.detectLanguage(extLang), languageScores: this.detectLanguage(extLang),
fileType: this.detectFileType(), fileType: this.detectFileType(),
isUTF8: this.isUTF8(), isUTF8: !!isUTF8(this.inputBuffer),
entropy: this.calcEntropy(), entropy: this.calcEntropy(),
matchingOps: matchingOps, matchingOps: matchingOps,
useful: useful, useful: useful,

144
src/core/lib/Salsa20.mjs Normal file
View File

@ -0,0 +1,144 @@
/**
* @author joostrijneveld [joost@joostrijneveld.nl]
* @copyright Crown Copyright 2024
* @license Apache-2.0
*/
import Utils from "../Utils.mjs";
/**
* Computes the Salsa20 permute function
*
* @param {byteArray} x
* @param {integer} rounds
*/
function salsa20Permute(x, rounds) {
/**
* Macro to compute a 32-bit rotate-left operation
*
* @param {integer} x
* @param {integer} n
* @returns {integer}
*/
function ROL32(x, n) {
return ((x << n) & 0xFFFFFFFF) | (x >>> (32 - n));
}
/**
* Macro to compute a single Salsa20 quarterround operation
*
* @param {integer} x
* @param {integer} a
* @param {integer} b
* @param {integer} c
* @param {integer} d
* @returns {integer}
*/
function quarterround(x, a, b, c, d) {
x[b] ^= ROL32((x[a] + x[d]) & 0xFFFFFFFF, 7);
x[c] ^= ROL32((x[b] + x[a]) & 0xFFFFFFFF, 9);
x[d] ^= ROL32((x[c] + x[b]) & 0xFFFFFFFF, 13);
x[a] ^= ROL32((x[d] + x[c]) & 0xFFFFFFFF, 18);
}
for (let i = 0; i < rounds / 2; i++) {
quarterround(x, 0, 4, 8, 12);
quarterround(x, 5, 9, 13, 1);
quarterround(x, 10, 14, 2, 6);
quarterround(x, 15, 3, 7, 11);
quarterround(x, 0, 1, 2, 3);
quarterround(x, 5, 6, 7, 4);
quarterround(x, 10, 11, 8, 9);
quarterround(x, 15, 12, 13, 14);
}
}
/**
* Computes the Salsa20 block function
*
* @param {byteArray} key
* @param {byteArray} nonce
* @param {byteArray} counter
* @param {integer} rounds
* @returns {byteArray}
*/
export function salsa20Block(key, nonce, counter, rounds) {
const tau = "expand 16-byte k";
const sigma = "expand 32-byte k";
let state, c;
if (key.length === 16) {
c = Utils.strToByteArray(tau);
key = key.concat(key);
} else {
c = Utils.strToByteArray(sigma);
}
state = c.slice(0, 4);
state = state.concat(key.slice(0, 16));
state = state.concat(c.slice(4, 8));
state = state.concat(nonce);
state = state.concat(counter);
state = state.concat(c.slice(8, 12));
state = state.concat(key.slice(16, 32));
state = state.concat(c.slice(12, 16));
const x = Array();
for (let i = 0; i < 64; i += 4) {
x.push(Utils.byteArrayToInt(state.slice(i, i + 4), "little"));
}
const a = [...x];
salsa20Permute(x, rounds);
for (let i = 0; i < 16; i++) {
x[i] = (x[i] + a[i]) & 0xFFFFFFFF;
}
let output = Array();
for (let i = 0; i < 16; i++) {
output = output.concat(Utils.intToByteArray(x[i], 4, "little"));
}
return output;
}
/**
* Computes the hSalsa20 function
*
* @param {byteArray} key
* @param {byteArray} nonce
* @param {integer} rounds
* @returns {byteArray}
*/
export function hsalsa20(key, nonce, rounds) {
const tau = "expand 16-byte k";
const sigma = "expand 32-byte k";
let state, c;
if (key.length === 16) {
c = Utils.strToByteArray(tau);
key = key.concat(key);
} else {
c = Utils.strToByteArray(sigma);
}
state = c.slice(0, 4);
state = state.concat(key.slice(0, 16));
state = state.concat(c.slice(4, 8));
state = state.concat(nonce);
state = state.concat(c.slice(8, 12));
state = state.concat(key.slice(16, 32));
state = state.concat(c.slice(12, 16));
const x = Array();
for (let i = 0; i < 64; i += 4) {
x.push(Utils.byteArrayToInt(state.slice(i, i + 4), "little"));
}
salsa20Permute(x, rounds);
let output = Array();
const idx = [0, 5, 10, 15, 6, 7, 8, 9];
for (let i = 0; i < 8; i++) {
output = output.concat(Utils.intToByteArray(x[idx[i]], 4, "little"));
}
return output;
}

View File

@ -18,12 +18,23 @@ export default class Stream {
* Stream constructor. * Stream constructor.
* *
* @param {Uint8Array} input * @param {Uint8Array} input
* @param {number} pos
* @param {number} bitPos
*/ */
constructor(input) { constructor(input, pos=0, bitPos=0) {
this.bytes = input; this.bytes = input;
this.length = this.bytes.length; this.length = this.bytes.length;
this.position = 0; this.position = pos;
this.bitPos = 0; this.bitPos = bitPos;
}
/**
* Clone this Stream returning a new identical Stream.
*
* @returns {Stream}
*/
clone() {
return new Stream(this.bytes, this.position, this.bitPos);
} }
/** /**

877
src/core/lib/TLS.mjs Normal file
View File

@ -0,0 +1,877 @@
/**
* TLS resources.
*
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2024
* @license Apache-2.0
*/
import OperationError from "../errors/OperationError.mjs";
import Stream from "../lib/Stream.mjs";
/**
* Parse a TLS Record
* @param {Uint8Array} bytes
* @returns {JSON}
*/
export function parseTLSRecord(bytes) {
const s = new Stream(bytes);
const b = s.clone();
const r = {};
// Content type
r.contentType = {
description: "Content Type",
length: 1,
data: b.getBytes(1),
value: s.readInt(1)
};
if (r.contentType.value !== 0x16)
throw new OperationError("Not handshake data.");
// Version
r.version = {
description: "Protocol Version",
length: 2,
data: b.getBytes(2),
value: s.readInt(2)
};
// Length
r.length = {
description: "Record Length",
length: 2,
data: b.getBytes(2),
value: s.readInt(2)
};
if (s.length !== r.length.value + 5)
throw new OperationError("Incorrect handshake length.");
// Handshake
r.handshake = {
description: "Handshake",
length: r.length.value,
data: b.getBytes(r.length.value),
value: parseHandshake(s.getBytes(r.length.value))
};
return r;
}
/**
* Parse a TLS Handshake
* @param {Uint8Array} bytes
* @returns {JSON}
*/
function parseHandshake(bytes) {
const s = new Stream(bytes);
const b = s.clone();
const h = {};
// Handshake type
h.handshakeType = {
description: "Handshake Type",
length: 1,
data: b.getBytes(1),
value: s.readInt(1)
};
// Handshake length
h.handshakeLength = {
description: "Handshake Length",
length: 3,
data: b.getBytes(3),
value: s.readInt(3)
};
if (s.length !== h.handshakeLength.value + 4)
throw new OperationError("Not enough data in Handshake message.");
switch (h.handshakeType.value) {
case 0x01:
h.handshakeType.description = "Client Hello";
parseClientHello(s, b, h);
break;
case 0x02:
h.handshakeType.description = "Server Hello";
parseServerHello(s, b, h);
break;
default:
throw new OperationError("Not a known handshake message.");
}
return h;
}
/**
* Parse a TLS Client Hello
* @param {Stream} s
* @param {Stream} b
* @param {Object} h
* @returns {JSON}
*/
function parseClientHello(s, b, h) {
// Hello version
h.helloVersion = {
description: "Client Hello Version",
length: 2,
data: b.getBytes(2),
value: s.readInt(2)
};
// Random
h.random = {
description: "Client Random",
length: 32,
data: b.getBytes(32),
value: s.getBytes(32)
};
// Session ID Length
h.sessionIDLength = {
description: "Session ID Length",
length: 1,
data: b.getBytes(1),
value: s.readInt(1)
};
// Session ID
h.sessionID = {
description: "Session ID",
length: h.sessionIDLength.value,
data: b.getBytes(h.sessionIDLength.value),
value: s.getBytes(h.sessionIDLength.value)
};
// Cipher Suites Length
h.cipherSuitesLength = {
description: "Cipher Suites Length",
length: 2,
data: b.getBytes(2),
value: s.readInt(2)
};
// Cipher Suites
h.cipherSuites = {
description: "Cipher Suites",
length: h.cipherSuitesLength.value,
data: b.getBytes(h.cipherSuitesLength.value),
value: parseCipherSuites(s.getBytes(h.cipherSuitesLength.value))
};
// Compression Methods Length
h.compressionMethodsLength = {
description: "Compression Methods Length",
length: 1,
data: b.getBytes(1),
value: s.readInt(1)
};
// Compression Methods
h.compressionMethods = {
description: "Compression Methods",
length: h.compressionMethodsLength.value,
data: b.getBytes(h.compressionMethodsLength.value),
value: parseCompressionMethods(s.getBytes(h.compressionMethodsLength.value))
};
// Extensions Length
h.extensionsLength = {
description: "Extensions Length",
length: 2,
data: b.getBytes(2),
value: s.readInt(2)
};
// Extensions
h.extensions = {
description: "Extensions",
length: h.extensionsLength.value,
data: b.getBytes(h.extensionsLength.value),
value: parseExtensions(s.getBytes(h.extensionsLength.value))
};
return h;
}
/**
* Parse a TLS Server Hello
* @param {Stream} s
* @param {Stream} b
* @param {Object} h
* @returns {JSON}
*/
function parseServerHello(s, b, h) {
// Hello version
h.helloVersion = {
description: "Server Hello Version",
length: 2,
data: b.getBytes(2),
value: s.readInt(2)
};
// Random
h.random = {
description: "Server Random",
length: 32,
data: b.getBytes(32),
value: s.getBytes(32)
};
// Session ID Length
h.sessionIDLength = {
description: "Session ID Length",
length: 1,
data: b.getBytes(1),
value: s.readInt(1)
};
// Session ID
h.sessionID = {
description: "Session ID",
length: h.sessionIDLength.value,
data: b.getBytes(h.sessionIDLength.value),
value: s.getBytes(h.sessionIDLength.value)
};
// Cipher Suite
h.cipherSuite = {
description: "Selected Cipher Suite",
length: 2,
data: b.getBytes(2),
value: CIPHER_SUITES_LOOKUP[s.readInt(2)] || "Unknown"
};
// Compression Method
h.compressionMethod = {
description: "Selected Compression Method",
length: 1,
data: b.getBytes(1),
value: s.readInt(1) // TODO: Compression method name here
};
// Extensions Length
h.extensionsLength = {
description: "Extensions Length",
length: 2,
data: b.getBytes(2),
value: s.readInt(2)
};
// Extensions
h.extensions = {
description: "Extensions",
length: h.extensionsLength.value,
data: b.getBytes(h.extensionsLength.value),
value: parseExtensions(s.getBytes(h.extensionsLength.value))
};
}
/**
* Parse Cipher Suites
* @param {Uint8Array} bytes
* @returns {JSON}
*/
function parseCipherSuites(bytes) {
const s = new Stream(bytes);
const b = s.clone();
const cs = [];
while (s.hasMore()) {
cs.push({
description: "Cipher Suite",
length: 2,
data: b.getBytes(2),
value: CIPHER_SUITES_LOOKUP[s.readInt(2)] || "Unknown"
});
}
return cs;
}
/**
* Parse Compression Methods
* @param {Uint8Array} bytes
* @returns {JSON}
*/
function parseCompressionMethods(bytes) {
const s = new Stream(bytes);
const b = s.clone();
const cm = [];
while (s.hasMore()) {
cm.push({
description: "Compression Method",
length: 1,
data: b.getBytes(1),
value: s.readInt(1) // TODO: Compression method name here
});
}
return cm;
}
/**
* Parse Extensions
* @param {Uint8Array} bytes
* @returns {JSON}
*/
function parseExtensions(bytes) {
const s = new Stream(bytes);
const b = s.clone();
const exts = [];
while (s.hasMore()) {
const ext = {};
// Type
ext.type = {
description: "Extension Type",
length: 2,
data: b.getBytes(2),
value: EXTENSION_LOOKUP[s.readInt(2)] || "unknown"
};
// Length
ext.length = {
description: "Extension Length",
length: 2,
data: b.getBytes(2),
value: s.readInt(2)
};
// Value
ext.value = {
description: "Extension Value",
length: ext.length.value,
data: b.getBytes(ext.length.value),
value: s.getBytes(ext.length.value)
};
exts.push(ext);
}
return exts;
}
/**
* Extension type lookup table
*/
const EXTENSION_LOOKUP = {
0: "server_name",
1: "max_fragment_length",
2: "client_certificate_url",
3: "trusted_ca_keys",
4: "truncated_hmac",
5: "status_request",
6: "user_mapping",
7: "client_authz",
8: "server_authz",
9: "cert_type",
10: "supported_groups",
11: "ec_point_formats",
12: "srp",
13: "signature_algorithms",
14: "use_srtp",
15: "heartbeat",
16: "application_layer_protocol_negotiation",
17: "status_request_v2",
18: "signed_certificate_timestamp",
19: "client_certificate_type",
20: "server_certificate_type",
21: "padding",
22: "encrypt_then_mac",
23: "extended_master_secret",
24: "token_binding",
25: "cached_info",
26: "tls_lts",
27: "compress_certificate",
28: "record_size_limit",
29: "pwd_protect",
30: "pwd_clear",
31: "password_salt",
32: "ticket_pinning",
33: "tls_cert_with_extern_psk",
34: "delegated_credential",
35: "session_ticket",
36: "TLMSP",
37: "TLMSP_proxying",
38: "TLMSP_delegate",
39: "supported_ekt_ciphers",
40: "Reserved",
41: "pre_shared_key",
42: "early_data",
43: "supported_versions",
44: "cookie",
45: "psk_key_exchange_modes",
46: "Reserved",
47: "certificate_authorities",
48: "oid_filters",
49: "post_handshake_auth",
50: "signature_algorithms_cert",
51: "key_share",
52: "transparency_info",
53: "connection_id (deprecated)",
54: "connection_id",
55: "external_id_hash",
56: "external_session_id",
57: "quic_transport_parameters",
58: "ticket_request",
59: "dnssec_chain",
60: "sequence_number_encryption_algorithms",
61: "rrc",
2570: "GREASE",
6682: "GREASE",
10794: "GREASE",
14906: "GREASE",
17513: "application_settings",
19018: "GREASE",
23130: "GREASE",
27242: "GREASE",
31354: "GREASE",
35466: "GREASE",
39578: "GREASE",
43690: "GREASE",
47802: "GREASE",
51914: "GREASE",
56026: "GREASE",
60138: "GREASE",
64250: "GREASE",
64768: "ech_outer_extensions",
65037: "encrypted_client_hello",
65281: "renegotiation_info"
};
/**
* Cipher suites lookup table
*/
const CIPHER_SUITES_LOOKUP = {
0x0000: "TLS_NULL_WITH_NULL_NULL",
0x0001: "TLS_RSA_WITH_NULL_MD5",
0x0002: "TLS_RSA_WITH_NULL_SHA",
0x0003: "TLS_RSA_EXPORT_WITH_RC4_40_MD5",
0x0004: "TLS_RSA_WITH_RC4_128_MD5",
0x0005: "TLS_RSA_WITH_RC4_128_SHA",
0x0006: "TLS_RSA_EXPORT_WITH_RC2_CBC_40_MD5",
0x0007: "TLS_RSA_WITH_IDEA_CBC_SHA",
0x0008: "TLS_RSA_EXPORT_WITH_DES40_CBC_SHA",
0x0009: "TLS_RSA_WITH_DES_CBC_SHA",
0x000A: "TLS_RSA_WITH_3DES_EDE_CBC_SHA",
0x000B: "TLS_DH_DSS_EXPORT_WITH_DES40_CBC_SHA",
0x000C: "TLS_DH_DSS_WITH_DES_CBC_SHA",
0x000D: "TLS_DH_DSS_WITH_3DES_EDE_CBC_SHA",
0x000E: "TLS_DH_RSA_EXPORT_WITH_DES40_CBC_SHA",
0x000F: "TLS_DH_RSA_WITH_DES_CBC_SHA",
0x0010: "TLS_DH_RSA_WITH_3DES_EDE_CBC_SHA",
0x0011: "TLS_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA",
0x0012: "TLS_DHE_DSS_WITH_DES_CBC_SHA",
0x0013: "TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA",
0x0014: "TLS_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA",
0x0015: "TLS_DHE_RSA_WITH_DES_CBC_SHA",
0x0016: "TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA",
0x0017: "TLS_DH_anon_EXPORT_WITH_RC4_40_MD5",
0x0018: "TLS_DH_anon_WITH_RC4_128_MD5",
0x0019: "TLS_DH_anon_EXPORT_WITH_DES40_CBC_SHA",
0x001A: "TLS_DH_anon_WITH_DES_CBC_SHA",
0x001B: "TLS_DH_anon_WITH_3DES_EDE_CBC_SHA",
0x001E: "TLS_KRB5_WITH_DES_CBC_SHA",
0x001F: "TLS_KRB5_WITH_3DES_EDE_CBC_SHA",
0x0020: "TLS_KRB5_WITH_RC4_128_SHA",
0x0021: "TLS_KRB5_WITH_IDEA_CBC_SHA",
0x0022: "TLS_KRB5_WITH_DES_CBC_MD5",
0x0023: "TLS_KRB5_WITH_3DES_EDE_CBC_MD5",
0x0024: "TLS_KRB5_WITH_RC4_128_MD5",
0x0025: "TLS_KRB5_WITH_IDEA_CBC_MD5",
0x0026: "TLS_KRB5_EXPORT_WITH_DES_CBC_40_SHA",
0x0027: "TLS_KRB5_EXPORT_WITH_RC2_CBC_40_SHA",
0x0028: "TLS_KRB5_EXPORT_WITH_RC4_40_SHA",
0x0029: "TLS_KRB5_EXPORT_WITH_DES_CBC_40_MD5",
0x002A: "TLS_KRB5_EXPORT_WITH_RC2_CBC_40_MD5",
0x002B: "TLS_KRB5_EXPORT_WITH_RC4_40_MD5",
0x002C: "TLS_PSK_WITH_NULL_SHA",
0x002D: "TLS_DHE_PSK_WITH_NULL_SHA",
0x002E: "TLS_RSA_PSK_WITH_NULL_SHA",
0x002F: "TLS_RSA_WITH_AES_128_CBC_SHA",
0x0030: "TLS_DH_DSS_WITH_AES_128_CBC_SHA",
0x0031: "TLS_DH_RSA_WITH_AES_128_CBC_SHA",
0x0032: "TLS_DHE_DSS_WITH_AES_128_CBC_SHA",
0x0033: "TLS_DHE_RSA_WITH_AES_128_CBC_SHA",
0x0034: "TLS_DH_anon_WITH_AES_128_CBC_SHA",
0x0035: "TLS_RSA_WITH_AES_256_CBC_SHA",
0x0036: "TLS_DH_DSS_WITH_AES_256_CBC_SHA",
0x0037: "TLS_DH_RSA_WITH_AES_256_CBC_SHA",
0x0038: "TLS_DHE_DSS_WITH_AES_256_CBC_SHA",
0x0039: "TLS_DHE_RSA_WITH_AES_256_CBC_SHA",
0x003A: "TLS_DH_anon_WITH_AES_256_CBC_SHA",
0x003B: "TLS_RSA_WITH_NULL_SHA256",
0x003C: "TLS_RSA_WITH_AES_128_CBC_SHA256",
0x003D: "TLS_RSA_WITH_AES_256_CBC_SHA256",
0x003E: "TLS_DH_DSS_WITH_AES_128_CBC_SHA256",
0x003F: "TLS_DH_RSA_WITH_AES_128_CBC_SHA256",
0x0040: "TLS_DHE_DSS_WITH_AES_128_CBC_SHA256",
0x0041: "TLS_RSA_WITH_CAMELLIA_128_CBC_SHA",
0x0042: "TLS_DH_DSS_WITH_CAMELLIA_128_CBC_SHA",
0x0043: "TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA",
0x0044: "TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA",
0x0045: "TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA",
0x0046: "TLS_DH_anon_WITH_CAMELLIA_128_CBC_SHA",
0x0067: "TLS_DHE_RSA_WITH_AES_128_CBC_SHA256",
0x0068: "TLS_DH_DSS_WITH_AES_256_CBC_SHA256",
0x0069: "TLS_DH_RSA_WITH_AES_256_CBC_SHA256",
0x006A: "TLS_DHE_DSS_WITH_AES_256_CBC_SHA256",
0x006B: "TLS_DHE_RSA_WITH_AES_256_CBC_SHA256",
0x006C: "TLS_DH_anon_WITH_AES_128_CBC_SHA256",
0x006D: "TLS_DH_anon_WITH_AES_256_CBC_SHA256",
0x0084: "TLS_RSA_WITH_CAMELLIA_256_CBC_SHA",
0x0085: "TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA",
0x0086: "TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA",
0x0087: "TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA",
0x0088: "TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA",
0x0089: "TLS_DH_anon_WITH_CAMELLIA_256_CBC_SHA",
0x008A: "TLS_PSK_WITH_RC4_128_SHA",
0x008B: "TLS_PSK_WITH_3DES_EDE_CBC_SHA",
0x008C: "TLS_PSK_WITH_AES_128_CBC_SHA",
0x008D: "TLS_PSK_WITH_AES_256_CBC_SHA",
0x008E: "TLS_DHE_PSK_WITH_RC4_128_SHA",
0x008F: "TLS_DHE_PSK_WITH_3DES_EDE_CBC_SHA",
0x0090: "TLS_DHE_PSK_WITH_AES_128_CBC_SHA",
0x0091: "TLS_DHE_PSK_WITH_AES_256_CBC_SHA",
0x0092: "TLS_RSA_PSK_WITH_RC4_128_SHA",
0x0093: "TLS_RSA_PSK_WITH_3DES_EDE_CBC_SHA",
0x0094: "TLS_RSA_PSK_WITH_AES_128_CBC_SHA",
0x0095: "TLS_RSA_PSK_WITH_AES_256_CBC_SHA",
0x0096: "TLS_RSA_WITH_SEED_CBC_SHA",
0x0097: "TLS_DH_DSS_WITH_SEED_CBC_SHA",
0x0098: "TLS_DH_RSA_WITH_SEED_CBC_SHA",
0x0099: "TLS_DHE_DSS_WITH_SEED_CBC_SHA",
0x009A: "TLS_DHE_RSA_WITH_SEED_CBC_SHA",
0x009B: "TLS_DH_anon_WITH_SEED_CBC_SHA",
0x009C: "TLS_RSA_WITH_AES_128_GCM_SHA256",
0x009D: "TLS_RSA_WITH_AES_256_GCM_SHA384",
0x009E: "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256",
0x009F: "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384",
0x00A0: "TLS_DH_RSA_WITH_AES_128_GCM_SHA256",
0x00A1: "TLS_DH_RSA_WITH_AES_256_GCM_SHA384",
0x00A2: "TLS_DHE_DSS_WITH_AES_128_GCM_SHA256",
0x00A3: "TLS_DHE_DSS_WITH_AES_256_GCM_SHA384",
0x00A4: "TLS_DH_DSS_WITH_AES_128_GCM_SHA256",
0x00A5: "TLS_DH_DSS_WITH_AES_256_GCM_SHA384",
0x00A6: "TLS_DH_anon_WITH_AES_128_GCM_SHA256",
0x00A7: "TLS_DH_anon_WITH_AES_256_GCM_SHA384",
0x00A8: "TLS_PSK_WITH_AES_128_GCM_SHA256",
0x00A9: "TLS_PSK_WITH_AES_256_GCM_SHA384",
0x00AA: "TLS_DHE_PSK_WITH_AES_128_GCM_SHA256",
0x00AB: "TLS_DHE_PSK_WITH_AES_256_GCM_SHA384",
0x00AC: "TLS_RSA_PSK_WITH_AES_128_GCM_SHA256",
0x00AD: "TLS_RSA_PSK_WITH_AES_256_GCM_SHA384",
0x00AE: "TLS_PSK_WITH_AES_128_CBC_SHA256",
0x00AF: "TLS_PSK_WITH_AES_256_CBC_SHA384",
0x00B0: "TLS_PSK_WITH_NULL_SHA256",
0x00B1: "TLS_PSK_WITH_NULL_SHA384",
0x00B2: "TLS_DHE_PSK_WITH_AES_128_CBC_SHA256",
0x00B3: "TLS_DHE_PSK_WITH_AES_256_CBC_SHA384",
0x00B4: "TLS_DHE_PSK_WITH_NULL_SHA256",
0x00B5: "TLS_DHE_PSK_WITH_NULL_SHA384",
0x00B6: "TLS_RSA_PSK_WITH_AES_128_CBC_SHA256",
0x00B7: "TLS_RSA_PSK_WITH_AES_256_CBC_SHA384",
0x00B8: "TLS_RSA_PSK_WITH_NULL_SHA256",
0x00B9: "TLS_RSA_PSK_WITH_NULL_SHA384",
0x00BA: "TLS_RSA_WITH_CAMELLIA_128_CBC_SHA256",
0x00BB: "TLS_DH_DSS_WITH_CAMELLIA_128_CBC_SHA256",
0x00BC: "TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA256",
0x00BD: "TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA256",
0x00BE: "TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA256",
0x00BF: "TLS_DH_anon_WITH_CAMELLIA_128_CBC_SHA256",
0x00C0: "TLS_RSA_WITH_CAMELLIA_256_CBC_SHA256",
0x00C1: "TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA256",
0x00C2: "TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA256",
0x00C3: "TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA256",
0x00C4: "TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA256",
0x00C5: "TLS_DH_anon_WITH_CAMELLIA_256_CBC_SHA256",
0x00C6: "TLS_SM4_GCM_SM3",
0x00C7: "TLS_SM4_CCM_SM3",
0x00FF: "TLS_EMPTY_RENEGOTIATION_INFO_SCSV",
0x0A0A: "GREASE",
0x1301: "TLS_AES_128_GCM_SHA256",
0x1302: "TLS_AES_256_GCM_SHA384",
0x1303: "TLS_CHACHA20_POLY1305_SHA256",
0x1304: "TLS_AES_128_CCM_SHA256",
0x1305: "TLS_AES_128_CCM_8_SHA256",
0x1306: "TLS_AEGIS_256_SHA512",
0x1307: "TLS_AEGIS_128L_SHA256",
0x1A1A: "GREASE",
0x2A2A: "GREASE",
0x3A3A: "GREASE",
0x4A4A: "GREASE",
0x5600: "TLS_FALLBACK_SCSV",
0x5A5A: "GREASE",
0x6A6A: "GREASE",
0x7A7A: "GREASE",
0x8A8A: "GREASE",
0x9A9A: "GREASE",
0xAAAA: "GREASE",
0xBABA: "GREASE",
0xC001: "TLS_ECDH_ECDSA_WITH_NULL_SHA",
0xC002: "TLS_ECDH_ECDSA_WITH_RC4_128_SHA",
0xC003: "TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA",
0xC004: "TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA",
0xC005: "TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA",
0xC006: "TLS_ECDHE_ECDSA_WITH_NULL_SHA",
0xC007: "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA",
0xC008: "TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA",
0xC009: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
0xC00A: "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA",
0xC00B: "TLS_ECDH_RSA_WITH_NULL_SHA",
0xC00C: "TLS_ECDH_RSA_WITH_RC4_128_SHA",
0xC00D: "TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA",
0xC00E: "TLS_ECDH_RSA_WITH_AES_128_CBC_SHA",
0xC00F: "TLS_ECDH_RSA_WITH_AES_256_CBC_SHA",
0xC010: "TLS_ECDHE_RSA_WITH_NULL_SHA",
0xC011: "TLS_ECDHE_RSA_WITH_RC4_128_SHA",
0xC012: "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA",
0xC013: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
0xC014: "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
0xC015: "TLS_ECDH_anon_WITH_NULL_SHA",
0xC016: "TLS_ECDH_anon_WITH_RC4_128_SHA",
0xC017: "TLS_ECDH_anon_WITH_3DES_EDE_CBC_SHA",
0xC018: "TLS_ECDH_anon_WITH_AES_128_CBC_SHA",
0xC019: "TLS_ECDH_anon_WITH_AES_256_CBC_SHA",
0xC01A: "TLS_SRP_SHA_WITH_3DES_EDE_CBC_SHA",
0xC01B: "TLS_SRP_SHA_RSA_WITH_3DES_EDE_CBC_SHA",
0xC01C: "TLS_SRP_SHA_DSS_WITH_3DES_EDE_CBC_SHA",
0xC01D: "TLS_SRP_SHA_WITH_AES_128_CBC_SHA",
0xC01E: "TLS_SRP_SHA_RSA_WITH_AES_128_CBC_SHA",
0xC01F: "TLS_SRP_SHA_DSS_WITH_AES_128_CBC_SHA",
0xC020: "TLS_SRP_SHA_WITH_AES_256_CBC_SHA",
0xC021: "TLS_SRP_SHA_RSA_WITH_AES_256_CBC_SHA",
0xC022: "TLS_SRP_SHA_DSS_WITH_AES_256_CBC_SHA",
0xC023: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
0xC024: "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384",
0xC025: "TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256",
0xC026: "TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384",
0xC027: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256",
0xC028: "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384",
0xC029: "TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256",
0xC02A: "TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384",
0xC02B: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
0xC02C: "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
0xC02D: "TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256",
0xC02E: "TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384",
0xC02F: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
0xC030: "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
0xC031: "TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256",
0xC032: "TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384",
0xC033: "TLS_ECDHE_PSK_WITH_RC4_128_SHA",
0xC034: "TLS_ECDHE_PSK_WITH_3DES_EDE_CBC_SHA",
0xC035: "TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA",
0xC036: "TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA",
0xC037: "TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256",
0xC038: "TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA384",
0xC039: "TLS_ECDHE_PSK_WITH_NULL_SHA",
0xC03A: "TLS_ECDHE_PSK_WITH_NULL_SHA256",
0xC03B: "TLS_ECDHE_PSK_WITH_NULL_SHA384",
0xC03C: "TLS_RSA_WITH_ARIA_128_CBC_SHA256",
0xC03D: "TLS_RSA_WITH_ARIA_256_CBC_SHA384",
0xC03E: "TLS_DH_DSS_WITH_ARIA_128_CBC_SHA256",
0xC03F: "TLS_DH_DSS_WITH_ARIA_256_CBC_SHA384",
0xC040: "TLS_DH_RSA_WITH_ARIA_128_CBC_SHA256",
0xC041: "TLS_DH_RSA_WITH_ARIA_256_CBC_SHA384",
0xC042: "TLS_DHE_DSS_WITH_ARIA_128_CBC_SHA256",
0xC043: "TLS_DHE_DSS_WITH_ARIA_256_CBC_SHA384",
0xC044: "TLS_DHE_RSA_WITH_ARIA_128_CBC_SHA256",
0xC045: "TLS_DHE_RSA_WITH_ARIA_256_CBC_SHA384",
0xC046: "TLS_DH_anon_WITH_ARIA_128_CBC_SHA256",
0xC047: "TLS_DH_anon_WITH_ARIA_256_CBC_SHA384",
0xC048: "TLS_ECDHE_ECDSA_WITH_ARIA_128_CBC_SHA256",
0xC049: "TLS_ECDHE_ECDSA_WITH_ARIA_256_CBC_SHA384",
0xC04A: "TLS_ECDH_ECDSA_WITH_ARIA_128_CBC_SHA256",
0xC04B: "TLS_ECDH_ECDSA_WITH_ARIA_256_CBC_SHA384",
0xC04C: "TLS_ECDHE_RSA_WITH_ARIA_128_CBC_SHA256",
0xC04D: "TLS_ECDHE_RSA_WITH_ARIA_256_CBC_SHA384",
0xC04E: "TLS_ECDH_RSA_WITH_ARIA_128_CBC_SHA256",
0xC04F: "TLS_ECDH_RSA_WITH_ARIA_256_CBC_SHA384",
0xC050: "TLS_RSA_WITH_ARIA_128_GCM_SHA256",
0xC051: "TLS_RSA_WITH_ARIA_256_GCM_SHA384",
0xC052: "TLS_DHE_RSA_WITH_ARIA_128_GCM_SHA256",
0xC053: "TLS_DHE_RSA_WITH_ARIA_256_GCM_SHA384",
0xC054: "TLS_DH_RSA_WITH_ARIA_128_GCM_SHA256",
0xC055: "TLS_DH_RSA_WITH_ARIA_256_GCM_SHA384",
0xC056: "TLS_DHE_DSS_WITH_ARIA_128_GCM_SHA256",
0xC057: "TLS_DHE_DSS_WITH_ARIA_256_GCM_SHA384",
0xC058: "TLS_DH_DSS_WITH_ARIA_128_GCM_SHA256",
0xC059: "TLS_DH_DSS_WITH_ARIA_256_GCM_SHA384",
0xC05A: "TLS_DH_anon_WITH_ARIA_128_GCM_SHA256",
0xC05B: "TLS_DH_anon_WITH_ARIA_256_GCM_SHA384",
0xC05C: "TLS_ECDHE_ECDSA_WITH_ARIA_128_GCM_SHA256",
0xC05D: "TLS_ECDHE_ECDSA_WITH_ARIA_256_GCM_SHA384",
0xC05E: "TLS_ECDH_ECDSA_WITH_ARIA_128_GCM_SHA256",
0xC05F: "TLS_ECDH_ECDSA_WITH_ARIA_256_GCM_SHA384",
0xC060: "TLS_ECDHE_RSA_WITH_ARIA_128_GCM_SHA256",
0xC061: "TLS_ECDHE_RSA_WITH_ARIA_256_GCM_SHA384",
0xC062: "TLS_ECDH_RSA_WITH_ARIA_128_GCM_SHA256",
0xC063: "TLS_ECDH_RSA_WITH_ARIA_256_GCM_SHA384",
0xC064: "TLS_PSK_WITH_ARIA_128_CBC_SHA256",
0xC065: "TLS_PSK_WITH_ARIA_256_CBC_SHA384",
0xC066: "TLS_DHE_PSK_WITH_ARIA_128_CBC_SHA256",
0xC067: "TLS_DHE_PSK_WITH_ARIA_256_CBC_SHA384",
0xC068: "TLS_RSA_PSK_WITH_ARIA_128_CBC_SHA256",
0xC069: "TLS_RSA_PSK_WITH_ARIA_256_CBC_SHA384",
0xC06A: "TLS_PSK_WITH_ARIA_128_GCM_SHA256",
0xC06B: "TLS_PSK_WITH_ARIA_256_GCM_SHA384",
0xC06C: "TLS_DHE_PSK_WITH_ARIA_128_GCM_SHA256",
0xC06D: "TLS_DHE_PSK_WITH_ARIA_256_GCM_SHA384",
0xC06E: "TLS_RSA_PSK_WITH_ARIA_128_GCM_SHA256",
0xC06F: "TLS_RSA_PSK_WITH_ARIA_256_GCM_SHA384",
0xC070: "TLS_ECDHE_PSK_WITH_ARIA_128_CBC_SHA256",
0xC071: "TLS_ECDHE_PSK_WITH_ARIA_256_CBC_SHA384",
0xC072: "TLS_ECDHE_ECDSA_WITH_CAMELLIA_128_CBC_SHA256",
0xC073: "TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_CBC_SHA384",
0xC074: "TLS_ECDH_ECDSA_WITH_CAMELLIA_128_CBC_SHA256",
0xC075: "TLS_ECDH_ECDSA_WITH_CAMELLIA_256_CBC_SHA384",
0xC076: "TLS_ECDHE_RSA_WITH_CAMELLIA_128_CBC_SHA256",
0xC077: "TLS_ECDHE_RSA_WITH_CAMELLIA_256_CBC_SHA384",
0xC078: "TLS_ECDH_RSA_WITH_CAMELLIA_128_CBC_SHA256",
0xC079: "TLS_ECDH_RSA_WITH_CAMELLIA_256_CBC_SHA384",
0xC07A: "TLS_RSA_WITH_CAMELLIA_128_GCM_SHA256",
0xC07B: "TLS_RSA_WITH_CAMELLIA_256_GCM_SHA384",
0xC07C: "TLS_DHE_RSA_WITH_CAMELLIA_128_GCM_SHA256",
0xC07D: "TLS_DHE_RSA_WITH_CAMELLIA_256_GCM_SHA384",
0xC07E: "TLS_DH_RSA_WITH_CAMELLIA_128_GCM_SHA256",
0xC07F: "TLS_DH_RSA_WITH_CAMELLIA_256_GCM_SHA384",
0xC080: "TLS_DHE_DSS_WITH_CAMELLIA_128_GCM_SHA256",
0xC081: "TLS_DHE_DSS_WITH_CAMELLIA_256_GCM_SHA384",
0xC082: "TLS_DH_DSS_WITH_CAMELLIA_128_GCM_SHA256",
0xC083: "TLS_DH_DSS_WITH_CAMELLIA_256_GCM_SHA384",
0xC084: "TLS_DH_anon_WITH_CAMELLIA_128_GCM_SHA256",
0xC085: "TLS_DH_anon_WITH_CAMELLIA_256_GCM_SHA384",
0xC086: "TLS_ECDHE_ECDSA_WITH_CAMELLIA_128_GCM_SHA256",
0xC087: "TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_GCM_SHA384",
0xC088: "TLS_ECDH_ECDSA_WITH_CAMELLIA_128_GCM_SHA256",
0xC089: "TLS_ECDH_ECDSA_WITH_CAMELLIA_256_GCM_SHA384",
0xC08A: "TLS_ECDHE_RSA_WITH_CAMELLIA_128_GCM_SHA256",
0xC08B: "TLS_ECDHE_RSA_WITH_CAMELLIA_256_GCM_SHA384",
0xC08C: "TLS_ECDH_RSA_WITH_CAMELLIA_128_GCM_SHA256",
0xC08D: "TLS_ECDH_RSA_WITH_CAMELLIA_256_GCM_SHA384",
0xC08E: "TLS_PSK_WITH_CAMELLIA_128_GCM_SHA256",
0xC08F: "TLS_PSK_WITH_CAMELLIA_256_GCM_SHA384",
0xC090: "TLS_DHE_PSK_WITH_CAMELLIA_128_GCM_SHA256",
0xC091: "TLS_DHE_PSK_WITH_CAMELLIA_256_GCM_SHA384",
0xC092: "TLS_RSA_PSK_WITH_CAMELLIA_128_GCM_SHA256",
0xC093: "TLS_RSA_PSK_WITH_CAMELLIA_256_GCM_SHA384",
0xC094: "TLS_PSK_WITH_CAMELLIA_128_CBC_SHA256",
0xC095: "TLS_PSK_WITH_CAMELLIA_256_CBC_SHA384",
0xC096: "TLS_DHE_PSK_WITH_CAMELLIA_128_CBC_SHA256",
0xC097: "TLS_DHE_PSK_WITH_CAMELLIA_256_CBC_SHA384",
0xC098: "TLS_RSA_PSK_WITH_CAMELLIA_128_CBC_SHA256",
0xC099: "TLS_RSA_PSK_WITH_CAMELLIA_256_CBC_SHA384",
0xC09A: "TLS_ECDHE_PSK_WITH_CAMELLIA_128_CBC_SHA256",
0xC09B: "TLS_ECDHE_PSK_WITH_CAMELLIA_256_CBC_SHA384",
0xC09C: "TLS_RSA_WITH_AES_128_CCM",
0xC09D: "TLS_RSA_WITH_AES_256_CCM",
0xC09E: "TLS_DHE_RSA_WITH_AES_128_CCM",
0xC09F: "TLS_DHE_RSA_WITH_AES_256_CCM",
0xC0A0: "TLS_RSA_WITH_AES_128_CCM_8",
0xC0A1: "TLS_RSA_WITH_AES_256_CCM_8",
0xC0A2: "TLS_DHE_RSA_WITH_AES_128_CCM_8",
0xC0A3: "TLS_DHE_RSA_WITH_AES_256_CCM_8",
0xC0A4: "TLS_PSK_WITH_AES_128_CCM",
0xC0A5: "TLS_PSK_WITH_AES_256_CCM",
0xC0A6: "TLS_DHE_PSK_WITH_AES_128_CCM",
0xC0A7: "TLS_DHE_PSK_WITH_AES_256_CCM",
0xC0A8: "TLS_PSK_WITH_AES_128_CCM_8",
0xC0A9: "TLS_PSK_WITH_AES_256_CCM_8",
0xC0AA: "TLS_PSK_DHE_WITH_AES_128_CCM_8",
0xC0AB: "TLS_PSK_DHE_WITH_AES_256_CCM_8",
0xC0AC: "TLS_ECDHE_ECDSA_WITH_AES_128_CCM",
0xC0AD: "TLS_ECDHE_ECDSA_WITH_AES_256_CCM",
0xC0AE: "TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8",
0xC0AF: "TLS_ECDHE_ECDSA_WITH_AES_256_CCM_8",
0xC0B0: "TLS_ECCPWD_WITH_AES_128_GCM_SHA256",
0xC0B1: "TLS_ECCPWD_WITH_AES_256_GCM_SHA384",
0xC0B2: "TLS_ECCPWD_WITH_AES_128_CCM_SHA256",
0xC0B3: "TLS_ECCPWD_WITH_AES_256_CCM_SHA384",
0xC0B4: "TLS_SHA256_SHA256",
0xC0B5: "TLS_SHA384_SHA384",
0xC100: "TLS_GOSTR341112_256_WITH_KUZNYECHIK_CTR_OMAC",
0xC101: "TLS_GOSTR341112_256_WITH_MAGMA_CTR_OMAC",
0xC102: "TLS_GOSTR341112_256_WITH_28147_CNT_IMIT",
0xC103: "TLS_GOSTR341112_256_WITH_KUZNYECHIK_MGM_L",
0xC104: "TLS_GOSTR341112_256_WITH_MAGMA_MGM_L",
0xC105: "TLS_GOSTR341112_256_WITH_KUZNYECHIK_MGM_S",
0xC106: "TLS_GOSTR341112_256_WITH_MAGMA_MGM_S",
0xCACA: "GREASE",
0xCCA8: "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
0xCCA9: "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
0xCCAA: "TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
0xCCAB: "TLS_PSK_WITH_CHACHA20_POLY1305_SHA256",
0xCCAC: "TLS_ECDHE_PSK_WITH_CHACHA20_POLY1305_SHA256",
0xCCAD: "TLS_DHE_PSK_WITH_CHACHA20_POLY1305_SHA256",
0xCCAE: "TLS_RSA_PSK_WITH_CHACHA20_POLY1305_SHA256",
0xD001: "TLS_ECDHE_PSK_WITH_AES_128_GCM_SHA256",
0xD002: "TLS_ECDHE_PSK_WITH_AES_256_GCM_SHA384",
0xD003: "TLS_ECDHE_PSK_WITH_AES_128_CCM_8_SHA256",
0xD005: "TLS_ECDHE_PSK_WITH_AES_128_CCM_SHA256",
0xDADA: "GREASE",
0xEAEA: "GREASE",
0xFAFA: "GREASE",
};
/**
* GREASE values
*/
export const GREASE_VALUES = [
0x0a0a,
0x1a1a,
0x2a2a,
0x3a3a,
0x4a4a,
0x5a5a,
0x6a6a,
0x7a7a,
0x8a8a,
0x9a9a,
0xaaaa,
0xbaba,
0xcaca,
0xdada,
0xeaea,
0xfafa
];
/**
* Parses the supported_versions extension and returns the highest supported version.
* @param {Uint8Array} bytes
* @returns {number}
*/
export function parseHighestSupportedVersion(bytes) {
const s = new Stream(bytes);
// The Server Hello supported_versions extension simply contains the chosen version
if (s.length === 2) {
return s.readInt(2);
}
// Length
let i = s.readInt(1);
let highestVersion = 0;
while (s.hasMore() && i-- > 0) {
const v = s.readInt(2);
if (GREASE_VALUES.includes(v)) continue;
if (v > highestVersion) highestVersion = v;
}
return highestVersion;
}
/**
* Parses the application_layer_protocol_negotiation extension and returns the first value.
* @param {Uint8Array} bytes
* @returns {number}
*/
export function parseFirstALPNValue(bytes) {
const s = new Stream(bytes);
const alpnExtLen = s.readInt(2);
if (alpnExtLen < 3) return "00";
const strLen = s.readInt(1);
if (strLen < 2) return "00";
return s.readString(strLen);
}

174
src/core/lib/XXTEA.mjs Normal file
View File

@ -0,0 +1,174 @@
/**
* XXTEA library
*
* Encryption Algorithm Authors:
* David J. Wheeler
* Roger M. Needham
*
* @author Ma Bingyao [mabingyao@gmail.com]
* @author n1474335 [n1474335@gmail.com]
* @license MIT
*/
const DELTA = 0x9E3779B9;
/**
* Convert a buffer to a Uint8Array
* @param {Uint32Array} v
* @param {boolean} includeLength
* @returns {Uint8Array}
*/
function toUint8Array(v, includeLength) {
const length = v.length;
let n = length << 2;
if (includeLength) {
const m = v[length - 1];
n -= 4;
if ((m < n - 3) || (m > n)) {
return null;
}
n = m;
}
const bytes = new Uint8Array(n);
for (let i = 0; i < n; i++) {
bytes[i] = v[i >> 2] >> ((i & 3) << 3);
}
return bytes;
}
/**
* Convert a buffer to a Uint32Array
* @param {TypedArray} bs
* @param {boolean} includeLength
* @returns {Uint32Array}
*/
function toUint32Array(bs, includeLength) {
const length = bs.length;
let n = length >> 2;
if ((length & 3) !== 0) {
++n;
}
let v;
if (includeLength) {
v = new Uint32Array(n + 1);
v[n] = length;
} else {
v = new Uint32Array(n);
}
for (let i = 0; i < length; ++i) {
v[i >> 2] |= bs[i] << ((i & 3) << 3);
}
return v;
}
/**
* Mask an int to 32 bits
* @param {number} i
* @returns {number}
*/
function int32(i) {
return i & 0xFFFFFFFF;
}
/**
* MX function for data randomisation
* @param {number} sum
* @param {number} y
* @param {number} z
* @param {number} p
* @param {number} e
* @param {number} k
* @returns {number}
*/
function mx(sum, y, z, p, e, k) {
return ((z >>> 5 ^ y << 2) + (y >>> 3 ^ z << 4)) ^ ((sum ^ y) + (k[p & 3 ^ e] ^ z));
}
/**
* Ensure an array is a multiple of 16 bits
* @param {TypedArray} k
* @returns {TypedArray}
*/
function fixk(k) {
if (k.length < 16) {
const key = new Uint8Array(16);
key.set(k);
return key;
}
return k;
}
/**
* Performs XXTEA encryption on a Uint32Array
* @param {Uint32Array} v
* @param {Uint32Array} k
* @returns {Uint32Array}
*/
function encryptUint32Array(v, k) {
const length = v.length;
const n = length - 1;
let y, z, sum, e, p, q;
z = v[n];
sum = 0;
for (q = Math.floor(6 + 52 / length) | 0; q > 0; --q) {
sum = int32(sum + DELTA);
e = sum >>> 2 & 3;
for (p = 0; p < n; ++p) {
y = v[p + 1];
z = v[p] = int32(v[p] + mx(sum, y, z, p, e, k));
}
y = v[0];
z = v[n] = int32(v[n] + mx(sum, y, z, n, e, k));
}
return v;
}
/**
* Performs XXTEA decryption on a Uint32Array
* @param {Uint32Array} v
* @param {Uint32Array} k
* @returns {Uint32Array}
*/
function decryptUint32Array(v, k) {
const length = v.length;
const n = length - 1;
let y, z, sum, e, p;
y = v[0];
const q = Math.floor(6 + 52 / length);
for (sum = int32(q * DELTA); sum !== 0; sum = int32(sum - DELTA)) {
e = sum >>> 2 & 3;
for (p = n; p > 0; --p) {
z = v[p - 1];
y = v[p] = int32(v[p] - mx(sum, y, z, p, e, k));
}
z = v[n];
y = v[0] = int32(v[0] - mx(sum, y, z, 0, e, k));
}
return v;
}
/**
* Encrypt function
* @param {TypedArray} data
* @param {TypedArray} key
* @returns {Uint8Array}
*/
export function encrypt(data, key) {
if (data === undefined || data === null || data.length === 0) {
return data;
}
return toUint8Array(encryptUint32Array(toUint32Array(data, true), toUint32Array(fixk(key), false)), false);
}
/**
* Decrypt function
* @param {TypedArray} data
* @param {TypedArray} key
* @returns {Uint8Array}
*/
export function decrypt(data, key) {
if (data === undefined || data === null || data.length === 0) {
return data;
}
return toUint8Array(decryptUint32Array(toUint32Array(data, false), toUint32Array(fixk(key), false)), true);
}

View File

@ -70,10 +70,14 @@ class BlowfishDecrypt extends Operation {
inputType = args[3], inputType = args[3],
outputType = args[4]; outputType = args[4];
if (key.length !== 8) { if (key.length < 4 || key.length > 56) {
throw new OperationError(`Invalid key length: ${key.length} bytes throw new OperationError(`Invalid key length: ${key.length} bytes
Blowfish uses a key length of 8 bytes (64 bits).`); Blowfish's key length needs to be between 4 and 56 bytes (32-448 bits).`);
}
if (iv.length !== 8) {
throw new OperationError(`Invalid IV length: ${iv.length} bytes. Expected 8 bytes`);
} }
input = Utils.convertToByteString(input, inputType); input = Utils.convertToByteString(input, inputType);

View File

@ -70,10 +70,14 @@ class BlowfishEncrypt extends Operation {
inputType = args[3], inputType = args[3],
outputType = args[4]; outputType = args[4];
if (key.length !== 8) { if (key.length < 4 || key.length > 56) {
throw new OperationError(`Invalid key length: ${key.length} bytes throw new OperationError(`Invalid key length: ${key.length} bytes
Blowfish uses a key length of 8 bytes (64 bits).`); Blowfish's key length needs to be between 4 and 56 bytes (32-448 bits).`);
}
if (iv.length !== 8) {
throw new OperationError(`Invalid IV length: ${iv.length} bytes. Expected 8 bytes`);
} }
input = Utils.convertToByteString(input, inputType); input = Utils.convertToByteString(input, inputType);

View File

@ -6,7 +6,7 @@
import Operation from "../Operation.mjs"; import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs"; import OperationError from "../errors/OperationError.mjs";
import xmldom from "xmldom"; import xmldom from "@xmldom/xmldom";
import nwmatcher from "nwmatcher"; import nwmatcher from "nwmatcher";
/** /**

View File

@ -21,7 +21,7 @@ class CTPH extends Operation {
this.name = "CTPH"; this.name = "CTPH";
this.module = "Crypto"; this.module = "Crypto";
this.description = "Context Triggered Piecewise Hashing, also called Fuzzy Hashing, can match inputs that have homologies. Such inputs have sequences of identical bytes in the same order, although bytes in between these sequences may be different in both content and length.<br><br>CTPH was originally based on the work of Dr. Andrew Tridgell and a spam email detector called SpamSum. This method was adapted by Jesse Kornblum and published at the DFRWS conference in 2006 in a paper 'Identifying Almost Identical Files Using Context Triggered Piecewise Hashing'."; this.description = "Context Triggered Piecewise Hashing, also called Fuzzy Hashing, can match inputs that have homologies. Such inputs have sequences of identical bytes in the same order, although bytes in between these sequences may be different in both content and length.<br><br>CTPH was originally based on the work of Dr. Andrew Tridgell and a spam email detector called SpamSum. This method was adapted by Jesse Kornblum and published at the DFRWS conference in 2006 in a paper 'Identifying Almost Identical Files Using Context Triggered Piecewise Hashing'.";
this.infoURL = "https://forensicswiki.xyz/wiki/index.php?title=Context_Triggered_Piecewise_Hashing"; this.infoURL = "https://forensics.wiki/context_triggered_piecewise_hashing/";
this.inputType = "string"; this.inputType = "string";
this.outputType = "string"; this.outputType = "string";
this.args = []; this.args = [];

View File

@ -0,0 +1,98 @@
/**
* @author tedk [tedk@ted.do]
* @copyright Crown Copyright 2024
* @license Apache-2.0
*/
import Operation from "../Operation.mjs";
/**
* Caret/M-decode operation
*
* https://gist.githubusercontent.com/JaHIY/3c91bbf7bea5661e6abfbd1349ee81a2/raw/c7b480e9ff24bcb8f5287a8a8a2dcb9bf5628506/decode_m_notation.cpp
*/
class CaretMdecode extends Operation {
/**
* CaretMdecode constructor
*/
constructor() {
super();
this.name = "Caret/M-decode";
this.module = "Default";
this.description = "Decodes caret or M-encoded strings, i.e. ^M turns into a newline, M-^] turns into 0x9d. Sources such as `cat -v`.\n\nPlease be aware that when using `cat -v` ^_ (caret-underscore) will not be encoded, but represents a valid encoding (namely that of 0x1f).";
this.infoURL = "https://en.wikipedia.org/wiki/Caret_notation";
this.inputType = "string";
this.outputType = "byteArray";
this.args = [];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {byteArray}
*/
run(input, args) {
const bytes = [];
let prev = "";
for (let i = 0; i < input.length; i++) {
const charCode = input.charCodeAt(i);
const curChar = input.charAt(i);
if (prev === "M-^") {
if (charCode > 63 && charCode <= 95) {
bytes.push(charCode + 64);
} else if (charCode === 63) {
bytes.push(255);
} else {
bytes.push(77, 45, 94, charCode);
}
prev = "";
} else if (prev === "M-") {
if (curChar === "^") {
prev = prev + "^";
} else if (charCode >= 32 && charCode <= 126) {
bytes.push(charCode + 128);
prev = "";
} else {
bytes.push(77, 45, charCode);
prev = "";
}
} else if (prev === "M") {
if (curChar === "-") {
prev = prev + "-";
} else {
bytes.push(77, charCode);
prev = "";
}
} else if (prev === "^") {
if (charCode > 63 && charCode <= 126) {
bytes.push(charCode - 64);
} else if (charCode === 63) {
bytes.push(127);
} else {
bytes.push(94, charCode);
}
prev = "";
} else {
if (curChar === "M") {
prev = "M";
} else if (curChar === "^") {
prev = "^";
} else {
bytes.push(charCode);
}
}
}
return bytes;
}
}
export default CaretMdecode;

View File

@ -100,7 +100,7 @@ class ChaCha extends Operation {
super(); super();
this.name = "ChaCha"; this.name = "ChaCha";
this.module = "Default"; this.module = "Ciphers";
this.description = "ChaCha is a stream cipher designed by Daniel J. Bernstein. It is a variant of the Salsa stream cipher. Several parameterizations exist; 'ChaCha' may refer to the original construction, or to the variant as described in RFC-8439. ChaCha is often used with Poly1305, in the ChaCha20-Poly1305 AEAD construction.<br><br><b>Key:</b> ChaCha uses a key of 16 or 32 bytes (128 or 256 bits).<br><br><b>Nonce:</b> ChaCha uses a nonce of 8 or 12 bytes (64 or 96 bits).<br><br><b>Counter:</b> ChaCha uses a counter of 4 or 8 bytes (32 or 64 bits); together, the nonce and counter must add up to 16 bytes. The counter starts at zero at the start of the keystream, and is incremented at every 64 bytes."; this.description = "ChaCha is a stream cipher designed by Daniel J. Bernstein. It is a variant of the Salsa stream cipher. Several parameterizations exist; 'ChaCha' may refer to the original construction, or to the variant as described in RFC-8439. ChaCha is often used with Poly1305, in the ChaCha20-Poly1305 AEAD construction.<br><br><b>Key:</b> ChaCha uses a key of 16 or 32 bytes (128 or 256 bits).<br><br><b>Nonce:</b> ChaCha uses a nonce of 8 or 12 bytes (64 or 96 bits).<br><br><b>Counter:</b> ChaCha uses a counter of 4 or 8 bytes (32 or 64 bits); together, the nonce and counter must add up to 16 bytes. The counter starts at zero at the start of the keystream, and is incremented at every 64 bytes.";
this.infoURL = "https://wikipedia.org/wiki/Salsa20#ChaCha_variant"; this.infoURL = "https://wikipedia.org/wiki/Salsa20#ChaCha_variant";
this.inputType = "string"; this.inputType = "string";
@ -191,7 +191,7 @@ ChaCha uses a nonce of 8 or 12 bytes (64 or 96 bits).`);
if (outputType === "Hex") { if (outputType === "Hex") {
return toHex(output); return toHex(output);
} else { } else {
return Utils.arrayBufferToStr(output); return Utils.arrayBufferToStr(Uint8Array.from(output).buffer);
} }
} }

View File

@ -24,7 +24,7 @@ class CompareCTPHHashes extends Operation {
this.name = "Compare CTPH hashes"; this.name = "Compare CTPH hashes";
this.module = "Crypto"; this.module = "Crypto";
this.description = "Compares two Context Triggered Piecewise Hashing (CTPH) fuzzy hashes to determine the similarity between them on a scale of 0 to 100."; this.description = "Compares two Context Triggered Piecewise Hashing (CTPH) fuzzy hashes to determine the similarity between them on a scale of 0 to 100.";
this.infoURL = "https://forensicswiki.xyz/wiki/index.php?title=Context_Triggered_Piecewise_Hashing"; this.infoURL = "https://forensics.wiki/context_triggered_piecewise_hashing/";
this.inputType = "string"; this.inputType = "string";
this.outputType = "Number"; this.outputType = "Number";
this.args = [ this.args = [

View File

@ -24,7 +24,7 @@ class CompareSSDEEPHashes extends Operation {
this.name = "Compare SSDEEP hashes"; this.name = "Compare SSDEEP hashes";
this.module = "Crypto"; this.module = "Crypto";
this.description = "Compares two SSDEEP fuzzy hashes to determine the similarity between them on a scale of 0 to 100."; this.description = "Compares two SSDEEP fuzzy hashes to determine the similarity between them on a scale of 0 to 100.";
this.infoURL = "https://forensicswiki.xyz/wiki/index.php?title=Ssdeep"; this.infoURL = "https://forensics.wiki/ssdeep/";
this.inputType = "string"; this.inputType = "string";
this.outputType = "Number"; this.outputType = "Number";
this.args = [ this.args = [

View File

@ -0,0 +1,107 @@
/**
* @author tomgond [tom.gonda@gmail.com]
* @copyright Crown Copyright 2024
* @license Apache-2.0
*/
import Operation from "../Operation.mjs";
import moment from "moment-timezone";
import {DATETIME_FORMATS, FORMAT_EXAMPLES} from "../lib/DateTime.mjs";
/**
* DateTime Delta operation
*/
class DateTimeDelta extends Operation {
/**
* DateTimeDelta constructor
*/
constructor() {
super();
this.name = "DateTime Delta";
this.module = "Default";
this.description = "Calculates a new DateTime value given an input DateTime value and a time difference (delta) from the input DateTime value.";
this.inputType = "string";
this.outputType = "html";
this.args = [
{
"name": "Built in formats",
"type": "populateOption",
"value": DATETIME_FORMATS,
"target": 1
},
{
"name": "Input format string",
"type": "binaryString",
"value": "DD/MM/YYYY HH:mm:ss"
},
{
"name": "Time Operation",
"type": "option",
"value": ["Add", "Subtract"]
},
{
"name": "Days",
"type": "number",
"value": 0
},
{
"name": "Hours",
"type": "number",
"value": 0
},
{
"name": "Minutes",
"type": "number",
"value": 0
},
{
"name": "Seconds",
"type": "number",
"value": 0
}
];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
const inputTimezone = "UTC";
const inputFormat = args[1];
const operationType = args[2];
const daysDelta = args[3];
const hoursDelta = args[4];
const minutesDelta = args[5];
const secondsDelta = args[6];
let date = "";
try {
date = moment.tz(input, inputFormat, inputTimezone);
if (!date || date.format() === "Invalid date") throw Error;
} catch (err) {
return `Invalid format.\n\n${FORMAT_EXAMPLES}`;
}
let newDate;
if (operationType === "Add") {
newDate = date.add(daysDelta, "days")
.add(hoursDelta, "hours")
.add(minutesDelta, "minutes")
.add(secondsDelta, "seconds");
} else {
newDate = date.add(-daysDelta, "days")
.add(-hoursDelta, "hours")
.add(-minutesDelta, "minutes")
.add(-secondsDelta, "seconds");
}
return newDate.tz(inputTimezone).format(inputFormat.replace(/[<>]/g, ""));
}
}
export default DateTimeDelta;

View File

@ -62,11 +62,13 @@ class DeriveEVPKey extends Operation {
* @returns {string} * @returns {string}
*/ */
run(input, args) { run(input, args) {
const passphrase = Utils.convertToByteString(args[0].string, args[0].option), const passphrase = CryptoJS.enc.Latin1.parse(
Utils.convertToByteString(args[0].string, args[0].option)),
keySize = args[1] / 32, keySize = args[1] / 32,
iterations = args[2], iterations = args[2],
hasher = args[3], hasher = args[3],
salt = Utils.convertToByteString(args[4].string, args[4].option), salt = CryptoJS.enc.Latin1.parse(
Utils.convertToByteString(args[4].string, args[4].option)),
key = CryptoJS.EvpKDF(passphrase, salt, { // lgtm [js/insufficient-password-hash] key = CryptoJS.EvpKDF(passphrase, salt, { // lgtm [js/insufficient-password-hash]
keySize: keySize, keySize: keySize,
hasher: CryptoJS.algo[hasher], hasher: CryptoJS.algo[hasher],

View File

@ -119,9 +119,9 @@ class Diff extends Operation {
for (let i = 0; i < diff.length; i++) { for (let i = 0; i < diff.length; i++) {
if (diff[i].added) { if (diff[i].added) {
if (showAdded) output += "<span class='hl5'>" + Utils.escapeHtml(diff[i].value) + "</span>"; if (showAdded) output += "<ins>" + Utils.escapeHtml(diff[i].value) + "</ins>";
} else if (diff[i].removed) { } else if (diff[i].removed) {
if (showRemoved) output += "<span class='hl3'>" + Utils.escapeHtml(diff[i].value) + "</span>"; if (showRemoved) output += "<del>" + Utils.escapeHtml(diff[i].value) + "</del>";
} else if (!showSubtraction) { } else if (!showSubtraction) {
output += Utils.escapeHtml(diff[i].value); output += Utils.escapeHtml(diff[i].value);
} }

View File

@ -0,0 +1,107 @@
/**
* @author cplussharp
* @copyright Crown Copyright 2021
* @license Apache-2.0
*/
import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs";
import { fromHex } from "../lib/Hex.mjs";
import { toBase64 } from "../lib/Base64.mjs";
import r from "jsrsasign";
/**
* ECDSA Sign operation
*/
class ECDSASign extends Operation {
/**
* ECDSASign constructor
*/
constructor() {
super();
this.name = "ECDSA Sign";
this.module = "Ciphers";
this.description = "Sign a plaintext message with a PEM encoded EC key.";
this.infoURL = "https://wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm";
this.inputType = "string";
this.outputType = "string";
this.args = [
{
name: "ECDSA Private Key (PEM)",
type: "text",
value: "-----BEGIN EC PRIVATE KEY-----"
},
{
name: "Message Digest Algorithm",
type: "option",
value: [
"SHA-256",
"SHA-384",
"SHA-512",
"SHA-1",
"MD5"
]
},
{
name: "Output Format",
type: "option",
value: [
"ASN.1 HEX",
"P1363 HEX",
"JSON Web Signature",
"Raw JSON"
]
}
];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
const [keyPem, mdAlgo, outputFormat] = args;
if (keyPem.replace("-----BEGIN EC PRIVATE KEY-----", "").length === 0) {
throw new OperationError("Please enter a private key.");
}
const internalAlgorithmName = mdAlgo.replace("-", "") + "withECDSA";
const sig = new r.KJUR.crypto.Signature({ alg: internalAlgorithmName });
const key = r.KEYUTIL.getKey(keyPem);
if (key.type !== "EC") {
throw new OperationError("Provided key is not an EC key.");
}
if (!key.isPrivate) {
throw new OperationError("Provided key is not a private key.");
}
sig.init(key);
const signatureASN1Hex = sig.signString(input);
let result;
switch (outputFormat) {
case "ASN.1 HEX":
result = signatureASN1Hex;
break;
case "P1363 HEX":
result = r.KJUR.crypto.ECDSA.asn1SigToConcatSig(signatureASN1Hex);
break;
case "JSON Web Signature":
result = r.KJUR.crypto.ECDSA.asn1SigToConcatSig(signatureASN1Hex);
result = toBase64(fromHex(result), "A-Za-z0-9-_"); // base64url
break;
case "Raw JSON": {
const signatureRS = r.KJUR.crypto.ECDSA.parseSigHexInHexRS(signatureASN1Hex);
result = JSON.stringify(signatureRS);
break;
}
}
return result;
}
}
export default ECDSASign;

View File

@ -0,0 +1,146 @@
/**
* @author cplussharp
* @copyright Crown Copyright 2021
* @license Apache-2.0
*/
import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs";
import { fromBase64, toBase64 } from "../lib/Base64.mjs";
import { fromHex, toHexFast } from "../lib/Hex.mjs";
import r from "jsrsasign";
/**
* ECDSA Sign operation
*/
class ECDSASignatureConversion extends Operation {
/**
* ECDSASignatureConversion constructor
*/
constructor() {
super();
this.name = "ECDSA Signature Conversion";
this.module = "Ciphers";
this.description = "Convert an ECDSA signature between hex, asn1 and json.";
this.infoURL = "https://wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm";
this.inputType = "string";
this.outputType = "string";
this.args = [
{
name: "Input Format",
type: "option",
value: [
"Auto",
"ASN.1 HEX",
"P1363 HEX",
"JSON Web Signature",
"Raw JSON"
]
},
{
name: "Output Format",
type: "option",
value: [
"ASN.1 HEX",
"P1363 HEX",
"JSON Web Signature",
"Raw JSON"
]
}
];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
let inputFormat = args[0];
const outputFormat = args[1];
// detect input format
let inputJson;
if (inputFormat === "Auto") {
try {
inputJson = JSON.parse(input);
if (typeof(inputJson) === "object") {
inputFormat = "Raw JSON";
}
} catch {}
}
if (inputFormat === "Auto") {
const hexRegex = /^[a-f\d]{2,}$/gi;
if (hexRegex.test(input)) {
if (input.substring(0, 2) === "30" && r.ASN1HEX.isASN1HEX(input)) {
inputFormat = "ASN.1 HEX";
} else {
inputFormat = "P1363 HEX";
}
}
}
let inputBase64;
if (inputFormat === "Auto") {
try {
inputBase64 = fromBase64(input, "A-Za-z0-9-_", false);
inputFormat = "JSON Web Signature";
} catch {}
}
// convert input to ASN.1 hex
let signatureASN1Hex;
switch (inputFormat) {
case "Auto":
throw new OperationError("Signature format could not be detected");
case "ASN.1 HEX":
signatureASN1Hex = input;
break;
case "P1363 HEX":
signatureASN1Hex = r.KJUR.crypto.ECDSA.concatSigToASN1Sig(input);
break;
case "JSON Web Signature":
if (!inputBase64) inputBase64 = fromBase64(input, "A-Za-z0-9-_");
signatureASN1Hex = r.KJUR.crypto.ECDSA.concatSigToASN1Sig(toHexFast(inputBase64));
break;
case "Raw JSON": {
if (!inputJson) inputJson = JSON.parse(input);
if (!inputJson.r) {
throw new OperationError('No "r" value in the signature JSON');
}
if (!inputJson.s) {
throw new OperationError('No "s" value in the signature JSON');
}
signatureASN1Hex = r.KJUR.crypto.ECDSA.hexRSSigToASN1Sig(inputJson.r, inputJson.s);
break;
}
}
// convert ASN.1 hex to output format
let result;
switch (outputFormat) {
case "ASN.1 HEX":
result = signatureASN1Hex;
break;
case "P1363 HEX":
result = r.KJUR.crypto.ECDSA.asn1SigToConcatSig(signatureASN1Hex);
break;
case "JSON Web Signature":
result = r.KJUR.crypto.ECDSA.asn1SigToConcatSig(signatureASN1Hex);
result = toBase64(fromHex(result), "A-Za-z0-9-_"); // base64url
break;
case "Raw JSON": {
const signatureRS = r.KJUR.crypto.ECDSA.parseSigHexInHexRS(signatureASN1Hex);
result = JSON.stringify(signatureRS);
break;
}
}
return result;
}
}
export default ECDSASignatureConversion;

View File

@ -0,0 +1,154 @@
/**
* @author cplussharp
* @copyright Crown Copyright 2021
* @license Apache-2.0
*/
import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs";
import { fromBase64 } from "../lib/Base64.mjs";
import { toHexFast } from "../lib/Hex.mjs";
import r from "jsrsasign";
/**
* ECDSA Verify operation
*/
class ECDSAVerify extends Operation {
/**
* ECDSAVerify constructor
*/
constructor() {
super();
this.name = "ECDSA Verify";
this.module = "Ciphers";
this.description = "Verify a message against a signature and a public PEM encoded EC key.";
this.infoURL = "https://wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm";
this.inputType = "string";
this.outputType = "string";
this.args = [
{
name: "Input Format",
type: "option",
value: [
"Auto",
"ASN.1 HEX",
"P1363 HEX",
"JSON Web Signature",
"Raw JSON"
]
},
{
name: "Message Digest Algorithm",
type: "option",
value: [
"SHA-256",
"SHA-384",
"SHA-512",
"SHA-1",
"MD5"
]
},
{
name: "ECDSA Public Key (PEM)",
type: "text",
value: "-----BEGIN PUBLIC KEY-----"
},
{
name: "Message",
type: "text",
value: ""
}
];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
let inputFormat = args[0];
const [, mdAlgo, keyPem, msg] = args;
if (keyPem.replace("-----BEGIN PUBLIC KEY-----", "").length === 0) {
throw new OperationError("Please enter a public key.");
}
// detect input format
let inputJson;
if (inputFormat === "Auto") {
try {
inputJson = JSON.parse(input);
if (typeof(inputJson) === "object") {
inputFormat = "Raw JSON";
}
} catch {}
}
if (inputFormat === "Auto") {
const hexRegex = /^[a-f\d]{2,}$/gi;
if (hexRegex.test(input)) {
if (input.substring(0, 2) === "30" && r.ASN1HEX.isASN1HEX(input)) {
inputFormat = "ASN.1 HEX";
} else {
inputFormat = "P1363 HEX";
}
}
}
let inputBase64;
if (inputFormat === "Auto") {
try {
inputBase64 = fromBase64(input, "A-Za-z0-9-_", false);
inputFormat = "JSON Web Signature";
} catch {}
}
// convert to ASN.1 signature
let signatureASN1Hex;
switch (inputFormat) {
case "Auto":
throw new OperationError("Signature format could not be detected");
case "ASN.1 HEX":
signatureASN1Hex = input;
break;
case "P1363 HEX":
signatureASN1Hex = r.KJUR.crypto.ECDSA.concatSigToASN1Sig(input);
break;
case "JSON Web Signature":
if (!inputBase64) inputBase64 = fromBase64(input, "A-Za-z0-9-_");
signatureASN1Hex = r.KJUR.crypto.ECDSA.concatSigToASN1Sig(toHexFast(inputBase64));
break;
case "Raw JSON": {
if (!inputJson) inputJson = JSON.parse(input);
if (!inputJson.r) {
throw new OperationError('No "r" value in the signature JSON');
}
if (!inputJson.s) {
throw new OperationError('No "s" value in the signature JSON');
}
signatureASN1Hex = r.KJUR.crypto.ECDSA.hexRSSigToASN1Sig(inputJson.r, inputJson.s);
break;
}
}
// verify signature
const internalAlgorithmName = mdAlgo.replace("-", "") + "withECDSA";
const sig = new r.KJUR.crypto.Signature({ alg: internalAlgorithmName });
const key = r.KEYUTIL.getKey(keyPem);
if (key.type !== "EC") {
throw new OperationError("Provided key is not an EC key.");
}
if (!key.isPublic) {
throw new OperationError("Provided key is not a public key.");
}
sig.init(key);
sig.updateString(msg);
const result = sig.verify(signatureASN1Hex);
return result ? "Verified OK" : "Verification Failure";
}
}
export default ECDSAVerify;

View File

@ -0,0 +1,84 @@
/**
* @author mshwed [m@ttshwed.com]
* @copyright Crown Copyright 2019
* @license Apache-2.0
*/
import Operation from "../Operation.mjs";
import { search } from "../lib/Extract.mjs";
/**
* Extract Hash Values operation
*/
class ExtractHashes extends Operation {
/**
* ExtractHashValues constructor
*/
constructor() {
super();
this.name = "Extract hashes";
this.module = "Regex";
this.description = "Extracts potential hashes based on hash character length";
this.infoURL = "https://wikipedia.org/wiki/Comparison_of_cryptographic_hash_functions";
this.inputType = "string";
this.outputType = "string";
this.args = [
{
name: "Hash character length",
type: "number",
value: 40
},
{
name: "All hashes",
type: "boolean",
value: false
},
{
name: "Display Total",
type: "boolean",
value: false
}
];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
const results = [];
let hashCount = 0;
const [hashLength, searchAllHashes, showDisplayTotal] = args;
// Convert character length to bit length
let hashBitLengths = [(hashLength / 2) * 8];
if (searchAllHashes) hashBitLengths = [4, 8, 16, 32, 64, 128, 160, 192, 224, 256, 320, 384, 512, 1024];
for (const hashBitLength of hashBitLengths) {
// Convert bit length to character length
const hashCharacterLength = (hashBitLength / 8) * 2;
const regex = new RegExp(`(\\b|^)[a-f0-9]{${hashCharacterLength}}(\\b|$)`, "g");
const searchResults = search(input, regex, null, false);
hashCount += searchResults.length;
results.push(...searchResults);
}
let output = "";
if (showDisplayTotal) {
output = `Total Results: ${hashCount}\n\n`;
}
output = output + results.join("\n");
return output;
}
}
export default ExtractHashes;

View File

@ -66,7 +66,7 @@ class ExtractIPAddresses extends Operation {
run(input, args) { run(input, args) {
const [includeIpv4, includeIpv6, removeLocal, displayTotal, sort, unique] = args, const [includeIpv4, includeIpv6, removeLocal, displayTotal, sort, unique] = args,
ipv4 = "(?:(?:\\d|[01]?\\d\\d|2[0-4]\\d|25[0-5])\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?\\d\\d|\\d)(?:\\/\\d{1,2})?", ipv4 = "(?:(?:\\d|[01]?\\d\\d|2[0-4]\\d|25[0-5])\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?\\d\\d|\\d)(?:\\/\\d{1,2})?",
ipv6 = "((?=.*::)(?!.*::.+::)(::)?([\\dA-F]{1,4}:(:|\\b)|){5}|([\\dA-F]{1,4}:){6})((([\\dA-F]{1,4}((?!\\3)::|:\\b|(?![\\dA-F])))|(?!\\2\\3)){2}|(((2[0-4]|1\\d|[1-9])?\\d|25[0-5])\\.?\\b){4})"; ipv6 = "((?=.*::)(?!.*::.+::)(::)?([\\dA-F]{1,4}:(:|\\b)|){5}|([\\dA-F]{1,4}:){6})(([\\dA-F]{1,4}((?!\\3)::|:\\b|(?![\\dA-F])))|(?!\\2\\3)){2}";
let ips = ""; let ips = "";
if (includeIpv4 && includeIpv6) { if (includeIpv4 && includeIpv6) {

View File

@ -0,0 +1,78 @@
/**
* @author arnydo [github@arnydo.com]
* @copyright Crown Copyright 2019
* @license Apache-2.0
*/
import Operation from "../Operation.mjs";
/**
* FangURL operation
*/
class FangURL extends Operation {
/**
* FangURL constructor
*/
constructor() {
super();
this.name = "Fang URL";
this.module = "Default";
this.description = "Takes a 'Defanged' Universal Resource Locator (URL) and 'Fangs' it. Meaning, it removes the alterations (defanged) that render it useless so that it can be used again.";
this.infoURL = "https://isc.sans.edu/forums/diary/Defang+all+the+things/22744/";
this.inputType = "string";
this.outputType = "string";
this.args = [
{
name: "Restore [.]",
type: "boolean",
value: true
},
{
name: "Restore hxxp",
type: "boolean",
value: true
},
{
name: "Restore ://",
type: "boolean",
value: true
}
];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
const [dots, http, slashes] = args;
input = fangURL(input, dots, http, slashes);
return input;
}
}
/**
* Defangs a given URL
*
* @param {string} url
* @param {boolean} dots
* @param {boolean} http
* @param {boolean} slashes
* @returns {string}
*/
function fangURL(url, dots, http, slashes) {
if (dots) url = url.replace(/\[\.\]/g, ".");
if (http) url = url.replace(/hxxp/g, "http");
if (slashes) url = url.replace(/\[:\/\/\]/g, "://");
return url;
}
export default FangURL;

View File

@ -0,0 +1,63 @@
/**
* @author Karsten Silkenbäumer [github.com/kassi]
* @copyright Karsten Silkenbäumer 2019
* @license Apache-2.0
*/
import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs";
import fernet from "fernet";
/**
* FernetDecrypt operation
*/
class FernetDecrypt extends Operation {
/**
* FernetDecrypt constructor
*/
constructor() {
super();
this.name = "Fernet Decrypt";
this.module = "Default";
this.description = "Fernet is a symmetric encryption method which makes sure that the message encrypted cannot be manipulated/read without the key. It uses URL safe encoding for the keys. Fernet uses 128-bit AES in CBC mode and PKCS7 padding, with HMAC using SHA256 for authentication. The IV is created from os.random().<br><br><b>Key:</b> The key must be 32 bytes (256 bits) encoded with Base64.";
this.infoURL = "https://asecuritysite.com/encryption/fer";
this.inputType = "string";
this.outputType = "string";
this.args = [
{
"name": "Key",
"type": "string",
"value": ""
},
];
this.patterns = [
{
match: "^[A-Z\\d\\-_=]{20,}$",
flags: "i",
args: []
},
];
}
/**
* @param {String} input
* @param {Object[]} args
* @returns {String}
*/
run(input, args) {
const [secretInput] = args;
try {
const secret = new fernet.Secret(secretInput);
const token = new fernet.Token({
secret: secret,
token: input,
ttl: 0
});
return token.decode();
} catch (err) {
throw new OperationError(err);
}
}
}
export default FernetDecrypt;

View File

@ -0,0 +1,54 @@
/**
* @author Karsten Silkenbäumer [github.com/kassi]
* @copyright Karsten Silkenbäumer 2019
* @license Apache-2.0
*/
import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs";
import fernet from "fernet";
/**
* FernetEncrypt operation
*/
class FernetEncrypt extends Operation {
/**
* FernetEncrypt constructor
*/
constructor() {
super();
this.name = "Fernet Encrypt";
this.module = "Default";
this.description = "Fernet is a symmetric encryption method which makes sure that the message encrypted cannot be manipulated/read without the key. It uses URL safe encoding for the keys. Fernet uses 128-bit AES in CBC mode and PKCS7 padding, with HMAC using SHA256 for authentication. The IV is created from os.random().<br><br><b>Key:</b> The key must be 32 bytes (256 bits) encoded with Base64.";
this.infoURL = "https://asecuritysite.com/encryption/fer";
this.inputType = "string";
this.outputType = "string";
this.args = [
{
"name": "Key",
"type": "string",
"value": ""
},
];
}
/**
* @param {String} input
* @param {Object[]} args
* @returns {String}
*/
run(input, args) {
const [secretInput] = args;
try {
const secret = new fernet.Secret(secretInput);
const token = new fernet.Token({
secret: secret,
});
return token.encode(input);
} catch (err) {
throw new OperationError(err);
}
}
}
export default FernetEncrypt;

View File

@ -0,0 +1,94 @@
/**
* @author sw5678
* @copyright Crown Copyright 2023
* @license Apache-2.0
*/
import Operation from "../Operation.mjs";
import Utils from "../Utils.mjs";
import {INPUT_DELIM_OPTIONS} from "../lib/Delim.mjs";
/**
* Unique operation
*/
class FileTree extends Operation {
/**
* Unique constructor
*/
constructor() {
super();
this.name = "File Tree";
this.module = "Default";
this.description = "Creates a file tree from a list of file paths (similar to the tree command in Linux)";
this.infoURL = "https://wikipedia.org/wiki/Tree_(command)";
this.inputType = "string";
this.outputType = "string";
this.args = [
{
name: "File Path Delimiter",
type: "binaryString",
value: "/"
},
{
name: "Delimiter",
type: "option",
value: INPUT_DELIM_OPTIONS
}
];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
// Set up arrow and pipe for nice output display
const ARROW = "|---";
const PIPE = "| ";
// Get args from input
const fileDelim = args[0];
const entryDelim = Utils.charRep(args[1]);
// Store path to print
const completedList = [];
const printList = [];
// Loop through all entries
const filePaths = input.split(entryDelim).unique().sort();
for (let i = 0; i < filePaths.length; i++) {
// Split by file delimiter
let path = filePaths[i].split(fileDelim);
if (path[0] === "") {
path = path.slice(1, path.length);
}
for (let j = 0; j < path.length; j++) {
let printLine;
let key;
if (j === 0) {
printLine = path[j];
key = path[j];
} else {
printLine = PIPE.repeat(j-1) + ARROW + path[j];
key = path.slice(0, j+1).join("/");
}
// Check to see we have already added that path
if (!completedList.includes(key)) {
completedList.push(key);
printList.push(printLine);
}
}
}
return printList.join("\n");
}
}
export default FileTree;

View File

@ -60,7 +60,7 @@ class FromBase58 extends Operation {
run(input, args) { run(input, args) {
let alphabet = args[0] || ALPHABET_OPTIONS[0].value; let alphabet = args[0] || ALPHABET_OPTIONS[0].value;
const removeNonAlphaChars = args[1] === undefined ? true : args[1], const removeNonAlphaChars = args[1] === undefined ? true : args[1],
result = [0]; result = [];
alphabet = Utils.expandAlphRange(alphabet).join(""); alphabet = Utils.expandAlphRange(alphabet).join("");
@ -87,11 +87,9 @@ class FromBase58 extends Operation {
} }
} }
let carry = result[0] * 58 + index; let carry = index;
result[0] = carry & 0xFF;
carry = carry >> 8;
for (let i = 1; i < result.length; i++) { for (let i = 0; i < result.length; i++) {
carry += result[i] * 58; carry += result[i] * 58;
result[i] = carry & 0xFF; result[i] = carry & 0xFF;
carry = carry >> 8; carry = carry >> 8;

View File

@ -0,0 +1,55 @@
/**
* @author sg5506844 [sg5506844@gmail.com]
* @copyright Crown Copyright 2021
* @license Apache-2.0
*/
import { base92Ord } from "../lib/Base92.mjs";
import Operation from "../Operation.mjs";
/**
* From Base92 operation
*/
class FromBase92 extends Operation {
/**
* FromBase92 constructor
*/
constructor() {
super();
this.name = "From Base92";
this.module = "Default";
this.description = "Base92 is a notation for encoding arbitrary byte data using a restricted set of symbols that can be conveniently used by humans and processed by computers.";
this.infoURL = "https://wikipedia.org/wiki/List_of_numeral_systems";
this.inputType = "string";
this.outputType = "byteArray";
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {byteArray}
*/
run(input, args) {
const res = [];
let bitString = "";
for (let i = 0; i < input.length; i += 2) {
if (i + 1 !== input.length) {
const x = base92Ord(input[i]) * 91 + base92Ord(input[i + 1]);
bitString += x.toString(2).padStart(13, "0");
} else {
const x = base92Ord(input[i]);
bitString += x.toString(2).padStart(6, "0");
}
while (bitString.length >= 8) {
res.push(parseInt(bitString.slice(0, 8), 2));
bitString = bitString.slice(8);
}
}
return res;
}
}
export default FromBase92;

View File

@ -0,0 +1,78 @@
/**
* @author tcode2k16 [tcode2k16@gmail.com]
* @copyright Crown Copyright 2019
* @license Apache-2.0
*/
import Operation from "../Operation.mjs";
import Utils from "../Utils.mjs";
import ieee754 from "ieee754";
import {DELIM_OPTIONS} from "../lib/Delim.mjs";
/**
* From Float operation
*/
class FromFloat extends Operation {
/**
* FromFloat constructor
*/
constructor() {
super();
this.name = "From Float";
this.module = "Default";
this.description = "Convert from IEEE754 Floating Point Numbers";
this.infoURL = "https://wikipedia.org/wiki/IEEE_754";
this.inputType = "string";
this.outputType = "byteArray";
this.args = [
{
"name": "Endianness",
"type": "option",
"value": [
"Big Endian",
"Little Endian"
]
},
{
"name": "Size",
"type": "option",
"value": [
"Float (4 bytes)",
"Double (8 bytes)"
]
},
{
"name": "Delimiter",
"type": "option",
"value": DELIM_OPTIONS
}
];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {byteArray}
*/
run(input, args) {
if (input.length === 0) return [];
const [endianness, size, delimiterName] = args;
const delim = Utils.charRep(delimiterName || "Space");
const byteSize = size === "Double (8 bytes)" ? 8 : 4;
const isLE = endianness === "Little Endian";
const mLen = byteSize === 4 ? 23 : 52;
const floats = input.split(delim);
const output = new Array(floats.length*byteSize);
for (let i = 0; i < floats.length; i++) {
ieee754.write(output, parseFloat(floats[i]), i*byteSize, isLE, mLen, byteSize);
}
return output;
}
}
export default FromFloat;

View File

@ -55,21 +55,18 @@ class GOSTDecrypt extends Operation {
type: "argSelector", type: "argSelector",
value: [ value: [
{ {
name: "GOST 28147 (Magma, 1989)", name: "GOST 28147 (1989)",
off: [5],
on: [6] on: [6]
}, },
{ {
name: "GOST R 34.12 (Kuznyechik, 2015)", name: "GOST R 34.12 (Magma, 2015)",
on: [5], off: [5]
off: [6]
}
]
}, },
{ {
name: "Block length", name: "GOST R 34.12 (Kuznyechik, 2015)",
type: "option", off: [5]
value: ["64", "128"] }
]
}, },
{ {
name: "sBox", name: "sBox",
@ -100,14 +97,30 @@ class GOSTDecrypt extends Operation {
* @returns {string} * @returns {string}
*/ */
async run(input, args) { async run(input, args) {
const [keyObj, ivObj, inputType, outputType, version, length, sBox, blockMode, keyMeshing, padding] = args; const [keyObj, ivObj, inputType, outputType, version, sBox, blockMode, keyMeshing, padding] = args;
const key = toHexFast(Utils.convertToByteArray(keyObj.string, keyObj.option)); const key = toHexFast(Utils.convertToByteArray(keyObj.string, keyObj.option));
const iv = toHexFast(Utils.convertToByteArray(ivObj.string, ivObj.option)); const iv = toHexFast(Utils.convertToByteArray(ivObj.string, ivObj.option));
input = inputType === "Hex" ? input : toHexFast(Utils.strToArrayBuffer(input)); input = inputType === "Hex" ? input : toHexFast(Utils.strToArrayBuffer(input));
const versionNum = version === "GOST 28147 (Magma, 1989)" ? 1989 : 2015; let blockLength, versionNum;
const blockLength = versionNum === 1989 ? 64 : parseInt(length, 10); switch (version) {
case "GOST 28147 (1989)":
versionNum = 1989;
blockLength = 64;
break;
case "GOST R 34.12 (Magma, 2015)":
versionNum = 2015;
blockLength = 64;
break;
case "GOST R 34.12 (Kuznyechik, 2015)":
versionNum = 2015;
blockLength = 128;
break;
default:
throw new OperationError(`Unknown algorithm version: ${version}`);
}
const sBoxVal = versionNum === 1989 ? sBox : null; const sBoxVal = versionNum === 1989 ? sBox : null;
const algorithm = { const algorithm = {

View File

@ -55,21 +55,18 @@ class GOSTEncrypt extends Operation {
type: "argSelector", type: "argSelector",
value: [ value: [
{ {
name: "GOST 28147 (Magma, 1989)", name: "GOST 28147 (1989)",
off: [5],
on: [6] on: [6]
}, },
{ {
name: "GOST R 34.12 (Kuznyechik, 2015)", name: "GOST R 34.12 (Magma, 2015)",
on: [5], off: [5]
off: [6]
}
]
}, },
{ {
name: "Block length", name: "GOST R 34.12 (Kuznyechik, 2015)",
type: "option", off: [5]
value: ["64", "128"] }
]
}, },
{ {
name: "sBox", name: "sBox",
@ -100,14 +97,30 @@ class GOSTEncrypt extends Operation {
* @returns {string} * @returns {string}
*/ */
async run(input, args) { async run(input, args) {
const [keyObj, ivObj, inputType, outputType, version, length, sBox, blockMode, keyMeshing, padding] = args; const [keyObj, ivObj, inputType, outputType, version, sBox, blockMode, keyMeshing, padding] = args;
const key = toHexFast(Utils.convertToByteArray(keyObj.string, keyObj.option)); const key = toHexFast(Utils.convertToByteArray(keyObj.string, keyObj.option));
const iv = toHexFast(Utils.convertToByteArray(ivObj.string, ivObj.option)); const iv = toHexFast(Utils.convertToByteArray(ivObj.string, ivObj.option));
input = inputType === "Hex" ? input : toHexFast(Utils.strToArrayBuffer(input)); input = inputType === "Hex" ? input : toHexFast(Utils.strToArrayBuffer(input));
const versionNum = version === "GOST 28147 (Magma, 1989)" ? 1989 : 2015; let blockLength, versionNum;
const blockLength = versionNum === 1989 ? 64 : parseInt(length, 10); switch (version) {
case "GOST 28147 (1989)":
versionNum = 1989;
blockLength = 64;
break;
case "GOST R 34.12 (Magma, 2015)":
versionNum = 2015;
blockLength = 64;
break;
case "GOST R 34.12 (Kuznyechik, 2015)":
versionNum = 2015;
blockLength = 128;
break;
default:
throw new OperationError(`Unknown algorithm version: ${version}`);
}
const sBoxVal = versionNum === 1989 ? sBox : null; const sBoxVal = versionNum === 1989 ? sBox : null;
const algorithm = { const algorithm = {

View File

@ -55,21 +55,18 @@ class GOSTKeyUnwrap extends Operation {
type: "argSelector", type: "argSelector",
value: [ value: [
{ {
name: "GOST 28147 (Magma, 1989)", name: "GOST 28147 (1989)",
off: [5],
on: [6] on: [6]
}, },
{ {
name: "GOST R 34.12 (Kuznyechik, 2015)", name: "GOST R 34.12 (Magma, 2015)",
on: [5], off: [5]
off: [6]
}
]
}, },
{ {
name: "Block length", name: "GOST R 34.12 (Kuznyechik, 2015)",
type: "option", off: [5]
value: ["64", "128"] }
]
}, },
{ {
name: "sBox", name: "sBox",
@ -90,14 +87,30 @@ class GOSTKeyUnwrap extends Operation {
* @returns {string} * @returns {string}
*/ */
async run(input, args) { async run(input, args) {
const [keyObj, ukmObj, inputType, outputType, version, length, sBox, keyWrapping] = args; const [keyObj, ukmObj, inputType, outputType, version, sBox, keyWrapping] = args;
const key = toHexFast(Utils.convertToByteArray(keyObj.string, keyObj.option)); const key = toHexFast(Utils.convertToByteArray(keyObj.string, keyObj.option));
const ukm = toHexFast(Utils.convertToByteArray(ukmObj.string, ukmObj.option)); const ukm = toHexFast(Utils.convertToByteArray(ukmObj.string, ukmObj.option));
input = inputType === "Hex" ? input : toHexFast(Utils.strToArrayBuffer(input)); input = inputType === "Hex" ? input : toHexFast(Utils.strToArrayBuffer(input));
const versionNum = version === "GOST 28147 (Magma, 1989)" ? 1989 : 2015; let blockLength, versionNum;
const blockLength = versionNum === 1989 ? 64 : parseInt(length, 10); switch (version) {
case "GOST 28147 (1989)":
versionNum = 1989;
blockLength = 64;
break;
case "GOST R 34.12 (Magma, 2015)":
versionNum = 2015;
blockLength = 64;
break;
case "GOST R 34.12 (Kuznyechik, 2015)":
versionNum = 2015;
blockLength = 128;
break;
default:
throw new OperationError(`Unknown algorithm version: ${version}`);
}
const sBoxVal = versionNum === 1989 ? sBox : null; const sBoxVal = versionNum === 1989 ? sBox : null;
const algorithm = { const algorithm = {

View File

@ -55,21 +55,18 @@ class GOSTKeyWrap extends Operation {
type: "argSelector", type: "argSelector",
value: [ value: [
{ {
name: "GOST 28147 (Magma, 1989)", name: "GOST 28147 (1989)",
off: [5],
on: [6] on: [6]
}, },
{ {
name: "GOST R 34.12 (Kuznyechik, 2015)", name: "GOST R 34.12 (Magma, 2015)",
on: [5], off: [5]
off: [6]
}
]
}, },
{ {
name: "Block length", name: "GOST R 34.12 (Kuznyechik, 2015)",
type: "option", off: [5]
value: ["64", "128"] }
]
}, },
{ {
name: "sBox", name: "sBox",
@ -90,14 +87,30 @@ class GOSTKeyWrap extends Operation {
* @returns {string} * @returns {string}
*/ */
async run(input, args) { async run(input, args) {
const [keyObj, ukmObj, inputType, outputType, version, length, sBox, keyWrapping] = args; const [keyObj, ukmObj, inputType, outputType, version, sBox, keyWrapping] = args;
const key = toHexFast(Utils.convertToByteArray(keyObj.string, keyObj.option)); const key = toHexFast(Utils.convertToByteArray(keyObj.string, keyObj.option));
const ukm = toHexFast(Utils.convertToByteArray(ukmObj.string, ukmObj.option)); const ukm = toHexFast(Utils.convertToByteArray(ukmObj.string, ukmObj.option));
input = inputType === "Hex" ? input : toHexFast(Utils.strToArrayBuffer(input)); input = inputType === "Hex" ? input : toHexFast(Utils.strToArrayBuffer(input));
const versionNum = version === "GOST 28147 (Magma, 1989)" ? 1989 : 2015; let blockLength, versionNum;
const blockLength = versionNum === 1989 ? 64 : parseInt(length, 10); switch (version) {
case "GOST 28147 (1989)":
versionNum = 1989;
blockLength = 64;
break;
case "GOST R 34.12 (Magma, 2015)":
versionNum = 2015;
blockLength = 64;
break;
case "GOST R 34.12 (Kuznyechik, 2015)":
versionNum = 2015;
blockLength = 128;
break;
default:
throw new OperationError(`Unknown algorithm version: ${version}`);
}
const sBoxVal = versionNum === 1989 ? sBox : null; const sBoxVal = versionNum === 1989 ? sBox : null;
const algorithm = { const algorithm = {

View File

@ -55,21 +55,18 @@ class GOSTSign extends Operation {
type: "argSelector", type: "argSelector",
value: [ value: [
{ {
name: "GOST 28147 (Magma, 1989)", name: "GOST 28147 (1989)",
off: [5],
on: [6] on: [6]
}, },
{ {
name: "GOST R 34.12 (Kuznyechik, 2015)", name: "GOST R 34.12 (Magma, 2015)",
on: [5], off: [5]
off: [6]
}
]
}, },
{ {
name: "Block length", name: "GOST R 34.12 (Kuznyechik, 2015)",
type: "option", off: [5]
value: ["64", "128"] }
]
}, },
{ {
name: "sBox", name: "sBox",
@ -93,14 +90,30 @@ class GOSTSign extends Operation {
* @returns {string} * @returns {string}
*/ */
async run(input, args) { async run(input, args) {
const [keyObj, ivObj, inputType, outputType, version, length, sBox, macLength] = args; const [keyObj, ivObj, inputType, outputType, version, sBox, macLength] = args;
const key = toHexFast(Utils.convertToByteArray(keyObj.string, keyObj.option)); const key = toHexFast(Utils.convertToByteArray(keyObj.string, keyObj.option));
const iv = toHexFast(Utils.convertToByteArray(ivObj.string, ivObj.option)); const iv = toHexFast(Utils.convertToByteArray(ivObj.string, ivObj.option));
input = inputType === "Hex" ? input : toHexFast(Utils.strToArrayBuffer(input)); input = inputType === "Hex" ? input : toHexFast(Utils.strToArrayBuffer(input));
const versionNum = version === "GOST 28147 (Magma, 1989)" ? 1989 : 2015; let blockLength, versionNum;
const blockLength = versionNum === 1989 ? 64 : parseInt(length, 10); switch (version) {
case "GOST 28147 (1989)":
versionNum = 1989;
blockLength = 64;
break;
case "GOST R 34.12 (Magma, 2015)":
versionNum = 2015;
blockLength = 64;
break;
case "GOST R 34.12 (Kuznyechik, 2015)":
versionNum = 2015;
blockLength = 128;
break;
default:
throw new OperationError(`Unknown algorithm version: ${version}`);
}
const sBoxVal = versionNum === 1989 ? sBox : null; const sBoxVal = versionNum === 1989 ? sBox : null;
const algorithm = { const algorithm = {

View File

@ -56,21 +56,18 @@ class GOSTVerify extends Operation {
type: "argSelector", type: "argSelector",
value: [ value: [
{ {
name: "GOST 28147 (Magma, 1989)", name: "GOST 28147 (1989)",
off: [5],
on: [6] on: [6]
}, },
{ {
name: "GOST R 34.12 (Kuznyechik, 2015)", name: "GOST R 34.12 (Magma, 2015)",
on: [5], off: [5]
off: [6]
}
]
}, },
{ {
name: "Block length", name: "GOST R 34.12 (Kuznyechik, 2015)",
type: "option", off: [5]
value: ["64", "128"] }
]
}, },
{ {
name: "sBox", name: "sBox",
@ -86,15 +83,31 @@ class GOSTVerify extends Operation {
* @returns {string} * @returns {string}
*/ */
async run(input, args) { async run(input, args) {
const [keyObj, ivObj, macObj, inputType, version, length, sBox] = args; const [keyObj, ivObj, macObj, inputType, version, sBox] = args;
const key = toHexFast(Utils.convertToByteArray(keyObj.string, keyObj.option)); const key = toHexFast(Utils.convertToByteArray(keyObj.string, keyObj.option));
const iv = toHexFast(Utils.convertToByteArray(ivObj.string, ivObj.option)); const iv = toHexFast(Utils.convertToByteArray(ivObj.string, ivObj.option));
const mac = toHexFast(Utils.convertToByteArray(macObj.string, macObj.option)); const mac = toHexFast(Utils.convertToByteArray(macObj.string, macObj.option));
input = inputType === "Hex" ? input : toHexFast(Utils.strToArrayBuffer(input)); input = inputType === "Hex" ? input : toHexFast(Utils.strToArrayBuffer(input));
const versionNum = version === "GOST 28147 (Magma, 1989)" ? 1989 : 2015; let blockLength, versionNum;
const blockLength = versionNum === 1989 ? 64 : parseInt(length, 10); switch (version) {
case "GOST 28147 (1989)":
versionNum = 1989;
blockLength = 64;
break;
case "GOST R 34.12 (Magma, 2015)":
versionNum = 2015;
blockLength = 64;
break;
case "GOST R 34.12 (Kuznyechik, 2015)":
versionNum = 2015;
blockLength = 128;
break;
default:
throw new OperationError(`Unknown algorithm version: ${version}`);
}
const sBoxVal = versionNum === 1989 ? sBox : null; const sBoxVal = versionNum === 1989 ? sBox : null;
const algorithm = { const algorithm = {

View File

@ -0,0 +1,102 @@
/**
* @author cplussharp
* @copyright Crown Copyright 2021
* @license Apache-2.0
*/
import Operation from "../Operation.mjs";
import { cryptNotice } from "../lib/Crypt.mjs";
import r from "jsrsasign";
/**
* Generate ECDSA Key Pair operation
*/
class GenerateECDSAKeyPair extends Operation {
/**
* GenerateECDSAKeyPair constructor
*/
constructor() {
super();
this.name = "Generate ECDSA Key Pair";
this.module = "Ciphers";
this.description = `Generate an ECDSA key pair with a given Curve.<br><br>${cryptNotice}`;
this.infoURL = "https://wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm";
this.inputType = "string";
this.outputType = "string";
this.args = [
{
name: "Elliptic Curve",
type: "option",
value: [
"P-256",
"P-384",
"P-521"
]
},
{
name: "Output Format",
type: "option",
value: [
"PEM",
"DER",
"JWK"
]
}
];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {string}
*/
async run(input, args) {
const [curveName, outputFormat] = args;
return new Promise((resolve, reject) => {
let internalCurveName;
switch (curveName) {
case "P-256":
internalCurveName = "secp256r1";
break;
case "P-384":
internalCurveName = "secp384r1";
break;
case "P-521":
internalCurveName = "secp521r1";
break;
}
const keyPair = r.KEYUTIL.generateKeypair("EC", internalCurveName);
let pubKey;
let privKey;
let result;
switch (outputFormat) {
case "PEM":
pubKey = r.KEYUTIL.getPEM(keyPair.pubKeyObj).replace(/\r/g, "");
privKey = r.KEYUTIL.getPEM(keyPair.prvKeyObj, "PKCS8PRV").replace(/\r/g, "");
result = pubKey + "\n" + privKey;
break;
case "DER":
result = keyPair.prvKeyObj.prvKeyHex;
break;
case "JWK":
pubKey = r.KEYUTIL.getJWKFromKey(keyPair.pubKeyObj);
pubKey.key_ops = ["verify"]; // eslint-disable-line camelcase
pubKey.kid = "PublicKey";
privKey = r.KEYUTIL.getJWKFromKey(keyPair.prvKeyObj);
privKey.key_ops = ["sign"]; // eslint-disable-line camelcase
privKey.kid = "PrivateKey";
result = JSON.stringify({keys: [privKey, pubKey]}, null, 4);
break;
}
resolve(result);
});
}
}
export default GenerateECDSAKeyPair;

View File

@ -0,0 +1,73 @@
/**
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2024
* @license Apache-2.0
*/
import Operation from "../Operation.mjs";
import Utils from "../Utils.mjs";
import {toJA4} from "../lib/JA4.mjs";
/**
* JA4 Fingerprint operation
*/
class JA4Fingerprint extends Operation {
/**
* JA4Fingerprint constructor
*/
constructor() {
super();
this.name = "JA4 Fingerprint";
this.module = "Crypto";
this.description = "Generates a JA4 fingerprint to help identify TLS clients based on hashing together values from the Client Hello.<br><br>Input: A hex stream of the TLS or QUIC Client Hello packet application layer.";
this.infoURL = "https://medium.com/foxio/ja4-network-fingerprinting-9376fe9ca637";
this.inputType = "string";
this.outputType = "string";
this.args = [
{
name: "Input format",
type: "option",
value: ["Hex", "Base64", "Raw"]
},
{
name: "Output format",
type: "option",
value: ["JA4", "JA4 Original Rendering", "JA4 Raw", "JA4 Raw Original Rendering", "All"]
}
];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
const [inputFormat, outputFormat] = args;
input = Utils.convertToByteArray(input, inputFormat);
const ja4 = toJA4(new Uint8Array(input));
// Output
switch (outputFormat) {
case "JA4":
return ja4.JA4;
case "JA4 Original Rendering":
return ja4.JA4_o;
case "JA4 Raw":
return ja4.JA4_r;
case "JA4 Raw Original Rendering":
return ja4.JA4_ro;
case "All":
default:
return `JA4: ${ja4.JA4}
JA4_o: ${ja4.JA4_o}
JA4_r: ${ja4.JA4_r}
JA4_ro: ${ja4.JA4_ro}`;
}
}
}
export default JA4Fingerprint;

View File

@ -0,0 +1,66 @@
/**
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2024
* @license Apache-2.0
*/
import Operation from "../Operation.mjs";
import Utils from "../Utils.mjs";
import {toJA4S} from "../lib/JA4.mjs";
/**
* JA4Server Fingerprint operation
*/
class JA4ServerFingerprint extends Operation {
/**
* JA4ServerFingerprint constructor
*/
constructor() {
super();
this.name = "JA4Server Fingerprint";
this.module = "Crypto";
this.description = "Generates a JA4Server Fingerprint (JA4S) to help identify TLS servers or sessions based on hashing together values from the Server Hello.<br><br>Input: A hex stream of the TLS or QUIC Server Hello packet application layer.";
this.infoURL = "https://medium.com/foxio/ja4-network-fingerprinting-9376fe9ca637";
this.inputType = "string";
this.outputType = "string";
this.args = [
{
name: "Input format",
type: "option",
value: ["Hex", "Base64", "Raw"]
},
{
name: "Output format",
type: "option",
value: ["JA4S", "JA4S Raw", "Both"]
}
];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
const [inputFormat, outputFormat] = args;
input = Utils.convertToByteArray(input, inputFormat);
const ja4s = toJA4S(new Uint8Array(input));
// Output
switch (outputFormat) {
case "JA4S":
return ja4s.JA4S;
case "JA4S Raw":
return ja4s.JA4S_r;
case "Both":
default:
return `JA4S: ${ja4s.JA4S}\nJA4S_r: ${ja4s.JA4S_r}`;
}
}
}
export default JA4ServerFingerprint;

View File

@ -0,0 +1,80 @@
/**
* @author cplussharp
* @copyright Crown Copyright 2021
* @license Apache-2.0
*/
import r from "jsrsasign";
import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs";
/**
* PEM to JWK operation
*/
class PEMToJWK extends Operation {
/**
* PEMToJWK constructor
*/
constructor() {
super();
this.name = "JWK to PEM";
this.module = "PublicKey";
this.description = "Converts Keys in JSON Web Key format to PEM format (PKCS#8).";
this.infoURL = "https://datatracker.ietf.org/doc/html/rfc7517";
this.inputType = "string";
this.outputType = "string";
this.args = [];
this.checks = [
{
"pattern": "\"kty\":\\s*\"(EC|RSA)\"",
"flags": "gm",
"args": []
}
];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
const inputJson = JSON.parse(input);
let keys = [];
if (Array.isArray(inputJson)) {
// list of keys => transform all keys
keys = inputJson;
} else if (Array.isArray(inputJson.keys)) {
// JSON Web Key Set => transform all keys
keys = inputJson.keys;
} else if (typeof inputJson === "object") {
// single key
keys.push(inputJson);
} else {
throw new OperationError("Input is not a JSON Web Key");
}
let output = "";
for (let i=0; i<keys.length; i++) {
const jwk = keys[i];
if (typeof jwk.kty !== "string") {
throw new OperationError("Invalid JWK format");
} else if ("|RSA|EC|".indexOf(jwk.kty) === -1) {
throw new OperationError(`Unsupported JWK key type '${inputJson.kty}'`);
}
const key = r.KEYUTIL.getKey(jwk);
const pem = key.isPrivate ? r.KEYUTIL.getPEM(key, "PKCS8PRV") : r.KEYUTIL.getPEM(key);
// PEM ends with '\n', so a new key always starts on a new line
output += pem;
}
return output;
}
}
export default PEMToJWK;

View File

@ -0,0 +1,41 @@
/**
* @author 0xThiebaut [thiebaut.dev]
* @copyright Crown Copyright 2023
* @license Apache-2.0
*/
import Operation from "../Operation.mjs";
import {decompress} from "../lib/LZNT1.mjs";
/**
* LZNT1 Decompress operation
*/
class LZNT1Decompress extends Operation {
/**
* LZNT1 Decompress constructor
*/
constructor() {
super();
this.name = "LZNT1 Decompress";
this.module = "Compression";
this.description = "Decompresses data using the LZNT1 algorithm.<br><br>Similar to the Windows API <code>RtlDecompressBuffer</code>.";
this.infoURL = "https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-xca/5655f4a3-6ba4-489b-959f-e1f407c52f15";
this.inputType = "byteArray";
this.outputType = "byteArray";
this.args = [];
}
/**
* @param {byteArray} input
* @param {Object[]} args
* @returns {byteArray}
*/
run(input, args) {
return decompress(input);
}
}
export default LZNT1Decompress;

View File

@ -0,0 +1,139 @@
/**
* Based on murmurhash-js (https://github.com/garycourt/murmurhash-js)
* @author Gary Court
* @license MIT
*
* @author AliceGrey [alice@grey.systems]
* @copyright Crown Copyright 2024
* @license Apache-2.0
*/
import Operation from "../Operation.mjs";
/**
* MurmurHash3 operation
*/
class MurmurHash3 extends Operation {
/**
* MurmurHash3 constructor
*/
constructor() {
super();
this.name = "MurmurHash3";
this.module = "Hashing";
this.description = "Generates a MurmurHash v3 for a string input and an optional seed input";
this.infoURL = "https://wikipedia.org/wiki/MurmurHash";
this.inputType = "string";
this.outputType = "number";
this.args = [
{
name: "Seed",
type: "number",
value: 0
},
{
name: "Convert to Signed",
type: "boolean",
value: false
}
];
}
/**
* Calculates the MurmurHash3 hash of the input.
* Based on Gary Court's JS MurmurHash implementation
* @see http://github.com/garycourt/murmurhash-js
* @author AliceGrey [alice@grey.systems]
* @param {string} input ASCII only
* @param {number} seed Positive integer only
* @return {number} 32-bit positive integer hash
*/
mmh3(input, seed) {
let h1b;
let k1;
const remainder = input.length & 3; // input.length % 4
const bytes = input.length - remainder;
let h1 = seed;
const c1 = 0xcc9e2d51;
const c2 = 0x1b873593;
let i = 0;
while (i < bytes) {
k1 =
((input.charCodeAt(i) & 0xff)) |
((input.charCodeAt(++i) & 0xff) << 8) |
((input.charCodeAt(++i) & 0xff) << 16) |
((input.charCodeAt(++i) & 0xff) << 24);
++i;
k1 = ((((k1 & 0xffff) * c1) + ((((k1 >>> 16) * c1) & 0xffff) << 16))) & 0xffffffff;
k1 = (k1 << 15) | (k1 >>> 17);
k1 = ((((k1 & 0xffff) * c2) + ((((k1 >>> 16) * c2) & 0xffff) << 16))) & 0xffffffff;
h1 ^= k1;
h1 = (h1 << 13) | (h1 >>> 19);
h1b = ((((h1 & 0xffff) * 5) + ((((h1 >>> 16) * 5) & 0xffff) << 16))) & 0xffffffff;
h1 = (((h1b & 0xffff) + 0x6b64) + ((((h1b >>> 16) + 0xe654) & 0xffff) << 16));
}
k1 = 0;
if (remainder === 3) {
k1 ^= (input.charCodeAt(i + 2) & 0xff) << 16;
}
if (remainder === 3 || remainder === 2) {
k1 ^= (input.charCodeAt(i + 1) & 0xff) << 8;
}
if (remainder === 3 || remainder === 2 || remainder === 1) {
k1 ^= (input.charCodeAt(i) & 0xff);
k1 = (((k1 & 0xffff) * c1) + ((((k1 >>> 16) * c1) & 0xffff) << 16)) & 0xffffffff;
k1 = (k1 << 15) | (k1 >>> 17);
k1 = (((k1 & 0xffff) * c2) + ((((k1 >>> 16) * c2) & 0xffff) << 16)) & 0xffffffff;
h1 ^= k1;
}
h1 ^= input.length;
h1 ^= h1 >>> 16;
h1 = (((h1 & 0xffff) * 0x85ebca6b) + ((((h1 >>> 16) * 0x85ebca6b) & 0xffff) << 16)) & 0xffffffff;
h1 ^= h1 >>> 13;
h1 = ((((h1 & 0xffff) * 0xc2b2ae35) + ((((h1 >>> 16) * 0xc2b2ae35) & 0xffff) << 16))) & 0xffffffff;
h1 ^= h1 >>> 16;
return h1 >>> 0;
}
/**
* Converts an unsigned 32-bit integer to a signed 32-bit integer
* @author AliceGrey [alice@grey.systems]
* @param {value} 32-bit unsigned integer
* @return {number} 32-bit signed integer
*/
unsignedToSigned(value) {
return value & 0x80000000 ? -0x100000000 + value : value;
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {number}
*/
run(input, args) {
if (args && args.length >= 1) {
const seed = args[0];
const hash = this.mmh3(input, seed);
if (args.length > 1 && args[1]) {
return this.unsignedToSigned(hash);
}
return hash;
}
return this.mmh3(input);
}
}
export default MurmurHash3;

View File

@ -0,0 +1,88 @@
/**
* @author cplussharp
* @copyright Crown Copyright 2021
* @license Apache-2.0
*/
import r from "jsrsasign";
import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs";
/**
* PEM to JWK operation
*/
class PEMToJWK extends Operation {
/**
* PEMToJWK constructor
*/
constructor() {
super();
this.name = "PEM to JWK";
this.module = "PublicKey";
this.description = "Converts Keys in PEM format to a JSON Web Key format.";
this.infoURL = "https://datatracker.ietf.org/doc/html/rfc7517";
this.inputType = "string";
this.outputType = "string";
this.args = [];
this.checks = [
{
"pattern": "-----BEGIN ((RSA |EC )?(PRIVATE|PUBLIC) KEY|CERTIFICATE)-----",
"args": []
}
];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
let output = "";
let match;
const regex = /-----BEGIN ([A-Z][A-Z ]+[A-Z])-----/g;
while ((match = regex.exec(input)) !== null) {
// find corresponding end tag
const indexBase64 = match.index + match[0].length;
const header = input.substring(match.index, indexBase64);
const footer = `-----END ${match[1]}-----`;
const indexFooter = input.indexOf(footer, indexBase64);
if (indexFooter === -1) {
throw new OperationError(`PEM footer '${footer}' not found`);
}
const pem = input.substring(match.index, indexFooter + footer.length);
if (match[1].indexOf("KEY") !== -1) {
if (header === "-----BEGIN RSA PUBLIC KEY-----") {
throw new OperationError("Unsupported RSA public key format. Only PKCS#8 is supported.");
}
const key = r.KEYUTIL.getKey(pem);
if (key.type === "DSA") {
throw new OperationError("DSA keys are not supported for JWK");
}
const jwk = r.KEYUTIL.getJWKFromKey(key);
if (output.length > 0) {
output += "\n";
}
output += JSON.stringify(jwk);
} else if (match[1] === "CERTIFICATE") {
const cert = new r.X509();
cert.readCertPEM(pem);
const key = cert.getPublicKey();
const jwk = r.KEYUTIL.getJWKFromKey(key);
if (output.length > 0) {
output += "\n";
}
output += JSON.stringify(jwk);
} else {
throw new OperationError(`Unsupported PEM type '${match[1]}'`);
}
}
return output;
}
}
export default PEMToJWK;

View File

@ -20,7 +20,7 @@ class ParseASN1HexString extends Operation {
this.name = "Parse ASN.1 hex string"; this.name = "Parse ASN.1 hex string";
this.module = "PublicKey"; this.module = "PublicKey";
this.description = "Abstract Syntax Notation One (ASN.1) is a standard and notation that describes rules and structures for representing, encoding, transmitting, and decoding data in telecommunications and computer networking.<br><br>This operation parses arbitrary ASN.1 data and presents the resulting tree."; this.description = "Abstract Syntax Notation One (ASN.1) is a standard and notation that describes rules and structures for representing, encoding, transmitting, and decoding data in telecommunications and computer networking.<br><br>This operation parses arbitrary ASN.1 data (encoded as an hex string: use the 'To Hex' operation if necessary) and presents the resulting tree.";
this.infoURL = "https://wikipedia.org/wiki/Abstract_Syntax_Notation_One"; this.infoURL = "https://wikipedia.org/wiki/Abstract_Syntax_Notation_One";
this.inputType = "string"; this.inputType = "string";
this.outputType = "string"; this.outputType = "string";

View File

@ -0,0 +1,273 @@
/**
* @author jkataja
* @copyright Crown Copyright 2023
* @license Apache-2.0
*/
import Operation from "../Operation.mjs";
import forge from "node-forge";
import Utils from "../Utils.mjs";
/**
* Parse CSR operation
*/
class ParseCSR extends Operation {
/**
* ParseCSR constructor
*/
constructor() {
super();
this.name = "Parse CSR";
this.module = "PublicKey";
this.description = "Parse Certificate Signing Request (CSR) for an X.509 certificate";
this.infoURL = "https://wikipedia.org/wiki/Certificate_signing_request";
this.inputType = "string";
this.outputType = "string";
this.args = [
{
"name": "Input format",
"type": "option",
"value": ["PEM"]
},
{
"name": "Key type",
"type": "option",
"value": ["RSA"]
},
{
"name": "Strict ASN.1 value lengths",
"type": "boolean",
"value": true
}
];
this.checks = [
{
"pattern": "^-+BEGIN CERTIFICATE REQUEST-+\\r?\\n[\\da-z+/\\n\\r]+-+END CERTIFICATE REQUEST-+\\r?\\n?$",
"flags": "i",
"args": ["PEM"]
}
];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {string} Human-readable description of a Certificate Signing Request (CSR).
*/
run(input, args) {
if (!input.length) {
return "No input";
}
const csr = forge.pki.certificationRequestFromPem(input, args[1]);
// RSA algorithm is the only one supported for CSR in node-forge as of 1.3.1
return `Version: ${1 + csr.version} (0x${Utils.hex(csr.version)})
Subject${formatSubject(csr.subject)}
Subject Alternative Names${formatSubjectAlternativeNames(csr)}
Public Key
Algorithm: RSA
Length: ${csr.publicKey.n.bitLength()} bits
Modulus: ${formatMultiLine(chop(csr.publicKey.n.toString(16).replace(/(..)/g, "$&:")))}
Exponent: ${csr.publicKey.e} (0x${Utils.hex(csr.publicKey.e)})
Signature
Algorithm: ${forge.pki.oids[csr.signatureOid]}
Signature: ${formatMultiLine(Utils.strToByteArray(csr.signature).map(b => Utils.hex(b)).join(":"))}
Extensions${formatExtensions(csr)}`;
}
}
/**
* Format Subject of the request as a multi-line string
* @param {*} subject CSR Subject
* @returns Multi-line string describing Subject
*/
function formatSubject(subject) {
let out = "\n";
for (const attribute of subject.attributes) {
out += ` ${attribute.shortName} = ${attribute.value}\n`;
}
return chop(out);
}
/**
* Format Subject Alternative Names from the name `subjectAltName` extension
* @param {*} extension CSR object
* @returns Multi-line string describing Subject Alternative Names
*/
function formatSubjectAlternativeNames(csr) {
let out = "\n";
for (const attribute of csr.attributes) {
for (const extension of attribute.extensions) {
if (extension.name === "subjectAltName") {
const names = [];
for (const altName of extension.altNames) {
switch (altName.type) {
case 1:
names.push(`EMAIL: ${altName.value}`);
break;
case 2:
names.push(`DNS: ${altName.value}`);
break;
case 6:
names.push(`URI: ${altName.value}`);
break;
case 7:
names.push(`IP: ${altName.ip}`);
break;
default:
names.push(`(unable to format type ${altName.type} name)\n`);
}
}
out += indent(2, names);
}
}
}
return chop(out);
}
/**
* Format known extensions of a CSR
* @param {*} csr CSR object
* @returns Multi-line string describing attributes
*/
function formatExtensions(csr) {
let out = "\n";
for (const attribute of csr.attributes) {
for (const extension of attribute.extensions) {
// formatted separately
if (extension.name === "subjectAltName") {
continue;
}
out += ` ${extension.name}${(extension.critical ? " CRITICAL" : "")}:\n`;
let parts = [];
switch (extension.name) {
case "basicConstraints" :
parts = describeBasicConstraints(extension);
break;
case "keyUsage" :
parts = describeKeyUsage(extension);
break;
case "extKeyUsage" :
parts = describeExtendedKeyUsage(extension);
break;
default :
parts = ["(unable to format extension)"];
}
out += indent(4, parts);
}
}
return chop(out);
}
/**
* Format hex string onto multiple lines
* @param {*} longStr
* @returns Hex string as a multi-line hex string
*/
function formatMultiLine(longStr) {
const lines = [];
for (let remain = longStr ; remain !== "" ; remain = remain.substring(48)) {
lines.push(remain.substring(0, 48));
}
return lines.join("\n ");
}
/**
* Describe Basic Constraints
* @see RFC 5280 4.2.1.9. Basic Constraints https://www.ietf.org/rfc/rfc5280.txt
* @param {*} extension CSR extension with the name `basicConstraints`
* @returns Array of strings describing Basic Constraints
*/
function describeBasicConstraints(extension) {
const constraints = [];
constraints.push(`CA = ${extension.cA}`);
if (extension.pathLenConstraint !== undefined) constraints.push(`PathLenConstraint = ${extension.pathLenConstraint}`);
return constraints;
}
/**
* Describe Key Usage extension permitted use cases
* @see RFC 5280 4.2.1.3. Key Usage https://www.ietf.org/rfc/rfc5280.txt
* @param {*} extension CSR extension with the name `keyUsage`
* @returns Array of strings describing Key Usage extension permitted use cases
*/
function describeKeyUsage(extension) {
const usage = [];
if (extension.digitalSignature) usage.push("Digital signature");
if (extension.nonRepudiation) usage.push("Non-repudiation");
if (extension.keyEncipherment) usage.push("Key encipherment");
if (extension.dataEncipherment) usage.push("Data encipherment");
if (extension.keyAgreement) usage.push("Key agreement");
if (extension.keyCertSign) usage.push("Key certificate signing");
if (extension.cRLSign) usage.push("CRL signing");
if (extension.encipherOnly) usage.push("Encipher only");
if (extension.decipherOnly) usage.push("Decipher only");
if (usage.length === 0) usage.push("(none)");
return usage;
}
/**
* Describe Extended Key Usage extension permitted use cases
* @see RFC 5280 4.2.1.12. Extended Key Usage https://www.ietf.org/rfc/rfc5280.txt
* @param {*} extension CSR extension with the name `extendedKeyUsage`
* @returns Array of strings describing Extended Key Usage extension permitted use cases
*/
function describeExtendedKeyUsage(extension) {
const usage = [];
if (extension.serverAuth) usage.push("TLS Web Server Authentication");
if (extension.clientAuth) usage.push("TLS Web Client Authentication");
if (extension.codeSigning) usage.push("Code signing");
if (extension.emailProtection) usage.push("E-mail Protection (S/MIME)");
if (extension.timeStamping) usage.push("Trusted Timestamping");
if (extension.msCodeInd) usage.push("Microsoft Individual Code Signing");
if (extension.msCodeCom) usage.push("Microsoft Commercial Code Signing");
if (extension.msCTLSign) usage.push("Microsoft Trust List Signing");
if (extension.msSGC) usage.push("Microsoft Server Gated Crypto");
if (extension.msEFS) usage.push("Microsoft Encrypted File System");
if (extension.nsSGC) usage.push("Netscape Server Gated Crypto");
if (usage.length === 0) usage.push("(none)");
return usage;
}
/**
* Join an array of strings and add leading spaces to each line.
* @param {*} n How many leading spaces
* @param {*} parts Array of strings
* @returns Joined and indented string.
*/
function indent(n, parts) {
const fluff = " ".repeat(n);
return fluff + parts.join("\n" + fluff) + "\n";
}
/**
* Remove last character from a string.
* @param {*} s String
* @returns Chopped string.
*/
function chop(s) {
return s.substring(0, s.length - 1);
}
export default ParseCSR;

View File

@ -0,0 +1,68 @@
/**
* @author cplussharp
* @copyright Crown Copyright 2023
* @license Apache-2.0
*/
import r from "jsrsasign";
import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs";
/**
* Public Key from Certificate operation
*/
class PubKeyFromCert extends Operation {
/**
* PubKeyFromCert constructor
*/
constructor() {
super();
this.name = "Public Key from Certificate";
this.module = "PublicKey";
this.description = "Extracts the Public Key from a Certificate.";
this.infoURL = "https://en.wikipedia.org/wiki/X.509";
this.inputType = "string";
this.outputType = "string";
this.args = [];
this.checks = [];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
let output = "";
let match;
const regex = /-----BEGIN CERTIFICATE-----/g;
while ((match = regex.exec(input)) !== null) {
// find corresponding end tag
const indexBase64 = match.index + match[0].length;
const footer = "-----END CERTIFICATE-----";
const indexFooter = input.indexOf(footer, indexBase64);
if (indexFooter === -1) {
throw new OperationError(`PEM footer '${footer}' not found`);
}
const certPem = input.substring(match.index, indexFooter + footer.length);
const cert = new r.X509();
cert.readCertPEM(certPem);
let pubKey;
try {
pubKey = cert.getPublicKey();
} catch {
throw new OperationError("Unsupported public key type");
}
const pubKeyPem = r.KEYUTIL.getPEM(pubKey);
// PEM ends with '\n', so a new key always starts on a new line
output += pubKeyPem;
}
return output;
}
}
export default PubKeyFromCert;

View File

@ -0,0 +1,82 @@
/**
* @author cplussharp
* @copyright Crown Copyright 2023
* @license Apache-2.0
*/
import r from "jsrsasign";
import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs";
/**
* Public Key from Private Key operation
*/
class PubKeyFromPrivKey extends Operation {
/**
* PubKeyFromPrivKey constructor
*/
constructor() {
super();
this.name = "Public Key from Private Key";
this.module = "PublicKey";
this.description = "Extracts the Public Key from a Private Key.";
this.infoURL = "https://en.wikipedia.org/wiki/PKCS_8";
this.inputType = "string";
this.outputType = "string";
this.args = [];
this.checks = [];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
let output = "";
let match;
const regex = /-----BEGIN ((RSA |EC |DSA )?PRIVATE KEY)-----/g;
while ((match = regex.exec(input)) !== null) {
// find corresponding end tag
const indexBase64 = match.index + match[0].length;
const footer = `-----END ${match[1]}-----`;
const indexFooter = input.indexOf(footer, indexBase64);
if (indexFooter === -1) {
throw new OperationError(`PEM footer '${footer}' not found`);
}
const privKeyPem = input.substring(match.index, indexFooter + footer.length);
let privKey;
try {
privKey = r.KEYUTIL.getKey(privKeyPem);
} catch (err) {
throw new OperationError(`Unsupported key type: ${err}`);
}
let pubKey;
if (privKey.type && privKey.type === "EC") {
pubKey = new r.KJUR.crypto.ECDSA({ curve: privKey.curve });
pubKey.setPublicKeyHex(privKey.generatePublicKeyHex());
} else if (privKey.type && privKey.type === "DSA") {
if (!privKey.y) {
throw new OperationError(`DSA Private Key in PKCS#8 is not supported`);
}
pubKey = new r.KJUR.crypto.DSA();
pubKey.setPublic(privKey.p, privKey.q, privKey.g, privKey.y);
} else if (privKey.n && privKey.e) {
pubKey = new r.RSAKey();
pubKey.setPublic(privKey.n, privKey.e);
} else {
throw new OperationError(`Unsupported key type`);
}
const pubKeyPem = r.KEYUTIL.getPEM(pubKey);
// PEM ends with '\n', so a new key always starts on a new line
output += pubKeyPem;
}
return output;
}
}
export default PubKeyFromPrivKey;

View File

@ -0,0 +1,144 @@
/**
* @author sw5678
* @copyright Crown Copyright 2024
* @license Apache-2.0
*/
import Operation from "../Operation.mjs";
/**
* RAKE operation
*/
class RAKE extends Operation {
/**
* RAKE constructor
*/
constructor() {
super();
this.name = "RAKE";
this.module = "Default";
this.description = [
"Rapid Keyword Extraction (RAKE)",
"<br><br>",
"RAKE is a domain-independent keyword extraction algorithm in Natural Language Processing.",
"<br><br>",
"The list of stop words are from the NLTK python package",
].join("\n");
this.inputType = "string";
this.outputType = "string";
this.args = [
{
name: "Word Delimiter (Regex)",
type: "text",
value: "\\s"
},
{
name: "Sentence Delimiter (Regex)",
type: "text",
value: "\\.\\s|\\n"
},
{
name: "Stop Words",
type: "text",
value: "i,me,my,myself,we,our,ours,ourselves,you,you're,you've,you'll,you'd,your,yours,yourself,yourselves,he,him,his,himself,she,she's,her,hers,herself,it,it's,its,itsef,they,them,their,theirs,themselves,what,which,who,whom,this,that,that'll,these,those,am,is,are,was,were,be,been,being,have,has,had,having,do,does',did,doing,a,an,the,and,but,if,or,because,as,until,while,of,at,by,for,with,about,against,between,into,through,during,before,after,above,below,to,from,up,down,in,out,on,off,over,under,again,further,then,once,here,there,when,where,why,how,all,any,both,each,few,more,most,other,some,such,no,nor,not,only,own,same,so,than,too,very,s,t,can,will,just,don,don't,should,should've,now,d,ll,m,o,re,ve,y,ain,aren,aren't,couldn,couldn't,didn,didn't,doesn,doesn't,hadn,hadn't,hasn,hasn't,haven,haven't,isn,isn't,ma,mightn,mightn't,mustn,mustn't,needn,needn't,shan,shan't,shouldn,shouldn't,wasn,wasn't,weren,weren't,won,won't,wouldn,wouldn't"
}
];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
// Get delimiter regexs
const wordDelim = new RegExp(args[0], "g");
const sentDelim = new RegExp(args[1], "g");
// Deduplicate the stop words and add the empty string
const stopWords = args[2].toLowerCase().replace(/ /g, "").split(",").unique();
stopWords.push("");
// Lower case input and remove start and ending whitespace
input = input.toLowerCase().trim();
// Get tokens, token count, and phrases
const tokens = [];
const wordFrequencies = [];
let phrases = [];
// Build up list of phrases and token counts
const sentences = input.split(sentDelim);
for (const sent of sentences) {
// Split sentence into words
const splitSent = sent.split(wordDelim);
let startIndex = 0;
for (let i = 0; i < splitSent.length; i++) {
const token = splitSent[i];
if (stopWords.includes(token)) {
// If token is stop word then split to create phrase
phrases.push(splitSent.slice(startIndex, i));
startIndex = i + 1;
} else {
// If token is not a stop word add to the count of the list of words
if (tokens.includes(token)) {
wordFrequencies[tokens.indexOf(token)]+=1;
} else {
tokens.push(token);
wordFrequencies.push(1);
}
}
}
phrases.push(splitSent.slice(startIndex));
}
// remove empty phrases
phrases = phrases.filter(subArray => subArray.length > 0);
// Remove duplicate phrases
phrases = phrases.unique();
// Generate word_degree_matrix and populate
const wordDegreeMatrix = Array(tokens.length).fill().map(() => Array(tokens.length).fill(0));
for (const phrase of phrases) {
for (const word1 of phrase) {
for (const word2 of phrase) {
wordDegreeMatrix[tokens.indexOf(word1)][tokens.indexOf(word2)]++;
}
}
}
// Calculate degree score for each token
const degreeScores = Array(tokens.length).fill(0);
for (let i=0; i<tokens.length; i++) {
let wordDegree = 0;
for (let j=0; j<wordDegreeMatrix.length; j++) {
wordDegree += wordDegreeMatrix[j][i];
}
degreeScores[i] = wordDegree / wordFrequencies[i];
}
// Calculate score for each phrase
const scores = phrases.map(function (phrase) {
let score = 0;
phrase.forEach(function (token) {
score += degreeScores[tokens.indexOf(token)];
});
return new Array(score, phrase.join(" "));
});
scores.sort((a, b) => b[0] - a[0]);
scores.unshift(new Array("Scores: ", "Keywords: "));
// Output works with the 'To Table' functionality already built into CC
return scores.map(function (score) {
return score.join(", ");
}).join("\n");
}
}
export default RAKE;

View File

@ -67,6 +67,10 @@ class RegularExpression extends Operation {
name: "MAC address", name: "MAC address",
value: "[A-Fa-f\\d]{2}(?:[:-][A-Fa-f\\d]{2}){5}" value: "[A-Fa-f\\d]{2}(?:[:-][A-Fa-f\\d]{2}){5}"
}, },
{
name: "UUID",
value: "[0-9a-fA-F]{8}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{12}"
},
{ {
name: "Date (yyyy-mm-dd)", name: "Date (yyyy-mm-dd)",
value: "((?:19|20)\\d\\d)[- /.](0[1-9]|1[012])[- /.](0[1-9]|[12][0-9]|3[01])" value: "((?:19|20)\\d\\d)[- /.](0[1-9]|1[012])[- /.](0[1-9]|[12][0-9]|3[01])"

View File

@ -0,0 +1,57 @@
/**
* @author sg5506844 [sg5506844@gmail.com]
* @copyright Crown Copyright 2021
* @license Apache-2.0
*/
import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs";
import rison from "rison";
/**
* Rison Decode operation
*/
class RisonDecode extends Operation {
/**
* RisonDecode constructor
*/
constructor() {
super();
this.name = "Rison Decode";
this.module = "Encodings";
this.description = "Rison, a data serialization format optimized for compactness in URIs. Rison is a slight variation of JSON that looks vastly superior after URI encoding. Rison still expresses exactly the same set of data structures as JSON, so data can be translated back and forth without loss or guesswork.";
this.infoURL = "https://github.com/Nanonid/rison";
this.inputType = "string";
this.outputType = "Object";
this.args = [
{
name: "Decode Option",
type: "editableOption",
value: ["Decode", "Decode Object", "Decode Array"]
},
];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {Object}
*/
run(input, args) {
const [decodeOption] = args;
switch (decodeOption) {
case "Decode":
return rison.decode(input);
case "Decode Object":
return rison.decode_object(input);
case "Decode Array":
return rison.decode_array(input);
default:
throw new OperationError("Invalid Decode option");
}
}
}
export default RisonDecode;

View File

@ -0,0 +1,59 @@
/**
* @author sg5506844 [sg5506844@gmail.com]
* @copyright Crown Copyright 2021
* @license Apache-2.0
*/
import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs";
import rison from "rison";
/**
* Rison Encode operation
*/
class RisonEncode extends Operation {
/**
* RisonEncode constructor
*/
constructor() {
super();
this.name = "Rison Encode";
this.module = "Encodings";
this.description = "Rison, a data serialization format optimized for compactness in URIs. Rison is a slight variation of JSON that looks vastly superior after URI encoding. Rison still expresses exactly the same set of data structures as JSON, so data can be translated back and forth without loss or guesswork.";
this.infoURL = "https://github.com/Nanonid/rison";
this.inputType = "Object";
this.outputType = "string";
this.args = [
{
name: "Encode Option",
type: "option",
value: ["Encode", "Encode Object", "Encode Array", "Encode URI"]
},
];
}
/**
* @param {Object} input
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
const [encodeOption] = args;
switch (encodeOption) {
case "Encode":
return rison.encode(input);
case "Encode Object":
return rison.encode_object(input);
case "Encode Array":
return rison.encode_array(input);
case "Encode URI":
return rison.encode_uri(input);
default:
throw new OperationError("Invalid encode option");
}
}
}
export default RisonEncode;

View File

@ -21,7 +21,7 @@ class SSDEEP extends Operation {
this.name = "SSDEEP"; this.name = "SSDEEP";
this.module = "Crypto"; this.module = "Crypto";
this.description = "SSDEEP is a program for computing context triggered piecewise hashes (CTPH). Also called fuzzy hashes, CTPH can match inputs that have homologies. Such inputs have sequences of identical bytes in the same order, although bytes in between these sequences may be different in both content and length.<br><br>SSDEEP hashes are now widely used for simple identification purposes (e.g. the 'Basic Properties' section in VirusTotal). Although 'better' fuzzy hashes are available, SSDEEP is still one of the primary choices because of its speed and being a de facto standard.<br><br>This operation is fundamentally the same as the CTPH operation, however their outputs differ in format."; this.description = "SSDEEP is a program for computing context triggered piecewise hashes (CTPH). Also called fuzzy hashes, CTPH can match inputs that have homologies. Such inputs have sequences of identical bytes in the same order, although bytes in between these sequences may be different in both content and length.<br><br>SSDEEP hashes are now widely used for simple identification purposes (e.g. the 'Basic Properties' section in VirusTotal). Although 'better' fuzzy hashes are available, SSDEEP is still one of the primary choices because of its speed and being a de facto standard.<br><br>This operation is fundamentally the same as the CTPH operation, however their outputs differ in format.";
this.infoURL = "https://forensicswiki.xyz/wiki/index.php?title=Ssdeep"; this.infoURL = "https://forensics.wiki/ssdeep";
this.inputType = "string"; this.inputType = "string";
this.outputType = "string"; this.outputType = "string";
this.args = []; this.args = [];

View File

@ -0,0 +1,154 @@
/**
* @author joostrijneveld [joost@joostrijneveld.nl]
* @copyright Crown Copyright 2024
* @license Apache-2.0
*/
import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs";
import Utils from "../Utils.mjs";
import { toHex } from "../lib/Hex.mjs";
import { salsa20Block } from "../lib/Salsa20.mjs";
/**
* Salsa20 operation
*/
class Salsa20 extends Operation {
/**
* Salsa20 constructor
*/
constructor() {
super();
this.name = "Salsa20";
this.module = "Ciphers";
this.description = "Salsa20 is a stream cipher designed by Daniel J. Bernstein and submitted to the eSTREAM project; Salsa20/8 and Salsa20/12 are round-reduced variants. It is closely related to the ChaCha stream cipher.<br><br><b>Key:</b> Salsa20 uses a key of 16 or 32 bytes (128 or 256 bits).<br><br><b>Nonce:</b> Salsa20 uses a nonce of 8 bytes (64 bits).<br><br><b>Counter:</b> Salsa uses a counter of 8 bytes (64 bits). The counter starts at zero at the start of the keystream, and is incremented at every 64 bytes.";
this.infoURL = "https://wikipedia.org/wiki/Salsa20";
this.inputType = "string";
this.outputType = "string";
this.args = [
{
"name": "Key",
"type": "toggleString",
"value": "",
"toggleValues": ["Hex", "UTF8", "Latin1", "Base64"]
},
{
"name": "Nonce",
"type": "toggleString",
"value": "",
"toggleValues": ["Hex", "UTF8", "Latin1", "Base64", "Integer"]
},
{
"name": "Counter",
"type": "number",
"value": 0,
"min": 0
},
{
"name": "Rounds",
"type": "option",
"value": ["20", "12", "8"]
},
{
"name": "Input",
"type": "option",
"value": ["Hex", "Raw"]
},
{
"name": "Output",
"type": "option",
"value": ["Raw", "Hex"]
}
];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
const key = Utils.convertToByteArray(args[0].string, args[0].option),
nonceType = args[1].option,
rounds = parseInt(args[3], 10),
inputType = args[4],
outputType = args[5];
if (key.length !== 16 && key.length !== 32) {
throw new OperationError(`Invalid key length: ${key.length} bytes.
Salsa20 uses a key of 16 or 32 bytes (128 or 256 bits).`);
}
let counter, nonce;
if (nonceType === "Integer") {
nonce = Utils.intToByteArray(parseInt(args[1].string, 10), 8, "little");
} else {
nonce = Utils.convertToByteArray(args[1].string, args[1].option);
if (!(nonce.length === 8)) {
throw new OperationError(`Invalid nonce length: ${nonce.length} bytes.
Salsa20 uses a nonce of 8 bytes (64 bits).`);
}
}
counter = Utils.intToByteArray(args[2], 8, "little");
const output = [];
input = Utils.convertToByteArray(input, inputType);
let counterAsInt = Utils.byteArrayToInt(counter, "little");
for (let i = 0; i < input.length; i += 64) {
counter = Utils.intToByteArray(counterAsInt, 8, "little");
const stream = salsa20Block(key, nonce, counter, rounds);
for (let j = 0; j < 64 && i + j < input.length; j++) {
output.push(input[i + j] ^ stream[j]);
}
counterAsInt++;
}
if (outputType === "Hex") {
return toHex(output);
} else {
return Utils.arrayBufferToStr(Uint8Array.from(output).buffer);
}
}
/**
* Highlight Salsa20
*
* @param {Object[]} pos
* @param {number} pos[].start
* @param {number} pos[].end
* @param {Object[]} args
* @returns {Object[]} pos
*/
highlight(pos, args) {
const inputType = args[4],
outputType = args[5];
if (inputType === "Raw" && outputType === "Raw") {
return pos;
}
}
/**
* Highlight Salsa20 in reverse
*
* @param {Object[]} pos
* @param {number} pos[].start
* @param {number} pos[].end
* @param {Object[]} args
* @returns {Object[]} pos
*/
highlightReverse(pos, args) {
const inputType = args[4],
outputType = args[5];
if (inputType === "Raw" && outputType === "Raw") {
return pos;
}
}
}
export default Salsa20;

View File

@ -43,7 +43,7 @@ class ToBase58 extends Operation {
run(input, args) { run(input, args) {
input = new Uint8Array(input); input = new Uint8Array(input);
let alphabet = args[0] || ALPHABET_OPTIONS[0].value, let alphabet = args[0] || ALPHABET_OPTIONS[0].value,
result = [0]; result = [];
alphabet = Utils.expandAlphRange(alphabet).join(""); alphabet = Utils.expandAlphRange(alphabet).join("");
@ -60,11 +60,9 @@ class ToBase58 extends Operation {
} }
input.forEach(function(b) { input.forEach(function(b) {
let carry = (result[0] << 8) + b; let carry = b;
result[0] = carry % 58;
carry = (carry / 58) | 0;
for (let i = 1; i < result.length; i++) { for (let i = 0; i < result.length; i++) {
carry += result[i] << 8; carry += result[i] << 8;
result[i] = carry % 58; result[i] = carry % 58;
carry = (carry / 58) | 0; carry = (carry / 58) | 0;

View File

@ -0,0 +1,67 @@
/**
* @author sg5506844 [sg5506844@gmail.com]
* @copyright Crown Copyright 2021
* @license Apache-2.0
*/
import { base92Chr } from "../lib/Base92.mjs";
import Operation from "../Operation.mjs";
/**
* To Base92 operation
*/
class ToBase92 extends Operation {
/**
* ToBase92 constructor
*/
constructor() {
super();
this.name = "To Base92";
this.module = "Default";
this.description = "Base92 is a notation for encoding arbitrary byte data using a restricted set of symbols that can be conveniently used by humans and processed by computers.";
this.infoURL = "https://wikipedia.org/wiki/List_of_numeral_systems";
this.inputType = "string";
this.outputType = "byteArray";
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {byteArray}
*/
run(input, args) {
const res = [];
let bitString = "";
while (input.length > 0) {
while (bitString.length < 13 && input.length > 0) {
bitString += input[0].charCodeAt(0).toString(2).padStart(8, "0");
input = input.slice(1);
}
if (bitString.length < 13)
break;
const i = parseInt(bitString.slice(0, 13), 2);
res.push(base92Chr(Math.floor(i / 91)));
res.push(base92Chr(i % 91));
bitString = bitString.slice(13);
}
if (bitString.length > 0) {
if (bitString.length < 7) {
bitString = bitString.padEnd(6, "0");
res.push(base92Chr(parseInt(bitString, 2)));
} else {
bitString = bitString.padEnd(13, "0");
const i = parseInt(bitString.slice(0, 13), 2);
res.push(base92Chr(Math.floor(i / 91)));
res.push(base92Chr(i % 91));
}
}
return res;
}
}
export default ToBase92;

View File

@ -0,0 +1,80 @@
/**
* @author tcode2k16 [tcode2k16@gmail.com]
* @copyright Crown Copyright 2019
* @license Apache-2.0
*/
import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs";
import Utils from "../Utils.mjs";
import ieee754 from "ieee754";
import {DELIM_OPTIONS} from "../lib/Delim.mjs";
/**
* To Float operation
*/
class ToFloat extends Operation {
/**
* ToFloat constructor
*/
constructor() {
super();
this.name = "To Float";
this.module = "Default";
this.description = "Convert to IEEE754 Floating Point Numbers";
this.infoURL = "https://wikipedia.org/wiki/IEEE_754";
this.inputType = "byteArray";
this.outputType = "string";
this.args = [
{
"name": "Endianness",
"type": "option",
"value": [
"Big Endian",
"Little Endian"
]
},
{
"name": "Size",
"type": "option",
"value": [
"Float (4 bytes)",
"Double (8 bytes)"
]
},
{
"name": "Delimiter",
"type": "option",
"value": DELIM_OPTIONS
}
];
}
/**
* @param {byteArray} input
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
const [endianness, size, delimiterName] = args;
const delim = Utils.charRep(delimiterName || "Space");
const byteSize = size === "Double (8 bytes)" ? 8 : 4;
const isLE = endianness === "Little Endian";
const mLen = byteSize === 4 ? 23 : 52;
if (input.length % byteSize !== 0) {
throw new OperationError(`Input is not a multiple of ${byteSize}`);
}
const output = [];
for (let i = 0; i < input.length; i+=byteSize) {
output.push(ieee754.read(input, i, isLE, mLen, byteSize));
}
return output.join(delim);
}
}
export default ToFloat;

View File

@ -6,7 +6,7 @@
import Operation from "../Operation.mjs"; import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs"; import OperationError from "../errors/OperationError.mjs";
import xmldom from "xmldom"; import xmldom from "@xmldom/xmldom";
import xpath from "xpath"; import xpath from "xpath";
/** /**

View File

@ -0,0 +1,156 @@
/**
* @author joostrijneveld [joost@joostrijneveld.nl]
* @copyright Crown Copyright 2024
* @license Apache-2.0
*/
import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs";
import Utils from "../Utils.mjs";
import { toHex } from "../lib/Hex.mjs";
import { salsa20Block, hsalsa20 } from "../lib/Salsa20.mjs";
/**
* XSalsa20 operation
*/
class XSalsa20 extends Operation {
/**
* XSalsa20 constructor
*/
constructor() {
super();
this.name = "XSalsa20";
this.module = "Ciphers";
this.description = "XSalsa20 is a variant of the Salsa20 stream cipher designed by Daniel J. Bernstein; XSalsa uses longer nonces.<br><br><b>Key:</b> XSalsa20 uses a key of 16 or 32 bytes (128 or 256 bits).<br><br><b>Nonce:</b> XSalsa20 uses a nonce of 24 bytes (192 bits).<br><br><b>Counter:</b> XSalsa uses a counter of 8 bytes (64 bits). The counter starts at zero at the start of the keystream, and is incremented at every 64 bytes.";
this.infoURL = "https://en.wikipedia.org/wiki/Salsa20#XSalsa20_with_192-bit_nonce";
this.inputType = "string";
this.outputType = "string";
this.args = [
{
"name": "Key",
"type": "toggleString",
"value": "",
"toggleValues": ["Hex", "UTF8", "Latin1", "Base64"]
},
{
"name": "Nonce",
"type": "toggleString",
"value": "",
"toggleValues": ["Hex", "UTF8", "Latin1", "Base64", "Integer"]
},
{
"name": "Counter",
"type": "number",
"value": 0,
"min": 0
},
{
"name": "Rounds",
"type": "option",
"value": ["20", "12", "8"]
},
{
"name": "Input",
"type": "option",
"value": ["Hex", "Raw"]
},
{
"name": "Output",
"type": "option",
"value": ["Raw", "Hex"]
}
];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
const key = Utils.convertToByteArray(args[0].string, args[0].option),
nonceType = args[1].option,
rounds = parseInt(args[3], 10),
inputType = args[4],
outputType = args[5];
if (key.length !== 16 && key.length !== 32) {
throw new OperationError(`Invalid key length: ${key.length} bytes.
XSalsa20 uses a key of 16 or 32 bytes (128 or 256 bits).`);
}
let counter, nonce;
if (nonceType === "Integer") {
nonce = Utils.intToByteArray(parseInt(args[1].string, 10), 8, "little");
} else {
nonce = Utils.convertToByteArray(args[1].string, args[1].option);
if (!(nonce.length === 24)) {
throw new OperationError(`Invalid nonce length: ${nonce.length} bytes.
XSalsa20 uses a nonce of 24 bytes (192 bits).`);
}
}
counter = Utils.intToByteArray(args[2], 8, "little");
const xsalsaKey = hsalsa20(key, nonce.slice(0, 16), rounds);
const output = [];
input = Utils.convertToByteArray(input, inputType);
let counterAsInt = Utils.byteArrayToInt(counter, "little");
for (let i = 0; i < input.length; i += 64) {
counter = Utils.intToByteArray(counterAsInt, 8, "little");
const stream = salsa20Block(xsalsaKey, nonce.slice(16, 24), counter, rounds);
for (let j = 0; j < 64 && i + j < input.length; j++) {
output.push(input[i + j] ^ stream[j]);
}
counterAsInt++;
}
if (outputType === "Hex") {
return toHex(output);
} else {
return Utils.arrayBufferToStr(Uint8Array.from(output).buffer);
}
}
/**
* Highlight XSalsa20
*
* @param {Object[]} pos
* @param {number} pos[].start
* @param {number} pos[].end
* @param {Object[]} args
* @returns {Object[]} pos
*/
highlight(pos, args) {
const inputType = args[4],
outputType = args[5];
if (inputType === "Raw" && outputType === "Raw") {
return pos;
}
}
/**
* Highlight XSalsa20 in reverse
*
* @param {Object[]} pos
* @param {number} pos[].start
* @param {number} pos[].end
* @param {Object[]} args
* @returns {Object[]} pos
*/
highlightReverse(pos, args) {
const inputType = args[4],
outputType = args[5];
if (inputType === "Raw" && outputType === "Raw") {
return pos;
}
}
}
export default XSalsa20;

View File

@ -0,0 +1,57 @@
/**
* @author devcydo [devcydo@gmail.com]
* @author Ma Bingyao [mabingyao@gmail.com]
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2024
* @license Apache-2.0
*/
import Operation from "../Operation.mjs";
import Utils from "../Utils.mjs";
import OperationError from "../errors/OperationError.mjs";
import {decrypt} from "../lib/XXTEA.mjs";
/**
* XXTEA Decrypt operation
*/
class XXTEADecrypt extends Operation {
/**
* XXTEADecrypt constructor
*/
constructor() {
super();
this.name = "XXTEA Decrypt";
this.module = "Ciphers";
this.description = "Corrected Block TEA (often referred to as XXTEA) is a block cipher designed to correct weaknesses in the original Block TEA. XXTEA operates on variable-length blocks that are some arbitrary multiple of 32 bits in size (minimum 64 bits). The number of full cycles depends on the block size, but there are at least six (rising to 32 for small block sizes). The original Block TEA applies the XTEA round function to each word in the block and combines it additively with its leftmost neighbour. Slow diffusion rate of the decryption process was immediately exploited to break the cipher. Corrected Block TEA uses a more involved round function which makes use of both immediate neighbours in processing each word in the block.";
this.infoURL = "https://wikipedia.org/wiki/XXTEA";
this.inputType = "ArrayBuffer";
this.outputType = "ArrayBuffer";
this.args = [
{
"name": "Key",
"type": "toggleString",
"value": "",
"toggleValues": ["Hex", "UTF8", "Latin1", "Base64"]
},
];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
const key = new Uint8Array(Utils.convertToByteArray(args[0].string, args[0].option));
try {
return decrypt(new Uint8Array(input), key).buffer;
} catch (err) {
throw new OperationError("Unable to decrypt using this key");
}
}
}
export default XXTEADecrypt;

View File

@ -0,0 +1,52 @@
/**
* @author devcydo [devcydo@gmail.com]
* @author Ma Bingyao [mabingyao@gmail.com]
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2024
* @license Apache-2.0
*/
import Operation from "../Operation.mjs";
import Utils from "../Utils.mjs";
import {encrypt} from "../lib/XXTEA.mjs";
/**
* XXTEA Encrypt operation
*/
class XXTEAEncrypt extends Operation {
/**
* XXTEAEncrypt constructor
*/
constructor() {
super();
this.name = "XXTEA Encrypt";
this.module = "Ciphers";
this.description = "Corrected Block TEA (often referred to as XXTEA) is a block cipher designed to correct weaknesses in the original Block TEA. XXTEA operates on variable-length blocks that are some arbitrary multiple of 32 bits in size (minimum 64 bits). The number of full cycles depends on the block size, but there are at least six (rising to 32 for small block sizes). The original Block TEA applies the XTEA round function to each word in the block and combines it additively with its leftmost neighbour. Slow diffusion rate of the decryption process was immediately exploited to break the cipher. Corrected Block TEA uses a more involved round function which makes use of both immediate neighbours in processing each word in the block.";
this.infoURL = "https://wikipedia.org/wiki/XXTEA";
this.inputType = "ArrayBuffer";
this.outputType = "ArrayBuffer";
this.args = [
{
"name": "Key",
"type": "toggleString",
"value": "",
"toggleValues": ["Hex", "UTF8", "Latin1", "Base64"]
},
];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
const key = new Uint8Array(Utils.convertToByteArray(args[0].string, args[0].option));
return encrypt(new Uint8Array(input), key).buffer;
}
}
export default XXTEAEncrypt;

View File

@ -4054,7 +4054,7 @@ function DecodeImmediate( type, BySize, SizeSetting )
//Sign bit adjust. //Sign bit adjust.
if( V32 >= ( n >> 1 ) ) { V32 -= n; } if( V32 >= ( n / 2 ) ) { V32 -= n; }
//Add position. //Add position.

View File

@ -39,7 +39,6 @@ class App {
this.baking = false; this.baking = false;
this.autoBake_ = false; this.autoBake_ = false;
this.autoBakePause = false;
this.progress = 0; this.progress = 0;
this.ingId = 0; this.ingId = 0;
@ -47,6 +46,7 @@ class App {
this.workerLoaded = false; this.workerLoaded = false;
this.waitersLoaded = false; this.waitersLoaded = false;
this.snackbars = [];
this.breakpoint = 1024; this.breakpoint = 1024;
} }
@ -157,12 +157,12 @@ class App {
* Runs Auto Bake if it is set. * Runs Auto Bake if it is set.
*/ */
autoBake() { autoBake() {
// If autoBakePause is set, we are loading a full recipe (and potentially input), so there is no if (this.baking) {
// need to set the staleness indicator. Just exit and wait until auto bake is called after loading this.manager.worker.cancelBakeForAutoBake();
// has completed. this.baking = false;
if (this.autoBakePause) return false; }
if (this.autoBake_ && !this.baking) { if (this.autoBake_) {
log.debug("Auto-baking"); log.debug("Auto-baking");
this.manager.worker.bakeInputs({ this.manager.worker.bakeInputs({
nums: [this.manager.tabs.getActiveTab("input")], nums: [this.manager.tabs.getActiveTab("input")],
@ -476,7 +476,6 @@ class App {
* @fires Manager#statechange * @fires Manager#statechange
*/ */
loadURIParams(params=this.getURIParams()) { loadURIParams(params=this.getURIParams()) {
this.autoBakePause = true;
this.uriParams = params; this.uriParams = params;
// Read in recipe from URI params // Read in recipe from URI params
@ -505,22 +504,22 @@ class App {
// Input Character Encoding // Input Character Encoding
// Must be set before the input is loaded // Must be set before the input is loaded
if (this.uriParams.ienc) { if (this.uriParams.ienc) {
this.manager.input.chrEncChange(parseInt(this.uriParams.ienc, 10)); this.manager.input.chrEncChange(parseInt(this.uriParams.ienc, 10), true, true);
} }
// Output Character Encoding // Output Character Encoding
if (this.uriParams.oenc) { if (this.uriParams.oenc) {
this.manager.output.chrEncChange(parseInt(this.uriParams.oenc, 10)); this.manager.output.chrEncChange(parseInt(this.uriParams.oenc, 10), true);
} }
// Input EOL sequence // Input EOL sequence
if (this.uriParams.ieol) { if (this.uriParams.ieol) {
this.manager.input.eolChange(this.uriParams.ieol); this.manager.input.eolChange(this.uriParams.ieol, true);
} }
// Output EOL sequence // Output EOL sequence
if (this.uriParams.oeol) { if (this.uriParams.oeol) {
this.manager.output.eolChange(this.uriParams.oeol); this.manager.output.eolChange(this.uriParams.oeol, true);
} }
// Read in input data from URI params // Read in input data from URI params
@ -543,7 +542,6 @@ class App {
this.manager.options.changeTheme(Utils.escapeHtml(this.uriParams.theme)); this.manager.options.changeTheme(Utils.escapeHtml(this.uriParams.theme));
} }
this.autoBakePause = false;
window.dispatchEvent(this.manager.statechange); window.dispatchEvent(this.manager.statechange);
} }
@ -577,10 +575,6 @@ class App {
setRecipeConfig(recipeConfig) { setRecipeConfig(recipeConfig) {
document.getElementById("rec-list").innerHTML = null; document.getElementById("rec-list").innerHTML = null;
// Pause auto-bake while loading but don't modify `this.autoBake_`
// otherwise `manualBake` cannot trigger.
this.autoBakePause = true;
for (let i = 0; i < recipeConfig.length; i++) { for (let i = 0; i < recipeConfig.length; i++) {
const item = this.manager.recipe.addOperation(recipeConfig[i].op); const item = this.manager.recipe.addOperation(recipeConfig[i].op);
@ -615,9 +609,6 @@ class App {
this.progress = 0; this.progress = 0;
} }
// Unpause auto bake
this.autoBakePause = false;
} }
@ -693,14 +684,14 @@ class App {
log.info("[" + time.toLocaleString() + "] " + str); log.info("[" + time.toLocaleString() + "] " + str);
if (silent) return; if (silent) return;
this.currentSnackbar = $.snackbar({ this.snackbars.push($.snackbar({
content: str, content: str,
timeout: timeout, timeout: timeout,
htmlAllowed: true, htmlAllowed: true,
onClose: () => { onClose: () => {
this.currentSnackbar.remove(); this.snackbars.shift().remove();
} }
}); }));
} }

View File

@ -139,6 +139,7 @@ class Manager {
document.getElementById("load-delete-button").addEventListener("click", this.controls.loadDeleteClick.bind(this.controls)); document.getElementById("load-delete-button").addEventListener("click", this.controls.loadDeleteClick.bind(this.controls));
document.getElementById("load-name").addEventListener("change", this.controls.loadNameChange.bind(this.controls)); 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("load-button").addEventListener("click", this.controls.loadButtonClick.bind(this.controls));
document.getElementById("hide-icon").addEventListener("click", this.controls.hideRecipeArgsClick.bind(this.recipe));
document.getElementById("support").addEventListener("click", this.controls.supportButtonClick.bind(this.controls)); document.getElementById("support").addEventListener("click", this.controls.supportButtonClick.bind(this.controls));
this.addMultiEventListeners("#save-texts textarea", "keyup paste", this.controls.saveTextChange, this.controls); this.addMultiEventListeners("#save-texts textarea", "keyup paste", this.controls.saveTextChange, this.controls);
document.getElementById("rec-list").addEventListener("click", this.controls.onMaximisedRecipeClick.bind(this.controls)); document.getElementById("rec-list").addEventListener("click", this.controls.onMaximisedRecipeClick.bind(this.controls));
@ -157,6 +158,7 @@ class Manager {
// Recipe // Recipe
this.addDynamicListener(".arg:not(select)", "input", this.recipe.ingChange, this.recipe); this.addDynamicListener(".arg:not(select)", "input", this.recipe.ingChange, this.recipe);
this.addDynamicListener(".arg[type=checkbox], .arg[type=radio], select.arg", "change", this.recipe.ingChange, this.recipe); this.addDynamicListener(".arg[type=checkbox], .arg[type=radio], select.arg", "change", this.recipe.ingChange, this.recipe);
this.addDynamicListener(".hide-args-icon", "click", this.recipe.hideArgsClick, this.recipe);
this.addDynamicListener("#rec-list .dropdown-menu.toggle-dropdown a", "click", this.recipe.dropdownToggleClick, this.recipe); this.addDynamicListener("#rec-list .dropdown-menu.toggle-dropdown a", "click", this.recipe.dropdownToggleClick, this.recipe);
this.addDynamicListener("textarea.arg", "dragover", this.recipe.textArgDragover, this.recipe); this.addDynamicListener("textarea.arg", "dragover", this.recipe.textArgDragover, this.recipe);
this.addDynamicListener("textarea.arg", "dragleave", this.recipe.textArgDragLeave, this.recipe); this.addDynamicListener("textarea.arg", "dragleave", this.recipe.textArgDragLeave, this.recipe);
@ -224,6 +226,7 @@ class Manager {
this.addDynamicListener(".option-item input[type=checkbox]", "change", this.options.switchChange, this.options); this.addDynamicListener(".option-item input[type=checkbox]", "change", this.options.switchChange, this.options);
this.addDynamicListener(".option-item input[type=checkbox]#wordWrap", "change", this.options.setWordWrap, this.options); this.addDynamicListener(".option-item input[type=checkbox]#wordWrap", "change", this.options.setWordWrap, this.options);
this.addDynamicListener(".option-item input[type=checkbox]#useMetaKey", "change", this.bindings.updateKeybList, this.bindings); this.addDynamicListener(".option-item input[type=checkbox]#useMetaKey", "change", this.bindings.updateKeybList, this.bindings);
this.addDynamicListener(".option-item input[type=checkbox]#showCatCount", "change", this.ops.setCatCount, this.ops);
this.addDynamicListener(".option-item input[type=number]", "keyup", this.options.numberChange, this.options); this.addDynamicListener(".option-item input[type=number]", "keyup", this.options.numberChange, this.options);
this.addDynamicListener(".option-item input[type=number]", "change", this.options.numberChange, this.options); this.addDynamicListener(".option-item input[type=number]", "change", this.options.numberChange, this.options);
this.addDynamicListener(".option-item select", "change", this.options.selectChange, this.options); this.addDynamicListener(".option-item select", "change", this.options.selectChange, this.options);

View File

@ -1,10 +1,10 @@
<!-- htmlmin:ignore --><!-- <!-- htmlmin:ignore --><!--
CyberChef - The Cyber Swiss Army Knife CyberChef - The Cyber Swiss Army Knife
@copyright Crown Copyright 2016 @copyright Crown Copyright 2016-<%= htmlWebpackPlugin.options.compileYear %>
@license Apache-2.0 @license Apache-2.0
Copyright 2016 Crown Copyright Copyright 2016-<%= htmlWebpackPlugin.options.compileYear %> Crown Copyright
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -25,7 +25,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>CyberChef</title> <title>CyberChef</title>
<meta name="copyright" content="Crown Copyright 2016" /> <meta name="copyright" content="Crown Copyright 2016-<%= htmlWebpackPlugin.options.compileYear %>" />
<meta name="description" content="The Cyber Swiss Army Knife - a web app for encryption, encoding, compression and data analysis" /> <meta name="description" content="The Cyber Swiss Army Knife - a web app for encryption, encoding, compression and data analysis" />
<meta name="keywords" content="base64, hex, decode, encode, encrypt, decrypt, compress, decompress, regex, regular expressions, hash, crypt, hexadecimal, user agent, url, certificate, x.509, parser, JSON, gzip, md5, sha1, aes, des, blowfish, xor" /> <meta name="keywords" content="base64, hex, decode, encode, encrypt, decrypt, compress, decompress, regex, regular expressions, hash, crypt, hexadecimal, user agent, url, certificate, x.509, parser, JSON, gzip, md5, sha1, aes, des, blowfish, xor" />
@ -63,7 +63,8 @@
"Training branch predictor...", "Training branch predictor...",
"Timing cache hits...", "Timing cache hits...",
"Speculatively executing recipes...", "Speculatively executing recipes...",
"Adding LLM hallucinations..." "Adding LLM hallucinations...",
"Decompressing malware..."
]; ];
// Shuffle array using Durstenfeld algorithm // Shuffle array using Durstenfeld algorithm
@ -142,6 +143,9 @@
<div id="preloader-error" class="loading-error"></div> <div id="preloader-error" class="loading-error"></div>
</div> </div>
<!-- End preloader overlay --> <!-- End preloader overlay -->
<button type="button" aria-label="Edit Favourites" class="btn btn-warning bmd-btn-icon" id="edit-favourites" data-toggle="tooltip" title="Edit favourites">
<i class="material-icons" aria-hidden="true">star</i>
</button>
<div id="content-wrapper"> <div id="content-wrapper">
<div id="banner"> <div id="banner">
<div> <div>
@ -189,6 +193,7 @@
data-help-title="Operations list" data-help-title="Operations list"
data-help="<p>The Operations list contains all the operations in CyberChef arranged into categories. Some operations may be present in multiple categories. You can search for operations using the search box.</p><p>To use an operation, either double click it, or drag it into the Recipe pane. You will then be able to configure its arguments (or 'Ingredients' in CyberChef terminology).</p>"> data-help="<p>The Operations list contains all the operations in CyberChef arranged into categories. Some operations may be present in multiple categories. You can search for operations using the search box.</p><p>To use an operation, either double click it, or drag it into the Recipe pane. You will then be able to configure its arguments (or 'Ingredients' in CyberChef terminology).</p>">
Operations Operations
<span class="op-count"></span>
<span class="pane-controls"> <span class="pane-controls">
<button type="button" <button type="button"
class="btn bmd-btn-icon mobile-only hidden" class="btn bmd-btn-icon mobile-only hidden"
@ -218,16 +223,20 @@
<div class="title no-select"> <div class="title no-select">
Recipe Recipe
<span class="pane-controls"> <span class="pane-controls">
<button type="button" class="btn btn-primary bmd-btn-icon" id="save" data-toggle="tooltip" title="Save recipe" data-help-title="Saving a recipe" data-help="<p>Recipes can be represented in a few different formats and saved for use at a later date. You can either copy the Recipe configuration and save it somewhere offline for later use, or use your browser's local storage.</p><ul><li><b>Deep link:</b> The easiest way to share a CyberChef Recipe is to copy the deep link, either from the address bar (which is updated as the Recipe or Input changes), or from the 'Save recipe' pane. When you visit this link, the Recipe and Input should be populated from where you left off.</li><li><b>Chef format:</b> This custom format is designed to be compact and easily readable. It is the format used in CyberChef's URL, so it largely uses characters that do not have to be escaped in URL encoding, making it a little easier to understand what a CyberChef URL contains.</li><li><b>Clean JSON:</b> This JSON format uses whitespace and indentation in a way that makes the Recipe easy to read.</li><li><b>Compact JSON:</b> This is the most compact way that the Recipe can be represented in JSON.</li><li><b>Local storage:</b> Alternatively, you can enter a name into the 'Recipe name' field and save to your browser's local storage. The Recipe will then be available to load from the 'Load Recipe' pane as long as you are using the same browser profile. Be aware that if your browser profile is cleaned, you may lose this data.</li></ul>"> <button type="button" aria-label="Hide arguments" class="btn btn-primary bmd-btn-icon" id="hide-icon" data-toggle="tooltip" title="Hide arguments" hide-args="false" data-help-title="Hiding every Operation's argument view in a Recipe" data-help="Clicking 'Hide arguments' will hide all the argument views for every Operation in the Recipe, to save space when you have too many Operation in your Recipe">
<i class="material-icons">save</i> <i class="material-icons">keyboard_arrow_up</i>
</button> </button>
<button type="button" class="btn btn-primary bmd-btn-icon" id="load" data-toggle="tooltip" title="Load recipe" data-help-title="Loading a recipe" data-help="<p>Saved recipes can be loaded using one of the following methods:</p><ul><li>If you have a CyberChef deep link, simply visit that link and the Recipe and Input should be populated automatically.</li><li>If you have a Recipe string in any of the accepted formats, paste it into the 'Load recipe' pane textbox and click 'Load'.</li><li>If you have saved a Recipe to your browser's local storage, it should be available in the dropdown menu in the 'Load recipe' pane. If it is not there, you may not be using the same browser profile, or your profile may have been cleared.</li></ul>">
<i class="material-icons">folder</i> <button type="button" aria-label="Save recipe" class="btn btn-primary bmd-btn-icon" id="save" data-toggle="tooltip" title="Save recipe" data-help-title="Saving a recipe" data-help="<p>Recipes can be represented in a few different formats and saved for use at a later date. You can either copy the Recipe configuration and save it somewhere offline for later use, or use your browser's local storage.</p><ul><li><b>Deep link:</b> The easiest way to share a CyberChef Recipe is to copy the deep link, either from the address bar (which is updated as the Recipe or Input changes), or from the 'Save recipe' pane. When you visit this link, the Recipe and Input will be populated from where you left off.</li><li><b>Chef format:</b> This custom format is designed to be compact and easily readable. It is the format used in CyberChef's URL, so it largely uses characters that do not have to be escaped in URL encoding, making it a little easier to understand what a CyberChef URL contains.</li><li><b>Clean JSON:</b> This JSON format uses whitespace and indentation in a way that makes the Recipe easy to read.</li><li><b>Compact JSON:</b> This is the most compact way that the Recipe can be represented in JSON.</li><li><b>Local storage:</b> Alternatively, you can enter a name into the 'Recipe name' field and save to your browser's local storage. The Recipe will then be available to load from the 'Load Recipe' pane as long as you are using the same browser profile. Be aware that if your browser profile is cleaned, you may lose this data.</li></ul>">
<i class="material-icons" aria-hidden="true">save</i>
</button> </button>
<button type="button" class="btn btn-primary bmd-btn-icon" id="clr-recipe" data-toggle="tooltip" title="Clear recipe" data-help-title="Clearing a recipe" data-help="Clicking the 'Clear recipe' button will remove all operations from the Recipe. It will not clear the Input, but it will trigger a Bake if Auto-bake is turned on, which will change the value of the Output."> <button type="button" aria-label="Load recipe" class="btn btn-primary bmd-btn-icon" id="load" data-toggle="tooltip" title="Load recipe" data-help-title="Loading a recipe" data-help="<p>Saved recipes can be loaded using one of the following methods:</p><ul><li>If you have a CyberChef deep link, simply visit that link and the Recipe and Input will be populated automatically.</li><li>If you have a Recipe string in any of the accepted formats, paste it into the 'Load recipe' pane textbox and click 'Load'.</li><li>If you have saved a Recipe to your browser's local storage, it should be available in the dropdown menu in the 'Load recipe' pane. If it is not there, you may not be using the same browser profile, or your profile may have been cleared.</li></ul>">
<i class="material-icons">delete</i> <i class="material-icons" aria-hidden="true">folder</i>
</button> </button>
<button type="button" class="mobile-only btn btn-primary bmd-btn-icon btn-maximise" id="maximise-recipe" data-toggle="tooltip" title="Maximise pane" data-help-title="Maximise pane" data-help="This button allows you to view the Recipe pane at maximum size, hiding the Operations, Input and Output panes. You can restore the pane to its normal size by clicking the same button again."> <button type="button" aria-label="Clear recipe" class="btn btn-primary bmd-btn-icon" id="clr-recipe" data-toggle="tooltip" title="Clear recipe" data-help-title="Clearing a recipe" data-help="Clicking the 'Clear recipe' button will remove all operations from the Recipe. It will not clear the Input, but it will trigger a Bake if Auto-bake is turned on, which will change the value of the Output.">
<i class="material-icons" aria-hidden="true">delete</i>
</button>
<button type="button" aria-label="Maximise pane" class="mobile-only btn btn-primary bmd-btn-icon btn-maximise" id="maximise-recipe" data-toggle="tooltip" title="Maximise pane" data-help-title="Maximise pane" data-help="This button allows you to view the Recipe pane at maximum size, hiding the Operations, Input and Output panes. You can restore the pane to its normal size by clicking the same button again.">
<i class="material-icons">fullscreen</i> <i class="material-icons">fullscreen</i>
</button> </button>
</span> </span>
@ -265,19 +274,19 @@
Input Input
<span class="pane-controls"> <span class="pane-controls">
<div class="io-info" id="input-files-info"></div> <div class="io-info" id="input-files-info"></div>
<button type="button" class="desktop-only btn btn-primary bmd-btn-icon" id="btn-new-tab" data-toggle="tooltip" title="Add a new input tab" data-help-title="Tabs" data-help="<p>New tabs can be created to support multiple Inputs. These tabs have their own associated character encodings and EOL separators, as defined in their status bars.</p><p>The deep link in the URL bar only contains information about the currently active tab.</p>"> <button type="button" aria-label="Add new input tab" class="desktop-only btn btn-primary bmd-btn-icon" id="btn-new-tab" data-toggle="tooltip" title="Add a new input tab" data-help-title="Tabs" data-help="<p>New tabs can be created to support multiple Inputs. These tabs have their own associated character encodings and EOL separators, as defined in their status bars.</p><p>The deep link in the URL bar only contains information about the currently active tab.</p>">
<i class="material-icons">add</i> <i class="material-icons" aria-hidden="true">add</i>
</button> </button>
<button type="button" class="btn btn-primary bmd-btn-icon" id="btn-open-folder" data-toggle="tooltip" title="Open folder as input" data-help-title="Opening a folder" data-help="<p>You can open a whole folder into CyberChef, which will result in each file being loaded into a separate Input tab.</p><p>CyberChef can handle lots of Input files, but be aware that performance may suffer, especially if the files are large in size.</p><p>Folders can also be loaded by dragging them over the Input pane and dropping them.</p>"> <button type="button" aria-label="Open folder as input" class="btn btn-primary bmd-btn-icon" id="btn-open-folder" data-toggle="tooltip" title="Open folder as input" data-help-title="Opening a folder" data-help="<p>You can open a whole folder into CyberChef, which will result in each file being loaded into a separate Input tab.</p><p>CyberChef can handle lots of Input files, but be aware that performance may suffer, especially if the files are large in size.</p><p>Folders can also be loaded by dragging them over the Input pane and dropping them.</p>">
<i class="material-icons">folder_open</i> <i class="material-icons" aria-hidden="true">folder_open</i>
<input type="file" id="open-folder" style="display: none" multiple directory webkitdirectory> <input type="file" id="open-folder" style="display: none" multiple directory webkitdirectory>
</button> </button>
<button type="button" class="btn btn-primary bmd-btn-icon" id="btn-open-file" data-toggle="tooltip" title="Open file as input" data-help-title="Opening a file" data-help="<p>Files can be loaded into CyberChef individually or in groups, either using the 'Open file as input' button, or by dragging and dropping them over the Input pane.</p><p>CyberChef can handle reasonably large files (at least 500MB, depending on hardware), but performance may be impacted and some Operations will run very slowly over large Inputs.</p>"> <button type="button" aria-label="Open file as input" class="btn btn-primary bmd-btn-icon" id="btn-open-file" data-toggle="tooltip" title="Open file as input" data-help-title="Opening a file" data-help="<p>Files can be loaded into CyberChef individually or in groups, either using the 'Open file as input' button, or by dragging and dropping them over the Input pane.</p><p>CyberChef can handle reasonably large files (at least 500MB, depending on hardware), but performance may be impacted and some Operations will run very slowly over large Inputs.</p>">
<i class="material-icons">input</i> <i class="material-icons" aria-hidden="true">input</i>
<input type="file" id="open-file" style="display: none" multiple> <input type="file" id="open-file" style="display: none" multiple>
</button> </button>
<button type="button" class="btn btn-primary bmd-btn-icon" id="clr-io" data-toggle="tooltip" title="Clear input and output" data-help-title="Clearing the Input and Output" data-help="Clicking the 'Clear input and output' button will remove all Inputs and Outputs. It will not clear the Recipe."> <button type="button" aria-label="Clear input and output" class="btn btn-primary bmd-btn-icon" id="clr-io" data-toggle="tooltip" title="Clear input and output" data-help-title="Clearing the Input and Output" data-help="Clicking the 'Clear input and output' button will remove all Inputs and Outputs. It will not clear the Recipe.">
<i class="material-icons">delete</i> <i class="material-icons" aria-hidden="true">delete</i>
</button> </button>
<button type="button" class="mobile-only btn btn-primary bmd-btn-icon btn-maximise" id="maximise-input" data-toggle="tooltip" title="Maximise pane" data-help-title="Maximise pane" data-help="This button allows you to view the Input pane at maximum size, hiding the Operations, Recipe and Output panes. You can restore the pane to its normal size by clicking the same button again."> <button type="button" class="mobile-only btn btn-primary bmd-btn-icon btn-maximise" id="maximise-input" data-toggle="tooltip" title="Maximise pane" data-help-title="Maximise pane" data-help="This button allows you to view the Input pane at maximum size, hiding the Operations, Recipe and Output panes. You can restore the pane to its normal size by clicking the same button again.">
<i class="material-icons">fullscreen</i> <i class="material-icons">fullscreen</i>
@ -285,6 +294,9 @@
<button type="button" class="desktop-only btn btn-primary bmd-btn-icon" id="reset-layout" data-toggle="tooltip" title="Reset pane layout" data-help-title="Resetting the pane layout" data-help="CyberChef's panes can be resized to suit your area of focus. This button will reset the pane sizes to their default configuration."> <button type="button" class="desktop-only btn btn-primary bmd-btn-icon" id="reset-layout" data-toggle="tooltip" title="Reset pane layout" data-help-title="Resetting the pane layout" data-help="CyberChef's panes can be resized to suit your area of focus. This button will reset the pane sizes to their default configuration.">
<i class="material-icons">view_compact</i> <i class="material-icons">view_compact</i>
</button> </button>
<button type="button" aria-label="Reset pane layout" class="desktop-only btn btn-primary bmd-btn-icon" id="reset-layout" data-toggle="tooltip" title="Reset pane layout" data-help-title="Resetting the pane layout" data-help="CyberChef's panes can be resized to suit your area of focus. This button will reset the pane sizes to their default configuration.">
<i class="material-icons" aria-hidden="true">view_compact</i>
</button>
</span> </span>
</div> </div>
@ -316,7 +328,8 @@
<div id="input-text"></div> <div id="input-text"></div>
</div> </div>
</div> </div>
<div id="output" class="split" data-help-title="Output pane" data-help="<p>This pane displays the results of the Recipe after it has processed your Input.</p><p>CyberChef does its best to represent data as accurately as possible to ensure you know exactly what you are working with. Non-printable characters are represented using control character pictures, for example a null byte (0x00) is displayed like this: <span title='Control character null' aria-label='Control character null' class='cm-specialChar'>␀</span>.</p><p>When copying these characters from the Output, the original byte value should be copied into your clipboard, rather than the control character picture itself.</p>">
<div id="output" class="split" data-help-title="Output pane" data-help="<p>This pane displays the results of the Recipe after it has processed your Input.</p><p>CyberChef does its best to represent data as accurately as possible to ensure you know exactly what you are working with. Non-printable characters are represented using control character pictures, for example a null byte (0x00) is displayed like this: <span title='Control character null' aria-label='Control character null' class='cm-specialChar'>␀</span>.</p><p>When copying these characters from the Output, the original byte value will be copied into your clipboard, rather than the control character picture itself.</p>">
<div class="title no-select"> <div class="title no-select">
Output Output
<div class="flex-row-reverse"> <div class="flex-row-reverse">
@ -333,20 +346,29 @@
<button type="button" class="btn btn-primary bmd-btn-icon" id="save-all-to-file" data-toggle="tooltip" title="Save all outputs to a zip file" style="display: none" data-help-title="Saving all outputs to a zip file" data-help="<p>When operating with multiple tabbed Inputs and Outputs, you can use this button to save off all the Outputs at once in a ZIP file.</p><p>Use the 'Bake' button to bake all Inputs at once.</p><p>You will be given the choice to specify the file extension for the Outputs, or you can let CyberChef attempt to detect the filetype of each one. If an Output's type is not clear, CyberChef will use the '.dat' extension.</p>"> <button type="button" class="btn btn-primary bmd-btn-icon" id="save-all-to-file" data-toggle="tooltip" title="Save all outputs to a zip file" style="display: none" data-help-title="Saving all outputs to a zip file" data-help="<p>When operating with multiple tabbed Inputs and Outputs, you can use this button to save off all the Outputs at once in a ZIP file.</p><p>Use the 'Bake' button to bake all Inputs at once.</p><p>You will be given the choice to specify the file extension for the Outputs, or you can let CyberChef attempt to detect the filetype of each one. If an Output's type is not clear, CyberChef will use the '.dat' extension.</p>">
<i class="material-icons">archive</i> <i class="material-icons">archive</i>
</button> </button>
<button type="button" class="btn btn-primary bmd-btn-icon" id="save-to-file" data-toggle="tooltip" title="Save output to file" data-help-title="Saving output to a file" data-help="The currently active Output can be saved to a file. You will be asked to specify a filename. CyberChef will attempt to guess the correct file extension based on the data. If a file type cannot be detected, the extension defaults to '.dat' but can be changed manually."> <button type="button" aria-label="save" class="btn btn-primary bmd-btn-icon" id="save-to-file" data-toggle="tooltip" title="Save output to file" data-help-title="Saving output to a file" data-help="The currently active Output can be saved to a file. You will be asked to specify a filename. CyberChef will attempt to guess the correct file extension based on the data. If a file type cannot be detected, the extension defaults to '.dat' but can be changed manually.">
<i class="material-icons">save</i> <i class="material-icons" aria-hidden="true">save</i>
</button> </button>
<button type="button" class="btn btn-primary bmd-btn-icon" id="copy-output" data-toggle="tooltip" title="Copy raw output to the clipboard" data-help-title="Copying raw output to the clipboard" data-help="<p>Data can be copied from the Output in the normal way by selecting text and copying it. This button provides a quick way of copying the entire output to the clipboard without having to select it. It directly copies the raw data rather than selecting text in the Output editor. Each method should have the same result, but the button may be more efficient for large Outputs as it does not require any DOM interaction.</p>"> <button type="button" aria-label="copy content" class="btn btn-primary bmd-btn-icon" id="copy-output" data-toggle="tooltip" title="Copy raw output to the clipboard" data-help-title="Copying raw output to the clipboard" data-help="<p>Data can be copied from the Output in the normal way by selecting text and copying it. This button provides a quick way of copying the entire output to the clipboard without having to select it. It directly copies the raw data rather than selecting text in the Output editor. Each method will have the same result, but the button may be more efficient for large Outputs as it does not require any DOM interaction.</p>">
<i class="material-icons">content_copy</i> <i class="material-icons" aria-hidden="true">content_copy</i>
</button> </button>
<button type="button" class="btn btn-primary bmd-btn-icon" id="switch" data-toggle="tooltip" title="Replace input with output" data-help-title="Replacing input with output" data-help="<p>This button moves the currently active Output data into the currently active Input tab, overwriting whatever data was already there.</p><p>The Input character encoding and EOL sequence will be changed to match the current Output values, so that the data is interpreted correctly.</p>"> <button type="button" aria-label="replace input with output" class="btn btn-primary bmd-btn-icon" id="switch" data-toggle="tooltip" title="Replace input with output" data-help-title="Replacing input with output" data-help="<p>This button moves the currently active Output data into the currently active Input tab, overwriting whatever data was already there.</p><p>The Input character encoding and EOL sequence will be changed to match the current Output values, so that the data is interpreted correctly.</p>">
<i class="material-icons">open_in_browser</i> <i class="material-icons" aria-hidden="true">open_in_browser</i>
</button> </button>
<button type="button" class="btn btn-primary bmd-btn-icon btn-maximise" id="maximise-output" data-toggle="tooltip" title="Maximise pane" data-help-title="Maximise pane" data-help="This button allows you to view the Output pane at maximum size, hiding the Operations, Recipe and Input panes. You can restore the pane to its normal size by clicking the same button again."> <button type="button" aria-label="maximise output pane" class="btn btn-primary bmd-btn-icon" id="maximise-output" data-toggle="tooltip" title="Maximise output pane" data-help-title="Maximising the Output pane" data-help="This button allows you to view the Output pane at maximum size, hiding the Operations, Recipe and Input panes. You can restore the pane to its normal size by clicking the same button again.">
<i class="material-icons">fullscreen</i> <i class="material-icons" aria-hidden="true">fullscreen</i>
</button> </button>
</span> </span>
</div> </div>
<button type="button" class="btn btn-primary bmd-btn-icon hidden" id="magic" data-toggle="tooltip" title="Magic!" data-html="true" data-help-title="CyberChef Magic!" data-help="<p>One of CyberChef's best features is its ability to automatically detect which Operations might make more sense of your data. The Magic button appears when CyberChef has a suggested Operation for you based on the data in the Output.</p><p>Clicking on the button will add the suggested Operation(s) to your Recipe.</p><p>This background Magic detection will inspect your Output up to three levels deep and attempt to unwrap it using a range of techniques. For more control, use the 'Magic' operation, which allows you to configure greater depth and filter based on various parameters.</p><p>Further information about CyberChef Magic can be found <a href='https://github.com/gchq/CyberChef/wiki/Automatic-detection-of-encoded-data-using-CyberChef-Magic'>here</a>.</p>">
<svg width="22" height="22" viewBox="0 0 24 24">
<path d="M7.5,5.6L5,7L6.4,4.5L5,2L7.5,3.4L10,2L8.6,4.5L10,7L7.5,5.6M19.5,15.4L22,14L20.6,16.5L22,19L19.5,17.6L17,19L18.4,16.5L17,14L19.5,15.4M22,2L20.6,4.5L22,7L19.5,5.6L17,7L18.4,4.5L17,2L19.5,3.4L22,2M13.34,12.78L15.78,10.34L13.66,8.22L11.22,10.66L13.34,12.78M14.37,7.29L16.71,9.63C17.1,10 17.1,10.65 16.71,11.04L5.04,22.71C4.65,23.1 4,23.1 3.63,22.71L1.29,20.37C0.9,20 0.9,19.35 1.29,18.96L12.96,7.29C13.35,6.9 14,6.9 14.37,7.29Z" />
</svg>
</button>
<span id="stale-indicator" class="hidden" data-toggle="tooltip" title="The output is stale. The input or recipe has changed since this output was generated. Bake again to get the new value." data-help-title="Staleness indicator" data-help="The staleness indicator is displayed when the Recipe or Input has changed but the Output has not yet been updated to reflect this. It is most commonly displayed when Auto-bake is turned off and indicates that you need to Bake in order to see an accurate Output.">
<i class="material-icons">access_time</i>
</span>
</div> </div>
<div id="output-wrapper" class="no-select"> <div id="output-wrapper" class="no-select">
@ -562,6 +584,13 @@
Keep the current tab in sync between the input and output Keep the current tab in sync between the input and output
</label> </label>
</div> </div>
<div class="checkbox option-item">
<label for="showCatCount">
<input type="checkbox" option="showCatCount" id="showCatCount">
Show the number of operations in each category
</label>
</div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" id="reset-options">Reset options to default</button> <button type="button" class="btn btn-secondary" id="reset-options">Reset options to default</button>
@ -607,7 +636,7 @@
Version <%= htmlWebpackPlugin.options.version %><br> Version <%= htmlWebpackPlugin.options.version %><br>
Compile time: <%= htmlWebpackPlugin.options.compileTime %> Compile time: <%= htmlWebpackPlugin.options.compileTime %>
</p> </p>
<p>&copy; Crown Copyright 2016.</p> <p>&copy; Crown Copyright 2016-<%= htmlWebpackPlugin.options.compileYear %>.</p>
<p>Released under the Apache Licence, Version 2.0.</p> <p>Released under the Apache Licence, Version 2.0.</p>
<p><a href="https://gitter.im/gchq/CyberChef"> <p><a href="https://gitter.im/gchq/CyberChef">
<img src="<%- require('../static/images/gitter-badge.svg') %>"> <img src="<%- require('../static/images/gitter-badge.svg') %>">
@ -650,7 +679,7 @@
What sort of things can I do with CyberChef? What sort of things can I do with CyberChef?
</a> </a>
<div class="collapse" id="faq-examples"> <div class="collapse" id="faq-examples">
<p>There are around 300 operations in CyberChef allowing you to carry out simple and complex tasks easily. Here are some examples:</p> <p>There are <span class="num-ops">hundreds of</span> operations in CyberChef allowing you to carry out simple and complex tasks easily. Here are some examples:</p>
<ul> <ul>
<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=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=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>
@ -721,7 +750,7 @@
<br> <br>
<p>There are around 200 useful operations in CyberChef for anyone working on anything vaguely Internet-related, whether you just want to convert a timestamp to a different format, decompress gzipped data, create a SHA3 hash, or parse an X.509 certificate to find out who issued it.</p> <p>There are <span class="num-ops">hundreds of</span> useful operations in CyberChef for anyone working on anything vaguely Internet-related, whether you just want to convert a timestamp to a different format, decompress gzipped data, create a SHA3 hash, or parse an X.509 certificate to find out who issued it.</p>
<p>Its the Cyber Swiss Army Knife.</p> <p>Its the Cyber Swiss Army Knife.</p>
</div> </div>
<div role="tabpanel" class="tab-pane" id="keybindings" style="padding: 20px;"> <div role="tabpanel" class="tab-pane" id="keybindings" style="padding: 20px;">
@ -898,8 +927,8 @@
<h6>CyberChef v<%= htmlWebpackPlugin.options.version %></h6> <h6>CyberChef v<%= htmlWebpackPlugin.options.version %></h6>
<ul> <ul>
<li>Build time: <%= htmlWebpackPlugin.options.compileTime %></li> <li>Build time: <%= htmlWebpackPlugin.options.compileTime %></li>
<li>The changelog for this version can be viewed <a href="https://github.com/gchq/CyberChef/blob/master/CHANGELOG.md">here</a></li> <li>The changelog for this version can be viewed <a href="https://github.com/gchq/CyberChef/blob/v<%= htmlWebpackPlugin.options.version %>/CHANGELOG.md">here</a></li>
<li>&copy; Crown Copyright 2016</li> <li>&copy; Crown Copyright 2016-<%= htmlWebpackPlugin.options.compileYear %></li>
<li>Released under the Apache Licence, Version 2.0</li> <li>Released under the Apache Licence, Version 2.0</li>
<li>SHA256 hash: DOWNLOAD_HASH_PLACEHOLDER</li> <li>SHA256 hash: DOWNLOAD_HASH_PLACEHOLDER</li>
</ul> </ul>

View File

@ -51,7 +51,8 @@ function main() {
logLevel: "info", logLevel: "info",
autoMagic: true, autoMagic: true,
imagePreview: true, imagePreview: true,
syncTabs: true syncTabs: true,
showCatCount: false,
}; };
document.removeEventListener("DOMContentLoaded", main, false); document.removeEventListener("DOMContentLoaded", main, false);

View File

@ -99,10 +99,12 @@
.bmd-form-group.is-focused [class^='bmd-label'], .bmd-form-group.is-focused [class^='bmd-label'],
.bmd-form-group.is-focused [class*=' bmd-label'], .bmd-form-group.is-focused [class*=' bmd-label'],
.bmd-form-group.is-focused label, .bmd-form-group.is-focused label,
.checkbox label:hover { .checkbox label:hover,
.bmd-form-group.is-filled:focus-within .checkbox.option-item label {
color: var(--input-highlight-colour); color: var(--input-highlight-colour);
} }
.bmd-form-group.option-item label+.form-control{ .bmd-form-group.option-item label+.form-control{
background-image: background-image:
linear-gradient(to top, var(--input-highlight-colour) 2px, rgba(0, 0, 0, 0) 2px), linear-gradient(to top, var(--input-highlight-colour) 2px, rgba(0, 0, 0, 0) 2px),

View File

@ -77,5 +77,3 @@ c-operation-li li.operation.focused-op {
background-color: var(--focused-operation-bg-colour) !important; background-color: var(--focused-operation-bg-colour) !important;
border-color: var(--focused-operation-border-colour) !important; border-color: var(--focused-operation-border-colour) !important;
} }

View File

@ -53,4 +53,5 @@
@import "./layout/_structure.css"; @import "./layout/_structure.css";
/* Operations */ /* Operations */
@import "./operations/diff.css";
@import "./operations/json.css"; @import "./operations/json.css";

View File

@ -0,0 +1,8 @@
del {
background-color: var(--hl3);
}
ins {
text-decoration: underline; /* shouldn't be needed, but Chromium doesn't copy to clipboard without it */
background-color: var(--hl5);
}

View File

@ -44,7 +44,8 @@ ul.json-dict, ol.json-array {
display: contents; display: contents;
} }
.json-summary { .json-summary {
display: contents; display: inline;
list-style: none;
} }
/* Display object and array brackets when closed */ /* Display object and array brackets when closed */

View File

@ -36,6 +36,10 @@ body {
line-height: 0; line-height: 0;
} }
.hidden {
display: none;
}
.blur { .blur {
color: transparent !important; color: transparent !important;
text-shadow: rgba(0, 0, 0, 0.95) 0 0 10px !important; text-shadow: rgba(0, 0, 0, 0.95) 0 0 10px !important;

View File

@ -95,3 +95,42 @@ export function escapeControlChars(str, preserveWs=false, lineBreak="\n") {
return n.outerHTML; return n.outerHTML;
}); });
} }
/**
* Convert and EOL sequence to its name
*/
export const eolSeqToCode = {
"\u000a": "LF",
"\u000b": "VT",
"\u000c": "FF",
"\u000d": "CR",
"\u000d\u000a": "CRLF",
"\u0085": "NEL",
"\u2028": "LS",
"\u2029": "PS"
};
/**
* Convert an EOL name to its sequence
*/
export const eolCodeToSeq = {
"LF": "\u000a",
"VT": "\u000b",
"FF": "\u000c",
"CR": "\u000d",
"CRLF": "\u000d\u000a",
"NEL": "\u0085",
"LS": "\u2028",
"PS": "\u2029"
};
export const eolCodeToName = {
"LF": "Line Feed",
"VT": "Vertical Tab",
"FF": "Form Feed",
"CR": "Carriage Return",
"CRLF": "Carriage Return + Line Feed",
"NEL": "Next Line",
"LS": "Line Separator",
"PS": "Paragraph Separator"
};

View File

@ -6,6 +6,7 @@
import {showPanel} from "@codemirror/view"; import {showPanel} from "@codemirror/view";
import {CHR_ENC_SIMPLE_LOOKUP, CHR_ENC_SIMPLE_REVERSE_LOOKUP} from "../../core/lib/ChrEnc.mjs"; import {CHR_ENC_SIMPLE_LOOKUP, CHR_ENC_SIMPLE_REVERSE_LOOKUP} from "../../core/lib/ChrEnc.mjs";
import { eolCodeToName, eolSeqToCode } from "./editorUtils.mjs";
/** /**
* A Status bar extension for CodeMirror * A Status bar extension for CodeMirror
@ -23,6 +24,8 @@ class StatusBarPanel {
this.eolHandler = opts.eolHandler; this.eolHandler = opts.eolHandler;
this.chrEncHandler = opts.chrEncHandler; this.chrEncHandler = opts.chrEncHandler;
this.chrEncGetter = opts.chrEncGetter; this.chrEncGetter = opts.chrEncGetter;
this.getEncodingState = opts.getEncodingState;
this.getEOLState = opts.getEOLState;
this.htmlOutput = opts.htmlOutput; this.htmlOutput = opts.htmlOutput;
this.eolVal = null; this.eolVal = null;
@ -93,22 +96,12 @@ class StatusBarPanel {
// preventDefault is required to stop the URL being modified and popState being triggered // preventDefault is required to stop the URL being modified and popState being triggered
e.preventDefault(); e.preventDefault();
const eolLookup = { const eolCode = e.target.getAttribute("data-val");
"LF": "\u000a", if (!eolCode) return;
"VT": "\u000b",
"FF": "\u000c",
"CR": "\u000d",
"CRLF": "\u000d\u000a",
"NEL": "\u0085",
"LS": "\u2028",
"PS": "\u2029"
};
const eolval = eolLookup[e.target.getAttribute("data-val")];
if (eolval === undefined) return;
// Call relevant EOL change handler // Call relevant EOL change handler
this.eolHandler(eolval); this.eolHandler(e.target.getAttribute("data-val"), true);
hideElement(e.target.closest(".cm-status-bar-select-content")); hideElement(e.target.closest(".cm-status-bar-select-content"));
} }
@ -125,7 +118,7 @@ class StatusBarPanel {
if (isNaN(chrEncVal)) return; if (isNaN(chrEncVal)) return;
this.chrEncHandler(chrEncVal); this.chrEncHandler(chrEncVal, true);
this.updateCharEnc(chrEncVal); this.updateCharEnc(chrEncVal);
hideElement(e.target.closest(".cm-status-bar-select-content")); hideElement(e.target.closest(".cm-status-bar-select-content"));
} }
@ -222,25 +215,34 @@ class StatusBarPanel {
* @param {EditorState} state * @param {EditorState} state
*/ */
updateEOL(state) { updateEOL(state) {
if (state.lineBreak === this.eolVal) return; if (this.getEOLState() < 2 && state.lineBreak === this.eolVal) return;
const eolLookup = {
"\u000a": ["LF", "Line Feed"],
"\u000b": ["VT", "Vertical Tab"],
"\u000c": ["FF", "Form Feed"],
"\u000d": ["CR", "Carriage Return"],
"\u000d\u000a": ["CRLF", "Carriage Return + Line Feed"],
"\u0085": ["NEL", "Next Line"],
"\u2028": ["LS", "Line Separator"],
"\u2029": ["PS", "Paragraph Separator"]
};
const val = this.dom.querySelector(".eol-value"); const val = this.dom.querySelector(".eol-value");
const button = val.closest(".cm-status-bar-select-btn"); const button = val.closest(".cm-status-bar-select-btn");
const eolName = eolLookup[state.lineBreak]; let eolCode = eolSeqToCode[state.lineBreak];
val.textContent = eolName[0]; let eolName = eolCodeToName[eolCode];
button.setAttribute("title", `End of line sequence:<br>${eolName[1]}`);
button.setAttribute("data-original-title", `End of line sequence:<br>${eolName[1]}`); switch (this.getEOLState()) {
case 1: // Detected
val.classList.add("font-italic");
eolCode += " (detected)";
eolName += " (detected)";
// Pulse
val.classList.add("pulse");
setTimeout(() => {
val.classList.remove("pulse");
}, 2000);
break;
case 0: // Unset
case 2: // Manually set
default:
val.classList.remove("font-italic");
break;
}
val.textContent = eolCode;
button.setAttribute("title", `End of line sequence:<br>${eolName}`);
button.setAttribute("data-original-title", `End of line sequence:<br>${eolName}`);
this.eolVal = state.lineBreak; this.eolVal = state.lineBreak;
} }
@ -250,12 +252,30 @@ class StatusBarPanel {
*/ */
updateCharEnc() { updateCharEnc() {
const chrEncVal = this.chrEncGetter(); const chrEncVal = this.chrEncGetter();
if (chrEncVal === this.chrEncVal) return; if (this.getEncodingState() < 2 && chrEncVal === this.chrEncVal) return;
const name = CHR_ENC_SIMPLE_REVERSE_LOOKUP[chrEncVal] ? CHR_ENC_SIMPLE_REVERSE_LOOKUP[chrEncVal] : "Raw Bytes"; let name = CHR_ENC_SIMPLE_REVERSE_LOOKUP[chrEncVal] ? CHR_ENC_SIMPLE_REVERSE_LOOKUP[chrEncVal] : "Raw Bytes";
const val = this.dom.querySelector(".chr-enc-value"); const val = this.dom.querySelector(".chr-enc-value");
const button = val.closest(".cm-status-bar-select-btn"); const button = val.closest(".cm-status-bar-select-btn");
switch (this.getEncodingState()) {
case 1: // Detected
val.classList.add("font-italic");
name += " (detected)";
// Pulse
val.classList.add("pulse");
setTimeout(() => {
val.classList.remove("pulse");
}, 2000);
break;
case 0: // Unset
case 2: // Manually set
default:
val.classList.remove("font-italic");
break;
}
val.textContent = name; val.textContent = name;
button.setAttribute("title", `${this.label} character encoding:<br>${name}`); button.setAttribute("title", `${this.label} character encoding:<br>${name}`);
button.setAttribute("data-original-title", `${this.label} character encoding:<br>${name}`); button.setAttribute("data-original-title", `${this.label} character encoding:<br>${name}`);

View File

@ -5,6 +5,7 @@
*/ */
import Utils from "../../core/Utils.mjs"; import Utils from "../../core/Utils.mjs";
import { eolSeqToCode } from "../utils/editorUtils.mjs";
/** /**
@ -35,6 +36,11 @@ class ControlsWaiter {
boundary: "viewport", boundary: "viewport",
trigger: "hover" trigger: "hover"
}); });
// Set number of operations in various places in the DOM
document.querySelectorAll(".num-ops").forEach(el => {
el.innerHTML = Object.keys(this.app.operations).length;
});
} }
@ -140,16 +146,16 @@ class ControlsWaiter {
const inputChrEnc = this.manager.input.getChrEnc(); const inputChrEnc = this.manager.input.getChrEnc();
const outputChrEnc = this.manager.output.getChrEnc(); const outputChrEnc = this.manager.output.getChrEnc();
const inputEOLSeq = this.manager.input.getEOLSeq(); const inputEOL = eolSeqToCode[this.manager.input.getEOLSeq()];
const outputEOLSeq = this.manager.output.getEOLSeq(); const outputEOL = eolSeqToCode[this.manager.output.getEOLSeq()];
const params = [ const params = [
includeRecipe ? ["recipe", recipeStr] : undefined, includeRecipe ? ["recipe", recipeStr] : undefined,
includeInput && input.length ? ["input", Utils.escapeHtml(input)] : undefined, includeInput && input.length ? ["input", Utils.escapeHtml(input)] : undefined,
inputChrEnc !== 0 ? ["ienc", inputChrEnc] : undefined, inputChrEnc !== 0 ? ["ienc", inputChrEnc] : undefined,
outputChrEnc !== 0 ? ["oenc", outputChrEnc] : undefined, outputChrEnc !== 0 ? ["oenc", outputChrEnc] : undefined,
inputEOLSeq !== "\n" ? ["ieol", inputEOLSeq] : undefined, inputEOL !== "LF" ? ["ieol", inputEOL] : undefined,
outputEOLSeq !== "\n" ? ["oeol", outputEOLSeq] : undefined outputEOL !== "LF" ? ["oeol", outputEOL] : undefined
]; ];
const hash = params const hash = params
@ -344,6 +350,36 @@ class ControlsWaiter {
} }
/**
* Hides the arguments for all the operations in the current recipe.
*/
hideRecipeArgsClick() {
const icon = document.getElementById("hide-icon");
if (icon.getAttribute("hide-args") === "false") {
icon.setAttribute("hide-args", "true");
icon.setAttribute("data-original-title", "Show arguments");
icon.children[0].innerText = "keyboard_arrow_down";
Array.from(document.getElementsByClassName("hide-args-icon")).forEach(function(item) {
item.setAttribute("hide-args", "true");
item.innerText = "keyboard_arrow_down";
item.classList.add("hide-args-selected");
item.parentNode.previousElementSibling.style.display = "none";
});
} else {
icon.setAttribute("hide-args", "false");
icon.setAttribute("data-original-title", "Hide arguments");
icon.children[0].innerText = "keyboard_arrow_up";
Array.from(document.getElementsByClassName("hide-args-icon")).forEach(function(item) {
item.setAttribute("hide-args", "false");
item.innerText = "keyboard_arrow_up";
item.classList.remove("hide-args-selected");
item.parentNode.previousElementSibling.style.display = "grid";
});
}
}
/** /**
* Populates the bug report information box with useful technical info. * Populates the bug report information box with useful technical info.
* *

View File

@ -42,7 +42,7 @@ import {
import {statusBar} from "../utils/statusBar.mjs"; import {statusBar} from "../utils/statusBar.mjs";
import {fileDetailsPanel} from "../utils/fileDetails.mjs"; import {fileDetailsPanel} from "../utils/fileDetails.mjs";
import {renderSpecialChar} from "../utils/editorUtils.mjs"; import {eolCodeToSeq, eolCodeToName, renderSpecialChar} from "../utils/editorUtils.mjs";
/** /**
@ -62,6 +62,8 @@ class InputWaiter {
this.inputTextEl = document.getElementById("input-text"); this.inputTextEl = document.getElementById("input-text");
this.inputChrEnc = 0; this.inputChrEnc = 0;
this.eolState = 0; // 0 = unset, 1 = detected, 2 = manual
this.encodingState = 0; // 0 = unset, 1 = detected, 2 = manual
this.initEditor(); this.initEditor();
this.inputWorker = null; this.inputWorker = null;
@ -92,6 +94,7 @@ class InputWaiter {
fileDetailsPanel: new Compartment fileDetailsPanel: new Compartment
}; };
const self = this;
const initialState = EditorState.create({ const initialState = EditorState.create({
doc: null, doc: null,
extensions: [ extensions: [
@ -114,7 +117,9 @@ class InputWaiter {
label: "Input", label: "Input",
eolHandler: this.eolChange.bind(this), eolHandler: this.eolChange.bind(this),
chrEncHandler: this.chrEncChange.bind(this), chrEncHandler: this.chrEncChange.bind(this),
chrEncGetter: this.getChrEnc.bind(this) chrEncGetter: this.getChrEnc.bind(this),
getEncodingState: this.getEncodingState.bind(this),
getEOLState: this.getEOLState.bind(this)
}), }),
// Mutable state // Mutable state
@ -141,10 +146,21 @@ class InputWaiter {
if (e.docChanged && !this.silentInputChange) if (e.docChanged && !this.silentInputChange)
this.inputChange(e); this.inputChange(e);
this.silentInputChange = false; this.silentInputChange = false;
}),
// Event handlers
EditorView.domEventHandlers({
paste(event, view) {
setTimeout(() => {
self.afterPaste(event);
});
}
}) })
] ]
}); });
if (this.inputEditorView) this.inputEditorView.destroy();
this.inputEditorView = new EditorView({ this.inputEditorView = new EditorView({
state: initialState, state: initialState,
parent: this.inputTextEl parent: this.inputTextEl
@ -154,12 +170,23 @@ class InputWaiter {
/** /**
* Handler for EOL change events * Handler for EOL change events
* Sets the line separator * Sets the line separator
* @param {string} eolVal * @param {string} eol
* @param {boolean} [manual=false]
*/ */
eolChange(eolVal) { eolChange(eol, manual=false) {
const oldInputVal = this.getInput(); const eolVal = eolCodeToSeq[eol];
if (eolVal === undefined) return;
this.eolState = manual ? 2 : this.eolState;
if (this.eolState < 2 && eolVal === this.getEOLSeq()) return;
if (this.eolState === 1) {
// Alert
this.app.alert(`Input end of line separator has been detected and changed to ${eolCodeToName[eol]}`, 5000);
}
// Update the EOL value // Update the EOL value
const oldInputVal = this.getInput();
this.inputEditorView.dispatch({ this.inputEditorView.dispatch({
effects: this.inputEditorConf.eol.reconfigure(EditorState.lineSeparator.of(eolVal)) effects: this.inputEditorConf.eol.reconfigure(EditorState.lineSeparator.of(eolVal))
}); });
@ -176,16 +203,29 @@ class InputWaiter {
return this.inputEditorView.state.lineBreak; return this.inputEditorView.state.lineBreak;
} }
/**
* Returns whether the input EOL sequence was set manually or has been detected automatically
* @returns {number} - 0 = unset, 1 = detected, 2 = manual
*/
getEOLState() {
return this.eolState;
}
/** /**
* Handler for Chr Enc change events * Handler for Chr Enc change events
* Sets the input character encoding * Sets the input character encoding
* @param {number} chrEncVal * @param {number} chrEncVal
* @param {boolean} [manual=false] - Flag to indicate the encoding was set by the user
* @param {boolean} [internal=false] - Flag to indicate this was set internally, i.e. by loading from URI
*/ */
chrEncChange(chrEncVal) { chrEncChange(chrEncVal, manual=false, internal=false) {
if (typeof chrEncVal !== "number") return; if (typeof chrEncVal !== "number") return;
this.inputChrEnc = chrEncVal; this.inputChrEnc = chrEncVal;
this.encodingState = manual ? 2 : this.encodingState;
if (!internal) {
this.inputChange(); this.inputChange();
} }
}
/** /**
* Getter for the input character encoding * Getter for the input character encoding
@ -195,6 +235,14 @@ class InputWaiter {
return this.inputChrEnc; return this.inputChrEnc;
} }
/**
* Returns whether the input character encoding was set manually or has been detected automatically
* @returns {number} - 0 = unset, 1 = detected, 2 = manual
*/
getEncodingState() {
return this.encodingState;
}
/** /**
* Sets word wrap on the input editor * Sets word wrap on the input editor
* @param {boolean} wrap * @param {boolean} wrap
@ -594,10 +642,6 @@ class InputWaiter {
const inputStr = toBase64(inputVal, "A-Za-z0-9+/"); const inputStr = toBase64(inputVal, "A-Za-z0-9+/");
this.app.updateURL(true, inputStr); this.app.updateURL(true, inputStr);
} }
// Trigger a state change
if (!silent) window.dispatchEvent(this.manager.statechange);
}.bind(this)); }.bind(this));
} }
@ -866,6 +910,55 @@ class InputWaiter {
}, delay, "inputChange", this, [e])(); }, delay, "inputChange", this, [e])();
} }
/**
* Handler that fires just after input paste events.
* Checks whether the EOL separator or character encoding should be updated.
*
* @param {event} e
*/
afterPaste(e) {
// If EOL has been fixed, skip this.
if (this.eolState > 1) return;
const inputText = this.getInput();
// Detect most likely EOL sequence
const eolCharCounts = {
"LF": inputText.count("\u000a"),
"VT": inputText.count("\u000b"),
"FF": inputText.count("\u000c"),
"CR": inputText.count("\u000d"),
"CRLF": inputText.count("\u000d\u000a"),
"NEL": inputText.count("\u0085"),
"LS": inputText.count("\u2028"),
"PS": inputText.count("\u2029")
};
// If all zero, leave alone
const total = Object.values(eolCharCounts).reduce((acc, curr) => {
return acc + curr;
}, 0);
if (total === 0) return;
// Find most prevalent line ending sequence
const highest = Object.entries(eolCharCounts).reduce((acc, curr) => {
return curr[1] > acc[1] ? curr : acc;
}, ["LF", 0]);
let choice = highest[0];
// If CRLF not zero and more than half the highest alternative, choose CRLF
if ((eolCharCounts.CRLF * 2) > highest[1]) {
choice = "CRLF";
}
const eolVal = eolCodeToSeq[choice];
if (eolVal === this.getEOLSeq()) return;
// Setting automatically
this.eolState = 1;
this.eolChange(choice);
}
/** /**
* Handler for input dragover events. * Handler for input dragover events.
* Gives the user a visual cue to show that items can be dropped here. * Gives the user a visual cue to show that items can be dropped here.
@ -1199,6 +1292,14 @@ class InputWaiter {
this.manager.output.removeAllOutputs(); this.manager.output.removeAllOutputs();
this.manager.output.terminateZipWorker(); this.manager.output.terminateZipWorker();
this.eolState = 0;
this.encodingState = 0;
this.manager.output.eolState = 0;
this.manager.output.encodingState = 0;
this.initEditor();
this.manager.output.initEditor();
const tabsList = document.getElementById("input-tabs"); const tabsList = document.getElementById("input-tabs");
const tabsListChildren = tabsList.children; const tabsListChildren = tabsList.children;

View File

@ -173,12 +173,71 @@ class OperationsWaiter {
return -1; return -1;
} }
/** /**
* Handler for edit favourites click events. * Handler for edit favourites click events.
* Displays the 'Edit favourites' modal and handles the c-operation-list in the modal * Displays the 'Edit favourites' modal and handles the c-operation-list in the modal
* *
* @param {Event} e * @param {Event} e
*/ */
opListCreate(e) {
this.manager.recipe.createSortableSeedList(e.target);
// Populate ops total
document.querySelector("#operations .title .op-count").innerText = Object.keys(this.app.operations).length;
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
*/
enableOpsListPopovers(el) {
$(el).find("[data-toggle=popover]").addBack("[data-toggle=popover]")
.popover({trigger: "manual"})
.on("mouseenter", function(e) {
if (e.buttons > 0) return; // Mouse button held down - likely dragging an operation
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 && !$($(_this).data("bs.popover").tip).is(":hover"))) {
$(_this).popover("hide");
}
}, 50);
});
}
/**
* Handler for operation doubleclick events.
* Adds the operation to the recipe and auto bakes.
*
* @param {event} e
*/
operationDblclick(e) {
const li = e.target;
this.manager.recipe.addOperation(li.textContent);
}
/**
* Handler for edit favourites click events.
* Sets up the 'Edit favourites' pane and displays it.
*
* @param {event} e
*/
editFavouritesClick(e) { editFavouritesClick(e) {
const div = document.getElementById("editable-favourites"); const div = document.getElementById("editable-favourites");
@ -263,6 +322,19 @@ class OperationsWaiter {
resetFavouritesClick() { resetFavouritesClick() {
this.app.resetFavourites(); this.app.resetFavourites();
} }
/**
* Sets whether operation counts are displayed next to a category title
*/
setCatCount() {
if (this.app.options.showCatCount) {
document.querySelectorAll(".category-title .op-count").forEach(el => el.classList.remove("hidden"));
} else {
document.querySelectorAll(".category-title .op-count").forEach(el => el.classList.add("hidden"));
}
}
} }
export default OperationsWaiter; export default OperationsWaiter;

View File

@ -50,6 +50,7 @@ class OptionsWaiter {
// Initialise options // Initialise options
this.setWordWrap(); this.setWordWrap();
this.manager.ops.setCatCount();
} }

View File

@ -7,6 +7,7 @@
import Utils, {debounce} from "../../core/Utils.mjs"; import Utils, {debounce} from "../../core/Utils.mjs";
import Dish from "../../core/Dish.mjs"; import Dish from "../../core/Dish.mjs";
import {isUTF8, CHR_ENC_SIMPLE_REVERSE_LOOKUP} from "../../core/lib/ChrEnc.mjs";
import {detectFileType} from "../../core/lib/FileType.mjs"; import {detectFileType} from "../../core/lib/FileType.mjs";
import FileSaver from "file-saver"; import FileSaver from "file-saver";
import ZipWorker from "worker-loader?inline=no-fallback!../workers/ZipWorker.mjs"; import ZipWorker from "worker-loader?inline=no-fallback!../workers/ZipWorker.mjs";
@ -38,7 +39,7 @@ import {
import {statusBar} from "../utils/statusBar.mjs"; import {statusBar} from "../utils/statusBar.mjs";
import {htmlPlugin} from "../utils/htmlWidget.mjs"; import {htmlPlugin} from "../utils/htmlWidget.mjs";
import {copyOverride} from "../utils/copyOverride.mjs"; import {copyOverride} from "../utils/copyOverride.mjs";
import {renderSpecialChar} from "../utils/editorUtils.mjs"; import {eolCodeToSeq, eolCodeToName, renderSpecialChar} from "../utils/editorUtils.mjs";
/** /**
@ -70,6 +71,8 @@ class OutputWaiter {
this.zipWorker = null; this.zipWorker = null;
this.maxTabs = this.manager.tabs.calcMaxTabs(); this.maxTabs = this.manager.tabs.calcMaxTabs();
this.tabTimeout = null; this.tabTimeout = null;
this.eolState = 0; // 0 = unset, 1 = detected, 2 = manual
this.encodingState = 0; // 0 = unset, 1 = detected, 2 = manual
} }
/** /**
@ -109,6 +112,8 @@ class OutputWaiter {
eolHandler: this.eolChange.bind(this), eolHandler: this.eolChange.bind(this),
chrEncHandler: this.chrEncChange.bind(this), chrEncHandler: this.chrEncChange.bind(this),
chrEncGetter: this.getChrEnc.bind(this), chrEncGetter: this.getChrEnc.bind(this),
getEncodingState: this.getEncodingState.bind(this),
getEOLState: this.getEOLState.bind(this),
htmlOutput: this.htmlOutput htmlOutput: this.htmlOutput
}), }),
htmlPlugin(this.htmlOutput), htmlPlugin(this.htmlOutput),
@ -137,6 +142,7 @@ class OutputWaiter {
] ]
}); });
if (this.outputEditorView) this.outputEditorView.destroy();
this.outputEditorView = new EditorView({ this.outputEditorView = new EditorView({
state: initialState, state: initialState,
parent: this.outputTextEl parent: this.outputTextEl
@ -146,9 +152,21 @@ class OutputWaiter {
/** /**
* Handler for EOL change events * Handler for EOL change events
* Sets the line separator * Sets the line separator
* @param {string} eolVal * @param {string} eol
* @param {boolean} [manual=false]
*/ */
async eolChange(eolVal) { async eolChange(eol, manual=false) {
const eolVal = eolCodeToSeq[eol];
if (eolVal === undefined) return;
this.eolState = manual ? 2 : this.eolState;
if (this.eolState < 2 && eolVal === this.getEOLSeq()) return;
if (this.eolState === 1) {
// Alert
this.app.alert(`Output end of line separator has been detected and changed to ${eolCodeToName[eol]}`, 5000);
}
const currentTabNum = this.manager.tabs.getActiveTab("output"); const currentTabNum = this.manager.tabs.getActiveTab("output");
if (currentTabNum >= 0) { if (currentTabNum >= 0) {
this.outputs[currentTabNum].eolSequence = eolVal; this.outputs[currentTabNum].eolSequence = eolVal;
@ -180,13 +198,23 @@ class OutputWaiter {
return this.outputs[currentTabNum].eolSequence; return this.outputs[currentTabNum].eolSequence;
} }
/**
* Returns whether the output EOL sequence was set manually or has been detected automatically
* @returns {number} - 0 = unset, 1 = detected, 2 = manual
*/
getEOLState() {
return this.eolState;
}
/** /**
* Handler for Chr Enc change events * Handler for Chr Enc change events
* Sets the output character encoding * Sets the output character encoding
* @param {number} chrEncVal * @param {number} chrEncVal
* @param {boolean} [manual=false]
*/ */
async chrEncChange(chrEncVal) { async chrEncChange(chrEncVal, manual=false) {
if (typeof chrEncVal !== "number") return; if (typeof chrEncVal !== "number") return;
const currentEnc = this.getChrEnc();
const currentTabNum = this.manager.tabs.getActiveTab("output"); const currentTabNum = this.manager.tabs.getActiveTab("output");
if (currentTabNum >= 0) { if (currentTabNum >= 0) {
@ -195,10 +223,17 @@ class OutputWaiter {
throw new Error(`Cannot change output ${currentTabNum} chrEnc to ${chrEncVal}`); throw new Error(`Cannot change output ${currentTabNum} chrEnc to ${chrEncVal}`);
} }
this.encodingState = manual ? 2 : this.encodingState;
if (this.encodingState > 1) {
// Reset the output, forcing it to re-decode the data with the new character encoding // Reset the output, forcing it to re-decode the data with the new character encoding
await this.setOutput(this.currentOutputCache, true); await this.setOutput(this.currentOutputCache, true);
// Update the URL manually since we aren't firing a statechange event // Update the URL manually since we aren't firing a statechange event
this.app.updateURL(true); this.app.updateURL(true);
} else if (currentEnc !== chrEncVal) {
// Alert
this.app.alert(`Output character encoding has been detected and changed to ${CHR_ENC_SIMPLE_REVERSE_LOOKUP[chrEncVal] || "Raw Bytes"}`, 5000);
}
} }
/** /**
@ -213,6 +248,14 @@ class OutputWaiter {
return this.outputs[currentTabNum].encoding; return this.outputs[currentTabNum].encoding;
} }
/**
* Returns whether the output character encoding was set manually or has been detected automatically
* @returns {number} - 0 = unset, 1 = detected, 2 = manual
*/
getEncodingState() {
return this.encodingState;
}
/** /**
* Sets word wrap on the output editor * Sets word wrap on the output editor
* @param {boolean} wrap * @param {boolean} wrap
@ -248,6 +291,7 @@ class OutputWaiter {
const tabNum = this.manager.tabs.getActiveTab("output"); const tabNum = this.manager.tabs.getActiveTab("output");
this.manager.timing.recordTime("outputDecodingStart", tabNum); this.manager.timing.recordTime("outputDecodingStart", tabNum);
if (data instanceof ArrayBuffer) { if (data instanceof ArrayBuffer) {
await this.detectEncoding(data);
data = await this.bufferToStr(data); data = await this.bufferToStr(data);
} }
this.manager.timing.recordTime("outputDecodingEnd", tabNum); this.manager.timing.recordTime("outputDecodingEnd", tabNum);
@ -276,6 +320,9 @@ class OutputWaiter {
// If turning word wrap off, do it before we populate the editor for performance reasons // If turning word wrap off, do it before we populate the editor for performance reasons
if (!wrap) this.setWordWrap(wrap); if (!wrap) this.setWordWrap(wrap);
// Detect suitable EOL sequence
this.detectEOLSequence(data);
// We use setTimeout here to delay the editor dispatch until the next event cycle, // We use setTimeout here to delay the editor dispatch until the next event cycle,
// ensuring all async actions have completed before attempting to set the contents // ensuring all async actions have completed before attempting to set the contents
// of the editor. This is mainly with the above call to setWordWrap() in mind. // of the editor. This is mainly with the above call to setWordWrap() in mind.
@ -345,6 +392,85 @@ class OutputWaiter {
}); });
} }
/**
* Checks whether the EOL separator should be updated
*
* @param {string} data
*/
detectEOLSequence(data) {
// If EOL has been fixed, skip this.
if (this.eolState > 1) return;
// If data is too long, skip this.
if (data.length > 1000000) return;
// Detect most likely EOL sequence
const eolCharCounts = {
"LF": data.count("\u000a"),
"VT": data.count("\u000b"),
"FF": data.count("\u000c"),
"CR": data.count("\u000d"),
"CRLF": data.count("\u000d\u000a"),
"NEL": data.count("\u0085"),
"LS": data.count("\u2028"),
"PS": data.count("\u2029")
};
// If all zero, leave alone
const total = Object.values(eolCharCounts).reduce((acc, curr) => {
return acc + curr;
}, 0);
if (total === 0) return;
// Find most prevalent line ending sequence
const highest = Object.entries(eolCharCounts).reduce((acc, curr) => {
return curr[1] > acc[1] ? curr : acc;
}, ["LF", 0]);
let choice = highest[0];
// If CRLF not zero and more than half the highest alternative, choose CRLF
if ((eolCharCounts.CRLF * 2) > highest[1]) {
choice = "CRLF";
}
const eolVal = eolCodeToSeq[choice];
if (eolVal === this.getEOLSeq()) return;
// Setting automatically
this.eolState = 1;
this.eolChange(choice);
}
/**
* Checks whether the character encoding should be updated.
*
* @param {ArrayBuffer} data
*/
async detectEncoding(data) {
// If encoding has been fixed, skip this.
if (this.encodingState > 1) return;
// If data is too long, skip this.
if (data.byteLength > 1000000) return;
const enc = isUTF8(data); // 0 = not UTF8, 1 = ASCII, 2 = UTF8
switch (enc) {
case 0: // not UTF8
// Set to Raw Bytes
this.encodingState = 1;
await this.chrEncChange(0, false);
break;
case 2: // UTF8
// Set to UTF8
this.encodingState = 1;
await this.chrEncChange(65001, false);
break;
case 1: // ASCII
default:
// Ignore
break;
}
}
/** /**
* Calculates the maximum number of tabs to display * Calculates the maximum number of tabs to display
*/ */
@ -1400,7 +1526,6 @@ class OutputWaiter {
switchButton.firstElementChild.innerHTML = "open_in_browser"; switchButton.firstElementChild.innerHTML = "open_in_browser";
} }
/** /**
* Handler for find tab button clicked * Handler for find tab button clicked
*/ */

View File

@ -160,6 +160,45 @@ class RecipeWaiter {
window.dispatchEvent(this.manager.statechange); window.dispatchEvent(this.manager.statechange);
} }
/**
* Handler for hide-args click events.
* Updates the icon status.
*
* @fires Manager#statechange
* @param {event} e
*/
hideArgsClick(e) {
const icon = e.target;
if (icon.getAttribute("hide-args") === "false") {
icon.setAttribute("hide-args", "true");
icon.innerText = "keyboard_arrow_down";
icon.classList.add("hide-args-selected");
icon.parentNode.previousElementSibling.style.display = "none";
} else {
icon.setAttribute("hide-args", "false");
icon.innerText = "keyboard_arrow_up";
icon.classList.remove("hide-args-selected");
icon.parentNode.previousElementSibling.style.display = "grid";
}
const icons = Array.from(document.getElementsByClassName("hide-args-icon"));
if (icons.length > 1) {
// Check if ALL the icons are hidden/shown
const uniqueIcons = icons.map(function(item) {
return item.getAttribute("hide-args");
}).unique();
const controlsIconStatus = document.getElementById("hide-icon").getAttribute("hide-args");
// If all icons are in the same state and the global icon isn't, fix it
if (uniqueIcons.length === 1 && icon.getAttribute("hide-args") !== controlsIconStatus) {
this.manager.controls.hideRecipeArgsClick();
}
}
window.dispatchEvent(this.manager.statechange);
}
/** /**
* Generates a configuration object to represent the current recipe. * Generates a configuration object to represent the current recipe.

View File

@ -322,6 +322,28 @@ class WorkerWaiter {
}; };
} }
/**
* Cancels the current bake making it possible to autobake again
*/
cancelBakeForAutoBake() {
if (this.totalOutputs > 1) {
this.cancelBake();
} else {
// In this case the UI changes can be skipped
for (let i = this.chefWorkers.length - 1; i >= 0; i--) {
if (this.chefWorkers[i].active) {
this.removeChefWorker(this.chefWorkers[i]);
}
}
this.inputs = [];
this.inputNums = [];
this.totalOutputs = 0;
this.loadingOutputs = 0;
}
}
/** /**
* Cancels the current bake by terminating and removing all ChefWorkers * Cancels the current bake by terminating and removing all ChefWorkers
* *

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