diff --git a/.editorconfig b/.editorconfig
index b50059bb..cef4cab0 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -12,3 +12,7 @@ indent_size = 4
[{package.json,.travis.yml,nightwatch.json}]
indent_style = space
indent_size = 2
+
+[.github/**.yml]
+indent_style = space
+indent_size = 2
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
deleted file mode 100644
index 1350e976..00000000
--- a/.github/workflows/codeql.yml
+++ /dev/null
@@ -1,40 +0,0 @@
-name: "CodeQL Analysis"
-
-on:
- workflow_dispatch:
- push:
- branches: [ master ]
- pull_request:
- # The branches below must be a subset of the branches above
- branches: [ master ]
- types: [synchronize, opened, reopened]
- schedule:
- - cron: '22 17 * * 5'
-
-jobs:
- analyze:
- name: Analyze
- runs-on: ubuntu-latest
- permissions:
- actions: read
- contents: read
- security-events: write
-
- strategy:
- fail-fast: false
- matrix:
- language: [ 'javascript' ]
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@v3
-
- - name: Initialize CodeQL
- uses: github/codeql-action/init@v2
- with:
- languages: ${{ matrix.language }}
-
- - name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v2
- with:
- category: "/language:${{matrix.language}}"
diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml
index 8a3aff54..74710dff 100644
--- a/.github/workflows/master.yml
+++ b/.github/workflows/master.yml
@@ -1,58 +1,64 @@
name: "Master Build, Test & Deploy"
+permissions:
+ contents: read
+
on:
workflow_dispatch:
push:
branches:
- - master
+ - master
jobs:
main:
+ permissions:
+ contents: write
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v6
- - name: Set node version
- uses: actions/setup-node@v3
- with:
- node-version: '18.x'
+ - name: Set node version
+ uses: actions/setup-node@v6
+ with:
+ node-version: 18
+ registry-url: "https://registry.npmjs.org"
- - name: Install
- run: |
- export DETECT_CHROMEDRIVER_VERSION=true
- npm install
- npm run setheapsize
+ - name: Install
+ run: |
+ export DETECT_CHROMEDRIVER_VERSION=true
+ npm install
+ npm run setheapsize
- - name: Lint
- run: npx grunt lint
+ - name: Lint
+ run: npx grunt lint
- - name: Unit Tests
- run: |
- npm test
- npm run testnodeconsumer
+ - name: Unit Tests
+ run: |
+ npm test
+ npm run testnodeconsumer
- - name: Production Build
- if: success()
- run: npx grunt prod --msg="Version 10 is here! Read about the new features here"
+ - name: Production Build
+ if: success()
+ run: npx grunt prod --msg=""
- - name: Generate sitemap
- run: npx grunt exec:sitemap
+ - name: Generate sitemap
+ run: npx grunt exec:sitemap
- - name: UI Tests
- if: success()
- run: |
- sudo apt-get install xvfb
- xvfb-run --server-args="-screen 0 1200x800x24" npx grunt testui
+ - name: UI Tests
+ if: success()
+ run: |
+ sudo apt-get install xvfb
+ xvfb-run --server-args="-screen 0 1200x800x24" npx grunt testui
- - name: Prepare for GitHub Pages
- if: success()
- run: npx grunt copy:ghPages
+ - name: Prepare for GitHub Pages
+ if: success()
+ run: npx grunt copy:ghPages
- - name: Deploy to GitHub Pages
- if: success() && github.ref == 'refs/heads/master'
- uses: crazy-max/ghaction-github-pages@v3
- with:
- target_branch: gh-pages
- build_dir: ./build/prod
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ - name: Deploy to GitHub Pages
+ if: success() && github.ref == 'refs/heads/master'
+ uses: crazy-max/ghaction-github-pages@v3
+ with:
+ target_branch: gh-pages
+ build_dir: ./build/prod
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/pull_requests.yml b/.github/workflows/pull_requests.yml
index 296e60b9..8f04df72 100644
--- a/.github/workflows/pull_requests.yml
+++ b/.github/workflows/pull_requests.yml
@@ -1,5 +1,8 @@
name: "Pull Requests"
+permissions:
+ contents: read
+
on:
workflow_dispatch:
pull_request:
@@ -9,47 +12,46 @@ jobs:
main:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v6
- - name: Set node version
- uses: actions/setup-node@v3
- with:
- node-version: '18.x'
+ - name: Set node version
+ uses: actions/setup-node@v6
+ with:
+ node-version: 18
+ registry-url: "https://registry.npmjs.org"
- - name: Install
- run: |
- export DETECT_CHROMEDRIVER_VERSION=true
- npm install
- npm run setheapsize
+ - name: Install
+ run: |
+ export DETECT_CHROMEDRIVER_VERSION=true
+ npm install
+ npm run setheapsize
- - name: Lint
- run: npx grunt lint
+ - name: Lint
+ run: npx grunt lint
- - name: Unit Tests
- run: |
- npm test
- npm run testnodeconsumer
+ - name: Unit Tests
+ run: |
+ npm test
+ npm run testnodeconsumer
- - name: Production Build
- if: success()
- run: npx grunt prod
+ - name: Production Build
+ if: success()
+ 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: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
- - name: UI Tests
- if: success()
- run: |
- sudo apt-get install xvfb
- xvfb-run --server-args="-screen 0 1200x800x24" npx grunt testui
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v3
+
+ - name: Production Image Build
+ if: success()
+ id: build-image
+ uses: docker/build-push-action@v6
+ with:
+ platforms: linux/amd64,linux/arm64
+ - name: UI Tests
+ if: success()
+ run: |
+ sudo apt-get install xvfb
+ xvfb-run --server-args="-screen 0 1200x800x24" npx grunt testui
diff --git a/.github/workflows/releases.yml b/.github/workflows/releases.yml
index a77f4984..b40af876 100644
--- a/.github/workflows/releases.yml
+++ b/.github/workflows/releases.yml
@@ -4,7 +4,11 @@ on:
workflow_dispatch:
push:
tags:
- - 'v*'
+ - "v*"
+
+permissions:
+ id-token: write
+ contents: read
env:
REGISTRY: ghcr.io
@@ -16,81 +20,78 @@ jobs:
main:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v6
- - name: Set node version
- uses: actions/setup-node@v3
- with:
- node-version: '18.x'
+ - name: Set node version
+ uses: actions/setup-node@v6
+ with:
+ node-version: 18
+ registry-url: "https://registry.npmjs.org"
- - name: Install
- run: |
- export DETECT_CHROMEDRIVER_VERSION=true
- npm ci
- npm run setheapsize
+ - name: Install
+ run: |
+ export DETECT_CHROMEDRIVER_VERSION=true
+ npm ci
+ npm run setheapsize
- - name: Lint
- run: npx grunt lint
+ - name: Lint
+ run: npx grunt lint
- - name: Unit Tests
- run: |
- npm test
- npm run testnodeconsumer
+ - name: Unit Tests
+ run: |
+ npm test
+ npm run testnodeconsumer
- - name: Production Build
- run: npx grunt prod
+ - name: Production Build
+ run: npx grunt prod
- - name: UI Tests
- run: |
- sudo apt-get install xvfb
- xvfb-run --server-args="-screen 0 1200x800x24" npx grunt testui
+ - name: UI Tests
+ run: |
+ sudo apt-get install xvfb
+ 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: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
- - 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,linux/arm64
- oci: true
- # enable build layer caching between platforms
- layers: true
- # Webpack seems to use a lot of open files, increase the max open file limit to accomodate.
- extra-args: |
- --ulimit nofile=10000
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v3
- - name: Publish to GHCR
- uses: redhat-actions/push-to-registry@v2
- with:
- image: ${{ steps.build-image.outputs.image }}
- tags: ${{ steps.build-image.outputs.tags }}
- registry: ${{ env.REGISTRY }}
- username: ${{ env.REGISTRY_USER }}
- password: ${{ env.REGISTRY_PASSWORD }}
+ - 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: Upload Release Assets
- id: upload-release-assets
- uses: svenstaro/upload-release-action@v2
- with:
- repo_token: ${{ secrets.GITHUB_TOKEN }}
- file: build/prod/*.zip
- tag: ${{ github.ref }}
- overwrite: true
- file_glob: true
- 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: Log in to GHCR
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ env.REGISTRY_USER }}
+ password: ${{ env.REGISTRY_PASSWORD }}
- - name: Publish to NPM
- uses: JS-DevTools/npm-publish@v1
- with:
- token: ${{ secrets.NPM_TOKEN }}
+ - name: Publish to GHCR
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ push: true
+ tags: ${{ steps.image-metadata.outputs.tags }}
+ labels: ${{ steps.image-metadata.outputs.labels }}
+ platforms: linux/amd64,linux/arm64
+
+ - name: Upload Release Assets
+ id: upload-release-assets
+ uses: svenstaro/upload-release-action@v2
+ with:
+ repo_token: ${{ secrets.GITHUB_TOKEN }}
+ file: build/prod/*.zip
+ tag: ${{ github.ref }}
+ overwrite: true
+ file_glob: true
+ 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
+ run: npm publish
diff --git a/Dockerfile b/Dockerfile
index ba605fd7..2184a294 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -27,9 +27,6 @@ RUN npm run build
#########################################
# Package static build files into nginx #
#########################################
-# We are using Github Actions: redhat-actions/buildah-build@v2 which needs manual selection of arch in base image
-# Remove TARGETARCH if docker buildx is supported in the CI release as --platform=$TARGETPLATFORM will be automatically set
-ARG TARGETPLATFORM
-FROM --platform=${TARGETPLATFORM} nginx:stable-alpine AS cyberchef
+FROM nginx:stable-alpine AS cyberchef
COPY --from=builder /app/build/prod /usr/share/nginx/html/
diff --git a/package-lock.json b/package-lock.json
index af76ee06..0b7bce14 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -31,7 +31,7 @@
"chi-squared": "^1.1.0",
"codepage": "^1.15.0",
"crypto-api": "^0.8.5",
- "crypto-browserify": "^3.12.0",
+ "crypto-browserify": "^3.12.1",
"crypto-js": "^4.2.0",
"ctph.js": "0.0.5",
"d3": "7.9.0",
@@ -61,7 +61,7 @@
"jsonwebtoken": "8.5.1",
"jsqr": "^1.4.0",
"jsrsasign": "^11.1.0",
- "kbpgp": "2.1.15",
+ "kbpgp": "^2.1.17",
"libbzip2-wasm": "0.0.4",
"libyara-wasm": "^1.2.1",
"lodash": "^4.17.21",
@@ -5137,7 +5137,6 @@
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
"integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"possible-typed-array-names": "^1.0.0"
@@ -8741,6 +8740,22 @@
"node": ">= 0.4"
}
},
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/es6-object-assign": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/es6-object-assign/-/es6-object-assign-1.1.0.tgz",
@@ -9844,7 +9859,6 @@
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
"integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"is-callable": "^1.1.3"
@@ -9904,14 +9918,16 @@
}
},
"node_modules/form-data": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
- "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"dev": true,
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
@@ -11706,7 +11722,6 @@
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
"integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -12057,6 +12072,21 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-typed-array": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
+ "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "which-typed-array": "^1.1.16"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-unc-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz",
@@ -12601,9 +12631,9 @@
}
},
"node_modules/kbpgp": {
- "version": "2.1.15",
- "resolved": "https://registry.npmjs.org/kbpgp/-/kbpgp-2.1.15.tgz",
- "integrity": "sha512-iFdQT+m2Mi2DB14kEFydF2joNe9x3E2VZCGZUt7UXsiZnQx5TtSl4KofP7EPtjHvf7weCxNKlEPSYiiCNMZ2jA==",
+ "version": "2.1.17",
+ "resolved": "https://registry.npmjs.org/kbpgp/-/kbpgp-2.1.17.tgz",
+ "integrity": "sha512-pnjH7amyg6dZLXyF42BKbCTST0l0r1ErunqtFRrJCkHkGJb83cZZmx1pnqNFr+d/ls+5gvcHrZLPfUG5q7oRYw==",
"license": "BSD-3-Clause",
"dependencies": {
"bn": "^1.0.5",
@@ -13983,9 +14013,9 @@
}
},
"node_modules/node-forge": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
- "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz",
+ "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==",
"license": "(BSD-3-Clause OR GPL-2.0)",
"engines": {
"node": ">= 6.13.0"
@@ -14900,19 +14930,20 @@
}
},
"node_modules/pbkdf2": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz",
- "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==",
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.5.tgz",
+ "integrity": "sha512-Q3CG/cYvCO1ye4QKkuH7EXxs3VC/rI1/trd+qX2+PolbaKG0H+bgcZzrTt96mMyRtejk+JMCiLUn3y29W8qmFQ==",
"license": "MIT",
"dependencies": {
- "create-hash": "^1.1.2",
- "create-hmac": "^1.1.4",
- "ripemd160": "^2.0.1",
- "safe-buffer": "^5.0.1",
- "sha.js": "^2.4.8"
+ "create-hash": "^1.2.0",
+ "create-hmac": "^1.1.7",
+ "ripemd160": "^2.0.3",
+ "safe-buffer": "^5.2.1",
+ "sha.js": "^2.4.12",
+ "to-buffer": "^1.2.1"
},
"engines": {
- "node": ">=0.12"
+ "node": ">= 0.10"
}
},
"node_modules/peek-readable": {
@@ -15170,7 +15201,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz",
"integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -16149,13 +16179,31 @@
}
},
"node_modules/ripemd160": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
- "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==",
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.3.tgz",
+ "integrity": "sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==",
"license": "MIT",
"dependencies": {
- "hash-base": "^3.0.0",
- "inherits": "^2.0.1"
+ "hash-base": "^3.1.2",
+ "inherits": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/ripemd160/node_modules/hash-base": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.2.tgz",
+ "integrity": "sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.4",
+ "readable-stream": "^2.3.8",
+ "safe-buffer": "^5.2.1",
+ "to-buffer": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
}
},
"node_modules/rison": {
@@ -16617,16 +16665,23 @@
"license": "ISC"
},
"node_modules/sha.js": {
- "version": "2.4.11",
- "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
- "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
+ "version": "2.4.12",
+ "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz",
+ "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==",
"license": "(MIT AND BSD-3-Clause)",
"dependencies": {
- "inherits": "^2.0.1",
- "safe-buffer": "^5.0.1"
+ "inherits": "^2.0.4",
+ "safe-buffer": "^5.2.1",
+ "to-buffer": "^1.2.0"
},
"bin": {
"sha.js": "bin.js"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/shebang-command": {
@@ -17663,6 +17718,26 @@
"node": ">=14.14"
}
},
+ "node_modules/to-buffer": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz",
+ "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==",
+ "license": "MIT",
+ "dependencies": {
+ "isarray": "^2.0.5",
+ "safe-buffer": "^5.2.1",
+ "typed-array-buffer": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/to-buffer/node_modules/isarray": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
+ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
+ "license": "MIT"
+ },
"node_modules/to-fast-properties": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz",
@@ -17845,6 +17920,20 @@
"node": ">= 0.6"
}
},
+ "node_modules/typed-array-buffer": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
+ "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-typed-array": "^1.1.14"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/ua-parser-js": {
"version": "1.0.40",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.40.tgz",
@@ -18855,7 +18944,6 @@
"version": "1.1.18",
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.18.tgz",
"integrity": "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"available-typed-arrays": "^1.0.7",
diff --git a/package.json b/package.json
index ec3f0520..fe50083c 100644
--- a/package.json
+++ b/package.json
@@ -117,7 +117,7 @@
"chi-squared": "^1.1.0",
"codepage": "^1.15.0",
"crypto-api": "^0.8.5",
- "crypto-browserify": "^3.12.0",
+ "crypto-browserify": "^3.12.1",
"crypto-js": "^4.2.0",
"ctph.js": "0.0.5",
"d3": "7.9.0",
@@ -147,7 +147,7 @@
"jsonwebtoken": "8.5.1",
"jsqr": "^1.4.0",
"jsrsasign": "^11.1.0",
- "kbpgp": "2.1.15",
+ "kbpgp": "^2.1.17",
"libbzip2-wasm": "0.0.4",
"libyara-wasm": "^1.2.1",
"lodash": "^4.17.21",
diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json
index 434c8bb6..aac00ca1 100644
--- a/src/core/config/Categories.json
+++ b/src/core/config/Categories.json
@@ -26,6 +26,8 @@
"From Base45",
"To Base58",
"From Base58",
+ "To Bech32",
+ "From Bech32",
"To Base62",
"From Base62",
"To Base64",
diff --git a/src/core/lib/Bech32.mjs b/src/core/lib/Bech32.mjs
new file mode 100644
index 00000000..6b87a142
--- /dev/null
+++ b/src/core/lib/Bech32.mjs
@@ -0,0 +1,371 @@
+/**
+ * Pure JavaScript implementation of Bech32 and Bech32m encoding.
+ *
+ * Bech32 is defined in BIP-0173: https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki
+ * Bech32m is defined in BIP-0350: https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki
+ *
+ * @author Medjedtxm
+ * @copyright Crown Copyright 2025
+ * @license Apache-2.0
+ */
+
+import OperationError from "../errors/OperationError.mjs";
+
+/** Bech32 character set (32 characters, excludes 1, b, i, o) */
+const CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
+
+/** Reverse lookup table for decoding */
+const CHARSET_REV = {};
+for (let i = 0; i < CHARSET.length; i++) {
+ CHARSET_REV[CHARSET[i]] = i;
+}
+
+/** Checksum constant for Bech32 (BIP-0173) */
+const BECH32_CONST = 1;
+
+/** Checksum constant for Bech32m (BIP-0350) */
+const BECH32M_CONST = 0x2bc830a3;
+
+/** Generator polynomial coefficients for checksum */
+const GENERATOR = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
+
+/**
+ * Compute the polymod checksum
+ * @param {number[]} values - Array of 5-bit values
+ * @returns {number} - Checksum value
+ */
+function polymod(values) {
+ let chk = 1;
+ for (const v of values) {
+ const top = chk >> 25;
+ chk = ((chk & 0x1ffffff) << 5) ^ v;
+ for (let i = 0; i < 5; i++) {
+ if ((top >> i) & 1) {
+ chk ^= GENERATOR[i];
+ }
+ }
+ }
+ return chk;
+}
+
+/**
+ * Expand HRP for checksum computation
+ * @param {string} hrp - Human-readable part (lowercase)
+ * @returns {number[]} - Expanded values
+ */
+function hrpExpand(hrp) {
+ const result = [];
+ for (let i = 0; i < hrp.length; i++) {
+ result.push(hrp.charCodeAt(i) >> 5);
+ }
+ result.push(0);
+ for (let i = 0; i < hrp.length; i++) {
+ result.push(hrp.charCodeAt(i) & 31);
+ }
+ return result;
+}
+
+/**
+ * Verify checksum of a Bech32/Bech32m string
+ * @param {string} hrp - Human-readable part (lowercase)
+ * @param {number[]} data - Data including checksum (5-bit values)
+ * @param {string} encoding - "Bech32" or "Bech32m"
+ * @returns {boolean} - True if checksum is valid
+ */
+function verifyChecksum(hrp, data, encoding) {
+ const constant = encoding === "Bech32m" ? BECH32M_CONST : BECH32_CONST;
+ return polymod(hrpExpand(hrp).concat(data)) === constant;
+}
+
+/**
+ * Create checksum for Bech32/Bech32m encoding
+ * @param {string} hrp - Human-readable part (lowercase)
+ * @param {number[]} data - Data values (5-bit)
+ * @param {string} encoding - "Bech32" or "Bech32m"
+ * @returns {number[]} - 6 checksum values
+ */
+function createChecksum(hrp, data, encoding) {
+ const constant = encoding === "Bech32m" ? BECH32M_CONST : BECH32_CONST;
+ const values = hrpExpand(hrp).concat(data).concat([0, 0, 0, 0, 0, 0]);
+ const mod = polymod(values) ^ constant;
+ const result = [];
+ for (let i = 0; i < 6; i++) {
+ result.push((mod >> (5 * (5 - i))) & 31);
+ }
+ return result;
+}
+
+/**
+ * Convert 8-bit bytes to 5-bit words
+ * @param {number[]|Uint8Array} data - Input bytes
+ * @returns {number[]} - 5-bit words
+ */
+export function toWords(data) {
+ let value = 0;
+ let bits = 0;
+ const result = [];
+
+ for (let i = 0; i < data.length; i++) {
+ value = (value << 8) | data[i];
+ bits += 8;
+
+ while (bits >= 5) {
+ bits -= 5;
+ result.push((value >> bits) & 31);
+ }
+ }
+
+ // Pad remaining bits
+ if (bits > 0) {
+ result.push((value << (5 - bits)) & 31);
+ }
+
+ return result;
+}
+
+/**
+ * Convert 5-bit words to 8-bit bytes
+ * @param {number[]} words - 5-bit words
+ * @returns {number[]} - Output bytes
+ */
+export function fromWords(words) {
+ let value = 0;
+ let bits = 0;
+ const result = [];
+
+ for (let i = 0; i < words.length; i++) {
+ value = (value << 5) | words[i];
+ bits += 5;
+
+ while (bits >= 8) {
+ bits -= 8;
+ result.push((value >> bits) & 255);
+ }
+ }
+
+ // Check for invalid padding per BIP-0173
+ // Condition 1: Cannot have 5+ bits remaining (would indicate incomplete byte)
+ if (bits >= 5) {
+ throw new OperationError("Invalid padding: too many bits remaining");
+ }
+ // Condition 2: Remaining padding bits must all be zero
+ if (bits > 0) {
+ const paddingValue = (value << (8 - bits)) & 255;
+ if (paddingValue !== 0) {
+ throw new OperationError("Invalid padding: non-zero bits in padding");
+ }
+ }
+
+ return result;
+}
+
+/**
+ * Encode data to Bech32/Bech32m string
+ *
+ * @param {string} hrp - Human-readable part
+ * @param {number[]|Uint8Array} data - Data bytes to encode
+ * @param {string} encoding - "Bech32" or "Bech32m"
+ * @param {boolean} segwit - If true, treat first byte as witness version (for Bitcoin SegWit)
+ * @returns {string} - Encoded Bech32/Bech32m string
+ */
+export function encode(hrp, data, encoding = "Bech32", segwit = false) {
+ // Validate HRP
+ if (!hrp || hrp.length === 0) {
+ throw new OperationError("Human-Readable Part (HRP) cannot be empty.");
+ }
+
+ // Check HRP characters (ASCII 33-126)
+ for (let i = 0; i < hrp.length; i++) {
+ const c = hrp.charCodeAt(i);
+ if (c < 33 || c > 126) {
+ throw new OperationError(`HRP contains invalid character at position ${i}. Only printable ASCII characters (33-126) are allowed.`);
+ }
+ }
+
+ // Convert HRP to lowercase
+ const hrpLower = hrp.toLowerCase();
+
+ let words;
+ if (segwit && data.length >= 2) {
+ // SegWit encoding: first byte is witness version (0-16), rest is witness program
+ const witnessVersion = data[0];
+ if (witnessVersion > 16) {
+ throw new OperationError(`Invalid witness version: ${witnessVersion}. Must be 0-16.`);
+ }
+ const witnessProgram = Array.prototype.slice.call(data, 1);
+
+ // Validate witness program length per BIP-0141
+ if (witnessProgram.length < 2 || witnessProgram.length > 40) {
+ throw new OperationError(`Invalid witness program length: ${witnessProgram.length}. Must be 2-40 bytes.`);
+ }
+ if (witnessVersion === 0 && witnessProgram.length !== 20 && witnessProgram.length !== 32) {
+ throw new OperationError(`Invalid witness program length for v0: ${witnessProgram.length}. Must be 20 or 32 bytes.`);
+ }
+
+ // Witness version is kept as single 5-bit value, program is converted
+ words = [witnessVersion].concat(toWords(witnessProgram));
+ } else {
+ // Generic encoding: convert all bytes to 5-bit words
+ words = toWords(data);
+ }
+
+ // Create checksum
+ const checksum = createChecksum(hrpLower, words, encoding);
+
+ // Build result string
+ let result = hrpLower + "1";
+ for (const w of words.concat(checksum)) {
+ result += CHARSET[w];
+ }
+
+ // Check maximum length (90 characters)
+ if (result.length > 90) {
+ throw new OperationError(`Encoded string exceeds maximum length of 90 characters (got ${result.length}). Consider using smaller input data.`);
+ }
+
+ return result;
+}
+
+/**
+ * Decode a Bech32/Bech32m string
+ *
+ * @param {string} str - Bech32/Bech32m encoded string
+ * @param {string} encoding - "Bech32", "Bech32m", or "Auto-detect"
+ * @returns {{hrp: string, data: number[]}} - Decoded HRP and data bytes
+ */
+export function decode(str, encoding = "Auto-detect") {
+ // Check for empty input
+ if (!str || str.length === 0) {
+ throw new OperationError("Input cannot be empty.");
+ }
+
+ // Check maximum length
+ if (str.length > 90) {
+ throw new OperationError(`Invalid Bech32 string: exceeds maximum length of 90 characters (got ${str.length}).`);
+ }
+
+ // Check for mixed case
+ const hasUpper = /[A-Z]/.test(str);
+ const hasLower = /[a-z]/.test(str);
+ if (hasUpper && hasLower) {
+ throw new OperationError("Invalid Bech32 string: mixed case is not allowed. Use all uppercase or all lowercase.");
+ }
+
+ // Convert to lowercase for processing
+ str = str.toLowerCase();
+
+ // Find separator (last occurrence of '1')
+ const sepIndex = str.lastIndexOf("1");
+ if (sepIndex === -1) {
+ throw new OperationError("Invalid Bech32 string: no separator '1' found.");
+ }
+
+ if (sepIndex === 0) {
+ throw new OperationError("Invalid Bech32 string: Human-Readable Part (HRP) cannot be empty.");
+ }
+
+ if (sepIndex + 7 > str.length) {
+ throw new OperationError("Invalid Bech32 string: data part is too short (minimum 6 characters for checksum).");
+ }
+
+ // Extract HRP and data part
+ const hrp = str.substring(0, sepIndex);
+ const dataPart = str.substring(sepIndex + 1);
+
+ // Validate HRP characters
+ for (let i = 0; i < hrp.length; i++) {
+ const c = hrp.charCodeAt(i);
+ if (c < 33 || c > 126) {
+ throw new OperationError(`HRP contains invalid character at position ${i}.`);
+ }
+ }
+
+ // Decode data characters to 5-bit values
+ const data = [];
+ for (let i = 0; i < dataPart.length; i++) {
+ const c = dataPart[i];
+ if (CHARSET_REV[c] === undefined) {
+ throw new OperationError(`Invalid character '${c}' at position ${sepIndex + 1 + i}.`);
+ }
+ data.push(CHARSET_REV[c]);
+ }
+
+ // Verify checksum
+ let usedEncoding;
+ if (encoding === "Bech32") {
+ if (!verifyChecksum(hrp, data, "Bech32")) {
+ throw new OperationError("Invalid Bech32 checksum.");
+ }
+ usedEncoding = "Bech32";
+ } else if (encoding === "Bech32m") {
+ if (!verifyChecksum(hrp, data, "Bech32m")) {
+ throw new OperationError("Invalid Bech32m checksum.");
+ }
+ usedEncoding = "Bech32m";
+ } else {
+ // Auto-detect: try Bech32 first, then Bech32m
+ if (verifyChecksum(hrp, data, "Bech32")) {
+ usedEncoding = "Bech32";
+ } else if (verifyChecksum(hrp, data, "Bech32m")) {
+ usedEncoding = "Bech32m";
+ } else {
+ throw new OperationError("Invalid Bech32/Bech32m string: checksum verification failed.");
+ }
+ }
+
+ // Remove checksum (last 6 values)
+ const words = data.slice(0, data.length - 6);
+
+ // Check if this is likely a SegWit address (Bitcoin, Litecoin, etc.)
+ // For SegWit, the first 5-bit word is the witness version (0-16)
+ // and should be extracted separately, not bit-converted with the rest
+ const segwitHrps = ["bc", "tb", "ltc", "tltc", "bcrt"];
+ const couldBeSegWit = segwitHrps.includes(hrp) && words.length > 0 && words[0] <= 16;
+
+ let bytes;
+ let witnessVersion = null;
+
+ if (couldBeSegWit) {
+ // Try SegWit decode first
+ try {
+ witnessVersion = words[0];
+ const programWords = words.slice(1);
+ const programBytes = fromWords(programWords);
+
+ // Validate SegWit witness program length (20 or 32 bytes for v0, 2-40 for others)
+ const validV0 = witnessVersion === 0 && (programBytes.length === 20 || programBytes.length === 32);
+ const validOther = witnessVersion !== 0 && programBytes.length >= 2 && programBytes.length <= 40;
+
+ if (validV0 || validOther) {
+ // Valid SegWit address
+ bytes = [witnessVersion, ...programBytes];
+ } else {
+ // Not valid SegWit, fall back to generic decode
+ witnessVersion = null;
+ bytes = fromWords(words);
+ }
+ } catch (e) {
+ // SegWit decode failed, try generic decode
+ witnessVersion = null;
+ try {
+ bytes = fromWords(words);
+ } catch (e2) {
+ throw new OperationError(`Failed to decode data: ${e2.message}`);
+ }
+ }
+ } else {
+ // Generic Bech32: convert all words
+ try {
+ bytes = fromWords(words);
+ } catch (e) {
+ throw new OperationError(`Failed to decode data: ${e.message}`);
+ }
+ }
+
+ return {
+ hrp: hrp,
+ data: bytes,
+ encoding: usedEncoding,
+ witnessVersion: witnessVersion
+ };
+}
diff --git a/src/core/lib/JA4.mjs b/src/core/lib/JA4.mjs
index f600f4d8..58422bca 100644
--- a/src/core/lib/JA4.mjs
+++ b/src/core/lib/JA4.mjs
@@ -91,9 +91,7 @@ export function toJA4(bytes) {
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";
+ alpn = alpnFingerprint(parseFirstALPNValue(ext.value.data));
break;
}
}
@@ -212,9 +210,7 @@ export function toJA4S(bytes) {
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";
+ alpn = alpnFingerprint(parseFirstALPNValue(ext.value.data));
break;
}
}
@@ -262,3 +258,33 @@ function tlsVersionMapper(version) {
default: return "00"; // Unknown
}
}
+
+/**
+ * Checks if a byte is ASCII alphanumeric (0-9, A-Z, a-z).
+ * @param {number} byte
+ * @returns {boolean}
+ */
+function isAlphanumeric(byte) {
+ return (byte >= 0x30 && byte <= 0x39) ||
+ (byte >= 0x41 && byte <= 0x5A) ||
+ (byte >= 0x61 && byte <= 0x7A);
+}
+
+/**
+ * Computes the 2-character ALPN fingerprint from raw ALPN bytes.
+ * If both first and last bytes are ASCII alphanumeric, returns their characters.
+ * Otherwise, returns first hex digit of first byte + last hex digit of last byte.
+ * @param {Uint8Array|null} rawBytes
+ * @returns {string}
+ */
+function alpnFingerprint(rawBytes) {
+ if (!rawBytes || rawBytes.length === 0) return "00";
+ const firstByte = rawBytes[0];
+ const lastByte = rawBytes[rawBytes.length - 1];
+ if (isAlphanumeric(firstByte) && isAlphanumeric(lastByte)) {
+ return String.fromCharCode(firstByte) + String.fromCharCode(lastByte);
+ }
+ const firstHex = firstByte.toString(16).padStart(2, "0");
+ const lastHex = lastByte.toString(16).padStart(2, "0");
+ return firstHex[0] + lastHex[1];
+}
diff --git a/src/core/lib/TLS.mjs b/src/core/lib/TLS.mjs
index 6373bfa2..eaf661a8 100644
--- a/src/core/lib/TLS.mjs
+++ b/src/core/lib/TLS.mjs
@@ -863,15 +863,15 @@ export function parseHighestSupportedVersion(bytes) {
}
/**
- * Parses the application_layer_protocol_negotiation extension and returns the first value.
+ * Parses the application_layer_protocol_negotiation extension and returns the first value as raw bytes.
* @param {Uint8Array} bytes
- * @returns {number}
+ * @returns {Uint8Array|null}
*/
export function parseFirstALPNValue(bytes) {
const s = new Stream(bytes);
const alpnExtLen = s.readInt(2);
- if (alpnExtLen < 3) return "00";
+ if (alpnExtLen < 2) return null;
const strLen = s.readInt(1);
- if (strLen < 2) return "00";
- return s.readString(strLen);
+ if (strLen < 1) return null;
+ return s.getBytes(strLen);
}
diff --git a/src/core/operations/FromBech32.mjs b/src/core/operations/FromBech32.mjs
new file mode 100644
index 00000000..8a01d4db
--- /dev/null
+++ b/src/core/operations/FromBech32.mjs
@@ -0,0 +1,149 @@
+/**
+ * @author Medjedtxm
+ * @copyright Crown Copyright 2025
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+import { decode } from "../lib/Bech32.mjs";
+import { toHex } from "../lib/Hex.mjs";
+
+/**
+ * From Bech32 operation
+ */
+class FromBech32 extends Operation {
+
+ /**
+ * FromBech32 constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "From Bech32";
+ this.module = "Default";
+ this.description = "Bech32 is an encoding scheme primarily used for Bitcoin SegWit addresses (BIP-0173). It uses a 32-character alphabet that excludes easily confused characters (1, b, i, o) and includes a checksum for error detection.
Bech32m (BIP-0350) is an updated version used for Bitcoin Taproot addresses.
Auto-detect will attempt Bech32 first, then Bech32m if the checksum fails.
Output format options allow you to see the Human-Readable Part (HRP) along with the decoded data.";
+ this.infoURL = "https://wikipedia.org/wiki/Bech32";
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [
+ {
+ "name": "Encoding",
+ "type": "option",
+ "value": ["Auto-detect", "Bech32", "Bech32m"]
+ },
+ {
+ "name": "Output Format",
+ "type": "option",
+ "value": ["Raw", "Hex", "Bitcoin scriptPubKey", "HRP: Hex", "JSON"]
+ }
+ ];
+ this.checks = [
+ {
+ // Bitcoin mainnet SegWit/Taproot addresses
+ pattern: "^bc1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{6,87}$",
+ flags: "i",
+ args: ["Auto-detect", "Hex"]
+ },
+ {
+ // Bitcoin testnet addresses
+ pattern: "^tb1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{6,87}$",
+ flags: "i",
+ args: ["Auto-detect", "Hex"]
+ },
+ {
+ // AGE public keys
+ pattern: "^age1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{6,87}$",
+ flags: "i",
+ args: ["Auto-detect", "HRP: Hex"]
+ },
+ {
+ // AGE secret keys
+ pattern: "^AGE-SECRET-KEY-1[QPZRY9X8GF2TVDW0S3JN54KHCE6MUA7L]{6,87}$",
+ flags: "",
+ args: ["Auto-detect", "HRP: Hex"]
+ },
+ {
+ // Litecoin mainnet addresses
+ pattern: "^ltc1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{6,87}$",
+ flags: "i",
+ args: ["Auto-detect", "Hex"]
+ },
+ {
+ // Generic bech32 pattern
+ pattern: "^[a-z]{1,83}1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{6,}$",
+ flags: "i",
+ args: ["Auto-detect", "Hex"]
+ }
+ ];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ const encoding = args[0];
+ const outputFormat = args[1];
+
+ input = input.trim();
+
+ if (input.length === 0) {
+ return "";
+ }
+
+ const decoded = decode(input, encoding);
+
+ // Format output based on selected option
+ switch (outputFormat) {
+ case "Raw":
+ return decoded.data.map(b => String.fromCharCode(b)).join("");
+
+ case "Hex":
+ return toHex(decoded.data, "");
+
+ case "Bitcoin scriptPubKey": {
+ // Convert to Bitcoin scriptPubKey format as shown in BIP-0173/BIP-0350
+ // Format: [OP_version][length][witness_program]
+ // OP_0 = 0x00, OP_1-OP_16 = 0x51-0x60
+ if (decoded.witnessVersion === null || decoded.data.length < 2) {
+ // Not a SegWit address, fall back to hex
+ return toHex(decoded.data, "");
+ }
+ const witnessVersion = decoded.data[0];
+ const witnessProgram = decoded.data.slice(1);
+
+ // Convert witness version to OP code
+ let opCode;
+ if (witnessVersion === 0) {
+ opCode = 0x00; // OP_0
+ } else if (witnessVersion >= 1 && witnessVersion <= 16) {
+ opCode = 0x50 + witnessVersion; // OP_1 = 0x51, ..., OP_16 = 0x60
+ } else {
+ // Invalid witness version, fall back to hex
+ return toHex(decoded.data, "");
+ }
+
+ // Build scriptPubKey: [OP_version][length][program]
+ const scriptPubKey = [opCode, witnessProgram.length, ...witnessProgram];
+ return toHex(scriptPubKey, "");
+ }
+
+ case "HRP: Hex":
+ return `${decoded.hrp}: ${toHex(decoded.data, "")}`;
+
+ case "JSON":
+ return JSON.stringify({
+ hrp: decoded.hrp,
+ encoding: decoded.encoding,
+ data: toHex(decoded.data, "")
+ }, null, 2);
+
+ default:
+ return toHex(decoded.data, "");
+ }
+ }
+
+}
+
+export default FromBech32;
diff --git a/src/core/operations/FromHexdump.mjs b/src/core/operations/FromHexdump.mjs
index e8c25441..6fd3c1dc 100644
--- a/src/core/operations/FromHexdump.mjs
+++ b/src/core/operations/FromHexdump.mjs
@@ -43,7 +43,7 @@ class FromHexdump extends Operation {
*/
run(input, args) {
const output = [],
- regex = /^\s*(?:[\dA-F]{4,16}h?:?)?[ \t]+((?:[\dA-F]{2} ){1,8}(?:[ \t]|[\dA-F]{2}-)(?:[\dA-F]{2} ){1,8}|(?:[\dA-F]{4} )*[\dA-F]{4}|(?:[\dA-F]{2} )*[\dA-F]{2})/igm;
+ regex = /^\s*(?:[\dA-F]{4,16}h?:?)?[ \t]+((?:[\dA-F]{2} ){1,8}(?:[ \t]|[\dA-F]{2}-)(?:[\dA-F]{2} ){1,8}|(?:[\dA-F]{4} )+(?:[\dA-F]{2})?|(?:[\dA-F]{2} )*[\dA-F]{2})/igm;
let block, line;
while ((block = regex.exec(input))) {
diff --git a/src/core/operations/ToBech32.mjs b/src/core/operations/ToBech32.mjs
new file mode 100644
index 00000000..a7c97355
--- /dev/null
+++ b/src/core/operations/ToBech32.mjs
@@ -0,0 +1,92 @@
+/**
+ * @author Medjedtxm
+ * @copyright Crown Copyright 2025
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+import { encode } from "../lib/Bech32.mjs";
+import { fromHex } from "../lib/Hex.mjs";
+
+/**
+ * To Bech32 operation
+ */
+class ToBech32 extends Operation {
+
+ /**
+ * ToBech32 constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "To Bech32";
+ this.module = "Default";
+ this.description = "Bech32 is an encoding scheme primarily used for Bitcoin SegWit addresses (BIP-0173). It uses a 32-character alphabet that excludes easily confused characters (1, b, i, o) and includes a checksum for error detection.
Bech32m (BIP-0350) is an updated version that fixes a weakness in the original Bech32 checksum and is used for Bitcoin Taproot addresses.
The Human-Readable Part (HRP) identifies the network or purpose (e.g., 'bc' for Bitcoin mainnet, 'tb' for testnet, 'age' for AGE encryption keys).
Maximum output length is 90 characters as per specification.";
+ this.infoURL = "https://wikipedia.org/wiki/Bech32";
+ this.inputType = "ArrayBuffer";
+ this.outputType = "string";
+ this.args = [
+ {
+ "name": "Human-Readable Part (HRP)",
+ "type": "string",
+ "value": "bc"
+ },
+ {
+ "name": "Encoding",
+ "type": "option",
+ "value": ["Bech32", "Bech32m"]
+ },
+ {
+ "name": "Input Format",
+ "type": "option",
+ "value": ["Raw bytes", "Hex"]
+ },
+ {
+ "name": "Mode",
+ "type": "option",
+ "value": ["Generic", "Bitcoin SegWit"]
+ },
+ {
+ "name": "Witness Version",
+ "type": "number",
+ "value": 0,
+ "hint": "SegWit witness version (0-16). Only used in Bitcoin SegWit mode."
+ }
+ ];
+ }
+
+ /**
+ * @param {ArrayBuffer} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ const hrp = args[0];
+ const encoding = args[1];
+ const inputFormat = args[2];
+ const mode = args[3];
+ const witnessVersion = args[4];
+
+ let inputArray;
+ if (inputFormat === "Hex") {
+ // Convert hex string to bytes
+ const hexStr = new TextDecoder().decode(new Uint8Array(input)).replace(/\s/g, "");
+ inputArray = fromHex(hexStr);
+ } else {
+ inputArray = new Uint8Array(input);
+ }
+
+ if (mode === "Bitcoin SegWit") {
+ // Prepend witness version to the input data
+ const withVersion = new Uint8Array(inputArray.length + 1);
+ withVersion[0] = witnessVersion;
+ withVersion.set(inputArray, 1);
+ return encode(hrp, withVersion, encoding, true);
+ }
+
+ return encode(hrp, inputArray, encoding, false);
+ }
+
+}
+
+export default ToBech32;
diff --git a/src/node/apiUtils.mjs b/src/node/apiUtils.mjs
index 64688073..9d1c43cc 100644
--- a/src/node/apiUtils.mjs
+++ b/src/node/apiUtils.mjs
@@ -66,7 +66,7 @@ export function removeSubheadingsFromArray(array) {
* @param str
*/
export function sanitise(str) {
- return str.replace(/ /g, "").toLowerCase();
+ return str.replace(/[/\s.-]/g, "").toLowerCase();
}
diff --git a/src/web/App.mjs b/src/web/App.mjs
index 7071854a..143545d6 100644
--- a/src/web/App.mjs
+++ b/src/web/App.mjs
@@ -650,7 +650,7 @@ class App {
// const compareURL = `https://github.com/gchq/CyberChef/compare/v${prev.join(".")}...v${PKG_VERSION}`;
- let compileInfo = `Last build: ${timeSinceCompile.substr(0, 1).toUpperCase() + timeSinceCompile.substr(1)} ago`;
+ let compileInfo = `Last build: ${timeSinceCompile.substring(0, 1).toUpperCase() + timeSinceCompile.substring(1)} ago`;
if (window.compileMessage !== "") {
compileInfo += " - " + window.compileMessage;
diff --git a/src/web/static/sitemap.mjs b/src/web/static/sitemap.mjs
index b96047fc..4f8101d4 100644
--- a/src/web/static/sitemap.mjs
+++ b/src/web/static/sitemap.mjs
@@ -1,6 +1,5 @@
import sm from "sitemap";
-import OperationConfig from "../../core/config/OperationConfig.json" assert {type: "json"};
-
+import OperationConfig from "../../core/config/OperationConfig.json" assert { type: "json" };
/**
* Generates an XML sitemap for all CyberChef operations and a number of recipes.
@@ -10,25 +9,25 @@ import OperationConfig from "../../core/config/OperationConfig.json" assert {typ
* @license Apache-2.0
*/
-const smStream = new sm.SitemapStream({
- hostname: "https://gchq.github.io/CyberChef",
-});
+const baseUrl = "https://gchq.github.io/CyberChef/";
+
+const smStream = new sm.SitemapStream({});
smStream.write({
- url: "/",
+ url: baseUrl,
changefreq: "weekly",
- priority: 1.0
+ priority: 1.0,
});
for (const op in OperationConfig) {
smStream.write({
- url: `/?op=${encodeURIComponent(op)}`,
+ url: `${baseUrl}?op=${encodeURIComponent(op)}`,
changeFreq: "yearly",
- priority: 0.5
+ priority: 0.5,
});
}
smStream.end();
sm.streamToPromise(smStream).then(
- buffer => console.log(buffer.toString()) // eslint-disable-line no-console
+ (buffer) => console.log(buffer.toString()), // eslint-disable-line no-console
);
diff --git a/tests/node/tests/nodeApi.mjs b/tests/node/tests/nodeApi.mjs
index 29a47ffc..92d4d991 100644
--- a/tests/node/tests/nodeApi.mjs
+++ b/tests/node/tests/nodeApi.mjs
@@ -345,6 +345,42 @@ TestRegister.addApiTests([
assert.strictEqual(result.toString(), "begin_something_aaaaaaaaaaaaaa_end_something");
}),
+ it("chef.bake: should accept operation names from Chef Website which contain forward slash", () => {
+ const result = chef.bake("I'll have the test salmon", [
+ { "op": "Find / Replace",
+ "args": [{ "option": "Regex", "string": "test" }, "good", true, false, true, false]}
+ ]);
+ assert.strictEqual(result.toString(), "I'll have the good salmon");
+ }),
+
+ it("chef.bake: should accept operation names from Chef Website which contain a hyphen", () => {
+ const result = chef.bake("I'll have the test salmon", [
+ { "op": "Adler-32 Checksum",
+ "args": [] }
+ ]);
+ assert.strictEqual(result.toString(), "6e4208f8");
+ }),
+
+ it("chef.bake: should accept operation names from Chef Website which contain a period", () => {
+ const result = chef.bake("30 13 02 01 05 16 0e 41 6e 79 62 6f 64 79 20 74 68 65 72 65 3f", [
+ { "op": "Parse ASN.1 hex string",
+ "args": [0, 32] }
+ ]);
+ assert.strictEqual(result.toString(), `SEQUENCE
+ INTEGER 05
+ IA5String 'Anybody there?'
+`);
+ }),
+
+ it("Excluded operations: throw a sensible error when you try and call one", () => {
+ try {
+ chef.fork();
+ } catch (e) {
+ assert.strictEqual(e.type, "ExcludedOperationError");
+ assert.strictEqual(e.message, "Sorry, the Fork operation is not available in the Node.js version of CyberChef.");
+ }
+ }),
+
it("chef.bake: cannot accept flowControl operations in recipe", () => {
assert.throws(() => chef.bake("some input", "magic"), {
name: "TypeError",
diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs
index 7c42cd11..e4539566 100644
--- a/tests/operations/index.mjs
+++ b/tests/operations/index.mjs
@@ -26,6 +26,7 @@ import "./tests/Base64.mjs";
import "./tests/Base85.mjs";
import "./tests/Base92.mjs";
import "./tests/BCD.mjs";
+import "./tests/Bech32.mjs";
import "./tests/BitwiseOp.mjs";
import "./tests/BLAKE2b.mjs";
import "./tests/BLAKE2s.mjs";
diff --git a/tests/operations/tests/Bech32.mjs b/tests/operations/tests/Bech32.mjs
new file mode 100644
index 00000000..85325d47
--- /dev/null
+++ b/tests/operations/tests/Bech32.mjs
@@ -0,0 +1,702 @@
+/**
+ * Bech32 tests.
+ *
+ * Test vectors from official BIP specifications:
+ * BIP-0173: https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki
+ * BIP-0350: https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki
+ *
+ * AGE key test vectors from:
+ * https://asecuritysite.com/age/go_age5
+ *
+ * @author Medjedtxm
+ * @copyright Crown Copyright 2025
+ * @license Apache-2.0
+ */
+
+import TestRegister from "../../lib/TestRegister.mjs";
+
+TestRegister.addTests([
+ // ============= To Bech32 Tests =============
+ {
+ name: "To Bech32: empty input",
+ input: "",
+ expectedOutput: "bc1gmk9yu",
+ recipeConfig: [
+ {
+ "op": "To Bech32",
+ "args": ["bc", "Bech32", "Raw bytes", "Generic", 0]
+ }
+ ],
+ },
+ {
+ name: "To Bech32: single byte",
+ input: "A",
+ expectedOutput: "bc1gyufle22",
+ recipeConfig: [
+ {
+ "op": "To Bech32",
+ "args": ["bc", "Bech32", "Raw bytes", "Generic", 0]
+ }
+ ],
+ },
+ {
+ name: "To Bech32: Hello",
+ input: "Hello",
+ expectedOutput: "bc1fpjkcmr0gzsgcg",
+ recipeConfig: [
+ {
+ "op": "To Bech32",
+ "args": ["bc", "Bech32", "Raw bytes", "Generic", 0]
+ }
+ ],
+ },
+ {
+ name: "To Bech32: custom HRP",
+ input: "test",
+ expectedOutput: "custom1w3jhxaq593qur",
+ recipeConfig: [
+ {
+ "op": "To Bech32",
+ "args": ["custom", "Bech32", "Raw bytes", "Generic", 0]
+ }
+ ],
+ },
+ {
+ name: "To Bech32: testnet HRP",
+ input: "data",
+ expectedOutput: "tb1v3shgcg3x07jr",
+ recipeConfig: [
+ {
+ "op": "To Bech32",
+ "args": ["tb", "Bech32", "Raw bytes", "Generic", 0]
+ }
+ ],
+ },
+ {
+ name: "To Bech32m: empty input",
+ input: "",
+ expectedOutput: "bc1a8xfp7",
+ recipeConfig: [
+ {
+ "op": "To Bech32",
+ "args": ["bc", "Bech32m", "Raw bytes", "Generic", 0]
+ }
+ ],
+ },
+ {
+ name: "To Bech32m: single byte",
+ input: "A",
+ expectedOutput: "bc1gyf4040g",
+ recipeConfig: [
+ {
+ "op": "To Bech32",
+ "args": ["bc", "Bech32m", "Raw bytes", "Generic", 0]
+ }
+ ],
+ },
+ {
+ name: "To Bech32m: Hello",
+ input: "Hello",
+ expectedOutput: "bc1fpjkcmr0a7qya2",
+ recipeConfig: [
+ {
+ "op": "To Bech32",
+ "args": ["bc", "Bech32m", "Raw bytes", "Generic", 0]
+ }
+ ],
+ },
+ {
+ name: "To Bech32: empty HRP error",
+ input: "test",
+ expectedOutput: "Human-Readable Part (HRP) cannot be empty.",
+ recipeConfig: [
+ {
+ "op": "To Bech32",
+ "args": ["", "Bech32", "Raw bytes", "Generic", 0]
+ }
+ ],
+ },
+
+ // ============= From Bech32 Tests (Raw output) =============
+ {
+ name: "From Bech32: decode single byte (Raw)",
+ input: "bc1gyufle22",
+ expectedOutput: "A",
+ recipeConfig: [
+ {
+ "op": "From Bech32",
+ "args": ["Bech32", "Raw"]
+ }
+ ],
+ },
+ {
+ name: "From Bech32: decode Hello (Raw)",
+ input: "bc1fpjkcmr0gzsgcg",
+ expectedOutput: "Hello",
+ recipeConfig: [
+ {
+ "op": "From Bech32",
+ "args": ["Bech32", "Raw"]
+ }
+ ],
+ },
+ {
+ name: "From Bech32: auto-detect Bech32 (Raw)",
+ input: "bc1fpjkcmr0gzsgcg",
+ expectedOutput: "Hello",
+ recipeConfig: [
+ {
+ "op": "From Bech32",
+ "args": ["Auto-detect", "Raw"]
+ }
+ ],
+ },
+ {
+ name: "From Bech32m: decode Hello (Raw)",
+ input: "bc1fpjkcmr0a7qya2",
+ expectedOutput: "Hello",
+ recipeConfig: [
+ {
+ "op": "From Bech32",
+ "args": ["Bech32m", "Raw"]
+ }
+ ],
+ },
+ {
+ name: "From Bech32: auto-detect Bech32m (Raw)",
+ input: "bc1fpjkcmr0a7qya2",
+ expectedOutput: "Hello",
+ recipeConfig: [
+ {
+ "op": "From Bech32",
+ "args": ["Auto-detect", "Raw"]
+ }
+ ],
+ },
+ {
+ name: "From Bech32: uppercase input (Raw)",
+ input: "BC1FPJKCMR0GZSGCG",
+ expectedOutput: "Hello",
+ recipeConfig: [
+ {
+ "op": "From Bech32",
+ "args": ["Auto-detect", "Raw"]
+ }
+ ],
+ },
+ {
+ name: "From Bech32: custom HRP (Raw)",
+ input: "custom1w3jhxaq593qur",
+ expectedOutput: "test",
+ recipeConfig: [
+ {
+ "op": "From Bech32",
+ "args": ["Bech32", "Raw"]
+ }
+ ],
+ },
+ {
+ name: "From Bech32: empty input",
+ input: "",
+ expectedOutput: "",
+ recipeConfig: [
+ {
+ "op": "From Bech32",
+ "args": ["Auto-detect", "Hex"]
+ }
+ ],
+ },
+ {
+ name: "From Bech32: empty data part (Hex)",
+ input: "bc1gmk9yu",
+ expectedOutput: "",
+ recipeConfig: [
+ {
+ "op": "From Bech32",
+ "args": ["Bech32", "Hex"]
+ }
+ ],
+ },
+
+ // ============= From Bech32 HRP Output Tests =============
+ {
+ name: "From Bech32: HRP: Hex output format",
+ input: "bc1fpjkcmr0gzsgcg",
+ expectedOutput: "bc: 48656c6c6f",
+ recipeConfig: [
+ {
+ "op": "From Bech32",
+ "args": ["Bech32", "HRP: Hex"]
+ }
+ ],
+ },
+ {
+ name: "From Bech32: JSON output format",
+ input: "bc1fpjkcmr0gzsgcg",
+ expectedOutput: "{\n \"hrp\": \"bc\",\n \"encoding\": \"Bech32\",\n \"data\": \"48656c6c6f\"\n}",
+ recipeConfig: [
+ {
+ "op": "From Bech32",
+ "args": ["Bech32", "JSON"]
+ }
+ ],
+ },
+ {
+ name: "From Bech32: Hex output format",
+ input: "bc1fpjkcmr0gzsgcg",
+ expectedOutput: "48656c6c6f",
+ recipeConfig: [
+ {
+ "op": "From Bech32",
+ "args": ["Bech32", "Hex"]
+ }
+ ],
+ },
+
+ // ============= AGE Key Test Vectors =============
+ // From: https://asecuritysite.com/age/go_age5
+ {
+ name: "From Bech32: AGE public key 1 (HRP: Hex)",
+ input: "age1kk86t4lr4s9uwvnqjzp2e35rflvcpnjt33q99547ct23xzk0ssss3ma49j",
+ expectedOutput: "age: b58fa5d7e3ac0bc732609082acc6834fd980ce4b8c4052d2bec2d5130acf8421",
+ recipeConfig: [
+ {
+ "op": "From Bech32",
+ "args": ["Auto-detect", "HRP: Hex"]
+ }
+ ],
+ },
+ {
+ name: "From Bech32: AGE private key 1 (HRP: Hex)",
+ input: "AGE-SECRET-KEY-1Z5N23X54Y4E9NLMPNH6EZDQQX9V883TMKJ3ZJF5QXXMKNZ2RPFXQUQF74G",
+ expectedOutput: "age-secret-key-: 1526a89a95257259ff619df5913400315873c57bb4a229268031b76989430a4c",
+ recipeConfig: [
+ {
+ "op": "From Bech32",
+ "args": ["Auto-detect", "HRP: Hex"]
+ }
+ ],
+ },
+ {
+ name: "From Bech32: AGE public key 2 (HRP: Hex)",
+ input: "age1nwt7gkq7udvalagqn7l8a4jgju7wtenkg925pvuqvn7cfcry6u2qkae4ad",
+ expectedOutput: "age: 9b97e4581ee359dff5009fbe7ed648973ce5e676415540b38064fd84e064d714",
+ recipeConfig: [
+ {
+ "op": "From Bech32",
+ "args": ["Auto-detect", "HRP: Hex"]
+ }
+ ],
+ },
+ {
+ name: "From Bech32: AGE private key 2 (HRP: Hex)",
+ input: "AGE-SECRET-KEY-137M0YVE3CL6M8C4ET9L2KU67FPQHJZTW547QD5CK0R5A5T09ZGJSQGR9LX",
+ expectedOutput: "age-secret-key-: 8fb6f23331c7f5b3e2b9597eab735e484179096ea57c06d31678e9da2de51225",
+ recipeConfig: [
+ {
+ "op": "From Bech32",
+ "args": ["Auto-detect", "HRP: Hex"]
+ }
+ ],
+ },
+ {
+ name: "From Bech32: AGE public key 1 (JSON)",
+ input: "age1kk86t4lr4s9uwvnqjzp2e35rflvcpnjt33q99547ct23xzk0ssss3ma49j",
+ expectedOutput: "{\n \"hrp\": \"age\",\n \"encoding\": \"Bech32\",\n \"data\": \"b58fa5d7e3ac0bc732609082acc6834fd980ce4b8c4052d2bec2d5130acf8421\"\n}",
+ recipeConfig: [
+ {
+ "op": "From Bech32",
+ "args": ["Auto-detect", "JSON"]
+ }
+ ],
+ },
+
+ // ============= Error Cases =============
+ {
+ name: "From Bech32: mixed case error",
+ input: "bc1FpjKcmr0gzsgcg",
+ expectedOutput: "Invalid Bech32 string: mixed case is not allowed. Use all uppercase or all lowercase.",
+ recipeConfig: [
+ {
+ "op": "From Bech32",
+ "args": ["Auto-detect", "Hex"]
+ }
+ ],
+ },
+ {
+ name: "From Bech32: no separator error",
+ input: "noseparator",
+ expectedOutput: "Invalid Bech32 string: no separator '1' found.",
+ recipeConfig: [
+ {
+ "op": "From Bech32",
+ "args": ["Auto-detect", "Hex"]
+ }
+ ],
+ },
+ {
+ name: "From Bech32: empty HRP error",
+ input: "1qqqqqqqqqqqqqqqq",
+ expectedOutput: "Invalid Bech32 string: Human-Readable Part (HRP) cannot be empty.",
+ recipeConfig: [
+ {
+ "op": "From Bech32",
+ "args": ["Auto-detect", "Hex"]
+ }
+ ],
+ },
+ {
+ name: "From Bech32: invalid checksum",
+ input: "bc1fpjkcmr0gzsgcx",
+ expectedOutput: "Invalid Bech32/Bech32m string: checksum verification failed.",
+ recipeConfig: [
+ {
+ "op": "From Bech32",
+ "args": ["Auto-detect", "Hex"]
+ }
+ ],
+ },
+ {
+ name: "From Bech32: data too short",
+ input: "bc1abc",
+ expectedOutput: "Invalid Bech32 string: data part is too short (minimum 6 characters for checksum).",
+ recipeConfig: [
+ {
+ "op": "From Bech32",
+ "args": ["Auto-detect", "Hex"]
+ }
+ ],
+ },
+ {
+ name: "From Bech32: wrong encoding specified",
+ input: "bc1fpjkcmr0gzsgcg",
+ expectedOutput: "Invalid Bech32m checksum.",
+ recipeConfig: [
+ {
+ "op": "From Bech32",
+ "args": ["Bech32m", "Hex"]
+ }
+ ],
+ },
+
+ // ============= BIP-0173 Test Vectors (Bech32) =============
+ {
+ name: "From Bech32: BIP-0173 A12UEL5L (empty data)",
+ input: "A12UEL5L",
+ expectedOutput: "",
+ recipeConfig: [
+ {
+ "op": "From Bech32",
+ "args": ["Bech32", "Hex"]
+ }
+ ],
+ },
+ {
+ name: "From Bech32: BIP-0173 a12uel5l lowercase",
+ input: "a12uel5l",
+ expectedOutput: "",
+ recipeConfig: [
+ {
+ "op": "From Bech32",
+ "args": ["Bech32", "Hex"]
+ }
+ ],
+ },
+ {
+ name: "From Bech32: BIP-0173 long HRP with bio",
+ input: "an83characterlonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1tt5tgs",
+ expectedOutput: "",
+ recipeConfig: [
+ {
+ "op": "From Bech32",
+ "args": ["Bech32", "Hex"]
+ }
+ ],
+ },
+ {
+ name: "From Bech32: BIP-0173 abcdef with data",
+ input: "abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw",
+ expectedOutput: "abcdef: 00443214c74254b635cf84653a56d7c675be77df",
+ recipeConfig: [
+ {
+ "op": "From Bech32",
+ "args": ["Bech32", "HRP: Hex"]
+ }
+ ],
+ },
+ {
+ name: "From Bech32: BIP-0173 split HRP",
+ input: "split1checkupstagehandshakeupstreamerranterredcaperred2y9e3w",
+ expectedOutput: "split: c5f38b70305f519bf66d85fb6cf03058f3dde463ecd7918f2dc743918f2d",
+ recipeConfig: [
+ {
+ "op": "From Bech32",
+ "args": ["Bech32", "HRP: Hex"]
+ }
+ ],
+ },
+ {
+ name: "From Bech32: BIP-0173 question mark HRP",
+ input: "?1ezyfcl",
+ expectedOutput: "",
+ recipeConfig: [
+ {
+ "op": "From Bech32",
+ "args": ["Bech32", "Hex"]
+ }
+ ],
+ },
+
+ // ============= BIP-0350 Test Vectors (Bech32m) =============
+ {
+ name: "From Bech32m: BIP-0350 A1LQFN3A (empty data)",
+ input: "A1LQFN3A",
+ expectedOutput: "",
+ recipeConfig: [
+ {
+ "op": "From Bech32",
+ "args": ["Bech32m", "Hex"]
+ }
+ ],
+ },
+ {
+ name: "From Bech32m: BIP-0350 a1lqfn3a lowercase",
+ input: "a1lqfn3a",
+ expectedOutput: "",
+ recipeConfig: [
+ {
+ "op": "From Bech32",
+ "args": ["Bech32m", "Hex"]
+ }
+ ],
+ },
+ {
+ name: "From Bech32m: BIP-0350 long HRP",
+ input: "an83characterlonghumanreadablepartthatcontainsthetheexcludedcharactersbioandnumber11sg7hg6",
+ expectedOutput: "",
+ recipeConfig: [
+ {
+ "op": "From Bech32",
+ "args": ["Bech32m", "Hex"]
+ }
+ ],
+ },
+ {
+ name: "From Bech32m: BIP-0350 abcdef with data",
+ input: "abcdef1l7aum6echk45nj3s0wdvt2fg8x9yrzpqzd3ryx",
+ expectedOutput: "abcdef: ffbbcdeb38bdab49ca307b9ac5a928398a418820",
+ recipeConfig: [
+ {
+ "op": "From Bech32",
+ "args": ["Bech32m", "HRP: Hex"]
+ }
+ ],
+ },
+ {
+ name: "From Bech32m: BIP-0350 split HRP",
+ input: "split1checkupstagehandshakeupstreamerranterredcaperredlc445v",
+ expectedOutput: "split: c5f38b70305f519bf66d85fb6cf03058f3dde463ecd7918f2dc743918f2d",
+ recipeConfig: [
+ {
+ "op": "From Bech32",
+ "args": ["Bech32m", "HRP: Hex"]
+ }
+ ],
+ },
+ {
+ name: "From Bech32m: BIP-0350 question mark HRP",
+ input: "?1v759aa",
+ expectedOutput: "",
+ recipeConfig: [
+ {
+ "op": "From Bech32",
+ "args": ["Bech32m", "Hex"]
+ }
+ ],
+ },
+
+ // ============= Bitcoin scriptPubKey Output Format Tests =============
+ // Test vectors from BIP-0173 and BIP-0350
+ {
+ name: "From Bech32: Bitcoin scriptPubKey v0 P2WPKH",
+ input: "BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4",
+ expectedOutput: "0014751e76e8199196d454941c45d1b3a323f1433bd6",
+ recipeConfig: [
+ {
+ "op": "From Bech32",
+ "args": ["Auto-detect", "Bitcoin scriptPubKey"]
+ }
+ ],
+ },
+ {
+ name: "From Bech32: Bitcoin scriptPubKey v0 P2WSH",
+ input: "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7",
+ expectedOutput: "00201863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262",
+ recipeConfig: [
+ {
+ "op": "From Bech32",
+ "args": ["Auto-detect", "Bitcoin scriptPubKey"]
+ }
+ ],
+ },
+ {
+ name: "From Bech32: Bitcoin scriptPubKey v1 Taproot (Bech32m)",
+ input: "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqzk5jj0",
+ expectedOutput: "512079be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
+ recipeConfig: [
+ {
+ "op": "From Bech32",
+ "args": ["Auto-detect", "Bitcoin scriptPubKey"]
+ }
+ ],
+ },
+ {
+ name: "From Bech32: Bitcoin scriptPubKey v16",
+ input: "BC1SW50QGDZ25J",
+ expectedOutput: "6002751e",
+ recipeConfig: [
+ {
+ "op": "From Bech32",
+ "args": ["Auto-detect", "Bitcoin scriptPubKey"]
+ }
+ ],
+ },
+ {
+ name: "From Bech32: Bitcoin scriptPubKey v2",
+ input: "bc1zw508d6qejxtdg4y5r3zarvaryvaxxpcs",
+ expectedOutput: "5210751e76e8199196d454941c45d1b3a323",
+ recipeConfig: [
+ {
+ "op": "From Bech32",
+ "args": ["Auto-detect", "Bitcoin scriptPubKey"]
+ }
+ ],
+ },
+
+ // ============= Bitcoin SegWit Encoding Tests =============
+ {
+ name: "To Bech32: Bitcoin SegWit v0 P2WPKH",
+ input: "751e76e8199196d454941c45d1b3a323f1433bd6",
+ expectedOutput: "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4",
+ recipeConfig: [
+ {
+ "op": "To Bech32",
+ "args": ["bc", "Bech32", "Hex", "Bitcoin SegWit", 0]
+ }
+ ],
+ },
+ {
+ name: "To Bech32: Bitcoin SegWit v0 P2WSH testnet",
+ input: "1863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262",
+ expectedOutput: "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7",
+ recipeConfig: [
+ {
+ "op": "To Bech32",
+ "args": ["tb", "Bech32", "Hex", "Bitcoin SegWit", 0]
+ }
+ ],
+ },
+ {
+ name: "To Bech32m: Bitcoin Taproot v1",
+ input: "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
+ expectedOutput: "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqzk5jj0",
+ recipeConfig: [
+ {
+ "op": "To Bech32",
+ "args": ["bc", "Bech32m", "Hex", "Bitcoin SegWit", 1]
+ }
+ ],
+ },
+ {
+ name: "To Bech32m: Bitcoin SegWit v16",
+ input: "751e",
+ expectedOutput: "bc1sw50qgdz25j",
+ recipeConfig: [
+ {
+ "op": "To Bech32",
+ "args": ["bc", "Bech32m", "Hex", "Bitcoin SegWit", 16]
+ }
+ ],
+ },
+
+ // ============= Round-trip Tests =============
+ {
+ name: "Bech32: encode then decode round-trip",
+ input: "The quick brown fox jumps over the lazy dog",
+ expectedOutput: "The quick brown fox jumps over the lazy dog",
+ recipeConfig: [
+ {
+ "op": "To Bech32",
+ "args": ["test", "Bech32", "Raw bytes", "Generic", 0]
+ },
+ {
+ "op": "From Bech32",
+ "args": ["Bech32", "Raw"]
+ }
+ ],
+ },
+ {
+ name: "Bech32m: encode then decode round-trip",
+ input: "The quick brown fox jumps over the lazy dog",
+ expectedOutput: "The quick brown fox jumps over the lazy dog",
+ recipeConfig: [
+ {
+ "op": "To Bech32",
+ "args": ["test", "Bech32m", "Raw bytes", "Generic", 0]
+ },
+ {
+ "op": "From Bech32",
+ "args": ["Bech32m", "Raw"]
+ }
+ ],
+ },
+ {
+ name: "Bech32: binary data round-trip",
+ input: "0001020304050607",
+ expectedOutput: "0001020304050607",
+ recipeConfig: [
+ {
+ "op": "From Hex",
+ "args": ["Auto"]
+ },
+ {
+ "op": "To Bech32",
+ "args": ["bc", "Bech32", "Raw bytes", "Generic", 0]
+ },
+ {
+ "op": "From Bech32",
+ "args": ["Bech32", "Hex"]
+ }
+ ],
+ },
+ {
+ name: "Bech32: auto-detect round-trip",
+ input: "CyberChef Bech32 Test",
+ expectedOutput: "CyberChef Bech32 Test",
+ recipeConfig: [
+ {
+ "op": "To Bech32",
+ "args": ["cyberchef", "Bech32", "Raw bytes", "Generic", 0]
+ },
+ {
+ "op": "From Bech32",
+ "args": ["Auto-detect", "Raw"]
+ }
+ ],
+ },
+ {
+ name: "Bech32m: auto-detect round-trip",
+ input: "CyberChef Bech32m Test",
+ expectedOutput: "CyberChef Bech32m Test",
+ recipeConfig: [
+ {
+ "op": "To Bech32",
+ "args": ["cyberchef", "Bech32m", "Raw bytes", "Generic", 0]
+ },
+ {
+ "op": "From Bech32",
+ "args": ["Auto-detect", "Raw"]
+ }
+ ],
+ },
+]);
diff --git a/tests/operations/tests/Hexdump.mjs b/tests/operations/tests/Hexdump.mjs
index 90523a08..6eb486db 100644
--- a/tests/operations/tests/Hexdump.mjs
+++ b/tests/operations/tests/Hexdump.mjs
@@ -152,6 +152,17 @@ TestRegister.addTests([
}
],
},
+ {
+ name: "From Hexdump: xxd format, odd number of bytes",
+ input: "00000000: 6162 6364 65 abcde",
+ expectedOutput: "abcde",
+ recipeConfig: [
+ {
+ op: "From Hexdump",
+ args: []
+ }
+ ],
+ },
{
name: "From Hexdump: Wireshark",
input: `00000000 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f ........ ........
diff --git a/tests/operations/tests/JA4.mjs b/tests/operations/tests/JA4.mjs
index 0fb4624e..699dca40 100644
--- a/tests/operations/tests/JA4.mjs
+++ b/tests/operations/tests/JA4.mjs
@@ -30,6 +30,28 @@ TestRegister.addTests([
}
],
},
+ {
+ name: "JA4 Fingerprint: TLS 1.3 with whitespace-only ALPN",
+ input: "1603010200010001fc0303ed338a18e711d670cdc472ff570a5b59f1ace12e5365918bf68bf845019147b6207e4437bfb062d98a4aeb753be8f09022a9dc9413d7694dad4db57fcdcf076e820024130213031301c02cc030c02bc02fcca9cca8c024c028c023c027009f009e006b006700ff0100018f0000001800160000136465762e636f6e74656e74677261622e6e6574000b000403000102000a00160014001d0017001e00190018010001010102010301040023000000100004000201200016000000170000000d002a0028040305030603080708080809080a080b080408050806040105010601030303010302040205020602002b00050403040303002d00020101003300260024001d00207af053336d5e2c1675aa4c6ce78de5e5fdbd296538113f051ea17ccb64289f22001500d2000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ expectedOutput: "t13d181220_85036bcba153_d41ae481755e",
+ recipeConfig: [
+ {
+ "op": "JA4 Fingerprint",
+ "args": ["Hex", "JA4"]
+ }
+ ],
+ },
+ {
+ name: "JA4 Fingerprint: TLS 1.3 with ALPN containing a whitespace",
+ input: "1603010200010001fc0303273682a603be3f64dd025df4ad0f4d2d13043c3a233405a68bb29b865808749a20f4dfc40242b2fce38fae26c516ef9bef20a1b9349eba3c003780168d72471f5c0024130213031301c02cc030c02bc02fcca9cca8c024c028c023c027009f009e006b006700ff0100018f0000001800160000136465762e636f6e74656e74677261622e6e6574000b000403000102000a00160014001d0017001e0019001801000101010201030104002300000010000500030261200016000000170000000d002a0028040305030603080708080809080a080b080408050806040105010601030303010302040205020602002b00050403040303002d00020101003300260024001d0020f4dd1567bd858d3a9f1d88db1fee6a10ab0ea1aa6afe96ffb6a7c4d79dea4075001500d10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ expectedOutput: "t13d181260_85036bcba153_d41ae481755e",
+ recipeConfig: [
+ {
+ "op": "JA4 Fingerprint",
+ "args": ["Hex", "JA4"]
+ }
+ ],
+ },
{
name: "JA4 Fingerprint: TLS 1.2",
input: "1603010200010001fc0303ecb2691addb2bf6c599c7aaae23de5f42561cc04eb41029acc6fc050a16ac1d22046f8617b580ac9358e2aa44e306d52466bcc989c87c8ca64309f5faf50ba7b4d0022130113031302c02bc02fcca9cca8c02cc030c00ac009c013c014009c009d002f00350100019100000021001f00001c636f6e74696c652e73657276696365732e6d6f7a696c6c612e636f6d00170000ff01000100000a000e000c001d00170018001901000101000b00020100002300000010000e000c02683208687474702f312e310005000501000000000022000a000804030503060302030033006b0069001d00208909858fbeb6ed2f1248ba5b9e2978bead0e840110192c61daed0096798b184400170041044d183d91f5eed35791fa982464e3b0214aaa5f5d1b78616d9b9fbebc22d11f535b2f94c686143136aa795e6e5a875d6c08064ad5b76d44caad766e2483012748002b00050403040303000d0018001604030503060308040805080604010501060102030201002d00020101001c000240010015007a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",