Merge branch 'master' into feat(ja4-fingerprint-summary)

This commit is contained in:
GCHQDeveloper581 2026-05-08 14:25:03 +01:00 committed by GitHub
commit 5c524efcc0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
90 changed files with 8028 additions and 2664 deletions

View File

@ -3,7 +3,7 @@
{
"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",
"image": "mcr.microsoft.com/devcontainers/javascript-node:24-trixie",
// Features to add to the dev container. More info: https://containers.dev/features.
"features": {

View File

@ -1 +0,0 @@
<!-- Prefix the title above with 'Misc:' -->

View File

@ -7,8 +7,14 @@ assignees: ''
---
## Summary
**Is your operation request related to a problem? Please describe.**
A clear and concise description of what the problem is. E.g. I'm always frustrated when [...]
### Example Input
**Describe the solution you'd like**
A clear and concise description of the new operation you would like.
### Example Output
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Example input and output**
Provide an example input to the operation, along with the output that you would expect.

70
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,70 @@
# See the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
#
# Check for minor/patch versions only on a weekly basis - we are likely to be able to
# merge these routinely. Major versions we'll check for and update manually.
#
- package-ecosystem: 'npm'
directory: '/'
versioning-strategy: increase
schedule:
interval: 'weekly'
day: 'friday'
time: '03:00'
timezone: Europe/London
commit-message:
prefix: 'chore (deps): '
ignore:
# we'll do any major version updates manually
- dependency-name: '*'
update-types: ['version-update:semver-major']
# packages we can't currently update
# see issue #2214 for rationale for each of these
- dependency-name: '@xmldom/xmldom'
versions: [ '>=0.9.0' ]
- dependency-name: 'bcryptjs'
versions: [ '>=3.0.0' ]
- dependency-name: 'bootstrap'
versions: [ '>=5.0.0' ]
- dependency-name: 'bson'
versions: [ '>=5.0.0' ]
- dependency-name: 'cbor'
versions: [ '>=10.0.0' ]
- dependency-name: 'eslint'
versions: [ '>=10.0.0' ]
- dependency-name: 'eslint-plugin-jsdoc'
versions: [ '>=51.0.0' ]
- dependency-name: 'fernet'
versions: [ '>=0.4.0' ]
- dependency-name: 'geodesy'
versions: [ '>=2.0.0' ]
- dependency-name: 'jimp'
versions: [ '1.6.1' ]
- dependency-name: 'webpack-dev-server'
versions: [ '>=5.1.0' ]
groups:
#
# Grouping so we don't get a seperate PR for every patch version.
#
patch-updates:
applies-to: version-updates
patterns:
- '*'
update-types:
- 'patch'
# Versioning on Github Actions
- package-ecosystem: "github-actions"
# Workflow files stored in the default location of `.github/workflows`; no need to
# specify `/.github/workflows` for `directory`
directory: '/'
schedule:
interval: 'weekly'
day: 'friday'
time: '03:00'
timezone: Europe/London
commit-message:
prefix: 'chore (deps): '

15
.github/pull_request_template.md vendored Normal file
View File

@ -0,0 +1,15 @@
**Description**
Provide a description of the pull request and the changes that it makes.
**Existing Issue**
If this pull request relates to an existing issue in the repository, please link it here.
**Screenshots**
If the pull request changes any visual aspects of CyberChef, please include screenshots.
**AI disclosure**
If you have used any AI tools while creating this code, **you must declare your usage along with the name of the tools that you used**.
Regardless of AI tool usage, you are responsible for any code that you submit, and we expect you to have checked the code and have enough of an understanding of it to answer any questions we might have.
**Test Coverage**
Please ensure you have added test coverage for your changes.

View File

@ -21,13 +21,12 @@ jobs:
- name: Set node version
uses: actions/setup-node@v6
with:
node-version: 18
node-version: 24
registry-url: "https://registry.npmjs.org"
- name: Install
run: |
export DETECT_CHROMEDRIVER_VERSION=true
npm install
npm ci
npm run setheapsize
- name: Lint
@ -57,7 +56,7 @@ jobs:
- name: Deploy to GitHub Pages
if: success() && github.ref == 'refs/heads/master'
uses: crazy-max/ghaction-github-pages@v3
uses: crazy-max/ghaction-github-pages@v5
with:
target_branch: gh-pages
build_dir: ./build/prod

View File

@ -17,13 +17,12 @@ jobs:
- name: Set node version
uses: actions/setup-node@v6
with:
node-version: 18
node-version: 24
registry-url: "https://registry.npmjs.org"
- name: Install
run: |
export DETECT_CHROMEDRIVER_VERSION=true
npm install
npm ci
npm run setheapsize
- name: Lint
@ -38,11 +37,26 @@ jobs:
if: success()
run: npx grunt prod
- name: Upload Build Artefact
if: success()
uses: actions/upload-artifact@v7
with:
name: zipped-build
path: build/prod/*.zip
retention-days: 5
- name: UI Tests
if: success()
run: |
sudo apt-get install xvfb
xvfb-run --server-args="-screen 0 1200x800x24" npx grunt testui
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
if: success()
uses: docker/setup-buildx-action@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
- name: Production Image Build
if: success()
@ -50,8 +64,3 @@ jobs:
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

View File

@ -27,12 +27,11 @@ jobs:
- name: Set node version
uses: actions/setup-node@v6
with:
node-version: 18
node-version: 24
registry-url: "https://registry.npmjs.org"
- name: Install
run: |
export DETECT_CHROMEDRIVER_VERSION=true
npm ci
npm run setheapsize
@ -53,14 +52,14 @@ jobs:
xvfb-run --server-args="-screen 0 1200x800x24" npx grunt testui
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
- name: Image Metadata
id: image-metadata
uses: docker/metadata-action@v4
uses: docker/metadata-action@v6
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
@ -69,7 +68,7 @@ jobs:
type=semver,pattern={{version}}
- name: Log in to GHCR
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ env.REGISTRY_USER }}
@ -107,7 +106,7 @@ jobs:
- name: Set node version
uses: actions/setup-node@v6
with:
node-version: 18
node-version: 24
registry-url: "https://registry.npmjs.org"
- name: Install
@ -116,11 +115,5 @@ jobs:
- name: Create machine generated files
run: npm run node
- name: Reset node version ready for publish
uses: actions/setup-node@v6
with:
node-version: ^24.5
registry-url: "https://registry.npmjs.org"
- name: Publish to NPM
run: npm publish

2
.nvmrc
View File

@ -1 +1 @@
18
24

View File

@ -13,6 +13,104 @@ All major and minor version changes will be documented in this file. Details of
## Details
## [11.0.0] - 2026-04-28
- Revert sitemap to v8.0.X to fix build/deploy on master [@GCHQDeveloper581] | [#2348]
- Node version update from 22 to 24 [@lzandman] [@GCHQDeveloper581] | [#2347]
- Fix XSS in Show Base64 offsets [@C85297] | [#2346]
- Make compatible with node >=22 [@GCHQDeveloper581] | [#2273]
- Fix(node): enable asynchronous operation support in Node.js API [@engin0223] [@GCHQDeveloper581] | [#2342]
- Feature: Change to nginx-unprivileged image for better kubernetes support [@hsolberg] | [#1922]
Breaking changes:
- Minimum supported node version - now v24 (was v16)
- Change of exported port on Docker Container - now 8080 (was 80)
- Node API now exports "bake" and "execute" functions as async.
<details>
<summary>Click to expand v10 minor versions</summary>
### [10.24.0] - 2026-04-27
- Update CONTRIBUTING.md [@GCHQDeveloper581] | [#2333]
- Fix, and link, Fernet tests [@GCHQDeveloper581] | [#2335]
- [#927] added parity bit operation [@j83305] | [#1036]
- Feature md link blanks [@BjoernAkAManf] [@GCHQDeveloper581] | [#660]
- Accessibility - Add support for screenreaders in operations search [@mattnotmitt] | [#1862]
- Added metadata extraction for UUID strings. [@ko80240] | [#2322]
- chore (deps): bump the patch-updates group with 6 updates | [#2330]
- chore (deps): bump @codemirror/search from 6.6.0 to 6.7.0 | [#2331]
- (Feature) Improve CI [@GCHQDeveloper581] | [#2328]
- Update dependabot.yml [@GCHQDeveloper581] | [#2326]
- chore (deps): bump lodash, grunt-legacy-log and grunt-legacy-util | [#2327]
- chore (deps): bump the patch-updates group with 6 updates [@GCHQDeveloper581] | [#2323]
- chore (deps): bump autoprefixer from 10.4.27 to 10.5.0 | [#2324]
- chore (deps): bump dompurify from 3.3.3 to 3.4.0 | [#2321]
- chore (deps): bump follow-redirects from 1.15.11 to 1.16.0 | [#2320]
- Regular Expression operation email address regex: Support IPv4 domains [@C85297] [@GCHQDeveloper581] | [#2167]
- Rewriting fixCryptoApiImports and fixSnackbarMarkup to js to make it OS agnostic [@BigYellowHammer] | [#2298]
- chore (deps): bump basic-ftp from 5.2.1 to 5.2.2 | [#2317]
- chore (deps): bump axios from 1.13.6 to 1.15.0 | [#2316]
- chore (deps): bump webpack from 5.105.4 to 5.106.0 | [#2315]
- chore (deps): bump basic-ftp from 5.2.0 to 5.2.1 | [#2313]
- Update vulnerable dependencies [@GCHQDeveloper581] | [#2311]
### [10.23.0] - 2026-04-06
- Properly escape HTML entities in sampleDelim to avoid XSS issue [@GCHQDeveloper581] | [#2307]
- chore (deps): bump lodash from 4.17.23 to 4.18.1 | [#2304]
- chore (deps): bump @codemirror/view from 6.40.0 to 6.41.0 | [#2305]
- chore (deps): bump the patch-updates group with 2 updates | [#2303]
- chore (deps): bump @xmldom/xmldom from 0.8.11 to 0.8.12 | [#2302]
- chore (deps): bump picomatch | [#2299]
- chore (deps): bump node-forge from 1.3.3 to 1.4.0 | [#2297]
- chore (deps): bump the patch-updates group with 3 updates | [#2296]
- chore (deps) bump chromedriver from 130.0.4 to 146.0.6 [@GCHQDeveloper581] | [#2292]
- ParseEthernetFrame - Fix vlan calculation [@Kalkran] | [#2295]
- Add pull request template with AI usage disclosure [@C85297] | [#2279]
- fix: return empty output for zero-length To Modhex input [@saschabuehrle] | [#2249]
- Added tab focus to top banner and navigation to About/Support Modal [@j264415] | [#1733]
- Add Parse Ethernet frame Operation, allow Parse IPv4 Header to cascade [@Kalkran] | [#1722]
- Selection and Deselection of autobake checkbox using keyboard [@j264415] | [#1727]
- chore (deps): bump @babel/runtime from 7.28.6 to 7.29.2 | [#2263]
- Add more helpful error for when numerical ingredient is left empty [@Lamby777] [@C85297] | [#1540]
- chore (deps): bump @codemirror/view from 6.39.17 to 6.40.0 | [#2262]
- Bump flatted from 3.3.2 to 3.4.2 [@GCHQDeveloper581] | [#2266]
- feat: add Raw option for Jq operation [@rtpt-romankarwacik] | [#2237]
- chore (deps): bump core-js from 3.48.0 to 3.49.0 | [#2261]
- chore (deps): bump the patch-updates group with 6 updates | [#2260]
- Add Extract Audio Metadata operation [@d0s1nt] [@GCHQDeveloper581] | [#2170]
- Fix Jq issue [@GCHQDeveloper581] | [#2210]
- Configure dependabot updates [@GCHQDeveloper581] | [#2259]
- fix(A1Z26): return empty string instead of empty array for empty input [@brick-pixel] | [#2257]
- Fix broken Docker link in README [@am-periphery] | [#2250]
- Update some dependencies, including a number causing npm audit warnings [@GCHQDeveloper581] | [#2236]
- Bump axios from 1.7.9 to 1.13.6 | [#2234]
- Bump jws from 3.2.2 to 3.2.3 | [#2235]
- Bump pbkdf2 from 3.1.2 to 3.1.5 | [#2229]
- Bump form-data from 4.0.1 to 4.0.5 | [#2228]
- Bump basic-ftp from 5.0.5 to 5.2.0 | [#2231]
- feat: add ARM disassembler operation [@thomasxm] | [#2156]
- Add Text/Integer Converter operation [@p-leriche] [@GCHQDeveloper581] | [#2213]
- Feat/rc6 add RC6 Encrypt/Decrypt operations [@thomasxm] | [#2163]
- [bugfix] Add Bootstrap form style for CodeMirror editor [@Swonkie] | [#2161]
- Add Flask Session operations (Decode, Sign, Verify) [@ThePlayer372-FR] | [#2208]
- fix: `jq-web` -> `jq-wasm`, includes `jq` version `1.8.1` [@W-Floyd] [@GCHQDeveloper581] | [#2223]
- Bump jsonwebtoken from 8.5.1 to 9.0.0 [@GCHQDeveloper581] | [#2219]
- Bump basic-ftp from 5.0.5 to 5.2.0 | [#2218]
- feat: add random integer generation operation [@cktgh] | [#2151]
- Add BigInt utility functions for number theory operations [@p-leriche] [@GCHQDeveloper581] | [#2205]
- Improve SQL Beautify: use sql-formatter and support bind variables [@aby-jo] [@GCHQDeveloper581] | [#2071]
- update tesseract.js to 6.0.1 [@atsiv1] | [#2133]
- Fix hint tooltip display issues [@bartvanandel] | [#2017]
- Simplify babel dependencies [@GCHQDeveloper581] | [#2204]
- Dependency updates [@GCHQDeveloper581] | [#2201]
- Fix: Move Magic checks from Escape to Unescape Unicode Characters [@fjh1997] | [#2195]
- Paste spreadsheets as text [@C85297] | [#2200]
- Fix Roboto Mono font [@C85297] | [#2199]
- Fix return of buffer for PNG QR image generation [@GCHQDeveloper581] [@C85297] | [#2125]
- Update JIMP [@C85297] | [#2171]
- Overwrite NGINX maintainer label [@C85297] | [#2194]
- Bump v10.22.1 [@GCHQDeveloper581] | [#2193]
- Fix npm publish - Run "npm ci" and "npm run node" under node 18 then switch to node 24.5 [@GCHQDeveloper581] | [#2192]
### [10.22.0] - 2026-02-11
- Separate npm publish out into separate job and run with Node 24.5 [@GCHQDeveloper581] | [#2188]
- Fixed Percent delimiter for hex encoding [@beneri] [@C85297] | [#2137]
@ -195,6 +293,8 @@ All major and minor version changes will be documented in this file. Details of
- Added 'Levenshtein Distance' operation [@mikecat] | [#1498]
- Added 'Swap case' operation [@mikecat] | [#1499]
</details>
## [10.0.0] - 2023-03-22
- [Full details explained here](https://github.com/gchq/CyberChef/wiki/Character-encoding,-EOL-separators,-and-editor-features)
- Status bars added to the Input and Output [@n1474335] | [#1405]
@ -538,6 +638,9 @@ All major and minor version changes will be documented in this file. Details of
## [4.0.0] - 2016-11-28
- Initial open source commit [@n1474335] | [b1d73a72](https://github.com/gchq/CyberChef/commit/b1d73a725dc7ab9fb7eb789296efd2b7e4b08306)
[11.0.0]: https://github.com/gchq/CyberChef/releases/tag/v11.0.0
[10.24.0]: https://github.com/gchq/CyberChef/releases/tag/v10.24.0
[10.23.0]: https://github.com/gchq/CyberChef/releases/tag/v10.23.0
[10.22.0]: https://github.com/gchq/CyberChef/releases/tag/v10.22.0
[10.21.0]: https://github.com/gchq/CyberChef/releases/tag/v10.21.0
[10.20.0]: https://github.com/gchq/CyberChef/releases/tag/v10.20.0
@ -797,6 +900,29 @@ All major and minor version changes will be documented in this file. Details of
[@t-martine]: https://github.com/t-martine
[@wesinator]: https://github.com/wesinator
[@Raka-loah]: https://github.com/Raka-loah
[@Kalkran]: https://github.com/Kalkran
[@saschabuehrle]: https://github.com/saschabuehrle
[@j264415]: https://github.com/j264415
[@Lamby777]: https://github.com/Lamby777
[@rtpt-romankarwacik]: https://github.com/rtpt-romankarwacik
[@d0s1nt]: https://github.com/d0s1nt
[@brick-pixel]: https://github.com/brick-pixel
[@am-periphery]: https://github.com/am-periphery
[@p-leriche]: https://github.com/p-leriche
[@Swonkie]: https://github.com/Swonkie
[@ThePlayer372-FR]: https://github.com/ThePlayer372-FR
[@W-Floyd]: https://github.com/W-Floyd
[@cktgh]: https://github.com/cktgh
[@aby-jo]: https://github.com/aby-jo
[@atsiv1]: https://github.com/atsiv1
[@fjh1997]: https://github.com/fjh1997
[@j83305]: https://github.com/j83305
[@BjoernAkAManf]: https://github.com/BjoernAkAManf
[@ko80240]: https://github.com/ko80240
[@BigYellowHammer]: https://github.com/BigYellowHammer
[@hsolberg]: https://github.com/hsolberg
[@lzandman]: https://github.com/lzandman
[@engin0223]: https://github.com/engin0223
[8ad18b]: https://github.com/gchq/CyberChef/commit/8ad18bc7db6d9ff184ba3518686293a7685bf7b7
@ -1010,4 +1136,88 @@ All major and minor version changes will be documented in this file. Details of
[#2183]: https://github.com/gchq/CyberChef/pull/2183
[#2182]: https://github.com/gchq/CyberChef/pull/2182
[#2181]: https://github.com/gchq/CyberChef/pull/2181
[#2307]: https://github.com/gchq/CyberChef/pull/2307
[#2304]: https://github.com/gchq/CyberChef/pull/2304
[#2305]: https://github.com/gchq/CyberChef/pull/2305
[#2303]: https://github.com/gchq/CyberChef/pull/2303
[#2302]: https://github.com/gchq/CyberChef/pull/2302
[#2299]: https://github.com/gchq/CyberChef/pull/2299
[#2297]: https://github.com/gchq/CyberChef/pull/2297
[#2296]: https://github.com/gchq/CyberChef/pull/2296
[#2292]: https://github.com/gchq/CyberChef/pull/2292
[#2295]: https://github.com/gchq/CyberChef/pull/2295
[#2279]: https://github.com/gchq/CyberChef/pull/2279
[#2249]: https://github.com/gchq/CyberChef/pull/2249
[#1733]: https://github.com/gchq/CyberChef/pull/1733
[#1722]: https://github.com/gchq/CyberChef/pull/1722
[#1727]: https://github.com/gchq/CyberChef/pull/1727
[#2263]: https://github.com/gchq/CyberChef/pull/2263
[#1540]: https://github.com/gchq/CyberChef/pull/1540
[#2262]: https://github.com/gchq/CyberChef/pull/2262
[#2266]: https://github.com/gchq/CyberChef/pull/2266
[#2237]: https://github.com/gchq/CyberChef/pull/2237
[#2261]: https://github.com/gchq/CyberChef/pull/2261
[#2260]: https://github.com/gchq/CyberChef/pull/2260
[#2170]: https://github.com/gchq/CyberChef/pull/2170
[#2210]: https://github.com/gchq/CyberChef/pull/2210
[#2259]: https://github.com/gchq/CyberChef/pull/2259
[#2257]: https://github.com/gchq/CyberChef/pull/2257
[#2250]: https://github.com/gchq/CyberChef/pull/2250
[#2236]: https://github.com/gchq/CyberChef/pull/2236
[#2234]: https://github.com/gchq/CyberChef/pull/2234
[#2235]: https://github.com/gchq/CyberChef/pull/2235
[#2229]: https://github.com/gchq/CyberChef/pull/2229
[#2228]: https://github.com/gchq/CyberChef/pull/2228
[#2231]: https://github.com/gchq/CyberChef/pull/2231
[#2156]: https://github.com/gchq/CyberChef/pull/2156
[#2213]: https://github.com/gchq/CyberChef/pull/2213
[#2163]: https://github.com/gchq/CyberChef/pull/2163
[#2161]: https://github.com/gchq/CyberChef/pull/2161
[#2208]: https://github.com/gchq/CyberChef/pull/2208
[#2223]: https://github.com/gchq/CyberChef/pull/2223
[#2219]: https://github.com/gchq/CyberChef/pull/2219
[#2218]: https://github.com/gchq/CyberChef/pull/2218
[#2151]: https://github.com/gchq/CyberChef/pull/2151
[#2205]: https://github.com/gchq/CyberChef/pull/2205
[#2071]: https://github.com/gchq/CyberChef/pull/2071
[#2133]: https://github.com/gchq/CyberChef/pull/2133
[#2017]: https://github.com/gchq/CyberChef/pull/2017
[#2204]: https://github.com/gchq/CyberChef/pull/2204
[#2201]: https://github.com/gchq/CyberChef/pull/2201
[#2195]: https://github.com/gchq/CyberChef/pull/2195
[#2200]: https://github.com/gchq/CyberChef/pull/2200
[#2199]: https://github.com/gchq/CyberChef/pull/2199
[#2125]: https://github.com/gchq/CyberChef/pull/2125
[#2171]: https://github.com/gchq/CyberChef/pull/2171
[#2194]: https://github.com/gchq/CyberChef/pull/2194
[#2193]: https://github.com/gchq/CyberChef/pull/2193
[#2192]: https://github.com/gchq/CyberChef/pull/2192
[#2333]: https://github.com/gchq/CyberChef/pull/2333
[#2335]: https://github.com/gchq/CyberChef/pull/2335
[#1036]: https://github.com/gchq/CyberChef/pull/1036
[#660]: https://github.com/gchq/CyberChef/pull/660
[#1862]: https://github.com/gchq/CyberChef/pull/1862
[#2322]: https://github.com/gchq/CyberChef/pull/2322
[#2330]: https://github.com/gchq/CyberChef/pull/2330
[#2331]: https://github.com/gchq/CyberChef/pull/2331
[#2328]: https://github.com/gchq/CyberChef/pull/2328
[#2326]: https://github.com/gchq/CyberChef/pull/2326
[#2327]: https://github.com/gchq/CyberChef/pull/2327
[#2323]: https://github.com/gchq/CyberChef/pull/2323
[#2324]: https://github.com/gchq/CyberChef/pull/2324
[#2321]: https://github.com/gchq/CyberChef/pull/2321
[#2320]: https://github.com/gchq/CyberChef/pull/2320
[#2167]: https://github.com/gchq/CyberChef/pull/2167
[#2298]: https://github.com/gchq/CyberChef/pull/2298
[#2317]: https://github.com/gchq/CyberChef/pull/2317
[#2316]: https://github.com/gchq/CyberChef/pull/2316
[#2315]: https://github.com/gchq/CyberChef/pull/2315
[#2313]: https://github.com/gchq/CyberChef/pull/2313
[#2311]: https://github.com/gchq/CyberChef/pull/2311
[#2348]: https://github.com/gchq/CyberChef/pull/2348
[#2347]: https://github.com/gchq/CyberChef/pull/2347
[#2346]: https://github.com/gchq/CyberChef/pull/2346
[#2273]: https://github.com/gchq/CyberChef/pull/2273
[#2342]: https://github.com/gchq/CyberChef/pull/2342
[#1922]: https://github.com/gchq/CyberChef/pull/1922

View File

@ -6,10 +6,13 @@ There are lots of opportunities to contribute to CyberChef. If you want ideas, t
Before your contributions can be accepted, you must:
- Sign the [GCHQ Contributor Licence Agreement](https://cla-assistant.io/gchq/CyberChef)
- Fork the CyberChef repo
- Create a new branch within your fork for the changes
- Push your changes to your fork.
- Sign the [GCHQ Contributor Licence Agreement](https://cla-assistant.io/gchq/CyberChef)
- Submit a pull request.
Please note that we will ***reject*** pull requests from the master branch of your fork owing to the mess it makes of our own working repositories and the extra work entailed.
## Coding conventions

View File

@ -4,7 +4,7 @@
# Modifier --platform=$BUILDPLATFORM limits the platform to "BUILDPLATFORM" during buildx multi-platform builds
# This is because npm "chromedriver" package is not compatiable with all platforms
# For more info see: https://docs.docker.com/build/building/multi-platform/#cross-compilation
FROM --platform=$BUILDPLATFORM node:18-alpine AS builder
FROM --platform=$BUILDPLATFORM node:24-alpine AS builder
WORKDIR /app
@ -27,7 +27,7 @@ RUN npm run build
#########################################
# Package static build files into nginx #
#########################################
FROM nginx:stable-alpine AS cyberchef
FROM nginxinc/nginx-unprivileged:stable-alpine AS cyberchef
LABEL maintainer="GCHQ <oss@gchq.gov.uk>"

View File

@ -6,7 +6,7 @@ const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPl
const glob = require("glob");
const path = require("path");
const nodeFlags = "--experimental-modules --experimental-json-modules --experimental-specifier-resolution=node --no-warnings --no-deprecation";
const nodeFlags = "--no-warnings --no-deprecation";
/**
* Grunt configuration for building the app in various formats.
@ -411,25 +411,11 @@ module.exports = function (grunt) {
stdout: false,
},
fixCryptoApiImports: {
command: function () {
switch (process.platform) {
case "darwin":
return `find ./node_modules/crypto-api/src/ \\( -type d -name .git -prune \\) -o -type f -print0 | xargs -0 sed -i '' -e '/\\.mjs/!s/\\(from "\\.[^"]*\\)";/\\1.mjs";/g'`;
default:
return `find ./node_modules/crypto-api/src/ \\( -type d -name .git -prune \\) -o -type f -print0 | xargs -0 sed -i -e '/\\.mjs/!s/\\(from "\\.[^"]*\\)";/\\1.mjs";/g'`;
}
},
command: `node ${nodeFlags} src/core/config/scripts/fixCryptoApiImports.mjs`,
stdout: false
},
fixSnackbarMarkup: {
command: function () {
switch (process.platform) {
case "darwin":
return `sed -i '' 's/<div id=snackbar-container\\/>/<div id=snackbar-container>/g' ./node_modules/snackbarjs/src/snackbar.js`;
default:
return `sed -i 's/<div id=snackbar-container\\/>/<div id=snackbar-container>/g' ./node_modules/snackbarjs/src/snackbar.js`;
}
},
command: `node ${nodeFlags} src/core/config/scripts/fixSnackBarMarkup.mjs`,
stdout: false
},
},

View File

@ -24,7 +24,7 @@ Cryptographic operations in CyberChef should not be relied upon to provide secur
**Prerequisites**
- [Docker](hhttps://www.docker.com/products/docker-desktop/)
- [Docker](https://www.docker.com/products/docker-desktop/)
- Docker Desktop must be open and running on your machine
@ -36,7 +36,7 @@ docker build --tag cyberchef --ulimit nofile=10000 .
```
2. Run the docker container
```bash
docker run -it -p 8080:80 cyberchef
docker run -it -p 8080:8080 cyberchef
```
3. Navigate to `http://localhost:8080` in your browser
@ -45,7 +45,7 @@ docker run -it -p 8080:80 cyberchef
If you prefer to skip the build process, you can use the pre-built image
```bash
docker run -it -p 8080:80 ghcr.io/gchq/cyberchef:latest
docker run -it -p 8080:8080 ghcr.io/gchq/cyberchef:latest
```
Just like before, navigate to `http://localhost:8080` in your browser.
@ -120,7 +120,7 @@ CyberChef is built to support
## Node.js support
CyberChef is built to fully support Node.js `v16`. For more information, see the ["Node API" wiki page](https://github.com/gchq/CyberChef/wiki/Node-API)
CyberChef is built to fully support Node.js `v24`. For more information, see the ["Node API" wiki page](https://github.com/gchq/CyberChef/wiki/Node-API)
## Contributing

View File

@ -16,6 +16,9 @@ module.exports = function(api) {
"regenerator": true
}
]
]
],
"generatorOpts": {
"importAttributesKeyword": "with"
}
};
};

4782
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "cyberchef",
"version": "10.22.1",
"version": "11.0.0",
"description": "The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.",
"author": "n1474335 <n1474335@gmail.com>",
"homepage": "https://gchq.github.io/CyberChef",
@ -36,34 +36,34 @@
"browserslist": [
"Chrome >= 50",
"Firefox >= 38",
"node >= 16"
"node >= 24"
],
"devDependencies": {
"@babel/eslint-parser": "^7.28.6",
"@babel/plugin-syntax-import-assertions": "^7.28.6",
"@babel/plugin-transform-runtime": "^7.29.0",
"@babel/preset-env": "^7.29.0",
"@babel/runtime": "^7.28.6",
"@codemirror/commands": "^6.10.2",
"@codemirror/language": "^6.12.1",
"@codemirror/search": "^6.6.0",
"@babel/preset-env": "^7.29.5",
"@babel/runtime": "^7.29.2",
"@codemirror/commands": "^6.10.3",
"@codemirror/language": "^6.12.3",
"@codemirror/search": "^6.7.0",
"@codemirror/state": "^6.5.4",
"@codemirror/view": "^6.39.15",
"autoprefixer": "^10.4.24",
"babel-loader": "^10.0.0",
"@codemirror/view": "^6.41.1",
"autoprefixer": "^10.5.0",
"babel-loader": "^10.1.1",
"base64-loader": "^1.0.0",
"chromedriver": "^130.0.4",
"chromedriver": "^146.0.6",
"cli-progress": "^3.12.0",
"colors": "^1.4.0",
"compression-webpack-plugin": "^11.1.0",
"copy-webpack-plugin": "^13.0.1",
"core-js": "^3.48.0",
"cspell": "^8.19.4",
"css-loader": "7.1.4",
"eslint": "^9.39.3",
"compression-webpack-plugin": "^12.0.0",
"copy-webpack-plugin": "^14.0.0",
"core-js": "^3.49.0",
"cspell": "^9.7.0",
"css-loader": "^7.1.4",
"eslint": "^9.39.4",
"eslint-plugin-jsdoc": "^50.8.0",
"globals": "^15.15.0",
"grunt": "^1.6.1",
"globals": "^17.4.0",
"grunt": "^1.6.2",
"grunt-chmod": "~1.1.1",
"grunt-concurrent": "^3.0.0",
"grunt-contrib-clean": "~2.0.1",
@ -74,35 +74,37 @@
"grunt-exec": "~3.0.0",
"grunt-webpack": "^6.0.0",
"grunt-zip": "^1.0.0",
"html-webpack-plugin": "^5.6.6",
"html-webpack-plugin": "^5.6.7",
"imports-loader": "^5.0.0",
"mini-css-extract-plugin": "2.10.0",
"mini-css-extract-plugin": "2.10.2",
"modify-source-webpack-plugin": "^4.1.0",
"nightwatch": "^3.15.0",
"postcss": "^8.5.6",
"postcss": "^8.5.14",
"postcss-css-variables": "^0.19.0",
"postcss-import": "^16.1.1",
"postcss-loader": "^8.2.1",
"prompt": "^1.3.0",
"sitemap": "^8.0.2",
"terser": "^5.46.0",
"webpack": "^5.105.2",
"webpack-bundle-analyzer": "^4.10.2",
"webpack-dev-server": "5.0.4",
"sitemap": "^8.0.3",
"terser": "^5.46.2",
"webpack": "^5.106.2",
"webpack-bundle-analyzer": "^5.3.0",
"webpack-dev-server": "^5.0.4",
"webpack-node-externals": "^3.0.0",
"worker-loader": "^3.0.8"
},
"dependencies": {
"@alexaltea/capstone-js": "^3.0.5",
"@astronautlabs/amf": "^0.0.6",
"@blu3r4y/lzma": "^2.3.3",
"@wavesenterprise/crypto-gost-js": "^2.1.0-RC1",
"@xmldom/xmldom": "^0.8.11",
"@xmldom/xmldom": "^0.8.13",
"argon2-browser": "^1.18.0",
"arrive": "^2.5.2",
"arrive": "^2.5.3",
"assert": "^2.1.0",
"avsc": "^5.7.9",
"bcryptjs": "^2.4.3",
"bignumber.js": "^9.3.1",
"@noble/hashes": "2.2.0",
"blakejs": "^1.2.1",
"bootstrap": "4.6.2",
"bootstrap-colorpicker": "^3.4.0",
@ -120,7 +122,7 @@
"d3": "7.9.0",
"d3-hexbin": "^0.2.2",
"diff": "^5.2.2",
"dompurify": "^3.3.1",
"dompurify": "^3.4.2",
"es6-promisify": "^7.0.0",
"escodegen": "^2.1.0",
"esprima": "^4.0.1",
@ -130,44 +132,44 @@
"file-saver": "^2.0.5",
"flat": "^6.0.1",
"geodesy": "1.1.3",
"handlebars": "^4.7.8",
"hash-wasm": "^4.12.0",
"handlebars": "^4.7.9",
"highlight.js": "^11.11.1",
"ieee754": "^1.2.1",
"jimp": "^1.6.0",
"jq-wasm": "^1.1.0-jq-1.8.1",
"jimp": "1.6.0",
"jq-web": "^0.5.1",
"jquery": "3.7.1",
"js-sha3": "^0.9.3",
"jsesc": "^3.1.0",
"json5": "^2.2.3",
"jsonata": "^2.1.0",
"jsonpath-plus": "^10.4.0",
"jsonwebtoken": "9.0.0",
"jsonwebtoken": "9.0.3",
"jsqr": "^1.4.0",
"jsrsasign": "^11.1.1",
"jsrsasign": "^11.1.3",
"kbpgp": "^2.1.17",
"libbzip2-wasm": "0.0.4",
"libyara-wasm": "^1.2.1",
"lodash": "^4.17.23",
"lodash": "^4.18.1",
"loglevel": "^1.9.2",
"loglevel-message-prefix": "^3.0.0",
"lz-string": "^1.5.0",
"lz4js": "^0.2.0",
"markdown-it": "^14.1.1",
"moment": "^2.30.1",
"moment-timezone": "^0.6.0",
"moment-timezone": "^0.6.2",
"ngeohash": "^0.6.3",
"node-forge": "^1.3.3",
"node-forge": "^1.4.0",
"node-md6": "^0.1.0",
"nodom": "^2.4.0",
"notepack.io": "^3.0.1",
"ntlm": "^0.1.3",
"nwmatcher": "^1.4.4",
"otpauth": "9.3.6",
"otpauth": "^9.5.0",
"path": "^0.12.7",
"popper.js": "^1.16.1",
"process": "^0.11.10",
"protobufjs": "^7.5.4",
"protobufjs": "^7.5.6",
"punycode.js": "^2.3.1",
"qr-image": "^3.2.0",
"reflect-metadata": "^0.2.2",
"rison": "^0.1.1",
@ -175,7 +177,7 @@
"snackbarjs": "^1.1.0",
"sortablejs": "^1.15.7",
"split.js": "^1.6.5",
"sql-formatter": "^15.6.5",
"sql-formatter": "^15.7.3",
"ssdeep.js": "0.0.3",
"stream-browserify": "^3.0.0",
"tesseract.js": "^6.0.1",
@ -183,7 +185,7 @@
"unorm": "^1.6.0",
"url": "^0.11.4",
"utf8": "^3.0.0",
"uuid": "^13.0.0",
"uuid": "^14.0.0",
"vkbeautify": "^0.99.3",
"xpath": "0.0.34",
"xregexp": "^5.1.2",
@ -193,18 +195,21 @@
"start": "npx grunt dev",
"build": "npx grunt prod",
"node": "npx grunt node",
"repl": "node --experimental-modules --experimental-json-modules --experimental-specifier-resolution=node --no-experimental-fetch --no-warnings src/node/repl.mjs",
"test": "npx grunt configTests && node --experimental-modules --experimental-json-modules --no-warnings --no-deprecation --openssl-legacy-provider --no-experimental-fetch tests/node/index.mjs && node --experimental-modules --experimental-json-modules --no-warnings --no-deprecation --openssl-legacy-provider --no-experimental-fetch --trace-uncaught tests/operations/index.mjs",
"repl": "node --no-warnings src/node/repl.mjs",
"test": "npx grunt configTests && node --no-warnings --no-deprecation --openssl-legacy-provider tests/node/index.mjs && node --no-warnings --no-deprecation --openssl-legacy-provider --trace-uncaught tests/operations/index.mjs",
"testnodeconsumer": "npx grunt testnodeconsumer",
"testui": "npx grunt testui",
"testuidev": "npx nightwatch --env=dev",
"lint": "npx grunt lint",
"lint:grammar": "cspell ./src",
"postinstall": "npx grunt exec:fixCryptoApiImports && npx grunt exec:fixSnackbarMarkup",
"newop": "node --experimental-modules --experimental-json-modules src/core/config/scripts/newOperation.mjs",
"minor": "node --experimental-modules --experimental-json-modules src/core/config/scripts/newMinorVersion.mjs && npm version minor --git-tag-version=false && echo \"Updated to version v$(npm pkg get version | xargs), please create a pull request and once merged use 'npm run tag'\"",
"newop": "node src/core/config/scripts/newOperation.mjs",
"minor": "node src/core/config/scripts/newMinorVersion.mjs && npm version minor --git-tag-version=false && echo \"Updated to version v$(npm pkg get version | xargs), please create a pull request and once merged use 'npm run tag'\"",
"tag": "git tag -s \"v$(npm pkg get version | xargs)\" -m \"$(npm pkg get version | xargs)\" && echo \"Created v$(npm pkg get version | xargs), now check and push the tag\"",
"getheapsize": "node -e 'console.log(`node heap limit = ${require(\"v8\").getHeapStatistics().heap_size_limit / (1024 * 1024)} Mb`)'",
"setheapsize": "export NODE_OPTIONS=--max_old_space_size=2048"
},
"engines": {
"node": ">=24 <25"
}
}

View File

@ -55,8 +55,15 @@ class Chef {
progress = await recipe.execute(this.dish, progress);
} catch (err) {
log.error(err);
let displayStr;
if ("displayStr" in err) {
displayStr = err.displayStr;
} else {
displayStr = err.toString();
}
error = {
displayStr: err.displayStr,
displayStr: displayStr,
};
progress = err.progress;
}

View File

@ -7,7 +7,7 @@
*/
import Chef from "./Chef.mjs";
import OperationConfig from "./config/OperationConfig.json" assert {type: "json"};
import OperationConfig from "./config/OperationConfig.json" with { type: "json" };
import OpModules from "./config/modules/OpModules.mjs";
import loglevelMessagePrefix from "loglevel-message-prefix";

7
src/core/Ingredient.mjs Executable file → Normal file
View File

@ -5,7 +5,8 @@
*/
import Utils from "./Utils.mjs";
import {fromHex} from "./lib/Hex.mjs";
import { fromHex } from "./lib/Hex.mjs";
import OperationError from "./errors/OperationError.mjs";
/**
* The arguments to operations.
@ -119,7 +120,9 @@ class Ingredient {
number = parseFloat(data);
if (isNaN(number)) {
const sample = Utils.truncate(data.toString(), 10);
throw "Invalid ingredient value. Not a number: " + sample;
throw new OperationError(
"Invalid ingredient value. Not a number: " + sample,
);
}
return number;
default:

View File

@ -5,6 +5,7 @@
*/
import Dish from "./Dish.mjs";
import OperationError from "./errors/OperationError.mjs";
import Ingredient from "./Ingredient.mjs";
/**
@ -223,7 +224,11 @@ class Operation {
*/
set ingValues(ingValues) {
ingValues.forEach((val, i) => {
try {
this._ingList[i].value = val;
} catch (err) {
throw new OperationError(`Failed to set value of ingredient '${this._ingList[i].name}': ${err}`);
}
});
}

View File

@ -4,7 +4,7 @@
* @license Apache-2.0
*/
import OperationConfig from "./config/OperationConfig.json" assert {type: "json"};
import OperationConfig from "./config/OperationConfig.json" with { type: "json" };
import OperationError from "./errors/OperationError.mjs";
import Operation from "./Operation.mjs";
import DishError from "./errors/DishError.mjs";
@ -70,11 +70,15 @@ class Recipe {
if (o instanceof Operation) {
return o;
} else {
try {
const op = new modules[o.module][o.name]();
op.ingValues = o.ingValues;
op.breakpoint = o.breakpoint;
op.disabled = o.disabled;
return op;
} catch (err) {
throw new Error(`Failed to hydrate operation '${o.name}': ${err}`);
}
}
});
}

View File

@ -41,6 +41,7 @@
"From Base",
"To BCD",
"From BCD",
"Text-Integer Conversion",
"To HTML Entity",
"From HTML Entity",
"URL Encode",
@ -109,6 +110,8 @@
"Rabbit",
"SM4 Encrypt",
"SM4 Decrypt",
"RC6 Encrypt",
"RC6 Decrypt",
"GOST Encrypt",
"GOST Decrypt",
"GOST Sign",
@ -246,6 +249,7 @@
"DNS over HTTPS",
"Strip HTTP headers",
"Dechunk HTTP response",
"Parse Ethernet frame",
"Parse User Agent",
"Parse IP range",
"Parse IPv6 address",
@ -383,6 +387,7 @@
"CSS selector",
"Extract EXIF",
"Extract ID3",
"Extract Audio Metadata",
"Extract Files",
"RAKE",
"Template"
@ -461,7 +466,8 @@
"Luhn Checksum",
"CRC Checksum",
"TCP/IP Checksum",
"XOR Checksum"
"XOR Checksum",
"Parity Bit"
]
},
{
@ -512,7 +518,8 @@
"View Bit Plane",
"Randomize Colour Palette",
"Extract LSB",
"ELF Info"
"ELF Info",
"Extract Audio Metadata"
]
},
{
@ -545,7 +552,8 @@
"Hex Density chart",
"Scatter chart",
"Series chart",
"Heatmap chart"
"Heatmap chart",
"Extract Audio Metadata"
]
},
{
@ -557,6 +565,7 @@
"Chi Square",
"P-list Viewer",
"Disassemble x86",
"Disassemble ARM",
"Pseudo-Random Number Generator",
"Pseudo-Random Integer Generator",
"Generate De Bruijn Sequence",

View File

@ -0,0 +1,54 @@
/**
* This script updates crypto-api package
* It adds .mjs to local imports where its missing
*
* before:
* import foo from "./bar";
* after
* import foo from "./bar.mjs";
*
*/
/* eslint no-console: ["off"] */
import { readdirSync, readFileSync, writeFileSync } from "fs";
import { join } from "path";
// Base directory of crypto-api source
const baseDir = join(process.cwd(), "node_modules/crypto-api/src");
/**
* Recursively walk a directory, updating import statements
* to include ".mjs" if missing
*/
function walk(dir) {
const entries = readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
if (entry.name === ".git") continue;
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
walk(fullPath);
} else if (entry.isFile()) {
const content = readFileSync(fullPath, "utf8");
// Add .mjs to imports if not present
const updated = content.replace(
/from "(\.[^"]*)";/g,
(match, p1) => {
if (p1.endsWith(".mjs")) return match;
return `from "${p1}.mjs";`;
}
);
if (updated !== content) {
writeFileSync(fullPath, updated, "utf8");
}
}
}
}
// Run the walker
walk(baseDir);

View File

@ -0,0 +1,28 @@
/**
* This script updates snackbarjs package
* Replaces self-closing div with standard opening div
*
* before:
* <div id=snackbar-container/>
* after:
* <div id=snackbar-container>
*
*/
/* eslint no-console: ["off"] */
import { readFileSync, writeFileSync } from "fs";
import { join } from "path";
// Base directory of snackbarjs source
const filePath = join(process.cwd(), "node_modules/snackbarjs/src/snackbar.js");
const content = readFileSync(filePath, "utf8");
// Replace self-closing div with standard opening div
const updated = content.replace(
/<div id=snackbar-container\/>/g,
"<div id=snackbar-container>"
);
writeFileSync(filePath, updated, "utf8");

103
src/core/lib/AudioBytes.mjs Normal file
View File

@ -0,0 +1,103 @@
/**
* Byte-reading and text-decoding utilities for audio metadata parsing.
*
* @author d0s1nt [d0s1nt@cyberchefaudio]
* @copyright Crown Copyright 2025
* @license Apache-2.0
*/
/** @returns {string} 4-byte ASCII at offset, or "" if out of bounds. */
export function ascii4(b, off) {
if (off + 4 > b.length) return "";
return String.fromCharCode(b[off], b[off + 1], b[off + 2], b[off + 3]);
}
/** @returns {number} Byte offset of ASCII needle `s`, or -1. */
export function indexOfAscii(b, s, start, end) {
const limit = Math.max(0, Math.min(end, b.length) - s.length);
for (let i = start; i <= limit; i++) {
let ok = true;
for (let j = 0; j < s.length; j++) {
if (b[i + j] !== s.charCodeAt(j)) {
ok = false;
break;
}
}
if (ok) return i;
}
return -1;
}
/** @returns {number} Unsigned 32-bit big-endian read. */
export function u32be(bytes, off) {
return ((bytes[off] << 24) >>> 0) | (bytes[off + 1] << 16) | (bytes[off + 2] << 8) | bytes[off + 3];
}
/** @returns {number} Unsigned 32-bit little-endian read. */
export function u32le(bytes, off) {
return (bytes[off] | (bytes[off + 1] << 8) | (bytes[off + 2] << 16) | (bytes[off + 3] << 24)) >>> 0;
}
/** @returns {number} Unsigned 16-bit little-endian read. */
export function u16le(bytes, off) {
return bytes[off] | (bytes[off + 1] << 8);
}
/** @returns {BigInt} Unsigned 64-bit little-endian read. */
export function u64le(bytes, off) {
return BigInt(u32le(bytes, off)) | (BigInt(u32le(bytes, off + 4)) << 32n);
}
/** @returns {number} Decoded ID3v2 synchsafe integer from four 7-bit bytes. */
export function synchsafeToInt(b0, b1, b2, b3) {
return ((b0 & 0x7f) << 21) | ((b1 & 0x7f) << 14) | ((b2 & 0x7f) << 7) | (b3 & 0x7f);
}
/** @returns {string} Decoded UTF-16LE byte range, nulls stripped. */
export function decodeUtf16LE(b, off, len) {
if (len <= 0 || off + len > b.length) return "";
try {
return new TextDecoder("utf-16le").decode(b.slice(off, off + len)).replace(/\u0000/g, "").trim();
} catch {
return "";
}
}
/** @returns {{valueBytes: Uint8Array, next: number}} Bytes until null terminator, UTF-16 aware. */
export function readNullTerminated(bytes, start, encoding) {
const isUtf16 = encoding === 1 || encoding === 2;
if (!isUtf16) {
let i = start;
while (i < bytes.length && bytes[i] !== 0x00) i++;
return { valueBytes: bytes.slice(start, i), next: i + 1 };
}
let i = start;
while (i + 1 < bytes.length && !(bytes[i] === 0x00 && bytes[i + 1] === 0x00)) i += 2;
return { valueBytes: bytes.slice(start, i), next: i + 2 };
}
const ID3_ENCODINGS = ["iso-8859-1", "utf-16", "utf-16be", "utf-8"];
/** @returns {string} Text decoded using ID3v2 encoding byte (0=latin1, 1=utf16, 2=utf16be, 3=utf8). */
export function decodeText(bytes, encoding) {
if (!bytes || bytes.length === 0) return "";
try {
return new TextDecoder(ID3_ENCODINGS[encoding] || "utf-16").decode(bytes);
} catch {
return safeUtf8(bytes);
}
}
/** @returns {string} UTF-8 decode with replacement (never throws). */
export function safeUtf8(bytes) {
try {
return new TextDecoder("utf-8", { fatal: false }).decode(bytes);
} catch {
return "";
}
}
/** @returns {string} ISO-8859-1 decode, nulls stripped, trimmed. */
export function decodeLatin1Trim(bytes) {
return decodeText(bytes, 0).replace(/\u0000/g, "").trim();
}

View File

@ -0,0 +1,82 @@
/**
* Report skeleton and container detection for audio metadata extraction.
*
* @author d0s1nt [d0s1nt@cyberchefaudio]
* @copyright Crown Copyright 2025
* @license Apache-2.0
*/
/* eslint-disable camelcase */
import { ascii4, indexOfAscii } from "./AudioBytes.mjs";
/** Builds the empty report skeleton ready for a format parser to populate. */
export function makeEmptyReport(filename, byteLength, container) {
return {
schema_version: "audio-meta-1.0",
artifact: {
filename,
byte_length: byteLength,
container: { type: container.type, brand: container.brand || null, mime: container.mime || null },
},
detections: { metadata_systems: [], provenance_systems: [] },
tags: {
common: {
title: null, artist: null, album: null, date: null, track: null,
genre: null, comment: null, composer: null, copyright: null, language: null,
},
raw: {},
},
embedded: [],
provenance: {
c2pa: {
present: false,
embedding: [],
manifest_store: { active_manifest_urn: null, instance_id: null, claim_generator: null },
assertions: [],
signature: {
algorithm: null, signing_time: null,
certificate: { subject_cn: null, issuer_cn: null, serial_number: null },
},
validation: { validation_state: "Unknown", reasons: [], details_raw: null },
},
},
errors: [],
};
}
/** Detects the audio container format from magic bytes. */
export function sniffContainer(b) {
if (b.length >= 3 && b[0] === 0x49 && b[1] === 0x44 && b[2] === 0x33)
return { type: "mp3", mime: "audio/mpeg" };
if (b.length >= 2 && b[0] === 0xff && (b[1] & 0xe0) === 0xe0) {
if ((b[1] & 0x06) === 0x00) return { type: "aac", mime: "audio/aac" };
return { type: "mp3", mime: "audio/mpeg" };
}
if (b.length >= 8 && b[0] === 0x0b && b[1] === 0x77)
return { type: "ac3", mime: "audio/ac3" };
if (b.length >= 16 &&
b[0] === 0x30 && b[1] === 0x26 && b[2] === 0xb2 && b[3] === 0x75 &&
b[4] === 0x8e && b[5] === 0x66 && b[6] === 0xcf && b[7] === 0x11)
return { type: "wma", mime: "audio/x-ms-wma" };
if (b.length >= 12 && ascii4(b, 0) === "RIFF" && ascii4(b, 8) === "WAVE")
return { type: "wav", mime: "audio/wav" };
if (b.length >= 12 && ascii4(b, 0) === "BW64" && ascii4(b, 8) === "WAVE")
return { type: "bw64", mime: "audio/wav" };
if (b.length >= 4 && ascii4(b, 0) === "fLaC")
return { type: "flac", mime: "audio/flac" };
if (b.length >= 4 && ascii4(b, 0) === "OggS") {
const idx = indexOfAscii(b, "OpusHead", 0, Math.min(b.length, 65536));
return idx >= 0 ? { type: "opus", mime: "audio/ogg" } : { type: "ogg", mime: "audio/ogg" };
}
if (b.length >= 12 && ascii4(b, 4) === "ftyp") {
const brand = ascii4(b, 8);
const isM4A = brand === "M4A " || brand === "M4B " || brand === "M4P ";
return { type: isM4A ? "m4a" : "mp4", mime: isM4A ? "audio/mp4" : "video/mp4", brand };
}
if (b.length >= 12 && ascii4(b, 0) === "FORM") {
const formType = ascii4(b, 8);
if (formType === "AIFF" || formType === "AIFC") return { type: "aiff", mime: "audio/aiff", brand: formType };
}
return { type: "unknown", mime: null };
}

View File

@ -0,0 +1,630 @@
/**
* Format-specific audio metadata parsers.
*
* @author d0s1nt [d0s1nt@cyberchefaudio]
* @copyright Crown Copyright 2025
* @license Apache-2.0
*/
/* eslint-disable camelcase */
import {
ascii4, indexOfAscii,
u32be, u32le, u16le, u64le, synchsafeToInt,
decodeUtf16LE, readNullTerminated, decodeText,
safeUtf8, decodeLatin1Trim,
} from "./AudioBytes.mjs";
/** Parses MP3 metadata: ID3v2 frames, ID3v1 footer, APEv2 tags. */
export function parseMp3(b, report) {
processId3v2(b, report);
processId3v1(b, report);
const ape = parseApeV2BestEffort(b);
if (ape) {
report.detections.metadata_systems.push("apev2");
report.tags.raw.apev2 = ape;
}
}
/** Iterates ID3v2 frames and populates the report. */
function processId3v2(b, report) {
report.detections.metadata_systems.push("id3v2");
const id3 = parseId3v2(b);
report.tags.raw.id3v2 = id3 ? { header: id3.header, frames: [] } : null;
if (id3) {
for (const f of id3.frames) {
const entry = { id: f.id, size: f.size, description: ID3_FRAME_DESCRIPTIONS[f.id] || null };
if (f.id[0] === "T" && f.id !== "TXXX") {
const text = f.data?.length >= 1 ?
decodeText(f.data.slice(1), f.data[0]).replace(/\u0000/g, "").trim() :
"";
entry.decoded = text;
if (f.id === "TLEN") {
const ms = normalizeTlen(text);
if (ms !== null) entry.normalized_ms = ms;
}
mapCommonId3(report, f.id, text);
} else if (f.id === "TXXX") {
const txxx = decodeTxxx(f.data);
entry.decoded = txxx;
if (!report.tags.raw.id3v2.txxx) report.tags.raw.id3v2.txxx = [];
report.tags.raw.id3v2.txxx.push(txxx);
} else if (f.id === "COMM") {
const comm = decodeCommFrame(f.data);
entry.decoded = comm;
if (comm?.text && !report.tags.common.comment) report.tags.common.comment = comm.text;
} else if (f.id === "GEOB") {
processGeobFrame(f, entry, report);
}
report.tags.raw.id3v2.frames.push(entry);
}
} else {
report.detections.metadata_systems = report.detections.metadata_systems.filter((x) => x !== "id3v2");
}
}
/** Parses GEOB frame contents, populates entry, embedded objects, and C2PA provenance. */
function processGeobFrame(f, entry, report) {
const d = f.data, enc = d[0];
let off = 1;
const mime = readNullTerminated(d, off, 0);
const mimeType = decodeLatin1Trim(mime.valueBytes);
off = mime.next;
const file = readNullTerminated(d, off, enc);
const filename = decodeText(file.valueBytes, enc).replace(/\u0000/g, "").trim();
off = file.next;
const desc = readNullTerminated(d, off, enc);
const description = decodeText(desc.valueBytes, enc).replace(/\u0000/g, "").trim();
off = desc.next;
const objLen = d.length - off;
entry.geob = { mimeType, filename, description, object_bytes: objLen };
const geobId = `geob_${report.embedded.filter((x) => x.source === "id3v2:GEOB").length}`;
report.embedded.push({
id: geobId, source: "id3v2:GEOB",
content_type: mimeType || null, byte_length: objLen,
description: description || null, filename: filename || null,
});
const mt = (mimeType || "").toLowerCase();
if (mt.includes("c2pa") || mt.includes("jumbf") || mt.includes("application/x-c2pa-manifest-store")) {
report.provenance.c2pa.present = true;
report.provenance.c2pa.embedding.push({
carrier: "id3v2:GEOB", content_type: mimeType || null, byte_length: objLen,
});
}
}
/** Processes the 128-byte ID3v1 footer tag. */
function processId3v1(b, report) {
const id3v1 = parseId3v1(b);
if (!id3v1) return;
report.detections.metadata_systems.push("id3v1");
report.tags.raw.id3v1 = id3v1;
mapCommon(report, id3v1, ID3V1_TO_COMMON);
}
/** Parses WAV/BWF/BW64 RIFF chunks: LIST/INFO, bext, iXML, axml, ds64. */
export function parseRiffWave(b, report, maxTextBytes) {
report.detections.metadata_systems.push("riff_info");
const chunks = enumerateChunks(b, 12, b.length, 50000);
const riff = { chunks: [], info: null, bext: null, ixml: null, axml: null, ds64: null };
const info = {};
for (const c of chunks) {
riff.chunks.push({ id: c.id, size: c.size, offset: c.dataOff });
processRiffChunk(b, c, riff, info, report, maxTextBytes);
}
riff.info = Object.keys(info).length ? info : null;
report.tags.raw.riff = riff;
if (riff.info) mapCommon(report, riff.info, RIFF_TO_COMMON);
}
/** Processes a single RIFF chunk, updating riff state and the report. */
function processRiffChunk(b, c, riff, info, report, maxTextBytes) {
if (c.id === "ds64") {
riff.ds64 = { present: true, size: c.size };
if (!report.detections.metadata_systems.includes("bw64_ds64")) report.detections.metadata_systems.push("bw64_ds64");
if (report.artifact.container.type === "wav") report.artifact.container.type = "bw64";
}
if (c.id === "LIST" && ascii4(b, c.dataOff) === "INFO") {
for (const s of enumerateChunks(b, c.dataOff + 4, c.dataOff + c.size, 10000))
info[s.id] = decodeLatin1Trim(b.slice(s.dataOff, s.dataOff + s.size));
}
if (c.id === "bext") {
if (!report.detections.metadata_systems.includes("bwf_bext")) report.detections.metadata_systems.push("bwf_bext");
riff.bext = parseBext(b, c.dataOff, c.size);
}
if (c.id === "iXML" || c.id === "axml") {
const key = c.id === "iXML" ? "ixml" : "axml";
if (!report.detections.metadata_systems.includes(key)) report.detections.metadata_systems.push(key);
const payload = b.slice(c.dataOff, c.dataOff + c.size);
riff[key] = { xml: safeUtf8(payload.slice(0, Math.min(payload.length, maxTextBytes))), truncated: payload.length > maxTextBytes };
report.embedded.push({
id: `${key}_0`, source: `riff:${c.id}`, content_type: "application/xml",
byte_length: payload.length, description: `${c.id} chunk`, filename: null,
});
}
}
/** Parses FLAC metablocks: STREAMINFO, Vorbis Comment, PICTURE. */
export function parseFlac(b, report, maxTextBytes) {
report.detections.metadata_systems.push("flac_metablocks");
const blocks = parseFlacMetaBlocks(b);
report.tags.raw.flac = { blocks: [] };
for (const blk of blocks) {
report.tags.raw.flac.blocks.push({ type: blk.typeName, length: blk.length });
if (blk.typeName === "VORBIS_COMMENT") {
if (!report.detections.metadata_systems.includes("vorbis_comments")) report.detections.metadata_systems.push("vorbis_comments");
const vc = parseVorbisComment(blk.data);
report.tags.raw.vorbis_comments = vc;
mapVorbisCommon(report, vc);
} else if (blk.typeName === "PICTURE") {
const pic = parseFlacPicture(blk.data, maxTextBytes);
report.embedded.push({
id: `cover_art_${report.embedded.filter((x) => x.id.startsWith("cover_art_")).length}`,
source: "flac:PICTURE", content_type: pic.mime || null,
byte_length: pic.dataLength, description: pic.description || null, filename: null,
});
}
}
}
/** Parses OGG/Opus Vorbis comments. */
export function parseOgg(b, report) {
if (!report.detections.metadata_systems.includes("ogg_opus_tags")) report.detections.metadata_systems.push("ogg_opus_tags");
const scanEnd = Math.min(b.length, 1024 * 1024);
let tags = null;
const opusTagsIdx = indexOfAscii(b, "OpusTags", 0, scanEnd);
if (opusTagsIdx >= 0) {
report.artifact.container.type = "opus";
tags = parseVorbisComment(b.slice(opusTagsIdx + 8, scanEnd));
} else {
const vorbisIdx = indexOfAscii(b, "\x03vorbis", 0, scanEnd);
if (vorbisIdx >= 0) tags = parseVorbisComment(b.slice(vorbisIdx + 7, scanEnd));
}
report.tags.raw.ogg = { has_opustags: opusTagsIdx >= 0, has_vorbis_comment: !!tags };
if (tags) {
if (!report.detections.metadata_systems.includes("vorbis_comments")) report.detections.metadata_systems.push("vorbis_comments");
report.tags.raw.vorbis_comments = tags;
mapVorbisCommon(report, tags);
}
}
/** Best-effort top-level atom scan for MP4/M4A. */
export function parseMp4BestEffort(b, report) {
report.detections.metadata_systems.push("mp4_atoms");
const atoms = [];
let off = 0;
while (off + 8 <= b.length && atoms.length < 2000) {
const size = u32be(b, off);
const type = ascii4(b, off + 4);
if (size < 8) break;
atoms.push({ type, size, offset: off });
off += size;
}
report.tags.raw.mp4 = {
top_level_atoms: atoms.slice(0, 200),
hints: {
hasMoov: atoms.some((a) => a.type === "moov"),
hasUdta: atoms.some((a) => a.type === "udta"),
hasMeta: atoms.some((a) => a.type === "meta"),
hasIlst: atoms.some((a) => a.type === "ilst"),
},
};
}
/** Best-effort AIFF/AIFC chunk scanning for NAME, AUTH, ANNO. */
export function parseAiffBestEffort(b, report, maxTextBytes) {
report.detections.metadata_systems.push("aiff_chunks");
let off = 12;
const chunks = [];
while (off + 8 <= b.length && chunks.length < 2000) {
const id = ascii4(b, off);
const size = u32be(b, off + 4);
const dataOff = off + 8;
chunks.push({ id, size, offset: off });
if (["NAME", "AUTH", "ANNO", "(c) "].includes(id)) {
const txt = safeUtf8(b.slice(dataOff, dataOff + Math.min(size, maxTextBytes)));
if (!report.tags.raw.aiff) report.tags.raw.aiff = { chunks: [] };
report.tags.raw.aiff.chunks.push({ id, value: txt, truncated: size > maxTextBytes });
}
off = dataOff + size + (size % 2);
}
if (!report.tags.raw.aiff) report.tags.raw.aiff = {};
report.tags.raw.aiff.chunk_index = chunks.slice(0, 500);
const nameChunk = report.tags.raw.aiff?.chunks?.find((ch) => ch.id === "NAME")?.value;
if (nameChunk) report.tags.common.title = report.tags.common.title || nameChunk;
}
const AAC_SAMPLE_RATES = [96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350];
const AAC_PROFILES = ["Main", "LC", "SSR", "LTP"];
const AAC_CHANNELS = ["defined in AOT", "mono", "stereo", "3.0", "4.0", "5.0", "5.1", "7.1"];
/** Parses AAC ADTS frame header for audio parameters. */
export function parseAacAdts(b, report) {
report.detections.metadata_systems.push("adts_header");
if (b.length < 7) return;
const id = (b[1] >> 3) & 0x01;
const profile = (b[2] >> 6) & 0x03;
const freqIdx = (b[2] >> 2) & 0x0f;
const chanCfg = ((b[2] & 0x01) << 2) | ((b[3] >> 6) & 0x03);
report.tags.raw.aac = {
mpeg_version: id === 1 ? "MPEG-2" : "MPEG-4",
profile: AAC_PROFILES[profile] || `Profile ${profile}`,
sample_rate: AAC_SAMPLE_RATES[freqIdx] || null,
sample_rate_index: freqIdx,
channel_configuration: chanCfg,
channel_description: AAC_CHANNELS[chanCfg] || null,
};
}
const AC3_SAMPLE_RATES = [48000, 44100, 32000];
const AC3_BITRATES = [32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 448, 512, 576, 640];
const AC3_ACMODES = [
"2.0 (Ch1+Ch2)", "1.0 (C)", "2.0 (L R)", "3.0 (L C R)",
"2.1 (L R S)", "3.1 (L C R S)", "2.2 (L R SL SR)", "3.2 (L C R SL SR)",
];
/** Parses AC3 (Dolby Digital) bitstream info. */
export function parseAc3(b, report) {
report.detections.metadata_systems.push("ac3_bsi");
if (b.length < 8) return;
const fscod = (b[4] >> 6) & 0x03;
const frmsizecod = b[4] & 0x3f;
const bsid = (b[5] >> 3) & 0x1f;
const bsmod = b[5] & 0x07;
const acmod = (b[6] >> 5) & 0x07;
report.tags.raw.ac3 = {
sample_rate: AC3_SAMPLE_RATES[fscod] || null,
fscod,
bitrate_kbps: AC3_BITRATES[frmsizecod >> 1] || null,
frmsizecod, bsid, bsmod, acmod,
channel_layout: AC3_ACMODES[acmod] || null,
};
}
/** Parses WMA files (ASF container) for content description metadata. */
export function parseWmaAsf(b, report) {
report.detections.metadata_systems.push("asf_header");
if (b.length < 30) return;
const headerSize = Number(u64le(b, 16));
const numObjects = u32le(b, 24);
const headerEnd = Math.min(b.length, headerSize);
const objects = [];
let off = 30;
for (let i = 0; i < numObjects && off + 24 <= headerEnd; i++) {
const guid4 = [b[off], b[off + 1], b[off + 2], b[off + 3]];
const objSize = Number(u64le(b, off + 16));
if (objSize < 24 || off + objSize > headerEnd) break;
const dataOff = off + 24;
const dataLen = objSize - 24;
if (guid4[0] === 0x33 && guid4[1] === 0x26 && guid4[2] === 0xb2 && guid4[3] === 0x75 && dataLen >= 10) {
const cd = parseAsfContentDescription(b, dataOff);
if (!report.detections.metadata_systems.includes("asf_content_desc"))
report.detections.metadata_systems.push("asf_content_desc");
if (!report.tags.raw.asf) report.tags.raw.asf = {};
report.tags.raw.asf.content_description = cd;
mapCommon(report, cd, ASF_CD_TO_COMMON);
}
if (guid4[0] === 0x40 && guid4[1] === 0xa4 && guid4[2] === 0xd0 && guid4[3] === 0xd2 && dataLen >= 2) {
const ext = parseAsfExtContentDescription(b, dataOff, dataOff + dataLen);
if (!report.detections.metadata_systems.includes("asf_ext_content_desc"))
report.detections.metadata_systems.push("asf_ext_content_desc");
if (!report.tags.raw.asf) report.tags.raw.asf = {};
report.tags.raw.asf.extended_content = ext;
const c = report.tags.common;
for (const d of ext) {
const field = WMA_TO_COMMON[(d.name || "").toUpperCase()];
if (field && d.value) c[field] = c[field] || d.value;
}
}
objects.push({ guid_prefix: guid4.map(x => x.toString(16).padStart(2, "0")).join(""), size: objSize });
off += objSize;
}
if (!report.tags.raw.asf) report.tags.raw.asf = {};
report.tags.raw.asf.header_objects = objects;
}
const ID3_FRAME_DESCRIPTIONS = {
TIT2: "Title/songname/content description", TPE1: "Lead performer(s)/Soloist(s)",
TRCK: "Track number/Position in set", TALB: "Album/Movie/Show title",
TDRC: "Recording time", TYER: "Year", TCON: "Content type",
TPE2: "Band/orchestra/accompaniment", TLEN: "Length (ms)", TCOM: "Composer",
COMM: "Comments", APIC: "Attached picture", GEOB: "General encapsulated object",
TXXX: "User defined text information frame", UFID: "Unique file identifier", PRIV: "Private frame",
};
const ID3_TO_COMMON = {
TIT2: "title", TPE1: "artist", TALB: "album", TDRC: "date", TYER: "date",
TRCK: "track", TCON: "genre", COMM: "comment", TCOM: "composer", TCOP: "copyright", TLAN: "language",
};
const VORBIS_TO_COMMON = {
TITLE: "title", ARTIST: "artist", ALBUM: "album", DATE: "date",
TRACKNUMBER: "track", GENRE: "genre", COMMENT: "comment", COMPOSER: "composer", LANGUAGE: "language",
};
const WMA_TO_COMMON = {
"WM/ALBUMTITLE": "album", "WM/GENRE": "genre", "WM/YEAR": "date",
"WM/TRACKNUMBER": "track", "WM/COMPOSER": "composer", "WM/LANGUAGE": "language",
};
const ID3V1_TO_COMMON = { title: "title", artist: "artist", album: "album", year: "date", comment: "comment", genre: "genre", track: "track" };
const RIFF_TO_COMMON = { INAM: "title", IART: "artist", ICMT: "comment", IGNR: "genre", ICRD: "date", ICOP: "copyright" };
const ASF_CD_TO_COMMON = { title: "title", author: "artist", copyright: "copyright", description: "comment" };
/** Maps source object fields to the common tags layer via a mapping table. */
function mapCommon(report, source, mapping) {
const c = report.tags.common;
for (const [sk, ck] of Object.entries(mapping))
c[ck] = c[ck] || source[sk] || null;
}
/** Maps an ID3v2 frame value to the common tags layer. */
function mapCommonId3(report, frameId, text) {
const field = ID3_TO_COMMON[frameId];
if (field) report.tags.common[field] = report.tags.common[field] || text || null;
}
/** Decodes an ID3v2 COMM (Comments) frame. */
function decodeCommFrame(data) {
if (!data || data.length < 5) return null;
const enc = data[0];
const language = String.fromCharCode(data[1], data[2], data[3]);
const { valueBytes: descBytes, next } = readNullTerminated(data, 4, enc);
const short_description = decodeText(descBytes, enc).replace(/\u0000/g, "").trim() || null;
const text = decodeText(data.slice(next), enc).replace(/\u0000/g, "").trim() || null;
return { language, short_description, text };
}
/** Normalizes TLEN to integer milliseconds. */
function normalizeTlen(s) {
if (!s) return null;
if (/^\s*\d+\s*$/.test(s)) return parseInt(s.trim(), 10);
const f = Number(s);
if (Number.isFinite(f) && f > 0 && f < 100000) return Math.round(f * 1000);
return null;
}
/** Parses the ID3v2 tag header and frames. */
function parseId3v2(mp3) {
if (mp3.length < 10 || mp3[0] !== 0x49 || mp3[1] !== 0x44 || mp3[2] !== 0x33) return null;
const major = mp3[3], minor = mp3[4], flags = mp3[5];
const tagSize = synchsafeToInt(mp3[6], mp3[7], mp3[8], mp3[9]);
let offset = 10;
const end = 10 + tagSize;
const frames = [];
while (offset + 10 <= end) {
const id = String.fromCharCode(mp3[offset], mp3[offset + 1], mp3[offset + 2], mp3[offset + 3]);
if (!/^[A-Z0-9]{4}$/.test(id)) break;
const size = major === 4 ?
synchsafeToInt(mp3[offset + 4], mp3[offset + 5], mp3[offset + 6], mp3[offset + 7]) :
u32be(mp3, offset + 4);
offset += 10;
if (size <= 0 || offset + size > mp3.length) break;
frames.push({ id, size, data: mp3.slice(offset, offset + size) });
offset += size;
}
return { header: { version: `${major}.${minor}`, flags, tag_size: tagSize }, frames };
}
/** Parses the 128-byte ID3v1 tag at the end of the file. */
function parseId3v1(b) {
if (b.length < 128) return null;
const off = b.length - 128;
if (b[off] !== 0x54 || b[off + 1] !== 0x41 || b[off + 2] !== 0x47) return null;
let track = null;
if (b[off + 125] === 0x00 && b[off + 126] !== 0x00) track = String(b[off + 126]);
return {
title: decodeLatin1Trim(b.slice(off + 3, off + 33)),
artist: decodeLatin1Trim(b.slice(off + 33, off + 63)),
album: decodeLatin1Trim(b.slice(off + 63, off + 93)),
year: decodeLatin1Trim(b.slice(off + 93, off + 97)),
comment: decodeLatin1Trim(b.slice(off + 97, off + 127)),
track, genre: String(b[off + 127]),
};
}
/** Decodes an ID3v2 TXXX (user-defined text) frame. */
function decodeTxxx(data) {
if (!data || data.length < 2) return null;
const enc = data[0];
const { valueBytes: descBytes, next } = readNullTerminated(data, 1, enc);
const desc = decodeText(descBytes, enc).replace(/\u0000/g, "").trim();
const val = decodeText(data.slice(next), enc).replace(/\u0000/g, "").trim();
return { description: desc || null, value: val || null };
}
/** Best-effort APEv2 tag parser scanning the last 32 KB. */
function parseApeV2BestEffort(b) {
const scanStart = Math.max(0, b.length - 32768);
const idx = indexOfAscii(b, "APETAGEX", scanStart, b.length);
if (idx < 0) return null;
if (idx + 32 > b.length) return { present: true, warning: "APETAGEX found but footer truncated." };
const ver = u32le(b, idx + 8), size = u32le(b, idx + 12);
const count = u32le(b, idx + 16), flags = u32le(b, idx + 20);
const tagStart = idx + 32 - size;
if (tagStart < 0 || tagStart >= b.length)
return { present: true, version: ver, size, count, flags, warning: "APEv2 bounds invalid (non-standard placement)." };
const items = [];
let off = tagStart + 32;
const end = Math.min(b.length, idx);
while (off + 8 < end && items.length < 5000) {
const valueSize = u32le(b, off), itemFlags = u32le(b, off + 4);
off += 8;
let keyEnd = off;
while (keyEnd < end && b[keyEnd] !== 0x00) keyEnd++;
const key = decodeLatin1Trim(b.slice(off, keyEnd));
off = keyEnd + 1;
if (!key || off + valueSize > end) break;
const value = safeUtf8(b.slice(off, off + valueSize)).replace(/\u0000/g, "").trim();
off += valueSize;
items.push({ key, value, flags: itemFlags });
}
return { present: true, version: ver, size, count, flags, items };
}
/** Enumerates RIFF-style chunks (id + LE32 size) within a byte range, padding to even. */
function enumerateChunks(b, start, end, maxCount) {
const chunks = [];
let off = start;
while (off + 8 <= end && chunks.length < maxCount) {
const id = ascii4(b, off);
const size = u32le(b, off + 4);
const dataOff = off + 8;
if (dataOff + size > end) break;
chunks.push({ id, size, dataOff });
off = dataOff + size + (size % 2);
}
return chunks;
}
/** Parses a BWF bext chunk. */
function parseBext(b, off, size) {
const slice = b.slice(off, off + size);
const timeRefLow = u32le(slice, 338), timeRefHigh = u32le(slice, 342);
return {
description: decodeLatin1Trim(slice.slice(0, 256)) || null,
originator: decodeLatin1Trim(slice.slice(256, 288)) || null,
originator_reference: decodeLatin1Trim(slice.slice(288, 320)) || null,
origination_date: decodeLatin1Trim(slice.slice(320, 330)) || null,
origination_time: decodeLatin1Trim(slice.slice(330, 338)) || null,
time_reference_samples: ((BigInt(timeRefHigh) << 32n) | BigInt(timeRefLow)).toString(),
};
}
const FLAC_TYPE_NAMES = { 0: "STREAMINFO", 1: "PADDING", 2: "APPLICATION", 3: "SEEKTABLE", 4: "VORBIS_COMMENT", 5: "CUESHEET", 6: "PICTURE" };
/** Parses FLAC metadata blocks following the "fLaC" marker. */
function parseFlacMetaBlocks(b) {
const blocks = [];
let off = 4;
while (off + 4 <= b.length && blocks.length < 10000) {
const header = b[off];
const isLast = (header & 0x80) !== 0;
const type = header & 0x7f;
const len = (b[off + 1] << 16) | (b[off + 2] << 8) | b[off + 3];
off += 4;
if (off + len > b.length) break;
blocks.push({ type, typeName: FLAC_TYPE_NAMES[type] || `TYPE_${type}`, length: len, data: b.slice(off, off + len) });
off += len;
if (isLast) break;
}
return blocks;
}
/** Parses a Vorbis Comment block (used by FLAC and OGG). */
function parseVorbisComment(buf) {
let off = 0;
const vendorLen = u32le(buf, off); off += 4;
if (off + vendorLen > buf.length) return { vendor: null, comments: [], warning: "vendor_len out of bounds" };
const vendor = safeUtf8(buf.slice(off, off + vendorLen)); off += vendorLen;
const count = u32le(buf, off); off += 4;
const comments = [];
for (let i = 0; i < count && off + 4 <= buf.length && comments.length < 20000; i++) {
const l = u32le(buf, off); off += 4;
if (off + l > buf.length) break;
const s = safeUtf8(buf.slice(off, off + l)); off += l;
const eq = s.indexOf("=");
if (eq > 0) comments.push({ key: s.slice(0, eq).toUpperCase(), value: s.slice(eq + 1) });
}
return { vendor, comments };
}
/** Maps Vorbis Comment fields to the common tags layer. */
function mapVorbisCommon(report, vc) {
const c = report.tags.common;
for (const [vk, ck] of Object.entries(VORBIS_TO_COMMON))
c[ck] = c[ck] || vc.comments?.find((x) => x.key === vk)?.value || null;
}
/** Parses a FLAC PICTURE metadata block (extracts mime, description, data length). */
function parseFlacPicture(data, maxTextBytes) {
let off = 4;
const mimeLen = u32be(data, off); off += 4;
const mime = safeUtf8(data.slice(off, off + Math.min(mimeLen, maxTextBytes))); off += mimeLen;
const descLen = u32be(data, off); off += 4;
const description = safeUtf8(data.slice(off, off + Math.min(descLen, maxTextBytes))); off += descLen + 16;
return { mime, description, dataLength: u32be(data, off) };
}
/** Parses the ASF Content Description Object fields. */
function parseAsfContentDescription(b, off) {
const titleLen = u16le(b, off), authorLen = u16le(b, off + 2);
const copyrightLen = u16le(b, off + 4), descLen = u16le(b, off + 6), ratingLen = u16le(b, off + 8);
let pos = off + 10;
const title = decodeUtf16LE(b, pos, titleLen); pos += titleLen;
const author = decodeUtf16LE(b, pos, authorLen); pos += authorLen;
const copyright = decodeUtf16LE(b, pos, copyrightLen); pos += copyrightLen;
const description = decodeUtf16LE(b, pos, descLen); pos += descLen;
const rating = decodeUtf16LE(b, pos, ratingLen);
return { title, author, copyright, description, rating };
}
/** Parses the ASF Extended Content Description Object descriptors. */
function parseAsfExtContentDescription(b, off, end) {
const count = u16le(b, off);
let pos = off + 2;
const descriptors = [];
for (let i = 0; i < count && pos + 6 <= end && descriptors.length < 5000; i++) {
const nameLen = u16le(b, pos); pos += 2;
if (pos + nameLen > end) break;
const name = decodeUtf16LE(b, pos, nameLen); pos += nameLen;
const valueType = u16le(b, pos); pos += 2;
const valueLen = u16le(b, pos); pos += 2;
if (pos + valueLen > end) break;
let value;
if (valueType === 0) value = decodeUtf16LE(b, pos, valueLen);
else if (valueType === 3) value = u32le(b, pos);
else if (valueType === 5) value = u16le(b, pos);
else if (valueType === 2) value = u32le(b, pos) !== 0;
else value = `(${valueLen} bytes, type ${valueType})`;
pos += valueLen;
descriptors.push({ name, value_type: valueType, value });
}
return descriptors;
}

View File

@ -45,6 +45,11 @@ export function search(input, searchRegex, removeRegex=null, sortBy=null, unique
return results;
}
/**
* Email regular expression
*/
export const EMAIL_REGEX = /(?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9](?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9-]*[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9])?\.)+[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9](?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9-]*[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\])/ig;
/**
* URL regular expression

View File

@ -1,4 +1,4 @@
import OperationConfig from "../config/OperationConfig.json" assert {type: "json"};
import OperationConfig from "../config/OperationConfig.json" with { type: "json" };
import Utils, { isWorkerEnvironment } from "../Utils.mjs";
import Recipe from "../Recipe.mjs";
import Dish from "../Dish.mjs";

View File

@ -50,6 +50,7 @@ const HEX_ALPHABET_MAP = HEX_ALPHABET.split("");
export function toModhex(data, delim=" ", padding=2, extraDelim="", lineSize=0) {
if (!data) return "";
if (data instanceof ArrayBuffer) data = new Uint8Array(data);
if (data.length === 0) return "";
const regularHexString = toHex(data, "", padding, "", 0);
@ -100,6 +101,7 @@ export function toModhex(data, delim=" ", padding=2, extraDelim="", lineSize=0)
export function toModhexFast(data) {
if (!data) return "";
if (data instanceof ArrayBuffer) data = new Uint8Array(data);
if (data.length === 0) return "";
const output = [];

View File

@ -0,0 +1,50 @@
/**
* Parity Bit functions.
*
* @author j83305 [awz22@protonmail.com]
* @copyright Crown Copyright 2020
* @license Apache-2.0
*
*/
import OperationError from "../errors/OperationError.mjs";
/**
* Function to take the user input and encode using the given arguments
* @param input string of binary
* @param args array
*/
export function calculateParityBit(input, args) {
let count1s = 0;
for (let i = 0; i < input.length; i++) {
const character = input.charAt(i);
if (character === "1") {
count1s++;
} else if (character !== args[3] && character !== "0" && character !== " ") {
throw new OperationError("unexpected character encountered: \"" + character + "\"");
}
}
let parityBit = "1";
const flipflop = args[0] === "Even Parity" ? 0 : 1;
if (count1s % 2 === flipflop) {
parityBit = "0";
}
if (args[1] === "End") {
return input + parityBit;
} else {
return parityBit + input;
}
}
/**
* just removes the parity bit to return the original data
* @param input string of binary, encoded
* @param args array
*/
export function decodeParityBit(input, args) {
if (args[1] === "End") {
return input.slice(0, -1);
} else {
return input.slice(1);
}
}

625
src/core/lib/RC6.mjs Normal file
View File

@ -0,0 +1,625 @@
/**
* Complete implementation of RC6 block cipher encryption/decryption with
* configurable word size (w), rounds (r), and key length (b).
*
* RC6 was an AES finalist designed by Ron Rivest, Matt Robshaw, Ray Sidney, and Yiqun Lisa Yin.
* Reference: https://en.wikipedia.org/wiki/RC6
* Test Vectors: https://datatracker.ietf.org/doc/html/draft-krovetz-rc6-rc5-vectors-00
*
* The P and Q constants are derived from mathematical constants e (Euler's number) and
* φ (golden ratio) as specified in the IETF draft. Master 256-bit values are scaled to
* any word size.
*
* @author Medjedtxm
* @copyright Crown Copyright 2026
* @license Apache-2.0
*/
import OperationError from "../errors/OperationError.mjs";
/**
* Master P constant (256-bit) from IETF draft-krovetz-rc6-rc5-vectors-00
* Derived from Odd((e-2) * 2^256) where e = 2.71828...
*/
const P_256 = 0xb7e151628aed2a6abf7158809cf4f3c762e7160f38b4da56a784d9045190cfefn;
/**
* Master Q constant (256-bit) from IETF draft-krovetz-rc6-rc5-vectors-00
* Derived from Odd((φ-1) * 2^256) where φ = 1.61803... (golden ratio)
*/
const Q_256 = 0x9e3779b97f4a7c15f39cc0605cedc8341082276bf3a27251f86c6a11d0c18e95n;
/**
* Get P constant for given word size by scaling the 256-bit master constant
* @param {number} w - Word size in bits
* @returns {bigint} - P constant for word size w
*/
function getP(w) {
return (P_256 >> BigInt(256 - w)) | 1n; // Ensure odd
}
/**
* Get Q constant for given word size by scaling the 256-bit master constant
* @param {number} w - Word size in bits
* @returns {bigint} - Q constant for word size w
*/
function getQ(w) {
return (Q_256 >> BigInt(256 - w)) | 1n; // Ensure odd
}
/**
* Get block size in bytes for given word size
* Block size = 4 words = 4 * (w/8) bytes
* @param {number} w - Word size in bits
* @returns {number} - Block size in bytes
*/
export function getBlockSize(w) {
return 4 * (w / 8);
}
/**
* Get recommended number of rounds for given word size
* @param {number} w - Word size in bits
* @returns {number} - Recommended rounds
*/
export function getDefaultRounds(w) {
if (w <= 16) return 16;
if (w <= 32) return 20;
if (w <= 64) return 24;
return 28;
}
/**
* Create mask for w-bit word
* @param {number} w - Word size in bits
* @returns {bigint} - Mask with w bits set
*/
function wordMask(w) {
return (1n << BigInt(w)) - 1n;
}
/**
* Rotate left for arbitrary word size using BigInt
* Uses lower lg(w) bits of n for rotation amount (RC6 spec)
* @param {bigint} x - Value to rotate
* @param {bigint} n - Rotation amount
* @param {number} w - Word size in bits
* @param {bigint} lgMask - Mask for lower lg(w) bits
* @returns {bigint} - Rotated value
*/
function ROL(x, n, w, lgMask) {
const mask = wordMask(w);
// Mask to lg(w) bits, then mod w for non-power-of-2 word sizes
// For power-of-2, (n & lgMask) < w always, so mod w is no-op
const shift = (n & lgMask) % BigInt(w);
return ((x << shift) | (x >> (BigInt(w) - shift))) & mask;
}
/**
* Rotate right for arbitrary word size using BigInt
* Uses lower lg(w) bits of n for rotation amount (RC6 spec)
* @param {bigint} x - Value to rotate
* @param {bigint} n - Rotation amount
* @param {number} w - Word size in bits
* @param {bigint} lgMask - Mask for lower lg(w) bits
* @returns {bigint} - Rotated value
*/
function ROR(x, n, w, lgMask) {
const mask = wordMask(w);
// Mask to lg(w) bits, then mod w for non-power-of-2 word sizes
// For power-of-2, (n & lgMask) < w always, so mod w is no-op
const shift = (n & lgMask) % BigInt(w);
return ((x >> shift) | (x << (BigInt(w) - shift))) & mask;
}
/**
* Convert byte array to word array (little-endian) using BigInt
* @param {number[]} bytes - Input byte array
* @param {number} w - Word size in bits
* @returns {bigint[]} - Array of w-bit words as BigInt
*/
function bytesToWords(bytes, w) {
const bytesPerWord = w / 8;
const words = [];
for (let i = 0; i < bytes.length; i += bytesPerWord) {
let word = 0n;
for (let j = 0; j < bytesPerWord && (i + j) < bytes.length; j++) {
word |= BigInt(bytes[i + j] || 0) << BigInt(j * 8);
}
words.push(word);
}
return words;
}
/**
* Convert word array to byte array (little-endian) using BigInt
* @param {bigint[]} words - Array of words
* @param {number} w - Word size in bits
* @returns {number[]} - Output byte array
*/
function wordsToBytes(words, w) {
const bytesPerWord = w / 8;
const bytes = [];
for (const word of words) {
for (let j = 0; j < bytesPerWord; j++) {
bytes.push(Number((word >> BigInt(j * 8)) & 0xFFn));
}
}
return bytes;
}
/**
* Generate round subkeys from user key
*
* @param {number[]} key - User key as byte array
* @param {number} rounds - Number of rounds
* @param {number} w - Word size in bits
* @returns {bigint[]} - Array of 2r+4 subkeys as BigInt
*/
function generateSubkeys(key, rounds, w) {
const bytesPerWord = w / 8;
const b = key.length;
const c = Math.max(Math.ceil(b / bytesPerWord), 1);
// Convert key bytes to words, pad with zeros if needed
const paddedKey = [...key];
while (paddedKey.length < c * bytesPerWord) {
paddedKey.push(0);
}
const L = bytesToWords(paddedKey, w);
// Number of subkeys: 2*r + 4
const t = 2 * rounds + 4;
// Get P and Q for this word size
const P = getP(w);
const Q = getQ(w);
const mask = wordMask(w);
// lg(w) mask for rotation amounts (floor of log2(w), per RC6 spec)
const lgw = Math.floor(Math.log2(w));
const lgMask = (1n << BigInt(lgw)) - 1n;
// Initialise S array with magic constants
const S = new Array(t);
S[0] = P;
for (let i = 1; i < t; i++) {
S[i] = (S[i - 1] + Q) & mask;
}
// Mix key into S
let A = 0n, B = 0n;
let i = 0, j = 0;
const v = 3 * Math.max(c, t);
for (let s = 0; s < v; s++) {
A = S[i] = ROL((S[i] + A + B) & mask, 3n, w, lgMask);
B = L[j] = ROL((L[j] + A + B) & mask, A + B, w, lgMask);
i = (i + 1) % t;
j = (j + 1) % c;
}
return S;
}
/**
* Encrypt a single block using RC6
*
* @param {number[]} block - Plaintext block (4*w/8 bytes)
* @param {bigint[]} S - Subkeys array
* @param {number} rounds - Number of rounds
* @param {number} w - Word size in bits
* @returns {number[]} - Ciphertext block
*/
function encryptBlock(block, S, rounds, w) {
const mask = wordMask(w);
const lgw = BigInt(Math.floor(Math.log2(w)));
const lgMask = (1n << lgw) - 1n;
// Convert block to 4 words (A, B, C, D)
let [A, B, C, D] = bytesToWords(block, w);
// Pre-whitening
B = (B + S[0]) & mask;
D = (D + S[1]) & mask;
// Main rounds
for (let i = 1; i <= rounds; i++) {
// t = ROL(B * (2B + 1), lg(w))
const t = ROL((B * ((2n * B + 1n) & mask)) & mask, lgw, w, lgMask);
// u = ROL(D * (2D + 1), lg(w))
const u = ROL((D * ((2n * D + 1n) & mask)) & mask, lgw, w, lgMask);
// A = ROL(A ^ t, u) + S[2i]
A = (ROL(A ^ t, u, w, lgMask) + S[2 * i]) & mask;
// C = ROL(C ^ u, t) + S[2i + 1]
C = (ROL(C ^ u, t, w, lgMask) + S[2 * i + 1]) & mask;
// Rotate registers: (A, B, C, D) = (B, C, D, A)
const temp = A;
A = B;
B = C;
C = D;
D = temp;
}
// Post-whitening
A = (A + S[2 * rounds + 2]) & mask;
C = (C + S[2 * rounds + 3]) & mask;
// Convert words back to bytes
return wordsToBytes([A, B, C, D], w);
}
/**
* Decrypt a single block using RC6
*
* @param {number[]} block - Ciphertext block (4*w/8 bytes)
* @param {bigint[]} S - Subkeys array
* @param {number} rounds - Number of rounds
* @param {number} w - Word size in bits
* @returns {number[]} - Plaintext block
*/
function decryptBlock(block, S, rounds, w) {
const mask = wordMask(w);
const lgw = BigInt(Math.floor(Math.log2(w)));
const lgMask = (1n << lgw) - 1n;
// Convert block to 4 words (A, B, C, D)
let [A, B, C, D] = bytesToWords(block, w);
// Reverse post-whitening
C = (C - S[2 * rounds + 3] + (1n << BigInt(w))) & mask;
A = (A - S[2 * rounds + 2] + (1n << BigInt(w))) & mask;
// Main rounds in reverse
for (let i = rounds; i >= 1; i--) {
// Reverse rotate registers: (A, B, C, D) = (D, A, B, C)
const temp = D;
D = C;
C = B;
B = A;
A = temp;
// u = ROL(D * (2D + 1), lg(w))
const u = ROL((D * ((2n * D + 1n) & mask)) & mask, lgw, w, lgMask);
// t = ROL(B * (2B + 1), lg(w))
const t = ROL((B * ((2n * B + 1n) & mask)) & mask, lgw, w, lgMask);
// C = ROR(C - S[2i + 1], t) ^ u
C = ROR((C - S[2 * i + 1] + (1n << BigInt(w))) & mask, t, w, lgMask) ^ u;
// A = ROR(A - S[2i], u) ^ t
A = ROR((A - S[2 * i] + (1n << BigInt(w))) & mask, u, w, lgMask) ^ t;
}
// Reverse pre-whitening
D = (D - S[1] + (1n << BigInt(w))) & mask;
B = (B - S[0] + (1n << BigInt(w))) & mask;
// Convert words back to bytes
return wordsToBytes([A, B, C, D], w);
}
/**
* XOR two blocks
* @param {number[]} a - First block
* @param {number[]} b - Second block
* @returns {number[]} - XOR result
*/
function xorBlocks(a, b) {
const result = new Array(a.length);
for (let i = 0; i < a.length; i++) {
result[i] = a[i] ^ b[i];
}
return result;
}
/**
* Increment counter (little-endian)
* @param {number[]} counter - Counter block
* @returns {number[]} - Incremented counter
*/
function incrementCounter(counter) {
const result = [...counter];
for (let i = 0; i < result.length; i++) {
result[i]++;
if (result[i] <= 255) break;
result[i] = 0;
}
return result;
}
/**
* Apply padding to message
* @param {number[]} message - Original message
* @param {string} padding - Padding type ("NO", "PKCS5", "ZERO", "RANDOM", "BIT")
* @param {number} blockSize - Block size in bytes
* @returns {number[]} - Padded message
*/
function applyPadding(message, padding, blockSize) {
const remainder = message.length % blockSize;
let nPadding = remainder === 0 ? 0 : blockSize - remainder;
// For PKCS5, always add at least one byte (full block if already aligned)
if (padding === "PKCS5" && remainder === 0) {
nPadding = blockSize;
}
if (nPadding === 0) return [...message];
const paddedMessage = [...message];
switch (padding) {
case "NO":
throw new OperationError(`No padding requested but input is not a ${blockSize}-byte multiple.`);
case "PKCS5":
for (let i = 0; i < nPadding; i++) {
paddedMessage.push(nPadding);
}
break;
case "ZERO":
for (let i = 0; i < nPadding; i++) {
paddedMessage.push(0);
}
break;
case "RANDOM":
for (let i = 0; i < nPadding; i++) {
paddedMessage.push(Math.floor(Math.random() * 256));
}
break;
case "BIT":
paddedMessage.push(0x80);
for (let i = 1; i < nPadding; i++) {
paddedMessage.push(0);
}
break;
default:
throw new OperationError(`Unknown padding type: ${padding}`);
}
return paddedMessage;
}
/**
* Remove padding from message
* @param {number[]} message - Padded message
* @param {string} padding - Padding type ("NO", "PKCS5", "ZERO", "RANDOM", "BIT")
* @param {number} blockSize - Block size in bytes
* @returns {number[]} - Unpadded message
*/
function removePadding(message, padding, blockSize) {
if (message.length === 0) return message;
switch (padding) {
case "NO":
case "ZERO":
case "RANDOM":
// These padding types cannot be reliably removed
return message;
case "PKCS5": {
const padByte = message[message.length - 1];
if (padByte > 0 && padByte <= blockSize) {
// Verify padding
for (let i = 0; i < padByte; i++) {
if (message[message.length - 1 - i] !== padByte) {
throw new OperationError("Invalid PKCS#5 padding.");
}
}
return message.slice(0, message.length - padByte);
}
throw new OperationError("Invalid PKCS#5 padding.");
}
case "BIT": {
// Find 0x80 byte working backwards, skipping zeros
for (let i = message.length - 1; i >= 0; i--) {
if (message[i] === 0x80) {
return message.slice(0, i);
} else if (message[i] !== 0) {
throw new OperationError("Invalid BIT padding.");
}
}
throw new OperationError("Invalid BIT padding.");
}
default:
throw new OperationError(`Unknown padding type: ${padding}`);
}
}
/**
* Encrypt using RC6 cipher with specified block mode
*
* @param {number[]} message - Plaintext as byte array
* @param {number[]} key - Key as byte array
* @param {number[]} iv - IV (block size bytes, not used for ECB)
* @param {string} mode - Block cipher mode ("ECB", "CBC", "CFB", "OFB", "CTR")
* @param {string} padding - Padding type ("NO", "PKCS5", "ZERO", "RANDOM", "BIT")
* @param {number} rounds - Number of rounds (default: 20)
* @param {number} w - Word size in bits (default: 32)
* @returns {number[]} - Ciphertext as byte array
*/
export function encryptRC6(message, key, iv, mode = "ECB", padding = "PKCS5", rounds = 20, w = 32) {
const blockSize = getBlockSize(w);
const messageLength = message.length;
if (messageLength === 0) return [];
const S = generateSubkeys(key, rounds, w);
// Apply padding for ECB/CBC modes
let paddedMessage;
if (mode === "ECB" || mode === "CBC") {
paddedMessage = applyPadding(message, padding, blockSize);
} else {
// Stream modes (CFB, OFB, CTR) don't need padding
paddedMessage = [...message];
}
const cipherText = [];
switch (mode) {
case "ECB":
for (let i = 0; i < paddedMessage.length; i += blockSize) {
const block = paddedMessage.slice(i, i + blockSize);
cipherText.push(...encryptBlock(block, S, rounds, w));
}
break;
case "CBC": {
let ivBlock = [...iv];
for (let i = 0; i < paddedMessage.length; i += blockSize) {
const block = paddedMessage.slice(i, i + blockSize);
const xored = xorBlocks(block, ivBlock);
ivBlock = encryptBlock(xored, S, rounds, w);
cipherText.push(...ivBlock);
}
break;
}
case "CFB": {
let ivBlock = [...iv];
for (let i = 0; i < paddedMessage.length; i += blockSize) {
const encrypted = encryptBlock(ivBlock, S, rounds, w);
const block = paddedMessage.slice(i, i + blockSize);
// Pad block if shorter than blockSize
while (block.length < blockSize) block.push(0);
ivBlock = xorBlocks(encrypted, block);
cipherText.push(...ivBlock);
}
return cipherText.slice(0, messageLength);
}
case "OFB": {
let ivBlock = [...iv];
for (let i = 0; i < paddedMessage.length; i += blockSize) {
ivBlock = encryptBlock(ivBlock, S, rounds, w);
const block = paddedMessage.slice(i, i + blockSize);
// Pad block if shorter than blockSize
while (block.length < blockSize) block.push(0);
cipherText.push(...xorBlocks(ivBlock, block));
}
return cipherText.slice(0, messageLength);
}
case "CTR": {
let counter = [...iv];
for (let i = 0; i < paddedMessage.length; i += blockSize) {
const encrypted = encryptBlock(counter, S, rounds, w);
const block = paddedMessage.slice(i, i + blockSize);
// Pad block if shorter than blockSize
while (block.length < blockSize) block.push(0);
cipherText.push(...xorBlocks(encrypted, block));
counter = incrementCounter(counter);
}
return cipherText.slice(0, messageLength);
}
default:
throw new OperationError(`Invalid block cipher mode: ${mode}`);
}
return cipherText;
}
/**
* Decrypt using RC6 cipher with specified block mode
*
* @param {number[]} cipherText - Ciphertext as byte array
* @param {number[]} key - Key as byte array
* @param {number[]} iv - IV (block size bytes, not used for ECB)
* @param {string} mode - Block cipher mode ("ECB", "CBC", "CFB", "OFB", "CTR")
* @param {string} padding - Padding type ("NO", "PKCS5", "ZERO", "RANDOM", "BIT")
* @param {number} rounds - Number of rounds (default: 20)
* @param {number} w - Word size in bits (default: 32)
* @returns {number[]} - Plaintext as byte array
*/
export function decryptRC6(cipherText, key, iv, mode = "ECB", padding = "PKCS5", rounds = 20, w = 32) {
const blockSize = getBlockSize(w);
const originalLength = cipherText.length;
if (originalLength === 0) return [];
const S = generateSubkeys(key, rounds, w);
if (mode === "ECB" || mode === "CBC") {
if ((originalLength % blockSize) !== 0)
throw new OperationError(`Invalid ciphertext length: ${originalLength} bytes. Must be a multiple of ${blockSize}.`);
} else {
// Pad for stream modes
while ((cipherText.length % blockSize) !== 0)
cipherText.push(0);
}
const plainText = [];
switch (mode) {
case "ECB":
for (let i = 0; i < cipherText.length; i += blockSize) {
const block = cipherText.slice(i, i + blockSize);
plainText.push(...decryptBlock(block, S, rounds, w));
}
break;
case "CBC": {
let ivBlock = [...iv];
for (let i = 0; i < cipherText.length; i += blockSize) {
const block = cipherText.slice(i, i + blockSize);
const decrypted = decryptBlock(block, S, rounds, w);
plainText.push(...xorBlocks(decrypted, ivBlock));
ivBlock = block;
}
break;
}
case "CFB": {
let ivBlock = [...iv];
for (let i = 0; i < cipherText.length; i += blockSize) {
const encrypted = encryptBlock(ivBlock, S, rounds, w);
const block = cipherText.slice(i, i + blockSize);
plainText.push(...xorBlocks(encrypted, block));
ivBlock = block;
}
return plainText.slice(0, originalLength);
}
case "OFB": {
let ivBlock = [...iv];
for (let i = 0; i < cipherText.length; i += blockSize) {
ivBlock = encryptBlock(ivBlock, S, rounds, w);
const block = cipherText.slice(i, i + blockSize);
plainText.push(...xorBlocks(ivBlock, block));
}
return plainText.slice(0, originalLength);
}
case "CTR": {
let counter = [...iv];
for (let i = 0; i < cipherText.length; i += blockSize) {
const encrypted = encryptBlock(counter, S, rounds, w);
const block = cipherText.slice(i, i + blockSize);
plainText.push(...xorBlocks(encrypted, block));
counter = incrementCounter(counter);
}
return plainText.slice(0, originalLength);
}
default:
throw new OperationError(`Invalid block cipher mode: ${mode}`);
}
// Remove padding for ECB/CBC modes
if (mode === "ECB" || mode === "CBC") {
return removePadding(plainText, padding, blockSize);
}
return plainText.slice(0, originalLength);
}

View File

@ -76,7 +76,7 @@ class A1Z26CipherDecode extends Operation {
const delim = Utils.charRep(args[0] || "Space");
if (input.length === 0) {
return [];
return "";
}
const bites = input.split(delim);

View File

@ -1,5 +1,6 @@
/**
* @author n1474335 [n1474335@gmail.com]
* @author ko80240 [csk.dev@proton.me]
* @copyright Crown Copyright 2023
* @license Apache-2.0
*/
@ -8,6 +9,7 @@ import * as uuid from "uuid";
import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs";
import { toHex } from "../lib/Hex.mjs";
/**
* Analyse UUID operation
@ -22,27 +24,128 @@ class AnalyseUUID extends Operation {
this.name = "Analyse UUID";
this.module = "Crypto";
this.description = "Tries to determine information about a given UUID and suggests which version may have been used to generate it";
this.description = "Operation for extracting metadata and detecting the version of a given UUID.";
this.infoURL = "https://wikipedia.org/wiki/Universally_unique_identifier";
this.inputType = "string";
this.outputType = "string";
this.args = [];
this.args = [
{
name: "Include Metadata",
type: "boolean",
value: true
}
];
}
/**
* @param {string} input
* @param {string} input - Expects a valid UUID string
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
input = input.trim();
let uuidVersion, uuidBytes;
try {
const uuidVersion = uuid.version(input);
return "UUID version: " + uuidVersion;
uuidVersion = uuid.version(input); // Re-using the uuid library to extract version
uuidBytes = uuid.parse(input); // Re-using the uuid library to parse bytes
} catch (error) {
throw new OperationError("Invalid UUID");
}
const [includeMetadata] = args;
const dv = new DataView(uuidBytes.buffer, uuidBytes.byteOffset, uuidBytes.byteLength); // Dataview helps handle the multi-byte ints
const uuidInteger = (dv.getBigUint64(0) << 64n) | dv.getBigUint64(8);
const sections = [`Version:\n${uuidVersion}`];
if (includeMetadata) {
const parser = UUID_PARSERS[uuidVersion];
const decoded = parser?.(uuidBytes, dv);
sections.push(formatDecoded(decoded));
}
sections.push(`UUID Integer:\n${uuidInteger}`);
return sections.filter(Boolean).join("\n\n");
}
}
export default AnalyseUUID;
/**
* Metadata can be extracted for versions 1, 6, and 7.
* Enum-like frozen mapping of UUID version to parser function.
*/
const UUID_PARSERS = Object.freeze({
1: parsev1v6,
6: parsev1v6,
7: parsev7,
});
/**
* Versions 1 and 6. Note 6 is a re-order of 1.
* Version 1 == layout: timeLow(32) | timeMid(16) | timeHi(12)
* Version 6 == layout: timeHi(32) | timeMid(16) | timeLow(12)
*/
function parsev1v6(uuidBytes, dv) {
const isV1 = (uuidBytes[6] >> 4) === 1;
const timeStamp =
isV1 ? (
(BigInt(dv.getUint16(6) & 0x0fff) << 48n) | // mask off version bits
(BigInt(dv.getUint16(4)) << 32n) |
BigInt(dv.getUint32(0))
) : (
(BigInt(dv.getUint32(0)) << 28n) |
(BigInt(dv.getUint16(4)) << 12n) |
(BigInt(dv.getUint16(6) & 0x0fff))
);
// Convert to Unix time
const milliseconds =
Number(
(timeStamp - 122192928000000000n) / 10000n
);
return {
timestamp: milliseconds,
isoTimestamp: new Date(milliseconds).toISOString(),
clock: ((uuidBytes[8] & 0x3f) << 8) | uuidBytes[9],
node: toHex(uuidBytes.slice(10), ":").toUpperCase()
};
}
/** Version 7 */
function parsev7(uuidBytes, dv) {
const milliseconds = Number((BigInt(dv.getUint32(0)) << 16n) | BigInt(dv.getUint16(4)));
return {
timestamp: milliseconds,
isoTimestamp: new Date(milliseconds).toISOString(),
randA: ((uuidBytes[6] & 0x0f) << 8) | uuidBytes[7],
randB: toHex(uuidBytes.slice(8), "").toUpperCase()
};
}
/**
* Formats metadata
*
* @param {Object|undefined} decoded
* @returns {string}
*/
function formatDecoded(decoded) {
if (!decoded) return "No metadata available. Only versions 1, 6, 7 are supported.";
return Object.entries({
"Timestamp": decoded.timestamp,
"Timestamp (ISO)": decoded.isoTimestamp,
"Node": decoded.node,
"Clock": decoded.clock,
"Rand A": decoded.randA,
"Rand B": decoded.randB
})
.filter(([, value]) => value !== undefined)
.map(([label, value]) => `${label}:\n${value}`)
.join("\n\n");
}

View File

@ -6,7 +6,10 @@
import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs";
import { blake3 } from "hash-wasm";
import Utils from "../Utils.mjs";
import { blake3 } from "@noble/hashes/blake3.js";
import { bytesToHex } from "@noble/hashes/utils.js";
/**
* BLAKE3 operation
*/
@ -44,13 +47,16 @@ class BLAKE3 extends Operation {
run(input, args) {
const key = args[1];
const size = args[0];
// Check if the user want a key hash or not
if (key === "") {
return blake3(input, size*8);
} if (key.length !== 32) {
const opts = { dkLen: size };
const inputBytes = new Uint8Array(Utils.strToArrayBuffer(input));
if (key !== "") {
const keyBytes = new Uint8Array(Utils.strToArrayBuffer(key));
if (keyBytes.length !== 32) {
throw new OperationError("The key must be exactly 32 bytes long");
}
return blake3(input, size*8, key);
opts.key = keyBytes;
}
return bytesToHex(blake3(inputBytes, opts));
}
}

View File

@ -0,0 +1,193 @@
/**
* @author MedjedThomasXM
* @copyright Crown Copyright 2024
* @license Apache-2.0
*/
import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs";
import { isWorkerEnvironment } from "../Utils.mjs";
import cs from "@alexaltea/capstone-js/dist/capstone.min.js";
/**
* Disassemble ARM operation
*/
class DisassembleARM extends Operation {
/**
* DisassembleARM constructor
*/
constructor() {
super();
this.name = "Disassemble ARM";
this.module = "Shellcode";
this.description = "Disassembles ARM machine code into assembly language.<br><br>Supports ARM (32-bit), Thumb, and ARM64 (AArch64) architectures using the Capstone disassembly framework.<br><br>Input should be in hexadecimal.";
this.infoURL = "https://wikipedia.org/wiki/ARM_architecture_family";
this.inputType = "string";
this.outputType = "string";
this.args = [
{
"name": "Architecture",
"type": "option",
"value": ["ARM (32-bit)", "ARM64 (AArch64)"]
},
{
"name": "Mode",
"type": "option",
"value": ["ARM", "Thumb", "Thumb + Cortex-M", "ARMv8"]
},
{
"name": "Endianness",
"type": "option",
"value": ["Little Endian", "Big Endian"]
},
{
"name": "Starting address (hex)",
"type": "number",
"value": 0
},
{
"name": "Show instruction hex",
"type": "boolean",
"value": true
},
{
"name": "Show instruction position",
"type": "boolean",
"value": true
}
];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {string}
*/
async run(input, args) {
const [
architecture,
mode,
endianness,
startAddress,
showHex,
showPosition
] = args;
// Remove whitespace from input
const hexInput = input.replace(/\s/g, "");
// Validate hex input
if (!/^[0-9a-fA-F]*$/.test(hexInput)) {
throw new OperationError("Invalid hexadecimal input. Please provide valid hex characters only.");
}
if (hexInput.length === 0) {
return "";
}
if (hexInput.length % 2 !== 0) {
throw new OperationError("Invalid hexadecimal input. Length must be even.");
}
// Convert hex string to byte array
const bytes = [];
for (let i = 0; i < hexInput.length; i += 2) {
bytes.push(parseInt(hexInput.substr(i, 2), 16));
}
// Determine architecture constant
let arch;
if (architecture === "ARM64 (AArch64)") {
arch = cs.ARCH_ARM64;
} else {
arch = cs.ARCH_ARM;
}
// Determine mode constant
let modeValue = cs.MODE_LITTLE_ENDIAN;
if (architecture === "ARM (32-bit)") {
switch (mode) {
case "ARM":
modeValue = cs.MODE_ARM;
break;
case "Thumb":
modeValue = cs.MODE_THUMB;
break;
case "Thumb + Cortex-M":
modeValue = cs.MODE_THUMB | cs.MODE_MCLASS;
break;
case "ARMv8":
modeValue = cs.MODE_ARM | cs.MODE_V8;
break;
default:
modeValue = cs.MODE_ARM;
}
} else {
// ARM64 only has one mode (ARM mode is default for ARM64)
modeValue = cs.MODE_ARM;
}
// Add endianness
if (endianness === "Big Endian") {
modeValue |= cs.MODE_BIG_ENDIAN;
}
if (isWorkerEnvironment()) {
self.sendStatusMessage("Disassembling...");
}
let disassembler;
try {
disassembler = new cs.Capstone(arch, modeValue);
} catch (e) {
throw new OperationError(`Failed to initialise Capstone disassembler: ${e}`);
}
let instructions;
try {
instructions = disassembler.disasm(bytes, startAddress);
} catch (e) {
disassembler.close();
// Check if it's a "no valid instructions" error (code 0 means OK but nothing decoded)
if (e && e.includes && e.includes("code 0:")) {
throw new OperationError(`No valid ${architecture} instructions found in input. The bytes may be for a different architecture or mode.`);
}
throw new OperationError(`Disassembly failed: ${e}`);
}
// Format output
const output = [];
for (const insn of instructions) {
let line = "";
if (showPosition) {
// Format address as hex with 0x prefix
const addrHex = "0x" + insn.address.toString(16).padStart(8, "0");
line += addrHex + " ";
}
if (showHex) {
// Format instruction bytes as hex
const bytesHex = insn.bytes.map(b => b.toString(16).padStart(2, "0")).join("");
line += bytesHex.padEnd(16, " ") + " ";
}
line += insn.mnemonic;
if (insn.op_str) {
line += " " + insn.op_str;
}
output.push(line);
}
disassembler.close();
return output.join("\n");
}
}
export default DisassembleARM;

View File

@ -0,0 +1,175 @@
/**
* @author d0s1nt [d0s1nt@cyberchefaudio]
* @copyright Crown Copyright 2025
* @license Apache-2.0
*/
import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs";
import Utils from "../Utils.mjs";
import { makeEmptyReport, sniffContainer } from "../lib/AudioMetaSchema.mjs";
import {
parseMp3, parseRiffWave, parseFlac, parseOgg,
parseMp4BestEffort, parseAiffBestEffort,
parseAacAdts, parseAc3, parseWmaAsf,
} from "../lib/AudioParsers.mjs";
/**
* Extract Audio Metadata operation.
*/
class ExtractAudioMetadata extends Operation {
/** Creates the Extract Audio Metadata operation. */
constructor() {
super();
this.name = "Extract Audio Metadata";
this.module = "Default";
this.description =
"Extract common audio metadata across MP3 (ID3v2/ID3v1/GEOB), WAV/BWF/BW64 (INFO/bext/iXML/axml), FLAC (Vorbis Comment/Picture), OGG (Vorbis/OpusTags), AAC (ADTS), AC3 (Dolby Digital), WMA (ASF), plus best-effort MP4/M4A and AIFF scanning. Outputs normalized JSON.";
this.infoURL = "https://wikipedia.org/wiki/Audio_file_format";
this.inputType = "ArrayBuffer";
this.outputType = "JSON";
this.presentType = "html";
this.args = [
{ name: "Filename (optional)", type: "string", value: "" },
{ name: "Max embedded text bytes (iXML/axml/etc)", type: "number", value: 1024 * 512 },
];
}
/**
* @param {ArrayBuffer} input
* @param {Object[]} args
* @returns {Object}
*/
run(input, args) {
const filename = (args?.[0] || "").trim() || null;
const maxTextBytes = Number.isFinite(args?.[1]) ? Math.max(1024, args[1]) : 1024 * 512;
if (!(input instanceof ArrayBuffer) || input.byteLength === 0)
throw new OperationError("No input data. Load an audio file (drag/drop or use the open file button).");
const bytes = new Uint8Array(input);
const container = sniffContainer(bytes);
const report = makeEmptyReport(filename, bytes.length, container);
try {
const parsers = {
mp3: () => parseMp3(bytes, report),
wav: () => parseRiffWave(bytes, report, maxTextBytes),
bw64: () => parseRiffWave(bytes, report, maxTextBytes),
flac: () => parseFlac(bytes, report, maxTextBytes),
ogg: () => parseOgg(bytes, report),
opus: () => parseOgg(bytes, report),
mp4: () => parseMp4BestEffort(bytes, report),
m4a: () => parseMp4BestEffort(bytes, report),
aiff: () => parseAiffBestEffort(bytes, report, maxTextBytes),
aac: () => parseAacAdts(bytes, report),
ac3: () => parseAc3(bytes, report),
wma: () => parseWmaAsf(bytes, report),
};
if (parsers[container.type]) {
parsers[container.type]();
} else {
report.errors.push({ stage: "sniff", message: "Unknown/unsupported container (best-effort scan not implemented)." });
}
} catch (e) {
report.errors.push({ stage: "parse", message: String(e?.message || e) });
}
return report;
}
/** Renders the extracted metadata as an HTML table. */
present(data) {
if (!data || typeof data !== "object") return JSON.stringify(data, null, 4);
const esc = Utils.escapeHtml;
const row = (k, v) => `<tr><td>${esc(String(k))}</td><td>${esc(String(v ?? ""))}</td></tr>\n`;
const section = (title) => `<tr><th colspan="2" style="background:#e9ecef;text-align:center">${esc(title)}</th></tr>\n`;
const objRows = (obj, filter = (v) => v !== null) => {
for (const [k, v] of Object.entries(obj)) {
if (filter(v)) html += row(k, v);
}
};
const objSection = (obj, title, filter) => {
if (!obj) return;
html += section(title);
objRows(obj, filter);
};
const listSection = (arr, title, fmt) => {
if (!arr?.length) return;
html += section(title);
for (const item of arr) html += fmt(item);
};
let html = `<table class="table table-hover table-sm table-bordered table-nonfluid">\n`;
html += section("Artifact");
html += row("Filename", data.artifact?.filename || "(none)");
html += row("Size", `${(data.artifact?.byte_length ?? 0).toLocaleString()} bytes`);
html += row("Container", data.artifact?.container?.type);
html += row("MIME", data.artifact?.container?.mime);
if (data.artifact?.container?.brand) html += row("Brand", data.artifact.container.brand);
html += section("Detections");
html += row("Metadata systems", (data.detections?.metadata_systems || []).join(", ") || "None");
html += row("Provenance systems", (data.detections?.provenance_systems || []).join(", ") || "None");
const common = data.tags?.common || {};
html += section("Common Tags");
if (Object.values(common).some((v) => v !== null)) {
for (const [key, val] of Object.entries(common)) {
if (val !== null) html += row(key.charAt(0).toUpperCase() + key.slice(1), val);
}
} else {
html += row("(none)", "No common tags found");
}
listSection(data.tags?.raw?.id3v2?.frames, "ID3v2 Frames", (f) => {
const val = typeof f.decoded === "object" ? JSON.stringify(f.decoded) : (f.decoded ?? `(${f.size} bytes)`);
return row(f.id + (f.description ? ` \u2014 ${f.description}` : ""), val);
});
objSection(data.tags?.raw?.id3v1, "ID3v1", (v) => !!v);
listSection(data.tags?.raw?.apev2?.items, "APEv2 Tags", (i) => row(i.key, i.value));
if (data.tags?.raw?.vorbis_comments?.comments?.length) {
html += section("Vorbis Comments");
html += row("Vendor", data.tags.raw.vorbis_comments.vendor);
for (const c of data.tags.raw.vorbis_comments.comments) html += row(c.key, c.value);
}
objSection(data.tags?.raw?.riff?.info, "RIFF INFO", () => true);
objSection(data.tags?.raw?.riff?.bext, "BWF bext");
listSection(data.tags?.raw?.riff?.chunks, "RIFF Chunks", (c) => row(c.id, `${c.size} bytes @ offset ${c.offset}`));
listSection(data.tags?.raw?.flac?.blocks, "FLAC Metadata Blocks", (b) => row(b.type, `${b.length} bytes`));
if (data.tags?.raw?.mp4?.top_level_atoms?.length) {
html += section("MP4 Top-Level Atoms");
const atoms = data.tags.raw.mp4.top_level_atoms;
for (const a of atoms.slice(0, 50)) html += row(a.type, `${a.size} bytes @ offset ${a.offset}`);
if (atoms.length > 50) html += row("...", `${atoms.length - 50} more atoms`);
}
listSection(data.tags?.raw?.aiff?.chunks, "AIFF Chunks", (c) => row(c.id, c.value));
objSection(data.tags?.raw?.aac, "AAC ADTS");
objSection(data.tags?.raw?.ac3, "AC3 (Dolby Digital)");
objSection(data.tags?.raw?.asf?.content_description, "ASF Content Description", (v) => !!v);
listSection(data.tags?.raw?.asf?.extended_content, "ASF Extended Content", (d) => row(d.name, d.value));
listSection(data.embedded, "Embedded Objects", (e) => row(e.id, `${e.content_type || "unknown"} \u2014 ${(e.byte_length ?? 0).toLocaleString()} bytes`));
if (data.provenance?.c2pa?.present) {
html += section("C2PA Provenance");
html += row("Present", "Yes");
for (const emb of (data.provenance.c2pa.embedding || []))
html += row("Carrier", `${emb.carrier} \u2014 ${(emb.byte_length ?? 0).toLocaleString()} bytes`);
}
listSection(data.errors, "Errors", (e) => row(e.stage, e.message));
html += "</table>";
return html;
}
}
export default ExtractAudioMetadata;

View File

@ -5,7 +5,7 @@
*/
import Operation from "../Operation.mjs";
import { search } from "../lib/Extract.mjs";
import { EMAIL_REGEX, search } from "../lib/Extract.mjs";
import { caseInsensitiveSort } from "../lib/Sort.mjs";
/**
@ -50,8 +50,7 @@ class ExtractEmailAddresses extends Operation {
*/
run(input, args) {
const [displayTotal, sort, unique] = args,
// email regex from: https://www.regextester.com/98066
regex = /(?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9](?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9-]*[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9])?\.)+[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9](?:[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9-]*[\u00A0-\uD7FF\uE000-\uFFFFa-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\])/ig;
regex = EMAIL_REGEX;
const results = search(
input,

View File

@ -5,7 +5,7 @@
*/
import Operation from "../Operation.mjs";
import punycode from "punycode";
import punycode from "punycode.js";
/**
* From Punycode operation

View File

@ -6,7 +6,7 @@
import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs";
import * as jq from "jq-wasm";
import jq from "jq-web";
/**
* jq operation
@ -30,7 +30,12 @@ class Jq extends Operation {
name: "Query",
type: "string",
value: ""
}
},
{
name: "Raw",
type: "boolean",
value: false
},
];
}
@ -40,15 +45,19 @@ class Jq extends Operation {
* @returns {string}
*/
run(input, args) {
return (async () => {
const [query] = args;
const [query, raw] = args;
let result;
try {
const result = await jq.json(input, query);
return JSON.stringify(result);
result = jq.json(input, query);
} catch (err) {
throw new OperationError(`Invalid jq expression: ${err.message}`);
}
})();
if (raw && typeof result === "string") {
return result;
} else {
return JSON.stringify(result);
}
}
}

View File

@ -99,7 +99,7 @@ class OffsetChecker extends Operation {
}
}
return outputs.join(sampleDelim);
return outputs.join(Utils.escapeHtml(sampleDelim));
}
}

View File

@ -0,0 +1,128 @@
/**
* @author j83305 [awz22@protonmail.com]
* @copyright Crown Copyright 2020
* @license Apache-2.0
*/
import Operation from "../Operation.mjs";
import { calculateParityBit, decodeParityBit } from "../lib/ParityBit.mjs";
/**
* Parity Bit operation
*/
class ParityBit extends Operation {
/**
* ParityBit constructor
*/
constructor() {
super();
this.name = "Parity Bit";
this.module = "Default";
this.description = "A parity bit, or check bit, is the simplest form of error detection. It is a bit which is added to a string of bits and represents if the number of 1's in the binary string is an even number or odd number.<br><br>If a delimiter is specified, the parity bit calculation will be performed on each 'block' of the input data, where the blocks are created by slicing the input at each occurence of the delimiter character";
this.infoURL = "https://wikipedia.org/wiki/Parity_bit";
this.inputType = "string";
this.outputType = "string";
this.args = [
{
name: "Mode",
type: "option",
value: [
"Even Parity",
"Odd Parity"
]
},
{
name: "Postion",
type: "option",
value: [
"Start",
"End"
]
},
{
name: "Encode or Decode",
type: "option",
value: [
"Encode",
"Decode"
]
},
{
name: "Delimiter",
type: "shortString",
value: ""
}
];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
if (input.length === 0) {
return input;
}
/**
* determines weather to use the encode or decode method based off args[2]
* @param input input to be encoded or decoded
* @param args array
*/
const method = (input, args) => args[2] === "Encode" ? calculateParityBit(input, args) : decodeParityBit(input, args);
if (args[3].length > 0) {
const byteStrings = input.split(args[3]);
for (let byteStringsArrayIndex = 0; byteStringsArrayIndex < byteStrings.length; byteStringsArrayIndex++) {
byteStrings[byteStringsArrayIndex] = method(byteStrings[byteStringsArrayIndex], args);
}
return byteStrings.join(args[3]);
}
return method(input, args);
}
/**
* Highlight Parity Bit
*
* @param {Object[]} pos
* @param {number} pos[].start
* @param {number} pos[].end
* @param {Object[]} args
* @returns {Object[]} pos
*/
highlight(pos, args) {
if (args[3].length === 0) {
if (args[1] === "Prepend") {
pos[0].start += 1;
pos[0].end += 1;
}
return pos;
}
// need to be able to read input to do the highlighting when there is a delimiter
}
/**
* Highlight Parity Bit in reverse
*
* @param {Object[]} pos
* @param {number} pos[].start
* @param {number} pos[].end
* @param {Object[]} args
* @returns {Object[]} pos
*/
highlightReverse(pos, args) {
if (args[3].length === 0) {
if (args[1] === "Prepend") {
if (pos[0].start > 0) {
pos[0].start -= 1;
}
pos[0].end -= 1;
}
return pos;
}
}
}
export default ParityBit;

View File

@ -0,0 +1,112 @@
/**
* @author tedk [tedk@ted.do]
* @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 {fromHex, toHex} from "../lib/Hex.mjs";
/**
* Parse Ethernet frame operation
*/
class ParseEthernetFrame extends Operation {
/**
* ParseEthernetFrame constructor
*/
constructor() {
super();
this.name = "Parse Ethernet frame";
this.module = "Default";
this.description = "Parses an Ethernet frame and either shows the deduced values (Source and destination MAC, VLANs) or returns the packet data.<br /><br />Good for use in conjunction with the Parse IPv4, and Parse TCP/UDP recipes.";
this.infoURL = "https://en.wikipedia.org/wiki/Ethernet_frame#Frame_%E2%80%93_data_link_layer";
this.inputType = "string";
this.outputType = "html";
this.args = [
{
name: "Input type",
type: "option",
value: [
"Raw", "Hex"
],
defaultIndex: 0,
},
{
name: "Return type",
type: "option",
value: [
"Text output", "Packet data", "Packet data (hex)",
],
defaultIndex: 0,
}
];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {html}
*/
run(input, args) {
const format = args[0];
const outputFormat = args[1];
if (format === "Hex") {
input = fromHex(input);
} else if (format === "Raw") {
input = new Uint8Array(Utils.strToArrayBuffer(input));
} else {
throw new OperationError("Invalid input format selected.");
}
const destinationMac = input.slice(0, 6);
const sourceMac = input.slice(6, 12);
let offset = 12;
const vlans = [];
while (offset < input.length) {
const ethType = Utils.byteArrayToChars(input.slice(offset, offset+2));
offset += 2;
if (ethType === "\x81\x00" || ethType === "\x88\xA8") {
// Parse the VLAN tag:
// [0000] 0000 0000 0000
// ^^^ PRIO - Ignored
// ^ DEI - Ignored
// ^^^^ ^^^^ ^^^^ VLAN ID
const vlanTag = input.slice(offset, offset+2);
vlans.push(((vlanTag[0] & 0b00001111) << 8) | vlanTag[1]);
offset += 2;
} else {
break;
}
}
const packetData = input.slice(offset);
if (outputFormat === "Packet data") {
return Utils.byteArrayToChars(packetData);
} else if (outputFormat === "Packet data (hex)") {
return toHex(packetData);
} else if (outputFormat === "Text output") {
let retval = `Source MAC: ${toHex(sourceMac, ":")}\nDestination MAC: ${toHex(destinationMac, ":")}\n`;
if (vlans.length > 0) {
retval += `VLAN: ${vlans.join(", ")}\n`;
}
retval += `Data:\n${toHex(packetData)}`;
return retval;
}
}
}
export default ParseEthernetFrame;

View File

@ -33,6 +33,12 @@ class ParseIPv4Header extends Operation {
"name": "Input format",
"type": "option",
"value": ["Hex", "Raw"]
},
{
"name": "Output format",
"type": "option",
"value": ["Table", "Data (hex)", "Data (raw)"],
defaultIndex: 0,
}
];
}
@ -44,6 +50,8 @@ class ParseIPv4Header extends Operation {
*/
run(input, args) {
const format = args[0];
const outputFormat = args[1];
let output;
if (format === "Hex") {
@ -98,6 +106,9 @@ class ParseIPv4Header extends Operation {
checksumResult = givenChecksum + " (incorrect, should be " + correctChecksum + ")";
}
const data = input.slice(ihl * 4);
if (outputFormat === "Table") {
output = `<table class='table table-hover table-sm table-bordered table-nonfluid'><tr><th>Field</th><th>Value</th></tr>
<tr><td>Version</td><td>${version}</td></tr>
<tr><td>Internet Header Length (IHL)</td><td>${ihl} (${ihl * 4} bytes)</td></tr>
@ -116,13 +127,19 @@ class ParseIPv4Header extends Operation {
<tr><td>Protocol</td><td>${protocol}, ${protocolInfo.protocol} (${protocolInfo.keyword})</td></tr>
<tr><td>Header checksum</td><td>${checksumResult}</td></tr>
<tr><td>Source IP address</td><td>${ipv4ToStr(srcIP)}</td></tr>
<tr><td>Destination IP address</td><td>${ipv4ToStr(dstIP)}</td></tr>`;
<tr><td>Destination IP address</td><td>${ipv4ToStr(dstIP)}</td></tr>
<tr><td>Data (hex)</td><td>${toHex(data)}</td></tr>`;
if (ihl > 5) {
output += `<tr><td>Options</td><td>${toHex(options)}</td></tr>`;
}
return output + "</table>";
} else if (outputFormat === "Data (hex)") {
return toHex(data);
} else if (outputFormat === "Data (raw)") {
return Utils.byteArrayToChars(data);
}
}
}

View File

@ -0,0 +1,119 @@
/**
* @author Medjedtxm
* @copyright Crown Copyright 2026
* @license Apache-2.0
*/
import Operation from "../Operation.mjs";
import Utils from "../Utils.mjs";
import OperationError from "../errors/OperationError.mjs";
import { toHex } from "../lib/Hex.mjs";
import { decryptRC6, getBlockSize, getDefaultRounds } from "../lib/RC6.mjs";
/**
* RC6 Decrypt operation
*/
class RC6Decrypt extends Operation {
/**
* RC6Decrypt constructor
*/
constructor() {
super();
this.name = "RC6 Decrypt";
this.module = "Ciphers";
this.description = "RC6 is a symmetric key block cipher derived from RC5. It was designed by Ron Rivest, Matt Robshaw, Ray Sidney, and Yiqun Lisa Yin to meet the requirements of the AES competition, and was one of the five finalists.<br><br>RC6 is parameterised as RC6-w/r/b where w is word size in bits (any multiple of 8 from 8-256), r is the number of rounds (1-255), and b is the key length in bytes. The standard AES submission uses w=32, r=20. Common word sizes: 8, 16, 32 (standard), 64, 128.<br><br><b>IV:</b> The Initialisation Vector should be 4*w/8 bytes (e.g. 16 bytes for w=32). If not entered, it will default to null bytes.<br><br><b>Padding:</b> In CBC and ECB mode, the PKCS#7 padding scheme is used.";
this.infoURL = "https://wikipedia.org/wiki/RC6";
this.inputType = "string";
this.outputType = "string";
this.args = [
{
"name": "Key",
"type": "toggleString",
"value": "",
"toggleValues": ["Hex", "UTF8", "Latin1", "Base64"]
},
{
"name": "IV",
"type": "toggleString",
"value": "",
"toggleValues": ["Hex", "UTF8", "Latin1", "Base64"]
},
{
"name": "Mode",
"type": "option",
"value": ["CBC", "CFB", "OFB", "CTR", "ECB"]
},
{
"name": "Input",
"type": "option",
"value": ["Hex", "Raw"]
},
{
"name": "Output",
"type": "option",
"value": ["Raw", "Hex"]
},
{
"name": "Padding",
"type": "option",
"value": ["PKCS5", "NO", "ZERO", "RANDOM", "BIT"]
},
{
"name": "Word Size",
"type": "number",
"value": 32,
"min": 8,
"max": 256,
"step": 8
},
{
"name": "Rounds",
"type": "number",
"value": 20,
"min": 1,
"max": 255
}
];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
const key = Utils.convertToByteArray(args[0].string, args[0].option),
iv = Utils.convertToByteArray(args[1].string, args[1].option),
[,, mode, inputType, outputType, padding, wordSize, rounds] = args;
// Validate word size
if (!Number.isInteger(wordSize) || wordSize < 8 || wordSize > 256 || wordSize % 8 !== 0)
throw new OperationError(`Invalid word size: ${wordSize}. Must be a multiple of 8 between 8 and 256.`);
const blockSize = getBlockSize(wordSize);
const defaultRounds = getDefaultRounds(wordSize);
if (iv.length !== blockSize && iv.length !== 0 && mode !== "ECB")
throw new OperationError(`Invalid IV length: ${iv.length} bytes
RC6-${wordSize} uses an IV length of ${blockSize} bytes (${blockSize * 8} bits).
Make sure you have specified the type correctly (e.g. Hex vs UTF8).`);
if (!Number.isInteger(rounds) || rounds < 1 || rounds > 255)
throw new OperationError(`Invalid number of rounds: ${rounds}
Rounds must be an integer between 1 and 255. Standard for w=${wordSize} is ${defaultRounds}.`);
// Default IV to null bytes if empty (like AES)
const actualIv = iv.length === 0 ? new Array(blockSize).fill(0) : iv;
input = Utils.convertToByteArray(input, inputType);
const output = decryptRC6(input, key, actualIv, mode, padding, rounds, wordSize);
return outputType === "Hex" ? toHex(output, "") : Utils.byteArrayToUtf8(output);
}
}
export default RC6Decrypt;

View File

@ -0,0 +1,119 @@
/**
* @author Medjedtxm
* @copyright Crown Copyright 2026
* @license Apache-2.0
*/
import Operation from "../Operation.mjs";
import Utils from "../Utils.mjs";
import OperationError from "../errors/OperationError.mjs";
import { toHex } from "../lib/Hex.mjs";
import { encryptRC6, getBlockSize, getDefaultRounds } from "../lib/RC6.mjs";
/**
* RC6 Encrypt operation
*/
class RC6Encrypt extends Operation {
/**
* RC6Encrypt constructor
*/
constructor() {
super();
this.name = "RC6 Encrypt";
this.module = "Ciphers";
this.description = "RC6 is a symmetric key block cipher derived from RC5. It was designed by Ron Rivest, Matt Robshaw, Ray Sidney, and Yiqun Lisa Yin to meet the requirements of the AES competition, and was one of the five finalists.<br><br>RC6 is parameterised as RC6-w/r/b where w is word size in bits (any multiple of 8 from 8-256), r is the number of rounds (1-255), and b is the key length in bytes. The standard AES submission uses w=32, r=20. Common word sizes: 8, 16, 32 (standard), 64, 128.<br><br><b>IV:</b> The Initialisation Vector should be 4*w/8 bytes (e.g. 16 bytes for w=32). If not entered, it will default to null bytes.<br><br><b>Padding:</b> In CBC and ECB mode, the PKCS#7 padding scheme is used.";
this.infoURL = "https://wikipedia.org/wiki/RC6";
this.inputType = "string";
this.outputType = "string";
this.args = [
{
"name": "Key",
"type": "toggleString",
"value": "",
"toggleValues": ["Hex", "UTF8", "Latin1", "Base64"]
},
{
"name": "IV",
"type": "toggleString",
"value": "",
"toggleValues": ["Hex", "UTF8", "Latin1", "Base64"]
},
{
"name": "Mode",
"type": "option",
"value": ["CBC", "CFB", "OFB", "CTR", "ECB"]
},
{
"name": "Input",
"type": "option",
"value": ["Raw", "Hex"]
},
{
"name": "Output",
"type": "option",
"value": ["Hex", "Raw"]
},
{
"name": "Padding",
"type": "option",
"value": ["PKCS5", "NO", "ZERO", "RANDOM", "BIT"]
},
{
"name": "Word Size",
"type": "number",
"value": 32,
"min": 8,
"max": 256,
"step": 8
},
{
"name": "Rounds",
"type": "number",
"value": 20,
"min": 1,
"max": 255
}
];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
const key = Utils.convertToByteArray(args[0].string, args[0].option),
iv = Utils.convertToByteArray(args[1].string, args[1].option),
[,, mode, inputType, outputType, padding, wordSize, rounds] = args;
// Validate word size
if (!Number.isInteger(wordSize) || wordSize < 8 || wordSize > 256 || wordSize % 8 !== 0)
throw new OperationError(`Invalid word size: ${wordSize}. Must be a multiple of 8 between 8 and 256.`);
const blockSize = getBlockSize(wordSize);
const defaultRounds = getDefaultRounds(wordSize);
if (iv.length !== blockSize && iv.length !== 0 && mode !== "ECB")
throw new OperationError(`Invalid IV length: ${iv.length} bytes
RC6-${wordSize} uses an IV length of ${blockSize} bytes (${blockSize * 8} bits).
Make sure you have specified the type correctly (e.g. Hex vs UTF8).`);
if (!Number.isInteger(rounds) || rounds < 1 || rounds > 255)
throw new OperationError(`Invalid number of rounds: ${rounds}
Rounds must be an integer between 1 and 255. Standard for w=${wordSize} is ${defaultRounds}.`);
// Default IV to null bytes if empty (like AES)
const actualIv = iv.length === 0 ? new Array(blockSize).fill(0) : iv;
input = Utils.convertToByteArray(input, inputType);
const output = encryptRC6(input, key, actualIv, mode, padding, rounds, wordSize);
return outputType === "Hex" ? toHex(output, "") : Utils.byteArrayToUtf8(output);
}
}
export default RC6Encrypt;

View File

@ -7,6 +7,7 @@
import XRegExp from "xregexp";
import Operation from "../Operation.mjs";
import Utils from "../Utils.mjs";
import { EMAIL_REGEX } from "../lib/Extract.mjs";
import OperationError from "../errors/OperationError.mjs";
/**
@ -45,7 +46,7 @@ class RegularExpression extends Operation {
},
{
name: "Email address",
value: "(?:[\\u00A0-\\uD7FF\\uE000-\\uFFFFa-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[\\u00A0-\\uD7FF\\uE000-\\uFFFFa-z0-9!#$%&'*+/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[\\u00A0-\\uD7FF\\uE000-\\uFFFFa-z0-9](?:[\\u00A0-\\uD7FF\\uE000-\\uFFFF-a-z0-9-]*[\\u00A0-\\uD7FF\\uE000-\\uFFFFa-z0-9])?\\.)+[\\u00A0-\\uD7FF\\uE000-\\uFFFFa-z0-9](?:[\\u00A0-\\uD7FF\\uE000-\\uFFFFa-z0-9-]*[\\u00A0-\\uD7FF\\uE000-\\uFFFFa-z0-9])?|\\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\\.){3}\\])"
value: EMAIL_REGEX.source // We use a different regex library, so just take the source regex string here
},
{
name: "URL",

View File

@ -35,6 +35,11 @@ class RenderMarkdown extends Operation {
name: "Enable syntax highlighting",
type: "boolean",
value: true
},
{
name: "Open links in new tab.",
type: "boolean",
value: false
}
];
}
@ -45,7 +50,7 @@ class RenderMarkdown extends Operation {
* @returns {html}
*/
run(input, args) {
const [convertLinks, enableHighlighting] = args,
const [convertLinks, enableHighlighting, openLinksBlank] = args,
md = new MarkdownIt({
linkify: convertLinks,
html: false, // Explicitly disable HTML rendering
@ -58,12 +63,38 @@ class RenderMarkdown extends Operation {
return "";
}
}),
rendered = md.render(input);
});
if (openLinksBlank) {
this.makeLinksOpenInNewTab(md);
}
const rendered = md.render(input);
return `<div style="font-family: var(--primary-font-family)">${rendered}</div>`;
}
/**
* Adds target="_blank" to links.
* @param {MarkdownIt} md
*/
makeLinksOpenInNewTab(md) {
// Adapted from: https://github.com/markdown-it/markdown-it/blob/master/docs/architecture.md#renderer
// Remember old renderer, if overridden, or proxy to default renderer
const defaultRender = md.renderer.rules.link_open || function(tokens, idx, options, env, self) {
return self.renderToken(tokens, idx, options);
};
// eslint-disable-next-line camelcase
md.renderer.rules.link_open = function (tokens, idx, options, env, self) {
const token = tokens[idx];
if (token.attrIndex("target") >= 0) {
// Target attribute already set, do not replace.
return;
}
token.attrPush(["target", "_blank"]); // add new attribute
// pass token to default renderer.
return defaultRender(tokens, idx, options, env, self);
};
}
}
export default RenderMarkdown;

View File

@ -77,84 +77,84 @@ class ShowBase64Offsets extends Operation {
staticSection = offset0.slice(0, -3);
offset0 = "<span data-toggle='tooltip' data-placement='top' title='" +
Utils.escapeHtml(fromBase64(staticSection, alphabet).slice(0, -2)) + "'>" +
staticSection + "</span>" +
"<span class='hl5'>" + offset0.substr(offset0.length - 3, 1) + "</span>" +
"<span class='hl3'>" + offset0.substr(offset0.length - 2) + "</span>";
Utils.escapeHtml(staticSection) + "</span>" +
"<span class='hl5'>" + Utils.escapeHtml(offset0.substr(offset0.length - 3, 1)) + "</span>" +
"<span class='hl3'>" + Utils.escapeHtml(offset0.substr(offset0.length - 2)) + "</span>";
} else if (len0 % 4 === 3) {
staticSection = offset0.slice(0, -2);
offset0 = "<span data-toggle='tooltip' data-placement='top' title='" +
Utils.escapeHtml(fromBase64(staticSection, alphabet).slice(0, -1)) + "'>" +
staticSection + "</span>" +
"<span class='hl5'>" + offset0.substr(offset0.length - 2, 1) + "</span>" +
"<span class='hl3'>" + offset0.substr(offset0.length - 1) + "</span>";
Utils.escapeHtml(staticSection) + "</span>" +
"<span class='hl5'>" + Utils.escapeHtml(offset0.substr(offset0.length - 2, 1)) + "</span>" +
"<span class='hl3'>" + Utils.escapeHtml(offset0.substr(offset0.length - 1)) + "</span>";
} else {
staticSection = offset0;
offset0 = "<span data-toggle='tooltip' data-placement='top' title='" +
Utils.escapeHtml(fromBase64(staticSection, alphabet)) + "'>" +
staticSection + "</span>";
Utils.escapeHtml(staticSection) + "</span>";
}
if (!showVariable) {
offset0 = staticSection;
offset0 = Utils.escapeHtml(staticSection);
}
// Highlight offset 1
padding = "<span class='hl3'>" + offset1.substr(0, 1) + "</span>" +
"<span class='hl5'>" + offset1.substr(1, 1) + "</span>";
padding = "<span class='hl3'>" + Utils.escapeHtml(offset1.substr(0, 1)) + "</span>" +
"<span class='hl5'>" + Utils.escapeHtml(offset1.substr(1, 1)) + "</span>";
offset1 = offset1.substr(2);
if (len1 % 4 === 2) {
staticSection = offset1.slice(0, -3);
offset1 = padding + "<span data-toggle='tooltip' data-placement='top' title='" +
Utils.escapeHtml(fromBase64("AA" + staticSection, alphabet).slice(1, -2)) + "'>" +
staticSection + "</span>" +
"<span class='hl5'>" + offset1.substr(offset1.length - 3, 1) + "</span>" +
"<span class='hl3'>" + offset1.substr(offset1.length - 2) + "</span>";
Utils.escapeHtml(staticSection) + "</span>" +
"<span class='hl5'>" + Utils.escapeHtml(offset1.substr(offset1.length - 3, 1)) + "</span>" +
"<span class='hl3'>" + Utils.escapeHtml(offset1.substr(offset1.length - 2)) + "</span>";
} else if (len1 % 4 === 3) {
staticSection = offset1.slice(0, -2);
offset1 = padding + "<span data-toggle='tooltip' data-placement='top' title='" +
Utils.escapeHtml(fromBase64("AA" + staticSection, alphabet).slice(1, -1)) + "'>" +
staticSection + "</span>" +
"<span class='hl5'>" + offset1.substr(offset1.length - 2, 1) + "</span>" +
"<span class='hl3'>" + offset1.substr(offset1.length - 1) + "</span>";
Utils.escapeHtml(staticSection) + "</span>" +
"<span class='hl5'>" + Utils.escapeHtml(offset1.substr(offset1.length - 2, 1)) + "</span>" +
"<span class='hl3'>" + Utils.escapeHtml(offset1.substr(offset1.length - 1)) + "</span>";
} else {
staticSection = offset1;
offset1 = padding + "<span data-toggle='tooltip' data-placement='top' title='" +
Utils.escapeHtml(fromBase64("AA" + staticSection, alphabet).slice(1)) + "'>" +
staticSection + "</span>";
Utils.escapeHtml(staticSection) + "</span>";
}
if (!showVariable) {
offset1 = staticSection;
offset1 = Utils.escapeHtml(staticSection);
}
// Highlight offset 2
padding = "<span class='hl3'>" + offset2.substr(0, 2) + "</span>" +
"<span class='hl5'>" + offset2.substr(2, 1) + "</span>";
padding = "<span class='hl3'>" + Utils.escapeHtml(offset2.substr(0, 2)) + "</span>" +
"<span class='hl5'>" + Utils.escapeHtml(offset2.substr(2, 1)) + "</span>";
offset2 = offset2.substr(3);
if (len2 % 4 === 2) {
staticSection = offset2.slice(0, -3);
offset2 = padding + "<span data-toggle='tooltip' data-placement='top' title='" +
Utils.escapeHtml(fromBase64("AAA" + staticSection, alphabet).slice(2, -2)) + "'>" +
staticSection + "</span>" +
"<span class='hl5'>" + offset2.substr(offset2.length - 3, 1) + "</span>" +
"<span class='hl3'>" + offset2.substr(offset2.length - 2) + "</span>";
Utils.escapeHtml(staticSection) + "</span>" +
"<span class='hl5'>" + Utils.escapeHtml(offset2.substr(offset2.length - 3, 1)) + "</span>" +
"<span class='hl3'>" + Utils.escapeHtml(offset2.substr(offset2.length - 2)) + "</span>";
} else if (len2 % 4 === 3) {
staticSection = offset2.slice(0, -2);
offset2 = padding + "<span data-toggle='tooltip' data-placement='top' title='" +
Utils.escapeHtml(fromBase64("AAA" + staticSection, alphabet).slice(2, -2)) + "'>" +
staticSection + "</span>" +
"<span class='hl5'>" + offset2.substr(offset2.length - 2, 1) + "</span>" +
"<span class='hl3'>" + offset2.substr(offset2.length - 1) + "</span>";
Utils.escapeHtml(staticSection) + "</span>" +
"<span class='hl5'>" + Utils.escapeHtml(offset2.substr(offset2.length - 2, 1)) + "</span>" +
"<span class='hl3'>" + Utils.escapeHtml(offset2.substr(offset2.length - 1)) + "</span>";
} else {
staticSection = offset2;
offset2 = padding + "<span data-toggle='tooltip' data-placement='top' title='" +
Utils.escapeHtml(fromBase64("AAA" + staticSection, alphabet).slice(2)) + "'>" +
staticSection + "</span>";
Utils.escapeHtml(staticSection) + "</span>";
}
if (!showVariable) {
offset2 = staticSection;
offset2 = Utils.escapeHtml(staticSection);
}
return (showVariable ? "Characters highlighted in <span class='hl5'>green</span> could change if the input is surrounded by more data." +

View File

@ -0,0 +1,123 @@
/**
* @author p-leriche [philip.leriche@cantab.net]
* @copyright Crown Copyright 2025
* @license Apache-2.0
*/
import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs";
/* ---------- helper functions ---------- */
/**
* Convert text to BigInt (big-endian byte interpretation)
*/
function textToBigInt(text) {
if (text.length === 0) return 0n;
let result = 0n;
for (let i = 0; i < text.length; i++) {
const charCode = BigInt(text.charCodeAt(i));
if (charCode > 255n) {
throw new OperationError(
`Character at position ${i} exceeds Latin-1 range (0-255).\n` +
"Only ASCII and Latin-1 characters are supported.");
}
result = (result << 8n) | charCode;
}
return result;
}
/**
* Convert BigInt to text (big-endian byte interpretation)
*/
function bigIntToText(value) {
if (value === 0n) return "";
const bytes = [];
let num = value;
while (num > 0n) {
bytes.unshift(Number(num & 0xFFn));
num >>= 8n;
}
return String.fromCharCode(...bytes);
}
/* ---------- operation class ---------- */
/**
* Text/Integer Converter operation
*/
class TextIntegerConverter extends Operation {
/**
* TextIntegerConverter constructor
*/
constructor() {
super();
this.description =
"Converts between text strings and large integers (decimal or hexadecimal).<br><br>" +
"Text is interpreted as a big-endian sequence of character codes. For example:<br>" +
"ABC is 0x414243 (hex) is 4276803 (decimal)<br>" +
"<b>Input format detection:</b><br>" +
"Decimal: digits 0-9 only<br>" +
"Hexadecimal: 0x... prefix<br>" +
"Quoted or unquoted text: treated as string<br><br>" +
"<b>Character limitations:</b><br>" +
"Text input may only contain ASCII and Latin-1 characters (code point < 256).<br>" +
"Multi-byte Unicode characters will generate an error.<br><br>." ;
this.infoURL = "https://wikipedia.org/wiki/Endianness";
this.inputType = "string";
this.outputType = "string";
this.args = [
{
name: "Output format",
type: "option",
value: ["String", "Decimal", "Hexadecimal"]
}
];
this.name = "Text-Integer Conversion";
this.module = "Default";
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
const outputFormat = args[0];
const trimmed = input.trim();
let bigIntValue;
if (!trimmed) {
// Null input - treat as zero
bigIntValue = 0;
} else if (/^0x[0-9a-f]+$/i.test(trimmed) ||
/^[+-]?[0-9]+$/.test(trimmed)) {
// Hex or decimal integer
bigIntValue = BigInt(trimmed);
} else if (/^["'].*["']$/.test(trimmed)) {
// Quoted string: Remove quotes and convert text to BigInt
const text = trimmed.slice(1, -1);
bigIntValue = textToBigInt(text);
} else {
// Assume it's unquoted text
bigIntValue = textToBigInt(trimmed);
}
// Convert to output format
if (outputFormat === "String") {
return bigIntToText(bigIntValue);
} else if (outputFormat === "Decimal") {
return bigIntValue.toString();
} else { // Hexadecimal
return "0x" + bigIntValue.toString(16);
}
}
}
export default TextIntegerConverter;

View File

@ -5,7 +5,7 @@
*/
import Operation from "../Operation.mjs";
import punycode from "punycode";
import punycode from "punycode.js";
/**
* To Punycode operation

View File

@ -88,16 +88,17 @@ class NodeRecipe {
* @param {NodeDish} dish
* @returns {NodeDish}
*/
execute(dish) {
return this.opList.reduce((prev, curr) => {
// CASE where opList item is op and args
async execute(dish) {
let prev = dish;
for (const curr of this.opList) {
if (Object.prototype.hasOwnProperty.call(curr, "op") &&
Object.prototype.hasOwnProperty.call(curr, "args")) {
return curr.op(prev, curr.args);
prev = await curr.op(prev, curr.args);
} else {
prev = await curr(prev);
}
// CASE opList item is just op.
return curr(prev);
}, dish);
}
return prev;
}
}

View File

@ -10,7 +10,7 @@
import NodeDish from "./NodeDish.mjs";
import NodeRecipe from "./NodeRecipe.mjs";
import OperationConfig from "../core/config/OperationConfig.json" assert {type: "json"};
import OperationConfig from "../core/config/OperationConfig.json" with { type: "json" };
import { sanitise, removeSubheadingsFromArray, sentenceToCamelCase } from "./apiUtils.mjs";
import ExcludedOperationError from "../core/errors/ExcludedOperationError.mjs";
@ -324,10 +324,10 @@ export function help(input) {
* @returns {NodeDish} of the result
* @throws {TypeError} if invalid recipe given.
*/
export function bake(input, recipeConfig) {
export async function bake(input, recipeConfig) {
const recipe = new NodeRecipe(recipeConfig);
const dish = ensureIsDish(input);
return recipe.execute(dish);
return await recipe.execute(dish);
}

View File

@ -43,17 +43,23 @@ class HTMLOperation {
/**
* Renders the operation in HTML as a stub operation with no ingredients.
*
* @param {boolean} removeIcon - show icon for removing operation
* @param {string} elementId - element ID for aria usage
* @returns {string}
*/
toStubHtml(removeIcon) {
toStubHtml(removeIcon = false, elementId = null) {
let html = "<li class='operation'";
if (elementId) {
html += ` id='${elementId}'`;
}
if (this.description) {
const infoLink = this.infoURL ? `<hr>${titleFromWikiLink(this.infoURL)}` : "";
html += ` data-container='body' data-toggle='popover' data-placement='right'
data-content="${this.description}${infoLink}" data-html='true' data-trigger='hover'
data-boundary='viewport'`;
data-boundary='viewport' role='button'`;
}
html += ">" + this.name;

View File

@ -130,6 +130,7 @@ class Manager {
// Controls
document.getElementById("bake").addEventListener("click", this.controls.bakeClick.bind(this.controls));
document.getElementById("auto-bake").addEventListener("change", this.controls.autoBakeChange.bind(this.controls));
document.getElementById("auto-bake").addEventListener("keydown", this.controls.autoBakeKeyboardHandler.bind(this.controls));
document.getElementById("step").addEventListener("click", this.controls.stepClick.bind(this.controls));
document.getElementById("clr-recipe").addEventListener("click", this.controls.clearRecipeClick.bind(this.controls));
document.getElementById("save").addEventListener("click", this.controls.saveClick.bind(this.controls));

View File

@ -145,7 +145,7 @@
<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 tabindex="0" id="content-wrapper">
<div id="banner" class="row">
<div class="col" style="text-align: left; padding-left: 10px;">
<a href="#" data-toggle="modal" data-target="#download-modal" data-help-title="Downloading CyberChef" data-help="<p>CyberChef can be downloaded to run locally or hosted within your own network. It has no server-side component so all that is required is that the ZIP file is uncompressed and the files are accessible.</p><p>As a user, it is worth noting that unofficial versions of CyberChef could have been modified to introduce Input and/or Recipe exfiltration. We recommend always using the official, open source, up-to-date version of CyberChef hosted at <a href='https://gchq.github.io/CyberChef'>https://gchq.github.io/CyberChef</a> if accessible.</p><p>The Network tab in your browser's Developer console (F12) can be used to inspect the network requests made by a website. This can confirm that no data is uploaded when a CyberChef recipe is baked.</p>">Download CyberChef <i class="material-icons">file_download</i></a>
@ -173,7 +173,7 @@
Operations
<span class="op-count"></span>
</div>
<input id="search" type="search" class="form-control" placeholder="Search..." autocomplete="off" tabindex="2" data-help-title="Searching for operations" data-help="<p>Use the search box to find useful operations.</p><p>Both operation names and descriptions are queried using a fuzzy matching algorithm.</p>">
<input id="search" type="search" class="form-control" placeholder="Search..." autocomplete="off" tabindex="0" data-help-title="Searching for operations" data-help="<p>Use the search box to find useful operations.</p><p>Both operation names and descriptions are queried using a fuzzy matching algorithm.</p>" aria-owns="search-results">
<ul id="search-results" class="op-list"></ul>
<div id="categories" class="panel-group no-select"></div>
</div>
@ -584,22 +584,22 @@
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item" role="presentation">
<a class="nav-link active" href="#faqs" aria-controls="profile" role="tab" data-toggle="tab">
<a id="tab-1" class="nav-link active" href="#faqs" aria-controls="profile" role="tab" data-toggle="tab">
FAQs
</a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link" href="#report-bug" aria-controls="messages" role="tab" data-toggle="tab">
<a id="tab-2" class="nav-link" href="#report-bug" aria-controls="messages" role="tab" data-toggle="tab">
Report a bug
</a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link" href="#about" aria-controls="messages" role="tab" data-toggle="tab">
<a id="tab-3" class="nav-link" href="#about" aria-controls="messages" role="tab" data-toggle="tab">
About
</a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link" href="#keybindings" aria-controls="messages" role="tab" data-toggle="tab">
<a id="tab-4" class="nav-link" href="#keybindings" aria-controls="messages" role="tab" data-toggle="tab">
Keybindings
</a>
</li>

View File

@ -17,8 +17,8 @@ import * as CanvasComponents from "../core/lib/CanvasComponents.mjs";
// CyberChef
import App from "./App.mjs";
import Categories from "../core/config/Categories.json" assert {type: "json"};
import OperationConfig from "../core/config/OperationConfig.json" assert {type: "json"};
import Categories from "../core/config/Categories.json" with { type: "json" };
import OperationConfig from "../core/config/OperationConfig.json" with { type: "json" };
/**

View File

@ -1,5 +1,5 @@
import sm from "sitemap";
import OperationConfig from "../../core/config/OperationConfig.json" assert { type: "json" };
import OperationConfig from "../../core/config/OperationConfig.json" with { type: "json" };
/**
* Generates an XML sitemap for all CyberChef operations and a number of recipes.

View File

@ -26,6 +26,36 @@
color: var(--banner-url-colour);
}
#options:focus {
background-color: #eef3ec;
border: solid black 2px;
border-radius: 4px;
}
#support:focus {
background-color: #eef3ec;
border: solid black 2px;
border-radius: 4px;
}
#notice:focus {
background-color: #eef3ec;
border: solid black 2px;
border-radius: 4px;
}
#banner .col a:focus {
background-color: #eef3ec;
border: solid black 2px;
border-radius: 4px;
}
#notice-wrapper #notice:focus {
background-color: #eef3ec;
border: solid black 2px;
border-radius: 4px;
}
#notice-wrapper {
text-align: center;
overflow: hidden;

View File

@ -78,6 +78,11 @@
border-left: 2px solid var(--primary-border-colour);
}
p a:focus {
color: #0a6ebd;
text-decoration: underline;
}
.checkbox label input[type=checkbox]+.checkbox-decorator .check,
.checkbox label input[type=checkbox]+.checkbox-decorator .check::before {
border-color: var(--input-border-colour);

View File

@ -249,6 +249,13 @@ optgroup {
}
/* Bootstrap form inside CodeMirror editor */
.cm-panel > .bmd-form-group {
padding-top: 0;
}
/* CodeMirror */
.ͼ2 .cm-specialChar,

View File

@ -57,6 +57,18 @@ class ControlsWaiter {
}
}
/**
* Checks or unchecks the Auto Bake checkbox with "Enter"
* @param {Event} ev
*/
autoBakeKeyboardHandler(ev) {
const checkBox = document.getElementById("auto-bake");
ev.preventDefault();
if (ev.key === "Enter" || ev.key === " ") {
checkBox.checked = !checkBox.checked;
}
}
/**
* Handler to trigger baking.
@ -387,6 +399,18 @@ class ControlsWaiter {
*/
supportButtonClick(e) {
e.preventDefault();
const faqs = document.getElementById("faqs");
const faqsAElement = faqs.getElementsByTagName("a");
for (let i = 0; i < faqsAElement.length; i++) {
faqsAElement[i].setAttribute("tabindex", "0");
faqsAElement[i].addEventListener("keydown", this.navigateFAQList, false);
}
const tabs = document.querySelectorAll('[role="tab"]');
for (let i = 0; i < tabs.length; i++) {
tabs[i].addEventListener("keydown", this.changeTabs, false);
}
const reportBugInfo = document.getElementById("report-bug-info");
const saveLink = this.generateStateUrl(true, true, null, null, "https://gchq.github.io/CyberChef/");
@ -403,6 +427,64 @@ ${navigator.userAgent}
}
/**
* @param {Event} ev
*/
changeTabs(ev) {
const tab = ev.target;
ev.preventDefault();
ev.stopPropagation();
if (ev.key === "ArrowRight") {
const nextTab = tab.parentElement;
if (nextTab.nextElementSibling === null) {
tab.parentElement.parentElement.firstElementChild.firstElementChild.focus();
} else {
nextTab.nextElementSibling.firstElementChild.focus();
}
} else if (ev.key === "ArrowLeft") {
const prevTab = tab.parentElement;
if (prevTab.previousElementSibling === null) {
tab.parentElement.parentElement.lastElementChild.firstElementChild.focus();
} else {
prevTab.previousElementSibling.firstElementChild.focus();
}
} else if (ev.key === "Tab" && !ev.shiftKey && ev.target === document.getElementById("tab-1")) {
document.getElementById("faqs").querySelector("[class='btn btn-primary']").focus();
} else if (ev.key === "Tab" && !ev.shiftKey && ev.target === document.getElementById("tab-2")) {
document.getElementById("report-bug").querySelector("[class='btn btn-primary']").focus();
} else if (ev.key === "Tab" && !ev.shiftKey && ev.target === document.getElementById("tab-3")) {
document.getElementById("about").querySelector("[href]").focus();
} else if (ev.key === "Tab" && !ev.shiftKey && ev.target === document.getElementById("tab-4")) {
const button = document.getElementById("support-modal").getElementsByClassName("modal-footer");
const close = button[0].firstElementChild;
close.focus();
} else if (ev.key === "Enter" || ev.key === "Space" || ev.key === " ") {
tab.click();
}
}
/**
* @param {Event} ev
*/
navigateFAQList(ev) {
const el = ev.target.nextElementSibling;
if (ev.key === "Enter" || ev.key === "Space" || ev.key === " ") {
ev.preventDefault();
const question = el.classList;
if (question !== undefined && question.value) {
if (!question.value.includes("show")) {
question.add("show");
} else if (question.contains("show")) {
question.remove("show");
}
}
}
}
/**
* Shows the stale indicator to show that the input or recipe has changed
* since the last bake.

View File

@ -28,17 +28,16 @@ class OperationsWaiter {
this.removeIntent = false;
}
/**
* Handler for search events.
* Finds operations which match the given search term and displays them under the search box.
*
* @param {event} e
* @param {KeyboardEvent | ClipboardEvent | Event} e
*/
searchOperations(e) {
let ops, selected;
if (e.type === "search" || e.keyCode === 13) { // Search or Return
if ((e.type === "search" && e.target.value !== "") || e.keyCode === 13) { // Search (non-empty) or Return
e.preventDefault();
ops = document.querySelectorAll("#search-results li");
if (ops.length) {
@ -49,27 +48,43 @@ class OperationsWaiter {
}
}
/**
* Sets up the operation element with the correct attributes when selected
* @param {HTMLElement} element
*/
const _selectOperation = (element) => {
element.classList.add("selected-op");
element.scrollIntoView({block: "nearest"});
$(element).popover("show");
e.target.setAttribute("aria-activedescendant", element.id);
};
/**
* Sets up the operation element with the correct attributes when deselected
* @param {HTMLElement} element
*/
const _deselectOperation = (element) => {
element.classList.remove("selected-op");
$(element).popover("hide");
};
if (e.keyCode === 40) { // Down
e.preventDefault();
ops = document.querySelectorAll("#search-results li");
if (ops.length) {
selected = this.getSelectedOp(ops);
if (selected > -1) {
ops[selected].classList.remove("selected-op");
}
if (selected > -1) _deselectOperation(ops[selected]);
if (selected === ops.length-1) selected = -1;
ops[selected+1].classList.add("selected-op");
_selectOperation(ops[selected+1]);
}
} else if (e.keyCode === 38) { // Up
e.preventDefault();
ops = document.querySelectorAll("#search-results li");
if (ops.length) {
selected = this.getSelectedOp(ops);
if (selected > -1) {
ops[selected].classList.remove("selected-op");
}
if (selected > -1) _deselectOperation(ops[selected]);
if (selected === 0) selected = ops.length;
ops[selected-1].classList.add("selected-op");
_selectOperation(ops[selected-1]);
}
} else {
const searchResultsEl = document.getElementById("search-results");
@ -83,11 +98,13 @@ class OperationsWaiter {
searchResultsEl.removeChild(searchResultsEl.firstChild);
}
document.querySelector("#search").removeAttribute("aria-activedescendant");
$("#categories .show").collapse("hide");
if (str) {
const matchedOps = this.filterOperations(str, true);
const matchedOpsHtml = matchedOps
.map(v => v.toStubHtml())
.map((operation, idx) => operation.toStubHtml(false, `search-result-${idx}`))
.join("");
searchResultsEl.innerHTML = matchedOpsHtml;
@ -103,7 +120,7 @@ class OperationsWaiter {
* @param {string} searchStr
* @param {boolean} highlight - Whether or not to highlight the matching string in the operation
* name and description
* @returns {string[]}
* @returns {HTMLOperation[]}
*/
filterOperations(inStr, highlight) {
const matchedOps = [];

View File

@ -268,7 +268,7 @@ module.exports = {
testOpHtml(browser, "Parse colour code", "#000", ".colorpicker-preview", "rgb(0, 0, 0)");
testOpHtml(browser, "Parse DateTime", "01/12/2000 13:00:00", "", /Date: Friday 1st December 2000/);
// testOp(browser, "Parse IP range", "test input", "test_output");
testOpHtml(browser, "Parse IPv4 header", "45 c0 00 c4 02 89 00 00 ff 11 1e 8c c0 a8 0c 01 c0 a8 0c 02", "tr:last-child td:last-child", "192.168.12.2");
testOpHtml(browser, "Parse IPv4 header", "45 c0 00 c4 02 89 00 00 ff 11 1e 8c c0 a8 0c 01 c0 a8 0c 02", "tr:nth-last-child(2) td:last-child", "192.168.12.2");
// testOp(browser, "Parse IPv6 address", "test input", "test_output");
// testOp(browser, "Parse ObjectID timestamp", "test input", "test_output");
// testOp(browser, "Parse QR Code", "test input", "test_output");
@ -354,6 +354,7 @@ module.exports = {
// testOp(browser, "Tail", "test input", "test_output");
// testOp(browser, "Take bytes", "test input", "test_output");
testOp(browser, "Tar", "test input", /^file\.txt\x00{92}/);
testOp(browser, "Template", "{\"one\": 1, \"two\": 2}", "1 2", ["{{ one }} {{ two }}"]);
testOpHtml(browser, "Text Encoding Brute Force", "test input", "tr:nth-of-type(4) td:last-child", /t\u2400e\u2400s\u2400t\u2400/);
// testOp(browser, "To BCD", "test input", "test_output");
// testOp(browser, "To Base", "test input", "test_output");

View File

@ -0,0 +1,31 @@
/**
* Polyfill for Node.js 22+ where globalThis.fetch is built-in but rejects
* bare filesystem paths. WASM libraries like argon2-browser call fetch() with
* an absolute path (e.g. "/path/to/argon2.wasm") expecting a browser-style
* fallback, but Node.js 22's fetch throws synchronously for non-URL strings.
*
* This wrapper intercepts such calls and serves the file via Node's fs module,
* returning a synthetic Response so the WASM module loads correctly.
*/
import { readFile } from "fs/promises";
if (globalThis.fetch) {
const originalFetch = globalThis.fetch;
globalThis.fetch = async function patchedFetch(url, options) {
const urlStr = typeof url === "string" ?
url :
url instanceof URL ?
url.href :
String(url);
// Intercept bare filesystem paths (absolute POSIX or Windows)
if (urlStr.startsWith("/") || /^[A-Za-z]:[/\\]/.test(urlStr)) {
const buffer = await readFile(urlStr);
return new Response(buffer, {
status: 200,
headers: { "Content-Type": "application/wasm" },
});
}
return originalFetch(url, options);
};
}

View File

@ -8,9 +8,9 @@
const assert = require("assert");
require("cyberchef").then(chef => {
require("cyberchef").then(async chef => {
const d = chef.bake("Testing, 1 2 3", [
const d = await chef.bake("Testing, 1 2 3", [
chef.toHex,
chef.reverse,
{

View File

@ -9,7 +9,7 @@ import assert from "assert";
import chef from "cyberchef";
import { bake, toHex, reverse, unique, multiply } from "cyberchef";
const a = bake("Testing, 1 2 3", [
const a = await bake("Testing, 1 2 3", [
toHex,
reverse,
{
@ -28,7 +28,7 @@ const a = bake("Testing, 1 2 3", [
assert.equal(a.value, "630957449041920");
const b = chef.bake("Testing, 1 2 3", [
const b = await chef.bake("Testing, 1 2 3", [
chef.toHex,
chef.reverse,
{

View File

@ -1,6 +1,6 @@
import TestRegister from "../../lib/TestRegister.mjs";
import Categories from "../../../src/core/config/Categories.json" assert {type: "json"};
import OperationConfig from "../../../src/core/config/OperationConfig.json" assert {type: "json"};
import Categories from "../../../src/core/config/Categories.json" with { type: "json" };
import OperationConfig from "../../../src/core/config/OperationConfig.json" with { type: "json" };
import it from "../assertionHandler.mjs";
import assert from "assert";

View File

@ -69,14 +69,14 @@ TestRegister.addApiTests([
it("BigIntUtils: egcd - coprime numbers", () => {
const [g, x, y] = egcd(BigInt("3"), BigInt("11"));
assert.strictEqual(g, BigInt("1"));
// Verify Bézout identity: a*x + b*y = gcd
// Verify Bézout identity: a*x + b*y = gcd
assert.strictEqual(BigInt("3") * x + BigInt("11") * y, g);
}),
it("BigIntUtils: egcd - non-coprime numbers", () => {
const [g, x, y] = egcd(BigInt("240"), BigInt("46"));
assert.strictEqual(g, BigInt("2"));
// Verify Bézout identity
// Verify Bézout identity
assert.strictEqual(BigInt("240") * x + BigInt("46") * y, g);
}),
@ -90,7 +90,7 @@ TestRegister.addApiTests([
it("BigIntUtils: egcd - identical numbers", () => {
const [g, x, y] = egcd(BigInt("42"), BigInt("42"));
assert.strictEqual(g, BigInt("42"));
// Verify Bézout identity
// Verify Bézout identity
assert.strictEqual(BigInt("42") * x + BigInt("42") * y, g);
}),
@ -98,7 +98,7 @@ TestRegister.addApiTests([
const a = BigInt("123456789012345678901234567890");
const b = BigInt("987654321098765432109876543210");
const [g, x, y] = egcd(a, b);
// Verify Bézout identity
// Verify Bézout identity
assert.strictEqual(a * x + b * y, g);
}),

View File

@ -170,77 +170,77 @@ TestRegister.addApiTests([
assert(chef.bake);
}),
it("chef.bake: should return NodeDish", () => {
const result = chef.bake("input", "to base 64");
it("chef.bake: should return NodeDish", async () => {
const result = await chef.bake("input", "to base 64");
assert(result instanceof NodeDish);
}),
it("chef.bake: should take an input and an op name and perform it", () => {
const result = chef.bake("some input", "to base 32");
it("chef.bake: should take an input and an op name and perform it", async () => {
const result = await chef.bake("some input", "to base 32");
assert.strictEqual(result.toString(), "ONXW2ZJANFXHA5LU");
}),
it("chef.bake: should complain if recipe isnt a valid object", () => {
assert.throws(() => chef.bake("some input", 3264), {
it("chef.bake: should complain if recipe isnt a valid object", async () => {
await assert.rejects(() => chef.bake("some input", 3264), {
name: "TypeError",
message: "Recipe can only contain function names or functions"
});
}),
it("chef.bake: Should complain if string op is invalid", () => {
assert.throws(() => chef.bake("some input", "not a valid operation"), {
it("chef.bake: Should complain if string op is invalid", async () => {
await assert.rejects(() => chef.bake("some input", "not a valid operation"), {
name: "TypeError",
message: "Couldn't find an operation with name 'not a valid operation'."
});
}),
it("chef.bake: Should take an input and an operation and perform it", () => {
const result = chef.bake("https://google.com/search?q=help", chef.parseURI);
it("chef.bake: Should take an input and an operation and perform it", async () => {
const result = await chef.bake("https://google.com/search?q=help", chef.parseURI);
assert.strictEqual(result.toString(), "Protocol:\thttps:\nHostname:\tgoogle.com\nPath name:\t/search\nArguments:\n\tq = help\n");
}),
it("chef.bake: Should complain if an invalid operation is inputted", () => {
assert.throws(() => chef.bake("https://google.com/search?q=help", () => {}), {
it("chef.bake: Should complain if an invalid operation is inputted", async () => {
await assert.rejects(() => chef.bake("https://google.com/search?q=help", () => {}), {
name: "TypeError",
message: "Inputted function not a Chef operation."
});
}),
it("chef.bake: accepts an array of operation names and performs them all in order", () => {
const result = chef.bake("https://google.com/search?q=that's a complicated question", ["URL encode", "URL decode", "Parse URI"]);
it("chef.bake: accepts an array of operation names and performs them all in order", async () => {
const result = await chef.bake("https://google.com/search?q=that's a complicated question", ["URL encode", "URL decode", "Parse URI"]);
assert.strictEqual(result.toString(), "Protocol:\thttps:\nHostname:\tgoogle.com\nPath name:\t/search\nArguments:\n\tq = that's a complicated question\n");
}),
it("chef.bake: forgiving with operation names", () =>{
const result = chef.bake("https://google.com/search?q=that's a complicated question", ["urlencode", "url decode", "parseURI"]);
it("chef.bake: forgiving with operation names", async () =>{
const result = await chef.bake("https://google.com/search?q=that's a complicated question", ["urlencode", "url decode", "parseURI"]);
assert.strictEqual(result.toString(), "Protocol:\thttps:\nHostname:\tgoogle.com\nPath name:\t/search\nArguments:\n\tq = that's a complicated question\n");
}),
it("chef.bake: forgiving with operation names", () =>{
const result = chef.bake("hello", ["to base 64"]);
it("chef.bake: forgiving with operation names", async () =>{
const result = await chef.bake("hello", ["to base 64"]);
assert.strictEqual(result.toString(), "aGVsbG8=");
}),
it("chef.bake: if recipe is empty array, return input as dish", () => {
const result = chef.bake("some input", []);
it("chef.bake: if recipe is empty array, return input as dish", async () => {
const result = await chef.bake("some input", []);
assert.strictEqual(result.toString(), "some input");
assert(result instanceof NodeDish, "Result is not instance of NodeDish");
}),
it("chef.bake: accepts an array of operations as recipe", () => {
const result = chef.bake("https://google.com/search?q=that's a complicated question", [chef.URLEncode, chef.URLDecode, chef.parseURI]);
it("chef.bake: accepts an array of operations as recipe", async () => {
const result = await chef.bake("https://google.com/search?q=that's a complicated question", [chef.URLEncode, chef.URLDecode, chef.parseURI]);
assert.strictEqual(result.toString(), "Protocol:\thttps:\nHostname:\tgoogle.com\nPath name:\t/search\nArguments:\n\tq = that's a complicated question\n");
}),
it("should complain if an invalid operation is inputted as part of array", () => {
assert.throws(() => chef.bake("something", [() => {}]), {
it("should complain if an invalid operation is inputted as part of array", async () => {
await assert.rejects(() => chef.bake("something", [() => {}]), {
name: "TypeError",
message: "Inputted function not a Chef operation."
});
}),
it("chef.bake: should take single JSON object describing op and args OBJ", () => {
const result = chef.bake("some input", {
it("chef.bake: should take single JSON object describing op and args OBJ", async () => {
const result = await chef.bake("some input", {
op: chef.toHex,
args: {
Delimiter: "Colon"
@ -249,23 +249,23 @@ TestRegister.addApiTests([
assert.strictEqual(result.toString(), "73:6f:6d:65:20:69:6e:70:75:74");
}),
it("chef.bake: should take single JSON object desribing op with optional args", () => {
const result = chef.bake("some input", {
it("chef.bake: should take single JSON object desribing op with optional args", async () => {
const result = await chef.bake("some input", {
op: chef.toHex,
});
assert.strictEqual(result.toString(), "73 6f 6d 65 20 69 6e 70 75 74");
}),
it("chef.bake: should take single JSON object describing op and args ARRAY", () => {
const result = chef.bake("some input", {
it("chef.bake: should take single JSON object describing op and args ARRAY", async () => {
const result = await chef.bake("some input", {
op: chef.toHex,
args: ["Colon"]
});
assert.strictEqual(result.toString(), "73:6f:6d:65:20:69:6e:70:75:74");
}),
it("chef.bake: should error if op in JSON is not chef op", () => {
assert.throws(() => chef.bake("some input", {
it("chef.bake: should error if op in JSON is not chef op", async () => {
await assert.rejects(() => chef.bake("some input", {
op: () => {},
args: ["Colon"],
}), {
@ -274,8 +274,8 @@ TestRegister.addApiTests([
});
}),
it("chef.bake: should take multiple ops in JSON object form, some ops by string", () => {
const result = chef.bake("some input", [
it("chef.bake: should take multiple ops in JSON object form, some ops by string", async () => {
const result = await chef.bake("some input", [
{
op: chef.toHex,
args: ["Colon"]
@ -290,8 +290,8 @@ TestRegister.addApiTests([
assert.strictEqual(result.toString(), "67;63;72;66;146;72;66;144;72;66;65;72;62;60;72;66;71;72;66;145;72;67;60;72;67;65;72;67;64");
}),
it("chef.bake: should take multiple ops in JSON object form, some without args", () => {
const result = chef.bake("some input", [
it("chef.bake: should take multiple ops in JSON object form, some without args", async () => {
const result = await chef.bake("some input", [
{
op: chef.toHex,
},
@ -305,8 +305,8 @@ TestRegister.addApiTests([
assert.strictEqual(result.toString(), "67;63;40;66;146;40;66;144;40;66;65;40;62;60;40;66;71;40;66;145;40;67;60;40;67;65;40;67;64");
}),
it("chef.bake: should handle op with multiple args", () => {
const result = chef.bake("some input", {
it("chef.bake: should handle op with multiple args", async () => {
const result = await chef.bake("some input", {
op: "to morse code",
args: {
formatOptions: "Dash/Dot",
@ -317,13 +317,13 @@ TestRegister.addApiTests([
assert.strictEqual(result.toString(), "DotDotDot\\DashDashDash\\DashDash\\Dot,DotDot\\DashDot\\DotDashDashDot\\DotDotDash\\Dash");
}),
it("chef.bake: should take compact JSON format from Chef Website as recipe", () => {
const result = chef.bake("some input", [{"op": "To Morse Code", "args": ["Dash/Dot", "Backslash", "Comma"]}, {"op": "Hex to PEM", "args": ["SOMETHING"]}, {"op": "To Snake case", "args": [false]}]);
it("chef.bake: should take compact JSON format from Chef Website as recipe", async () => {
const result = await chef.bake("some input", [{"op": "To Morse Code", "args": ["Dash/Dot", "Backslash", "Comma"]}, {"op": "Hex to PEM", "args": ["SOMETHING"]}, {"op": "To Snake case", "args": [false]}]);
assert.strictEqual(result.toString(), "begin_something_anananaaaaak_da_aaak_da_aaaaananaaaaaaan_da_aaaaaaanan_da_aaak_end_something");
}),
it("chef.bake: should accept Clean JSON format from Chef website as recipe", () => {
const result = chef.bake("some input", [
it("chef.bake: should accept Clean JSON format from Chef website as recipe", async () => {
const result = await chef.bake("some input", [
{ "op": "To Morse Code",
"args": ["Dash/Dot", "Backslash", "Comma"] },
{ "op": "Hex to PEM",
@ -334,8 +334,8 @@ TestRegister.addApiTests([
assert.strictEqual(result.toString(), "begin_something_anananaaaaak_da_aaak_da_aaaaananaaaaaaan_da_aaaaaaanan_da_aaak_end_something");
}),
it("chef.bake: should accept Clean JSON format from Chef website - args optional", () => {
const result = chef.bake("some input", [
it("chef.bake: should accept Clean JSON format from Chef website - args optional", async () => {
const result = await chef.bake("some input", [
{ "op": "To Morse Code" },
{ "op": "Hex to PEM",
"args": ["SOMETHING"] },
@ -345,31 +345,28 @@ 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", [
it("chef.bake: should accept operation names from Chef Website which contain forward slash", async () => {
const result = await 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", [
it("chef.bake: should accept operation names from Chef Website which contain a hyphen", async () => {
const result = await 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", [
it("chef.bake: should accept operation names from Chef Website which contain a period", async () => {
const result = await 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?'
`);
assert.strictEqual(result.toString(), `SEQUENCE\n INTEGER 05\n IA5String 'Anybody there?'\n`);
}),
it("Excluded operations: throw a sensible error when you try and call one", () => {
@ -381,16 +378,16 @@ TestRegister.addApiTests([
}
}),
it("chef.bake: cannot accept flowControl operations in recipe", () => {
assert.throws(() => chef.bake("some input", "magic"), {
it("chef.bake: cannot accept flowControl operations in recipe", async () => {
await assert.rejects(() => chef.bake("some input", "magic"), {
name: "TypeError",
message: "flowControl operations like Magic are not currently allowed in recipes for chef.bake in the Node API"
});
assert.throws(() => chef.bake("some input", magic), {
await assert.rejects(() => chef.bake("some input", magic), {
name: "TypeError",
message: "flowControl operations like Magic are not currently allowed in recipes for chef.bake in the Node API"
});
assert.throws(() => chef.bake("some input", ["to base 64", "magic"]), {
await assert.rejects(() => chef.bake("some input", ["to base 64", "magic"]), {
name: "TypeError",
message: "flowControl operations like Magic are not currently allowed in recipes for chef.bake in the Node API"
});

View File

@ -589,8 +589,7 @@ Password: 282760`;
...[1, 3, 4, 5, 6, 7].map(version => it(`Analyze UUID v${version}`, () => {
const uuid = chef.generateUUID("", { "version": `v${version}` }).toString();
const result = chef.analyseUUID(uuid).toString();
const expected = `UUID version: ${version}`;
assert.strictEqual(result, expected);
assert.ok(result.startsWith(`Version:\n${version}\n`), `Expected output to start with "Version:\\n${version}\\n", got: ${result}`);
})),
it("Generate UUID using defaults", () => {
@ -598,7 +597,7 @@ Password: 282760`;
assert.ok(uuid);
const analysis = chef.analyseUUID(uuid).toString();
assert.strictEqual(analysis, "UUID version: 4");
assert.ok(analysis.startsWith("Version:\n4\n"), `Expected output to start with "Version:\\n4\\n", got: ${analysis}`);
}),
it("Gzip, Gunzip", () => {

View File

@ -11,10 +11,13 @@
* @license Apache-2.0
*/
import "../lib/wasmFetchPolyfill.mjs";
import { setLongTestFailure, logTestReport } from "../lib/utils.mjs";
import TestRegister from "../lib/TestRegister.mjs";
import "./tests/A1Z26CipherDecode.mjs";
import "./tests/AESKeyWrap.mjs";
import "./tests/AnalyseUUID.mjs";
import "./tests/AlternatingCaps.mjs";
import "./tests/AvroToJSON.mjs";
import "./tests/BaconCipher.mjs";
@ -61,13 +64,16 @@ import "./tests/Crypt.mjs";
import "./tests/CSV.mjs";
import "./tests/DateTime.mjs";
import "./tests/DefangIP.mjs";
import "./tests/DisassembleARM.mjs";
import "./tests/DropNthBytes.mjs";
import "./tests/ECDSA.mjs";
import "./tests/ELFInfo.mjs";
import "./tests/Enigma.mjs";
import "./tests/ExtractAudioMetadata.mjs";
import "./tests/ExtractEmailAddresses.mjs";
import "./tests/ExtractHashes.mjs";
import "./tests/ExtractIPAddresses.mjs";
import "./tests/Fernet.mjs";
import "./tests/Float.mjs";
import "./tests/FileTree.mjs";
import "./tests/FletcherChecksum.mjs";
@ -119,6 +125,7 @@ import "./tests/NetBIOS.mjs";
import "./tests/NormaliseUnicode.mjs";
import "./tests/NTLM.mjs";
import "./tests/OTP.mjs";
import "./tests/ParseEthernetFrame.mjs";
import "./tests/ParseIPRange.mjs";
import "./tests/ParseObjectIDTimestamp.mjs";
import "./tests/ParseQRCode.mjs";
@ -130,6 +137,7 @@ import "./tests/ParseUDP.mjs";
import "./tests/PEMtoHex.mjs";
import "./tests/PGP.mjs";
import "./tests/PHP.mjs";
import "./tests/ParityBit.mjs";
import "./tests/PHPSerialize.mjs";
import "./tests/PowerSet.mjs";
import "./tests/Protobuf.mjs";
@ -139,6 +147,8 @@ import "./tests/Rabbit.mjs";
import "./tests/RAKE.mjs";
import "./tests/Regex.mjs";
import "./tests/Register.mjs";
import "./tests/RegularExpression.mjs";
import "./tests/RenderMarkdown.mjs";
import "./tests/RisonEncodeDecode.mjs";
import "./tests/Rotate.mjs";
import "./tests/RSA.mjs";
@ -152,6 +162,7 @@ import "./tests/Shuffle.mjs";
import "./tests/SIGABA.mjs";
import "./tests/SM2.mjs";
import "./tests/SM4.mjs";
import "./tests/RC6.mjs";
// import "./tests/SplitColourChannels.mjs"; // Cannot test operations that use the File type yet
import "./tests/SQLBeautify.mjs";
import "./tests/StrUtils.mjs";
@ -164,6 +175,7 @@ import "./tests/SymmetricDifference.mjs";
import "./tests/TakeNthBytes.mjs";
import "./tests/Template.mjs";
import "./tests/TextEncodingBruteForce.mjs";
import "./tests/TextIntegerConverter.mjs";
import "./tests/ToFromInsensitiveRegex.mjs";
import "./tests/TranslateDateTimeFormat.mjs";
import "./tests/Typex.mjs";

View File

@ -0,0 +1,33 @@
/**
* A1Z26 Cipher Decode tests
*
* @author brick-pixel
* @copyright Crown Copyright 2026
* @license Apache-2.0
*/
import TestRegister from "../../lib/TestRegister.mjs";
TestRegister.addTests([
{
"name": "A1Z26 Cipher Decode: basic decode",
"input": "8 5 12 12 15",
"expectedOutput": "hello",
"recipeConfig": [
{
"op": "A1Z26 Cipher Decode",
"args": ["Space"]
}
]
},
{
"name": "A1Z26 Cipher Decode: empty input returns empty string",
"input": "",
"expectedOutput": "",
"recipeConfig": [
{
"op": "A1Z26 Cipher Decode",
"args": ["Space"]
}
]
}
]);

View File

@ -0,0 +1,66 @@
/**
* Analyse UUID tests
*
* @author ko80240 [csk.dev@proton.me]
* @copyright Crown Copyright 2023
* @license Apache-2.0
*/
import TestRegister from "../../lib/TestRegister.mjs";
TestRegister.addTests([
{
"name": "Analyse UUID: v1 UUID extracts timestamp, clock, and node",
"input": "cefa1760-28ee-11f1-9f95-1fb76af3e239",
"expectedOutput": "Version:\n1\n\nTimestamp:\n1774514156502\n\nTimestamp (ISO):\n2026-03-26T08:35:56.502Z\n\nNode:\n1F:B7:6A:F3:E2:39\n\nClock:\n8085\n\nUUID Integer:\n275119515460318071558429785403790975545",
"recipeConfig": [
{
"op": "Analyse UUID",
"args": [true]
}
]
},
{
"name": "Analyse UUID: v7 UUID extracts timestamp, randA, and randB",
"input": "019d294a-af64-7728-9524-26da08f50708",
"expectedOutput": "Version:\n7\n\nTimestamp:\n1774514253668\n\nTimestamp (ISO):\n2026-03-26T08:37:33.668Z\n\nRand A:\n1832\n\nRand B:\n952426DA08F50708\n\nUUID Integer:\n2145256098533991595556290452700595976",
"recipeConfig": [
{
"op": "Analyse UUID",
"args": [true]
}
]
},
{
"name": "Analyse UUID: v4 UUID should show no metadata - not possible",
"input": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"expectedOutput": "Version:\n4\n\nNo metadata available. Only versions 1, 6, 7 are supported.\n\nUUID Integer:\n324969006592305634633390616021200786553",
"recipeConfig": [
{
"op": "Analyse UUID",
"args": [true]
}
]
},
{
"name": "Analyse UUID: if the 'Include Metadata' option is false it should return not metadata",
"input": "cefa1760-28ee-11f1-9f95-1fb76af3e239",
"expectedOutput": "Version:\n1\n\nUUID Integer:\n275119515460318071558429785403790975545",
"recipeConfig": [
{
"op": "Analyse UUID",
"args": [false]
}
]
},
{
"name": "Analyse UUID: invalid UUID should return error message",
"input": "not-a-uuid",
"expectedOutput": "Invalid UUID",
"recipeConfig": [
{
"op": "Analyse UUID",
"args": [true]
}
]
}
]);

View File

@ -51,5 +51,23 @@ TestRegister.addTests([
{ "op": "BLAKE3",
"args": [8, "ThiskeyisexactlythirtytwoByteslo"] }
]
},
{
name: "BLAKE3: 16390 - test",
input: "test",
expectedMatch: /4878.{32760}555fe06b242738d5/,
recipeConfig: [
{ "op": "BLAKE3",
"args": [16390, ""] }
]
},
{
name: "BLAKE3: 16390 - key test",
input: "test",
expectedMatch: /a8d0.{32760}19ccd9b9726b46ae/,
recipeConfig: [
{ "op": "BLAKE3",
"args": [16390, "ThiskeyisexactlythirtytwoBytesLo"] }
]
}
]);

View File

@ -116,4 +116,15 @@ TestRegister.addTests([
},
],
},
{
name: "Show Base64 offsets: escapes static output",
input: "\x00\x10\x83\x10\x51\x87",
expectedOutput: "&lt;script&gt;\n&lt;AQmsBRk66\n&lt;ia1AEIM6",
recipeConfig: [
{
op: "Show Base64 offsets",
args: ["<script>ale(1)/.ABCDEFGHIJKLMNOPQRSTUVWXYZbdfghjkmnoquvwxyz023456", false, "Raw"],
},
],
},
]);

View File

@ -0,0 +1,377 @@
/**
* Disassemble ARM tests.
*
* @author MedjedThomasXM
*
* @copyright Crown Copyright 2024
* @license Apache-2.0
*/
import TestRegister from "../../lib/TestRegister.mjs";
TestRegister.addTests([
// ==================== ARM32 TESTS ====================
{
name: "Disassemble ARM: ARM32 NOP (mov r0, r0)",
input: "00 00 a0 e1",
expectedMatch: /mov\s+r0,\s*r0/,
recipeConfig: [
{
op: "Disassemble ARM",
args: ["ARM (32-bit)", "ARM", "Little Endian", 0, true, true],
},
],
},
{
name: "Disassemble ARM: ARM32 bx lr",
input: "1e ff 2f e1",
expectedMatch: /bx\s+lr/,
recipeConfig: [
{
op: "Disassemble ARM",
args: ["ARM (32-bit)", "ARM", "Little Endian", 0, true, true],
},
],
},
{
name: "Disassemble ARM: ARM32 push {fp, lr}",
input: "00 48 2d e9",
expectedMatch: /push\s+\{fp,\s*lr\}/,
recipeConfig: [
{
op: "Disassemble ARM",
args: ["ARM (32-bit)", "ARM", "Little Endian", 0, true, true],
},
],
},
{
name: "Disassemble ARM: ARM32 add fp, sp, #4",
input: "04 b0 8d e2",
expectedMatch: /add\s+fp,\s*sp/,
recipeConfig: [
{
op: "Disassemble ARM",
args: ["ARM (32-bit)", "ARM", "Little Endian", 0, true, true],
},
],
},
{
name: "Disassemble ARM: ARM32 ldr r0, [r1]",
input: "00 00 91 e5",
expectedMatch: /ldr\s+r0,\s*\[r1\]/,
recipeConfig: [
{
op: "Disassemble ARM",
args: ["ARM (32-bit)", "ARM", "Little Endian", 0, true, true],
},
],
},
{
name: "Disassemble ARM: ARM32 str r0, [r1]",
input: "00 00 81 e5",
expectedMatch: /str\s+r0,\s*\[r1\]/,
recipeConfig: [
{
op: "Disassemble ARM",
args: ["ARM (32-bit)", "ARM", "Little Endian", 0, true, true],
},
],
},
{
name: "Disassemble ARM: ARM32 bl (branch link)",
input: "00 00 00 eb",
expectedMatch: /bl\s+/,
recipeConfig: [
{
op: "Disassemble ARM",
args: ["ARM (32-bit)", "ARM", "Little Endian", 0, true, true],
},
],
},
{
name: "Disassemble ARM: ARM32 mul r0, r1, r2",
input: "91 02 00 e0",
expectedMatch: /mul\s+r0,\s*r1,\s*r2/,
recipeConfig: [
{
op: "Disassemble ARM",
args: ["ARM (32-bit)", "ARM", "Little Endian", 0, true, true],
},
],
},
// ==================== ARM32 THUMB TESTS ====================
{
name: "Disassemble ARM: Thumb mov r0, r0",
input: "00 46",
expectedMatch: /mov\s+r0,\s*r0/,
recipeConfig: [
{
op: "Disassemble ARM",
args: ["ARM (32-bit)", "Thumb", "Little Endian", 0, true, true],
},
],
},
{
name: "Disassemble ARM: Thumb bx lr",
input: "70 47",
expectedMatch: /bx\s+lr/,
recipeConfig: [
{
op: "Disassemble ARM",
args: ["ARM (32-bit)", "Thumb", "Little Endian", 0, true, true],
},
],
},
{
name: "Disassemble ARM: Thumb push {r4, lr}",
input: "10 b5",
expectedMatch: /push\s+\{r4,\s*lr\}/,
recipeConfig: [
{
op: "Disassemble ARM",
args: ["ARM (32-bit)", "Thumb", "Little Endian", 0, true, true],
},
],
},
{
name: "Disassemble ARM: Thumb pop {r4, pc}",
input: "10 bd",
expectedMatch: /pop\s+\{r4,\s*pc\}/,
recipeConfig: [
{
op: "Disassemble ARM",
args: ["ARM (32-bit)", "Thumb", "Little Endian", 0, true, true],
},
],
},
// ==================== ARM64 TESTS ====================
{
name: "Disassemble ARM: ARM64 ret",
input: "c0 03 5f d6",
expectedMatch: /ret/,
recipeConfig: [
{
op: "Disassemble ARM",
args: ["ARM64 (AArch64)", "ARM", "Little Endian", 0, true, true],
},
],
},
{
name: "Disassemble ARM: ARM64 mov x0, #0",
input: "00 00 80 d2",
expectedMatch: /mov[z]?\s+x0,\s*#0/,
recipeConfig: [
{
op: "Disassemble ARM",
args: ["ARM64 (AArch64)", "ARM", "Little Endian", 0, true, true],
},
],
},
{
name: "Disassemble ARM: ARM64 stp x29, x30, [sp, #-16]!",
input: "fd 7b bf a9",
expectedMatch: /stp\s+x29,\s*x30,\s*\[sp/,
recipeConfig: [
{
op: "Disassemble ARM",
args: ["ARM64 (AArch64)", "ARM", "Little Endian", 0, true, true],
},
],
},
{
name: "Disassemble ARM: ARM64 ldp x29, x30, [sp], #16",
input: "fd 7b c1 a8",
expectedMatch: /ldp\s+x29,\s*x30,\s*\[sp\]/,
recipeConfig: [
{
op: "Disassemble ARM",
args: ["ARM64 (AArch64)", "ARM", "Little Endian", 0, true, true],
},
],
},
{
name: "Disassemble ARM: ARM64 add x0, x1, x2",
input: "20 00 02 8b",
expectedMatch: /add\s+x0,\s*x1,\s*x2/,
recipeConfig: [
{
op: "Disassemble ARM",
args: ["ARM64 (AArch64)", "ARM", "Little Endian", 0, true, true],
},
],
},
{
name: "Disassemble ARM: ARM64 sub x0, x1, x2",
input: "20 00 02 cb",
expectedMatch: /sub\s+x0,\s*x1,\s*x2/,
recipeConfig: [
{
op: "Disassemble ARM",
args: ["ARM64 (AArch64)", "ARM", "Little Endian", 0, true, true],
},
],
},
{
name: "Disassemble ARM: ARM64 mul x0, x1, x2",
input: "20 7c 02 9b",
expectedMatch: /mul\s+x0,\s*x1,\s*x2/,
recipeConfig: [
{
op: "Disassemble ARM",
args: ["ARM64 (AArch64)", "ARM", "Little Endian", 0, true, true],
},
],
},
{
name: "Disassemble ARM: ARM64 ldr x0, [x1]",
input: "20 00 40 f9",
expectedMatch: /ldr\s+x0,\s*\[x1\]/,
recipeConfig: [
{
op: "Disassemble ARM",
args: ["ARM64 (AArch64)", "ARM", "Little Endian", 0, true, true],
},
],
},
{
name: "Disassemble ARM: ARM64 str x0, [x1]",
input: "20 00 00 f9",
expectedMatch: /str\s+x0,\s*\[x1\]/,
recipeConfig: [
{
op: "Disassemble ARM",
args: ["ARM64 (AArch64)", "ARM", "Little Endian", 0, true, true],
},
],
},
{
name: "Disassemble ARM: ARM64 bl (branch link)",
input: "00 00 00 94",
expectedMatch: /bl\s+/,
recipeConfig: [
{
op: "Disassemble ARM",
args: ["ARM64 (AArch64)", "ARM", "Little Endian", 0, true, true],
},
],
},
{
name: "Disassemble ARM: ARM64 cbz x0",
input: "00 00 00 b4",
expectedMatch: /cbz\s+x0/,
recipeConfig: [
{
op: "Disassemble ARM",
args: ["ARM64 (AArch64)", "ARM", "Little Endian", 0, true, true],
},
],
},
{
name: "Disassemble ARM: ARM64 cbnz x0",
input: "00 00 00 b5",
expectedMatch: /cbnz\s+x0/,
recipeConfig: [
{
op: "Disassemble ARM",
args: ["ARM64 (AArch64)", "ARM", "Little Endian", 0, true, true],
},
],
},
{
name: "Disassemble ARM: ARM64 sub sp, sp, #0x20",
input: "ff 83 00 d1",
expectedMatch: /sub\s+sp,\s*sp/,
recipeConfig: [
{
op: "Disassemble ARM",
args: ["ARM64 (AArch64)", "ARM", "Little Endian", 0, true, true],
},
],
},
{
name: "Disassemble ARM: ARM64 add sp, sp, #0x20",
input: "ff 83 00 91",
expectedMatch: /add\s+sp,\s*sp/,
recipeConfig: [
{
op: "Disassemble ARM",
args: ["ARM64 (AArch64)", "ARM", "Little Endian", 0, true, true],
},
],
},
// ==================== MULTI-INSTRUCTION TESTS ====================
{
name: "Disassemble ARM: ARM32 multiple instructions",
input: "00 48 2d e9 04 b0 8d e2 00 00 a0 e1 00 88 bd e8",
expectedMatch: /push.*\n.*add.*\n.*mov.*\n.*pop/s,
recipeConfig: [
{
op: "Disassemble ARM",
args: ["ARM (32-bit)", "ARM", "Little Endian", 0, true, true],
},
],
},
{
name: "Disassemble ARM: ARM64 function prologue/epilogue",
input: "fd 7b bf a9 fd 03 00 91 00 00 80 52 fd 7b c1 a8 c0 03 5f d6",
expectedMatch: /stp.*\n.*mov.*\n.*mov.*\n.*ldp.*\n.*ret/s,
recipeConfig: [
{
op: "Disassemble ARM",
args: ["ARM64 (AArch64)", "ARM", "Little Endian", 0, true, true],
},
],
},
// ==================== ADDRESS TESTS ====================
{
name: "Disassemble ARM: ARM64 with start address 0x1000",
input: "c0 03 5f d6",
expectedMatch: /0x00001000/,
recipeConfig: [
{
op: "Disassemble ARM",
args: ["ARM64 (AArch64)", "ARM", "Little Endian", 4096, true, true],
},
],
},
{
name: "Disassemble ARM: ARM32 with start address 0x8000",
input: "00 00 a0 e1",
expectedMatch: /0x00008000/,
recipeConfig: [
{
op: "Disassemble ARM",
args: ["ARM (32-bit)", "ARM", "Little Endian", 32768, true, true],
},
],
},
// ==================== ENDIANNESS TESTS ====================
{
name: "Disassemble ARM: ARM32 Big Endian",
input: "e1 a0 00 00",
expectedMatch: /mov\s+r0,\s*r0/,
recipeConfig: [
{
op: "Disassemble ARM",
args: ["ARM (32-bit)", "ARM", "Big Endian", 0, true, true],
},
],
},
// ==================== EDGE CASES ====================
{
name: "Disassemble ARM: Empty input",
input: "",
expectedOutput: "",
recipeConfig: [
{
op: "Disassemble ARM",
args: ["ARM64 (AArch64)", "ARM", "Little Endian", 0, true, true],
},
],
},
]);

View File

@ -0,0 +1,287 @@
/**
* Extract Audio Metadata operation tests.
*
* @author d0s1nt
* @copyright Crown Copyright 2025
* @license Apache-2.0
*/
import TestRegister from "../../lib/TestRegister.mjs";
import {
MP3_HEX, WAV_HEX, FLAC_HEX, AAC_HEX,
AC3_HEX, OGG_HEX, OPUS_HEX, WMA_HEX,
M4A_HEX, AIFF_HEX
} from "../../samples/Audio.mjs";
TestRegister.addTests([
// ---- MP3 ----
{
name: "Extract Audio Metadata: MP3 container and MIME",
input: MP3_HEX,
expectedMatch: /Container<\/td><td>mp3<\/td>.*MIME<\/td><td>audio\/mpeg<\/td>/s,
recipeConfig: [
{ op: "From Hex", args: ["None"] },
{ op: "Extract Audio Metadata", args: ["test.mp3", 524288] }
]
},
{
name: "Extract Audio Metadata: MP3 common tags (title, artist)",
input: MP3_HEX,
expectedMatch: /Title<\/td><td>Galway<\/td>.*Artist<\/td><td>Kevin MacLeod<\/td>/s,
recipeConfig: [
{ op: "From Hex", args: ["None"] },
{ op: "Extract Audio Metadata", args: ["test.mp3", 524288] }
]
},
{
name: "Extract Audio Metadata: MP3 ID3v2 frames (TIT2, TPE1, TSSE)",
input: MP3_HEX,
expectedMatch: /ID3v2 Frames.*TIT2.*Galway.*TPE1.*Kevin MacLeod.*TSSE.*Lavf56\.40\.101/s,
recipeConfig: [
{ op: "From Hex", args: ["None"] },
{ op: "Extract Audio Metadata", args: ["test.mp3", 524288] }
]
},
{
name: "Extract Audio Metadata: MP3 detections (id3v2)",
input: MP3_HEX,
expectedMatch: /Metadata systems<\/td><td>id3v2<\/td>/,
recipeConfig: [
{ op: "From Hex", args: ["None"] },
{ op: "Extract Audio Metadata", args: ["test.mp3", 524288] }
]
},
// ---- WAV ----
{
name: "Extract Audio Metadata: WAV container and MIME",
input: WAV_HEX,
expectedMatch: /Container<\/td><td>wav<\/td>.*MIME<\/td><td>audio\/wav<\/td>/s,
recipeConfig: [
{ op: "From Hex", args: ["None"] },
{ op: "Extract Audio Metadata", args: ["test.wav", 524288] }
]
},
{
name: "Extract Audio Metadata: WAV RIFF chunks (fmt)",
input: WAV_HEX,
expectedMatch: /RIFF Chunks.*fmt .*16 bytes @ offset 20/s,
recipeConfig: [
{ op: "From Hex", args: ["None"] },
{ op: "Extract Audio Metadata", args: ["test.wav", 524288] }
]
},
// ---- FLAC ----
{
name: "Extract Audio Metadata: FLAC container and common tags",
input: FLAC_HEX,
expectedMatch: /Container<\/td><td>flac<\/td>.*Title<\/td><td>Galway<\/td>.*Artist<\/td><td>Kevin MacLeod<\/td>/s,
recipeConfig: [
{ op: "From Hex", args: ["None"] },
{ op: "Extract Audio Metadata", args: ["test.flac", 524288] }
]
},
{
name: "Extract Audio Metadata: FLAC metadata blocks (STREAMINFO, VORBIS_COMMENT)",
input: FLAC_HEX,
expectedMatch: /FLAC Metadata Blocks.*STREAMINFO<\/td><td>34 bytes<\/td>.*VORBIS_COMMENT<\/td><td>86 bytes<\/td>/s,
recipeConfig: [
{ op: "From Hex", args: ["None"] },
{ op: "Extract Audio Metadata", args: ["test.flac", 524288] }
]
},
{
name: "Extract Audio Metadata: FLAC Vorbis comments (vendor, tags)",
input: FLAC_HEX,
expectedMatch: /Vorbis Comments.*Vendor<\/td><td>Lavf56\.40\.101<\/td>.*TITLE<\/td><td>Galway<\/td>.*ARTIST<\/td><td>Kevin MacLeod<\/td>.*ENCODER<\/td><td>Lavf56\.40\.101<\/td>/s,
recipeConfig: [
{ op: "From Hex", args: ["None"] },
{ op: "Extract Audio Metadata", args: ["test.flac", 524288] }
]
},
{
name: "Extract Audio Metadata: FLAC detections",
input: FLAC_HEX,
expectedMatch: /Metadata systems<\/td><td>flac_metablocks, vorbis_comments<\/td>/,
recipeConfig: [
{ op: "From Hex", args: ["None"] },
{ op: "Extract Audio Metadata", args: ["test.flac", 524288] }
]
},
// ---- AAC ----
{
name: "Extract Audio Metadata: AAC container and MIME",
input: AAC_HEX,
expectedMatch: /Container<\/td><td>aac<\/td>.*MIME<\/td><td>audio\/aac<\/td>/s,
recipeConfig: [
{ op: "From Hex", args: ["None"] },
{ op: "Extract Audio Metadata", args: ["test.aac", 524288] }
]
},
{
name: "Extract Audio Metadata: AAC ADTS technical fields",
input: AAC_HEX,
expectedMatch: /AAC ADTS.*mpeg_version<\/td><td>MPEG-4<\/td>.*profile<\/td><td>LC<\/td>.*sample_rate<\/td><td>44100<\/td>.*channel_description<\/td><td>stereo<\/td>/s,
recipeConfig: [
{ op: "From Hex", args: ["None"] },
{ op: "Extract Audio Metadata", args: ["test.aac", 524288] }
]
},
// ---- AC3 ----
{
name: "Extract Audio Metadata: AC3 container and MIME",
input: AC3_HEX,
expectedMatch: /Container<\/td><td>ac3<\/td>.*MIME<\/td><td>audio\/ac3<\/td>/s,
recipeConfig: [
{ op: "From Hex", args: ["None"] },
{ op: "Extract Audio Metadata", args: ["test.ac3", 524288] }
]
},
{
name: "Extract Audio Metadata: AC3 technical fields (sample rate, bitrate, channels)",
input: AC3_HEX,
expectedMatch: /AC3 \(Dolby Digital\).*sample_rate<\/td><td>44100<\/td>.*bitrate_kbps<\/td><td>192<\/td>.*channel_layout<\/td><td>2\.0 \(L R\)<\/td>/s,
recipeConfig: [
{ op: "From Hex", args: ["None"] },
{ op: "Extract Audio Metadata", args: ["test.ac3", 524288] }
]
},
// ---- OGG Vorbis ----
{
name: "Extract Audio Metadata: OGG container and common tags",
input: OGG_HEX,
expectedMatch: /Container<\/td><td>ogg<\/td>.*Title<\/td><td>Galway<\/td>.*Artist<\/td><td>Kevin MacLeod<\/td>/s,
recipeConfig: [
{ op: "From Hex", args: ["None"] },
{ op: "Extract Audio Metadata", args: ["test.ogg", 524288] }
]
},
{
name: "Extract Audio Metadata: OGG Vorbis comments (vendor, encoder)",
input: OGG_HEX,
expectedMatch: /Vorbis Comments.*Vendor<\/td><td>Lavf56\.40\.101<\/td>.*ENCODER<\/td><td>Lavc56\.60\.100 libvorbis<\/td>/s,
recipeConfig: [
{ op: "From Hex", args: ["None"] },
{ op: "Extract Audio Metadata", args: ["test.ogg", 524288] }
]
},
// ---- Opus ----
{
name: "Extract Audio Metadata: Opus container and common tags",
input: OPUS_HEX,
expectedMatch: /Container<\/td><td>opus<\/td>.*Title<\/td><td>Galway<\/td>.*Artist<\/td><td>Kevin MacLeod<\/td>/s,
recipeConfig: [
{ op: "From Hex", args: ["None"] },
{ op: "Extract Audio Metadata", args: ["test.opus", 524288] }
]
},
{
name: "Extract Audio Metadata: Opus Vorbis comments (vendor, encoder)",
input: OPUS_HEX,
expectedMatch: /Vorbis Comments.*Vendor<\/td><td>Lavf58\.19\.102<\/td>.*ENCODER<\/td><td>Lavc58\.34\.100 libopus<\/td>/s,
recipeConfig: [
{ op: "From Hex", args: ["None"] },
{ op: "Extract Audio Metadata", args: ["test.opus", 524288] }
]
},
// ---- WMA/ASF ----
{
name: "Extract Audio Metadata: WMA container and MIME",
input: WMA_HEX,
expectedMatch: /Container<\/td><td>wma<\/td>.*MIME<\/td><td>audio\/x-ms-wma<\/td>/s,
recipeConfig: [
{ op: "From Hex", args: ["None"] },
{ op: "Extract Audio Metadata", args: ["test.wma", 524288] }
]
},
{
name: "Extract Audio Metadata: WMA common tags (title, artist)",
input: WMA_HEX,
expectedMatch: /Title<\/td><td>Galway<\/td>.*Artist<\/td><td>Kevin MacLeod<\/td>/s,
recipeConfig: [
{ op: "From Hex", args: ["None"] },
{ op: "Extract Audio Metadata", args: ["test.wma", 524288] }
]
},
{
name: "Extract Audio Metadata: WMA ASF Content Description",
input: WMA_HEX,
expectedMatch: /ASF Content Description.*title<\/td><td>Galway<\/td>.*author<\/td><td>Kevin MacLeod<\/td>/s,
recipeConfig: [
{ op: "From Hex", args: ["None"] },
{ op: "Extract Audio Metadata", args: ["test.wma", 524288] }
]
},
{
name: "Extract Audio Metadata: WMA ASF Extended Content (encoding settings)",
input: WMA_HEX,
expectedMatch: /ASF Extended Content.*WM\/EncodingSettings<\/td><td>Lavf56\.40\.101<\/td>/s,
recipeConfig: [
{ op: "From Hex", args: ["None"] },
{ op: "Extract Audio Metadata", args: ["test.wma", 524288] }
]
},
{
name: "Extract Audio Metadata: WMA detections",
input: WMA_HEX,
expectedMatch: /Metadata systems<\/td><td>asf_header, asf_content_desc, asf_ext_content_desc<\/td>/,
recipeConfig: [
{ op: "From Hex", args: ["None"] },
{ op: "Extract Audio Metadata", args: ["test.wma", 524288] }
]
},
// ---- M4A ----
{
name: "Extract Audio Metadata: M4A container, MIME and brand",
input: M4A_HEX,
expectedMatch: /Container<\/td><td>m4a<\/td>.*MIME<\/td><td>audio\/mp4<\/td>.*Brand<\/td><td>M4A <\/td>/s,
recipeConfig: [
{ op: "From Hex", args: ["None"] },
{ op: "Extract Audio Metadata", args: ["test.m4a", 524288] }
]
},
{
name: "Extract Audio Metadata: M4A top-level atoms (ftyp, mdat)",
input: M4A_HEX,
expectedMatch: /MP4 Top-Level Atoms.*ftyp<\/td>.*mdat<\/td>/s,
recipeConfig: [
{ op: "From Hex", args: ["None"] },
{ op: "Extract Audio Metadata", args: ["test.m4a", 524288] }
]
},
// ---- AIFF ----
{
name: "Extract Audio Metadata: AIFF container, MIME and brand",
input: AIFF_HEX,
expectedMatch: /Container<\/td><td>aiff<\/td>.*MIME<\/td><td>audio\/aiff<\/td>.*Brand<\/td><td>AIFF<\/td>/s,
recipeConfig: [
{ op: "From Hex", args: ["None"] },
{ op: "Extract Audio Metadata", args: ["test.aiff", 524288] }
]
},
{
name: "Extract Audio Metadata: AIFF common tag (title from NAME chunk)",
input: AIFF_HEX,
expectedMatch: /Title<\/td><td>Galway<\/td>/,
recipeConfig: [
{ op: "From Hex", args: ["None"] },
{ op: "Extract Audio Metadata", args: ["test.aiff", 524288] }
]
},
{
name: "Extract Audio Metadata: AIFF chunks (NAME)",
input: AIFF_HEX,
expectedMatch: /AIFF Chunks.*NAME<\/td><td>Galway<\/td>/s,
recipeConfig: [
{ op: "From Hex", args: ["None"] },
{ op: "Extract Audio Metadata", args: ["test.aiff", 524288] }
]
},
]);

View File

@ -11,44 +11,70 @@ TestRegister.addTests([
{
name: "Extract email address",
input: "email@example.com\nfirstname.lastname@example.com\nemail@subdomain.example.com\nfirstname+lastname@example.com\n1234567890@example.com\nemail@example-one.com\n_______@example.com email@example.name\nemail@example.museum email@example.co.jp firstname-lastname@example.com",
expectedOutput: "email@example.com\nfirstname.lastname@example.com\nemail@subdomain.example.com\nfirstname+lastname@example.com\n1234567890@example.com\nemail@example-one.com\n_______@example.com\nemail@example.name\nemail@example.museum\nemail@example.co.jp\nfirstname-lastname@example.com",
expectedOutput:
"email@example.com\nfirstname.lastname@example.com\nemail@subdomain.example.com\nfirstname+lastname@example.com\n1234567890@example.com\nemail@example-one.com\n_______@example.com\nemail@example.name\nemail@example.museum\nemail@example.co.jp\nfirstname-lastname@example.com",
recipeConfig: [
{
"op": "Extract email addresses",
"args": [false]
op: "Extract email addresses",
args: [false],
},
],
},
{
name: "Extract email address - Display total",
input: "email@example.com\nfirstname.lastname@example.com\nemail@subdomain.example.com\nfirstname+lastname@example.com\n1234567890@example.com\nemail@example-one.com\n_______@example.com email@example.name\nemail@example.museum email@example.co.jp firstname-lastname@example.com",
expectedOutput: "Total found: 11\n\nemail@example.com\nfirstname.lastname@example.com\nemail@subdomain.example.com\nfirstname+lastname@example.com\n1234567890@example.com\nemail@example-one.com\n_______@example.com\nemail@example.name\nemail@example.museum\nemail@example.co.jp\nfirstname-lastname@example.com",
expectedOutput:
"Total found: 11\n\nemail@example.com\nfirstname.lastname@example.com\nemail@subdomain.example.com\nfirstname+lastname@example.com\n1234567890@example.com\nemail@example-one.com\n_______@example.com\nemail@example.name\nemail@example.museum\nemail@example.co.jp\nfirstname-lastname@example.com",
recipeConfig: [
{
"op": "Extract email addresses",
"args": [true]
op: "Extract email addresses",
args: [true],
},
],
},
{
name: "Extract email address (Internationalized)",
input: "\u4f0a\u662d\u5091@\u90f5\u4ef6.\u5546\u52d9 \u093e\u092e@\u092e\u094b\u0939\u0928.\u0908\u0928\u094d\u092b\u094b\n\u044e\u0437\u0435\u0440@\u0435\u043a\u0437\u0430\u043c\u043f\u043b.\u043a\u043e\u043c \u03b8\u03c3\u03b5\u03c1@\u03b5\u03c7\u03b1\u03bc\u03c0\u03bb\u03b5.\u03c8\u03bf\u03bc Jos\u1ec5Silv\u1ec5@googl\u1ec5.com\nJos\u1ec5Silv\u1ec5@google.com and Jos\u1ec5Silva@google.com\nFoO@BaR.CoM, john@192.168.10.100\ng\xf3mez@junk.br and Abc.123@example.com.\nuser+mailbox/department=shipping@example.com\n\u7528\u6237@\u4f8b\u5b50.\u5e7f\u544a\n\u0909\u092a\u092f\u094b\u0917\u0915\u0930\u094d\u0924\u093e@\u0909\u0926\u093e\u0939\u0930\u0923.\u0915\u0949\u092e\n\u044e\u0437\u0435\u0440@\u0435\u043a\u0437\u0430\u043c\u043f\u043b.\u043a\u043e\u043c\n\u03b8\u03c3\u03b5\u03c1@\u03b5\u03c7\u03b1\u03bc\u03c0\u03bb\u03b5.\u03c8\u03bf\u03bc\nD\xf6rte@S\xf6rensen.example.com\n\u0430\u0434\u0436\u0430\u0439@\u044d\u043a\u0437\u0430\u043c\u043f\u043b.\u0440\u0443\u0441\ntest@xn--bcher-kva.com",
expectedOutput: "\u4f0a\u662d\u5091@\u90f5\u4ef6.\u5546\u52d9\n\u093e\u092e@\u092e\u094b\u0939\u0928.\u0908\u0928\u094d\u092b\u094b\n\u044e\u0437\u0435\u0440@\u0435\u043a\u0437\u0430\u043c\u043f\u043b.\u043a\u043e\u043c\n\u03b8\u03c3\u03b5\u03c1@\u03b5\u03c7\u03b1\u03bc\u03c0\u03bb\u03b5.\u03c8\u03bf\u03bc\nJos\u1ec5Silv\u1ec5@googl\u1ec5.com\nJos\u1ec5Silv\u1ec5@google.com\nJos\u1ec5Silva@google.com\nFoO@BaR.CoM\njohn@192.168.10.100\ng\xf3mez@junk.br\nAbc.123@example.com\nuser+mailbox/department=shipping@example.com\n\u7528\u6237@\u4f8b\u5b50.\u5e7f\u544a\n\u0909\u092a\u092f\u094b\u0917\u0915\u0930\u094d\u0924\u093e@\u0909\u0926\u093e\u0939\u0930\u0923.\u0915\u0949\u092e\n\u044e\u0437\u0435\u0440@\u0435\u043a\u0437\u0430\u043c\u043f\u043b.\u043a\u043e\u043c\n\u03b8\u03c3\u03b5\u03c1@\u03b5\u03c7\u03b1\u03bc\u03c0\u03bb\u03b5.\u03c8\u03bf\u03bc\nD\xf6rte@S\xf6rensen.example.com\n\u0430\u0434\u0436\u0430\u0439@\u044d\u043a\u0437\u0430\u043c\u043f\u043b.\u0440\u0443\u0441\ntest@xn--bcher-kva.com",
expectedOutput:
"\u4f0a\u662d\u5091@\u90f5\u4ef6.\u5546\u52d9\n\u093e\u092e@\u092e\u094b\u0939\u0928.\u0908\u0928\u094d\u092b\u094b\n\u044e\u0437\u0435\u0440@\u0435\u043a\u0437\u0430\u043c\u043f\u043b.\u043a\u043e\u043c\n\u03b8\u03c3\u03b5\u03c1@\u03b5\u03c7\u03b1\u03bc\u03c0\u03bb\u03b5.\u03c8\u03bf\u03bc\nJos\u1ec5Silv\u1ec5@googl\u1ec5.com\nJos\u1ec5Silv\u1ec5@google.com\nJos\u1ec5Silva@google.com\nFoO@BaR.CoM\njohn@192.168.10.100\ng\xf3mez@junk.br\nAbc.123@example.com\nuser+mailbox/department=shipping@example.com\n\u7528\u6237@\u4f8b\u5b50.\u5e7f\u544a\n\u0909\u092a\u092f\u094b\u0917\u0915\u0930\u094d\u0924\u093e@\u0909\u0926\u093e\u0939\u0930\u0923.\u0915\u0949\u092e\n\u044e\u0437\u0435\u0440@\u0435\u043a\u0437\u0430\u043c\u043f\u043b.\u043a\u043e\u043c\n\u03b8\u03c3\u03b5\u03c1@\u03b5\u03c7\u03b1\u03bc\u03c0\u03bb\u03b5.\u03c8\u03bf\u03bc\nD\xf6rte@S\xf6rensen.example.com\n\u0430\u0434\u0436\u0430\u0439@\u044d\u043a\u0437\u0430\u043c\u043f\u043b.\u0440\u0443\u0441\ntest@xn--bcher-kva.com",
recipeConfig: [
{
"op": "Extract email addresses",
"args": [false]
op: "Extract email addresses",
args: [false],
},
],
},
{
name: "Extract email address - Display total (Internationalized)",
input: "\u4f0a\u662d\u5091@\u90f5\u4ef6.\u5546\u52d9 \u093e\u092e@\u092e\u094b\u0939\u0928.\u0908\u0928\u094d\u092b\u094b\n\u044e\u0437\u0435\u0440@\u0435\u043a\u0437\u0430\u043c\u043f\u043b.\u043a\u043e\u043c \u03b8\u03c3\u03b5\u03c1@\u03b5\u03c7\u03b1\u03bc\u03c0\u03bb\u03b5.\u03c8\u03bf\u03bc Jos\u1ec5Silv\u1ec5@googl\u1ec5.com\nJos\u1ec5Silv\u1ec5@google.com and Jos\u1ec5Silva@google.com\nFoO@BaR.CoM, john@192.168.10.100\ng\xf3mez@junk.br and Abc.123@example.com.\nuser+mailbox/department=shipping@example.com\n\u7528\u6237@\u4f8b\u5b50.\u5e7f\u544a\n\u0909\u092a\u092f\u094b\u0917\u0915\u0930\u094d\u0924\u093e@\u0909\u0926\u093e\u0939\u0930\u0923.\u0915\u0949\u092e\n\u044e\u0437\u0435\u0440@\u0435\u043a\u0437\u0430\u043c\u043f\u043b.\u043a\u043e\u043c\n\u03b8\u03c3\u03b5\u03c1@\u03b5\u03c7\u03b1\u03bc\u03c0\u03bb\u03b5.\u03c8\u03bf\u03bc\nD\xf6rte@S\xf6rensen.example.com\n\u0430\u0434\u0436\u0430\u0439@\u044d\u043a\u0437\u0430\u043c\u043f\u043b.\u0440\u0443\u0441\ntest@xn--bcher-kva.com",
expectedOutput: "Total found: 19\n\n\u4f0a\u662d\u5091@\u90f5\u4ef6.\u5546\u52d9\n\u093e\u092e@\u092e\u094b\u0939\u0928.\u0908\u0928\u094d\u092b\u094b\n\u044e\u0437\u0435\u0440@\u0435\u043a\u0437\u0430\u043c\u043f\u043b.\u043a\u043e\u043c\n\u03b8\u03c3\u03b5\u03c1@\u03b5\u03c7\u03b1\u03bc\u03c0\u03bb\u03b5.\u03c8\u03bf\u03bc\nJos\u1ec5Silv\u1ec5@googl\u1ec5.com\nJos\u1ec5Silv\u1ec5@google.com\nJos\u1ec5Silva@google.com\nFoO@BaR.CoM\njohn@192.168.10.100\ng\xf3mez@junk.br\nAbc.123@example.com\nuser+mailbox/department=shipping@example.com\n\u7528\u6237@\u4f8b\u5b50.\u5e7f\u544a\n\u0909\u092a\u092f\u094b\u0917\u0915\u0930\u094d\u0924\u093e@\u0909\u0926\u093e\u0939\u0930\u0923.\u0915\u0949\u092e\n\u044e\u0437\u0435\u0440@\u0435\u043a\u0437\u0430\u043c\u043f\u043b.\u043a\u043e\u043c\n\u03b8\u03c3\u03b5\u03c1@\u03b5\u03c7\u03b1\u03bc\u03c0\u03bb\u03b5.\u03c8\u03bf\u03bc\nD\xf6rte@S\xf6rensen.example.com\n\u0430\u0434\u0436\u0430\u0439@\u044d\u043a\u0437\u0430\u043c\u043f\u043b.\u0440\u0443\u0441\ntest@xn--bcher-kva.com",
expectedOutput:
"Total found: 19\n\n\u4f0a\u662d\u5091@\u90f5\u4ef6.\u5546\u52d9\n\u093e\u092e@\u092e\u094b\u0939\u0928.\u0908\u0928\u094d\u092b\u094b\n\u044e\u0437\u0435\u0440@\u0435\u043a\u0437\u0430\u043c\u043f\u043b.\u043a\u043e\u043c\n\u03b8\u03c3\u03b5\u03c1@\u03b5\u03c7\u03b1\u03bc\u03c0\u03bb\u03b5.\u03c8\u03bf\u03bc\nJos\u1ec5Silv\u1ec5@googl\u1ec5.com\nJos\u1ec5Silv\u1ec5@google.com\nJos\u1ec5Silva@google.com\nFoO@BaR.CoM\njohn@192.168.10.100\ng\xf3mez@junk.br\nAbc.123@example.com\nuser+mailbox/department=shipping@example.com\n\u7528\u6237@\u4f8b\u5b50.\u5e7f\u544a\n\u0909\u092a\u092f\u094b\u0917\u0915\u0930\u094d\u0924\u093e@\u0909\u0926\u093e\u0939\u0930\u0923.\u0915\u0949\u092e\n\u044e\u0437\u0435\u0440@\u0435\u043a\u0437\u0430\u043c\u043f\u043b.\u043a\u043e\u043c\n\u03b8\u03c3\u03b5\u03c1@\u03b5\u03c7\u03b1\u03bc\u03c0\u03bb\u03b5.\u03c8\u03bf\u03bc\nD\xf6rte@S\xf6rensen.example.com\n\u0430\u0434\u0436\u0430\u0439@\u044d\u043a\u0437\u0430\u043c\u043f\u043b.\u0440\u0443\u0441\ntest@xn--bcher-kva.com",
recipeConfig: [
{
"op": "Extract email addresses",
"args": [true]
op: "Extract email addresses",
args: [true],
},
],
},
{
name: "Extract email address - IP address",
input: "yaunwfkb\nexample@[127.0.0.1]\n091nvka",
expectedOutput: "example@[127.0.0.1]",
recipeConfig: [
{
op: "Extract email addresses",
args: [false],
},
],
},
{
name: "Extract email address - invalid IP address",
input: "yaunwfkb\nfalse_positive@[1.2.3.]\n091nvka",
expectedOutput: "",
recipeConfig: [
{
op: "Extract email addresses",
args: [false],
},
],
},

View File

@ -5,7 +5,7 @@
* @copyright Karsten Silkenbäumer 2019
* @license Apache-2.0
*/
import TestRegister from "../TestRegister";
import TestRegister from "../../lib/TestRegister.mjs";
TestRegister.addTests([
{
@ -47,7 +47,7 @@ TestRegister.addTests([
{
name: "Fernet Encrypt: no input",
input: "",
expectedMatch: /^gAAAAABce-[\w-]+={0,2}$/,
expectedMatch: /^gAAA[\w-]+={0,2}$/,
recipeConfig: [
{
op: "Fernet Encrypt",
@ -69,12 +69,27 @@ TestRegister.addTests([
{
name: "Fernet Encrypt: valid arguments",
input: "This is a secret message.\n",
expectedMatch: /^gAAAAABce-[\w-]+={0,2}$/,
expectedMatch: /^gAAA[\w-]+={0,2}$/,
recipeConfig: [
{
op: "Fernet Encrypt",
args: ["MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI="]
}
],
},
{
name: "Fernet Encrypt/Decrypt: round trip",
input: "This is a secret message.\n",
expectedOutput: "This is a secret message.\n",
recipeConfig: [
{
op: "Fernet Encrypt",
args: ["MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI="]
},
{
op: "Fernet Decrypt",
args: ["MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI="]
},
],
}
]);

View File

@ -0,0 +1,32 @@
/**
* Jq tests.
*
* @author rtpt-romankarwacik [roman.karwacik@redteam-pentesting.de]
*
*/
import TestRegister from "../../lib/TestRegister.mjs";
TestRegister.addTests([
{
name: "Get raw JSON Property",
input: '{"data": "testString\\u0000"}',
expectedOutput: "testString\u0000",
recipeConfig: [
{
op: "Jq",
args: [".data", true],
},
],
},
{
name: "Get JSON Property",
input: '{"data": "testString\\u0000"}',
expectedOutput: "\"testString\\u0000\"",
recipeConfig: [
{
op: "Jq",
args: [".data", false],
},
],
},
]);

View File

@ -147,4 +147,24 @@ dc;ii;hv;ig;hr;hf;dc;he;hj;hv;hv;ie;hg;du",
}
]
},
{
name: "Empty input through From Hex and To Modhex returns empty output",
input: "",
expectedOutput: "",
recipeConfig: [
{
"op": "From Hex",
"args": [
"Auto"
]
},
{
"op": "To Modhex",
"args": [
"Space",
0
]
}
]
},
]);

View File

@ -0,0 +1,147 @@
/**
* Parity Bit tests
*
* @author j83305 [awz22@protonmail.com]
* @copyright Crown Copyright 2020
* @license Apache-2.0
*/
import TestRegister from "../../lib/TestRegister.mjs";
TestRegister.addTests([
{
name: "Parity bit encode in even parity, 1 block of binary of arbitrary length, prepend, even number of 1s",
input: "01010101 10101010",
expectedOutput: "001010101 10101010",
recipeConfig: [
{
"op": "Parity Bit",
"args": [
"Even Parity",
"Start",
"Encode",
""
]
}
]
},
{
name: "Parity bit encode in even parity, 1 block of binary of arbitrary length, prepend, odd number of 1s",
input: "01010101 10101011",
expectedOutput: "101010101 10101011",
recipeConfig: [
{
"op": "Parity Bit",
"args": [
"Even Parity",
"Start",
"Encode",
""
]
}
]
},
{
name: "Parity bit encode in even parity, 1 block of binary of arbitrary length, append, odd number of 1s",
input: "01010101 10101011",
expectedOutput: "01010101 101010111",
recipeConfig: [
{
"op": "Parity Bit",
"args": [
"Even Parity",
"End",
"Encode",
""
]
}
]
},
{
name: "Parity bit encode in odd parity, 1 block of binary of arbitrary length, prepend, even number of 1s",
input: "01010101 10101010",
expectedOutput: "101010101 10101010",
recipeConfig: [
{
"op": "Parity Bit",
"args": [
"Odd Parity",
"Start",
"Encode",
""
]
}
]
},
{
name: "Parity bit encode in odd parity, 1 block of binary of arbitrary length, prepend, odd number of 1s",
input: "01010101 10101011",
expectedOutput: "001010101 10101011",
recipeConfig: [
{
"op": "Parity Bit",
"args": [
"Odd Parity",
"Start",
"Encode",
""
]
}
]
},
{
name: "Parity bit encode in odd parity, 1 block of binary of arbitrary length, append, odd number of 1s",
input: "01010101 10101011",
expectedOutput: "01010101 101010110",
recipeConfig: [
{
"op": "Parity Bit",
"args": [
"Odd Parity",
"End",
"Encode",
""
]
}
]
},
{
name: "Parity bit encode in even parity, binary for 'hello world!', prepend to each byte",
input: "hello world!",
expectedOutput: "101101000 001100101 001101100 001101100 001101111 100100000 001110111 001101111 001110010 001101100 101100100 000100001",
recipeConfig: [
{
"op": "To Binary",
"args": ["Space"]
},
{
"op": "Parity Bit",
"args": [
"Even Parity",
"Start",
"Encode",
" "
]
}
]
},
{
name: "Parity bit encode in odd parity, binary for 'hello world!', append to each byte",
input: "hello world!",
expectedOutput: "011010000 011001011 011011001 011011001 011011111 001000000 011101111 011011111 011100101 011011001 011001000 001000011",
recipeConfig: [
{
"op": "To Binary",
"args": ["Space"]
},
{
"op": "Parity Bit",
"args": [
"Odd Parity",
"End",
"Encode",
" "
]
}
]
},
]);

View File

@ -0,0 +1,45 @@
/**
* Parse Ethernet frame tests.
*
* @author tedk [tedk@ted.do]
* @copyright Crown Copyright 2017
* @license Apache-2.0
*/
import TestRegister from "../../lib/TestRegister.mjs";
TestRegister.addTests([
{
name: "Parse plain Ethernet frame",
input: "000000000000ffffffffffff08004500",
expectedOutput: "Source MAC: ff:ff:ff:ff:ff:ff\nDestination MAC: 00:00:00:00:00:00\nData:\n45 00",
recipeConfig: [
{
"op": "Parse Ethernet frame",
"args": ["Hex", "Text output"]
}
]
},
// Example PCAP data from: https://packetlife.net/captures/protocol/vlan/
{
name: "Parse Ethernet frame with one VLAN tag (802.1q)",
input: "01000ccdcdd00013c3dfae188100a0760165aaaa",
expectedOutput: "Source MAC: 00:13:c3:df:ae:18\nDestination MAC: 01:00:0c:cd:cd:d0\nVLAN: 118\nData:\naa aa",
recipeConfig: [
{
"op": "Parse Ethernet frame",
"args": ["Hex", "Text output"]
}
]
},
{
name: "Parse Ethernet frame with two VLAN tags (802.1ad)",
input: "0019aa7de688002155c8f13c810000d18100001408004500",
expectedOutput: "Source MAC: 00:21:55:c8:f1:3c\nDestination MAC: 00:19:aa:7d:e6:88\nVLAN: 209, 20\nData:\n45 00",
recipeConfig: [
{
"op": "Parse Ethernet frame",
"args": ["Hex", "Text output"]
}
]
}
]);

View File

@ -0,0 +1,487 @@
/**
* RC6 cipher tests.
*
* Test vectors from the IETF draft:
* "Test Vectors for RC6 and RC5"
* https://datatracker.ietf.org/doc/html/draft-krovetz-rc6-rc5-vectors-00
*
* @author Medjedtxm
* @copyright Crown Copyright 2026
* @license Apache-2.0
*/
import TestRegister from "../../lib/TestRegister.mjs";
TestRegister.addTests([
// ============================================================
// IETF TEST VECTORS - RC6-8/12/4
// ============================================================
{
name: "RC6-8/12/4: IETF vector encrypt",
input: "00010203",
expectedOutput: "aefc4612",
recipeConfig: [
{
op: "RC6 Encrypt",
args: [
{ string: "00010203", option: "Hex" },
{ string: "", option: "Hex" },
"ECB", "Hex", "Hex", "NO", 8, 12
]
}
]
},
{
name: "RC6-8/12/4: IETF vector decrypt",
input: "aefc4612",
expectedOutput: "00010203",
recipeConfig: [
{
op: "RC6 Decrypt",
args: [
{ string: "00010203", option: "Hex" },
{ string: "", option: "Hex" },
"ECB", "Hex", "Hex", "NO", 8, 12
]
}
]
},
// ============================================================
// IETF TEST VECTORS - RC6-16/16/8
// ============================================================
{
name: "RC6-16/16/8: IETF vector encrypt",
input: "0001020304050607",
expectedOutput: "2ff0b68eaeffad5b",
recipeConfig: [
{
op: "RC6 Encrypt",
args: [
{ string: "0001020304050607", option: "Hex" },
{ string: "", option: "Hex" },
"ECB", "Hex", "Hex", "NO", 16, 16
]
}
]
},
{
name: "RC6-16/16/8: IETF vector decrypt",
input: "2ff0b68eaeffad5b",
expectedOutput: "0001020304050607",
recipeConfig: [
{
op: "RC6 Decrypt",
args: [
{ string: "0001020304050607", option: "Hex" },
{ string: "", option: "Hex" },
"ECB", "Hex", "Hex", "NO", 16, 16
]
}
]
},
// ============================================================
// IETF TEST VECTORS - RC6-32/20/16 (AES standard)
// ============================================================
{
name: "RC6-32/20/16: IETF vector encrypt (AES standard)",
input: "000102030405060708090a0b0c0d0e0f",
expectedOutput: "3a96f9c7f6755cfe46f00e3dcd5d2a3c",
recipeConfig: [
{
op: "RC6 Encrypt",
args: [
{ string: "000102030405060708090a0b0c0d0e0f", option: "Hex" },
{ string: "", option: "Hex" },
"ECB", "Hex", "Hex", "NO", 32, 20
]
}
]
},
{
name: "RC6-32/20/16: IETF vector decrypt (AES standard)",
input: "3a96f9c7f6755cfe46f00e3dcd5d2a3c",
expectedOutput: "000102030405060708090a0b0c0d0e0f",
recipeConfig: [
{
op: "RC6 Decrypt",
args: [
{ string: "000102030405060708090a0b0c0d0e0f", option: "Hex" },
{ string: "", option: "Hex" },
"ECB", "Hex", "Hex", "NO", 32, 20
]
}
]
},
// ============================================================
// IETF TEST VECTORS - RC6-64/24/24
// ============================================================
{
name: "RC6-64/24/24: IETF vector encrypt",
input: "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
expectedOutput: "c002de050bd55e5d36864ab9853338e6dc4a1326c6bdaaeb1bc9e4fd67886617",
recipeConfig: [
{
op: "RC6 Encrypt",
args: [
{ string: "000102030405060708090a0b0c0d0e0f1011121314151617", option: "Hex" },
{ string: "", option: "Hex" },
"ECB", "Hex", "Hex", "NO", 64, 24
]
}
]
},
{
name: "RC6-64/24/24: IETF vector decrypt",
input: "c002de050bd55e5d36864ab9853338e6dc4a1326c6bdaaeb1bc9e4fd67886617",
expectedOutput: "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
recipeConfig: [
{
op: "RC6 Decrypt",
args: [
{ string: "000102030405060708090a0b0c0d0e0f1011121314151617", option: "Hex" },
{ string: "", option: "Hex" },
"ECB", "Hex", "Hex", "NO", 64, 24
]
}
]
},
// ============================================================
// IETF TEST VECTORS - RC6-128/28/32
// ============================================================
{
name: "RC6-128/28/32: IETF vector encrypt",
input: "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f",
expectedOutput: "4ed87c64baffecd4303ee6a79aafaef575b351c024272be70a70b4a392cfc157dba52d529a79e83845bf43d67545383aed3dbf4f0d23640e44cbf6cdaa034dcb",
recipeConfig: [
{
op: "RC6 Encrypt",
args: [
{ string: "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", option: "Hex" },
{ string: "", option: "Hex" },
"ECB", "Hex", "Hex", "NO", 128, 28
]
}
]
},
{
name: "RC6-128/28/32: IETF vector decrypt",
input: "4ed87c64baffecd4303ee6a79aafaef575b351c024272be70a70b4a392cfc157dba52d529a79e83845bf43d67545383aed3dbf4f0d23640e44cbf6cdaa034dcb",
expectedOutput: "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f",
recipeConfig: [
{
op: "RC6 Decrypt",
args: [
{ string: "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", option: "Hex" },
{ string: "", option: "Hex" },
"ECB", "Hex", "Hex", "NO", 128, 28
]
}
]
},
// ============================================================
// IETF TEST VECTORS - RC6-24/4/0 (non-power-of-2)
// ============================================================
{
name: "RC6-24/4/0: IETF non-standard vector encrypt (w=24, empty key)",
input: "000102030405060708090a0b",
expectedOutput: "0177982579be2ee3303269b9",
recipeConfig: [
{
op: "RC6 Encrypt",
args: [
{ string: "", option: "Hex" },
{ string: "", option: "Hex" },
"ECB", "Hex", "Hex", "NO", 24, 4
]
}
]
},
{
name: "RC6-24/4/0: IETF non-standard vector decrypt (w=24, empty key)",
input: "0177982579be2ee3303269b9",
expectedOutput: "000102030405060708090a0b",
recipeConfig: [
{
op: "RC6 Decrypt",
args: [
{ string: "", option: "Hex" },
{ string: "", option: "Hex" },
"ECB", "Hex", "Hex", "NO", 24, 4
]
}
]
},
// ============================================================
// IETF TEST VECTORS - RC6-80/4/12 (non-power-of-2)
// ============================================================
{
name: "RC6-80/4/12: IETF non-standard vector encrypt (w=80)",
input: "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f2021222324252627",
expectedOutput: "26d9d6128601d06dec3817d401f1c0ff715473543875da417c2116d1e87c919a49311b00b4e17962",
recipeConfig: [
{
op: "RC6 Encrypt",
args: [
{ string: "000102030405060708090a0b", option: "Hex" },
{ string: "", option: "Hex" },
"ECB", "Hex", "Hex", "NO", 80, 4
]
}
]
},
{
name: "RC6-80/4/12: IETF non-standard vector decrypt (w=80)",
input: "26d9d6128601d06dec3817d401f1c0ff715473543875da417c2116d1e87c919a49311b00b4e17962",
expectedOutput: "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f2021222324252627",
recipeConfig: [
{
op: "RC6 Decrypt",
args: [
{ string: "000102030405060708090a0b", option: "Hex" },
{ string: "", option: "Hex" },
"ECB", "Hex", "Hex", "NO", 80, 4
]
}
]
},
// ============================================================
// ADDITIONAL KEY SIZE TESTS - RC6-32 (192-bit and 256-bit keys)
// ============================================================
{
name: "RC6-32/20/24: 192-bit key encrypt",
input: "000102030405060708090a0b0c0d0e0f",
expectedOutput: "a68a14ff1342262a2bbd21f7966615eb",
recipeConfig: [
{
op: "RC6 Encrypt",
args: [
{ string: "000102030405060708090a0b0c0d0e0f1011121314151617", option: "Hex" },
{ string: "", option: "Hex" },
"ECB", "Hex", "Hex", "NO", 32, 20
]
}
]
},
{
name: "RC6-32/20/32: 256-bit key encrypt",
input: "000102030405060708090a0b0c0d0e0f",
expectedOutput: "921c3ecd43d9426a90089334d67aea2e",
recipeConfig: [
{
op: "RC6 Encrypt",
args: [
{ string: "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", option: "Hex" },
{ string: "", option: "Hex" },
"ECB", "Hex", "Hex", "NO", 32, 20
]
}
]
},
// ============================================================
// ROUND-TRIP TESTS - One per word size to verify encrypt/decrypt
// ============================================================
{
name: "RC6-8 Round-trip: CBC mode",
input: "Hello World!",
expectedOutput: "Hello World!",
recipeConfig: [
{
op: "RC6 Encrypt",
args: [
{ string: "mysecret", option: "UTF8" },
{ string: "abcd", option: "UTF8" },
"CBC", "Raw", "Hex", "PKCS5", 8, 12
]
},
{
op: "RC6 Decrypt",
args: [
{ string: "mysecret", option: "UTF8" },
{ string: "abcd", option: "UTF8" },
"CBC", "Hex", "Raw", "PKCS5", 8, 12
]
}
]
},
{
name: "RC6-16 Round-trip: CBC mode",
input: "The quick brown fox",
expectedOutput: "The quick brown fox",
recipeConfig: [
{
op: "RC6 Encrypt",
args: [
{ string: "secretkey1234567", option: "UTF8" },
{ string: "initvec!", option: "UTF8" },
"CBC", "Raw", "Hex", "PKCS5", 16, 16
]
},
{
op: "RC6 Decrypt",
args: [
{ string: "secretkey1234567", option: "UTF8" },
{ string: "initvec!", option: "UTF8" },
"CBC", "Hex", "Raw", "PKCS5", 16, 16
]
}
]
},
{
name: "RC6-32 Round-trip: CBC mode",
input: "The quick brown fox jumps over the lazy dog",
expectedOutput: "The quick brown fox jumps over the lazy dog",
recipeConfig: [
{
op: "RC6 Encrypt",
args: [
{ string: "aabbccddeeff00112233445566778899", option: "Hex" },
{ string: "00112233445566778899aabbccddeeff", option: "Hex" },
"CBC", "Raw", "Hex", "PKCS5", 32, 20
]
},
{
op: "RC6 Decrypt",
args: [
{ string: "aabbccddeeff00112233445566778899", option: "Hex" },
{ string: "00112233445566778899aabbccddeeff", option: "Hex" },
"CBC", "Hex", "Raw", "PKCS5", 32, 20
]
}
]
},
{
name: "RC6-64 Round-trip: CBC mode",
input: "RC6 with 64-bit words is powerful!",
expectedOutput: "RC6 with 64-bit words is powerful!",
recipeConfig: [
{
op: "RC6 Encrypt",
args: [
{ string: "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", option: "Hex" },
{ string: "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", option: "Hex" },
"CBC", "Raw", "Hex", "PKCS5", 64, 24
]
},
{
op: "RC6 Decrypt",
args: [
{ string: "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", option: "Hex" },
{ string: "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", option: "Hex" },
"CBC", "Hex", "Raw", "PKCS5", 64, 24
]
}
]
},
{
name: "RC6-128 Round-trip: ECB mode",
input: "RC6 with 128-bit words provides massive block size for testing purposes!",
expectedOutput: "RC6 with 128-bit words provides massive block size for testing purposes!",
recipeConfig: [
{
op: "RC6 Encrypt",
args: [
{ string: "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", option: "Hex" },
{ string: "", option: "Hex" },
"ECB", "Raw", "Hex", "PKCS5", 128, 28
]
},
{
op: "RC6 Decrypt",
args: [
{ string: "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", option: "Hex" },
{ string: "", option: "Hex" },
"ECB", "Hex", "Raw", "PKCS5", 128, 28
]
}
]
},
// ============================================================
// STREAM MODES TEST - Verify CFB/OFB/CTR work correctly
// ============================================================
{
name: "RC6-32 Round-trip: CTR mode",
input: "CTR mode test message",
expectedOutput: "CTR mode test message",
recipeConfig: [
{
op: "RC6 Encrypt",
args: [
{ string: "00112233445566778899aabbccddeeff", option: "Hex" },
{ string: "00000000000000000000000000000001", option: "Hex" },
"CTR", "Raw", "Hex", "PKCS5", 32, 20
]
},
{
op: "RC6 Decrypt",
args: [
{ string: "00112233445566778899aabbccddeeff", option: "Hex" },
{ string: "00000000000000000000000000000001", option: "Hex" },
"CTR", "Hex", "Raw", "PKCS5", 32, 20
]
}
]
},
// ============================================================
// CUSTOM ROUNDS TEST - Verify non-standard round count works
// ============================================================
{
name: "RC6-32 Round-trip: Custom 8 rounds",
input: "Testing custom rounds",
expectedOutput: "Testing custom rounds",
recipeConfig: [
{
op: "RC6 Encrypt",
args: [
{ string: "00112233445566778899aabbccddeeff", option: "Hex" },
{ string: "", option: "Hex" },
"ECB", "Raw", "Hex", "PKCS5", 32, 8
]
},
{
op: "RC6 Decrypt",
args: [
{ string: "00112233445566778899aabbccddeeff", option: "Hex" },
{ string: "", option: "Hex" },
"ECB", "Hex", "Raw", "PKCS5", 32, 8
]
}
]
},
// ============================================================
// EDGE CASE TEST - Padding boundary
// ============================================================
{
name: "RC6-32 Round-trip: Exact block size input",
input: "1234567890123456",
expectedOutput: "1234567890123456",
recipeConfig: [
{
op: "RC6 Encrypt",
args: [
{ string: "00112233445566778899aabbccddeeff", option: "Hex" },
{ string: "", option: "Hex" },
"ECB", "Raw", "Hex", "PKCS5", 32, 20
]
},
{
op: "RC6 Decrypt",
args: [
{ string: "00112233445566778899aabbccddeeff", option: "Hex" },
{ string: "", option: "Hex" },
"ECB", "Hex", "Raw", "PKCS5", 32, 20
]
}
]
}
]);

View File

@ -0,0 +1,75 @@
/**
* Regular Expression tests.
*
* @author C85297 [95289555+C85297@users.noreply.github.com]
* @copyright Crown Copyright 2017
* @license Apache-2.0
*/
import TestRegister from "../../lib/TestRegister.mjs";
import { EMAIL_REGEX } from "../../../src/core/lib/Extract.mjs";
TestRegister.addTests([
{
name: "Regular Expression - built in email regex - IP address",
input: "yaunwfkb\nexample@[127.0.0.1]\n091nvka",
expectedOutput: "example@[127.0.0.1]",
recipeConfig: [
{
op: "Regular expression",
args: [
"Email address",
EMAIL_REGEX.source,
true,
true,
false,
false,
false,
false,
"List matches",
],
},
],
},
{
name: "Regular Expression - built in email regex - invalid IP address",
input: "yaunwfkb\nfalse_positive@[1.2.3.]\n091nvka",
expectedOutput: "",
recipeConfig: [
{
op: "Regular expression",
args: [
"Email address",
EMAIL_REGEX.source,
true,
true,
false,
false,
false,
false,
"List matches",
],
},
],
},
{
name: "Regular Expression - built in email regex - IPv4 from #2318",
input: "user@[1.2.3.4]\ntest@[192.168.0.1]\nno-match@[1.2.3.]",
expectedOutput: "user@[1.2.3.4]\ntest@[192.168.0.1]",
recipeConfig: [
{
op: "Regular expression",
args: [
"Email address",
EMAIL_REGEX.source,
true,
true,
false,
false,
false,
false,
"List matches",
],
},
],
},
]);

View File

@ -0,0 +1,110 @@
/**
* RenderMarkdown tests.
*
* @copyright Crown Copyright 2026
* @license Apache-2.0
*/
import TestRegister from "../../lib/TestRegister.mjs";
TestRegister.addTests([
{
name: "Render Markdown: Nothing",
input: "",
expectedOutput: '<div style="font-family: var(--primary-font-family)"></div>',
recipeConfig: [
{
"op": "Render Markdown",
"args": []
}
]
},
{
name: "Render Markdown: Basic Text",
input: "Hello World!",
expectedOutput: '<div style="font-family: var(--primary-font-family)"><p>Hello World!</p>\n</div>',
recipeConfig: [
{
"op": "Render Markdown",
"args": []
}
]
},
{
name: "Render Markdown: Simple Markdown",
input: "# Hello World!",
expectedOutput: '<div style="font-family: var(--primary-font-family)"><h1>Hello World!</h1>\n</div>',
recipeConfig: [
{
"op": "Render Markdown",
"args": []
}
]
},
{
name: "Render Markdown: URL (not expanded)",
input: "https://gchq.github.io/CyberChef/",
expectedOutput: '<div style="font-family: var(--primary-font-family)"><p>https://gchq.github.io/CyberChef/</p>\n</div>',
recipeConfig: [
{
"op": "Render Markdown",
"args": [false, false, false]
}
]
},
{
name: "Render Markdown: URL (expanded)",
input: "https://gchq.github.io/CyberChef/",
expectedOutput: '<div style="font-family: var(--primary-font-family)"><p><a href="https://gchq.github.io/CyberChef/">https://gchq.github.io/CyberChef/</a></p>\n</div>',
recipeConfig: [
{
"op": "Render Markdown",
"args": [true, false, false]
}
]
},
{
name: "Render Markdown: Link (not expanded)",
input: "[CyberChef](https://gchq.github.io/CyberChef/)",
expectedOutput: '<div style="font-family: var(--primary-font-family)"><p><a href="https://gchq.github.io/CyberChef/">CyberChef</a></p>\n</div>',
recipeConfig: [
{
"op": "Render Markdown",
"args": [false, false, false]
}
]
},
{
name: "Render Markdown: Link (expanded)",
input: "[CyberChef](https://gchq.github.io/CyberChef/)",
expectedOutput: '<div style="font-family: var(--primary-font-family)"><p><a href="https://gchq.github.io/CyberChef/">CyberChef</a></p>\n</div>',
recipeConfig: [
{
"op": "Render Markdown",
"args": [true, false, false]
}
]
},
{
name: "Render Markdown: Link (open in new window)",
input: "[CyberChef](https://gchq.github.io/CyberChef/)",
expectedOutput: '<div style="font-family: var(--primary-font-family)"><p><a href="https://gchq.github.io/CyberChef/" target="_blank">CyberChef</a></p>\n</div>',
recipeConfig: [
{
"op": "Render Markdown",
"args": [true, false, true]
}
]
},
{
name: "Render Markdown: URL (open in new window)",
input: "https://gchq.github.io/CyberChef/",
expectedOutput: '<div style="font-family: var(--primary-font-family)"><p><a href="https://gchq.github.io/CyberChef/" target="_blank">https://gchq.github.io/CyberChef/</a></p>\n</div>',
recipeConfig: [
{
"op": "Render Markdown",
"args": [true, false, true]
}
]
},
]);

View File

@ -0,0 +1,199 @@
/**
* Text-Integer Conversion tests.
*
* @author p-leriche [philip.leriche@cantab.net]
*
* @copyright Crown Copyright 2025
* @license Apache-2.0
*/
import TestRegister from "../../lib/TestRegister.mjs";
TestRegister.addTests([
{
name: "Text-Integer Conversion quoted string to decimal",
input: "\"ABC\"",
expectedOutput: "4276803",
recipeConfig: [
{
op: "Text-Integer Conversion",
args: ["Decimal"],
},
],
},
{
name: "Text-Integer Conversion quoted string to hexadecimal",
input: "\"ABC\"",
expectedOutput: "0x414243",
recipeConfig: [
{
op: "Text-Integer Conversion",
args: ["Hexadecimal"],
},
],
},
{
name: "Text-Integer Conversion single quoted string to decimal",
input: "'Hello'",
expectedOutput: "310939249775",
recipeConfig: [
{
op: "Text-Integer Conversion",
args: ["Decimal"],
},
],
},
{
name: "Text-Integer Conversion decimal to string",
input: "4276803",
expectedOutput: "ABC",
recipeConfig: [
{
op: "Text-Integer Conversion",
args: ["String"],
},
],
},
{
name: "Text-Integer Conversion hexadecimal to string",
input: "0x48656C6C6F",
expectedOutput: "Hello",
recipeConfig: [
{
op: "Text-Integer Conversion",
args: ["String"],
},
],
},
{
name: "Text-Integer Conversion round-trip string.decimal.string",
input: "\"Test\"",
expectedOutput: "Test",
recipeConfig: [
{
op: "Text-Integer Conversion",
args: ["Decimal"],
},
{
op: "Text-Integer Conversion",
args: ["String"],
},
],
},
{
name: "Text-Integer Conversion round-trip string.hex.string",
input: "\"CyberChef\"",
expectedOutput: "CyberChef",
recipeConfig: [
{
op: "Text-Integer Conversion",
args: ["Hexadecimal"],
},
{
op: "Text-Integer Conversion",
args: ["String"],
},
],
},
{
name: "Text-Integer Conversion implicit round trip string-string Latin-1",
input: "U+00FF",
expectedOutput: "U+00FF", // U+00FF (Latin small letter y with diaeresis)
recipeConfig: [
{
op: "Unescape Unicode Characters",
args: ["U+"],
},
{
op: "Text-Integer Conversion",
args: ["String"],
},
{
op: "Escape Unicode Characters",
args: ["U+", false, 4, true],
},
],
},
{
name: "Text-Integer Conversion unquoted text to decimal",
input: "Hi",
expectedOutput: "18537",
recipeConfig: [
{
op: "Text-Integer Conversion",
args: ["Decimal"],
},
],
},
{
name: "Text-Integer Conversion single character",
input: "\"A\"",
expectedOutput: "65",
recipeConfig: [
{
op: "Text-Integer Conversion",
args: ["Decimal"],
},
],
},
{
name: "Text-Integer Conversion hex to decimal conversion",
input: "0xFF",
expectedOutput: "255",
recipeConfig: [
{
op: "Text-Integer Conversion",
args: ["Decimal"],
},
],
},
{
name: "Text-Integer Conversion decimal to hex conversion",
input: "255",
expectedOutput: "0xff",
recipeConfig: [
{
op: "Text-Integer Conversion",
args: ["Hexadecimal"],
},
],
},
{
name: "Text-Integer Conversion large number to string",
input: "113091951015816448506195587157728348242683688608116",
expectedOutput: "Mary had a little cat",
recipeConfig: [
{
op: "Text-Integer Conversion",
args: ["String"],
},
],
},
{
name: "Text-Integer Conversion whitespace handling (quoted)",
input: "\" test \"",
expectedOutput: "2314978187545944096",
recipeConfig: [
{
op: "Text-Integer Conversion",
args: ["Decimal"],
},
],
},
{
name: "Text-Integer Conversion non-Latin1 character in input",
input: "61 ce 93 61",
expectedOutput:
`Character at position 1 exceeds Latin-1 range (0-255).
Only ASCII and Latin-1 characters are supported.`,
recipeConfig: [
{
"op": "From Hex",
"args": ["Auto"]
},
{
op: "Text-Integer Conversion",
args: ["Decimal"],
},
],
},
]);

73
tests/samples/Audio.mjs Normal file
View File

@ -0,0 +1,73 @@
/**
* Audio file headers in various formats for use in tests.
*
* Each constant contains the minimal bytes needed for container
* detection and metadata extraction (trimmed from real audio files).
*
* @author d0s1nt [d0s1nt@cyberchefaudio]
* @copyright Crown Copyright 2025
* @license Apache-2.0
*/
/**
* MP3 with ID3v2.4 header title: Galway, artist: Kevin MacLeod
* 78 bytes: ID3v2 header + TIT2 + TPE1 + TSSE frames
*/
export const MP3_HEX = "4944330400000000004e544954320000000800000347616c77617900545045310000000f0000034b6576696e204d61634c656f6400545353450000000f0000034c61766635362e34302e3130310000000000000000000000";
/**
* WAV (RIFF/WAVE) header 2 channels, 16-bit, 44100 Hz
* 48 bytes: RIFF header + fmt chunk + data chunk start
*/
export const WAV_HEX = "52494646e69d2a0057415645666d7420100000000100020044ac000010b102000400100064617461489d2a0000000100";
/**
* FLAC with streaminfo + Vorbis comment block title: Galway, artist: Kevin MacLeod
* 174 bytes: fLaC magic + STREAMINFO (34 bytes) + VORBIS_COMMENT block (86 bytes)
*/
export const FLAC_HEX = "664c6143000000221200120000052e00319f0ac442f0000aa752c925754a50e5f02e117eeb46467e7053040000560d0000004c61766635362e34302e313031030000000c0000007469746c653d47616c776179140000006172746973743d4b6576696e204d61634c656f6415000000656e636f6465723d4c61766635362e34302e313031";
/**
* AAC ADTS frame header MPEG-4, LC profile, 44100 Hz, stereo
* 32 bytes: ADTS sync + frame header fields
*/
export const AAC_HEX = "fff150800bbffcde02004c61766335382e33342e31303000423590002000001e";
/**
* AC3 (Dolby Digital) sync frame header 44100 Hz, 192 kbps, 2.0 stereo
* 32 bytes: AC3 sync word + BSI fields
*/
export const AC3_HEX = "0b773968544043e106f575f0d4da1c1ac159850953e549a125736e8d37359d3f";
/**
* OGG Vorbis two OGG pages with identification + comment headers
* title: Galway, artist: Kevin MacLeod, vendor: Lavf56.40.101
* 281 bytes
*/
export const OGG_HEX = "4f6767530002000000000000000027a7032a000000002acbc833011e01766f72626973000000000244ac00000000000080b5010000000000b8014f6767530000000000000000000027a7032a010000007e1abea41168ffffffffffffffffffffffffffffff0703766f726269730d0000004c61766635362e34302e313031030000001f000000656e636f6465723d4c61766335362e36302e313030206c6962766f726269730c0000007469746c653d47616c776179140000006172746973743d4b6576696e204d61634c656f64";
/**
* Opus two OGG pages with OpusHead + OpusTags headers
* title: Galway, artist: Kevin MacLeod, vendor: Lavf58.19.102
* 233 bytes
*/
export const OPUS_HEX = "4f67675300020000000000000000919a59f200000000f6117eb601134f707573486561640102380180bb00000000004f67675300000000000000000000919a59f201000000b047e56601664f707573546167730d0000004c61766635382e31392e313032030000001d000000656e636f6465723d4c61766335382e33342e313030206c69626f7075730c0000007469746c653d47616c776179140000006172746973743d4b6576696e204d61634c656f64";
/**
* WMA/ASF ASF header with Content Description + Extended Content
* title: Galway, author: Kevin MacLeod, encoder: Lavf56.40.101
* 700 bytes: ASF Header Object + all sub-objects
*/
export const WMA_HEX = "3026b2758e66cf11a6d900aa0062ce6c8a02000000000000060000000102a1dcab8c47a9cf118ee400c00c205365680000000000000000000000000000000000000000000000bc3504000000000000803ed5deb19d0156000000000000007040490b00000000b03a7009000000001c0c00000000000002000000800c0000800c000000f40100b503bf5f2ea9cf118ee300c00c2053652e0000000000000011d2d3abbaa9cf118ee600c00c2053650600000000003326b2758e66cf11a6d900aa0062ce6c4c000000000000000e001c00000000000000470061006c0077006100790000004b006500760069006e0020004d00610063004c0065006f006400000040a4d0d207e3d21197f000a0c95ea850b40000000000000003000c007400690074006c006500000000000e00470061006c0077006100790000000e0041007500740068006f007200000000001c004b006500760069006e0020004d00610063004c0065006f0064000000280057004d002f0045006e0063006f00640069006e006700530065007400740069006e0067007300000000001c004c00610076006600350036002e00340030002e0031003000310000009107dcb7b7a9cf118ee600c00c2053657200000000000000409e69f84d5bcf11a8fd00805f5c442b50cdc3bf8f61cf118bb200aa00b4e22000000000000000001c000000080000000100000000006101020044ac0000803e0000e70210000a000000000001000000000001e702e7020100004052d1861d31d011a3a400a0c90348f664000000000000004152d1861d31d011a3a400a0c90348f60100000002001700570069006e0064006f007700730020004d006500640069006100200041007500640069006f0020005600380000000000020061013626b2758e66cf11a6d900aa0062ce6c32330400000000000000000000000000000000000000000056000000000000000101";
/**
* M4A (MPEG-4 Audio) ftyp atom with brand "M4A ", plus mdat
* 512 bytes: ftyp + free + mdat start (moov not included in slice)
*/
export const M4A_HEX = "0000001c667479704d344120000002004d34412069736f6d69736f3200000008667265650003e2236d6461742111450014500146fff10a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5de98214b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4bc211a93a09c3e310803595989841e21e02814c4d2f28f925da49e5fe61d4521f0088400d65662610788780a0563ab2a671af1d0cd2fd9997d18be037ff8852d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d";
/**
* AIFF (FORM/AIFF) header with NAME chunk = "Galway", COMM and SSND chunks
* 36 bytes: FORM header + NAME chunk + COMM chunk start
*/
export const AIFF_HEX = "464f524d002a9d84414946464e414d450000000647616c776179434f4d4d00000012000200";