Merge branch 'master' into feat(InsertDelimiter)
This commit is contained in:
commit
e7a7e3c533
17
.cspell.json
Normal file
17
.cspell.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2",
|
||||||
|
"language": "en,en-gb",
|
||||||
|
"words": [],
|
||||||
|
"dictionaries": [
|
||||||
|
"npm",
|
||||||
|
"softwareTerms",
|
||||||
|
"node",
|
||||||
|
"html",
|
||||||
|
"css",
|
||||||
|
"bash",
|
||||||
|
"en-gb",
|
||||||
|
"misc"
|
||||||
|
],
|
||||||
|
"ignorePaths": ["package.json", "package-lock.json", "node_modules"]
|
||||||
|
}
|
||||||
|
|
||||||
@ -3,7 +3,7 @@
|
|||||||
{
|
{
|
||||||
"name": "CyberChef",
|
"name": "CyberChef",
|
||||||
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
// 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 to add to the dev container. More info: https://containers.dev/features.
|
||||||
"features": {
|
"features": {
|
||||||
|
|||||||
@ -12,3 +12,7 @@ indent_size = 4
|
|||||||
[{package.json,.travis.yml,nightwatch.json}]
|
[{package.json,.travis.yml,nightwatch.json}]
|
||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
|
||||||
|
[.github/**.yml]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|||||||
1
.github/ISSUE_TEMPLATE.md
vendored
1
.github/ISSUE_TEMPLATE.md
vendored
@ -1 +0,0 @@
|
|||||||
<!-- Prefix the title above with 'Misc:' -->
|
|
||||||
12
.github/ISSUE_TEMPLATE/operation-request.md
vendored
12
.github/ISSUE_TEMPLATE/operation-request.md
vendored
@ -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
70
.github/dependabot.yml
vendored
Normal 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
15
.github/pull_request_template.md
vendored
Normal 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.
|
||||||
40
.github/workflows/codeql.yml
vendored
40
.github/workflows/codeql.yml
vendored
@ -1,40 +0,0 @@
|
|||||||
name: "CodeQL Analysis"
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
branches: [ master ]
|
|
||||||
pull_request:
|
|
||||||
# The branches below must be a subset of the branches above
|
|
||||||
branches: [ master ]
|
|
||||||
types: [synchronize, opened, reopened]
|
|
||||||
schedule:
|
|
||||||
- cron: '22 17 * * 5'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
analyze:
|
|
||||||
name: Analyze
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
actions: read
|
|
||||||
contents: read
|
|
||||||
security-events: write
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
language: [ 'javascript' ]
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
|
||||||
uses: github/codeql-action/init@v2
|
|
||||||
with:
|
|
||||||
languages: ${{ matrix.language }}
|
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
|
||||||
uses: github/codeql-action/analyze@v2
|
|
||||||
with:
|
|
||||||
category: "/language:${{matrix.language}}"
|
|
||||||
20
.github/workflows/master.yml
vendored
20
.github/workflows/master.yml
vendored
@ -6,21 +6,27 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
main:
|
main:
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pages: write
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set node version
|
- name: Set node version
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: '18.x'
|
node-version: 24
|
||||||
|
registry-url: "https://registry.npmjs.org"
|
||||||
|
|
||||||
- name: Install
|
- name: Install
|
||||||
run: |
|
run: |
|
||||||
export DETECT_CHROMEDRIVER_VERSION=true
|
npm ci
|
||||||
npm install
|
|
||||||
npm run setheapsize
|
npm run setheapsize
|
||||||
|
|
||||||
- name: Lint
|
- name: Lint
|
||||||
@ -33,7 +39,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Production Build
|
- name: Production Build
|
||||||
if: success()
|
if: success()
|
||||||
run: npx grunt prod --msg="Version 10 is here! Read about the new features <a href='https://github.com/gchq/CyberChef/wiki/Character-encoding,-EOL-separators,-and-editor-features'>here</a>"
|
run: npx grunt prod --msg=""
|
||||||
|
|
||||||
- name: Generate sitemap
|
- name: Generate sitemap
|
||||||
run: npx grunt exec:sitemap
|
run: npx grunt exec:sitemap
|
||||||
@ -50,7 +56,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Deploy to GitHub Pages
|
- name: Deploy to GitHub Pages
|
||||||
if: success() && github.ref == 'refs/heads/master'
|
if: success() && github.ref == 'refs/heads/master'
|
||||||
uses: crazy-max/ghaction-github-pages@v3
|
uses: crazy-max/ghaction-github-pages@v5
|
||||||
with:
|
with:
|
||||||
target_branch: gh-pages
|
target_branch: gh-pages
|
||||||
build_dir: ./build/prod
|
build_dir: ./build/prod
|
||||||
|
|||||||
43
.github/workflows/pull_requests.yml
vendored
43
.github/workflows/pull_requests.yml
vendored
@ -1,5 +1,8 @@
|
|||||||
name: "Pull Requests"
|
name: "Pull Requests"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
pull_request:
|
pull_request:
|
||||||
@ -9,17 +12,17 @@ jobs:
|
|||||||
main:
|
main:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set node version
|
- name: Set node version
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: '18.x'
|
node-version: 24
|
||||||
|
registry-url: "https://registry.npmjs.org"
|
||||||
|
|
||||||
- name: Install
|
- name: Install
|
||||||
run: |
|
run: |
|
||||||
export DETECT_CHROMEDRIVER_VERSION=true
|
npm ci
|
||||||
npm install
|
|
||||||
npm run setheapsize
|
npm run setheapsize
|
||||||
|
|
||||||
- name: Lint
|
- name: Lint
|
||||||
@ -34,22 +37,30 @@ jobs:
|
|||||||
if: success()
|
if: success()
|
||||||
run: npx grunt prod
|
run: npx grunt prod
|
||||||
|
|
||||||
- name: Production Image Build
|
- name: Upload Build Artefact
|
||||||
if: success()
|
if: success()
|
||||||
id: build-image
|
uses: actions/upload-artifact@v7
|
||||||
uses: redhat-actions/buildah-build@v2
|
|
||||||
with:
|
with:
|
||||||
# Not being uploaded to any registry, use a simple name to allow Buildah to build correctly.
|
name: zipped-build
|
||||||
image: cyberchef
|
path: build/prod/*.zip
|
||||||
containerfiles: ./Dockerfile
|
retention-days: 5
|
||||||
platforms: linux/amd64
|
|
||||||
oci: true
|
|
||||||
# Webpack seems to use a lot of open files, increase the max open file limit to accomodate.
|
|
||||||
extra-args: |
|
|
||||||
--ulimit nofile=10000
|
|
||||||
|
|
||||||
- name: UI Tests
|
- name: UI Tests
|
||||||
if: success()
|
if: success()
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get install xvfb
|
sudo apt-get install xvfb
|
||||||
xvfb-run --server-args="-screen 0 1200x800x24" npx grunt testui
|
xvfb-run --server-args="-screen 0 1200x800x24" npx grunt testui
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
if: success()
|
||||||
|
uses: docker/setup-buildx-action@v4
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v4
|
||||||
|
|
||||||
|
- name: Production Image Build
|
||||||
|
if: success()
|
||||||
|
id: build-image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
|||||||
76
.github/workflows/releases.yml
vendored
76
.github/workflows/releases.yml
vendored
@ -4,7 +4,10 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- "v*"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
env:
|
env:
|
||||||
REGISTRY: ghcr.io
|
REGISTRY: ghcr.io
|
||||||
@ -14,14 +17,18 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
main:
|
main:
|
||||||
|
permissions:
|
||||||
|
packages: write
|
||||||
|
contents: write
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set node version
|
- name: Set node version
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: '18.x'
|
node-version: 24
|
||||||
|
registry-url: "https://registry.npmjs.org"
|
||||||
|
|
||||||
- name: Install
|
- name: Install
|
||||||
run: |
|
run: |
|
||||||
@ -44,9 +51,15 @@ jobs:
|
|||||||
sudo apt-get install xvfb
|
sudo apt-get install xvfb
|
||||||
xvfb-run --server-args="-screen 0 1200x800x24" npx grunt testui
|
xvfb-run --server-args="-screen 0 1200x800x24" npx grunt testui
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v4
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v4
|
||||||
|
|
||||||
- name: Image Metadata
|
- name: Image Metadata
|
||||||
id: image-metadata
|
id: image-metadata
|
||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v6
|
||||||
with:
|
with:
|
||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
tags: |
|
tags: |
|
||||||
@ -54,19 +67,21 @@ jobs:
|
|||||||
type=semver,pattern={{major}}.{{minor}}
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
type=semver,pattern={{version}}
|
type=semver,pattern={{version}}
|
||||||
|
|
||||||
- name: Production Image Build
|
- name: Log in to GHCR
|
||||||
id: build-image
|
uses: docker/login-action@v4
|
||||||
uses: redhat-actions/buildah-build@v2
|
|
||||||
with:
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ env.REGISTRY_USER }}
|
||||||
|
password: ${{ env.REGISTRY_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Publish to GHCR
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
tags: ${{ steps.image-metadata.outputs.tags }}
|
tags: ${{ steps.image-metadata.outputs.tags }}
|
||||||
labels: ${{ steps.image-metadata.outputs.labels }}
|
labels: ${{ steps.image-metadata.outputs.labels }}
|
||||||
containerfiles: ./Dockerfile
|
platforms: linux/amd64,linux/arm64
|
||||||
platforms: linux/amd64
|
|
||||||
oci: true
|
|
||||||
# Webpack seems to use a lot of open files, increase the max open file limit to accomodate.
|
|
||||||
extra-args: |
|
|
||||||
--ulimit nofile=10000
|
|
||||||
|
|
||||||
|
|
||||||
- name: Upload Release Assets
|
- name: Upload Release Assets
|
||||||
id: upload-release-assets
|
id: upload-release-assets
|
||||||
@ -79,15 +94,26 @@ jobs:
|
|||||||
file_glob: true
|
file_glob: true
|
||||||
body: "See the [CHANGELOG](https://github.com/gchq/CyberChef/blob/master/CHANGELOG.md) and [commit messages](https://github.com/gchq/CyberChef/commits/master) for details."
|
body: "See the [CHANGELOG](https://github.com/gchq/CyberChef/blob/master/CHANGELOG.md) and [commit messages](https://github.com/gchq/CyberChef/commits/master) for details."
|
||||||
|
|
||||||
- name: Publish to NPM
|
npm-publish:
|
||||||
uses: JS-DevTools/npm-publish@v1
|
permissions:
|
||||||
with:
|
id-token: write
|
||||||
token: ${{ secrets.NPM_TOKEN }}
|
contents: read
|
||||||
|
needs: main
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Publish to GHCR
|
- name: Set node version
|
||||||
uses: redhat-actions/push-to-registry@v2
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
tags: ${{ steps.build-image.outputs.tags }}
|
node-version: 24
|
||||||
registry: ${{ env.REGISTRY }}
|
registry-url: "https://registry.npmjs.org"
|
||||||
username: ${{ env.REGISTRY_USER }}
|
|
||||||
password: ${{ env.REGISTRY_PASSWORD }}
|
- name: Install
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Create machine generated files
|
||||||
|
run: npm run node
|
||||||
|
|
||||||
|
- name: Publish to NPM
|
||||||
|
run: npm publish
|
||||||
|
|||||||
442
CHANGELOG.md
442
CHANGELOG.md
@ -13,6 +13,202 @@ All major and minor version changes will be documented in this file. Details of
|
|||||||
|
|
||||||
## Details
|
## 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]
|
||||||
|
- Added the ability to paste one or more Images from the Clipboard [@t-martine] [@a3957273] [@C85297] | [#1876]
|
||||||
|
- Quoted Printable - consistent reference to 'email' [@wesinator] | [#2186]
|
||||||
|
- Fix freeze when output text decoding fails [@Raka-loah] | [#1573]
|
||||||
|
- Update Browserslist DB [@C85297] | [#2183]
|
||||||
|
- Add contents write permission to releases workflow [@C85297] | [#2182]
|
||||||
|
- Fix release workflow permissions [@C85297] | [#2181]
|
||||||
|
|
||||||
|
### [10.21.0] - 2026-02-05
|
||||||
|
- Fix import operations with special chars in them [@d98762625] [@jg42526] | [#1040]
|
||||||
|
- Remove custom CodeQL workflow [@C85297] | [#2176]
|
||||||
|
- Fix code scanning warnings in workflows [@GCHQDeveloper581] | [#2177]
|
||||||
|
- Use NPM trusted publishing [@C85297] [@GCHQDeveloper581] | [#2174]
|
||||||
|
- Fix: Correctly parse xxd odd byte hexdumps [@ThomasNotTom] [@GCHQDeveloper581] | [#2058]
|
||||||
|
- Update Sitemap URLs to Use Valid Paths in sitemap.mjs [@rbpi] [@C85297] | [#1861]
|
||||||
|
- Use recommended GitHub Actions to build image [@AlexGustafsson] [@C85297] | [#2055]
|
||||||
|
- Remove version 10 message from banner [@C85297] | [#2169]
|
||||||
|
- Bump form-data from 4.0.1 to 4.0.5 | [#2175]
|
||||||
|
- Bump node-forge from 1.3.1 to 1.3.3 | [#2173]
|
||||||
|
- Update crypto browserify [@C85297] | [#2172]
|
||||||
|
- Update kbpgp package (resolves #2135) [@GCHQDeveloper581] | [#2136]
|
||||||
|
- Fix the processing of ALPNs for JA4 to align with new specification update [@tuliperis] | [#2165]
|
||||||
|
- Add Bech32 and Bech32m encoding/decoding operations [@thomasxm] | [#2159]
|
||||||
|
- Exclude Delete character from hex dump output [@mikecat] [@C85297] | [#2086]
|
||||||
|
- Tiny typo fix in "To Base85" operation [@twostraws] | [#2118]
|
||||||
|
- Bump jsonpath-plus [@C85297] | [#2166]
|
||||||
|
|
||||||
|
### [10.20.0] - 2026-01-28
|
||||||
|
- Fixed Optical Character Recognition and added tests [@n1474335] | [ab37c1e]
|
||||||
|
- Fixed JA4 version fallback value [@n1474335] | [7a5225c]
|
||||||
|
- Updated chromedriver [@n1474335] | [0e82e4b]
|
||||||
|
- Fixed RSA Sign and Verify character encodings [@n1474335] | [895a929]
|
||||||
|
- Updated chromedriver [@n1474335] | [d3adfc7]
|
||||||
|
- Added message format arg to RSA Verify operation [@n1474335] | [47c85a1]
|
||||||
|
- Add operation for parsing X.509 CRLs [@robinsandhu] | [#1887]
|
||||||
|
- Fix typo in description of JWT Sign recipe [@GuilhermoReadonly] | [#1961]
|
||||||
|
- Corrected path to generateNodeIndex.mjs [@simonarnell] | [#1959]
|
||||||
|
- Add 'header' ingredient to JWT Sign operation [@RandomByte] | [#1957]
|
||||||
|
- Add Parse TLS record operation [@c65722] | [#1936]
|
||||||
|
- Automatically detect chrome driver version [@gchq] | [#1972]
|
||||||
|
- Add Strip UDP header operation [@c65722] | [#1900]
|
||||||
|
- Add Strip TCP header operation [@c65722] | [#1898]
|
||||||
|
- Webpack compress with gzip and brotli [@max0x53] | [#1955]
|
||||||
|
- add offset field to 'Add Line Numbers' operation [@Adamkadaban] | [#1866]
|
||||||
|
- Disable flakey URL test [@a3957273] | [#1973]
|
||||||
|
- Add Strip IPv4 header operation [@c65722] | [#1899]
|
||||||
|
- IPv6 Transition Operation [@jb30795] | [#1780]
|
||||||
|
- fix: Blowfish - ignore IV length in ECB mode [@FranciscoPombal] | [#1902]
|
||||||
|
- Add 'Drop nth bytes' operation [@Oshawk] | [#1914]
|
||||||
|
- Add 'Take nth bytes' operation [@Oshawk] | [#1915]
|
||||||
|
- Add Leet Speak [@bartblaze] | [#1971]
|
||||||
|
- Fix Generate TOTP & HOPT [@exactlyaron] | [#1966]
|
||||||
|
- Updated luhn checksum operation to work with different bases [@k3ach] | [#1933]
|
||||||
|
- automatically theme mode based on user preference [@vs4vijay] | [#1921]
|
||||||
|
- fix: DES/Triple DES - misleading error messages [@FranciscoPombal] | [#1904]
|
||||||
|
- fix: ROT13 - shifting numbers by negative amounts [@FranciscoPombal] | [#1903]
|
||||||
|
- Introduce Yubico's Modhex for Conversion [@linuxgemini] | [#1105]
|
||||||
|
- Feature: MIME RFC2047 Decoding [@MShwed] | [#630]
|
||||||
|
- CC-1889 add _ option [@depperm] | [#1977]
|
||||||
|
- chore(root): add cspell [@evenstensberg] | [#1976]
|
||||||
|
- Preserve uppercase for Leet Speak [@bartblaze] | [#1981]
|
||||||
|
- Load the user's preferred color scheme if the URL contains an invalid theme [@0xh3xa] | [#2007]
|
||||||
|
- Add SM2 Encrypt and Decrypt Operations [@flakjacket95] | [#1909]
|
||||||
|
- Support jq as an operation. [@zhzy0077] | [#1604]
|
||||||
|
- Add fingerprints to the 'Parse X.509 certificate' operation [@JSCU-CNI] | [#1863]
|
||||||
|
- Added a JSON to YAML and a YAML to JSON operation [@ccarpo] | [#1286]
|
||||||
|
- Add CRC Operation [@r4mos] | [#1993]
|
||||||
|
- Bug Fix: selected theme not loading when refreshing [@0xh3xa] | [#2006]
|
||||||
|
- Fix(RecipeWaiter): sanitize user input in addOperation to prevent XSS [@0xh3xa] | [#2014]
|
||||||
|
- Docker multiplatform build support [@PathToLife] | [#1974]
|
||||||
|
- Add Base32 Hex Extended Alphabet and Base32 Tests. [@peterc-s] | [#1991]
|
||||||
|
- Add ECB/NoPadding and CBC/NoPadding support to AES encryption [@plvie] | [#2013]
|
||||||
|
- Add new operation: PHP Serialize [@brun0ne] | [#1548]
|
||||||
|
- Push input through postmessage [@kenduguay1] | [#1992]
|
||||||
|
- Add jsonata query operation [@jonking-ajar] | [#1587]
|
||||||
|
- Re-enable Npm Release in github workflows [@PathToLife] | [#2031]
|
||||||
|
- Add to ECDSA Verify the message format [@r4mos] | [#2027]
|
||||||
|
- Added alternating caps functionality [@sw5678] | [#1897]
|
||||||
|
- XOR Checksum operation added [@jg42526] | [#2035]
|
||||||
|
- Add GenerateAllChecksums operation * Remove checksums from GenerateAllHashes operation [@es45411] | [66d445c]
|
||||||
|
- Update GenerateAllChecksums infoURL [@es45411] | [#2037]
|
||||||
|
- Add toggle "+" character to URLDecode operation [@es45411] | [#2040]
|
||||||
|
- Workaround for Safari load bug [@GCHQDeveloper94872] | [#2038]
|
||||||
|
- Updated Dockerfile to correctly build on ARM64 platforms [@Sma-Das] | [#2042]
|
||||||
|
- Addresses bug report #2008 Added explicit support for octal IP addresses. Changed approach to IPv4 regex to be string manipulation generated. Added some unit tests for IP address parsing - probably not full coverage. Added lookahead and lookbehind tricks to resolve warned issue that 1.2.3.256 would still be extracted as 1.2.3.25. Now only accepts valid IP addresses. Warning replaced with clause about infinite length dotted decimal forms. [@gchqdev364] | [#2041]
|
||||||
|
- Remove trim from rail fence [@Odyhibit] | [#1986]
|
||||||
|
- Fix email regex [@ericli-splunk] | [#2025]
|
||||||
|
- Add Blake3 hashing [@xumptex] | [#2023]
|
||||||
|
- Use defaultIndex instead of 0 in transformArgs [@bartvanandel] | [#2015]
|
||||||
|
- Add "Generate UUID" and "Analyse UUID" operations [@bartvanandel] | [#2011]
|
||||||
|
- Add new operation: Template [@kendallgoto] | [#2021]
|
||||||
|
- Add more clear build instructions [@remingtr] | [#1873]
|
||||||
|
- Show On Map updated to use leaflet over WikiMedia [@0xff1ce] | [#1884]
|
||||||
|
- Fixed ToDecimal signed logic [@starplanet] | [#1545]
|
||||||
|
- Use BigInt for encoding/decoding VarInt [@mikecat] | [#1978]
|
||||||
|
|
||||||
### [10.19.0] - 2024-06-21
|
### [10.19.0] - 2024-06-21
|
||||||
- Add support for ECDSA and DSA in 'Parse CSR' [@robinsandhu] | [#1828]
|
- Add support for ECDSA and DSA in 'Parse CSR' [@robinsandhu] | [#1828]
|
||||||
- Fix typos in SIGABA.mjs [@eltociear] | [#1834]
|
- Fix typos in SIGABA.mjs [@eltociear] | [#1834]
|
||||||
@ -97,6 +293,8 @@ All major and minor version changes will be documented in this file. Details of
|
|||||||
- Added 'Levenshtein Distance' operation [@mikecat] | [#1498]
|
- Added 'Levenshtein Distance' operation [@mikecat] | [#1498]
|
||||||
- Added 'Swap case' operation [@mikecat] | [#1499]
|
- Added 'Swap case' operation [@mikecat] | [#1499]
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
## [10.0.0] - 2023-03-22
|
## [10.0.0] - 2023-03-22
|
||||||
- [Full details explained here](https://github.com/gchq/CyberChef/wiki/Character-encoding,-EOL-separators,-and-editor-features)
|
- [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]
|
- Status bars added to the Input and Output [@n1474335] | [#1405]
|
||||||
@ -440,6 +638,12 @@ All major and minor version changes will be documented in this file. Details of
|
|||||||
## [4.0.0] - 2016-11-28
|
## [4.0.0] - 2016-11-28
|
||||||
- Initial open source commit [@n1474335] | [b1d73a72](https://github.com/gchq/CyberChef/commit/b1d73a725dc7ab9fb7eb789296efd2b7e4b08306)
|
- Initial open source commit [@n1474335] | [b1d73a72](https://github.com/gchq/CyberChef/commit/b1d73a725dc7ab9fb7eb789296efd2b7e4b08306)
|
||||||
|
|
||||||
|
[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
|
||||||
[10.19.0]: https://github.com/gchq/CyberChef/releases/tag/v10.19.0
|
[10.19.0]: https://github.com/gchq/CyberChef/releases/tag/v10.19.0
|
||||||
[10.18.0]: https://github.com/gchq/CyberChef/releases/tag/v10.18.0
|
[10.18.0]: https://github.com/gchq/CyberChef/releases/tag/v10.18.0
|
||||||
[10.17.0]: https://github.com/gchq/CyberChef/releases/tag/v10.17.0
|
[10.17.0]: https://github.com/gchq/CyberChef/releases/tag/v10.17.0
|
||||||
@ -630,6 +834,95 @@ All major and minor version changes will be documented in this file. Details of
|
|||||||
[@cplussharp]: https://github.com/cplussharp
|
[@cplussharp]: https://github.com/cplussharp
|
||||||
[@robinsandhu]: https://github.com/robinsandhu
|
[@robinsandhu]: https://github.com/robinsandhu
|
||||||
[@eltociear]: https://github.com/eltociear
|
[@eltociear]: https://github.com/eltociear
|
||||||
|
[@GuilhermoReadonly]: https://github.com/GuilhermoReadonly
|
||||||
|
[@simonarnell]: https://github.com/simonarnell
|
||||||
|
[@RandomByte]: https://github.com/RandomByte
|
||||||
|
[@c65722]: https://github.com/c65722
|
||||||
|
[@c65722]: https://github.com/c65722
|
||||||
|
[@c65722]: https://github.com/c65722
|
||||||
|
[@max0x53]: https://github.com/max0x53
|
||||||
|
[@Adamkadaban]: https://github.com/Adamkadaban
|
||||||
|
[@c65722]: https://github.com/c65722
|
||||||
|
[@jb30795]: https://github.com/jb30795
|
||||||
|
[@FranciscoPombal]: https://github.com/FranciscoPombal
|
||||||
|
[@Oshawk]: https://github.com/Oshawk
|
||||||
|
[@Oshawk]: https://github.com/Oshawk
|
||||||
|
[@bartblaze]: https://github.com/bartblaze
|
||||||
|
[@exactlyaron]: https://github.com/exactlyaron
|
||||||
|
[@k3ach]: https://github.com/k3ach
|
||||||
|
[@vs4vijay]: https://github.com/vs4vijay
|
||||||
|
[@FranciscoPombal]: https://github.com/FranciscoPombal
|
||||||
|
[@FranciscoPombal]: https://github.com/FranciscoPombal
|
||||||
|
[@linuxgemini]: https://github.com/linuxgemini
|
||||||
|
[@depperm]: https://github.com/depperm
|
||||||
|
[@evenstensberg]: https://github.com/evenstensberg
|
||||||
|
[@bartblaze]: https://github.com/bartblaze
|
||||||
|
[@0xh3xa]: https://github.com/0xh3xa
|
||||||
|
[@flakjacket95]: https://github.com/flakjacket95
|
||||||
|
[@zhzy0077]: https://github.com/zhzy0077
|
||||||
|
[@JSCU-CNI]: https://github.com/JSCU-CNI
|
||||||
|
[@ccarpo]: https://github.com/ccarpo
|
||||||
|
[@r4mos]: https://github.com/r4mos
|
||||||
|
[@0xh3xa]: https://github.com/0xh3xa
|
||||||
|
[@0xh3xa]: https://github.com/0xh3xa
|
||||||
|
[@PathToLife]: https://github.com/PathToLife
|
||||||
|
[@peterc-s]: https://github.com/peterc-s
|
||||||
|
[@plvie]: https://github.com/plvie
|
||||||
|
[@kenduguay1]: https://github.com/kenduguay1
|
||||||
|
[@jonking-ajar]: https://github.com/jonking-ajar
|
||||||
|
[@PathToLife]: https://github.com/PathToLife
|
||||||
|
[@r4mos]: https://github.com/r4mos
|
||||||
|
[@jg42526]: https://github.com/jg42526
|
||||||
|
[@es45411]: https://github.com/es45411
|
||||||
|
[@gchq]: https://github.com/gchq
|
||||||
|
[@gchqdev364]: https://github.com/gchqdev364
|
||||||
|
[@GCHQDeveloper94872]: https://github.com/GCHQDeveloper94872
|
||||||
|
[@Sma-Das]: https://github.com/Sma-Das
|
||||||
|
[@gchq]: https://github.com/gchq
|
||||||
|
[@Odyhibit]: https://github.com/Odyhibit
|
||||||
|
[@ericli-splunk]: https://github.com/ericli-splunk
|
||||||
|
[@xumptex]: https://github.com/xumptex
|
||||||
|
[@bartvanandel]: https://github.com/bartvanandel
|
||||||
|
[@bartvanandel]: https://github.com/bartvanandel
|
||||||
|
[@kendallgoto]: https://github.com/kendallgoto
|
||||||
|
[@remingtr]: https://github.com/remingtr
|
||||||
|
[@0xff1ce]: https://github.com/0xff1ce
|
||||||
|
[@starplanet]: https://github.com/starplanet
|
||||||
|
[@C85297]: https://github.com/C85297
|
||||||
|
[@GCHQDeveloper581]: https://github.com/GCHQDeveloper581
|
||||||
|
[@ThomasNotTom]: https://github.com/ThomasNotTom
|
||||||
|
[@rbpi]: https://github.com/rbpi
|
||||||
|
[@AlexGustafsson]: https://github.com/AlexGustafsson
|
||||||
|
[@tuliperis]: https://github.com/tuliperis
|
||||||
|
[@thomasxm]: https://github.com/thomasxm
|
||||||
|
[@twostraws]: https://github.com/twostraws
|
||||||
|
[@beneri]: https://github.com/beneri
|
||||||
|
[@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
|
[8ad18b]: https://github.com/gchq/CyberChef/commit/8ad18bc7db6d9ff184ba3518686293a7685bf7b7
|
||||||
@ -642,6 +935,46 @@ All major and minor version changes will be documented in this file. Details of
|
|||||||
[760eff4]: https://github.com/gchq/CyberChef/commit/760eff49b5307aaa3104c5e5b437ffe62299acd1
|
[760eff4]: https://github.com/gchq/CyberChef/commit/760eff49b5307aaa3104c5e5b437ffe62299acd1
|
||||||
[65ffd8d]: https://github.com/gchq/CyberChef/commit/65ffd8d65d88eb369f6f61a5d1d0f807179bffb7
|
[65ffd8d]: https://github.com/gchq/CyberChef/commit/65ffd8d65d88eb369f6f61a5d1d0f807179bffb7
|
||||||
[0a353ee]: https://github.com/gchq/CyberChef/commit/0a353eeb378b9ca5d49e23c7dfc175ae07107b08
|
[0a353ee]: https://github.com/gchq/CyberChef/commit/0a353eeb378b9ca5d49e23c7dfc175ae07107b08
|
||||||
|
[66d445c]: https://github.com/gchq/CyberChef/commit/66d445c5ef4e8bd896fd15396e3ce2d660d8ace1
|
||||||
|
[ab37c1e]: https://github.com/gchq/CyberChef/commit/ab37c1e562dbee0495ed32876ecbb8225282af25
|
||||||
|
[965570d]: https://github.com/gchq/CyberChef/commit/965570d2504c17ee1f96211a1dc10ed40cd2b332
|
||||||
|
[a477f47]: https://github.com/gchq/CyberChef/commit/a477f47aecd01d78b11fe186ed4b20d9c487cfac
|
||||||
|
[7a5225c]: https://github.com/gchq/CyberChef/commit/7a5225c961a5e0d192b03152117cd10a761f73d6
|
||||||
|
[5f88ae4]: https://github.com/gchq/CyberChef/commit/5f88ae44ec77228d9bed8f11e8cc8e7dcfb36914
|
||||||
|
[0e82e4b]: https://github.com/gchq/CyberChef/commit/0e82e4b7c6c77cadb8be61cb145e081d6ecfdc88
|
||||||
|
[d635cca]: https://github.com/gchq/CyberChef/commit/d635cca2106aae2a59caf0e5d7e3633ee1ea3155
|
||||||
|
[895a929]: https://github.com/gchq/CyberChef/commit/895a9299255525cb57886deb9d9fd4ba17ae9548
|
||||||
|
[270a333]: https://github.com/gchq/CyberChef/commit/270a33317944612d27ea1cc15275ad6b0ed097e5
|
||||||
|
[d3adfc7]: https://github.com/gchq/CyberChef/commit/d3adfc7c3e5719279524356bce5261bd8350c0f8
|
||||||
|
[47c85a1]: https://github.com/gchq/CyberChef/commit/47c85a105ddbdd4cabfa44ddddbc56e3907a8c33
|
||||||
|
[3822c6c]: https://github.com/gchq/CyberChef/commit/3822c6c520a0b4200abc675c33f46082f5b9efc6
|
||||||
|
[66d445c]: https://github.com/gchq/CyberChef/commit/66d445c5ef4e8bd896fd15396e3ce2d660d8ace1
|
||||||
|
[ab37c1e]: https://github.com/gchq/CyberChef/commit/ab37c1e562dbee0495ed32876ecbb8225282af25
|
||||||
|
[965570d]: https://github.com/gchq/CyberChef/commit/965570d2504c17ee1f96211a1dc10ed40cd2b332
|
||||||
|
[a477f47]: https://github.com/gchq/CyberChef/commit/a477f47aecd01d78b11fe186ed4b20d9c487cfac
|
||||||
|
[7a5225c]: https://github.com/gchq/CyberChef/commit/7a5225c961a5e0d192b03152117cd10a761f73d6
|
||||||
|
[5f88ae4]: https://github.com/gchq/CyberChef/commit/5f88ae44ec77228d9bed8f11e8cc8e7dcfb36914
|
||||||
|
[0e82e4b]: https://github.com/gchq/CyberChef/commit/0e82e4b7c6c77cadb8be61cb145e081d6ecfdc88
|
||||||
|
[d635cca]: https://github.com/gchq/CyberChef/commit/d635cca2106aae2a59caf0e5d7e3633ee1ea3155
|
||||||
|
[895a929]: https://github.com/gchq/CyberChef/commit/895a9299255525cb57886deb9d9fd4ba17ae9548
|
||||||
|
[270a333]: https://github.com/gchq/CyberChef/commit/270a33317944612d27ea1cc15275ad6b0ed097e5
|
||||||
|
[d3adfc7]: https://github.com/gchq/CyberChef/commit/d3adfc7c3e5719279524356bce5261bd8350c0f8
|
||||||
|
[47c85a1]: https://github.com/gchq/CyberChef/commit/47c85a105ddbdd4cabfa44ddddbc56e3907a8c33
|
||||||
|
[3822c6c]: https://github.com/gchq/CyberChef/commit/3822c6c520a0b4200abc675c33f46082f5b9efc6
|
||||||
|
[66d445c]: https://github.com/gchq/CyberChef/commit/66d445c5ef4e8bd896fd15396e3ce2d660d8ace1
|
||||||
|
[ab37c1e]: https://github.com/gchq/CyberChef/commit/ab37c1e562dbee0495ed32876ecbb8225282af25
|
||||||
|
[965570d]: https://github.com/gchq/CyberChef/commit/965570d2504c17ee1f96211a1dc10ed40cd2b332
|
||||||
|
[a477f47]: https://github.com/gchq/CyberChef/commit/a477f47aecd01d78b11fe186ed4b20d9c487cfac
|
||||||
|
[7a5225c]: https://github.com/gchq/CyberChef/commit/7a5225c961a5e0d192b03152117cd10a761f73d6
|
||||||
|
[5f88ae4]: https://github.com/gchq/CyberChef/commit/5f88ae44ec77228d9bed8f11e8cc8e7dcfb36914
|
||||||
|
[0e82e4b]: https://github.com/gchq/CyberChef/commit/0e82e4b7c6c77cadb8be61cb145e081d6ecfdc88
|
||||||
|
[d635cca]: https://github.com/gchq/CyberChef/commit/d635cca2106aae2a59caf0e5d7e3633ee1ea3155
|
||||||
|
[895a929]: https://github.com/gchq/CyberChef/commit/895a9299255525cb57886deb9d9fd4ba17ae9548
|
||||||
|
[270a333]: https://github.com/gchq/CyberChef/commit/270a33317944612d27ea1cc15275ad6b0ed097e5
|
||||||
|
[d3adfc7]: https://github.com/gchq/CyberChef/commit/d3adfc7c3e5719279524356bce5261bd8350c0f8
|
||||||
|
[47c85a1]: https://github.com/gchq/CyberChef/commit/47c85a105ddbdd4cabfa44ddddbc56e3907a8c33
|
||||||
|
[3822c6c]: https://github.com/gchq/CyberChef/commit/3822c6c520a0b4200abc675c33f46082f5b9efc6
|
||||||
|
[66d445c]: https://github.com/gchq/CyberChef/commit/66d445c5ef4e8bd896fd15396e3ce2d660d8ace1
|
||||||
|
|
||||||
[#95]: https://github.com/gchq/CyberChef/pull/299
|
[#95]: https://github.com/gchq/CyberChef/pull/299
|
||||||
[#173]: https://github.com/gchq/CyberChef/pull/173
|
[#173]: https://github.com/gchq/CyberChef/pull/173
|
||||||
@ -778,4 +1111,113 @@ All major and minor version changes will be documented in this file. Details of
|
|||||||
[#512]: https://github.com/gchq/CyberChef/issues/512
|
[#512]: https://github.com/gchq/CyberChef/issues/512
|
||||||
[#1732]: https://github.com/gchq/CyberChef/issues/1732
|
[#1732]: https://github.com/gchq/CyberChef/issues/1732
|
||||||
[#1789]: https://github.com/gchq/CyberChef/issues/1789
|
[#1789]: https://github.com/gchq/CyberChef/issues/1789
|
||||||
|
[#1040]: https://github.com/gchq/CyberChef/pull/1040
|
||||||
|
[#2176]: https://github.com/gchq/CyberChef/pull/2176
|
||||||
|
[#2177]: https://github.com/gchq/CyberChef/pull/2177
|
||||||
|
[#2174]: https://github.com/gchq/CyberChef/pull/2174
|
||||||
|
[#2058]: https://github.com/gchq/CyberChef/pull/2058
|
||||||
|
[#1861]: https://github.com/gchq/CyberChef/pull/1861
|
||||||
|
[#2055]: https://github.com/gchq/CyberChef/pull/2055
|
||||||
|
[#2169]: https://github.com/gchq/CyberChef/pull/2169
|
||||||
|
[#2175]: https://github.com/gchq/CyberChef/pull/2175
|
||||||
|
[#2173]: https://github.com/gchq/CyberChef/pull/2173
|
||||||
|
[#2172]: https://github.com/gchq/CyberChef/pull/2172
|
||||||
|
[#2136]: https://github.com/gchq/CyberChef/pull/2136
|
||||||
|
[#2165]: https://github.com/gchq/CyberChef/pull/2165
|
||||||
|
[#2159]: https://github.com/gchq/CyberChef/pull/2159
|
||||||
|
[#2086]: https://github.com/gchq/CyberChef/pull/2086
|
||||||
|
[#2118]: https://github.com/gchq/CyberChef/pull/2118
|
||||||
|
[#2166]: https://github.com/gchq/CyberChef/pull/2166
|
||||||
|
[#2188]: https://github.com/gchq/CyberChef/pull/2188
|
||||||
|
[#2137]: https://github.com/gchq/CyberChef/pull/2137
|
||||||
|
[#1876]: https://github.com/gchq/CyberChef/pull/1876
|
||||||
|
[#2186]: https://github.com/gchq/CyberChef/pull/2186
|
||||||
|
[#1573]: https://github.com/gchq/CyberChef/pull/1573
|
||||||
|
[#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
|
||||||
|
|
||||||
|
|||||||
@ -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:
|
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.
|
- Push your changes to your fork.
|
||||||
|
- Sign the [GCHQ Contributor Licence Agreement](https://cla-assistant.io/gchq/CyberChef)
|
||||||
- Submit a pull request.
|
- 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
|
## Coding conventions
|
||||||
|
|
||||||
33
Dockerfile
33
Dockerfile
@ -1,9 +1,34 @@
|
|||||||
FROM node:18-alpine AS build
|
#####################################
|
||||||
|
# Build the app to a static website #
|
||||||
|
#####################################
|
||||||
|
# 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:24-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json .
|
||||||
|
COPY package-lock.json .
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
# --ignore-scripts prevents postinstall script (which runs grunt) as it depends on files other than package.json
|
||||||
|
RUN npm ci --ignore-scripts
|
||||||
|
|
||||||
|
# Copy files needed for postinstall and build
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npm ci
|
|
||||||
|
# npm postinstall runs grunt, which depends on files other than package.json
|
||||||
|
RUN npm run postinstall
|
||||||
|
|
||||||
|
# Build the app
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
FROM nginx:1.25-alpine3.18 AS cyberchef
|
#########################################
|
||||||
|
# Package static build files into nginx #
|
||||||
|
#########################################
|
||||||
|
FROM nginxinc/nginx-unprivileged:stable-alpine AS cyberchef
|
||||||
|
|
||||||
COPY --from=build ./build/prod /usr/share/nginx/html/
|
LABEL maintainer="GCHQ <oss@gchq.gov.uk>"
|
||||||
|
|
||||||
|
COPY --from=builder /app/build/prod /usr/share/nginx/html/
|
||||||
|
|||||||
32
Gruntfile.js
32
Gruntfile.js
@ -6,7 +6,7 @@ const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPl
|
|||||||
const glob = require("glob");
|
const glob = require("glob");
|
||||||
const path = require("path");
|
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.
|
* Grunt configuration for building the app in various formats.
|
||||||
@ -411,39 +411,13 @@ module.exports = function (grunt) {
|
|||||||
stdout: false,
|
stdout: false,
|
||||||
},
|
},
|
||||||
fixCryptoApiImports: {
|
fixCryptoApiImports: {
|
||||||
command: function () {
|
command: `node ${nodeFlags} src/core/config/scripts/fixCryptoApiImports.mjs`,
|
||||||
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'`;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
stdout: false
|
stdout: false
|
||||||
},
|
},
|
||||||
fixSnackbarMarkup: {
|
fixSnackbarMarkup: {
|
||||||
command: function () {
|
command: `node ${nodeFlags} src/core/config/scripts/fixSnackBarMarkup.mjs`,
|
||||||
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`;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
stdout: false
|
stdout: false
|
||||||
},
|
},
|
||||||
fixJimpModule: {
|
|
||||||
command: function () {
|
|
||||||
switch (process.platform) {
|
|
||||||
case "darwin":
|
|
||||||
// Space added before comma to prevent multiple modifications
|
|
||||||
return `sed -i '' 's/"es\\/index.js",/"es\\/index.js" ,\\n "type": "module",/' ./node_modules/jimp/package.json`;
|
|
||||||
default:
|
|
||||||
return `sed -i 's/"es\\/index.js",/"es\\/index.js" ,\\n "type": "module",/' ./node_modules/jimp/package.json`;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
stdout: false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
27
README.md
27
README.md
@ -20,21 +20,36 @@ Cryptographic operations in CyberChef should not be relied upon to provide secur
|
|||||||
|
|
||||||
[A live demo can be found here][1] - have fun!
|
[A live demo can be found here][1] - have fun!
|
||||||
|
|
||||||
## Containers
|
## Running Locally with Docker
|
||||||
|
|
||||||
If you would like to try out CyberChef locally you can either build it yourself:
|
**Prerequisites**
|
||||||
|
|
||||||
|
- [Docker](https://www.docker.com/products/docker-desktop/)
|
||||||
|
- Docker Desktop must be open and running on your machine
|
||||||
|
|
||||||
|
|
||||||
|
#### Option 1: Build the Docker Image Yourself
|
||||||
|
|
||||||
|
1. Build the docker image
|
||||||
```bash
|
```bash
|
||||||
docker build --tag cyberchef --ulimit nofile=10000 .
|
docker build --tag cyberchef --ulimit nofile=10000 .
|
||||||
docker run -it -p 8080:80 cyberchef
|
|
||||||
```
|
```
|
||||||
|
2. Run the docker container
|
||||||
|
```bash
|
||||||
|
docker run -it -p 8080:8080 cyberchef
|
||||||
|
```
|
||||||
|
3. Navigate to `http://localhost:8080` in your browser
|
||||||
|
|
||||||
Or you can use our image directly:
|
#### Option 2: Use the pre-built Docker Image
|
||||||
|
|
||||||
|
If you prefer to skip the build process, you can use the pre-built image
|
||||||
|
|
||||||
```bash
|
```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.
|
||||||
|
|
||||||
This image is built and published through our [GitHub Workflows](.github/workflows/releases.yml)
|
This image is built and published through our [GitHub Workflows](.github/workflows/releases.yml)
|
||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
@ -105,7 +120,7 @@ CyberChef is built to support
|
|||||||
|
|
||||||
## Node.js 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
|
## Contributing
|
||||||
|
|||||||
@ -10,18 +10,15 @@ module.exports = function(api) {
|
|||||||
}]
|
}]
|
||||||
],
|
],
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"dynamic-import-node",
|
|
||||||
"@babel/plugin-syntax-import-assertions",
|
"@babel/plugin-syntax-import-assertions",
|
||||||
[
|
|
||||||
"babel-plugin-transform-builtin-extend", {
|
|
||||||
"globals": ["Error"]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[
|
[
|
||||||
"@babel/plugin-transform-runtime", {
|
"@babel/plugin-transform-runtime", {
|
||||||
"regenerator": true
|
"regenerator": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
]
|
],
|
||||||
|
"generatorOpts": {
|
||||||
|
"importAttributesKeyword": "with"
|
||||||
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
15258
package-lock.json
generated
15258
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
153
package.json
153
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "cyberchef",
|
"name": "cyberchef",
|
||||||
"version": "10.19.4",
|
"version": "11.0.0",
|
||||||
"description": "The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.",
|
"description": "The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.",
|
||||||
"author": "n1474335 <n1474335@gmail.com>",
|
"author": "n1474335 <n1474335@gmail.com>",
|
||||||
"homepage": "https://gchq.github.io/CyberChef",
|
"homepage": "https://gchq.github.io/CyberChef",
|
||||||
@ -36,75 +36,75 @@
|
|||||||
"browserslist": [
|
"browserslist": [
|
||||||
"Chrome >= 50",
|
"Chrome >= 50",
|
||||||
"Firefox >= 38",
|
"Firefox >= 38",
|
||||||
"node >= 16"
|
"node >= 24"
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.24.7",
|
"@babel/eslint-parser": "^7.28.6",
|
||||||
"@babel/eslint-parser": "^7.24.7",
|
"@babel/plugin-syntax-import-assertions": "^7.28.6",
|
||||||
"@babel/plugin-syntax-import-assertions": "^7.24.7",
|
"@babel/plugin-transform-runtime": "^7.29.0",
|
||||||
"@babel/plugin-transform-runtime": "^7.24.7",
|
"@babel/preset-env": "^7.29.5",
|
||||||
"@babel/preset-env": "^7.24.7",
|
"@babel/runtime": "^7.29.2",
|
||||||
"@babel/runtime": "^7.24.7",
|
"@codemirror/commands": "^6.10.3",
|
||||||
"@codemirror/commands": "^6.6.0",
|
"@codemirror/language": "^6.12.3",
|
||||||
"@codemirror/language": "^6.10.2",
|
"@codemirror/search": "^6.7.0",
|
||||||
"@codemirror/search": "^6.5.6",
|
"@codemirror/state": "^6.5.4",
|
||||||
"@codemirror/state": "^6.4.1",
|
"@codemirror/view": "^6.41.1",
|
||||||
"@codemirror/view": "^6.28.0",
|
"autoprefixer": "^10.5.0",
|
||||||
"autoprefixer": "^10.4.19",
|
"babel-loader": "^10.1.1",
|
||||||
"babel-loader": "^9.1.3",
|
|
||||||
"babel-plugin-dynamic-import-node": "^2.3.3",
|
|
||||||
"babel-plugin-transform-builtin-extend": "1.1.2",
|
|
||||||
"base64-loader": "^1.0.0",
|
"base64-loader": "^1.0.0",
|
||||||
"chromedriver": "^130.0.0",
|
"chromedriver": "^146.0.6",
|
||||||
"cli-progress": "^3.12.0",
|
"cli-progress": "^3.12.0",
|
||||||
"colors": "^1.4.0",
|
"colors": "^1.4.0",
|
||||||
"compression-webpack-plugin": "^11.1.0",
|
"compression-webpack-plugin": "^12.0.0",
|
||||||
"copy-webpack-plugin": "^12.0.2",
|
"copy-webpack-plugin": "^14.0.0",
|
||||||
"core-js": "^3.37.1",
|
"core-js": "^3.49.0",
|
||||||
"css-loader": "7.1.2",
|
"cspell": "^9.7.0",
|
||||||
"eslint": "^9.4.0",
|
"css-loader": "^7.1.4",
|
||||||
"eslint-plugin-jsdoc": "^48.2.9",
|
"eslint": "^9.39.4",
|
||||||
"globals": "^15.4.0",
|
"eslint-plugin-jsdoc": "^50.8.0",
|
||||||
"grunt": "^1.6.1",
|
"globals": "^17.4.0",
|
||||||
|
"grunt": "^1.6.2",
|
||||||
"grunt-chmod": "~1.1.1",
|
"grunt-chmod": "~1.1.1",
|
||||||
"grunt-concurrent": "^3.0.0",
|
"grunt-concurrent": "^3.0.0",
|
||||||
"grunt-contrib-clean": "~2.0.1",
|
"grunt-contrib-clean": "~2.0.1",
|
||||||
"grunt-contrib-connect": "^4.0.0",
|
"grunt-contrib-connect": "^5.0.1",
|
||||||
"grunt-contrib-copy": "~1.0.0",
|
"grunt-contrib-copy": "~1.0.0",
|
||||||
"grunt-contrib-watch": "^1.1.0",
|
"grunt-contrib-watch": "^1.1.0",
|
||||||
"grunt-eslint": "^25.0.0",
|
"grunt-eslint": "^25.0.0",
|
||||||
"grunt-exec": "~3.0.0",
|
"grunt-exec": "~3.0.0",
|
||||||
"grunt-webpack": "^6.0.0",
|
"grunt-webpack": "^6.0.0",
|
||||||
"grunt-zip": "^1.0.0",
|
"grunt-zip": "^1.0.0",
|
||||||
"html-webpack-plugin": "^5.6.0",
|
"html-webpack-plugin": "^5.6.7",
|
||||||
"imports-loader": "^5.0.0",
|
"imports-loader": "^5.0.0",
|
||||||
"mini-css-extract-plugin": "2.9.0",
|
"mini-css-extract-plugin": "2.10.2",
|
||||||
"modify-source-webpack-plugin": "^4.1.0",
|
"modify-source-webpack-plugin": "^4.1.0",
|
||||||
"nightwatch": "^3.6.3",
|
"nightwatch": "^3.15.0",
|
||||||
"postcss": "^8.4.38",
|
"postcss": "^8.5.14",
|
||||||
"postcss-css-variables": "^0.19.0",
|
"postcss-css-variables": "^0.19.0",
|
||||||
"postcss-import": "^16.1.0",
|
"postcss-import": "^16.1.1",
|
||||||
"postcss-loader": "^8.1.1",
|
"postcss-loader": "^8.2.1",
|
||||||
"prompt": "^1.3.0",
|
"prompt": "^1.3.0",
|
||||||
"sitemap": "^8.0.0",
|
"sitemap": "^8.0.3",
|
||||||
"terser": "^5.31.1",
|
"terser": "^5.46.2",
|
||||||
"webpack": "^5.91.0",
|
"webpack": "^5.106.2",
|
||||||
"webpack-bundle-analyzer": "^4.10.2",
|
"webpack-bundle-analyzer": "^5.3.0",
|
||||||
"webpack-dev-server": "5.0.4",
|
"webpack-dev-server": "^5.0.4",
|
||||||
"webpack-node-externals": "^3.0.0",
|
"webpack-node-externals": "^3.0.0",
|
||||||
"worker-loader": "^3.0.8"
|
"worker-loader": "^3.0.8"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@alexaltea/capstone-js": "^3.0.5",
|
||||||
"@astronautlabs/amf": "^0.0.6",
|
"@astronautlabs/amf": "^0.0.6",
|
||||||
"@babel/polyfill": "^7.12.1",
|
|
||||||
"@blu3r4y/lzma": "^2.3.3",
|
"@blu3r4y/lzma": "^2.3.3",
|
||||||
"@wavesenterprise/crypto-gost-js": "^2.1.0-RC1",
|
"@wavesenterprise/crypto-gost-js": "^2.1.0-RC1",
|
||||||
"@xmldom/xmldom": "^0.8.10",
|
"@xmldom/xmldom": "^0.8.13",
|
||||||
"argon2-browser": "^1.18.0",
|
"argon2-browser": "^1.18.0",
|
||||||
"arrive": "^2.4.1",
|
"arrive": "^2.5.3",
|
||||||
"avsc": "^5.7.7",
|
"assert": "^2.1.0",
|
||||||
|
"avsc": "^5.7.9",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"bignumber.js": "^9.1.2",
|
"bignumber.js": "^9.3.1",
|
||||||
|
"@noble/hashes": "2.2.0",
|
||||||
"blakejs": "^1.2.1",
|
"blakejs": "^1.2.1",
|
||||||
"bootstrap": "4.6.2",
|
"bootstrap": "4.6.2",
|
||||||
"bootstrap-colorpicker": "^3.4.0",
|
"bootstrap-colorpicker": "^3.4.0",
|
||||||
@ -116,87 +116,100 @@
|
|||||||
"chi-squared": "^1.1.0",
|
"chi-squared": "^1.1.0",
|
||||||
"codepage": "^1.15.0",
|
"codepage": "^1.15.0",
|
||||||
"crypto-api": "^0.8.5",
|
"crypto-api": "^0.8.5",
|
||||||
"crypto-browserify": "^3.12.0",
|
"crypto-browserify": "^3.12.1",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"ctph.js": "0.0.5",
|
"ctph.js": "0.0.5",
|
||||||
"d3": "7.9.0",
|
"d3": "7.9.0",
|
||||||
"d3-hexbin": "^0.2.2",
|
"d3-hexbin": "^0.2.2",
|
||||||
"diff": "^5.2.0",
|
"diff": "^5.2.2",
|
||||||
|
"dompurify": "^3.4.2",
|
||||||
"es6-promisify": "^7.0.0",
|
"es6-promisify": "^7.0.0",
|
||||||
"escodegen": "^2.1.0",
|
"escodegen": "^2.1.0",
|
||||||
"esprima": "^4.0.1",
|
"esprima": "^4.0.1",
|
||||||
|
"events": "^3.3.0",
|
||||||
"exif-parser": "^0.1.12",
|
"exif-parser": "^0.1.12",
|
||||||
"fernet": "^0.4.0",
|
"fernet": "^0.3.3",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"flat": "^6.0.1",
|
"flat": "^6.0.1",
|
||||||
"geodesy": "1.1.3",
|
"geodesy": "1.1.3",
|
||||||
"highlight.js": "^11.9.0",
|
"handlebars": "^4.7.9",
|
||||||
|
"highlight.js": "^11.11.1",
|
||||||
"ieee754": "^1.2.1",
|
"ieee754": "^1.2.1",
|
||||||
"jimp": "^0.22.12",
|
"jimp": "1.6.0",
|
||||||
|
"jq-web": "^0.5.1",
|
||||||
"jquery": "3.7.1",
|
"jquery": "3.7.1",
|
||||||
"js-crc": "^0.2.0",
|
|
||||||
"js-sha3": "^0.9.3",
|
"js-sha3": "^0.9.3",
|
||||||
"jsesc": "^3.0.2",
|
"jsesc": "^3.1.0",
|
||||||
"json5": "^2.2.3",
|
"json5": "^2.2.3",
|
||||||
"jsonpath-plus": "^9.0.0",
|
"jsonata": "^2.1.0",
|
||||||
"jsonwebtoken": "8.5.1",
|
"jsonpath-plus": "^10.4.0",
|
||||||
|
"jsonwebtoken": "9.0.3",
|
||||||
"jsqr": "^1.4.0",
|
"jsqr": "^1.4.0",
|
||||||
"jsrsasign": "^11.1.0",
|
"jsrsasign": "^11.1.3",
|
||||||
"kbpgp": "2.1.15",
|
"kbpgp": "^2.1.17",
|
||||||
"libbzip2-wasm": "0.0.4",
|
"libbzip2-wasm": "0.0.4",
|
||||||
"libyara-wasm": "^1.2.1",
|
"libyara-wasm": "^1.2.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.18.1",
|
||||||
"loglevel": "^1.9.1",
|
"loglevel": "^1.9.2",
|
||||||
"loglevel-message-prefix": "^3.0.0",
|
"loglevel-message-prefix": "^3.0.0",
|
||||||
"lz-string": "^1.5.0",
|
"lz-string": "^1.5.0",
|
||||||
"lz4js": "^0.2.0",
|
"lz4js": "^0.2.0",
|
||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.1",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"moment-timezone": "^0.5.45",
|
"moment-timezone": "^0.6.2",
|
||||||
"ngeohash": "^0.6.3",
|
"ngeohash": "^0.6.3",
|
||||||
"node-forge": "^1.3.1",
|
"node-forge": "^1.4.0",
|
||||||
"node-md6": "^0.1.0",
|
"node-md6": "^0.1.0",
|
||||||
"nodom": "^2.4.0",
|
"nodom": "^2.4.0",
|
||||||
"notepack.io": "^3.0.1",
|
"notepack.io": "^3.0.1",
|
||||||
"ntlm": "^0.1.3",
|
"ntlm": "^0.1.3",
|
||||||
"nwmatcher": "^1.4.4",
|
"nwmatcher": "^1.4.4",
|
||||||
"otp": "0.1.3",
|
"otpauth": "^9.5.0",
|
||||||
"path": "^0.12.7",
|
"path": "^0.12.7",
|
||||||
"popper.js": "^1.16.1",
|
"popper.js": "^1.16.1",
|
||||||
"process": "^0.11.10",
|
"process": "^0.11.10",
|
||||||
"protobufjs": "^7.3.1",
|
"protobufjs": "^7.5.6",
|
||||||
|
"punycode.js": "^2.3.1",
|
||||||
"qr-image": "^3.2.0",
|
"qr-image": "^3.2.0",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rison": "^0.1.1",
|
"rison": "^0.1.1",
|
||||||
"scryptsy": "^2.1.0",
|
"scryptsy": "^2.1.0",
|
||||||
"snackbarjs": "^1.1.0",
|
"snackbarjs": "^1.1.0",
|
||||||
"sortablejs": "^1.15.2",
|
"sortablejs": "^1.15.7",
|
||||||
"split.js": "^1.6.5",
|
"split.js": "^1.6.5",
|
||||||
|
"sql-formatter": "^15.7.3",
|
||||||
"ssdeep.js": "0.0.3",
|
"ssdeep.js": "0.0.3",
|
||||||
"stream-browserify": "^3.0.0",
|
"stream-browserify": "^3.0.0",
|
||||||
"tesseract.js": "5.1.0",
|
"tesseract.js": "^6.0.1",
|
||||||
"ua-parser-js": "^1.0.38",
|
"ua-parser-js": "^1.0.41",
|
||||||
"unorm": "^1.6.0",
|
"unorm": "^1.6.0",
|
||||||
|
"url": "^0.11.4",
|
||||||
"utf8": "^3.0.0",
|
"utf8": "^3.0.0",
|
||||||
|
"uuid": "^14.0.0",
|
||||||
"vkbeautify": "^0.99.3",
|
"vkbeautify": "^0.99.3",
|
||||||
"xpath": "0.0.34",
|
"xpath": "0.0.34",
|
||||||
"xregexp": "^5.1.1",
|
"xregexp": "^5.1.2",
|
||||||
"zlibjs": "^0.3.1"
|
"zlibjs": "^0.3.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "npx grunt dev",
|
"start": "npx grunt dev",
|
||||||
"build": "npx grunt prod",
|
"build": "npx grunt prod",
|
||||||
"node": "npx grunt node",
|
"node": "npx grunt node",
|
||||||
"repl": "node --experimental-modules --experimental-json-modules --experimental-specifier-resolution=node --no-experimental-fetch --no-warnings src/node/repl.mjs",
|
"repl": "node --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",
|
"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",
|
"testnodeconsumer": "npx grunt testnodeconsumer",
|
||||||
"testui": "npx grunt testui",
|
"testui": "npx grunt testui",
|
||||||
"testuidev": "npx nightwatch --env=dev",
|
"testuidev": "npx nightwatch --env=dev",
|
||||||
"lint": "npx grunt lint",
|
"lint": "npx grunt lint",
|
||||||
"postinstall": "npx grunt exec:fixCryptoApiImports && npx grunt exec:fixSnackbarMarkup && npx grunt exec:fixJimpModule",
|
"lint:grammar": "cspell ./src",
|
||||||
"newop": "node --experimental-modules --experimental-json-modules src/core/config/scripts/newOperation.mjs",
|
"postinstall": "npx grunt exec:fixCryptoApiImports && npx grunt exec:fixSnackbarMarkup",
|
||||||
"minor": "node --experimental-modules --experimental-json-modules src/core/config/scripts/newMinorVersion.mjs",
|
"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`)'",
|
"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"
|
"setheapsize": "export NODE_OPTIONS=--max_old_space_size=2048"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=24 <25"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -55,8 +55,15 @@ class Chef {
|
|||||||
progress = await recipe.execute(this.dish, progress);
|
progress = await recipe.execute(this.dish, progress);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error(err);
|
log.error(err);
|
||||||
|
|
||||||
|
let displayStr;
|
||||||
|
if ("displayStr" in err) {
|
||||||
|
displayStr = err.displayStr;
|
||||||
|
} else {
|
||||||
|
displayStr = err.toString();
|
||||||
|
}
|
||||||
error = {
|
error = {
|
||||||
displayStr: err.displayStr,
|
displayStr: displayStr,
|
||||||
};
|
};
|
||||||
progress = err.progress;
|
progress = err.progress;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import Chef from "./Chef.mjs";
|
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 OpModules from "./config/modules/OpModules.mjs";
|
||||||
import loglevelMessagePrefix from "loglevel-message-prefix";
|
import loglevelMessagePrefix from "loglevel-message-prefix";
|
||||||
|
|
||||||
|
|||||||
7
src/core/Ingredient.mjs
Executable file → Normal file
7
src/core/Ingredient.mjs
Executable file → Normal file
@ -5,7 +5,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import Utils from "./Utils.mjs";
|
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.
|
* The arguments to operations.
|
||||||
@ -119,7 +120,9 @@ class Ingredient {
|
|||||||
number = parseFloat(data);
|
number = parseFloat(data);
|
||||||
if (isNaN(number)) {
|
if (isNaN(number)) {
|
||||||
const sample = Utils.truncate(data.toString(), 10);
|
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;
|
return number;
|
||||||
default:
|
default:
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import Dish from "./Dish.mjs";
|
import Dish from "./Dish.mjs";
|
||||||
|
import OperationError from "./errors/OperationError.mjs";
|
||||||
import Ingredient from "./Ingredient.mjs";
|
import Ingredient from "./Ingredient.mjs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -223,7 +224,11 @@ class Operation {
|
|||||||
*/
|
*/
|
||||||
set ingValues(ingValues) {
|
set ingValues(ingValues) {
|
||||||
ingValues.forEach((val, i) => {
|
ingValues.forEach((val, i) => {
|
||||||
|
try {
|
||||||
this._ingList[i].value = val;
|
this._ingList[i].value = val;
|
||||||
|
} catch (err) {
|
||||||
|
throw new OperationError(`Failed to set value of ingredient '${this._ingList[i].name}': ${err}`);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
* @license Apache-2.0
|
* @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 OperationError from "./errors/OperationError.mjs";
|
||||||
import Operation from "./Operation.mjs";
|
import Operation from "./Operation.mjs";
|
||||||
import DishError from "./errors/DishError.mjs";
|
import DishError from "./errors/DishError.mjs";
|
||||||
@ -70,11 +70,15 @@ class Recipe {
|
|||||||
if (o instanceof Operation) {
|
if (o instanceof Operation) {
|
||||||
return o;
|
return o;
|
||||||
} else {
|
} else {
|
||||||
|
try {
|
||||||
const op = new modules[o.module][o.name]();
|
const op = new modules[o.module][o.name]();
|
||||||
op.ingValues = o.ingValues;
|
op.ingValues = o.ingValues;
|
||||||
op.breakpoint = o.breakpoint;
|
op.breakpoint = o.breakpoint;
|
||||||
op.disabled = o.disabled;
|
op.disabled = o.disabled;
|
||||||
return op;
|
return op;
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(`Failed to hydrate operation '${o.name}': ${err}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -229,6 +233,7 @@ class Recipe {
|
|||||||
}
|
}
|
||||||
this.lastRunOp = op;
|
this.lastRunOp = op;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
log.error(err);
|
||||||
// Return expected errors as output
|
// Return expected errors as output
|
||||||
if (err instanceof OperationError || err?.type === "OperationError") {
|
if (err instanceof OperationError || err?.type === "OperationError") {
|
||||||
// Cannot rely on `err instanceof OperationError` here as extending
|
// Cannot rely on `err instanceof OperationError` here as extending
|
||||||
|
|||||||
@ -177,7 +177,7 @@ class Utils {
|
|||||||
*/
|
*/
|
||||||
static printable(str, preserveWs=false, onlyAscii=false) {
|
static printable(str, preserveWs=false, onlyAscii=false) {
|
||||||
if (onlyAscii) {
|
if (onlyAscii) {
|
||||||
return str.replace(/[^\x20-\x7f]/g, ".");
|
return str.replace(/[^\x20-\x7e]/g, ".");
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-misleading-character-class
|
// eslint-disable-next-line no-misleading-character-class
|
||||||
|
|||||||
@ -26,6 +26,8 @@
|
|||||||
"From Base45",
|
"From Base45",
|
||||||
"To Base58",
|
"To Base58",
|
||||||
"From Base58",
|
"From Base58",
|
||||||
|
"To Bech32",
|
||||||
|
"From Bech32",
|
||||||
"To Base62",
|
"To Base62",
|
||||||
"From Base62",
|
"From Base62",
|
||||||
"To Base64",
|
"To Base64",
|
||||||
@ -39,6 +41,7 @@
|
|||||||
"From Base",
|
"From Base",
|
||||||
"To BCD",
|
"To BCD",
|
||||||
"From BCD",
|
"From BCD",
|
||||||
|
"Text-Integer Conversion",
|
||||||
"To HTML Entity",
|
"To HTML Entity",
|
||||||
"From HTML Entity",
|
"From HTML Entity",
|
||||||
"URL Encode",
|
"URL Encode",
|
||||||
@ -72,9 +75,14 @@
|
|||||||
"Avro to JSON",
|
"Avro to JSON",
|
||||||
"CBOR Encode",
|
"CBOR Encode",
|
||||||
"CBOR Decode",
|
"CBOR Decode",
|
||||||
|
"YAML to JSON",
|
||||||
|
"JSON to YAML",
|
||||||
"Caret/M-decode",
|
"Caret/M-decode",
|
||||||
"Rison Encode",
|
"Rison Encode",
|
||||||
"Rison Decode"
|
"Rison Decode",
|
||||||
|
"To Modhex",
|
||||||
|
"From Modhex",
|
||||||
|
"MIME Decoding"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -102,6 +110,8 @@
|
|||||||
"Rabbit",
|
"Rabbit",
|
||||||
"SM4 Encrypt",
|
"SM4 Encrypt",
|
||||||
"SM4 Decrypt",
|
"SM4 Decrypt",
|
||||||
|
"RC6 Encrypt",
|
||||||
|
"RC6 Decrypt",
|
||||||
"GOST Encrypt",
|
"GOST Encrypt",
|
||||||
"GOST Decrypt",
|
"GOST Decrypt",
|
||||||
"GOST Sign",
|
"GOST Sign",
|
||||||
@ -157,7 +167,10 @@
|
|||||||
"Typex",
|
"Typex",
|
||||||
"Lorenz",
|
"Lorenz",
|
||||||
"Colossus",
|
"Colossus",
|
||||||
"SIGABA"
|
"SIGABA",
|
||||||
|
"Flask Session Decode",
|
||||||
|
"Flask Session Sign",
|
||||||
|
"Flask Session Verify"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -190,7 +203,9 @@
|
|||||||
"Parse SSH Host Key",
|
"Parse SSH Host Key",
|
||||||
"Parse CSR",
|
"Parse CSR",
|
||||||
"Public Key from Certificate",
|
"Public Key from Certificate",
|
||||||
"Public Key from Private Key"
|
"Public Key from Private Key",
|
||||||
|
"SM2 Encrypt",
|
||||||
|
"SM2 Decrypt"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -213,6 +228,9 @@
|
|||||||
"Subtract",
|
"Subtract",
|
||||||
"Multiply",
|
"Multiply",
|
||||||
"Divide",
|
"Divide",
|
||||||
|
"Modular Exponentiation",
|
||||||
|
"Modular Inverse",
|
||||||
|
"Extended GCD",
|
||||||
"Mean",
|
"Mean",
|
||||||
"Median",
|
"Median",
|
||||||
"Standard Deviation",
|
"Standard Deviation",
|
||||||
@ -231,9 +249,11 @@
|
|||||||
"DNS over HTTPS",
|
"DNS over HTTPS",
|
||||||
"Strip HTTP headers",
|
"Strip HTTP headers",
|
||||||
"Dechunk HTTP response",
|
"Dechunk HTTP response",
|
||||||
|
"Parse Ethernet frame",
|
||||||
"Parse User Agent",
|
"Parse User Agent",
|
||||||
"Parse IP range",
|
"Parse IP range",
|
||||||
"Parse IPv6 address",
|
"Parse IPv6 address",
|
||||||
|
"IPv6 Transition Addresses",
|
||||||
"Parse IPv4 header",
|
"Parse IPv4 header",
|
||||||
"Strip IPv4 header",
|
"Strip IPv4 header",
|
||||||
"Parse TCP",
|
"Parse TCP",
|
||||||
@ -273,7 +293,8 @@
|
|||||||
"Unicode Text Format",
|
"Unicode Text Format",
|
||||||
"Remove Diacritics",
|
"Remove Diacritics",
|
||||||
"Unescape Unicode Characters",
|
"Unescape Unicode Characters",
|
||||||
"Convert to NATO alphabet"
|
"Convert to NATO alphabet",
|
||||||
|
"Convert Leet Speak"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -285,6 +306,7 @@
|
|||||||
"To Upper case",
|
"To Upper case",
|
||||||
"To Lower case",
|
"To Lower case",
|
||||||
"Swap case",
|
"Swap case",
|
||||||
|
"Alternating Caps",
|
||||||
"To Case Insensitive Regex",
|
"To Case Insensitive Regex",
|
||||||
"From Case Insensitive Regex",
|
"From Case Insensitive Regex",
|
||||||
"Add line numbers",
|
"Add line numbers",
|
||||||
@ -326,7 +348,10 @@
|
|||||||
"Pseudo-Random Number Generator",
|
"Pseudo-Random Number Generator",
|
||||||
"Sleep",
|
"Sleep",
|
||||||
"File Tree",
|
"File Tree",
|
||||||
"Insert Delimiter"
|
"Insert Delimiter",
|
||||||
|
"Wrap",
|
||||||
|
"Take nth bytes",
|
||||||
|
"Drop nth bytes"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -359,11 +384,14 @@
|
|||||||
"Regular expression",
|
"Regular expression",
|
||||||
"XPath expression",
|
"XPath expression",
|
||||||
"JPath expression",
|
"JPath expression",
|
||||||
|
"Jsonata Query",
|
||||||
"CSS selector",
|
"CSS selector",
|
||||||
"Extract EXIF",
|
"Extract EXIF",
|
||||||
"Extract ID3",
|
"Extract ID3",
|
||||||
|
"Extract Audio Metadata",
|
||||||
"Extract Files",
|
"Extract Files",
|
||||||
"RAKE"
|
"RAKE",
|
||||||
|
"Template"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -394,6 +422,7 @@
|
|||||||
"name": "Hashing",
|
"name": "Hashing",
|
||||||
"ops": [
|
"ops": [
|
||||||
"Analyse hash",
|
"Analyse hash",
|
||||||
|
"Generate all checksums",
|
||||||
"Generate all hashes",
|
"Generate all hashes",
|
||||||
"MD2",
|
"MD2",
|
||||||
"MD4",
|
"MD4",
|
||||||
@ -412,6 +441,7 @@
|
|||||||
"Snefru",
|
"Snefru",
|
||||||
"BLAKE2b",
|
"BLAKE2b",
|
||||||
"BLAKE2s",
|
"BLAKE2s",
|
||||||
|
"BLAKE3",
|
||||||
"GOST Hash",
|
"GOST Hash",
|
||||||
"Streebog",
|
"Streebog",
|
||||||
"SSDEEP",
|
"SSDEEP",
|
||||||
@ -435,10 +465,10 @@
|
|||||||
"Fletcher-64 Checksum",
|
"Fletcher-64 Checksum",
|
||||||
"Adler-32 Checksum",
|
"Adler-32 Checksum",
|
||||||
"Luhn Checksum",
|
"Luhn Checksum",
|
||||||
"CRC-8 Checksum",
|
"CRC Checksum",
|
||||||
"CRC-16 Checksum",
|
"TCP/IP Checksum",
|
||||||
"CRC-32 Checksum",
|
"XOR Checksum",
|
||||||
"TCP/IP Checksum"
|
"Parity Bit"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -459,8 +489,10 @@
|
|||||||
"CSS Minify",
|
"CSS Minify",
|
||||||
"XPath expression",
|
"XPath expression",
|
||||||
"JPath expression",
|
"JPath expression",
|
||||||
|
"Jq",
|
||||||
"CSS selector",
|
"CSS selector",
|
||||||
"PHP Deserialize",
|
"PHP Deserialize",
|
||||||
|
"PHP Serialize",
|
||||||
"Microsoft Script Decoder",
|
"Microsoft Script Decoder",
|
||||||
"Strip HTML tags",
|
"Strip HTML tags",
|
||||||
"Diff",
|
"Diff",
|
||||||
@ -487,7 +519,8 @@
|
|||||||
"View Bit Plane",
|
"View Bit Plane",
|
||||||
"Randomize Colour Palette",
|
"Randomize Colour Palette",
|
||||||
"Extract LSB",
|
"Extract LSB",
|
||||||
"ELF Info"
|
"ELF Info",
|
||||||
|
"Extract Audio Metadata"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -520,7 +553,8 @@
|
|||||||
"Hex Density chart",
|
"Hex Density chart",
|
||||||
"Scatter chart",
|
"Scatter chart",
|
||||||
"Series chart",
|
"Series chart",
|
||||||
"Heatmap chart"
|
"Heatmap chart",
|
||||||
|
"Extract Audio Metadata"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -532,9 +566,12 @@
|
|||||||
"Chi Square",
|
"Chi Square",
|
||||||
"P-list Viewer",
|
"P-list Viewer",
|
||||||
"Disassemble x86",
|
"Disassemble x86",
|
||||||
|
"Disassemble ARM",
|
||||||
"Pseudo-Random Number Generator",
|
"Pseudo-Random Number Generator",
|
||||||
|
"Pseudo-Random Integer Generator",
|
||||||
"Generate De Bruijn Sequence",
|
"Generate De Bruijn Sequence",
|
||||||
"Generate UUID",
|
"Generate UUID",
|
||||||
|
"Analyse UUID",
|
||||||
"Generate TOTP",
|
"Generate TOTP",
|
||||||
"Generate HOTP",
|
"Generate HOTP",
|
||||||
"Generate QR Code",
|
"Generate QR Code",
|
||||||
|
|||||||
54
src/core/config/scripts/fixCryptoApiImports.mjs
Normal file
54
src/core/config/scripts/fixCryptoApiImports.mjs
Normal 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);
|
||||||
28
src/core/config/scripts/fixSnackBarMarkup.mjs
Normal file
28
src/core/config/scripts/fixSnackBarMarkup.mjs
Normal 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");
|
||||||
@ -7,138 +7,197 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/* eslint no-console: ["off"] */
|
/* eslint no-console: ["off"] */
|
||||||
|
/* eslint jsdoc/require-jsdoc: ["off"] */
|
||||||
|
|
||||||
import prompt from "prompt";
|
|
||||||
import colors from "colors";
|
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import process from "process";
|
import process from "process";
|
||||||
|
import { execSync } from "child_process";
|
||||||
|
|
||||||
const dir = path.join(process.cwd() + "/src/core/config/");
|
const ignoredAuthors = ["github-advanced-security[bot]", "dependabot[bot]"];
|
||||||
if (!fs.existsSync(dir)) {
|
|
||||||
|
async function main() {
|
||||||
|
const dir = path.join(process.cwd() + "/src/core/config/");
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
console.log("\nCWD: " + process.cwd());
|
console.log("\nCWD: " + process.cwd());
|
||||||
console.log("Error: newMinorVersion.mjs should be run from the project root");
|
console.log(
|
||||||
console.log("Example> node --experimental-modules src/core/config/scripts/newMinorVersion.mjs");
|
"Error: newMinorVersion.mjs should be run from the project root",
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
"Example> node --experimental-modules src/core/config/scripts/newMinorVersion.mjs",
|
||||||
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
let changelogData = fs.readFileSync(path.join(process.cwd(), "CHANGELOG.md"), "utf8");
|
let changelogData = fs.readFileSync(
|
||||||
const lastVersion = changelogData.match(/## Details\s+### \[(\d+)\.(\d+)\.(\d+)\]/);
|
path.join(process.cwd(), "CHANGELOG.md"),
|
||||||
const newVersion = [
|
"utf8",
|
||||||
|
);
|
||||||
|
const lastVersion = changelogData.match(
|
||||||
|
/## Details\s+### \[(\d+)\.(\d+)\.(\d+)\]/,
|
||||||
|
);
|
||||||
|
const newVersion = [
|
||||||
parseInt(lastVersion[1], 10),
|
parseInt(lastVersion[1], 10),
|
||||||
parseInt(lastVersion[2], 10) + 1,
|
parseInt(lastVersion[2], 10) + 1,
|
||||||
0
|
0,
|
||||||
];
|
];
|
||||||
|
|
||||||
let knownContributors = changelogData.match(/^\[@([^\]]+)\]/gm);
|
let knownContributors = changelogData.match(/^\[@([^\]]+)\]/gm);
|
||||||
knownContributors = knownContributors.map(c => c.slice(2, -1));
|
knownContributors = knownContributors.map((c) => c.slice(2, -1));
|
||||||
|
|
||||||
const date = (new Date()).toISOString().split("T")[0];
|
const date = new Date().toISOString().split("T")[0];
|
||||||
|
|
||||||
const schema = {
|
const lastVersionSha = execSync(
|
||||||
properties: {
|
`git rev-list -n 1 v${lastVersion[1]}.${lastVersion[2]}.${lastVersion[3]}`,
|
||||||
message: {
|
{
|
||||||
description: "A short but descriptive summary of a feature in this version",
|
encoding: "utf8",
|
||||||
example: "Added 'Op name' operation",
|
|
||||||
prompt: "Feature description",
|
|
||||||
type: "string",
|
|
||||||
required: true,
|
|
||||||
},
|
},
|
||||||
author: {
|
).trim();
|
||||||
description: "The author of the feature (only one supported, edit manually to add more)",
|
if (lastVersionSha.length !== 40) {
|
||||||
example: "n1474335",
|
throw new Error(
|
||||||
prompt: "Author",
|
`Unexpected output from git rev-list: ${lastVersionSha}`,
|
||||||
type: "string",
|
);
|
||||||
default: "n1474335"
|
|
||||||
},
|
|
||||||
id: {
|
|
||||||
description: "The PR number or full commit hash for this feature.",
|
|
||||||
example: "1200",
|
|
||||||
prompt: "Pull request or commit ID",
|
|
||||||
type: "string"
|
|
||||||
},
|
|
||||||
another: {
|
|
||||||
description: "y/n",
|
|
||||||
example: "y",
|
|
||||||
prompt: "Add another feature?",
|
|
||||||
type: "string",
|
|
||||||
pattern: /^[yn]$/,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Build schema
|
|
||||||
for (const prop in schema.properties) {
|
|
||||||
const p = schema.properties[prop];
|
|
||||||
p.description = "\n" + colors.white(p.description) + colors.cyan("\nExample: " + p.example) + "\n" + colors.green(p.prompt);
|
|
||||||
}
|
|
||||||
|
|
||||||
prompt.message = "";
|
|
||||||
prompt.delimiter = ":".green;
|
|
||||||
|
|
||||||
const features = [];
|
|
||||||
const authors = [];
|
|
||||||
const prIDs = [];
|
|
||||||
const commitIDs = [];
|
|
||||||
|
|
||||||
prompt.start();
|
|
||||||
|
|
||||||
const getFeature = function() {
|
|
||||||
prompt.get(schema, (err, result) => {
|
|
||||||
if (err) {
|
|
||||||
console.log("\nExiting script.");
|
|
||||||
process.exit(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
features.push(result);
|
const features = [];
|
||||||
|
|
||||||
if (result.another === "y") {
|
const commits = await (
|
||||||
getFeature();
|
await fetch(`https://api.github.com/repos/gchq/cyberchef/commits`)
|
||||||
|
).json();
|
||||||
|
let foundLast = false;
|
||||||
|
for (const commit of commits) {
|
||||||
|
if (commit.sha === lastVersionSha) {
|
||||||
|
foundLast = true;
|
||||||
|
break;
|
||||||
} else {
|
} else {
|
||||||
|
const feature = {
|
||||||
|
message: "",
|
||||||
|
authors: [],
|
||||||
|
id: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const msgparts = commit.commit.message.split("\n\n");
|
||||||
|
feature.message = msgparts[0];
|
||||||
|
const prIdMatch = feature.message.match(/\(#(\d+)\)$/);
|
||||||
|
if (prIdMatch !== null) {
|
||||||
|
feature.message = feature.message
|
||||||
|
.replace(prIdMatch[0], "")
|
||||||
|
.trim();
|
||||||
|
feature.id = prIdMatch[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ignoredAuthors.includes(commit.author.login)) {
|
||||||
|
feature.authors.push(commit.author.login);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msgparts.length > 1) {
|
||||||
|
msgparts[1]
|
||||||
|
.split("\n")
|
||||||
|
.filter((line) => line.startsWith("Co-authored-by: "))
|
||||||
|
.forEach((line) => {
|
||||||
|
let coAuthor = line.slice("Co-authored-by: ".length);
|
||||||
|
if (coAuthor.indexOf(">") !== -1) {
|
||||||
|
const email = coAuthor.slice(
|
||||||
|
coAuthor.indexOf("<") + 1,
|
||||||
|
coAuthor.indexOf(">"),
|
||||||
|
);
|
||||||
|
if (email.endsWith("@users.noreply.github.com")) {
|
||||||
|
coAuthor = email.slice(
|
||||||
|
email.indexOf("+") + 1,
|
||||||
|
-"@users.noreply.github.com".length,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
"Could not get ID of co-author: " +
|
||||||
|
coAuthor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
"Could not get email of co-author: " + coAuthor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!ignoredAuthors.includes(coAuthor)) {
|
||||||
|
feature.authors.push(coAuthor);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
features.push(feature);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!foundLast) {
|
||||||
|
throw new Error(
|
||||||
|
`Could not find last version commit: ${lastVersionSha} - need to add paging functionality`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let message = `### [${newVersion[0]}.${newVersion[1]}.${newVersion[2]}] - ${date}\n`;
|
let message = `### [${newVersion[0]}.${newVersion[1]}.${newVersion[2]}] - ${date}\n`;
|
||||||
|
|
||||||
features.forEach(feature => {
|
const authors = [];
|
||||||
const id = feature.id.length > 10 ? feature.id.slice(0, 7) : "#" + feature.id;
|
const prIDs = [];
|
||||||
message += `- ${feature.message} [@${feature.author}] | [${id}]\n`;
|
const commitIDs = [];
|
||||||
|
|
||||||
if (!knownContributors.includes(feature.author)) {
|
features.forEach((feature) => {
|
||||||
authors.push(`[@${feature.author}]: https://github.com/${feature.author}`);
|
const id =
|
||||||
|
feature.id.length > 10 ? feature.id.slice(0, 7) : "#" + feature.id;
|
||||||
|
message += `- ${feature.message} ${feature.authors.map((a) => `[@${a}]`).join(" ")} | [${id}]\n`;
|
||||||
|
|
||||||
|
feature.authors.forEach((author) => {
|
||||||
|
if (!knownContributors.includes(author)) {
|
||||||
|
knownContributors.push(author);
|
||||||
|
authors.push(`[@${author}]: https://github.com/${author}`);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (feature.id.length > 10) {
|
if (feature.id.length > 10) {
|
||||||
commitIDs.push(`[${id}]: https://github.com/gchq/CyberChef/commit/${feature.id}`);
|
commitIDs.push(
|
||||||
|
`[${id}]: https://github.com/gchq/CyberChef/commit/${feature.id}`,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
prIDs.push(`[#${feature.id}]: https://github.com/gchq/CyberChef/pull/${feature.id}`);
|
prIDs.push(
|
||||||
|
`[#${feature.id}]: https://github.com/gchq/CyberChef/pull/${feature.id}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Message
|
// Message
|
||||||
changelogData = changelogData.replace(/## Details\n\n/, "## Details\n\n" + message + "\n");
|
changelogData = changelogData.replace(
|
||||||
|
/## Details\n\n/,
|
||||||
|
"## Details\n\n" + message + "\n",
|
||||||
|
);
|
||||||
|
|
||||||
// Tag
|
// Tag
|
||||||
const newTag = `[${newVersion[0]}.${newVersion[1]}.${newVersion[2]}]: https://github.com/gchq/CyberChef/releases/tag/v${newVersion[0]}.${newVersion[1]}.${newVersion[2]}\n`;
|
const newTag = `[${newVersion[0]}.${newVersion[1]}.${newVersion[2]}]: https://github.com/gchq/CyberChef/releases/tag/v${newVersion[0]}.${newVersion[1]}.${newVersion[2]}\n`;
|
||||||
changelogData = changelogData.replace(/\n\n(\[\d+\.\d+\.\d+\]: https)/, "\n\n" + newTag + "$1");
|
changelogData = changelogData.replace(
|
||||||
|
/\n\n(\[\d+\.\d+\.\d+\]: https)/,
|
||||||
|
"\n\n" + newTag + "$1",
|
||||||
|
);
|
||||||
|
|
||||||
// Author
|
// Author
|
||||||
authors.forEach(author => {
|
authors.forEach((author) => {
|
||||||
changelogData = changelogData.replace(/(\n\[@[^\]]+\]: https:\/\/github\.com\/[^\n]+\n)\n/, "$1" + author + "\n\n");
|
changelogData = changelogData.replace(
|
||||||
|
/(\n\[@[^\]]+\]: https:\/\/github\.com\/[^\n]+\n)\n/,
|
||||||
|
"$1" + author + "\n\n",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Commit IDs
|
// Commit IDs
|
||||||
commitIDs.forEach(commitID => {
|
commitIDs.forEach((commitID) => {
|
||||||
changelogData = changelogData.replace(/(\n\[[^\].]+\]: https:\/\/github.com\/gchq\/CyberChef\/commit\/[^\n]+\n)\n/, "$1" + commitID + "\n\n");
|
changelogData = changelogData.replace(
|
||||||
|
/(\n\[[^\].]+\]: https:\/\/github.com\/gchq\/CyberChef\/commit\/[^\n]+\n)\n/,
|
||||||
|
"$1" + commitID + "\n\n",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// PR IDs
|
// PR IDs
|
||||||
prIDs.forEach(prID => {
|
prIDs.forEach((prID) => {
|
||||||
changelogData = changelogData.replace(/(\n\[#[^\]]+\]: https:\/\/github.com\/gchq\/CyberChef\/pull\/[^\n]+\n)\n*$/, "$1" + prID + "\n\n");
|
changelogData = changelogData.replace(
|
||||||
|
/(\n\[#[^\]]+\]: https:\/\/github.com\/gchq\/CyberChef\/(?:pull|issues)\/[^\n]+\n)\n*$/,
|
||||||
|
"$1" + prID + "\n\n",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
fs.writeFileSync(path.join(process.cwd(), "CHANGELOG.md"), changelogData);
|
fs.writeFileSync(path.join(process.cwd(), "CHANGELOG.md"), changelogData);
|
||||||
|
}
|
||||||
console.log("Written CHANGELOG.md\nCommit changes and then run `npm version minor`.");
|
main().catch(console.error);
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
getFeature();
|
|
||||||
|
|||||||
103
src/core/lib/AudioBytes.mjs
Normal file
103
src/core/lib/AudioBytes.mjs
Normal 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();
|
||||||
|
}
|
||||||
82
src/core/lib/AudioMetaSchema.mjs
Normal file
82
src/core/lib/AudioMetaSchema.mjs
Normal 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 };
|
||||||
|
}
|
||||||
630
src/core/lib/AudioParsers.mjs
Normal file
630
src/core/lib/AudioParsers.mjs
Normal 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;
|
||||||
|
}
|
||||||
23
src/core/lib/Base32.mjs
Normal file
23
src/core/lib/Base32.mjs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
// import Utils from "../Utils.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base32 resources.
|
||||||
|
*
|
||||||
|
* @author Peter C-S [petercs@purelymail.com]
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base32 alphabets.
|
||||||
|
*/
|
||||||
|
export const ALPHABET_OPTIONS = [
|
||||||
|
{
|
||||||
|
name: "Standard", // https://www.rfc-editor.org/rfc/rfc4648#section-6
|
||||||
|
value: "A-Z2-7=",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Hex Extended", // https://www.rfc-editor.org/rfc/rfc4648#section-7
|
||||||
|
value: "0-9A-V=",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
371
src/core/lib/Bech32.mjs
Normal file
371
src/core/lib/Bech32.mjs
Normal file
@ -0,0 +1,371 @@
|
|||||||
|
/**
|
||||||
|
* Pure JavaScript implementation of Bech32 and Bech32m encoding.
|
||||||
|
*
|
||||||
|
* Bech32 is defined in BIP-0173: https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki
|
||||||
|
* Bech32m is defined in BIP-0350: https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki
|
||||||
|
*
|
||||||
|
* @author Medjedtxm
|
||||||
|
* @copyright Crown Copyright 2025
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import OperationError from "../errors/OperationError.mjs";
|
||||||
|
|
||||||
|
/** Bech32 character set (32 characters, excludes 1, b, i, o) */
|
||||||
|
const CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
||||||
|
|
||||||
|
/** Reverse lookup table for decoding */
|
||||||
|
const CHARSET_REV = {};
|
||||||
|
for (let i = 0; i < CHARSET.length; i++) {
|
||||||
|
CHARSET_REV[CHARSET[i]] = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Checksum constant for Bech32 (BIP-0173) */
|
||||||
|
const BECH32_CONST = 1;
|
||||||
|
|
||||||
|
/** Checksum constant for Bech32m (BIP-0350) */
|
||||||
|
const BECH32M_CONST = 0x2bc830a3;
|
||||||
|
|
||||||
|
/** Generator polynomial coefficients for checksum */
|
||||||
|
const GENERATOR = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute the polymod checksum
|
||||||
|
* @param {number[]} values - Array of 5-bit values
|
||||||
|
* @returns {number} - Checksum value
|
||||||
|
*/
|
||||||
|
function polymod(values) {
|
||||||
|
let chk = 1;
|
||||||
|
for (const v of values) {
|
||||||
|
const top = chk >> 25;
|
||||||
|
chk = ((chk & 0x1ffffff) << 5) ^ v;
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
if ((top >> i) & 1) {
|
||||||
|
chk ^= GENERATOR[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return chk;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expand HRP for checksum computation
|
||||||
|
* @param {string} hrp - Human-readable part (lowercase)
|
||||||
|
* @returns {number[]} - Expanded values
|
||||||
|
*/
|
||||||
|
function hrpExpand(hrp) {
|
||||||
|
const result = [];
|
||||||
|
for (let i = 0; i < hrp.length; i++) {
|
||||||
|
result.push(hrp.charCodeAt(i) >> 5);
|
||||||
|
}
|
||||||
|
result.push(0);
|
||||||
|
for (let i = 0; i < hrp.length; i++) {
|
||||||
|
result.push(hrp.charCodeAt(i) & 31);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify checksum of a Bech32/Bech32m string
|
||||||
|
* @param {string} hrp - Human-readable part (lowercase)
|
||||||
|
* @param {number[]} data - Data including checksum (5-bit values)
|
||||||
|
* @param {string} encoding - "Bech32" or "Bech32m"
|
||||||
|
* @returns {boolean} - True if checksum is valid
|
||||||
|
*/
|
||||||
|
function verifyChecksum(hrp, data, encoding) {
|
||||||
|
const constant = encoding === "Bech32m" ? BECH32M_CONST : BECH32_CONST;
|
||||||
|
return polymod(hrpExpand(hrp).concat(data)) === constant;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create checksum for Bech32/Bech32m encoding
|
||||||
|
* @param {string} hrp - Human-readable part (lowercase)
|
||||||
|
* @param {number[]} data - Data values (5-bit)
|
||||||
|
* @param {string} encoding - "Bech32" or "Bech32m"
|
||||||
|
* @returns {number[]} - 6 checksum values
|
||||||
|
*/
|
||||||
|
function createChecksum(hrp, data, encoding) {
|
||||||
|
const constant = encoding === "Bech32m" ? BECH32M_CONST : BECH32_CONST;
|
||||||
|
const values = hrpExpand(hrp).concat(data).concat([0, 0, 0, 0, 0, 0]);
|
||||||
|
const mod = polymod(values) ^ constant;
|
||||||
|
const result = [];
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
result.push((mod >> (5 * (5 - i))) & 31);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert 8-bit bytes to 5-bit words
|
||||||
|
* @param {number[]|Uint8Array} data - Input bytes
|
||||||
|
* @returns {number[]} - 5-bit words
|
||||||
|
*/
|
||||||
|
export function toWords(data) {
|
||||||
|
let value = 0;
|
||||||
|
let bits = 0;
|
||||||
|
const result = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
value = (value << 8) | data[i];
|
||||||
|
bits += 8;
|
||||||
|
|
||||||
|
while (bits >= 5) {
|
||||||
|
bits -= 5;
|
||||||
|
result.push((value >> bits) & 31);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pad remaining bits
|
||||||
|
if (bits > 0) {
|
||||||
|
result.push((value << (5 - bits)) & 31);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert 5-bit words to 8-bit bytes
|
||||||
|
* @param {number[]} words - 5-bit words
|
||||||
|
* @returns {number[]} - Output bytes
|
||||||
|
*/
|
||||||
|
export function fromWords(words) {
|
||||||
|
let value = 0;
|
||||||
|
let bits = 0;
|
||||||
|
const result = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < words.length; i++) {
|
||||||
|
value = (value << 5) | words[i];
|
||||||
|
bits += 5;
|
||||||
|
|
||||||
|
while (bits >= 8) {
|
||||||
|
bits -= 8;
|
||||||
|
result.push((value >> bits) & 255);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for invalid padding per BIP-0173
|
||||||
|
// Condition 1: Cannot have 5+ bits remaining (would indicate incomplete byte)
|
||||||
|
if (bits >= 5) {
|
||||||
|
throw new OperationError("Invalid padding: too many bits remaining");
|
||||||
|
}
|
||||||
|
// Condition 2: Remaining padding bits must all be zero
|
||||||
|
if (bits > 0) {
|
||||||
|
const paddingValue = (value << (8 - bits)) & 255;
|
||||||
|
if (paddingValue !== 0) {
|
||||||
|
throw new OperationError("Invalid padding: non-zero bits in padding");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode data to Bech32/Bech32m string
|
||||||
|
*
|
||||||
|
* @param {string} hrp - Human-readable part
|
||||||
|
* @param {number[]|Uint8Array} data - Data bytes to encode
|
||||||
|
* @param {string} encoding - "Bech32" or "Bech32m"
|
||||||
|
* @param {boolean} segwit - If true, treat first byte as witness version (for Bitcoin SegWit)
|
||||||
|
* @returns {string} - Encoded Bech32/Bech32m string
|
||||||
|
*/
|
||||||
|
export function encode(hrp, data, encoding = "Bech32", segwit = false) {
|
||||||
|
// Validate HRP
|
||||||
|
if (!hrp || hrp.length === 0) {
|
||||||
|
throw new OperationError("Human-Readable Part (HRP) cannot be empty.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check HRP characters (ASCII 33-126)
|
||||||
|
for (let i = 0; i < hrp.length; i++) {
|
||||||
|
const c = hrp.charCodeAt(i);
|
||||||
|
if (c < 33 || c > 126) {
|
||||||
|
throw new OperationError(`HRP contains invalid character at position ${i}. Only printable ASCII characters (33-126) are allowed.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert HRP to lowercase
|
||||||
|
const hrpLower = hrp.toLowerCase();
|
||||||
|
|
||||||
|
let words;
|
||||||
|
if (segwit && data.length >= 2) {
|
||||||
|
// SegWit encoding: first byte is witness version (0-16), rest is witness program
|
||||||
|
const witnessVersion = data[0];
|
||||||
|
if (witnessVersion > 16) {
|
||||||
|
throw new OperationError(`Invalid witness version: ${witnessVersion}. Must be 0-16.`);
|
||||||
|
}
|
||||||
|
const witnessProgram = Array.prototype.slice.call(data, 1);
|
||||||
|
|
||||||
|
// Validate witness program length per BIP-0141
|
||||||
|
if (witnessProgram.length < 2 || witnessProgram.length > 40) {
|
||||||
|
throw new OperationError(`Invalid witness program length: ${witnessProgram.length}. Must be 2-40 bytes.`);
|
||||||
|
}
|
||||||
|
if (witnessVersion === 0 && witnessProgram.length !== 20 && witnessProgram.length !== 32) {
|
||||||
|
throw new OperationError(`Invalid witness program length for v0: ${witnessProgram.length}. Must be 20 or 32 bytes.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Witness version is kept as single 5-bit value, program is converted
|
||||||
|
words = [witnessVersion].concat(toWords(witnessProgram));
|
||||||
|
} else {
|
||||||
|
// Generic encoding: convert all bytes to 5-bit words
|
||||||
|
words = toWords(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create checksum
|
||||||
|
const checksum = createChecksum(hrpLower, words, encoding);
|
||||||
|
|
||||||
|
// Build result string
|
||||||
|
let result = hrpLower + "1";
|
||||||
|
for (const w of words.concat(checksum)) {
|
||||||
|
result += CHARSET[w];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check maximum length (90 characters)
|
||||||
|
if (result.length > 90) {
|
||||||
|
throw new OperationError(`Encoded string exceeds maximum length of 90 characters (got ${result.length}). Consider using smaller input data.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode a Bech32/Bech32m string
|
||||||
|
*
|
||||||
|
* @param {string} str - Bech32/Bech32m encoded string
|
||||||
|
* @param {string} encoding - "Bech32", "Bech32m", or "Auto-detect"
|
||||||
|
* @returns {{hrp: string, data: number[]}} - Decoded HRP and data bytes
|
||||||
|
*/
|
||||||
|
export function decode(str, encoding = "Auto-detect") {
|
||||||
|
// Check for empty input
|
||||||
|
if (!str || str.length === 0) {
|
||||||
|
throw new OperationError("Input cannot be empty.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check maximum length
|
||||||
|
if (str.length > 90) {
|
||||||
|
throw new OperationError(`Invalid Bech32 string: exceeds maximum length of 90 characters (got ${str.length}).`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for mixed case
|
||||||
|
const hasUpper = /[A-Z]/.test(str);
|
||||||
|
const hasLower = /[a-z]/.test(str);
|
||||||
|
if (hasUpper && hasLower) {
|
||||||
|
throw new OperationError("Invalid Bech32 string: mixed case is not allowed. Use all uppercase or all lowercase.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to lowercase for processing
|
||||||
|
str = str.toLowerCase();
|
||||||
|
|
||||||
|
// Find separator (last occurrence of '1')
|
||||||
|
const sepIndex = str.lastIndexOf("1");
|
||||||
|
if (sepIndex === -1) {
|
||||||
|
throw new OperationError("Invalid Bech32 string: no separator '1' found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sepIndex === 0) {
|
||||||
|
throw new OperationError("Invalid Bech32 string: Human-Readable Part (HRP) cannot be empty.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sepIndex + 7 > str.length) {
|
||||||
|
throw new OperationError("Invalid Bech32 string: data part is too short (minimum 6 characters for checksum).");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract HRP and data part
|
||||||
|
const hrp = str.substring(0, sepIndex);
|
||||||
|
const dataPart = str.substring(sepIndex + 1);
|
||||||
|
|
||||||
|
// Validate HRP characters
|
||||||
|
for (let i = 0; i < hrp.length; i++) {
|
||||||
|
const c = hrp.charCodeAt(i);
|
||||||
|
if (c < 33 || c > 126) {
|
||||||
|
throw new OperationError(`HRP contains invalid character at position ${i}.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode data characters to 5-bit values
|
||||||
|
const data = [];
|
||||||
|
for (let i = 0; i < dataPart.length; i++) {
|
||||||
|
const c = dataPart[i];
|
||||||
|
if (CHARSET_REV[c] === undefined) {
|
||||||
|
throw new OperationError(`Invalid character '${c}' at position ${sepIndex + 1 + i}.`);
|
||||||
|
}
|
||||||
|
data.push(CHARSET_REV[c]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify checksum
|
||||||
|
let usedEncoding;
|
||||||
|
if (encoding === "Bech32") {
|
||||||
|
if (!verifyChecksum(hrp, data, "Bech32")) {
|
||||||
|
throw new OperationError("Invalid Bech32 checksum.");
|
||||||
|
}
|
||||||
|
usedEncoding = "Bech32";
|
||||||
|
} else if (encoding === "Bech32m") {
|
||||||
|
if (!verifyChecksum(hrp, data, "Bech32m")) {
|
||||||
|
throw new OperationError("Invalid Bech32m checksum.");
|
||||||
|
}
|
||||||
|
usedEncoding = "Bech32m";
|
||||||
|
} else {
|
||||||
|
// Auto-detect: try Bech32 first, then Bech32m
|
||||||
|
if (verifyChecksum(hrp, data, "Bech32")) {
|
||||||
|
usedEncoding = "Bech32";
|
||||||
|
} else if (verifyChecksum(hrp, data, "Bech32m")) {
|
||||||
|
usedEncoding = "Bech32m";
|
||||||
|
} else {
|
||||||
|
throw new OperationError("Invalid Bech32/Bech32m string: checksum verification failed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove checksum (last 6 values)
|
||||||
|
const words = data.slice(0, data.length - 6);
|
||||||
|
|
||||||
|
// Check if this is likely a SegWit address (Bitcoin, Litecoin, etc.)
|
||||||
|
// For SegWit, the first 5-bit word is the witness version (0-16)
|
||||||
|
// and should be extracted separately, not bit-converted with the rest
|
||||||
|
const segwitHrps = ["bc", "tb", "ltc", "tltc", "bcrt"];
|
||||||
|
const couldBeSegWit = segwitHrps.includes(hrp) && words.length > 0 && words[0] <= 16;
|
||||||
|
|
||||||
|
let bytes;
|
||||||
|
let witnessVersion = null;
|
||||||
|
|
||||||
|
if (couldBeSegWit) {
|
||||||
|
// Try SegWit decode first
|
||||||
|
try {
|
||||||
|
witnessVersion = words[0];
|
||||||
|
const programWords = words.slice(1);
|
||||||
|
const programBytes = fromWords(programWords);
|
||||||
|
|
||||||
|
// Validate SegWit witness program length (20 or 32 bytes for v0, 2-40 for others)
|
||||||
|
const validV0 = witnessVersion === 0 && (programBytes.length === 20 || programBytes.length === 32);
|
||||||
|
const validOther = witnessVersion !== 0 && programBytes.length >= 2 && programBytes.length <= 40;
|
||||||
|
|
||||||
|
if (validV0 || validOther) {
|
||||||
|
// Valid SegWit address
|
||||||
|
bytes = [witnessVersion, ...programBytes];
|
||||||
|
} else {
|
||||||
|
// Not valid SegWit, fall back to generic decode
|
||||||
|
witnessVersion = null;
|
||||||
|
bytes = fromWords(words);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// SegWit decode failed, try generic decode
|
||||||
|
witnessVersion = null;
|
||||||
|
try {
|
||||||
|
bytes = fromWords(words);
|
||||||
|
} catch (e2) {
|
||||||
|
throw new OperationError(`Failed to decode data: ${e2.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Generic Bech32: convert all words
|
||||||
|
try {
|
||||||
|
bytes = fromWords(words);
|
||||||
|
} catch (e) {
|
||||||
|
throw new OperationError(`Failed to decode data: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
hrp: hrp,
|
||||||
|
data: bytes,
|
||||||
|
encoding: usedEncoding,
|
||||||
|
witnessVersion: witnessVersion
|
||||||
|
};
|
||||||
|
}
|
||||||
73
src/core/lib/BigIntUtils.mjs
Normal file
73
src/core/lib/BigIntUtils.mjs
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
/**
|
||||||
|
* @author p-leriche [philip.leriche@cantab.net]
|
||||||
|
* @copyright Crown Copyright 2025
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import OperationError from "../errors/OperationError.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number theory utilities used by cryptographic operations.
|
||||||
|
*
|
||||||
|
* Currently provides:
|
||||||
|
* - parseBigInt
|
||||||
|
* - Extended Euclidean Algorithm
|
||||||
|
* - Modular Exponentiation
|
||||||
|
*
|
||||||
|
* Additional algorithms may be added as required.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* parseBigInt helper operation
|
||||||
|
*/
|
||||||
|
export function parseBigInt(value, param) {
|
||||||
|
const v = (value ?? "").trim();
|
||||||
|
if (/^0x[0-9a-f]+$/i.test(v)) return BigInt(v);
|
||||||
|
if (/^[+-]?[0-9]+$/.test(v)) return BigInt(v);
|
||||||
|
throw new OperationError(param + " must be decimal or hex (0x...)");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extended Euclidean Algorithm
|
||||||
|
*
|
||||||
|
* Returns [g, x, y] such that:
|
||||||
|
* a*x + b*y = g = gcd(a, b)
|
||||||
|
*
|
||||||
|
* (Uses an iterative algorithm to avoid possible stack overflow)
|
||||||
|
*/
|
||||||
|
export function egcd(a, b) {
|
||||||
|
let oldR = a, r = b;
|
||||||
|
let oldS = 1n, s = 0n;
|
||||||
|
let oldT = 0n, t = 1n;
|
||||||
|
|
||||||
|
while (r !== 0n) {
|
||||||
|
const quotient = oldR / r;
|
||||||
|
|
||||||
|
[oldR, r] = [r, oldR - quotient * r];
|
||||||
|
[oldS, s] = [s, oldS - quotient * s];
|
||||||
|
[oldT, t] = [t, oldT - quotient * t];
|
||||||
|
}
|
||||||
|
|
||||||
|
// oldR is the gcd
|
||||||
|
// oldS and oldT are the Bézout coefficients
|
||||||
|
return [oldR, oldS, oldT];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modular exponentiation
|
||||||
|
*/
|
||||||
|
export function modPow(base, exponent, modulus) {
|
||||||
|
let result = 1n;
|
||||||
|
base %= modulus;
|
||||||
|
|
||||||
|
while (exponent > 0n) {
|
||||||
|
if (exponent & 1n) {
|
||||||
|
result = (result * base) % modulus;
|
||||||
|
}
|
||||||
|
base = (base * base) % modulus;
|
||||||
|
exponent >>= 1n;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
@ -45,6 +45,11 @@ export function search(input, searchRegex, removeRegex=null, sortBy=null, unique
|
|||||||
return results;
|
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
|
* URL regular expression
|
||||||
@ -62,3 +67,9 @@ export const URL_REGEX = new RegExp(protocol + hostname + "(?:" + port + ")?(?:"
|
|||||||
* Domain name regular expression
|
* Domain name regular expression
|
||||||
*/
|
*/
|
||||||
export const DOMAIN_REGEX = /\b((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63}\b/ig;
|
export const DOMAIN_REGEX = /\b((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63}\b/ig;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DMARC Domain name regular expression
|
||||||
|
*/
|
||||||
|
export const DMARC_DOMAIN_REGEX = /\b((?=[a-z0-9_-]{1,63}\.)(xn--)?[a-z0-9_]+(-[a-z0-9_]+)*\.)+[a-z]{2,63}\b/ig;
|
||||||
|
|||||||
@ -33,7 +33,7 @@ export function toHex(data, delim=" ", padding=2, extraDelim="", lineSize=0) {
|
|||||||
if (data instanceof ArrayBuffer) data = new Uint8Array(data);
|
if (data instanceof ArrayBuffer) data = new Uint8Array(data);
|
||||||
|
|
||||||
let output = "";
|
let output = "";
|
||||||
const prepend = (delim === "0x" || delim === "\\x");
|
const prepend = (delim === "0x" || delim === "\\x" || delim === "%");
|
||||||
|
|
||||||
for (let i = 0; i < data.length; i++) {
|
for (let i = 0; i < data.length; i++) {
|
||||||
const hex = data[i].toString(16).padStart(padding, "0");
|
const hex = data[i].toString(16).padStart(padding, "0");
|
||||||
|
|||||||
@ -1,251 +0,0 @@
|
|||||||
/**
|
|
||||||
* Image manipulation resources
|
|
||||||
*
|
|
||||||
* @author j433866 [j433866@gmail.com]
|
|
||||||
* @copyright Crown Copyright 2019
|
|
||||||
* @license Apache-2.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
import OperationError from "../errors/OperationError.mjs";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gaussian blurs an image.
|
|
||||||
*
|
|
||||||
* @param {jimp} input
|
|
||||||
* @param {number} radius
|
|
||||||
* @param {boolean} fast
|
|
||||||
* @returns {jimp}
|
|
||||||
*/
|
|
||||||
export function gaussianBlur (input, radius) {
|
|
||||||
try {
|
|
||||||
// From http://blog.ivank.net/fastest-gaussian-blur.html
|
|
||||||
const boxes = boxesForGauss(radius, 3);
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
input = boxBlur(input, (boxes[i] - 1) / 2);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
throw new OperationError(`Error blurring image. (${err})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return input;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {number} radius
|
|
||||||
* @param {number} numBoxes
|
|
||||||
* @returns {Array}
|
|
||||||
*/
|
|
||||||
function boxesForGauss(radius, numBoxes) {
|
|
||||||
const idealWidth = Math.sqrt((12 * radius * radius / numBoxes) + 1);
|
|
||||||
|
|
||||||
let wl = Math.floor(idealWidth);
|
|
||||||
|
|
||||||
if (wl % 2 === 0) {
|
|
||||||
wl--;
|
|
||||||
}
|
|
||||||
|
|
||||||
const wu = wl + 2;
|
|
||||||
|
|
||||||
const mIdeal = (12 * radius * radius - numBoxes * wl * wl - 4 * numBoxes * wl - 3 * numBoxes) / (-4 * wl - 4);
|
|
||||||
const m = Math.round(mIdeal);
|
|
||||||
|
|
||||||
const sizes = [];
|
|
||||||
for (let i = 0; i < numBoxes; i++) {
|
|
||||||
sizes.push(i < m ? wl : wu);
|
|
||||||
}
|
|
||||||
return sizes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Applies a box blur effect to the image
|
|
||||||
*
|
|
||||||
* @param {jimp} source
|
|
||||||
* @param {number} radius
|
|
||||||
* @returns {jimp}
|
|
||||||
*/
|
|
||||||
function boxBlur (source, radius) {
|
|
||||||
const width = source.bitmap.width;
|
|
||||||
const height = source.bitmap.height;
|
|
||||||
let output = source.clone();
|
|
||||||
output = boxBlurH(source, output, width, height, radius);
|
|
||||||
source = boxBlurV(output, source, width, height, radius);
|
|
||||||
|
|
||||||
return source;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Applies the horizontal blur
|
|
||||||
*
|
|
||||||
* @param {jimp} source
|
|
||||||
* @param {jimp} output
|
|
||||||
* @param {number} width
|
|
||||||
* @param {number} height
|
|
||||||
* @param {number} radius
|
|
||||||
* @returns {jimp}
|
|
||||||
*/
|
|
||||||
function boxBlurH (source, output, width, height, radius) {
|
|
||||||
const iarr = 1 / (radius + radius + 1);
|
|
||||||
for (let i = 0; i < height; i++) {
|
|
||||||
let ti = 0,
|
|
||||||
li = ti,
|
|
||||||
ri = ti + radius;
|
|
||||||
const idx = source.getPixelIndex(ti, i);
|
|
||||||
const firstValRed = source.bitmap.data[idx],
|
|
||||||
firstValGreen = source.bitmap.data[idx + 1],
|
|
||||||
firstValBlue = source.bitmap.data[idx + 2],
|
|
||||||
firstValAlpha = source.bitmap.data[idx + 3];
|
|
||||||
|
|
||||||
const lastIdx = source.getPixelIndex(width - 1, i),
|
|
||||||
lastValRed = source.bitmap.data[lastIdx],
|
|
||||||
lastValGreen = source.bitmap.data[lastIdx + 1],
|
|
||||||
lastValBlue = source.bitmap.data[lastIdx + 2],
|
|
||||||
lastValAlpha = source.bitmap.data[lastIdx + 3];
|
|
||||||
|
|
||||||
let red = (radius + 1) * firstValRed;
|
|
||||||
let green = (radius + 1) * firstValGreen;
|
|
||||||
let blue = (radius + 1) * firstValBlue;
|
|
||||||
let alpha = (radius + 1) * firstValAlpha;
|
|
||||||
|
|
||||||
for (let j = 0; j < radius; j++) {
|
|
||||||
const jIdx = source.getPixelIndex(ti + j, i);
|
|
||||||
red += source.bitmap.data[jIdx];
|
|
||||||
green += source.bitmap.data[jIdx + 1];
|
|
||||||
blue += source.bitmap.data[jIdx + 2];
|
|
||||||
alpha += source.bitmap.data[jIdx + 3];
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let j = 0; j <= radius; j++) {
|
|
||||||
const jIdx = source.getPixelIndex(ri++, i);
|
|
||||||
red += source.bitmap.data[jIdx] - firstValRed;
|
|
||||||
green += source.bitmap.data[jIdx + 1] - firstValGreen;
|
|
||||||
blue += source.bitmap.data[jIdx + 2] - firstValBlue;
|
|
||||||
alpha += source.bitmap.data[jIdx + 3] - firstValAlpha;
|
|
||||||
|
|
||||||
const tiIdx = source.getPixelIndex(ti++, i);
|
|
||||||
output.bitmap.data[tiIdx] = Math.round(red * iarr);
|
|
||||||
output.bitmap.data[tiIdx + 1] = Math.round(green * iarr);
|
|
||||||
output.bitmap.data[tiIdx + 2] = Math.round(blue * iarr);
|
|
||||||
output.bitmap.data[tiIdx + 3] = Math.round(alpha * iarr);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let j = radius + 1; j < width - radius; j++) {
|
|
||||||
const riIdx = source.getPixelIndex(ri++, i);
|
|
||||||
const liIdx = source.getPixelIndex(li++, i);
|
|
||||||
red += source.bitmap.data[riIdx] - source.bitmap.data[liIdx];
|
|
||||||
green += source.bitmap.data[riIdx + 1] - source.bitmap.data[liIdx + 1];
|
|
||||||
blue += source.bitmap.data[riIdx + 2] - source.bitmap.data[liIdx + 2];
|
|
||||||
alpha += source.bitmap.data[riIdx + 3] - source.bitmap.data[liIdx + 3];
|
|
||||||
|
|
||||||
const tiIdx = source.getPixelIndex(ti++, i);
|
|
||||||
output.bitmap.data[tiIdx] = Math.round(red * iarr);
|
|
||||||
output.bitmap.data[tiIdx + 1] = Math.round(green * iarr);
|
|
||||||
output.bitmap.data[tiIdx + 2] = Math.round(blue * iarr);
|
|
||||||
output.bitmap.data[tiIdx + 3] = Math.round(alpha * iarr);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let j = width - radius; j < width; j++) {
|
|
||||||
const liIdx = source.getPixelIndex(li++, i);
|
|
||||||
red += lastValRed - source.bitmap.data[liIdx];
|
|
||||||
green += lastValGreen - source.bitmap.data[liIdx + 1];
|
|
||||||
blue += lastValBlue - source.bitmap.data[liIdx + 2];
|
|
||||||
alpha += lastValAlpha - source.bitmap.data[liIdx + 3];
|
|
||||||
|
|
||||||
const tiIdx = source.getPixelIndex(ti++, i);
|
|
||||||
output.bitmap.data[tiIdx] = Math.round(red * iarr);
|
|
||||||
output.bitmap.data[tiIdx + 1] = Math.round(green * iarr);
|
|
||||||
output.bitmap.data[tiIdx + 2] = Math.round(blue * iarr);
|
|
||||||
output.bitmap.data[tiIdx + 3] = Math.round(alpha * iarr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Applies the vertical blur
|
|
||||||
*
|
|
||||||
* @param {jimp} source
|
|
||||||
* @param {jimp} output
|
|
||||||
* @param {number} width
|
|
||||||
* @param {number} height
|
|
||||||
* @param {number} radius
|
|
||||||
* @returns {jimp}
|
|
||||||
*/
|
|
||||||
function boxBlurV (source, output, width, height, radius) {
|
|
||||||
const iarr = 1 / (radius + radius + 1);
|
|
||||||
for (let i = 0; i < width; i++) {
|
|
||||||
let ti = 0,
|
|
||||||
li = ti,
|
|
||||||
ri = ti + radius;
|
|
||||||
|
|
||||||
const idx = source.getPixelIndex(i, ti);
|
|
||||||
|
|
||||||
const firstValRed = source.bitmap.data[idx],
|
|
||||||
firstValGreen = source.bitmap.data[idx + 1],
|
|
||||||
firstValBlue = source.bitmap.data[idx + 2],
|
|
||||||
firstValAlpha = source.bitmap.data[idx + 3];
|
|
||||||
|
|
||||||
const lastIdx = source.getPixelIndex(i, height - 1),
|
|
||||||
lastValRed = source.bitmap.data[lastIdx],
|
|
||||||
lastValGreen = source.bitmap.data[lastIdx + 1],
|
|
||||||
lastValBlue = source.bitmap.data[lastIdx + 2],
|
|
||||||
lastValAlpha = source.bitmap.data[lastIdx + 3];
|
|
||||||
|
|
||||||
let red = (radius + 1) * firstValRed;
|
|
||||||
let green = (radius + 1) * firstValGreen;
|
|
||||||
let blue = (radius + 1) * firstValBlue;
|
|
||||||
let alpha = (radius + 1) * firstValAlpha;
|
|
||||||
|
|
||||||
for (let j = 0; j < radius; j++) {
|
|
||||||
const jIdx = source.getPixelIndex(i, ti + j);
|
|
||||||
red += source.bitmap.data[jIdx];
|
|
||||||
green += source.bitmap.data[jIdx + 1];
|
|
||||||
blue += source.bitmap.data[jIdx + 2];
|
|
||||||
alpha += source.bitmap.data[jIdx + 3];
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let j = 0; j <= radius; j++) {
|
|
||||||
const riIdx = source.getPixelIndex(i, ri++);
|
|
||||||
red += source.bitmap.data[riIdx] - firstValRed;
|
|
||||||
green += source.bitmap.data[riIdx + 1] - firstValGreen;
|
|
||||||
blue += source.bitmap.data[riIdx + 2] - firstValBlue;
|
|
||||||
alpha += source.bitmap.data[riIdx + 3] - firstValAlpha;
|
|
||||||
|
|
||||||
const tiIdx = source.getPixelIndex(i, ti++);
|
|
||||||
output.bitmap.data[tiIdx] = Math.round(red * iarr);
|
|
||||||
output.bitmap.data[tiIdx + 1] = Math.round(green * iarr);
|
|
||||||
output.bitmap.data[tiIdx + 2] = Math.round(blue * iarr);
|
|
||||||
output.bitmap.data[tiIdx + 3] = Math.round(alpha * iarr);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let j = radius + 1; j < height - radius; j++) {
|
|
||||||
const riIdx = source.getPixelIndex(i, ri++);
|
|
||||||
const liIdx = source.getPixelIndex(i, li++);
|
|
||||||
red += source.bitmap.data[riIdx] - source.bitmap.data[liIdx];
|
|
||||||
green += source.bitmap.data[riIdx + 1] - source.bitmap.data[liIdx + 1];
|
|
||||||
blue += source.bitmap.data[riIdx + 2] - source.bitmap.data[liIdx + 2];
|
|
||||||
alpha += source.bitmap.data[riIdx + 3] - source.bitmap.data[liIdx + 3];
|
|
||||||
|
|
||||||
const tiIdx = source.getPixelIndex(i, ti++);
|
|
||||||
output.bitmap.data[tiIdx] = Math.round(red * iarr);
|
|
||||||
output.bitmap.data[tiIdx + 1] = Math.round(green * iarr);
|
|
||||||
output.bitmap.data[tiIdx + 2] = Math.round(blue * iarr);
|
|
||||||
output.bitmap.data[tiIdx + 3] = Math.round(alpha * iarr);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let j = height - radius; j < height; j++) {
|
|
||||||
const liIdx = source.getPixelIndex(i, li++);
|
|
||||||
red += lastValRed - source.bitmap.data[liIdx];
|
|
||||||
green += lastValGreen - source.bitmap.data[liIdx + 1];
|
|
||||||
blue += lastValBlue - source.bitmap.data[liIdx + 2];
|
|
||||||
alpha += lastValAlpha - source.bitmap.data[liIdx + 3];
|
|
||||||
|
|
||||||
const tiIdx = source.getPixelIndex(i, ti++);
|
|
||||||
output.bitmap.data[tiIdx] = Math.round(red * iarr);
|
|
||||||
output.bitmap.data[tiIdx + 1] = Math.round(green * iarr);
|
|
||||||
output.bitmap.data[tiIdx + 2] = Math.round(blue * iarr);
|
|
||||||
output.bitmap.data[tiIdx + 3] = Math.round(alpha * iarr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
@ -91,9 +91,7 @@ export function toJA4(bytes) {
|
|||||||
let alpn = "00";
|
let alpn = "00";
|
||||||
for (const ext of tlsr.handshake.value.extensions.value) {
|
for (const ext of tlsr.handshake.value.extensions.value) {
|
||||||
if (ext.type.value === "application_layer_protocol_negotiation") {
|
if (ext.type.value === "application_layer_protocol_negotiation") {
|
||||||
alpn = parseFirstALPNValue(ext.value.data);
|
alpn = alpnFingerprint(parseFirstALPNValue(ext.value.data));
|
||||||
alpn = alpn.charAt(0) + alpn.charAt(alpn.length - 1);
|
|
||||||
if (alpn.charCodeAt(0) > 127) alpn = "99";
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -212,9 +210,7 @@ export function toJA4S(bytes) {
|
|||||||
let alpn = "00";
|
let alpn = "00";
|
||||||
for (const ext of tlsr.handshake.value.extensions.value) {
|
for (const ext of tlsr.handshake.value.extensions.value) {
|
||||||
if (ext.type.value === "application_layer_protocol_negotiation") {
|
if (ext.type.value === "application_layer_protocol_negotiation") {
|
||||||
alpn = parseFirstALPNValue(ext.value.data);
|
alpn = alpnFingerprint(parseFirstALPNValue(ext.value.data));
|
||||||
alpn = alpn.charAt(0) + alpn.charAt(alpn.length - 1);
|
|
||||||
if (alpn.charCodeAt(0) > 127) alpn = "99";
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -262,3 +258,33 @@ function tlsVersionMapper(version) {
|
|||||||
default: return "00"; // Unknown
|
default: return "00"; // Unknown
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a byte is ASCII alphanumeric (0-9, A-Z, a-z).
|
||||||
|
* @param {number} byte
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function isAlphanumeric(byte) {
|
||||||
|
return (byte >= 0x30 && byte <= 0x39) ||
|
||||||
|
(byte >= 0x41 && byte <= 0x5A) ||
|
||||||
|
(byte >= 0x61 && byte <= 0x7A);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the 2-character ALPN fingerprint from raw ALPN bytes.
|
||||||
|
* If both first and last bytes are ASCII alphanumeric, returns their characters.
|
||||||
|
* Otherwise, returns first hex digit of first byte + last hex digit of last byte.
|
||||||
|
* @param {Uint8Array|null} rawBytes
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function alpnFingerprint(rawBytes) {
|
||||||
|
if (!rawBytes || rawBytes.length === 0) return "00";
|
||||||
|
const firstByte = rawBytes[0];
|
||||||
|
const lastByte = rawBytes[rawBytes.length - 1];
|
||||||
|
if (isAlphanumeric(firstByte) && isAlphanumeric(lastByte)) {
|
||||||
|
return String.fromCharCode(firstByte) + String.fromCharCode(lastByte);
|
||||||
|
}
|
||||||
|
const firstHex = firstByte.toString(16).padStart(2, "0");
|
||||||
|
const lastHex = lastByte.toString(16).padStart(2, "0");
|
||||||
|
return firstHex[0] + lastHex[1];
|
||||||
|
}
|
||||||
|
|||||||
@ -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 Utils, { isWorkerEnvironment } from "../Utils.mjs";
|
||||||
import Recipe from "../Recipe.mjs";
|
import Recipe from "../Recipe.mjs";
|
||||||
import Dish from "../Dish.mjs";
|
import Dish from "../Dish.mjs";
|
||||||
|
|||||||
167
src/core/lib/Modhex.mjs
Normal file
167
src/core/lib/Modhex.mjs
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
/**
|
||||||
|
* @author linuxgemini [ilteris@asenkron.com.tr]
|
||||||
|
* @copyright Crown Copyright 2024
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Utils from "../Utils.mjs";
|
||||||
|
import OperationError from "../errors/OperationError.mjs";
|
||||||
|
import { fromHex, toHex } from "./Hex.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modhex alphabet.
|
||||||
|
*/
|
||||||
|
const MODHEX_ALPHABET = "cbdefghijklnrtuv";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modhex alphabet map.
|
||||||
|
*/
|
||||||
|
const MODHEX_ALPHABET_MAP = MODHEX_ALPHABET.split("");
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hex alphabet to substitute Modhex.
|
||||||
|
*/
|
||||||
|
const HEX_ALPHABET = "0123456789abcdef";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hex alphabet map to substitute Modhex.
|
||||||
|
*/
|
||||||
|
const HEX_ALPHABET_MAP = HEX_ALPHABET.split("");
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a byte array into a modhex string.
|
||||||
|
*
|
||||||
|
* @param {byteArray|Uint8Array|ArrayBuffer} data
|
||||||
|
* @param {string} [delim=" "]
|
||||||
|
* @param {number} [padding=2]
|
||||||
|
* @returns {string}
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // returns "cl bf bu"
|
||||||
|
* toModhex([10,20,30]);
|
||||||
|
*
|
||||||
|
* // returns "cl:bf:bu"
|
||||||
|
* toModhex([10,20,30], ":");
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
|
||||||
|
let modhexString = "";
|
||||||
|
for (const letter of regularHexString.split("")) {
|
||||||
|
modhexString += MODHEX_ALPHABET_MAP[HEX_ALPHABET_MAP.indexOf(letter)];
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = "";
|
||||||
|
const groupingRegexp = new RegExp(`.{1,${padding}}`, "g");
|
||||||
|
const groupedModhex = modhexString.match(groupingRegexp);
|
||||||
|
|
||||||
|
for (let i = 0; i < groupedModhex.length; i++) {
|
||||||
|
const group = groupedModhex[i];
|
||||||
|
output += group + delim;
|
||||||
|
|
||||||
|
if (extraDelim) {
|
||||||
|
output += extraDelim;
|
||||||
|
}
|
||||||
|
// Add LF after each lineSize amount of bytes but not at the end
|
||||||
|
if ((i !== groupedModhex.length - 1) && ((i + 1) % lineSize === 0)) {
|
||||||
|
output += "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the extraDelim at the end (if there is one)
|
||||||
|
// and remove the delim at the end, but if it's prepended there's nothing to remove
|
||||||
|
const rTruncLen = extraDelim.length + delim.length;
|
||||||
|
if (rTruncLen) {
|
||||||
|
// If rTruncLen === 0 then output.slice(0,0) will be returned, which is nothing
|
||||||
|
return output.slice(0, -rTruncLen);
|
||||||
|
} else {
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a byte array into a modhex string as efficiently as possible with no options.
|
||||||
|
*
|
||||||
|
* @param {byteArray|Uint8Array|ArrayBuffer} data
|
||||||
|
* @returns {string}
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // returns "clbfbu"
|
||||||
|
* toModhexFast([10,20,30]);
|
||||||
|
*/
|
||||||
|
export function toModhexFast(data) {
|
||||||
|
if (!data) return "";
|
||||||
|
if (data instanceof ArrayBuffer) data = new Uint8Array(data);
|
||||||
|
if (data.length === 0) return "";
|
||||||
|
|
||||||
|
const output = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
output.push(MODHEX_ALPHABET_MAP[(data[i] >> 4) & 0xf]);
|
||||||
|
output.push(MODHEX_ALPHABET_MAP[data[i] & 0xf]);
|
||||||
|
}
|
||||||
|
return output.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a modhex string into a byte array.
|
||||||
|
*
|
||||||
|
* @param {string} data
|
||||||
|
* @param {string} [delim]
|
||||||
|
* @param {number} [byteLen=2]
|
||||||
|
* @returns {byteArray}
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // returns [10,20,30]
|
||||||
|
* fromModhex("cl bf bu");
|
||||||
|
*
|
||||||
|
* // returns [10,20,30]
|
||||||
|
* fromModhex("cl:bf:bu", "Colon");
|
||||||
|
*/
|
||||||
|
export function fromModhex(data, delim="Auto", byteLen=2) {
|
||||||
|
if (byteLen < 1 || Math.round(byteLen) !== byteLen)
|
||||||
|
throw new OperationError("Byte length must be a positive integer");
|
||||||
|
|
||||||
|
// The `.replace(/\s/g, "")` an interesting workaround: Hex "multiline" tests aren't actually
|
||||||
|
// multiline. Tests for Modhex fixes that, thus exposing the issue.
|
||||||
|
data = data.toLowerCase().replace(/\s/g, "");
|
||||||
|
|
||||||
|
if (delim !== "None") {
|
||||||
|
const delimRegex = delim === "Auto" ? /[^cbdefghijklnrtuv]/gi : Utils.regexRep(delim);
|
||||||
|
data = data.split(delimRegex);
|
||||||
|
} else {
|
||||||
|
data = [data];
|
||||||
|
}
|
||||||
|
|
||||||
|
let regularHexString = "";
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
for (const letter of data[i].split("")) {
|
||||||
|
regularHexString += HEX_ALPHABET_MAP[MODHEX_ALPHABET_MAP.indexOf(letter)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const output = fromHex(regularHexString, "None", byteLen);
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To Modhex delimiters.
|
||||||
|
*/
|
||||||
|
export const TO_MODHEX_DELIM_OPTIONS = ["Space", "Percent", "Comma", "Semi-colon", "Colon", "Line feed", "CRLF", "None"];
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* From Modhex delimiters.
|
||||||
|
*/
|
||||||
|
export const FROM_MODHEX_DELIM_OPTIONS = ["Auto"].concat(TO_MODHEX_DELIM_OPTIONS);
|
||||||
50
src/core/lib/ParityBit.mjs
Normal file
50
src/core/lib/ParityBit.mjs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -10,7 +10,7 @@ import OperationError from "../errors/OperationError.mjs";
|
|||||||
import jsQR from "jsqr";
|
import jsQR from "jsqr";
|
||||||
import qr from "qr-image";
|
import qr from "qr-image";
|
||||||
import Utils from "../Utils.mjs";
|
import Utils from "../Utils.mjs";
|
||||||
import Jimp from "jimp/es/index.js";
|
import { Jimp, JimpMime } from "jimp";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses a QR code image from an image
|
* Parses a QR code image from an image
|
||||||
@ -29,18 +29,31 @@ export async function parseQrCode(input, normalise) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (normalise) {
|
if (normalise) {
|
||||||
image.rgba(false);
|
|
||||||
image.background(0xFFFFFFFF);
|
|
||||||
image.normalize();
|
|
||||||
image.greyscale();
|
image.greyscale();
|
||||||
image = await image.getBufferAsync(Jimp.MIME_JPEG);
|
image.normalize();
|
||||||
image = await Jimp.read(image);
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new OperationError(`Error normalising image. (${err})`);
|
throw new OperationError(`Error normalising image. (${err})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const qrData = jsQR(image.bitmap.data, image.getWidth(), image.getHeight());
|
// Remove transparency which jsQR cannot handle
|
||||||
|
image.scan((x, y, idx) => {
|
||||||
|
// If pixel is fully transparent, make it opaque white
|
||||||
|
if (image.bitmap.data[idx + 3] === 0x00) {
|
||||||
|
image.bitmap.data[idx + 0] = 0xff;
|
||||||
|
image.bitmap.data[idx + 1] = 0xff;
|
||||||
|
image.bitmap.data[idx + 2] = 0xff;
|
||||||
|
}
|
||||||
|
// Otherwise, make it fully opaque at its existing colour
|
||||||
|
image.bitmap.data[idx + 3] = 0xff;
|
||||||
|
});
|
||||||
|
image = await Jimp.read(await image.getBuffer(JimpMime.jpeg));
|
||||||
|
|
||||||
|
const qrData = jsQR(
|
||||||
|
new Uint8ClampedArray(image.bitmap.data),
|
||||||
|
image.width,
|
||||||
|
image.height,
|
||||||
|
);
|
||||||
if (qrData) {
|
if (qrData) {
|
||||||
return qrData.data;
|
return qrData.data;
|
||||||
} else {
|
} else {
|
||||||
@ -58,7 +71,13 @@ export async function parseQrCode(input, normalise) {
|
|||||||
* @param {string} errorCorrection
|
* @param {string} errorCorrection
|
||||||
* @returns {ArrayBuffer}
|
* @returns {ArrayBuffer}
|
||||||
*/
|
*/
|
||||||
export function generateQrCode(input, format, moduleSize, margin, errorCorrection) {
|
export function generateQrCode(
|
||||||
|
input,
|
||||||
|
format,
|
||||||
|
moduleSize,
|
||||||
|
margin,
|
||||||
|
errorCorrection,
|
||||||
|
) {
|
||||||
const formats = ["SVG", "EPS", "PDF", "PNG"];
|
const formats = ["SVG", "EPS", "PDF", "PNG"];
|
||||||
if (!formats.includes(format.toUpperCase())) {
|
if (!formats.includes(format.toUpperCase())) {
|
||||||
throw new OperationError("Unsupported QR code format.");
|
throw new OperationError("Unsupported QR code format.");
|
||||||
@ -70,7 +89,8 @@ export function generateQrCode(input, format, moduleSize, margin, errorCorrectio
|
|||||||
type: format,
|
type: format,
|
||||||
size: moduleSize,
|
size: moduleSize,
|
||||||
margin: margin,
|
margin: margin,
|
||||||
"ec_level": errorCorrection.charAt(0).toUpperCase()
|
// eslint-disable-next-line camelcase
|
||||||
|
ec_level: errorCorrection.charAt(0).toUpperCase(),
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new OperationError(`Error generating QR code. (${err})`);
|
throw new OperationError(`Error generating QR code. (${err})`);
|
||||||
@ -86,7 +106,7 @@ export function generateQrCode(input, format, moduleSize, margin, errorCorrectio
|
|||||||
case "PDF":
|
case "PDF":
|
||||||
return Utils.strToArrayBuffer(qrImage);
|
return Utils.strToArrayBuffer(qrImage);
|
||||||
case "PNG":
|
case "PNG":
|
||||||
return qrImage.buffer;
|
return qrImage.buffer.slice(qrImage.byteOffset, qrImage.byteLength + qrImage.byteOffset);
|
||||||
default:
|
default:
|
||||||
throw new OperationError("Unsupported QR code format.");
|
throw new OperationError("Unsupported QR code format.");
|
||||||
}
|
}
|
||||||
|
|||||||
625
src/core/lib/RC6.mjs
Normal file
625
src/core/lib/RC6.mjs
Normal 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);
|
||||||
|
}
|
||||||
258
src/core/lib/SM2.mjs
Normal file
258
src/core/lib/SM2.mjs
Normal file
@ -0,0 +1,258 @@
|
|||||||
|
/**
|
||||||
|
* Utilities and operations utilized for SM2 encryption and decryption
|
||||||
|
* @author flakjacket95 [dflack95@gmail.com]
|
||||||
|
* @copyright Crown Copyright 2024
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import OperationError from "../errors/OperationError.mjs";
|
||||||
|
import { fromHex } from "../lib/Hex.mjs";
|
||||||
|
import Utils from "../Utils.mjs";
|
||||||
|
import Sm3 from "crypto-api/src/hasher/sm3.mjs";
|
||||||
|
import {toHex} from "crypto-api/src/encoder/hex.mjs";
|
||||||
|
import r from "jsrsasign";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SM2 Class for encryption and decryption operations
|
||||||
|
*/
|
||||||
|
export class SM2 {
|
||||||
|
/**
|
||||||
|
* Constructor for SM2 class; sets up with the curve and the output format as specified in user args
|
||||||
|
*
|
||||||
|
* @param {*} curve
|
||||||
|
* @param {*} format
|
||||||
|
*/
|
||||||
|
constructor(curve, format) {
|
||||||
|
this.ecParams = null;
|
||||||
|
this.rng = new r.SecureRandom();
|
||||||
|
/*
|
||||||
|
For any additional curve definitions utilized by SM2, add another block like the below for that curve, then add the curve name to the Curve selection dropdown
|
||||||
|
*/
|
||||||
|
r.crypto.ECParameterDB.regist(
|
||||||
|
"sm2p256v1", // name / p = 2**256 - 2**224 - 2**96 + 2**64 - 1
|
||||||
|
256,
|
||||||
|
"FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF", // p
|
||||||
|
"FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFC", // a
|
||||||
|
"28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93", // b
|
||||||
|
"FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123", // n
|
||||||
|
"1", // h
|
||||||
|
"32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7", // gx
|
||||||
|
"BC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0", // gy
|
||||||
|
[]
|
||||||
|
); // alias
|
||||||
|
this.ecParams = r.crypto.ECParameterDB.getByName(curve);
|
||||||
|
|
||||||
|
this.format = format;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the public key coordinates for the SM2 class
|
||||||
|
*
|
||||||
|
* @param {string} publicKeyX
|
||||||
|
* @param {string} publicKeyY
|
||||||
|
*/
|
||||||
|
setPublicKey(publicKeyX, publicKeyY) {
|
||||||
|
/*
|
||||||
|
* TODO: This needs some additional length validation; and checking for errors in the decoding process
|
||||||
|
* TODO: Can probably support other public key encoding methods here as well in the future
|
||||||
|
*/
|
||||||
|
this.publicKey = this.ecParams.curve.decodePointHex("04" + publicKeyX + publicKeyY);
|
||||||
|
|
||||||
|
if (this.publicKey.isInfinity()) {
|
||||||
|
throw new OperationError("Invalid Public Key");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the private key value for the SM2 class
|
||||||
|
*
|
||||||
|
* @param {string} privateKey
|
||||||
|
*/
|
||||||
|
setPrivateKey(privateKeyHex) {
|
||||||
|
this.privateKey = new r.BigInteger(privateKeyHex, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main encryption function; takes user input, processes encryption and returns the result in hex (with the components arranged as configured by the user args)
|
||||||
|
*
|
||||||
|
* @param {*} input
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
encrypt(input) {
|
||||||
|
const G = this.ecParams.G;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Compute a new, random public key along the same elliptic curve to form the starting point for our encryption process (record the resulting X and Y as hex to provide as part of the operation output)
|
||||||
|
* k: Randomly generated BigInteger
|
||||||
|
* c1: Result of dotting our curve generator point `G` with the value of `k`
|
||||||
|
*/
|
||||||
|
const k = this.generatePublicKey();
|
||||||
|
const c1 = G.multiply(k);
|
||||||
|
const [hexC1X, hexC1Y] = this.getPointAsHex(c1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Compute p2 (secret) using the public key, and the chosen k value above
|
||||||
|
*/
|
||||||
|
const p2 = this.publicKey.multiply(k);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Compute the C3 SM3 hash before we transform the array
|
||||||
|
*/
|
||||||
|
const c3 = this.c3(p2, input);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Genreate a proper length encryption key, XOR iteratively, and convert newly encrypted data to hex
|
||||||
|
*/
|
||||||
|
const key = this.kdf(p2, input.byteLength);
|
||||||
|
for (let i = 0; i < input.byteLength; i++) {
|
||||||
|
input[i] ^= Utils.ord(key[i]);
|
||||||
|
}
|
||||||
|
const c2 = Buffer.from(input).toString("hex");
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Check user input specs; order the output components as selected
|
||||||
|
*/
|
||||||
|
if (this.format === "C1C3C2") {
|
||||||
|
return hexC1X + hexC1Y + c3 + c2;
|
||||||
|
} else {
|
||||||
|
return hexC1X + hexC1Y + c2 + c3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Function to decrypt an SM2 encrypted message
|
||||||
|
*
|
||||||
|
* @param {*} input
|
||||||
|
*/
|
||||||
|
decrypt(input) {
|
||||||
|
const c1X = input.slice(0, 64);
|
||||||
|
const c1Y = input.slice(64, 128);
|
||||||
|
|
||||||
|
let c3 = "";
|
||||||
|
let c2 = "";
|
||||||
|
|
||||||
|
if (this.format === "C1C3C2") {
|
||||||
|
c3 = input.slice(128, 192);
|
||||||
|
c2 = input.slice(192);
|
||||||
|
} else {
|
||||||
|
c2 = input.slice(128, -64);
|
||||||
|
c3 = input.slice(-64);
|
||||||
|
}
|
||||||
|
c2 = Uint8Array.from(fromHex(c2));
|
||||||
|
const c1 = this.ecParams.curve.decodePointHex("04" + c1X + c1Y);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Compute the p2 (secret) value by taking the C1 point provided in the encrypted package, and multiplying by the private k value
|
||||||
|
*/
|
||||||
|
const p2 = c1.multiply(this.privateKey);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Similar to encryption; compute sufficient length key material and XOR the input data to recover the original message
|
||||||
|
*/
|
||||||
|
const key = this.kdf(p2, c2.byteLength);
|
||||||
|
|
||||||
|
for (let i = 0; i < c2.byteLength; i++) {
|
||||||
|
c2[i] ^= Utils.ord(key[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const check = this.c3(p2, c2);
|
||||||
|
if (check === c3) {
|
||||||
|
return c2.buffer;
|
||||||
|
} else {
|
||||||
|
throw new OperationError("Decryption Error -- Computed Hashes Do Not Match");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a large random number
|
||||||
|
*
|
||||||
|
* @param {*} limit
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
getBigRandom(limit) {
|
||||||
|
return new r.BigInteger(limit.bitLength(), this.rng)
|
||||||
|
.mod(limit.subtract(r.BigInteger.ONE))
|
||||||
|
.add(r.BigInteger.ONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function for generating a large random K number; utilized for generating our initial C1 point
|
||||||
|
* TODO: Do we need to do any sort of validation on the resulting k values?
|
||||||
|
*
|
||||||
|
* @returns {BigInteger}
|
||||||
|
*/
|
||||||
|
generatePublicKey() {
|
||||||
|
const n = this.ecParams.n;
|
||||||
|
const k = this.getBigRandom(n);
|
||||||
|
return k;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SM2 Key Derivation Function (KDF); Takes P2 point, and generates a key material stream large enough to encrypt all of the input data
|
||||||
|
*
|
||||||
|
* @param {*} p2
|
||||||
|
* @param {*} len
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
kdf(p2, len) {
|
||||||
|
const [hX, hY] = this.getPointAsHex(p2);
|
||||||
|
|
||||||
|
const total = Math.ceil(len / 32) + 1;
|
||||||
|
let cnt = 1;
|
||||||
|
|
||||||
|
let keyMaterial = "";
|
||||||
|
|
||||||
|
while (cnt < total) {
|
||||||
|
const num = Utils.intToByteArray(cnt, 4, "big");
|
||||||
|
const overall = fromHex(hX).concat(fromHex(hY)).concat(num);
|
||||||
|
keyMaterial += this.sm3(overall);
|
||||||
|
cnt++;
|
||||||
|
}
|
||||||
|
return keyMaterial;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the C3 component of our final encrypted payload; which is the SM3 hash of the P2 point and the original, unencrypted input data
|
||||||
|
*
|
||||||
|
* @param {*} p2
|
||||||
|
* @param {*} input
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
c3(p2, input) {
|
||||||
|
const [hX, hY] = this.getPointAsHex(p2);
|
||||||
|
|
||||||
|
const overall = fromHex(hX).concat(Array.from(input)).concat(fromHex(hY));
|
||||||
|
|
||||||
|
return toHex(this.sm3(overall));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SM3 setup helper function; takes input data as an array, processes the hash and returns the result
|
||||||
|
*
|
||||||
|
* @param {*} data
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
sm3(data) {
|
||||||
|
const hashData = Utils.arrayBufferToStr(Uint8Array.from(data).buffer, false);
|
||||||
|
const hasher = new Sm3();
|
||||||
|
hasher.update(hashData);
|
||||||
|
return hasher.finalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function, returns an elliptic curve points X and Y values as hex;
|
||||||
|
*
|
||||||
|
* @param {EcPointFp} point
|
||||||
|
* @returns {[]}
|
||||||
|
*/
|
||||||
|
getPointAsHex(point) {
|
||||||
|
const biX = point.getX().toBigInteger();
|
||||||
|
const biY = point.getY().toBigInteger();
|
||||||
|
|
||||||
|
const charlen = this.ecParams.keycharlen;
|
||||||
|
const hX = ("0000000000" + biX.toString(16)).slice(- charlen);
|
||||||
|
const hY = ("0000000000" + biY.toString(16)).slice(- charlen);
|
||||||
|
return [hX, hY];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -863,15 +863,15 @@ export function parseHighestSupportedVersion(bytes) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses the application_layer_protocol_negotiation extension and returns the first value.
|
* Parses the application_layer_protocol_negotiation extension and returns the first value as raw bytes.
|
||||||
* @param {Uint8Array} bytes
|
* @param {Uint8Array} bytes
|
||||||
* @returns {number}
|
* @returns {Uint8Array|null}
|
||||||
*/
|
*/
|
||||||
export function parseFirstALPNValue(bytes) {
|
export function parseFirstALPNValue(bytes) {
|
||||||
const s = new Stream(bytes);
|
const s = new Stream(bytes);
|
||||||
const alpnExtLen = s.readInt(2);
|
const alpnExtLen = s.readInt(2);
|
||||||
if (alpnExtLen < 3) return "00";
|
if (alpnExtLen < 2) return null;
|
||||||
const strLen = s.readInt(1);
|
const strLen = s.readInt(1);
|
||||||
if (strLen < 2) return "00";
|
if (strLen < 1) return null;
|
||||||
return s.readString(strLen);
|
return s.getBytes(strLen);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -76,7 +76,7 @@ class A1Z26CipherDecode extends Operation {
|
|||||||
const delim = Utils.charRep(args[0] || "Space");
|
const delim = Utils.charRep(args[0] || "Space");
|
||||||
|
|
||||||
if (input.length === 0) {
|
if (input.length === 0) {
|
||||||
return [];
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
const bites = input.split(delim);
|
const bites = input.split(delim);
|
||||||
|
|||||||
@ -112,7 +112,7 @@ class AESDecrypt extends Operation {
|
|||||||
run(input, args) {
|
run(input, args) {
|
||||||
const key = Utils.convertToByteString(args[0].string, args[0].option),
|
const key = Utils.convertToByteString(args[0].string, args[0].option),
|
||||||
iv = Utils.convertToByteString(args[1].string, args[1].option),
|
iv = Utils.convertToByteString(args[1].string, args[1].option),
|
||||||
mode = args[2].substring(0, 3),
|
mode = args[2].split("/")[0],
|
||||||
noPadding = args[2].endsWith("NoPadding"),
|
noPadding = args[2].endsWith("NoPadding"),
|
||||||
inputType = args[3],
|
inputType = args[3],
|
||||||
outputType = args[4],
|
outputType = args[4],
|
||||||
|
|||||||
@ -66,6 +66,14 @@ class AESEncrypt extends Operation {
|
|||||||
{
|
{
|
||||||
name: "ECB",
|
name: "ECB",
|
||||||
off: [5]
|
off: [5]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "CBC/NoPadding",
|
||||||
|
off: [5]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ECB/NoPadding",
|
||||||
|
off: [5]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -98,7 +106,8 @@ class AESEncrypt extends Operation {
|
|||||||
run(input, args) {
|
run(input, args) {
|
||||||
const key = Utils.convertToByteString(args[0].string, args[0].option),
|
const key = Utils.convertToByteString(args[0].string, args[0].option),
|
||||||
iv = Utils.convertToByteString(args[1].string, args[1].option),
|
iv = Utils.convertToByteString(args[1].string, args[1].option),
|
||||||
mode = args[2],
|
mode = args[2].split("/")[0],
|
||||||
|
noPadding = args[2].endsWith("NoPadding"),
|
||||||
inputType = args[3],
|
inputType = args[3],
|
||||||
outputType = args[4],
|
outputType = args[4],
|
||||||
aad = Utils.convertToByteString(args[5].string, args[5].option);
|
aad = Utils.convertToByteString(args[5].string, args[5].option);
|
||||||
@ -114,11 +123,20 @@ The following algorithms will be used based on the size of the key:
|
|||||||
|
|
||||||
input = Utils.convertToByteString(input, inputType);
|
input = Utils.convertToByteString(input, inputType);
|
||||||
|
|
||||||
|
// Handle NoPadding modes
|
||||||
|
if (noPadding && input.length % 16 !== 0) {
|
||||||
|
throw new OperationError("Input length must be a multiple of 16 bytes for NoPadding modes.");
|
||||||
|
}
|
||||||
const cipher = forge.cipher.createCipher("AES-" + mode, key);
|
const cipher = forge.cipher.createCipher("AES-" + mode, key);
|
||||||
cipher.start({
|
cipher.start({
|
||||||
iv: iv,
|
iv: iv,
|
||||||
additionalData: mode === "GCM" ? aad : undefined
|
additionalData: mode === "GCM" ? aad : undefined
|
||||||
});
|
});
|
||||||
|
if (noPadding) {
|
||||||
|
cipher.mode.pad = function(output, options) {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
cipher.update(forge.util.createBuffer(input));
|
cipher.update(forge.util.createBuffer(input));
|
||||||
cipher.finish();
|
cipher.finish();
|
||||||
|
|
||||||
|
|||||||
@ -9,13 +9,19 @@ import OperationError from "../errors/OperationError.mjs";
|
|||||||
import { isImage } from "../lib/FileType.mjs";
|
import { isImage } from "../lib/FileType.mjs";
|
||||||
import { toBase64 } from "../lib/Base64.mjs";
|
import { toBase64 } from "../lib/Base64.mjs";
|
||||||
import { isWorkerEnvironment } from "../Utils.mjs";
|
import { isWorkerEnvironment } from "../Utils.mjs";
|
||||||
import Jimp from "jimp/es/index.js";
|
import {
|
||||||
|
Jimp,
|
||||||
|
JimpMime,
|
||||||
|
ResizeStrategy,
|
||||||
|
measureText,
|
||||||
|
measureTextHeight,
|
||||||
|
loadFont,
|
||||||
|
} from "jimp";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add Text To Image operation
|
* Add Text To Image operation
|
||||||
*/
|
*/
|
||||||
class AddTextToImage extends Operation {
|
class AddTextToImage extends Operation {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AddTextToImage constructor
|
* AddTextToImage constructor
|
||||||
*/
|
*/
|
||||||
@ -24,7 +30,8 @@ class AddTextToImage extends Operation {
|
|||||||
|
|
||||||
this.name = "Add Text To Image";
|
this.name = "Add Text To Image";
|
||||||
this.module = "Image";
|
this.module = "Image";
|
||||||
this.description = "Adds text onto an image.<br><br>Text can be horizontally or vertically aligned, or the position can be manually specified.<br>Variants of the Roboto font face are available in any size or colour.";
|
this.description =
|
||||||
|
"Adds text onto an image.<br><br>Text can be horizontally or vertically aligned, or the position can be manually specified.<br>Variants of the Roboto font face are available in any size or colour.";
|
||||||
this.infoURL = "";
|
this.infoURL = "";
|
||||||
this.inputType = "ArrayBuffer";
|
this.inputType = "ArrayBuffer";
|
||||||
this.outputType = "ArrayBuffer";
|
this.outputType = "ArrayBuffer";
|
||||||
@ -33,72 +40,67 @@ class AddTextToImage extends Operation {
|
|||||||
{
|
{
|
||||||
name: "Text",
|
name: "Text",
|
||||||
type: "string",
|
type: "string",
|
||||||
value: ""
|
value: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Horizontal align",
|
name: "Horizontal align",
|
||||||
type: "option",
|
type: "option",
|
||||||
value: ["None", "Left", "Center", "Right"]
|
value: ["None", "Left", "Center", "Right"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Vertical align",
|
name: "Vertical align",
|
||||||
type: "option",
|
type: "option",
|
||||||
value: ["None", "Top", "Middle", "Bottom"]
|
value: ["None", "Top", "Middle", "Bottom"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "X position",
|
name: "X position",
|
||||||
type: "number",
|
type: "number",
|
||||||
value: 0
|
value: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Y position",
|
name: "Y position",
|
||||||
type: "number",
|
type: "number",
|
||||||
value: 0
|
value: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Size",
|
name: "Size",
|
||||||
type: "number",
|
type: "number",
|
||||||
value: 32,
|
value: 32,
|
||||||
min: 8
|
min: 8,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Font face",
|
name: "Font face",
|
||||||
type: "option",
|
type: "option",
|
||||||
value: [
|
value: ["Roboto", "Roboto Black", "Roboto Mono", "Roboto Slab"],
|
||||||
"Roboto",
|
|
||||||
"Roboto Black",
|
|
||||||
"Roboto Mono",
|
|
||||||
"Roboto Slab"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Red",
|
name: "Red",
|
||||||
type: "number",
|
type: "number",
|
||||||
value: 255,
|
value: 255,
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 255
|
max: 255,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Green",
|
name: "Green",
|
||||||
type: "number",
|
type: "number",
|
||||||
value: 255,
|
value: 255,
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 255
|
max: 255,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Blue",
|
name: "Blue",
|
||||||
type: "number",
|
type: "number",
|
||||||
value: 255,
|
value: 255,
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 255
|
max: 255,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Alpha",
|
name: "Alpha",
|
||||||
type: "number",
|
type: "number",
|
||||||
value: 255,
|
value: 255,
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 255
|
max: 255,
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,41 +133,60 @@ class AddTextToImage extends Operation {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new OperationError(`Error loading image. (${err})`);
|
throw new OperationError(`Error loading image. (${err})`);
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
if (isWorkerEnvironment())
|
if (isWorkerEnvironment())
|
||||||
self.sendStatusMessage("Adding text to image...");
|
self.sendStatusMessage("Adding text to image...");
|
||||||
|
|
||||||
const fontsMap = {};
|
const fontsMap = {};
|
||||||
|
try {
|
||||||
const fonts = [
|
const fonts = [
|
||||||
import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/Roboto72White.fnt"),
|
import(
|
||||||
import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/RobotoBlack72White.fnt"),
|
/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/Roboto72White.fnt"
|
||||||
import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/RobotoMono72White.fnt"),
|
),
|
||||||
import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/RobotoSlab72White.fnt")
|
import(
|
||||||
|
/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/RobotoBlack72White.fnt"
|
||||||
|
),
|
||||||
|
import(
|
||||||
|
/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/RobotoMono72White.fnt"
|
||||||
|
),
|
||||||
|
import(
|
||||||
|
/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/RobotoSlab72White.fnt"
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
await Promise.all(fonts)
|
await Promise.all(fonts).then((fonts) => {
|
||||||
.then(fonts => {
|
|
||||||
fontsMap.Roboto = fonts[0];
|
fontsMap.Roboto = fonts[0];
|
||||||
fontsMap["Roboto Black"] = fonts[1];
|
fontsMap["Roboto Black"] = fonts[1];
|
||||||
fontsMap["Roboto Mono"] = fonts[2];
|
fontsMap["Roboto Mono"] = fonts[2];
|
||||||
fontsMap["Roboto Slab"] = fonts[3];
|
fontsMap["Roboto Slab"] = fonts[3];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// Make Webpack load the png font images
|
// Make Webpack load the png font images
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/Roboto72White.png"),
|
import(
|
||||||
import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/RobotoSlab72White.png"),
|
/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/Roboto72White.png"
|
||||||
import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/RobotoMono72White.png"),
|
),
|
||||||
import(/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/RobotoBlack72White.png")
|
import(
|
||||||
|
/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/RobotoSlab72White.png"
|
||||||
|
),
|
||||||
|
import(
|
||||||
|
/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/RobotoMono72White.png"
|
||||||
|
),
|
||||||
|
import(
|
||||||
|
/* webpackMode: "eager" */ "../../web/static/fonts/bmfonts/RobotoBlack72White.png"
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
|
} catch (err) {
|
||||||
|
throw new OperationError(`Error preparing fonts. (${err})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let jimpFont;
|
||||||
|
try {
|
||||||
const font = fontsMap[fontFace];
|
const font = fontsMap[fontFace];
|
||||||
|
|
||||||
// LoadFont needs an absolute url, so append the font name to self.docURL
|
// LoadFont needs an absolute url, so append the font name to self.docURL
|
||||||
const jimpFont = await Jimp.loadFont(self.docURL + "/" + font.default);
|
jimpFont = await loadFont(self.docURL + "/" + font.default);
|
||||||
|
|
||||||
jimpFont.pages.forEach(function(page) {
|
jimpFont.pages.forEach(function (page) {
|
||||||
if (page.bitmap) {
|
if (page.bitmap) {
|
||||||
// Adjust the RGB values of the image pages to change the font colour.
|
// Adjust the RGB values of the image pages to change the font colour.
|
||||||
const pageWidth = page.bitmap.width;
|
const pageWidth = page.bitmap.width;
|
||||||
@ -175,32 +196,56 @@ class AddTextToImage extends Operation {
|
|||||||
const idx = (iy * pageWidth + ix) << 2;
|
const idx = (iy * pageWidth + ix) << 2;
|
||||||
|
|
||||||
const newRed = page.bitmap.data[idx] - (255 - red);
|
const newRed = page.bitmap.data[idx] - (255 - red);
|
||||||
const newGreen = page.bitmap.data[idx + 1] - (255 - green);
|
const newGreen =
|
||||||
const newBlue = page.bitmap.data[idx + 2] - (255 - blue);
|
page.bitmap.data[idx + 1] - (255 - green);
|
||||||
const newAlpha = page.bitmap.data[idx + 3] - (255 - alpha);
|
const newBlue =
|
||||||
|
page.bitmap.data[idx + 2] - (255 - blue);
|
||||||
|
const newAlpha =
|
||||||
|
page.bitmap.data[idx + 3] - (255 - alpha);
|
||||||
|
|
||||||
// Make sure the bitmap values don't go below 0 as that makes jimp very unhappy
|
// Make sure the bitmap values don't go below 0 as that makes jimp very unhappy
|
||||||
page.bitmap.data[idx] = (newRed > 0) ? newRed : 0;
|
page.bitmap.data[idx] = newRed > 0 ? newRed : 0;
|
||||||
page.bitmap.data[idx + 1] = (newGreen > 0) ? newGreen : 0;
|
page.bitmap.data[idx + 1] =
|
||||||
page.bitmap.data[idx + 2] = (newBlue > 0) ? newBlue : 0;
|
newGreen > 0 ? newGreen : 0;
|
||||||
page.bitmap.data[idx + 3] = (newAlpha > 0) ? newAlpha : 0;
|
page.bitmap.data[idx + 2] =
|
||||||
|
newBlue > 0 ? newBlue : 0;
|
||||||
|
page.bitmap.data[idx + 3] =
|
||||||
|
newAlpha > 0 ? newAlpha : 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} catch (err) {
|
||||||
|
throw new OperationError(`Error loading font. (${err})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
// Create a temporary image to hold the rendered text
|
// Create a temporary image to hold the rendered text
|
||||||
const textImage = new Jimp(Jimp.measureText(jimpFont, text), Jimp.measureTextHeight(jimpFont, text));
|
const textImage = new Jimp({
|
||||||
textImage.print(jimpFont, 0, 0, text);
|
width: measureText(jimpFont, text),
|
||||||
|
height: measureTextHeight(jimpFont, text),
|
||||||
|
});
|
||||||
|
textImage.print({
|
||||||
|
font: jimpFont,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
text,
|
||||||
|
});
|
||||||
|
|
||||||
// Scale the rendered text image to the correct size
|
// Scale the rendered text image to the correct size
|
||||||
const scaleFactor = size / 72;
|
const scaleFactor = size / 72;
|
||||||
if (size !== 1) {
|
if (size !== 1) {
|
||||||
// Use bicubic for decreasing size
|
// Use bicubic for decreasing size
|
||||||
if (size > 1) {
|
if (size > 1) {
|
||||||
textImage.scale(scaleFactor, Jimp.RESIZE_BICUBIC);
|
textImage.scale({
|
||||||
|
f: scaleFactor,
|
||||||
|
mode: ResizeStrategy.BICUBIC,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
textImage.scale(scaleFactor, Jimp.RESIZE_BILINEAR);
|
textImage.scale({
|
||||||
|
f: scaleFactor,
|
||||||
|
mode: ResizeStrategy.BILINEAR,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -210,10 +255,10 @@ class AddTextToImage extends Operation {
|
|||||||
xPos = 0;
|
xPos = 0;
|
||||||
break;
|
break;
|
||||||
case "Center":
|
case "Center":
|
||||||
xPos = (image.getWidth() / 2) - (textImage.getWidth() / 2);
|
xPos = image.width / 2 - textImage.width / 2;
|
||||||
break;
|
break;
|
||||||
case "Right":
|
case "Right":
|
||||||
xPos = image.getWidth() - textImage.getWidth();
|
xPos = image.width - textImage.width;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -222,25 +267,33 @@ class AddTextToImage extends Operation {
|
|||||||
yPos = 0;
|
yPos = 0;
|
||||||
break;
|
break;
|
||||||
case "Middle":
|
case "Middle":
|
||||||
yPos = (image.getHeight() / 2) - (textImage.getHeight() / 2);
|
yPos = image.height / 2 - textImage.height / 2;
|
||||||
break;
|
break;
|
||||||
case "Bottom":
|
case "Bottom":
|
||||||
yPos = image.getHeight() - textImage.getHeight();
|
yPos = image.height - textImage.height;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Blit the rendered text image onto the original source image
|
// Blit the rendered text image onto the original source image
|
||||||
image.blit(textImage, xPos, yPos);
|
image.blit({
|
||||||
|
src: textImage,
|
||||||
|
x: xPos,
|
||||||
|
y: yPos,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
throw new OperationError(`Error adding text to image. (${err})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
let imageBuffer;
|
let imageBuffer;
|
||||||
if (image.getMIME() === "image/gif") {
|
if (image.mime === "image/gif") {
|
||||||
imageBuffer = await image.getBufferAsync(Jimp.MIME_PNG);
|
imageBuffer = await image.getBuffer(JimpMime.png);
|
||||||
} else {
|
} else {
|
||||||
imageBuffer = await image.getBufferAsync(Jimp.AUTO);
|
imageBuffer = await image.getBuffer(image.mime);
|
||||||
}
|
}
|
||||||
return imageBuffer.buffer;
|
return imageBuffer.buffer;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new OperationError(`Error adding text to image. (${err})`);
|
throw new OperationError(`Error exporting image. (${err})`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -261,7 +314,6 @@ class AddTextToImage extends Operation {
|
|||||||
|
|
||||||
return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
|
return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AddTextToImage;
|
export default AddTextToImage;
|
||||||
|
|||||||
53
src/core/operations/AlternatingCaps.mjs
Normal file
53
src/core/operations/AlternatingCaps.mjs
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* @author sw5678
|
||||||
|
* @copyright Crown Copyright 2023
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Operation from "../Operation.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alternating caps operation
|
||||||
|
*/
|
||||||
|
class AlternatingCaps extends Operation {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AlternatingCaps constructor
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.name = "Alternating Caps";
|
||||||
|
this.module = "Default";
|
||||||
|
this.description = "Alternating caps, also known as studly caps, sticky caps, or spongecase is a form of text notation in which the capitalization of letters varies by some pattern, or arbitrarily. An example of this would be spelling 'alternative caps' as 'aLtErNaTiNg CaPs'.";
|
||||||
|
this.infoURL = "https://en.wikipedia.org/wiki/Alternating_caps";
|
||||||
|
this.inputType = "string";
|
||||||
|
this.outputType = "string";
|
||||||
|
this.args= [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} input
|
||||||
|
* @param {Object[]} args
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
run(input, args) {
|
||||||
|
let output = "";
|
||||||
|
let previousCaps = true;
|
||||||
|
for (let i = 0; i < input.length; i++) {
|
||||||
|
// Check if the element is a letter
|
||||||
|
if (!RegExp(/^\p{L}/, "u").test(input[i])) {
|
||||||
|
output += input[i];
|
||||||
|
} else if (previousCaps) {
|
||||||
|
output += input[i].toLowerCase();
|
||||||
|
previousCaps = false;
|
||||||
|
} else {
|
||||||
|
output += input[i].toUpperCase();
|
||||||
|
previousCaps = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AlternatingCaps;
|
||||||
151
src/core/operations/AnalyseUUID.mjs
Normal file
151
src/core/operations/AnalyseUUID.mjs
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
/**
|
||||||
|
* @author n1474335 [n1474335@gmail.com]
|
||||||
|
* @author ko80240 [csk.dev@proton.me]
|
||||||
|
* @copyright Crown Copyright 2023
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
class AnalyseUUID extends Operation {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AnalyseUUID constructor
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.name = "Analyse UUID";
|
||||||
|
this.module = "Crypto";
|
||||||
|
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 = [
|
||||||
|
{
|
||||||
|
name: "Include Metadata",
|
||||||
|
type: "boolean",
|
||||||
|
value: true
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} input - Expects a valid UUID string
|
||||||
|
* @param {Object[]} args
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
run(input, args) {
|
||||||
|
input = input.trim();
|
||||||
|
|
||||||
|
let uuidVersion, uuidBytes;
|
||||||
|
try {
|
||||||
|
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");
|
||||||
|
}
|
||||||
64
src/core/operations/BLAKE3.mjs
Normal file
64
src/core/operations/BLAKE3.mjs
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
/**
|
||||||
|
* @author xumptex [xumptex@outlook.fr]
|
||||||
|
* @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 { blake3 } from "@noble/hashes/blake3.js";
|
||||||
|
import { bytesToHex } from "@noble/hashes/utils.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BLAKE3 operation
|
||||||
|
*/
|
||||||
|
class BLAKE3 extends Operation {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BLAKE3 constructor
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.name = "BLAKE3";
|
||||||
|
this.module = "Hashing";
|
||||||
|
this.description = "Hashes the input using BLAKE3 (UTF-8 encoded), with an optional key (also UTF-8), and outputs the result in hexadecimal format.";
|
||||||
|
this.infoURL = "https://en.wikipedia.org/wiki/BLAKE_(hash_function)#BLAKE3";
|
||||||
|
this.inputType = "string";
|
||||||
|
this.outputType = "string";
|
||||||
|
this.args = [
|
||||||
|
{
|
||||||
|
"name": "Size (bytes)",
|
||||||
|
"type": "number"
|
||||||
|
}, {
|
||||||
|
"name": "Key",
|
||||||
|
"type": "string",
|
||||||
|
"value": ""
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} input
|
||||||
|
* @param {Object[]} args
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
run(input, args) {
|
||||||
|
const key = args[1];
|
||||||
|
const size = args[0];
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
opts.key = keyBytes;
|
||||||
|
}
|
||||||
|
return bytesToHex(blake3(inputBytes, opts));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BLAKE3;
|
||||||
@ -76,8 +76,8 @@ class BlowfishDecrypt extends Operation {
|
|||||||
Blowfish's key length needs to be between 4 and 56 bytes (32-448 bits).`);
|
Blowfish's key length needs to be between 4 and 56 bytes (32-448 bits).`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (iv.length !== 8) {
|
if (mode !== "ECB" && iv.length !== 8) {
|
||||||
throw new OperationError(`Invalid IV length: ${iv.length} bytes. Expected 8 bytes`);
|
throw new OperationError(`Invalid IV length: ${iv.length} bytes. Expected 8 bytes.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
input = Utils.convertToByteString(input, inputType);
|
input = Utils.convertToByteString(input, inputType);
|
||||||
|
|||||||
@ -76,8 +76,8 @@ class BlowfishEncrypt extends Operation {
|
|||||||
Blowfish's key length needs to be between 4 and 56 bytes (32-448 bits).`);
|
Blowfish's key length needs to be between 4 and 56 bytes (32-448 bits).`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (iv.length !== 8) {
|
if (mode !== "ECB" && iv.length !== 8) {
|
||||||
throw new OperationError(`Invalid IV length: ${iv.length} bytes. Expected 8 bytes`);
|
throw new OperationError(`Invalid IV length: ${iv.length} bytes. Expected 8 bytes.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
input = Utils.convertToByteString(input, inputType);
|
input = Utils.convertToByteString(input, inputType);
|
||||||
|
|||||||
@ -9,14 +9,12 @@ import OperationError from "../errors/OperationError.mjs";
|
|||||||
import { isWorkerEnvironment } from "../Utils.mjs";
|
import { isWorkerEnvironment } from "../Utils.mjs";
|
||||||
import { isImage } from "../lib/FileType.mjs";
|
import { isImage } from "../lib/FileType.mjs";
|
||||||
import { toBase64 } from "../lib/Base64.mjs";
|
import { toBase64 } from "../lib/Base64.mjs";
|
||||||
import { gaussianBlur } from "../lib/ImageManipulation.mjs";
|
import { Jimp, JimpMime } from "jimp";
|
||||||
import Jimp from "jimp/es/index.js";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Blur Image operation
|
* Blur Image operation
|
||||||
*/
|
*/
|
||||||
class BlurImage extends Operation {
|
class BlurImage extends Operation {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* BlurImage constructor
|
* BlurImage constructor
|
||||||
*/
|
*/
|
||||||
@ -25,7 +23,8 @@ class BlurImage extends Operation {
|
|||||||
|
|
||||||
this.name = "Blur Image";
|
this.name = "Blur Image";
|
||||||
this.module = "Image";
|
this.module = "Image";
|
||||||
this.description = "Applies a blur effect to the image.<br><br>Gaussian blur is much slower than fast blur, but produces better results.";
|
this.description =
|
||||||
|
"Applies a blur effect to the image.<br><br>Gaussian blur is much slower than fast blur, but produces better results.";
|
||||||
this.infoURL = "https://wikipedia.org/wiki/Gaussian_blur";
|
this.infoURL = "https://wikipedia.org/wiki/Gaussian_blur";
|
||||||
this.inputType = "ArrayBuffer";
|
this.inputType = "ArrayBuffer";
|
||||||
this.outputType = "ArrayBuffer";
|
this.outputType = "ArrayBuffer";
|
||||||
@ -35,13 +34,13 @@ class BlurImage extends Operation {
|
|||||||
name: "Amount",
|
name: "Amount",
|
||||||
type: "number",
|
type: "number",
|
||||||
value: 5,
|
value: 5,
|
||||||
min: 1
|
min: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Type",
|
name: "Type",
|
||||||
type: "option",
|
type: "option",
|
||||||
value: ["Fast", "Gaussian"]
|
value: ["Fast", "Gaussian"],
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,15 +72,15 @@ class BlurImage extends Operation {
|
|||||||
case "Gaussian":
|
case "Gaussian":
|
||||||
if (isWorkerEnvironment())
|
if (isWorkerEnvironment())
|
||||||
self.sendStatusMessage("Gaussian blurring image...");
|
self.sendStatusMessage("Gaussian blurring image...");
|
||||||
image = gaussianBlur(image, blurAmount);
|
image.gaussian(blurAmount);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
let imageBuffer;
|
let imageBuffer;
|
||||||
if (image.getMIME() === "image/gif") {
|
if (image.mime === "image/gif") {
|
||||||
imageBuffer = await image.getBufferAsync(Jimp.MIME_PNG);
|
imageBuffer = await image.getBuffer(JimpMime.png);
|
||||||
} else {
|
} else {
|
||||||
imageBuffer = await image.getBufferAsync(Jimp.AUTO);
|
imageBuffer = await image.getBuffer(image.mime);
|
||||||
}
|
}
|
||||||
return imageBuffer.buffer;
|
return imageBuffer.buffer;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -106,7 +105,6 @@ class BlurImage extends Operation {
|
|||||||
|
|
||||||
return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
|
return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default BlurImage;
|
export default BlurImage;
|
||||||
|
|||||||
@ -1,41 +0,0 @@
|
|||||||
/**
|
|
||||||
* @author n1474335 [n1474335@gmail.com]
|
|
||||||
* @copyright Crown Copyright 2016
|
|
||||||
* @license Apache-2.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
import Operation from "../Operation.mjs";
|
|
||||||
import JSCRC from "js-crc";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CRC-16 Checksum operation
|
|
||||||
*/
|
|
||||||
class CRC16Checksum extends Operation {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CRC16Checksum constructor
|
|
||||||
*/
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.name = "CRC-16 Checksum";
|
|
||||||
this.module = "Crypto";
|
|
||||||
this.description = "A cyclic redundancy check (CRC) is an error-detecting code commonly used in digital networks and storage devices to detect accidental changes to raw data.<br><br>The CRC was invented by W. Wesley Peterson in 1961.";
|
|
||||||
this.infoURL = "https://wikipedia.org/wiki/Cyclic_redundancy_check";
|
|
||||||
this.inputType = "ArrayBuffer";
|
|
||||||
this.outputType = "string";
|
|
||||||
this.args = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {ArrayBuffer} input
|
|
||||||
* @param {Object[]} args
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
run(input, args) {
|
|
||||||
return JSCRC.crc16(input);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CRC16Checksum;
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
/**
|
|
||||||
* @author n1474335 [n1474335@gmail.com]
|
|
||||||
* @copyright Crown Copyright 2016
|
|
||||||
* @license Apache-2.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
import Operation from "../Operation.mjs";
|
|
||||||
import JSCRC from "js-crc";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CRC-32 Checksum operation
|
|
||||||
*/
|
|
||||||
class CRC32Checksum extends Operation {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CRC32Checksum constructor
|
|
||||||
*/
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.name = "CRC-32 Checksum";
|
|
||||||
this.module = "Crypto";
|
|
||||||
this.description = "A cyclic redundancy check (CRC) is an error-detecting code commonly used in digital networks and storage devices to detect accidental changes to raw data.<br><br>The CRC was invented by W. Wesley Peterson in 1961; the 32-bit CRC function of Ethernet and many other standards is the work of several researchers and was published in 1975.";
|
|
||||||
this.infoURL = "https://wikipedia.org/wiki/Cyclic_redundancy_check";
|
|
||||||
this.inputType = "ArrayBuffer";
|
|
||||||
this.outputType = "string";
|
|
||||||
this.args = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {ArrayBuffer} input
|
|
||||||
* @param {Object[]} args
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
run(input, args) {
|
|
||||||
return JSCRC.crc32(input);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CRC32Checksum;
|
|
||||||
@ -1,157 +0,0 @@
|
|||||||
/**
|
|
||||||
* @author mshwed [m@ttshwed.com]
|
|
||||||
* @copyright Crown Copyright 2019
|
|
||||||
* @license Apache-2.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
import Operation from "../Operation.mjs";
|
|
||||||
import OperationError from "../errors/OperationError.mjs";
|
|
||||||
|
|
||||||
import { toHexFast } from "../lib/Hex.mjs";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CRC-8 Checksum operation
|
|
||||||
*/
|
|
||||||
class CRC8Checksum extends Operation {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CRC8Checksum constructor
|
|
||||||
*/
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.name = "CRC-8 Checksum";
|
|
||||||
this.module = "Crypto";
|
|
||||||
this.description = "A cyclic redundancy check (CRC) is an error-detecting code commonly used in digital networks and storage devices to detect accidental changes to raw data.<br><br>The CRC was invented by W. Wesley Peterson in 1961.";
|
|
||||||
this.infoURL = "https://wikipedia.org/wiki/Cyclic_redundancy_check";
|
|
||||||
this.inputType = "ArrayBuffer";
|
|
||||||
this.outputType = "string";
|
|
||||||
this.args = [
|
|
||||||
{
|
|
||||||
"name": "Algorithm",
|
|
||||||
"type": "option",
|
|
||||||
"value": [
|
|
||||||
"CRC-8",
|
|
||||||
"CRC-8/CDMA2000",
|
|
||||||
"CRC-8/DARC",
|
|
||||||
"CRC-8/DVB-S2",
|
|
||||||
"CRC-8/EBU",
|
|
||||||
"CRC-8/I-CODE",
|
|
||||||
"CRC-8/ITU",
|
|
||||||
"CRC-8/MAXIM",
|
|
||||||
"CRC-8/ROHC",
|
|
||||||
"CRC-8/WCDMA"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates the pre-computed lookup table for byte division
|
|
||||||
*
|
|
||||||
* @param polynomial
|
|
||||||
*/
|
|
||||||
calculateCRC8LookupTable(polynomial) {
|
|
||||||
const crc8Table = new Uint8Array(256);
|
|
||||||
|
|
||||||
let currentByte;
|
|
||||||
for (let i = 0; i < 256; i++) {
|
|
||||||
currentByte = i;
|
|
||||||
for (let bit = 0; bit < 8; bit++) {
|
|
||||||
if ((currentByte & 0x80) !== 0) {
|
|
||||||
currentByte <<= 1;
|
|
||||||
currentByte ^= polynomial;
|
|
||||||
} else {
|
|
||||||
currentByte <<= 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
crc8Table[i] = currentByte;
|
|
||||||
}
|
|
||||||
|
|
||||||
return crc8Table;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculates the CRC-8 Checksum from an input
|
|
||||||
*
|
|
||||||
* @param {ArrayBuffer} input
|
|
||||||
* @param {number} polynomial
|
|
||||||
* @param {number} initializationValue
|
|
||||||
* @param {boolean} inputReflection
|
|
||||||
* @param {boolean} outputReflection
|
|
||||||
* @param {number} xorOut
|
|
||||||
*/
|
|
||||||
calculateCRC8(input, polynomial, initializationValue, inputReflection, outputReflection, xorOut) {
|
|
||||||
const crcSize = 8;
|
|
||||||
const crcTable = this.calculateCRC8LookupTable(polynomial);
|
|
||||||
|
|
||||||
let crc = initializationValue !== 0 ? initializationValue : 0;
|
|
||||||
let currentByte, position;
|
|
||||||
|
|
||||||
input = new Uint8Array(input);
|
|
||||||
for (const inputByte of input) {
|
|
||||||
currentByte = inputReflection ? this.reverseBits(inputByte, crcSize) : inputByte;
|
|
||||||
|
|
||||||
position = (currentByte ^ crc) & 255;
|
|
||||||
crc = crcTable[position];
|
|
||||||
}
|
|
||||||
|
|
||||||
crc = outputReflection ? this.reverseBits(crc, crcSize) : crc;
|
|
||||||
|
|
||||||
if (xorOut !== 0) crc = crc ^ xorOut;
|
|
||||||
|
|
||||||
return toHexFast(new Uint8Array([crc]));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the bits for a given input byte.
|
|
||||||
*
|
|
||||||
* @param {number} input
|
|
||||||
*/
|
|
||||||
reverseBits(input, hashSize) {
|
|
||||||
let reversedByte = 0;
|
|
||||||
for (let i = hashSize - 1; i >= 0; i--) {
|
|
||||||
reversedByte |= ((input & 1) << i);
|
|
||||||
input >>= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return reversedByte;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {ArrayBuffer} input
|
|
||||||
* @param {Object[]} args
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
run(input, args) {
|
|
||||||
const algorithm = args[0];
|
|
||||||
|
|
||||||
switch (algorithm) {
|
|
||||||
case "CRC-8":
|
|
||||||
return this.calculateCRC8(input, 0x7, 0x0, false, false, 0x0);
|
|
||||||
case "CRC-8/CDMA2000":
|
|
||||||
return this.calculateCRC8(input, 0x9B, 0xFF, false, false, 0x0);
|
|
||||||
case "CRC-8/DARC":
|
|
||||||
return this.calculateCRC8(input, 0x39, 0x0, true, true, 0x0);
|
|
||||||
case "CRC-8/DVB-S2":
|
|
||||||
return this.calculateCRC8(input, 0xD5, 0x0, false, false, 0x0);
|
|
||||||
case "CRC-8/EBU":
|
|
||||||
return this.calculateCRC8(input, 0x1D, 0xFF, true, true, 0x0);
|
|
||||||
case "CRC-8/I-CODE":
|
|
||||||
return this.calculateCRC8(input, 0x1D, 0xFD, false, false, 0x0);
|
|
||||||
case "CRC-8/ITU":
|
|
||||||
return this.calculateCRC8(input, 0x7, 0x0, false, false, 0x55);
|
|
||||||
case "CRC-8/MAXIM":
|
|
||||||
return this.calculateCRC8(input, 0x31, 0x0, true, true, 0x0);
|
|
||||||
case "CRC-8/ROHC":
|
|
||||||
return this.calculateCRC8(input, 0x7, 0xFF, true, true, 0x0);
|
|
||||||
case "CRC-8/WCDMA":
|
|
||||||
return this.calculateCRC8(input, 0x9B, 0x0, true, true, 0x0);
|
|
||||||
default:
|
|
||||||
throw new OperationError("Unknown checksum algorithm");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CRC8Checksum;
|
|
||||||
1110
src/core/operations/CRCChecksum.mjs
Normal file
1110
src/core/operations/CRCChecksum.mjs
Normal file
File diff suppressed because it is too large
Load Diff
@ -9,13 +9,18 @@ import OperationError from "../errors/OperationError.mjs";
|
|||||||
import { isImage } from "../lib/FileType.mjs";
|
import { isImage } from "../lib/FileType.mjs";
|
||||||
import { toBase64 } from "../lib/Base64.mjs";
|
import { toBase64 } from "../lib/Base64.mjs";
|
||||||
import { isWorkerEnvironment } from "../Utils.mjs";
|
import { isWorkerEnvironment } from "../Utils.mjs";
|
||||||
import Jimp from "jimp/es/index.js";
|
import {
|
||||||
|
Jimp,
|
||||||
|
JimpMime,
|
||||||
|
ResizeStrategy,
|
||||||
|
HorizontalAlign,
|
||||||
|
VerticalAlign,
|
||||||
|
} from "jimp";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contain Image operation
|
* Contain Image operation
|
||||||
*/
|
*/
|
||||||
class ContainImage extends Operation {
|
class ContainImage extends Operation {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ContainImage constructor
|
* ContainImage constructor
|
||||||
*/
|
*/
|
||||||
@ -24,7 +29,8 @@ class ContainImage extends Operation {
|
|||||||
|
|
||||||
this.name = "Contain Image";
|
this.name = "Contain Image";
|
||||||
this.module = "Image";
|
this.module = "Image";
|
||||||
this.description = "Scales an image to the specified width and height, maintaining the aspect ratio. The image may be letterboxed.";
|
this.description =
|
||||||
|
"Scales an image to the specified width and height, maintaining the aspect ratio. The image may be letterboxed.";
|
||||||
this.infoURL = "";
|
this.infoURL = "";
|
||||||
this.inputType = "ArrayBuffer";
|
this.inputType = "ArrayBuffer";
|
||||||
this.outputType = "ArrayBuffer";
|
this.outputType = "ArrayBuffer";
|
||||||
@ -34,33 +40,25 @@ class ContainImage extends Operation {
|
|||||||
name: "Width",
|
name: "Width",
|
||||||
type: "number",
|
type: "number",
|
||||||
value: 100,
|
value: 100,
|
||||||
min: 1
|
min: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Height",
|
name: "Height",
|
||||||
type: "number",
|
type: "number",
|
||||||
value: 100,
|
value: 100,
|
||||||
min: 1
|
min: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Horizontal align",
|
name: "Horizontal align",
|
||||||
type: "option",
|
type: "option",
|
||||||
value: [
|
value: ["Left", "Center", "Right"],
|
||||||
"Left",
|
defaultIndex: 1,
|
||||||
"Center",
|
|
||||||
"Right"
|
|
||||||
],
|
|
||||||
defaultIndex: 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Vertical align",
|
name: "Vertical align",
|
||||||
type: "option",
|
type: "option",
|
||||||
value: [
|
value: ["Top", "Middle", "Bottom"],
|
||||||
"Top",
|
defaultIndex: 1,
|
||||||
"Middle",
|
|
||||||
"Bottom"
|
|
||||||
],
|
|
||||||
defaultIndex: 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Resizing algorithm",
|
name: "Resizing algorithm",
|
||||||
@ -70,15 +68,15 @@ class ContainImage extends Operation {
|
|||||||
"Bilinear",
|
"Bilinear",
|
||||||
"Bicubic",
|
"Bicubic",
|
||||||
"Hermite",
|
"Hermite",
|
||||||
"Bezier"
|
"Bezier",
|
||||||
],
|
],
|
||||||
defaultIndex: 1
|
defaultIndex: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Opaque background",
|
name: "Opaque background",
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
value: true
|
value: true,
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,20 +89,20 @@ class ContainImage extends Operation {
|
|||||||
const [width, height, hAlign, vAlign, alg, opaqueBg] = args;
|
const [width, height, hAlign, vAlign, alg, opaqueBg] = args;
|
||||||
|
|
||||||
const resizeMap = {
|
const resizeMap = {
|
||||||
"Nearest Neighbour": Jimp.RESIZE_NEAREST_NEIGHBOR,
|
"Nearest Neighbour": ResizeStrategy.NEAREST_NEIGHBOR,
|
||||||
"Bilinear": Jimp.RESIZE_BILINEAR,
|
Bilinear: ResizeStrategy.BILINEAR,
|
||||||
"Bicubic": Jimp.RESIZE_BICUBIC,
|
Bicubic: ResizeStrategy.BICUBIC,
|
||||||
"Hermite": Jimp.RESIZE_HERMITE,
|
Hermite: ResizeStrategy.HERMITE,
|
||||||
"Bezier": Jimp.RESIZE_BEZIER
|
Bezier: ResizeStrategy.BEZIER,
|
||||||
};
|
};
|
||||||
|
|
||||||
const alignMap = {
|
const alignMap = {
|
||||||
"Left": Jimp.HORIZONTAL_ALIGN_LEFT,
|
Left: HorizontalAlign.LEFT,
|
||||||
"Center": Jimp.HORIZONTAL_ALIGN_CENTER,
|
Center: HorizontalAlign.CENTER,
|
||||||
"Right": Jimp.HORIZONTAL_ALIGN_RIGHT,
|
Right: HorizontalAlign.RIGHT,
|
||||||
"Top": Jimp.VERTICAL_ALIGN_TOP,
|
Top: VerticalAlign.TOP,
|
||||||
"Middle": Jimp.VERTICAL_ALIGN_MIDDLE,
|
Middle: VerticalAlign.MIDDLE,
|
||||||
"Bottom": Jimp.VERTICAL_ALIGN_BOTTOM
|
Bottom: VerticalAlign.BOTTOM,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!isImage(input)) {
|
if (!isImage(input)) {
|
||||||
@ -117,22 +115,35 @@ class ContainImage extends Operation {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new OperationError(`Error loading image. (${err})`);
|
throw new OperationError(`Error loading image. (${err})`);
|
||||||
}
|
}
|
||||||
|
const originalMime = image.mime;
|
||||||
try {
|
try {
|
||||||
if (isWorkerEnvironment())
|
if (isWorkerEnvironment())
|
||||||
self.sendStatusMessage("Containing image...");
|
self.sendStatusMessage("Containing image...");
|
||||||
image.contain(width, height, alignMap[hAlign] | alignMap[vAlign], resizeMap[alg]);
|
image.contain({
|
||||||
|
w: width,
|
||||||
|
h: height,
|
||||||
|
align: alignMap[hAlign] | alignMap[vAlign],
|
||||||
|
mode: resizeMap[alg],
|
||||||
|
});
|
||||||
|
|
||||||
if (opaqueBg) {
|
if (opaqueBg) {
|
||||||
const newImage = await Jimp.read(width, height, 0x000000FF);
|
const newImage = new Jimp({
|
||||||
newImage.blit(image, 0, 0);
|
width,
|
||||||
image = newImage;
|
height,
|
||||||
|
color: 0x000000ff,
|
||||||
|
});
|
||||||
|
image = newImage.blit({
|
||||||
|
src: image,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let imageBuffer;
|
let imageBuffer;
|
||||||
if (image.getMIME() === "image/gif") {
|
if (originalMime === "image/gif") {
|
||||||
imageBuffer = await image.getBufferAsync(Jimp.MIME_PNG);
|
imageBuffer = await image.getBuffer(JimpMime.png);
|
||||||
} else {
|
} else {
|
||||||
imageBuffer = await image.getBufferAsync(Jimp.AUTO);
|
imageBuffer = await image.getBuffer(originalMime);
|
||||||
}
|
}
|
||||||
return imageBuffer.buffer;
|
return imageBuffer.buffer;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -156,7 +167,6 @@ class ContainImage extends Operation {
|
|||||||
|
|
||||||
return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
|
return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ContainImage;
|
export default ContainImage;
|
||||||
|
|||||||
@ -8,13 +8,12 @@ import Operation from "../Operation.mjs";
|
|||||||
import OperationError from "../errors/OperationError.mjs";
|
import OperationError from "../errors/OperationError.mjs";
|
||||||
import { isImage } from "../lib/FileType.mjs";
|
import { isImage } from "../lib/FileType.mjs";
|
||||||
import { toBase64 } from "../lib/Base64.mjs";
|
import { toBase64 } from "../lib/Base64.mjs";
|
||||||
import Jimp from "jimp/es/index.js";
|
import { Jimp, JimpMime, PNGFilterType } from "jimp";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert Image Format operation
|
* Convert Image Format operation
|
||||||
*/
|
*/
|
||||||
class ConvertImageFormat extends Operation {
|
class ConvertImageFormat extends Operation {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ConvertImageFormat constructor
|
* ConvertImageFormat constructor
|
||||||
*/
|
*/
|
||||||
@ -23,7 +22,8 @@ class ConvertImageFormat extends Operation {
|
|||||||
|
|
||||||
this.name = "Convert Image Format";
|
this.name = "Convert Image Format";
|
||||||
this.module = "Image";
|
this.module = "Image";
|
||||||
this.description = "Converts an image between different formats. Supported formats:<br><ul><li>Joint Photographic Experts Group (JPEG)</li><li>Portable Network Graphics (PNG)</li><li>Bitmap (BMP)</li><li>Tagged Image File Format (TIFF)</li></ul><br>Note: GIF files are supported for input, but cannot be outputted.";
|
this.description =
|
||||||
|
"Converts an image between different formats. Supported formats:<br><ul><li>Joint Photographic Experts Group (JPEG)</li><li>Portable Network Graphics (PNG)</li><li>Bitmap (BMP)</li><li>Tagged Image File Format (TIFF)</li></ul><br>Note: GIF files are supported for input, but cannot be outputted.";
|
||||||
this.infoURL = "https://wikipedia.org/wiki/Image_file_formats";
|
this.infoURL = "https://wikipedia.org/wiki/Image_file_formats";
|
||||||
this.inputType = "ArrayBuffer";
|
this.inputType = "ArrayBuffer";
|
||||||
this.outputType = "ArrayBuffer";
|
this.outputType = "ArrayBuffer";
|
||||||
@ -32,39 +32,27 @@ class ConvertImageFormat extends Operation {
|
|||||||
{
|
{
|
||||||
name: "Output Format",
|
name: "Output Format",
|
||||||
type: "option",
|
type: "option",
|
||||||
value: [
|
value: ["JPEG", "PNG", "BMP", "TIFF"],
|
||||||
"JPEG",
|
|
||||||
"PNG",
|
|
||||||
"BMP",
|
|
||||||
"TIFF"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "JPEG Quality",
|
name: "JPEG Quality",
|
||||||
type: "number",
|
type: "number",
|
||||||
value: 80,
|
value: 80,
|
||||||
min: 1,
|
min: 1,
|
||||||
max: 100
|
max: 100,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "PNG Filter Type",
|
name: "PNG Filter Type",
|
||||||
type: "option",
|
type: "option",
|
||||||
value: [
|
value: ["Auto", "None", "Sub", "Up", "Average", "Paeth"],
|
||||||
"Auto",
|
|
||||||
"None",
|
|
||||||
"Sub",
|
|
||||||
"Up",
|
|
||||||
"Average",
|
|
||||||
"Paeth"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "PNG Deflate Level",
|
name: "PNG Deflate Level",
|
||||||
type: "number",
|
type: "number",
|
||||||
value: 9,
|
value: 9,
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 9
|
max: 9,
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,19 +64,19 @@ class ConvertImageFormat extends Operation {
|
|||||||
async run(input, args) {
|
async run(input, args) {
|
||||||
const [format, jpegQuality, pngFilterType, pngDeflateLevel] = args;
|
const [format, jpegQuality, pngFilterType, pngDeflateLevel] = args;
|
||||||
const formatMap = {
|
const formatMap = {
|
||||||
"JPEG": Jimp.MIME_JPEG,
|
JPEG: JimpMime.jpeg,
|
||||||
"PNG": Jimp.MIME_PNG,
|
PNG: JimpMime.png,
|
||||||
"BMP": Jimp.MIME_BMP,
|
BMP: JimpMime.bmp,
|
||||||
"TIFF": Jimp.MIME_TIFF
|
TIFF: JimpMime.tiff,
|
||||||
};
|
};
|
||||||
|
|
||||||
const pngFilterMap = {
|
const pngFilterMap = {
|
||||||
"Auto": Jimp.PNG_FILTER_AUTO,
|
Auto: PNGFilterType.AUTO,
|
||||||
"None": Jimp.PNG_FILTER_NONE,
|
None: PNGFilterType.NONE,
|
||||||
"Sub": Jimp.PNG_FILTER_SUB,
|
Sub: PNGFilterType.SUB,
|
||||||
"Up": Jimp.PNG_FILTER_UP,
|
Up: PNGFilterType.UP,
|
||||||
"Average": Jimp.PNG_FILTER_AVERAGE,
|
Average: PNGFilterType.AVERAGE,
|
||||||
"Paeth": Jimp.PNG_FILTER_PATH
|
Paeth: PNGFilterType.PATH,
|
||||||
};
|
};
|
||||||
|
|
||||||
const mime = formatMap[format];
|
const mime = formatMap[format];
|
||||||
@ -103,18 +91,25 @@ class ConvertImageFormat extends Operation {
|
|||||||
throw new OperationError(`Error opening image file. (${err})`);
|
throw new OperationError(`Error opening image file. (${err})`);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
switch (format) {
|
let buffer;
|
||||||
case "JPEG":
|
switch (mime) {
|
||||||
image.quality(jpegQuality);
|
case JimpMime.jpeg:
|
||||||
|
buffer = await image.getBuffer(mime, {
|
||||||
|
quality: jpegQuality,
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case "PNG":
|
case JimpMime.png:
|
||||||
image.filterType(pngFilterMap[pngFilterType]);
|
buffer = await image.getBuffer(mime, {
|
||||||
image.deflateLevel(pngDeflateLevel);
|
filterType: pngFilterMap[pngFilterType],
|
||||||
|
deflateLevel: pngDeflateLevel,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
buffer = await image.getBuffer(mime);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const imageBuffer = await image.getBufferAsync(mime);
|
return buffer.buffer;
|
||||||
return imageBuffer.buffer;
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new OperationError(`Error converting image format. (${err})`);
|
throw new OperationError(`Error converting image format. (${err})`);
|
||||||
}
|
}
|
||||||
@ -137,7 +132,6 @@ class ConvertImageFormat extends Operation {
|
|||||||
|
|
||||||
return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
|
return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ConvertImageFormat;
|
export default ConvertImageFormat;
|
||||||
|
|||||||
116
src/core/operations/ConvertLeetSpeak.mjs
Normal file
116
src/core/operations/ConvertLeetSpeak.mjs
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
/**
|
||||||
|
* @author bartblaze []
|
||||||
|
* @copyright Crown Copyright 2025
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Operation from "../Operation.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert Leet Speak operation
|
||||||
|
*/
|
||||||
|
class ConvertLeetSpeak extends Operation {
|
||||||
|
/**
|
||||||
|
* ConvertLeetSpeak constructor
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.name = "Convert Leet Speak";
|
||||||
|
this.module = "Default";
|
||||||
|
this.description = "Converts to and from Leet Speak.";
|
||||||
|
this.infoURL = "https://wikipedia.org/wiki/Leet";
|
||||||
|
this.inputType = "string";
|
||||||
|
this.outputType = "string";
|
||||||
|
this.args = [
|
||||||
|
{
|
||||||
|
name: "Direction",
|
||||||
|
type: "option",
|
||||||
|
value: ["To Leet Speak", "From Leet Speak"],
|
||||||
|
defaultIndex: 0
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} input
|
||||||
|
* @param {Object[]} args
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
run(input, args) {
|
||||||
|
const direction = args[0];
|
||||||
|
|
||||||
|
if (direction === "To Leet Speak") {
|
||||||
|
return input.replace(/[a-z]/gi, char => {
|
||||||
|
const leetChar = toLeetMap[char.toLowerCase()] || char;
|
||||||
|
return char === char.toUpperCase() ? leetChar.toUpperCase() : leetChar;
|
||||||
|
});
|
||||||
|
} else if (direction === "From Leet Speak") {
|
||||||
|
return input.replace(/[48cd3f6h1jklmn0pqr57uvwxyz]/gi, char => {
|
||||||
|
const normalChar = fromLeetMap[char] || char;
|
||||||
|
return normalChar;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const toLeetMap = {
|
||||||
|
"a": "4",
|
||||||
|
"b": "b",
|
||||||
|
"c": "c",
|
||||||
|
"d": "d",
|
||||||
|
"e": "3",
|
||||||
|
"f": "f",
|
||||||
|
"g": "g",
|
||||||
|
"h": "h",
|
||||||
|
"i": "1",
|
||||||
|
"j": "j",
|
||||||
|
"k": "k",
|
||||||
|
"l": "l",
|
||||||
|
"m": "m",
|
||||||
|
"n": "n",
|
||||||
|
"o": "0",
|
||||||
|
"p": "p",
|
||||||
|
"q": "q",
|
||||||
|
"r": "r",
|
||||||
|
"s": "5",
|
||||||
|
"t": "7",
|
||||||
|
"u": "u",
|
||||||
|
"v": "v",
|
||||||
|
"w": "w",
|
||||||
|
"x": "x",
|
||||||
|
"y": "y",
|
||||||
|
"z": "z"
|
||||||
|
};
|
||||||
|
|
||||||
|
const fromLeetMap = {
|
||||||
|
"4": "a",
|
||||||
|
"b": "b",
|
||||||
|
"c": "c",
|
||||||
|
"d": "d",
|
||||||
|
"3": "e",
|
||||||
|
"f": "f",
|
||||||
|
"g": "g",
|
||||||
|
"h": "h",
|
||||||
|
"1": "i",
|
||||||
|
"j": "j",
|
||||||
|
"k": "k",
|
||||||
|
"l": "l",
|
||||||
|
"m": "m",
|
||||||
|
"n": "n",
|
||||||
|
"0": "o",
|
||||||
|
"p": "p",
|
||||||
|
"q": "q",
|
||||||
|
"r": "r",
|
||||||
|
"5": "s",
|
||||||
|
"7": "t",
|
||||||
|
"u": "u",
|
||||||
|
"v": "v",
|
||||||
|
"w": "w",
|
||||||
|
"x": "x",
|
||||||
|
"y": "y",
|
||||||
|
"z": "z"
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConvertLeetSpeak;
|
||||||
|
|
||||||
@ -9,13 +9,18 @@ import OperationError from "../errors/OperationError.mjs";
|
|||||||
import { isImage } from "../lib/FileType.mjs";
|
import { isImage } from "../lib/FileType.mjs";
|
||||||
import { toBase64 } from "../lib/Base64.mjs";
|
import { toBase64 } from "../lib/Base64.mjs";
|
||||||
import { isWorkerEnvironment } from "../Utils.mjs";
|
import { isWorkerEnvironment } from "../Utils.mjs";
|
||||||
import jimp from "jimp/es/index.js";
|
import {
|
||||||
|
Jimp,
|
||||||
|
JimpMime,
|
||||||
|
ResizeStrategy,
|
||||||
|
HorizontalAlign,
|
||||||
|
VerticalAlign,
|
||||||
|
} from "jimp";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cover Image operation
|
* Cover Image operation
|
||||||
*/
|
*/
|
||||||
class CoverImage extends Operation {
|
class CoverImage extends Operation {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CoverImage constructor
|
* CoverImage constructor
|
||||||
*/
|
*/
|
||||||
@ -24,7 +29,8 @@ class CoverImage extends Operation {
|
|||||||
|
|
||||||
this.name = "Cover Image";
|
this.name = "Cover Image";
|
||||||
this.module = "Image";
|
this.module = "Image";
|
||||||
this.description = "Scales the image to the given width and height, keeping the aspect ratio. The image may be clipped.";
|
this.description =
|
||||||
|
"Scales the image to the given width and height, keeping the aspect ratio. The image may be clipped.";
|
||||||
this.infoURL = "";
|
this.infoURL = "";
|
||||||
this.inputType = "ArrayBuffer";
|
this.inputType = "ArrayBuffer";
|
||||||
this.outputType = "ArrayBuffer";
|
this.outputType = "ArrayBuffer";
|
||||||
@ -34,33 +40,25 @@ class CoverImage extends Operation {
|
|||||||
name: "Width",
|
name: "Width",
|
||||||
type: "number",
|
type: "number",
|
||||||
value: 100,
|
value: 100,
|
||||||
min: 1
|
min: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Height",
|
name: "Height",
|
||||||
type: "number",
|
type: "number",
|
||||||
value: 100,
|
value: 100,
|
||||||
min: 1
|
min: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Horizontal align",
|
name: "Horizontal align",
|
||||||
type: "option",
|
type: "option",
|
||||||
value: [
|
value: ["Left", "Center", "Right"],
|
||||||
"Left",
|
defaultIndex: 1,
|
||||||
"Center",
|
|
||||||
"Right"
|
|
||||||
],
|
|
||||||
defaultIndex: 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Vertical align",
|
name: "Vertical align",
|
||||||
type: "option",
|
type: "option",
|
||||||
value: [
|
value: ["Top", "Middle", "Bottom"],
|
||||||
"Top",
|
defaultIndex: 1,
|
||||||
"Middle",
|
|
||||||
"Bottom"
|
|
||||||
],
|
|
||||||
defaultIndex: 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Resizing algorithm",
|
name: "Resizing algorithm",
|
||||||
@ -70,10 +68,10 @@ class CoverImage extends Operation {
|
|||||||
"Bilinear",
|
"Bilinear",
|
||||||
"Bicubic",
|
"Bicubic",
|
||||||
"Hermite",
|
"Hermite",
|
||||||
"Bezier"
|
"Bezier",
|
||||||
],
|
],
|
||||||
defaultIndex: 1
|
defaultIndex: 1,
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,20 +84,20 @@ class CoverImage extends Operation {
|
|||||||
const [width, height, hAlign, vAlign, alg] = args;
|
const [width, height, hAlign, vAlign, alg] = args;
|
||||||
|
|
||||||
const resizeMap = {
|
const resizeMap = {
|
||||||
"Nearest Neighbour": jimp.RESIZE_NEAREST_NEIGHBOR,
|
"Nearest Neighbour": ResizeStrategy.NEAREST_NEIGHBOR,
|
||||||
"Bilinear": jimp.RESIZE_BILINEAR,
|
Bilinear: ResizeStrategy.BILINEAR,
|
||||||
"Bicubic": jimp.RESIZE_BICUBIC,
|
Bicubic: ResizeStrategy.BICUBIC,
|
||||||
"Hermite": jimp.RESIZE_HERMITE,
|
Hermite: ResizeStrategy.HERMITE,
|
||||||
"Bezier": jimp.RESIZE_BEZIER
|
Bezier: ResizeStrategy.BEZIER,
|
||||||
};
|
};
|
||||||
|
|
||||||
const alignMap = {
|
const alignMap = {
|
||||||
"Left": jimp.HORIZONTAL_ALIGN_LEFT,
|
Left: HorizontalAlign.LEFT,
|
||||||
"Center": jimp.HORIZONTAL_ALIGN_CENTER,
|
Center: HorizontalAlign.CENTER,
|
||||||
"Right": jimp.HORIZONTAL_ALIGN_RIGHT,
|
Right: HorizontalAlign.RIGHT,
|
||||||
"Top": jimp.VERTICAL_ALIGN_TOP,
|
Top: VerticalAlign.TOP,
|
||||||
"Middle": jimp.VERTICAL_ALIGN_MIDDLE,
|
Middle: VerticalAlign.MIDDLE,
|
||||||
"Bottom": jimp.VERTICAL_ALIGN_BOTTOM
|
Bottom: VerticalAlign.BOTTOM,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!isImage(input)) {
|
if (!isImage(input)) {
|
||||||
@ -108,19 +106,24 @@ class CoverImage extends Operation {
|
|||||||
|
|
||||||
let image;
|
let image;
|
||||||
try {
|
try {
|
||||||
image = await jimp.read(input);
|
image = await Jimp.read(input);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new OperationError(`Error loading image. (${err})`);
|
throw new OperationError(`Error loading image. (${err})`);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (isWorkerEnvironment())
|
if (isWorkerEnvironment())
|
||||||
self.sendStatusMessage("Covering image...");
|
self.sendStatusMessage("Covering image...");
|
||||||
image.cover(width, height, alignMap[hAlign] | alignMap[vAlign], resizeMap[alg]);
|
image.cover({
|
||||||
|
w: width,
|
||||||
|
h: height,
|
||||||
|
align: alignMap[hAlign] | alignMap[vAlign],
|
||||||
|
mode: resizeMap[alg],
|
||||||
|
});
|
||||||
let imageBuffer;
|
let imageBuffer;
|
||||||
if (image.getMIME() === "image/gif") {
|
if (image.mime === "image/gif") {
|
||||||
imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
|
imageBuffer = await image.getBuffer(JimpMime.png);
|
||||||
} else {
|
} else {
|
||||||
imageBuffer = await image.getBufferAsync(jimp.AUTO);
|
imageBuffer = await image.getBuffer(image.mime);
|
||||||
}
|
}
|
||||||
return imageBuffer.buffer;
|
return imageBuffer.buffer;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -144,7 +147,6 @@ class CoverImage extends Operation {
|
|||||||
|
|
||||||
return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
|
return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CoverImage;
|
export default CoverImage;
|
||||||
|
|||||||
@ -9,13 +9,12 @@ import OperationError from "../errors/OperationError.mjs";
|
|||||||
import { isImage } from "../lib/FileType.mjs";
|
import { isImage } from "../lib/FileType.mjs";
|
||||||
import { toBase64 } from "../lib/Base64.mjs";
|
import { toBase64 } from "../lib/Base64.mjs";
|
||||||
import { isWorkerEnvironment } from "../Utils.mjs";
|
import { isWorkerEnvironment } from "../Utils.mjs";
|
||||||
import Jimp from "jimp/es/index.js";
|
import { Jimp, JimpMime } from "jimp";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Crop Image operation
|
* Crop Image operation
|
||||||
*/
|
*/
|
||||||
class CropImage extends Operation {
|
class CropImage extends Operation {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CropImage constructor
|
* CropImage constructor
|
||||||
*/
|
*/
|
||||||
@ -24,7 +23,8 @@ class CropImage extends Operation {
|
|||||||
|
|
||||||
this.name = "Crop Image";
|
this.name = "Crop Image";
|
||||||
this.module = "Image";
|
this.module = "Image";
|
||||||
this.description = "Crops an image to the specified region, or automatically crops edges.<br><br><b><u>Autocrop</u></b><br>Automatically crops same-colour borders from the image.<br><br><u>Autocrop tolerance</u><br>A percentage value for the tolerance of colour difference between pixels.<br><br><u>Only autocrop frames</u><br>Only crop real frames (all sides must have the same border)<br><br><u>Symmetric autocrop</u><br>Force autocrop to be symmetric (top/bottom and left/right are cropped by the same amount)<br><br><u>Autocrop keep border</u><br>The number of pixels of border to leave around the image.";
|
this.description =
|
||||||
|
"Crops an image to the specified region, or automatically crops edges.<br><br><b><u>Autocrop</u></b><br>Automatically crops same-colour borders from the image.<br><br><u>Autocrop tolerance</u><br>A percentage value for the tolerance of colour difference between pixels.<br><br><u>Only autocrop frames</u><br>Only crop real frames (all sides must have the same border)<br><br><u>Symmetric autocrop</u><br>Force autocrop to be symmetric (top/bottom and left/right are cropped by the same amount)<br><br><u>Autocrop keep border</u><br>The number of pixels of border to leave around the image.";
|
||||||
this.infoURL = "https://wikipedia.org/wiki/Cropping_(image)";
|
this.infoURL = "https://wikipedia.org/wiki/Cropping_(image)";
|
||||||
this.inputType = "ArrayBuffer";
|
this.inputType = "ArrayBuffer";
|
||||||
this.outputType = "ArrayBuffer";
|
this.outputType = "ArrayBuffer";
|
||||||
@ -34,30 +34,30 @@ class CropImage extends Operation {
|
|||||||
name: "X Position",
|
name: "X Position",
|
||||||
type: "number",
|
type: "number",
|
||||||
value: 0,
|
value: 0,
|
||||||
min: 0
|
min: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Y Position",
|
name: "Y Position",
|
||||||
type: "number",
|
type: "number",
|
||||||
value: 0,
|
value: 0,
|
||||||
min: 0
|
min: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Width",
|
name: "Width",
|
||||||
type: "number",
|
type: "number",
|
||||||
value: 10,
|
value: 10,
|
||||||
min: 1
|
min: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Height",
|
name: "Height",
|
||||||
type: "number",
|
type: "number",
|
||||||
value: 10,
|
value: 10,
|
||||||
min: 1
|
min: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Autocrop",
|
name: "Autocrop",
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
value: false
|
value: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Autocrop tolerance (%)",
|
name: "Autocrop tolerance (%)",
|
||||||
@ -65,24 +65,24 @@ class CropImage extends Operation {
|
|||||||
value: 0.02,
|
value: 0.02,
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 100,
|
max: 100,
|
||||||
step: 0.01
|
step: 0.01,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Only autocrop frames",
|
name: "Only autocrop frames",
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
value: true
|
value: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Symmetric autocrop",
|
name: "Symmetric autocrop",
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
value: false
|
value: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Autocrop keep border (px)",
|
name: "Autocrop keep border (px)",
|
||||||
type: "number",
|
type: "number",
|
||||||
value: 0,
|
value: 0,
|
||||||
min: 0
|
min: 0,
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,7 +92,17 @@ class CropImage extends Operation {
|
|||||||
* @returns {byteArray}
|
* @returns {byteArray}
|
||||||
*/
|
*/
|
||||||
async run(input, args) {
|
async run(input, args) {
|
||||||
const [xPos, yPos, width, height, autocrop, autoTolerance, autoFrames, autoSymmetric, autoBorder] = args;
|
const [
|
||||||
|
xPos,
|
||||||
|
yPos,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
autocrop,
|
||||||
|
autoTolerance,
|
||||||
|
autoFrames,
|
||||||
|
autoSymmetric,
|
||||||
|
autoBorder,
|
||||||
|
] = args;
|
||||||
if (!isImage(input)) {
|
if (!isImage(input)) {
|
||||||
throw new OperationError("Invalid file type.");
|
throw new OperationError("Invalid file type.");
|
||||||
}
|
}
|
||||||
@ -108,20 +118,25 @@ class CropImage extends Operation {
|
|||||||
self.sendStatusMessage("Cropping image...");
|
self.sendStatusMessage("Cropping image...");
|
||||||
if (autocrop) {
|
if (autocrop) {
|
||||||
image.autocrop({
|
image.autocrop({
|
||||||
tolerance: (autoTolerance / 100),
|
tolerance: autoTolerance / 100,
|
||||||
cropOnlyFrames: autoFrames,
|
cropOnlyFrames: autoFrames,
|
||||||
cropSymmetric: autoSymmetric,
|
cropSymmetric: autoSymmetric,
|
||||||
leaveBorder: autoBorder
|
leaveBorder: autoBorder,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
image.crop(xPos, yPos, width, height);
|
image.crop({
|
||||||
|
x: xPos,
|
||||||
|
y: yPos,
|
||||||
|
w: width,
|
||||||
|
h: height,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let imageBuffer;
|
let imageBuffer;
|
||||||
if (image.getMIME() === "image/gif") {
|
if (image.mime === "image/gif") {
|
||||||
imageBuffer = await image.getBufferAsync(Jimp.MIME_PNG);
|
imageBuffer = await image.getBuffer(JimpMime.png);
|
||||||
} else {
|
} else {
|
||||||
imageBuffer = await image.getBufferAsync(Jimp.AUTO);
|
imageBuffer = await image.getBuffer(image.mime);
|
||||||
}
|
}
|
||||||
return imageBuffer.buffer;
|
return imageBuffer.buffer;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -145,7 +160,6 @@ class CropImage extends Operation {
|
|||||||
|
|
||||||
return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
|
return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CropImage;
|
export default CropImage;
|
||||||
|
|||||||
@ -22,7 +22,7 @@ class DESDecrypt extends Operation {
|
|||||||
|
|
||||||
this.name = "DES Decrypt";
|
this.name = "DES Decrypt";
|
||||||
this.module = "Ciphers";
|
this.module = "Ciphers";
|
||||||
this.description = "DES is a previously dominant algorithm for encryption, and was published as an official U.S. Federal Information Processing Standard (FIPS). It is now considered to be insecure due to its small key size.<br><br><b>Key:</b> DES uses a key length of 8 bytes (64 bits).<br>Triple DES uses a key length of 24 bytes (192 bits).<br><br><b>IV:</b> The Initialization Vector should be 8 bytes long. If not entered, it will default to 8 null bytes.<br><br><b>Padding:</b> In CBC and ECB mode, PKCS#7 padding will be used as a default.";
|
this.description = "DES is a previously dominant algorithm for encryption, and was published as an official U.S. Federal Information Processing Standard (FIPS). It is now considered to be insecure due to its small key size.<br><br><b>Key:</b> DES uses a key length of 8 bytes (64 bits).<br><br><b>IV:</b> The Initialization Vector should be 8 bytes long. If not entered, it will default to 8 null bytes.<br><br><b>Padding:</b> In CBC and ECB mode, PKCS#7 padding will be used as a default.";
|
||||||
this.infoURL = "https://wikipedia.org/wiki/Data_Encryption_Standard";
|
this.infoURL = "https://wikipedia.org/wiki/Data_Encryption_Standard";
|
||||||
this.inputType = "string";
|
this.inputType = "string";
|
||||||
this.outputType = "string";
|
this.outputType = "string";
|
||||||
@ -72,8 +72,7 @@ class DESDecrypt extends Operation {
|
|||||||
if (key.length !== 8) {
|
if (key.length !== 8) {
|
||||||
throw new OperationError(`Invalid key length: ${key.length} bytes
|
throw new OperationError(`Invalid key length: ${key.length} bytes
|
||||||
|
|
||||||
DES uses a key length of 8 bytes (64 bits).
|
DES uses a key length of 8 bytes (64 bits).`);
|
||||||
Triple DES uses a key length of 24 bytes (192 bits).`);
|
|
||||||
}
|
}
|
||||||
if (iv.length !== 8 && mode !== "ECB") {
|
if (iv.length !== 8 && mode !== "ECB") {
|
||||||
throw new OperationError(`Invalid IV length: ${iv.length} bytes
|
throw new OperationError(`Invalid IV length: ${iv.length} bytes
|
||||||
|
|||||||
@ -22,7 +22,7 @@ class DESEncrypt extends Operation {
|
|||||||
|
|
||||||
this.name = "DES Encrypt";
|
this.name = "DES Encrypt";
|
||||||
this.module = "Ciphers";
|
this.module = "Ciphers";
|
||||||
this.description = "DES is a previously dominant algorithm for encryption, and was published as an official U.S. Federal Information Processing Standard (FIPS). It is now considered to be insecure due to its small key size.<br><br><b>Key:</b> DES uses a key length of 8 bytes (64 bits).<br>Triple DES uses a key length of 24 bytes (192 bits).<br><br>You can generate a password-based key using one of the KDF operations.<br><br><b>IV:</b> The Initialization Vector should be 8 bytes long. If not entered, it will default to 8 null bytes.<br><br><b>Padding:</b> In CBC and ECB mode, PKCS#7 padding will be used.";
|
this.description = "DES is a previously dominant algorithm for encryption, and was published as an official U.S. Federal Information Processing Standard (FIPS). It is now considered to be insecure due to its small key size.<br><br><b>Key:</b> DES uses a key length of 8 bytes (64 bits).<br><br>You can generate a password-based key using one of the KDF operations.<br><br><b>IV:</b> The Initialization Vector should be 8 bytes long. If not entered, it will default to 8 null bytes.<br><br><b>Padding:</b> In CBC and ECB mode, PKCS#7 padding will be used.";
|
||||||
this.infoURL = "https://wikipedia.org/wiki/Data_Encryption_Standard";
|
this.infoURL = "https://wikipedia.org/wiki/Data_Encryption_Standard";
|
||||||
this.inputType = "string";
|
this.inputType = "string";
|
||||||
this.outputType = "string";
|
this.outputType = "string";
|
||||||
@ -70,8 +70,7 @@ class DESEncrypt extends Operation {
|
|||||||
if (key.length !== 8) {
|
if (key.length !== 8) {
|
||||||
throw new OperationError(`Invalid key length: ${key.length} bytes
|
throw new OperationError(`Invalid key length: ${key.length} bytes
|
||||||
|
|
||||||
DES uses a key length of 8 bytes (64 bits).
|
DES uses a key length of 8 bytes (64 bits).`);
|
||||||
Triple DES uses a key length of 24 bytes (192 bits).`);
|
|
||||||
}
|
}
|
||||||
if (iv.length !== 8 && mode !== "ECB") {
|
if (iv.length !== 8 && mode !== "ECB") {
|
||||||
throw new OperationError(`Invalid IV length: ${iv.length} bytes
|
throw new OperationError(`Invalid IV length: ${iv.length} bytes
|
||||||
|
|||||||
193
src/core/operations/DisassembleARM.mjs
Normal file
193
src/core/operations/DisassembleARM.mjs
Normal 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;
|
||||||
@ -9,13 +9,12 @@ import OperationError from "../errors/OperationError.mjs";
|
|||||||
import { isImage } from "../lib/FileType.mjs";
|
import { isImage } from "../lib/FileType.mjs";
|
||||||
import { toBase64 } from "../lib/Base64.mjs";
|
import { toBase64 } from "../lib/Base64.mjs";
|
||||||
import { isWorkerEnvironment } from "../Utils.mjs";
|
import { isWorkerEnvironment } from "../Utils.mjs";
|
||||||
import Jimp from "jimp/es/index.js";
|
import { Jimp, JimpMime } from "jimp";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Image Dither operation
|
* Image Dither operation
|
||||||
*/
|
*/
|
||||||
class DitherImage extends Operation {
|
class DitherImage extends Operation {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DitherImage constructor
|
* DitherImage constructor
|
||||||
*/
|
*/
|
||||||
@ -51,17 +50,19 @@ class DitherImage extends Operation {
|
|||||||
try {
|
try {
|
||||||
if (isWorkerEnvironment())
|
if (isWorkerEnvironment())
|
||||||
self.sendStatusMessage("Applying dither to image...");
|
self.sendStatusMessage("Applying dither to image...");
|
||||||
image.dither565();
|
image.dither();
|
||||||
|
|
||||||
let imageBuffer;
|
let imageBuffer;
|
||||||
if (image.getMIME() === "image/gif") {
|
if (image.mime === "image/gif") {
|
||||||
imageBuffer = await image.getBufferAsync(Jimp.MIME_PNG);
|
imageBuffer = await image.getBuffer(JimpMime.png);
|
||||||
} else {
|
} else {
|
||||||
imageBuffer = await image.getBufferAsync(Jimp.AUTO);
|
imageBuffer = await image.getBuffer(image.mime);
|
||||||
}
|
}
|
||||||
return imageBuffer.buffer;
|
return imageBuffer.buffer;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new OperationError(`Error applying dither to image. (${err})`);
|
throw new OperationError(
|
||||||
|
`Error applying dither to image. (${err})`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,7 +82,6 @@ class DitherImage extends Operation {
|
|||||||
|
|
||||||
return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
|
return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DitherImage;
|
export default DitherImage;
|
||||||
|
|||||||
79
src/core/operations/DropNthBytes.mjs
Normal file
79
src/core/operations/DropNthBytes.mjs
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
/**
|
||||||
|
* @author Oshawk [oshawk@protonmail.com]
|
||||||
|
* @copyright Crown Copyright 2019
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Operation from "../Operation.mjs";
|
||||||
|
import OperationError from "../errors/OperationError.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drop nth bytes operation
|
||||||
|
*/
|
||||||
|
class DropNthBytes extends Operation {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DropNthBytes constructor
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.name = "Drop nth bytes";
|
||||||
|
this.module = "Default";
|
||||||
|
this.description = "Drops every nth byte starting with a given byte.";
|
||||||
|
this.infoURL = "";
|
||||||
|
this.inputType = "byteArray";
|
||||||
|
this.outputType = "byteArray";
|
||||||
|
this.args = [
|
||||||
|
{
|
||||||
|
name: "Drop every",
|
||||||
|
type: "number",
|
||||||
|
value: 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Starting at",
|
||||||
|
type: "number",
|
||||||
|
value: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Apply to each line",
|
||||||
|
type: "boolean",
|
||||||
|
value: false
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {byteArray} input
|
||||||
|
* @param {Object[]} args
|
||||||
|
* @returns {byteArray}
|
||||||
|
*/
|
||||||
|
run(input, args) {
|
||||||
|
const n = args[0];
|
||||||
|
const start = args[1];
|
||||||
|
const eachLine = args[2];
|
||||||
|
|
||||||
|
if (parseInt(n, 10) !== n || n <= 0) {
|
||||||
|
throw new OperationError("'Drop every' must be a positive integer.");
|
||||||
|
}
|
||||||
|
if (parseInt(start, 10) !== start || start < 0) {
|
||||||
|
throw new OperationError("'Starting at' must be a positive or zero integer.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let offset = 0;
|
||||||
|
const output = [];
|
||||||
|
for (let i = 0; i < input.length; i++) {
|
||||||
|
if (eachLine && input[i] === 0x0a) {
|
||||||
|
output.push(0x0a);
|
||||||
|
offset = i + 1;
|
||||||
|
} else if (i - offset < start || (i - (start + offset)) % n !== 0) {
|
||||||
|
output.push(input[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DropNthBytes;
|
||||||
@ -9,6 +9,7 @@ import OperationError from "../errors/OperationError.mjs";
|
|||||||
import { fromBase64 } from "../lib/Base64.mjs";
|
import { fromBase64 } from "../lib/Base64.mjs";
|
||||||
import { toHexFast } from "../lib/Hex.mjs";
|
import { toHexFast } from "../lib/Hex.mjs";
|
||||||
import r from "jsrsasign";
|
import r from "jsrsasign";
|
||||||
|
import Utils from "../Utils.mjs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ECDSA Verify operation
|
* ECDSA Verify operation
|
||||||
@ -59,6 +60,11 @@ class ECDSAVerify extends Operation {
|
|||||||
name: "Message",
|
name: "Message",
|
||||||
type: "text",
|
type: "text",
|
||||||
value: ""
|
value: ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Message format",
|
||||||
|
type: "option",
|
||||||
|
value: ["Raw", "Hex", "Base64"]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@ -70,7 +76,7 @@ class ECDSAVerify extends Operation {
|
|||||||
*/
|
*/
|
||||||
run(input, args) {
|
run(input, args) {
|
||||||
let inputFormat = args[0];
|
let inputFormat = args[0];
|
||||||
const [, mdAlgo, keyPem, msg] = args;
|
const [, mdAlgo, keyPem, msg, msgFormat] = args;
|
||||||
|
|
||||||
if (keyPem.replace("-----BEGIN PUBLIC KEY-----", "").length === 0) {
|
if (keyPem.replace("-----BEGIN PUBLIC KEY-----", "").length === 0) {
|
||||||
throw new OperationError("Please enter a public key.");
|
throw new OperationError("Please enter a public key.");
|
||||||
@ -145,7 +151,8 @@ class ECDSAVerify extends Operation {
|
|||||||
throw new OperationError("Provided key is not a public key.");
|
throw new OperationError("Provided key is not a public key.");
|
||||||
}
|
}
|
||||||
sig.init(key);
|
sig.init(key);
|
||||||
sig.updateString(msg);
|
const messageStr = Utils.convertToByteString(msg, msgFormat);
|
||||||
|
sig.updateString(messageStr);
|
||||||
const result = sig.verify(signatureASN1Hex);
|
const result = sig.verify(signatureASN1Hex);
|
||||||
return result ? "Verified OK" : "Verification Failure";
|
return result ? "Verified OK" : "Verification Failure";
|
||||||
}
|
}
|
||||||
|
|||||||
@ -44,23 +44,6 @@ class EscapeUnicodeCharacters extends Operation {
|
|||||||
"value": true
|
"value": true
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
this.checks = [
|
|
||||||
{
|
|
||||||
pattern: "\\\\u(?:[\\da-f]{4,6})",
|
|
||||||
flags: "i",
|
|
||||||
args: ["\\u"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: "%u(?:[\\da-f]{4,6})",
|
|
||||||
flags: "i",
|
|
||||||
args: ["%u"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: "U\\+(?:[\\da-f]{4,6})",
|
|
||||||
flags: "i",
|
|
||||||
args: ["U+"]
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
175
src/core/operations/ExtractAudioMetadata.mjs
Normal file
175
src/core/operations/ExtractAudioMetadata.mjs
Normal 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;
|
||||||
@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import Operation from "../Operation.mjs";
|
import Operation from "../Operation.mjs";
|
||||||
import { search, DOMAIN_REGEX } from "../lib/Extract.mjs";
|
import { search, DOMAIN_REGEX, DMARC_DOMAIN_REGEX } from "../lib/Extract.mjs";
|
||||||
import { caseInsensitiveSort } from "../lib/Sort.mjs";
|
import { caseInsensitiveSort } from "../lib/Sort.mjs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -39,6 +39,11 @@ class ExtractDomains extends Operation {
|
|||||||
name: "Unique",
|
name: "Unique",
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
value: false
|
value: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Underscore (DMARC, DKIM, etc)",
|
||||||
|
type: "boolean",
|
||||||
|
value: false
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@ -49,11 +54,11 @@ class ExtractDomains extends Operation {
|
|||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
run(input, args) {
|
run(input, args) {
|
||||||
const [displayTotal, sort, unique] = args;
|
const [displayTotal, sort, unique, dmarc] = args;
|
||||||
|
|
||||||
const results = search(
|
const results = search(
|
||||||
input,
|
input,
|
||||||
DOMAIN_REGEX,
|
dmarc ? DMARC_DOMAIN_REGEX : DOMAIN_REGEX,
|
||||||
null,
|
null,
|
||||||
sort ? caseInsensitiveSort : null,
|
sort ? caseInsensitiveSort : null,
|
||||||
unique
|
unique
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import Operation from "../Operation.mjs";
|
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";
|
import { caseInsensitiveSort } from "../lib/Sort.mjs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -50,8 +50,7 @@ class ExtractEmailAddresses extends Operation {
|
|||||||
*/
|
*/
|
||||||
run(input, args) {
|
run(input, args) {
|
||||||
const [displayTotal, sort, unique] = args,
|
const [displayTotal, sort, unique] = args,
|
||||||
// email regex from: https://www.regextester.com/98066
|
regex = EMAIL_REGEX;
|
||||||
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}\])/ig;
|
|
||||||
|
|
||||||
const results = search(
|
const results = search(
|
||||||
input,
|
input,
|
||||||
|
|||||||
@ -21,7 +21,7 @@ class ExtractIPAddresses extends Operation {
|
|||||||
|
|
||||||
this.name = "Extract IP addresses";
|
this.name = "Extract IP addresses";
|
||||||
this.module = "Regex";
|
this.module = "Regex";
|
||||||
this.description = "Extracts all IPv4 and IPv6 addresses.<br><br>Warning: Given a string <code>710.65.0.456</code>, this will match <code>10.65.0.45</code> so always check the original input!";
|
this.description = "Extracts all IPv4 and IPv6 addresses.<br><br>Warning: Given a string <code>1.2.3.4.5.6.7.8</code>, this will match <code>1.2.3.4 and 5.6.7.8</code> so always check the original input!";
|
||||||
this.inputType = "string";
|
this.inputType = "string";
|
||||||
this.outputType = "string";
|
this.outputType = "string";
|
||||||
this.args = [
|
this.args = [
|
||||||
@ -65,7 +65,21 @@ class ExtractIPAddresses extends Operation {
|
|||||||
*/
|
*/
|
||||||
run(input, args) {
|
run(input, args) {
|
||||||
const [includeIpv4, includeIpv6, removeLocal, displayTotal, sort, unique] = args,
|
const [includeIpv4, includeIpv6, removeLocal, displayTotal, sort, unique] = args,
|
||||||
ipv4 = "(?:(?:\\d|[01]?\\d\\d|2[0-4]\\d|25[0-5])\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?\\d\\d|\\d)(?:\\/\\d{1,2})?",
|
|
||||||
|
// IPv4 decimal groups can have values 0 to 255. To construct a regex the following sub-regex is reused:
|
||||||
|
ipv4DecimalByte = "(?:25[0-5]|2[0-4]\\d|1?[0-9]\\d|\\d)",
|
||||||
|
ipv4OctalByte = "(?:0[1-3]?[0-7]{1,2})",
|
||||||
|
|
||||||
|
// Look behind and ahead will be used to exclude matches with additional decimal digits left and right of IP address
|
||||||
|
lookBehind = "(?<!\\d)",
|
||||||
|
lookAhead = "(?!\\d)",
|
||||||
|
|
||||||
|
// Each variant requires exactly 4 groups with literal . between.
|
||||||
|
ipv4Decimal = "(?:" + lookBehind + ipv4DecimalByte + "\\.){3}" + "(?:" + ipv4DecimalByte + lookAhead + ")",
|
||||||
|
ipv4Octal = "(?:" + lookBehind + ipv4OctalByte + "\\.){3}" + "(?:" + ipv4OctalByte + lookAhead + ")",
|
||||||
|
|
||||||
|
// Then we allow IPv4 addresses to be expressed either entirely in decimal or entirely in Octal
|
||||||
|
ipv4 = "(?:" + ipv4Decimal + "|" + ipv4Octal + ")",
|
||||||
ipv6 = "((?=.*::)(?!.*::.+::)(::)?([\\dA-F]{1,4}:(:|\\b)|){5}|([\\dA-F]{1,4}:){6})(([\\dA-F]{1,4}((?!\\3)::|:\\b|(?![\\dA-F])))|(?!\\2\\3)){2}";
|
ipv6 = "((?=.*::)(?!.*::.+::)(::)?([\\dA-F]{1,4}:(:|\\b)|){5}|([\\dA-F]{1,4}:){6})(([\\dA-F]{1,4}((?!\\3)::|:\\b|(?![\\dA-F])))|(?!\\2\\3)){2}";
|
||||||
let ips = "";
|
let ips = "";
|
||||||
|
|
||||||
|
|||||||
@ -9,13 +9,12 @@ import OperationError from "../errors/OperationError.mjs";
|
|||||||
import Utils from "../Utils.mjs";
|
import Utils from "../Utils.mjs";
|
||||||
import { fromBinary } from "../lib/Binary.mjs";
|
import { fromBinary } from "../lib/Binary.mjs";
|
||||||
import { isImage } from "../lib/FileType.mjs";
|
import { isImage } from "../lib/FileType.mjs";
|
||||||
import Jimp from "jimp/es/index.js";
|
import { Jimp } from "jimp";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract LSB operation
|
* Extract LSB operation
|
||||||
*/
|
*/
|
||||||
class ExtractLSB extends Operation {
|
class ExtractLSB extends Operation {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ExtractLSB constructor
|
* ExtractLSB constructor
|
||||||
*/
|
*/
|
||||||
@ -24,8 +23,10 @@ class ExtractLSB extends Operation {
|
|||||||
|
|
||||||
this.name = "Extract LSB";
|
this.name = "Extract LSB";
|
||||||
this.module = "Image";
|
this.module = "Image";
|
||||||
this.description = "Extracts the Least Significant Bit data from each pixel in an image. This is a common way to hide data in Steganography.";
|
this.description =
|
||||||
this.infoURL = "https://wikipedia.org/wiki/Bit_numbering#Least_significant_bit_in_digital_steganography";
|
"Extracts the Least Significant Bit data from each pixel in an image. This is a common way to hide data in Steganography.";
|
||||||
|
this.infoURL =
|
||||||
|
"https://wikipedia.org/wiki/Bit_numbering#Least_significant_bit_in_digital_steganography";
|
||||||
this.inputType = "ArrayBuffer";
|
this.inputType = "ArrayBuffer";
|
||||||
this.outputType = "byteArray";
|
this.outputType = "byteArray";
|
||||||
this.args = [
|
this.args = [
|
||||||
@ -57,8 +58,8 @@ class ExtractLSB extends Operation {
|
|||||||
{
|
{
|
||||||
name: "Bit",
|
name: "Bit",
|
||||||
type: "number",
|
type: "number",
|
||||||
value: 0
|
value: 0,
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,21 +69,27 @@ class ExtractLSB extends Operation {
|
|||||||
* @returns {byteArray}
|
* @returns {byteArray}
|
||||||
*/
|
*/
|
||||||
async run(input, args) {
|
async run(input, args) {
|
||||||
if (!isImage(input)) throw new OperationError("Please enter a valid image file.");
|
if (!isImage(input))
|
||||||
|
throw new OperationError("Please enter a valid image file.");
|
||||||
|
|
||||||
const bit = 7 - args.pop(),
|
const bit = 7 - args.pop(),
|
||||||
pixelOrder = args.pop(),
|
pixelOrder = args.pop(),
|
||||||
colours = args.filter(option => option !== "").map(option => COLOUR_OPTIONS.indexOf(option)),
|
colours = args
|
||||||
|
.filter((option) => option !== "")
|
||||||
|
.map((option) => COLOUR_OPTIONS.indexOf(option)),
|
||||||
parsedImage = await Jimp.read(input),
|
parsedImage = await Jimp.read(input),
|
||||||
width = parsedImage.bitmap.width,
|
width = parsedImage.bitmap.width,
|
||||||
height = parsedImage.bitmap.height,
|
height = parsedImage.bitmap.height,
|
||||||
rgba = parsedImage.bitmap.data;
|
rgba = parsedImage.bitmap.data;
|
||||||
|
|
||||||
if (bit < 0 || bit > 7) {
|
if (bit < 0 || bit > 7) {
|
||||||
throw new OperationError("Error: Bit argument must be between 0 and 7");
|
throw new OperationError(
|
||||||
|
"Error: Bit argument must be between 0 and 7",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let i, combinedBinary = "";
|
let i,
|
||||||
|
combinedBinary = "";
|
||||||
|
|
||||||
if (pixelOrder === "Row") {
|
if (pixelOrder === "Row") {
|
||||||
for (i = 0; i < rgba.length; i += 4) {
|
for (i = 0; i < rgba.length; i += 4) {
|
||||||
@ -106,7 +113,6 @@ class ExtractLSB extends Operation {
|
|||||||
|
|
||||||
return fromBinary(combinedBinary);
|
return fromBinary(combinedBinary);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const COLOUR_OPTIONS = ["R", "G", "B", "A"];
|
const COLOUR_OPTIONS = ["R", "G", "B", "A"];
|
||||||
|
|||||||
@ -7,15 +7,14 @@
|
|||||||
import Operation from "../Operation.mjs";
|
import Operation from "../Operation.mjs";
|
||||||
import OperationError from "../errors/OperationError.mjs";
|
import OperationError from "../errors/OperationError.mjs";
|
||||||
import { isImage } from "../lib/FileType.mjs";
|
import { isImage } from "../lib/FileType.mjs";
|
||||||
import Jimp from "jimp/es/index.js";
|
import { Jimp } from "jimp";
|
||||||
|
|
||||||
import {RGBA_DELIM_OPTIONS} from "../lib/Delim.mjs";
|
import { RGBA_DELIM_OPTIONS } from "../lib/Delim.mjs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract RGBA operation
|
* Extract RGBA operation
|
||||||
*/
|
*/
|
||||||
class ExtractRGBA extends Operation {
|
class ExtractRGBA extends Operation {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ExtractRGBA constructor
|
* ExtractRGBA constructor
|
||||||
*/
|
*/
|
||||||
@ -24,7 +23,8 @@ class ExtractRGBA extends Operation {
|
|||||||
|
|
||||||
this.name = "Extract RGBA";
|
this.name = "Extract RGBA";
|
||||||
this.module = "Image";
|
this.module = "Image";
|
||||||
this.description = "Extracts each pixel's RGBA value in an image. These are sometimes used in Steganography to hide text or data.";
|
this.description =
|
||||||
|
"Extracts each pixel's RGBA value in an image. These are sometimes used in Steganography to hide text or data.";
|
||||||
this.infoURL = "https://wikipedia.org/wiki/RGBA_color_space";
|
this.infoURL = "https://wikipedia.org/wiki/RGBA_color_space";
|
||||||
this.inputType = "ArrayBuffer";
|
this.inputType = "ArrayBuffer";
|
||||||
this.outputType = "string";
|
this.outputType = "string";
|
||||||
@ -32,13 +32,13 @@ class ExtractRGBA extends Operation {
|
|||||||
{
|
{
|
||||||
name: "Delimiter",
|
name: "Delimiter",
|
||||||
type: "editableOption",
|
type: "editableOption",
|
||||||
value: RGBA_DELIM_OPTIONS
|
value: RGBA_DELIM_OPTIONS,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Include Alpha",
|
name: "Include Alpha",
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
value: true
|
value: true,
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,18 +48,20 @@ class ExtractRGBA extends Operation {
|
|||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
async run(input, args) {
|
async run(input, args) {
|
||||||
if (!isImage(input)) throw new OperationError("Please enter a valid image file.");
|
if (!isImage(input))
|
||||||
|
throw new OperationError("Please enter a valid image file.");
|
||||||
|
|
||||||
const delimiter = args[0],
|
const delimiter = args[0],
|
||||||
includeAlpha = args[1],
|
includeAlpha = args[1],
|
||||||
parsedImage = await Jimp.read(input);
|
parsedImage = await Jimp.read(input);
|
||||||
|
|
||||||
let bitmap = parsedImage.bitmap.data;
|
let bitmap = parsedImage.bitmap.data;
|
||||||
bitmap = includeAlpha ? bitmap : bitmap.filter((val, idx) => idx % 4 !== 3);
|
bitmap = includeAlpha ?
|
||||||
|
bitmap :
|
||||||
|
bitmap.filter((val, idx) => idx % 4 !== 3);
|
||||||
|
|
||||||
return bitmap.join(delimiter);
|
return bitmap.join(delimiter);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ExtractRGBA;
|
export default ExtractRGBA;
|
||||||
|
|||||||
80
src/core/operations/FlaskSessionDecode.mjs
Normal file
80
src/core/operations/FlaskSessionDecode.mjs
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
/**
|
||||||
|
* @author ThePlayer372-FR []
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Operation from "../Operation.mjs";
|
||||||
|
import OperationError from "../errors/OperationError.mjs";
|
||||||
|
import { fromBase64 } from "../lib/Base64.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flask Session Decode operation
|
||||||
|
*/
|
||||||
|
class FlaskSessionDecode extends Operation {
|
||||||
|
/**
|
||||||
|
* FlaskSessionDecode constructor
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.name = "Flask Session Decode";
|
||||||
|
this.module = "Crypto";
|
||||||
|
this.description = "Decodes the payload of a Flask session cookie (itsdangerous) into JSON.";
|
||||||
|
this.inputType = "string";
|
||||||
|
this.outputType = "JSON";
|
||||||
|
this.args = [
|
||||||
|
{
|
||||||
|
name: "View TimeStamp",
|
||||||
|
type: "boolean",
|
||||||
|
value: false
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} input
|
||||||
|
* @param {Object[]} args
|
||||||
|
* @returns {Object[]}
|
||||||
|
*/
|
||||||
|
run(input, args) {
|
||||||
|
input = input.trim();
|
||||||
|
const parts = input.split(".");
|
||||||
|
if (parts.length !== 3) {
|
||||||
|
throw new OperationError("Invalid Flask token format. Expected payload.timestamp.signature");
|
||||||
|
}
|
||||||
|
|
||||||
|
const payloadB64 = parts[0];
|
||||||
|
const time = parts[1];
|
||||||
|
|
||||||
|
const timeB64 = time.replace(/-/g, "+").replace(/_/g, "/");
|
||||||
|
const binary = fromBase64(timeB64);
|
||||||
|
const bytes = new Uint8Array(4);
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
bytes[i] = binary.charCodeAt(i);
|
||||||
|
}
|
||||||
|
const view = new DataView(bytes.buffer);
|
||||||
|
const timestamp = view.getInt32(0, false);
|
||||||
|
|
||||||
|
const base64 = payloadB64.replace(/-/g, "+").replace(/_/g, "/");
|
||||||
|
const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
|
||||||
|
let payloadJson;
|
||||||
|
try {
|
||||||
|
payloadJson = fromBase64(padded);
|
||||||
|
} catch (e) {
|
||||||
|
throw new OperationError("Invalid Base64 payload");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let data = JSON.parse(payloadJson);
|
||||||
|
|
||||||
|
if (args[0]) {
|
||||||
|
data = {payload: data, timestamp: timestamp};
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
} catch (e) {
|
||||||
|
throw new OperationError("Unable to decode JSON payload: " + e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FlaskSessionDecode;
|
||||||
89
src/core/operations/FlaskSessionSign.mjs
Normal file
89
src/core/operations/FlaskSessionSign.mjs
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
/**
|
||||||
|
* @author ThePlayer372-FR []
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Operation from "../Operation.mjs";
|
||||||
|
import CryptoApi from "crypto-api/src/crypto-api.mjs";
|
||||||
|
import Utils from "../Utils.mjs";
|
||||||
|
import { toBase64 } from "../lib/Base64.mjs";
|
||||||
|
import OperationError from "../errors/OperationError.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flask Session Sign operation
|
||||||
|
*/
|
||||||
|
class FlaskSessionSign extends Operation {
|
||||||
|
/**
|
||||||
|
* FlaskSessionSign constructor
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.name = "Flask Session Sign";
|
||||||
|
this.module = "Crypto";
|
||||||
|
this.description = "Signs a JSON payload to produce a Flask session cookie (itsdangerous HMAC).";
|
||||||
|
this.inputType = "JSON";
|
||||||
|
this.outputType = "string";
|
||||||
|
this.args = [
|
||||||
|
{
|
||||||
|
name: "Key",
|
||||||
|
type: "toggleString",
|
||||||
|
value: "",
|
||||||
|
toggleValues: ["Hex", "Decimal", "Binary", "Base64", "UTF8", "Latin1"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Salt",
|
||||||
|
type: "toggleString",
|
||||||
|
value: "cookie-session",
|
||||||
|
toggleValues: ["UTF8", "Hex", "Decimal", "Binary", "Base64", "Latin1"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Algorithm",
|
||||||
|
type: "option",
|
||||||
|
value: ["sha1", "sha256"],
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} input
|
||||||
|
* @param {Object[]} args
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
run(input, args) {
|
||||||
|
if (!args[0].string) {
|
||||||
|
throw new OperationError("Secret key required");
|
||||||
|
}
|
||||||
|
const key = Utils.convertToByteString(args[0].string, args[0].option);
|
||||||
|
const salt = Utils.convertToByteString(args[1].string || "cookie-session", args[1].option);
|
||||||
|
const algorithm = args[2] || "sha1";
|
||||||
|
|
||||||
|
const payloadB64 = toBase64(Utils.strToByteArray(JSON.stringify(input)));
|
||||||
|
const payload = payloadB64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
||||||
|
|
||||||
|
const derivedKey = CryptoApi.getHmac(key, CryptoApi.getHasher(algorithm));
|
||||||
|
derivedKey.update(salt);
|
||||||
|
|
||||||
|
const currentTimeStamp = Math.ceil(Date.now() / 1000);
|
||||||
|
const buffer = new ArrayBuffer(4);
|
||||||
|
const view = new DataView(buffer);
|
||||||
|
view.setInt32(0, currentTimeStamp, false);
|
||||||
|
const bytes = new Uint8Array(buffer);
|
||||||
|
let binary = "";
|
||||||
|
bytes.forEach(b => binary += String.fromCharCode(b));
|
||||||
|
const timeB64 = toBase64(Utils.strToByteArray(binary));
|
||||||
|
const time = timeB64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
||||||
|
|
||||||
|
const data = Utils.convertToByteString(payload + "." + time, "utf8");
|
||||||
|
const sign = CryptoApi.getHmac(derivedKey.finalize(), CryptoApi.getHasher(algorithm));
|
||||||
|
sign.update(data);
|
||||||
|
|
||||||
|
const signB64 = toBase64(sign.finalize());
|
||||||
|
const sign64 = signB64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
||||||
|
|
||||||
|
return payload + "." + time + "." + sign64;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default FlaskSessionSign;
|
||||||
136
src/core/operations/FlaskSessionVerify.mjs
Normal file
136
src/core/operations/FlaskSessionVerify.mjs
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
/**
|
||||||
|
* @author ThePlayer372-FR []
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Operation from "../Operation.mjs";
|
||||||
|
import OperationError from "../errors/OperationError.mjs";
|
||||||
|
import CryptoApi from "crypto-api/src/crypto-api.mjs";
|
||||||
|
import Utils from "../Utils.mjs";
|
||||||
|
import { toBase64, fromBase64 } from "../lib/Base64.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flask Session Verify operation
|
||||||
|
*/
|
||||||
|
class FlaskSessionVerify extends Operation {
|
||||||
|
/**
|
||||||
|
* FlaskSessionVerify constructor
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.name = "Flask Session Verify";
|
||||||
|
this.module = "Crypto";
|
||||||
|
this.description = "Verifies the HMAC signature of a Flask session cookie (itsdangerous) generated.";
|
||||||
|
this.inputType = "string";
|
||||||
|
this.outputType = "JSON";
|
||||||
|
this.args = [
|
||||||
|
{
|
||||||
|
name: "Key",
|
||||||
|
type: "toggleString",
|
||||||
|
value: "",
|
||||||
|
toggleValues: ["Hex", "Decimal", "Binary", "Base64", "UTF8", "Latin1"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Salt",
|
||||||
|
type: "toggleString",
|
||||||
|
value: "cookie-session",
|
||||||
|
toggleValues: ["UTF8", "Hex", "Decimal", "Binary", "Base64", "Latin1"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Algorithm",
|
||||||
|
type: "option",
|
||||||
|
value: ["sha1", "sha256"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "View TimeStamp",
|
||||||
|
type: "boolean",
|
||||||
|
value: true
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} input
|
||||||
|
* @param {Object[]} args
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
run(input, args) {
|
||||||
|
|
||||||
|
if (!args[0].string) {
|
||||||
|
throw new OperationError("Secret key required");
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = Utils.convertToByteString(args[0].string, args[0].option);
|
||||||
|
const salt = Utils.convertToByteString(args[1].string || "cookie-session", args[1].option);
|
||||||
|
const algorithm = args[2] || "sha1";
|
||||||
|
|
||||||
|
input = input.trim();
|
||||||
|
|
||||||
|
const parts = input.split(".");
|
||||||
|
|
||||||
|
if (parts.length !== 3) {
|
||||||
|
throw new OperationError("Invalid Flask token format. Expected payload.timestamp.signature");
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = Utils.convertToByteString(parts[0] + "." + parts[1], "utf8");
|
||||||
|
|
||||||
|
|
||||||
|
const derivedKey = CryptoApi.getHmac(key, CryptoApi.getHasher(algorithm));
|
||||||
|
derivedKey.update(salt);
|
||||||
|
|
||||||
|
const sign = CryptoApi.getHmac(derivedKey.finalize(), CryptoApi.getHasher(algorithm));
|
||||||
|
sign.update(data);
|
||||||
|
|
||||||
|
const payloadB64 = parts[0];
|
||||||
|
const base64 = payloadB64.replace(/-/g, "+").replace(/_/g, "/");
|
||||||
|
const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
|
||||||
|
|
||||||
|
const time = parts[1];
|
||||||
|
|
||||||
|
const timeB64 = time.replace(/-/g, "+").replace(/_/g, "/");
|
||||||
|
const binary = fromBase64(timeB64);
|
||||||
|
const bytes = new Uint8Array(4);
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
bytes[i] = binary.charCodeAt(i);
|
||||||
|
}
|
||||||
|
const view = new DataView(bytes.buffer);
|
||||||
|
const timestamp = view.getInt32(0, false);
|
||||||
|
|
||||||
|
let payloadJson;
|
||||||
|
try {
|
||||||
|
payloadJson = fromBase64(padded);
|
||||||
|
} catch (e) {
|
||||||
|
throw new OperationError("Invalid Base64 payload");
|
||||||
|
}
|
||||||
|
|
||||||
|
const signB64 = toBase64(sign.finalize());
|
||||||
|
const sign64 = signB64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
||||||
|
|
||||||
|
if (sign64 !== parts[2]) {
|
||||||
|
throw new OperationError("Invalid signature!");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const decoded = JSON.parse(payloadJson);
|
||||||
|
if (!args[3]) {
|
||||||
|
return {
|
||||||
|
valid: true,
|
||||||
|
payload: decoded,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
valid: true,
|
||||||
|
payload: decoded,
|
||||||
|
timestamp: timestamp
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
throw new OperationError("Unable to decode JSON payload: " + e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default FlaskSessionVerify;
|
||||||
@ -9,13 +9,12 @@ import OperationError from "../errors/OperationError.mjs";
|
|||||||
import { isImage } from "../lib/FileType.mjs";
|
import { isImage } from "../lib/FileType.mjs";
|
||||||
import { toBase64 } from "../lib/Base64.mjs";
|
import { toBase64 } from "../lib/Base64.mjs";
|
||||||
import { isWorkerEnvironment } from "../Utils.mjs";
|
import { isWorkerEnvironment } from "../Utils.mjs";
|
||||||
import Jimp from "jimp/es/index.js";
|
import { Jimp, JimpMime } from "jimp";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flip Image operation
|
* Flip Image operation
|
||||||
*/
|
*/
|
||||||
class FlipImage extends Operation {
|
class FlipImage extends Operation {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FlipImage constructor
|
* FlipImage constructor
|
||||||
*/
|
*/
|
||||||
@ -33,8 +32,8 @@ class FlipImage extends Operation {
|
|||||||
{
|
{
|
||||||
name: "Axis",
|
name: "Axis",
|
||||||
type: "option",
|
type: "option",
|
||||||
value: ["Horizontal", "Vertical"]
|
value: ["Horizontal", "Vertical"],
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,18 +59,24 @@ class FlipImage extends Operation {
|
|||||||
self.sendStatusMessage("Flipping image...");
|
self.sendStatusMessage("Flipping image...");
|
||||||
switch (flipAxis) {
|
switch (flipAxis) {
|
||||||
case "Horizontal":
|
case "Horizontal":
|
||||||
image.flip(true, false);
|
image.flip({
|
||||||
|
horizontal: true,
|
||||||
|
vertical: false,
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case "Vertical":
|
case "Vertical":
|
||||||
image.flip(false, true);
|
image.flip({
|
||||||
|
horizontal: false,
|
||||||
|
vertical: true,
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
let imageBuffer;
|
let imageBuffer;
|
||||||
if (image.getMIME() === "image/gif") {
|
if (image.mime === "image/gif") {
|
||||||
imageBuffer = await image.getBufferAsync(Jimp.MIME_PNG);
|
imageBuffer = await image.getBuffer(JimpMime.png);
|
||||||
} else {
|
} else {
|
||||||
imageBuffer = await image.getBufferAsync(Jimp.AUTO);
|
imageBuffer = await image.getBuffer(image.mime);
|
||||||
}
|
}
|
||||||
return imageBuffer.buffer;
|
return imageBuffer.buffer;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -95,7 +100,6 @@ class FlipImage extends Operation {
|
|||||||
|
|
||||||
return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
|
return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default FlipImage;
|
export default FlipImage;
|
||||||
|
|||||||
@ -6,6 +6,8 @@
|
|||||||
|
|
||||||
import Operation from "../Operation.mjs";
|
import Operation from "../Operation.mjs";
|
||||||
import Utils from "../Utils.mjs";
|
import Utils from "../Utils.mjs";
|
||||||
|
import {ALPHABET_OPTIONS} from "../lib/Base32.mjs";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* From Base32 operation
|
* From Base32 operation
|
||||||
@ -27,8 +29,8 @@ class FromBase32 extends Operation {
|
|||||||
this.args = [
|
this.args = [
|
||||||
{
|
{
|
||||||
name: "Alphabet",
|
name: "Alphabet",
|
||||||
type: "binaryString",
|
type: "editableOption",
|
||||||
value: "A-Z2-7="
|
value: ALPHABET_OPTIONS
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Remove non-alphabet chars",
|
name: "Remove non-alphabet chars",
|
||||||
@ -41,6 +43,11 @@ class FromBase32 extends Operation {
|
|||||||
pattern: "^(?:[A-Z2-7]{8})+(?:[A-Z2-7]{2}={6}|[A-Z2-7]{4}={4}|[A-Z2-7]{5}={3}|[A-Z2-7]{7}={1})?$",
|
pattern: "^(?:[A-Z2-7]{8})+(?:[A-Z2-7]{2}={6}|[A-Z2-7]{4}={4}|[A-Z2-7]{5}={3}|[A-Z2-7]{7}={1})?$",
|
||||||
flags: "",
|
flags: "",
|
||||||
args: ["A-Z2-7=", false]
|
args: ["A-Z2-7=", false]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: "^(?:[0-9A-V]{8})+(?:[0-9A-V]{2}={6}|[0-9A-V]{4}={4}|[0-9A-V]{5}={3}|[0-9A-V]{7}={1})?$",
|
||||||
|
flags: "",
|
||||||
|
args: ["0-9A-V=", false]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@ -96,3 +103,4 @@ class FromBase32 extends Operation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default FromBase32;
|
export default FromBase32;
|
||||||
|
|
||||||
|
|||||||
149
src/core/operations/FromBech32.mjs
Normal file
149
src/core/operations/FromBech32.mjs
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
/**
|
||||||
|
* @author Medjedtxm
|
||||||
|
* @copyright Crown Copyright 2025
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Operation from "../Operation.mjs";
|
||||||
|
import { decode } from "../lib/Bech32.mjs";
|
||||||
|
import { toHex } from "../lib/Hex.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* From Bech32 operation
|
||||||
|
*/
|
||||||
|
class FromBech32 extends Operation {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FromBech32 constructor
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.name = "From Bech32";
|
||||||
|
this.module = "Default";
|
||||||
|
this.description = "Bech32 is an encoding scheme primarily used for Bitcoin SegWit addresses (BIP-0173). It uses a 32-character alphabet that excludes easily confused characters (1, b, i, o) and includes a checksum for error detection.<br><br>Bech32m (BIP-0350) is an updated version used for Bitcoin Taproot addresses.<br><br>Auto-detect will attempt Bech32 first, then Bech32m if the checksum fails.<br><br>Output format options allow you to see the Human-Readable Part (HRP) along with the decoded data.";
|
||||||
|
this.infoURL = "https://wikipedia.org/wiki/Bech32";
|
||||||
|
this.inputType = "string";
|
||||||
|
this.outputType = "string";
|
||||||
|
this.args = [
|
||||||
|
{
|
||||||
|
"name": "Encoding",
|
||||||
|
"type": "option",
|
||||||
|
"value": ["Auto-detect", "Bech32", "Bech32m"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Output Format",
|
||||||
|
"type": "option",
|
||||||
|
"value": ["Raw", "Hex", "Bitcoin scriptPubKey", "HRP: Hex", "JSON"]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
this.checks = [
|
||||||
|
{
|
||||||
|
// Bitcoin mainnet SegWit/Taproot addresses
|
||||||
|
pattern: "^bc1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{6,87}$",
|
||||||
|
flags: "i",
|
||||||
|
args: ["Auto-detect", "Hex"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Bitcoin testnet addresses
|
||||||
|
pattern: "^tb1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{6,87}$",
|
||||||
|
flags: "i",
|
||||||
|
args: ["Auto-detect", "Hex"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// AGE public keys
|
||||||
|
pattern: "^age1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{6,87}$",
|
||||||
|
flags: "i",
|
||||||
|
args: ["Auto-detect", "HRP: Hex"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// AGE secret keys
|
||||||
|
pattern: "^AGE-SECRET-KEY-1[QPZRY9X8GF2TVDW0S3JN54KHCE6MUA7L]{6,87}$",
|
||||||
|
flags: "",
|
||||||
|
args: ["Auto-detect", "HRP: Hex"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Litecoin mainnet addresses
|
||||||
|
pattern: "^ltc1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{6,87}$",
|
||||||
|
flags: "i",
|
||||||
|
args: ["Auto-detect", "Hex"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Generic bech32 pattern
|
||||||
|
pattern: "^[a-z]{1,83}1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{6,}$",
|
||||||
|
flags: "i",
|
||||||
|
args: ["Auto-detect", "Hex"]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} input
|
||||||
|
* @param {Object[]} args
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
run(input, args) {
|
||||||
|
const encoding = args[0];
|
||||||
|
const outputFormat = args[1];
|
||||||
|
|
||||||
|
input = input.trim();
|
||||||
|
|
||||||
|
if (input.length === 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const decoded = decode(input, encoding);
|
||||||
|
|
||||||
|
// Format output based on selected option
|
||||||
|
switch (outputFormat) {
|
||||||
|
case "Raw":
|
||||||
|
return decoded.data.map(b => String.fromCharCode(b)).join("");
|
||||||
|
|
||||||
|
case "Hex":
|
||||||
|
return toHex(decoded.data, "");
|
||||||
|
|
||||||
|
case "Bitcoin scriptPubKey": {
|
||||||
|
// Convert to Bitcoin scriptPubKey format as shown in BIP-0173/BIP-0350
|
||||||
|
// Format: [OP_version][length][witness_program]
|
||||||
|
// OP_0 = 0x00, OP_1-OP_16 = 0x51-0x60
|
||||||
|
if (decoded.witnessVersion === null || decoded.data.length < 2) {
|
||||||
|
// Not a SegWit address, fall back to hex
|
||||||
|
return toHex(decoded.data, "");
|
||||||
|
}
|
||||||
|
const witnessVersion = decoded.data[0];
|
||||||
|
const witnessProgram = decoded.data.slice(1);
|
||||||
|
|
||||||
|
// Convert witness version to OP code
|
||||||
|
let opCode;
|
||||||
|
if (witnessVersion === 0) {
|
||||||
|
opCode = 0x00; // OP_0
|
||||||
|
} else if (witnessVersion >= 1 && witnessVersion <= 16) {
|
||||||
|
opCode = 0x50 + witnessVersion; // OP_1 = 0x51, ..., OP_16 = 0x60
|
||||||
|
} else {
|
||||||
|
// Invalid witness version, fall back to hex
|
||||||
|
return toHex(decoded.data, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build scriptPubKey: [OP_version][length][program]
|
||||||
|
const scriptPubKey = [opCode, witnessProgram.length, ...witnessProgram];
|
||||||
|
return toHex(scriptPubKey, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
case "HRP: Hex":
|
||||||
|
return `${decoded.hrp}: ${toHex(decoded.data, "")}`;
|
||||||
|
|
||||||
|
case "JSON":
|
||||||
|
return JSON.stringify({
|
||||||
|
hrp: decoded.hrp,
|
||||||
|
encoding: decoded.encoding,
|
||||||
|
data: toHex(decoded.data, "")
|
||||||
|
}, null, 2);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return toHex(decoded.data, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FromBech32;
|
||||||
@ -43,7 +43,7 @@ class FromHexdump extends Operation {
|
|||||||
*/
|
*/
|
||||||
run(input, args) {
|
run(input, args) {
|
||||||
const output = [],
|
const output = [],
|
||||||
regex = /^\s*(?:[\dA-F]{4,16}h?:?)?[ \t]+((?:[\dA-F]{2} ){1,8}(?:[ \t]|[\dA-F]{2}-)(?:[\dA-F]{2} ){1,8}|(?:[\dA-F]{4} )*[\dA-F]{4}|(?:[\dA-F]{2} )*[\dA-F]{2})/igm;
|
regex = /^\s*(?:[\dA-F]{4,16}h?:?)?[ \t]+((?:[\dA-F]{2} ){1,8}(?:[ \t]|[\dA-F]{2}-)(?:[\dA-F]{2} ){1,8}|(?:[\dA-F]{4} )+(?:[\dA-F]{2})?|(?:[\dA-F]{2} )*[\dA-F]{2})/igm;
|
||||||
let block, line;
|
let block, line;
|
||||||
|
|
||||||
while ((block = regex.exec(input))) {
|
while ((block = regex.exec(input))) {
|
||||||
|
|||||||
84
src/core/operations/FromModhex.mjs
Normal file
84
src/core/operations/FromModhex.mjs
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
/**
|
||||||
|
* @author linuxgemini [ilteris@asenkron.com.tr]
|
||||||
|
* @copyright Crown Copyright 2024
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Operation from "../Operation.mjs";
|
||||||
|
import { FROM_MODHEX_DELIM_OPTIONS, fromModhex } from "../lib/Modhex.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* From Modhex operation
|
||||||
|
*/
|
||||||
|
class FromModhex extends Operation {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FromModhex constructor
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.name = "From Modhex";
|
||||||
|
this.module = "Default";
|
||||||
|
this.description = "Converts a modhex byte string back into its raw value.";
|
||||||
|
this.infoURL = "https://en.wikipedia.org/wiki/YubiKey#ModHex";
|
||||||
|
this.inputType = "string";
|
||||||
|
this.outputType = "byteArray";
|
||||||
|
this.args = [
|
||||||
|
{
|
||||||
|
name: "Delimiter",
|
||||||
|
type: "option",
|
||||||
|
value: FROM_MODHEX_DELIM_OPTIONS
|
||||||
|
}
|
||||||
|
];
|
||||||
|
this.checks = [
|
||||||
|
{
|
||||||
|
pattern: "^(?:[cbdefghijklnrtuv]{2})+$",
|
||||||
|
flags: "i",
|
||||||
|
args: ["None"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: "^[cbdefghijklnrtuv]{2}(?: [cbdefghijklnrtuv]{2})*$",
|
||||||
|
flags: "i",
|
||||||
|
args: ["Space"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: "^[cbdefghijklnrtuv]{2}(?:,[cbdefghijklnrtuv]{2})*$",
|
||||||
|
flags: "i",
|
||||||
|
args: ["Comma"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: "^[cbdefghijklnrtuv]{2}(?:;[cbdefghijklnrtuv]{2})*$",
|
||||||
|
flags: "i",
|
||||||
|
args: ["Semi-colon"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: "^[cbdefghijklnrtuv]{2}(?::[cbdefghijklnrtuv]{2})*$",
|
||||||
|
flags: "i",
|
||||||
|
args: ["Colon"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: "^[cbdefghijklnrtuv]{2}(?:\\n[cbdefghijklnrtuv]{2})*$",
|
||||||
|
flags: "i",
|
||||||
|
args: ["Line feed"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: "^[cbdefghijklnrtuv]{2}(?:\\r\\n[cbdefghijklnrtuv]{2})*$",
|
||||||
|
flags: "i",
|
||||||
|
args: ["CRLF"]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} input
|
||||||
|
* @param {Object[]} args
|
||||||
|
* @returns {byteArray}
|
||||||
|
*/
|
||||||
|
run(input, args) {
|
||||||
|
const delim = args[0] || "Auto";
|
||||||
|
return fromModhex(input, delim, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FromModhex;
|
||||||
@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import Operation from "../Operation.mjs";
|
import Operation from "../Operation.mjs";
|
||||||
import punycode from "punycode";
|
import punycode from "punycode.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* From Punycode operation
|
* From Punycode operation
|
||||||
|
|||||||
@ -23,7 +23,7 @@ class FromQuotedPrintable extends Operation {
|
|||||||
|
|
||||||
this.name = "From Quoted Printable";
|
this.name = "From Quoted Printable";
|
||||||
this.module = "Default";
|
this.module = "Default";
|
||||||
this.description = "Converts QP-encoded text back to standard text.<br><br>e.g. The quoted-printable encoded string <code>hello=20world</code> becomes <code>hello world</code>";
|
this.description = "Converts QP-encoded text back to standard text. This format is a content transfer encoding common in email messages.<br><br>e.g. The quoted-printable encoded string <code>hello=20world</code> becomes <code>hello world</code>";
|
||||||
this.infoURL = "https://wikipedia.org/wiki/Quoted-printable";
|
this.infoURL = "https://wikipedia.org/wiki/Quoted-printable";
|
||||||
this.inputType = "string";
|
this.inputType = "string";
|
||||||
this.outputType = "byteArray";
|
this.outputType = "byteArray";
|
||||||
|
|||||||
254
src/core/operations/GenerateAllChecksums.mjs
Normal file
254
src/core/operations/GenerateAllChecksums.mjs
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
/**
|
||||||
|
* @author r4mos [2k95ljkhg@mozmail.com]
|
||||||
|
* @copyright Crown Copyright 2025
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Operation from "../Operation.mjs";
|
||||||
|
import Adler32Checksum from "./Adler32Checksum.mjs";
|
||||||
|
import CRCChecksum from "./CRCChecksum.mjs";
|
||||||
|
import Fletcher8Checksum from "./Fletcher8Checksum.mjs";
|
||||||
|
import Fletcher16Checksum from "./Fletcher16Checksum.mjs";
|
||||||
|
import Fletcher32Checksum from "./Fletcher32Checksum.mjs";
|
||||||
|
import Fletcher64Checksum from "./Fletcher64Checksum.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate all checksums operation
|
||||||
|
*/
|
||||||
|
class GenerateAllChecksums extends Operation {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GenerateAllChecksums constructor
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.name = "Generate all checksums";
|
||||||
|
this.module = "Crypto";
|
||||||
|
this.description = "Generates all available checksums for the input.";
|
||||||
|
this.infoURL = "https://wikipedia.org/wiki/Checksum";
|
||||||
|
this.inputType = "ArrayBuffer";
|
||||||
|
this.outputType = "string";
|
||||||
|
this.args = [
|
||||||
|
{
|
||||||
|
name: "Length (bits)",
|
||||||
|
type: "option",
|
||||||
|
value: [
|
||||||
|
"All", "3", "4", "5", "6", "7", "8", "10", "11", "12", "13", "14", "15", "16", "17", "21", "24", "30", "31", "32", "40", "64", "82"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Include names",
|
||||||
|
type: "boolean",
|
||||||
|
value: true
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const adler32 = new Adler32Checksum;
|
||||||
|
const crc = new CRCChecksum;
|
||||||
|
const fletcher8 = new Fletcher8Checksum;
|
||||||
|
const fletcher16 = new Fletcher16Checksum;
|
||||||
|
const fletcher32 = new Fletcher32Checksum;
|
||||||
|
const fletcher64 = new Fletcher64Checksum;
|
||||||
|
this.checksums = [
|
||||||
|
{name: "CRC-3/GSM", algo: crc, params: ["CRC-3/GSM"]},
|
||||||
|
{name: "CRC-3/ROHC", algo: crc, params: ["CRC-3/ROHC"]},
|
||||||
|
{name: "CRC-4/G-704", algo: crc, params: ["CRC-4/G-704"]},
|
||||||
|
{name: "CRC-4/INTERLAKEN", algo: crc, params: ["CRC-4/INTERLAKEN"]},
|
||||||
|
{name: "CRC-4/ITU", algo: crc, params: ["CRC-4/ITU"]},
|
||||||
|
{name: "CRC-5/EPC", algo: crc, params: ["CRC-5/EPC"]},
|
||||||
|
{name: "CRC-5/EPC-C1G2", algo: crc, params: ["CRC-5/EPC-C1G2"]},
|
||||||
|
{name: "CRC-5/G-704", algo: crc, params: ["CRC-5/G-704"]},
|
||||||
|
{name: "CRC-5/ITU", algo: crc, params: ["CRC-5/ITU"]},
|
||||||
|
{name: "CRC-5/USB", algo: crc, params: ["CRC-5/USB"]},
|
||||||
|
{name: "CRC-6/CDMA2000-A", algo: crc, params: ["CRC-6/CDMA2000-A"]},
|
||||||
|
{name: "CRC-6/CDMA2000-B", algo: crc, params: ["CRC-6/CDMA2000-B"]},
|
||||||
|
{name: "CRC-6/DARC", algo: crc, params: ["CRC-6/DARC"]},
|
||||||
|
{name: "CRC-6/G-704", algo: crc, params: ["CRC-6/G-704"]},
|
||||||
|
{name: "CRC-6/GSM", algo: crc, params: ["CRC-6/GSM"]},
|
||||||
|
{name: "CRC-6/ITU", algo: crc, params: ["CRC-6/ITU"]},
|
||||||
|
{name: "CRC-7/MMC", algo: crc, params: ["CRC-7/MMC"]},
|
||||||
|
{name: "CRC-7/ROHC", algo: crc, params: ["CRC-7/ROHC"]},
|
||||||
|
{name: "CRC-7/UMTS", algo: crc, params: ["CRC-7/UMTS"]},
|
||||||
|
{name: "CRC-8", algo: crc, params: ["CRC-8"]},
|
||||||
|
{name: "CRC-8/8H2F", algo: crc, params: ["CRC-8/8H2F"]},
|
||||||
|
{name: "CRC-8/AES", algo: crc, params: ["CRC-8/AES"]},
|
||||||
|
{name: "CRC-8/AUTOSAR", algo: crc, params: ["CRC-8/AUTOSAR"]},
|
||||||
|
{name: "CRC-8/BLUETOOTH", algo: crc, params: ["CRC-8/BLUETOOTH"]},
|
||||||
|
{name: "CRC-8/CDMA2000", algo: crc, params: ["CRC-8/CDMA2000"]},
|
||||||
|
{name: "CRC-8/DARC", algo: crc, params: ["CRC-8/DARC"]},
|
||||||
|
{name: "CRC-8/DVB-S2", algo: crc, params: ["CRC-8/DVB-S2"]},
|
||||||
|
{name: "CRC-8/EBU", algo: crc, params: ["CRC-8/EBU"]},
|
||||||
|
{name: "CRC-8/GSM-A", algo: crc, params: ["CRC-8/GSM-A"]},
|
||||||
|
{name: "CRC-8/GSM-B", algo: crc, params: ["CRC-8/GSM-B"]},
|
||||||
|
{name: "CRC-8/HITAG", algo: crc, params: ["CRC-8/HITAG"]},
|
||||||
|
{name: "CRC-8/I-432-1", algo: crc, params: ["CRC-8/I-432-1"]},
|
||||||
|
{name: "CRC-8/I-CODE", algo: crc, params: ["CRC-8/I-CODE"]},
|
||||||
|
{name: "CRC-8/ITU", algo: crc, params: ["CRC-8/ITU"]},
|
||||||
|
{name: "CRC-8/LTE", algo: crc, params: ["CRC-8/LTE"]},
|
||||||
|
{name: "CRC-8/MAXIM", algo: crc, params: ["CRC-8/MAXIM"]},
|
||||||
|
{name: "CRC-8/MAXIM-DOW", algo: crc, params: ["CRC-8/MAXIM-DOW"]},
|
||||||
|
{name: "CRC-8/MIFARE-MAD", algo: crc, params: ["CRC-8/MIFARE-MAD"]},
|
||||||
|
{name: "CRC-8/NRSC-5", algo: crc, params: ["CRC-8/NRSC-5"]},
|
||||||
|
{name: "CRC-8/OPENSAFETY", algo: crc, params: ["CRC-8/OPENSAFETY"]},
|
||||||
|
{name: "CRC-8/ROHC", algo: crc, params: ["CRC-8/ROHC"]},
|
||||||
|
{name: "CRC-8/SAE-J1850", algo: crc, params: ["CRC-8/SAE-J1850"]},
|
||||||
|
{name: "CRC-8/SAE-J1850-ZERO", algo: crc, params: ["CRC-8/SAE-J1850-ZERO"]},
|
||||||
|
{name: "CRC-8/SMBUS", algo: crc, params: ["CRC-8/SMBUS"]},
|
||||||
|
{name: "CRC-8/TECH-3250", algo: crc, params: ["CRC-8/TECH-3250"]},
|
||||||
|
{name: "CRC-8/WCDMA", algo: crc, params: ["CRC-8/WCDMA"]},
|
||||||
|
{name: "Fletcher-8", algo: fletcher8, params: []},
|
||||||
|
{name: "CRC-10/ATM", algo: crc, params: ["CRC-10/ATM"]},
|
||||||
|
{name: "CRC-10/CDMA2000", algo: crc, params: ["CRC-10/CDMA2000"]},
|
||||||
|
{name: "CRC-10/GSM", algo: crc, params: ["CRC-10/GSM"]},
|
||||||
|
{name: "CRC-10/I-610", algo: crc, params: ["CRC-10/I-610"]},
|
||||||
|
{name: "CRC-11/FLEXRAY", algo: crc, params: ["CRC-11/FLEXRAY"]},
|
||||||
|
{name: "CRC-11/UMTS", algo: crc, params: ["CRC-11/UMTS"]},
|
||||||
|
{name: "CRC-12/3GPP", algo: crc, params: ["CRC-12/3GPP"]},
|
||||||
|
{name: "CRC-12/CDMA2000", algo: crc, params: ["CRC-12/CDMA2000"]},
|
||||||
|
{name: "CRC-12/DECT", algo: crc, params: ["CRC-12/DECT"]},
|
||||||
|
{name: "CRC-12/GSM", algo: crc, params: ["CRC-12/GSM"]},
|
||||||
|
{name: "CRC-12/UMTS", algo: crc, params: ["CRC-12/UMTS"]},
|
||||||
|
{name: "CRC-13/BBC", algo: crc, params: ["CRC-13/BBC"]},
|
||||||
|
{name: "CRC-14/DARC", algo: crc, params: ["CRC-14/DARC"]},
|
||||||
|
{name: "CRC-14/GSM", algo: crc, params: ["CRC-14/GSM"]},
|
||||||
|
{name: "CRC-15/CAN", algo: crc, params: ["CRC-15/CAN"]},
|
||||||
|
{name: "CRC-15/MPT1327", algo: crc, params: ["CRC-15/MPT1327"]},
|
||||||
|
{name: "CRC-16", algo: crc, params: ["CRC-16"]},
|
||||||
|
{name: "CRC-16/A", algo: crc, params: ["CRC-16/A"]},
|
||||||
|
{name: "CRC-16/ACORN", algo: crc, params: ["CRC-16/ACORN"]},
|
||||||
|
{name: "CRC-16/ARC", algo: crc, params: ["CRC-16/ARC"]},
|
||||||
|
{name: "CRC-16/AUG-CCITT", algo: crc, params: ["CRC-16/AUG-CCITT"]},
|
||||||
|
{name: "CRC-16/AUTOSAR", algo: crc, params: ["CRC-16/AUTOSAR"]},
|
||||||
|
{name: "CRC-16/B", algo: crc, params: ["CRC-16/B"]},
|
||||||
|
{name: "CRC-16/BLUETOOTH", algo: crc, params: ["CRC-16/BLUETOOTH"]},
|
||||||
|
{name: "CRC-16/BUYPASS", algo: crc, params: ["CRC-16/BUYPASS"]},
|
||||||
|
{name: "CRC-16/CCITT", algo: crc, params: ["CRC-16/CCITT"]},
|
||||||
|
{name: "CRC-16/CCITT-FALSE", algo: crc, params: ["CRC-16/CCITT-FALSE"]},
|
||||||
|
{name: "CRC-16/CCITT-TRUE", algo: crc, params: ["CRC-16/CCITT-TRUE"]},
|
||||||
|
{name: "CRC-16/CCITT-ZERO", algo: crc, params: ["CRC-16/CCITT-ZERO"]},
|
||||||
|
{name: "CRC-16/CDMA2000", algo: crc, params: ["CRC-16/CDMA2000"]},
|
||||||
|
{name: "CRC-16/CMS", algo: crc, params: ["CRC-16/CMS"]},
|
||||||
|
{name: "CRC-16/DARC", algo: crc, params: ["CRC-16/DARC"]},
|
||||||
|
{name: "CRC-16/DDS-110", algo: crc, params: ["CRC-16/DDS-110"]},
|
||||||
|
{name: "CRC-16/DECT-R", algo: crc, params: ["CRC-16/DECT-R"]},
|
||||||
|
{name: "CRC-16/DECT-X", algo: crc, params: ["CRC-16/DECT-X"]},
|
||||||
|
{name: "CRC-16/DNP", algo: crc, params: ["CRC-16/DNP"]},
|
||||||
|
{name: "CRC-16/EN-13757", algo: crc, params: ["CRC-16/EN-13757"]},
|
||||||
|
{name: "CRC-16/EPC", algo: crc, params: ["CRC-16/EPC"]},
|
||||||
|
{name: "CRC-16/EPC-C1G2", algo: crc, params: ["CRC-16/EPC-C1G2"]},
|
||||||
|
{name: "CRC-16/GENIBUS", algo: crc, params: ["CRC-16/GENIBUS"]},
|
||||||
|
{name: "CRC-16/GSM", algo: crc, params: ["CRC-16/GSM"]},
|
||||||
|
{name: "CRC-16/I-CODE", algo: crc, params: ["CRC-16/I-CODE"]},
|
||||||
|
{name: "CRC-16/IBM", algo: crc, params: ["CRC-16/IBM"]},
|
||||||
|
{name: "CRC-16/IBM-3740", algo: crc, params: ["CRC-16/IBM-3740"]},
|
||||||
|
{name: "CRC-16/IBM-SDLC", algo: crc, params: ["CRC-16/IBM-SDLC"]},
|
||||||
|
{name: "CRC-16/IEC-61158-2", algo: crc, params: ["CRC-16/IEC-61158-2"]},
|
||||||
|
{name: "CRC-16/ISO-HDLC", algo: crc, params: ["CRC-16/ISO-HDLC"]},
|
||||||
|
{name: "CRC-16/ISO-IEC-14443-3-A", algo: crc, params: ["CRC-16/ISO-IEC-14443-3-A"]},
|
||||||
|
{name: "CRC-16/ISO-IEC-14443-3-B", algo: crc, params: ["CRC-16/ISO-IEC-14443-3-B"]},
|
||||||
|
{name: "CRC-16/KERMIT", algo: crc, params: ["CRC-16/KERMIT"]},
|
||||||
|
{name: "CRC-16/LHA", algo: crc, params: ["CRC-16/LHA"]},
|
||||||
|
{name: "CRC-16/LJ1200", algo: crc, params: ["CRC-16/LJ1200"]},
|
||||||
|
{name: "CRC-16/LTE", algo: crc, params: ["CRC-16/LTE"]},
|
||||||
|
{name: "CRC-16/M17", algo: crc, params: ["CRC-16/M17"]},
|
||||||
|
{name: "CRC-16/MAXIM", algo: crc, params: ["CRC-16/MAXIM"]},
|
||||||
|
{name: "CRC-16/MAXIM-DOW", algo: crc, params: ["CRC-16/MAXIM-DOW"]},
|
||||||
|
{name: "CRC-16/MCRF4XX", algo: crc, params: ["CRC-16/MCRF4XX"]},
|
||||||
|
{name: "CRC-16/MODBUS", algo: crc, params: ["CRC-16/MODBUS"]},
|
||||||
|
{name: "CRC-16/NRSC-5", algo: crc, params: ["CRC-16/NRSC-5"]},
|
||||||
|
{name: "CRC-16/OPENSAFETY-A", algo: crc, params: ["CRC-16/OPENSAFETY-A"]},
|
||||||
|
{name: "CRC-16/OPENSAFETY-B", algo: crc, params: ["CRC-16/OPENSAFETY-B"]},
|
||||||
|
{name: "CRC-16/PROFIBUS", algo: crc, params: ["CRC-16/PROFIBUS"]},
|
||||||
|
{name: "CRC-16/RIELLO", algo: crc, params: ["CRC-16/RIELLO"]},
|
||||||
|
{name: "CRC-16/SPI-FUJITSU", algo: crc, params: ["CRC-16/SPI-FUJITSU"]},
|
||||||
|
{name: "CRC-16/T10-DIF", algo: crc, params: ["CRC-16/T10-DIF"]},
|
||||||
|
{name: "CRC-16/TELEDISK", algo: crc, params: ["CRC-16/TELEDISK"]},
|
||||||
|
{name: "CRC-16/TMS37157", algo: crc, params: ["CRC-16/TMS37157"]},
|
||||||
|
{name: "CRC-16/UMTS", algo: crc, params: ["CRC-16/UMTS"]},
|
||||||
|
{name: "CRC-16/USB", algo: crc, params: ["CRC-16/USB"]},
|
||||||
|
{name: "CRC-16/V-41-LSB", algo: crc, params: ["CRC-16/V-41-LSB"]},
|
||||||
|
{name: "CRC-16/V-41-MSB", algo: crc, params: ["CRC-16/V-41-MSB"]},
|
||||||
|
{name: "CRC-16/VERIFONE", algo: crc, params: ["CRC-16/VERIFONE"]},
|
||||||
|
{name: "CRC-16/X-25", algo: crc, params: ["CRC-16/X-25"]},
|
||||||
|
{name: "CRC-16/XMODEM", algo: crc, params: ["CRC-16/XMODEM"]},
|
||||||
|
{name: "CRC-16/ZMODEM", algo: crc, params: ["CRC-16/ZMODEM"]},
|
||||||
|
{name: "Fletcher-16", algo: fletcher16, params: []},
|
||||||
|
{name: "CRC-17/CAN-FD", algo: crc, params: ["CRC-17/CAN-FD"]},
|
||||||
|
{name: "CRC-21/CAN-FD", algo: crc, params: ["CRC-21/CAN-FD"]},
|
||||||
|
{name: "CRC-24/BLE", algo: crc, params: ["CRC-24/BLE"]},
|
||||||
|
{name: "CRC-24/FLEXRAY-A", algo: crc, params: ["CRC-24/FLEXRAY-A"]},
|
||||||
|
{name: "CRC-24/FLEXRAY-B", algo: crc, params: ["CRC-24/FLEXRAY-B"]},
|
||||||
|
{name: "CRC-24/INTERLAKEN", algo: crc, params: ["CRC-24/INTERLAKEN"]},
|
||||||
|
{name: "CRC-24/LTE-A", algo: crc, params: ["CRC-24/LTE-A"]},
|
||||||
|
{name: "CRC-24/LTE-B", algo: crc, params: ["CRC-24/LTE-B"]},
|
||||||
|
{name: "CRC-24/OPENPGP", algo: crc, params: ["CRC-24/OPENPGP"]},
|
||||||
|
{name: "CRC-24/OS-9", algo: crc, params: ["CRC-24/OS-9"]},
|
||||||
|
{name: "CRC-30/CDMA", algo: crc, params: ["CRC-30/CDMA"]},
|
||||||
|
{name: "CRC-31/PHILIPS", algo: crc, params: ["CRC-31/PHILIPS"]},
|
||||||
|
{name: "Adler-32", algo: adler32, params: []},
|
||||||
|
{name: "CRC-32", algo: crc, params: ["CRC-32"]},
|
||||||
|
{name: "CRC-32/AAL5", algo: crc, params: ["CRC-32/AAL5"]},
|
||||||
|
{name: "CRC-32/ADCCP", algo: crc, params: ["CRC-32/ADCCP"]},
|
||||||
|
{name: "CRC-32/AIXM", algo: crc, params: ["CRC-32/AIXM"]},
|
||||||
|
{name: "CRC-32/AUTOSAR", algo: crc, params: ["CRC-32/AUTOSAR"]},
|
||||||
|
{name: "CRC-32/BASE91-C", algo: crc, params: ["CRC-32/BASE91-C"]},
|
||||||
|
{name: "CRC-32/BASE91-D", algo: crc, params: ["CRC-32/BASE91-D"]},
|
||||||
|
{name: "CRC-32/BZIP2", algo: crc, params: ["CRC-32/BZIP2"]},
|
||||||
|
{name: "CRC-32/C", algo: crc, params: ["CRC-32/C"]},
|
||||||
|
{name: "CRC-32/CASTAGNOLI", algo: crc, params: ["CRC-32/CASTAGNOLI"]},
|
||||||
|
{name: "CRC-32/CD-ROM-EDC", algo: crc, params: ["CRC-32/CD-ROM-EDC"]},
|
||||||
|
{name: "CRC-32/CKSUM", algo: crc, params: ["CRC-32/CKSUM"]},
|
||||||
|
{name: "CRC-32/D", algo: crc, params: ["CRC-32/D"]},
|
||||||
|
{name: "CRC-32/DECT-B", algo: crc, params: ["CRC-32/DECT-B"]},
|
||||||
|
{name: "CRC-32/INTERLAKEN", algo: crc, params: ["CRC-32/INTERLAKEN"]},
|
||||||
|
{name: "CRC-32/ISCSI", algo: crc, params: ["CRC-32/ISCSI"]},
|
||||||
|
{name: "CRC-32/ISO-HDLC", algo: crc, params: ["CRC-32/ISO-HDLC"]},
|
||||||
|
{name: "CRC-32/JAMCRC", algo: crc, params: ["CRC-32/JAMCRC"]},
|
||||||
|
{name: "CRC-32/MEF", algo: crc, params: ["CRC-32/MEF"]},
|
||||||
|
{name: "CRC-32/MPEG-2", algo: crc, params: ["CRC-32/MPEG-2"]},
|
||||||
|
{name: "CRC-32/NVME", algo: crc, params: ["CRC-32/NVME"]},
|
||||||
|
{name: "CRC-32/PKZIP", algo: crc, params: ["CRC-32/PKZIP"]},
|
||||||
|
{name: "CRC-32/POSIX", algo: crc, params: ["CRC-32/POSIX"]},
|
||||||
|
{name: "CRC-32/Q", algo: crc, params: ["CRC-32/Q"]},
|
||||||
|
{name: "CRC-32/SATA", algo: crc, params: ["CRC-32/SATA"]},
|
||||||
|
{name: "CRC-32/V-42", algo: crc, params: ["CRC-32/V-42"]},
|
||||||
|
{name: "CRC-32/XFER", algo: crc, params: ["CRC-32/XFER"]},
|
||||||
|
{name: "CRC-32/XZ", algo: crc, params: ["CRC-32/XZ"]},
|
||||||
|
{name: "Fletcher-32", algo: fletcher32, params: []},
|
||||||
|
{name: "CRC-40/GSM", algo: crc, params: ["CRC-40/GSM"]},
|
||||||
|
{name: "CRC-64/ECMA-182", algo: crc, params: ["CRC-64/ECMA-182"]},
|
||||||
|
{name: "CRC-64/GO-ECMA", algo: crc, params: ["CRC-64/GO-ECMA"]},
|
||||||
|
{name: "CRC-64/GO-ISO", algo: crc, params: ["CRC-64/GO-ISO"]},
|
||||||
|
{name: "CRC-64/MS", algo: crc, params: ["CRC-64/MS"]},
|
||||||
|
{name: "CRC-64/NVME", algo: crc, params: ["CRC-64/NVME"]},
|
||||||
|
{name: "CRC-64/REDIS", algo: crc, params: ["CRC-64/REDIS"]},
|
||||||
|
{name: "CRC-64/WE", algo: crc, params: ["CRC-64/WE"]},
|
||||||
|
{name: "CRC-64/XZ", algo: crc, params: ["CRC-64/XZ"]},
|
||||||
|
{name: "Fletcher-64", algo: fletcher64, params: []},
|
||||||
|
{name: "CRC-82/DARC", algo: crc, params: ["CRC-82/DARC"]}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {ArrayBuffer} input
|
||||||
|
* @param {Object[]} args
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
run(input, args) {
|
||||||
|
const [length, includeNames] = args;
|
||||||
|
let output = "";
|
||||||
|
this.checksums.forEach(checksum => {
|
||||||
|
const checksumLength = checksum.name.match(new RegExp("-(\\d{1,2})(\\/|$)"))[1];
|
||||||
|
if (length === "All" || length === checksumLength) {
|
||||||
|
const value = checksum.algo.run(new Uint8Array(input), checksum.params || []);
|
||||||
|
output += includeNames ?
|
||||||
|
`${checksum.name}:${" ".repeat(25-checksum.name.length)}${value}\n`:
|
||||||
|
`${value}\n`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GenerateAllChecksums;
|
||||||
@ -22,14 +22,6 @@ import HAS160 from "./HAS160.mjs";
|
|||||||
import Whirlpool from "./Whirlpool.mjs";
|
import Whirlpool from "./Whirlpool.mjs";
|
||||||
import SSDEEP from "./SSDEEP.mjs";
|
import SSDEEP from "./SSDEEP.mjs";
|
||||||
import CTPH from "./CTPH.mjs";
|
import CTPH from "./CTPH.mjs";
|
||||||
import Fletcher8Checksum from "./Fletcher8Checksum.mjs";
|
|
||||||
import Fletcher16Checksum from "./Fletcher16Checksum.mjs";
|
|
||||||
import Fletcher32Checksum from "./Fletcher32Checksum.mjs";
|
|
||||||
import Fletcher64Checksum from "./Fletcher64Checksum.mjs";
|
|
||||||
import Adler32Checksum from "./Adler32Checksum.mjs";
|
|
||||||
import CRC8Checksum from "./CRC8Checksum.mjs";
|
|
||||||
import CRC16Checksum from "./CRC16Checksum.mjs";
|
|
||||||
import CRC32Checksum from "./CRC32Checksum.mjs";
|
|
||||||
import BLAKE2b from "./BLAKE2b.mjs";
|
import BLAKE2b from "./BLAKE2b.mjs";
|
||||||
import BLAKE2s from "./BLAKE2s.mjs";
|
import BLAKE2s from "./BLAKE2s.mjs";
|
||||||
import Streebog from "./Streebog.mjs";
|
import Streebog from "./Streebog.mjs";
|
||||||
@ -114,16 +106,6 @@ class GenerateAllHashes extends Operation {
|
|||||||
{name: "SSDEEP", algo: (new SSDEEP()), inputType: "str"},
|
{name: "SSDEEP", algo: (new SSDEEP()), inputType: "str"},
|
||||||
{name: "CTPH", algo: (new CTPH()), inputType: "str"}
|
{name: "CTPH", algo: (new CTPH()), inputType: "str"}
|
||||||
];
|
];
|
||||||
this.checksums = [
|
|
||||||
{name: "Fletcher-8", algo: (new Fletcher8Checksum), inputType: "byteArray", params: []},
|
|
||||||
{name: "Fletcher-16", algo: (new Fletcher16Checksum), inputType: "byteArray", params: []},
|
|
||||||
{name: "Fletcher-32", algo: (new Fletcher32Checksum), inputType: "byteArray", params: []},
|
|
||||||
{name: "Fletcher-64", algo: (new Fletcher64Checksum), inputType: "byteArray", params: []},
|
|
||||||
{name: "Adler-32", algo: (new Adler32Checksum), inputType: "byteArray", params: []},
|
|
||||||
{name: "CRC-8", algo: (new CRC8Checksum), inputType: "arrayBuffer", params: ["CRC-8"]},
|
|
||||||
{name: "CRC-16", algo: (new CRC16Checksum), inputType: "arrayBuffer", params: []},
|
|
||||||
{name: "CRC-32", algo: (new CRC32Checksum), inputType: "arrayBuffer", params: []}
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -144,14 +126,6 @@ class GenerateAllHashes extends Operation {
|
|||||||
output += this.formatDigest(digest, length, includeNames, hash.name);
|
output += this.formatDigest(digest, length, includeNames, hash.name);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (length === "All") {
|
|
||||||
output += "\nChecksums:\n";
|
|
||||||
this.checksums.forEach(checksum => {
|
|
||||||
digest = this.executeAlgo(checksum.algo, checksum.inputType, checksum.params || []);
|
|
||||||
output += this.formatDigest(digest, length, includeNames, checksum.name);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,16 +5,14 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import Operation from "../Operation.mjs";
|
import Operation from "../Operation.mjs";
|
||||||
import otp from "otp";
|
import * as OTPAuth from "otpauth";
|
||||||
import ToBase32 from "./ToBase32.mjs";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate HOTP operation
|
* Generate HOTP operation
|
||||||
*/
|
*/
|
||||||
class GenerateHOTP extends Operation {
|
class GenerateHOTP extends Operation {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GenerateHOTP constructor
|
*
|
||||||
*/
|
*/
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@ -31,11 +29,6 @@ class GenerateHOTP extends Operation {
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"value": ""
|
"value": ""
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "Key size",
|
|
||||||
"type": "number",
|
|
||||||
"value": 32
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "Code length",
|
"name": "Code length",
|
||||||
"type": "number",
|
"type": "number",
|
||||||
@ -50,21 +43,26 @@ class GenerateHOTP extends Operation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {ArrayBuffer} input
|
*
|
||||||
* @param {Object[]} args
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
*/
|
||||||
run(input, args) {
|
run(input, args) {
|
||||||
const otpObj = otp({
|
const secretStr = new TextDecoder("utf-8").decode(input).trim();
|
||||||
name: args[0],
|
const secret = secretStr ? secretStr.toUpperCase().replace(/\s+/g, "") : "";
|
||||||
keySize: args[1],
|
|
||||||
codeLength: args[2],
|
|
||||||
secret: (new ToBase32).run(input, []).split("=")[0],
|
|
||||||
});
|
|
||||||
const counter = args[3];
|
|
||||||
return `URI: ${otpObj.hotpURL}\n\nPassword: ${otpObj.hotp(counter)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const hotp = new OTPAuth.HOTP({
|
||||||
|
issuer: "",
|
||||||
|
label: args[0],
|
||||||
|
algorithm: "SHA1",
|
||||||
|
digits: args[1],
|
||||||
|
counter: args[2],
|
||||||
|
secret: OTPAuth.Secret.fromBase32(secret)
|
||||||
|
});
|
||||||
|
|
||||||
|
const uri = hotp.toString();
|
||||||
|
const code = hotp.generate();
|
||||||
|
|
||||||
|
return `URI: ${uri}\n\nPassword: ${code}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default GenerateHOTP;
|
export default GenerateHOTP;
|
||||||
|
|||||||
@ -7,16 +7,15 @@
|
|||||||
import Operation from "../Operation.mjs";
|
import Operation from "../Operation.mjs";
|
||||||
import OperationError from "../errors/OperationError.mjs";
|
import OperationError from "../errors/OperationError.mjs";
|
||||||
import Utils from "../Utils.mjs";
|
import Utils from "../Utils.mjs";
|
||||||
import {isImage} from "../lib/FileType.mjs";
|
import { isImage } from "../lib/FileType.mjs";
|
||||||
import {toBase64} from "../lib/Base64.mjs";
|
import { toBase64 } from "../lib/Base64.mjs";
|
||||||
import {isWorkerEnvironment} from "../Utils.mjs";
|
import { isWorkerEnvironment } from "../Utils.mjs";
|
||||||
import Jimp from "jimp/es/index.js";
|
import { Jimp, JimpMime, ResizeStrategy, rgbaToInt } from "jimp";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate Image operation
|
* Generate Image operation
|
||||||
*/
|
*/
|
||||||
class GenerateImage extends Operation {
|
class GenerateImage extends Operation {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GenerateImage constructor
|
* GenerateImage constructor
|
||||||
*/
|
*/
|
||||||
@ -25,27 +24,28 @@ class GenerateImage extends Operation {
|
|||||||
|
|
||||||
this.name = "Generate Image";
|
this.name = "Generate Image";
|
||||||
this.module = "Image";
|
this.module = "Image";
|
||||||
this.description = "Generates an image using the input as pixel values.";
|
this.description =
|
||||||
|
"Generates an image using the input as pixel values.";
|
||||||
this.infoURL = "";
|
this.infoURL = "";
|
||||||
this.inputType = "ArrayBuffer";
|
this.inputType = "ArrayBuffer";
|
||||||
this.outputType = "ArrayBuffer";
|
this.outputType = "ArrayBuffer";
|
||||||
this.presentType = "html";
|
this.presentType = "html";
|
||||||
this.args = [
|
this.args = [
|
||||||
{
|
{
|
||||||
"name": "Mode",
|
name: "Mode",
|
||||||
"type": "option",
|
type: "option",
|
||||||
"value": ["Greyscale", "RG", "RGB", "RGBA", "Bits"]
|
value: ["Greyscale", "RG", "RGB", "RGBA", "Bits"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Pixel Scale Factor",
|
name: "Pixel Scale Factor",
|
||||||
"type": "number",
|
type: "number",
|
||||||
"value": 8,
|
value: 8,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Pixels per row",
|
name: "Pixels per row",
|
||||||
"type": "number",
|
type: "number",
|
||||||
"value": 64,
|
value: 64,
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,21 +67,23 @@ class GenerateImage extends Operation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const bytePerPixelMap = {
|
const bytePerPixelMap = {
|
||||||
"Greyscale": 1,
|
Greyscale: 1,
|
||||||
"RG": 2,
|
RG: 2,
|
||||||
"RGB": 3,
|
RGB: 3,
|
||||||
"RGBA": 4,
|
RGBA: 4,
|
||||||
"Bits": 1/8,
|
Bits: 1 / 8,
|
||||||
};
|
};
|
||||||
|
|
||||||
const bytesPerPixel = bytePerPixelMap[mode];
|
const bytesPerPixel = bytePerPixelMap[mode];
|
||||||
|
|
||||||
if (bytesPerPixel > 0 && input.length % bytesPerPixel !== 0) {
|
if (bytesPerPixel > 0 && input.length % bytesPerPixel !== 0) {
|
||||||
throw new OperationError(`Number of bytes is not a divisor of ${bytesPerPixel}`);
|
throw new OperationError(
|
||||||
|
`Number of bytes is not a divisor of ${bytesPerPixel}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const height = Math.ceil(input.length / bytesPerPixel / width);
|
const height = Math.ceil(input.length / bytesPerPixel / width);
|
||||||
const image = await new Jimp(width, height, (err, image) => {});
|
const image = new Jimp({ width, height });
|
||||||
|
|
||||||
if (isWorkerEnvironment())
|
if (isWorkerEnvironment())
|
||||||
self.sendStatusMessage("Generating image from data...");
|
self.sendStatusMessage("Generating image from data...");
|
||||||
@ -94,8 +96,8 @@ class GenerateImage extends Operation {
|
|||||||
const x = index % width;
|
const x = index % width;
|
||||||
const y = Math.floor(index / width);
|
const y = Math.floor(index / width);
|
||||||
|
|
||||||
const value = curByte[k] === "0" ? 0xFF : 0x00;
|
const value = curByte[k] === "0" ? 0xff : 0x00;
|
||||||
const pixel = Jimp.rgbaToInt(value, value, value, 0xFF);
|
const pixel = rgbaToInt(value, value, value, 0xff);
|
||||||
image.setPixelColor(pixel, x, y);
|
image.setPixelColor(pixel, x, y);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -109,7 +111,7 @@ class GenerateImage extends Operation {
|
|||||||
let red = 0x00;
|
let red = 0x00;
|
||||||
let green = 0x00;
|
let green = 0x00;
|
||||||
let blue = 0x00;
|
let blue = 0x00;
|
||||||
let alpha = 0xFF;
|
let alpha = 0xff;
|
||||||
|
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case "Greyscale":
|
case "Greyscale":
|
||||||
@ -139,10 +141,12 @@ class GenerateImage extends Operation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const pixel = Jimp.rgbaToInt(red, green, blue, alpha);
|
const pixel = rgbaToInt(red, green, blue, alpha);
|
||||||
image.setPixelColor(pixel, x, y);
|
image.setPixelColor(pixel, x, y);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new OperationError(`Error while generating image from pixel values. (${err})`);
|
throw new OperationError(
|
||||||
|
`Error while generating image from pixel values. (${err})`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -151,11 +155,15 @@ class GenerateImage extends Operation {
|
|||||||
if (isWorkerEnvironment())
|
if (isWorkerEnvironment())
|
||||||
self.sendStatusMessage("Scaling image...");
|
self.sendStatusMessage("Scaling image...");
|
||||||
|
|
||||||
image.scaleToFit(width*scale, height*scale, Jimp.RESIZE_NEAREST_NEIGHBOR);
|
image.scaleToFit({
|
||||||
|
w: width * scale,
|
||||||
|
h: height * scale,
|
||||||
|
mode: ResizeStrategy.NEAREST_NEIGHBOR,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const imageBuffer = await image.getBufferAsync(Jimp.MIME_PNG);
|
const imageBuffer = await image.getBuffer(JimpMime.png);
|
||||||
return imageBuffer.buffer;
|
return imageBuffer.buffer;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new OperationError(`Error generating image. (${err})`);
|
throw new OperationError(`Error generating image. (${err})`);
|
||||||
@ -178,7 +186,6 @@ class GenerateImage extends Operation {
|
|||||||
|
|
||||||
return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
|
return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default GenerateImage;
|
export default GenerateImage;
|
||||||
|
|||||||
@ -5,20 +5,17 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import Operation from "../Operation.mjs";
|
import Operation from "../Operation.mjs";
|
||||||
import otp from "otp";
|
import * as OTPAuth from "otpauth";
|
||||||
import ToBase32 from "./ToBase32.mjs";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate TOTP operation
|
* Generate TOTP operation
|
||||||
*/
|
*/
|
||||||
class GenerateTOTP extends Operation {
|
class GenerateTOTP extends Operation {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GenerateTOTP constructor
|
*
|
||||||
*/
|
*/
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.name = "Generate TOTP";
|
this.name = "Generate TOTP";
|
||||||
this.module = "Default";
|
this.module = "Default";
|
||||||
this.description = "The Time-based One-Time Password algorithm (TOTP) is an algorithm that computes a one-time password from a shared secret key and the current time. It has been adopted as Internet Engineering Task Force standard RFC 6238, is the cornerstone of Initiative For Open Authentication (OAUTH), and is used in a number of two-factor authentication systems. A TOTP is an HOTP where the counter is the current time.<br><br>Enter the secret as the input or leave it blank for a random secret to be generated. T0 and T1 are in seconds.";
|
this.description = "The Time-based One-Time Password algorithm (TOTP) is an algorithm that computes a one-time password from a shared secret key and the current time. It has been adopted as Internet Engineering Task Force standard RFC 6238, is the cornerstone of Initiative For Open Authentication (OAUTH), and is used in a number of two-factor authentication systems. A TOTP is an HOTP where the counter is the current time.<br><br>Enter the secret as the input or leave it blank for a random secret to be generated. T0 and T1 are in seconds.";
|
||||||
@ -31,11 +28,6 @@ class GenerateTOTP extends Operation {
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"value": ""
|
"value": ""
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "Key size",
|
|
||||||
"type": "number",
|
|
||||||
"value": 32
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "Code length",
|
"name": "Code length",
|
||||||
"type": "number",
|
"type": "number",
|
||||||
@ -55,22 +47,27 @@ class GenerateTOTP extends Operation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {ArrayBuffer} input
|
*
|
||||||
* @param {Object[]} args
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
*/
|
||||||
run(input, args) {
|
run(input, args) {
|
||||||
const otpObj = otp({
|
const secretStr = new TextDecoder("utf-8").decode(input).trim();
|
||||||
name: args[0],
|
const secret = secretStr ? secretStr.toUpperCase().replace(/\s+/g, "") : "";
|
||||||
keySize: args[1],
|
|
||||||
codeLength: args[2],
|
|
||||||
secret: (new ToBase32).run(input, []).split("=")[0],
|
|
||||||
epoch: args[3],
|
|
||||||
timeSlice: args[4]
|
|
||||||
});
|
|
||||||
return `URI: ${otpObj.totpURL}\n\nPassword: ${otpObj.totp()}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const totp = new OTPAuth.TOTP({
|
||||||
|
issuer: "",
|
||||||
|
label: args[0],
|
||||||
|
algorithm: "SHA1",
|
||||||
|
digits: args[1],
|
||||||
|
period: args[3],
|
||||||
|
epoch: args[2] * 1000, // Convert seconds to milliseconds
|
||||||
|
secret: OTPAuth.Secret.fromBase32(secret)
|
||||||
|
});
|
||||||
|
|
||||||
|
const uri = totp.toString();
|
||||||
|
const code = totp.generate();
|
||||||
|
|
||||||
|
return `URI: ${uri}\n\nPassword: ${code}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default GenerateTOTP;
|
export default GenerateTOTP;
|
||||||
|
|||||||
@ -5,8 +5,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import Operation from "../Operation.mjs";
|
import Operation from "../Operation.mjs";
|
||||||
import crypto from "crypto";
|
import * as uuid from "uuid";
|
||||||
|
import OperationError from "../errors/OperationError.mjs";
|
||||||
/**
|
/**
|
||||||
* Generate UUID operation
|
* Generate UUID operation
|
||||||
*/
|
*/
|
||||||
@ -20,11 +20,38 @@ class GenerateUUID extends Operation {
|
|||||||
|
|
||||||
this.name = "Generate UUID";
|
this.name = "Generate UUID";
|
||||||
this.module = "Crypto";
|
this.module = "Crypto";
|
||||||
this.description = "Generates an RFC 4122 version 4 compliant Universally Unique Identifier (UUID), also known as a Globally Unique Identifier (GUID).<br><br>A version 4 UUID relies on random numbers, in this case generated using <code>window.crypto</code> if available and falling back to <code>Math.random</code> if not.";
|
this.description =
|
||||||
|
"Generates an RFC 9562 (formerly RFC 4122) compliant Universally Unique Identifier (UUID), " +
|
||||||
|
"also known as a Globally Unique Identifier (GUID).<br>" +
|
||||||
|
"<br>" +
|
||||||
|
"We currently support generating the following UUID versions:<br>" +
|
||||||
|
"<ul>" +
|
||||||
|
"<li><strong>v1</strong>: Timestamp-based</li>" +
|
||||||
|
"<li><strong>v3</strong>: Namespace w/ MD5</li>" +
|
||||||
|
"<li><strong>v4</strong>: Random (default)</li>" +
|
||||||
|
"<li><strong>v5</strong>: Namespace w/ SHA-1</li>" +
|
||||||
|
"<li><strong>v6</strong>: Timestamp, reordered</li>" +
|
||||||
|
"<li><strong>v7</strong>: Unix Epoch time-based</li>" +
|
||||||
|
"</ul>" +
|
||||||
|
"UUIDs are generated using the <a href='https://npmjs.org/uuid/'><code>uuid</code><a> package.<br>";
|
||||||
this.infoURL = "https://wikipedia.org/wiki/Universally_unique_identifier";
|
this.infoURL = "https://wikipedia.org/wiki/Universally_unique_identifier";
|
||||||
this.inputType = "string";
|
this.inputType = "string";
|
||||||
this.outputType = "string";
|
this.outputType = "string";
|
||||||
this.args = [];
|
this.args = [
|
||||||
|
{
|
||||||
|
name: "Version",
|
||||||
|
hint: "UUID version",
|
||||||
|
type: "option",
|
||||||
|
value: ["v1", "v3", "v4", "v5", "v6", "v7"],
|
||||||
|
defaultIndex: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Namespace",
|
||||||
|
hint: "UUID namespace (UUID; valid for v3 and v5)",
|
||||||
|
type: "string",
|
||||||
|
value: "1b671a64-40d5-491e-99b0-da01ff1f3341"
|
||||||
|
}
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -33,16 +60,17 @@ class GenerateUUID extends Operation {
|
|||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
run(input, args) {
|
run(input, args) {
|
||||||
const buf = new Uint32Array(4).map(() => {
|
const [version, namespace] = args;
|
||||||
return crypto.randomBytes(4).readUInt32BE(0, true);
|
const hasDesiredVersion = typeof uuid[version] === "function";
|
||||||
});
|
if (!hasDesiredVersion) throw new OperationError("Invalid UUID version");
|
||||||
let i = 0;
|
|
||||||
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) {
|
const requiresNamespace = ["v3", "v5"].includes(version);
|
||||||
const r = (buf[i >> 3] >> ((i % 8) * 4)) & 0xf,
|
if (!requiresNamespace) return uuid[version]();
|
||||||
v = c === "x" ? r : (r & 0x3 | 0x8);
|
|
||||||
i++;
|
const hasValidNamespace = typeof namespace === "string" && uuid.validate(namespace);
|
||||||
return v.toString(16);
|
if (!hasValidNamespace) throw new OperationError("Invalid UUID namespace");
|
||||||
});
|
|
||||||
|
return uuid[version](input, namespace);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
209
src/core/operations/IPv6TransitionAddresses.mjs
Normal file
209
src/core/operations/IPv6TransitionAddresses.mjs
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
/**
|
||||||
|
* @author jb30795 [jb30795@proton.me]
|
||||||
|
* @copyright Crown Copyright 2024
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Operation from "../Operation.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IPv6 Transition Addresses operation
|
||||||
|
*/
|
||||||
|
class IPv6TransitionAddresses extends Operation {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IPv6TransitionAddresses constructor
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.name = "IPv6 Transition Addresses";
|
||||||
|
this.module = "Default";
|
||||||
|
this.description = "Converts IPv4 addresses to their IPv6 Transition addresses. IPv6 Transition addresses can also be converted back into their original IPv4 address. MAC addresses can also be converted into the EUI-64 format, this can them be appended to your IPv6 /64 range to obtain a full /128 address.<br><br>Transition technologies enable translation between IPv4 and IPv6 addresses or tunneling to allow traffic to pass through the incompatible network, allowing the two standards to coexist.<br><br>Only /24 ranges and currently handled. Remove headers to easily copy out results.";
|
||||||
|
this.infoURL = "https://wikipedia.org/wiki/IPv6_transition_mechanism";
|
||||||
|
this.inputType = "string";
|
||||||
|
this.outputType = "string";
|
||||||
|
this.args = [
|
||||||
|
{
|
||||||
|
"name": "Ignore ranges",
|
||||||
|
"type": "boolean",
|
||||||
|
"value": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Remove headers",
|
||||||
|
"type": "boolean",
|
||||||
|
"value": false
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} input
|
||||||
|
* @param {Object[]} args
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
run(input, args) {
|
||||||
|
const XOR = {"0": "2", "1": "3", "2": "0", "3": "1", "4": "6", "5": "7", "6": "4", "7": "5", "8": "a", "9": "b", "a": "8", "b": "9", "c": "e", "d": "f", "e": "c", "f": "d"};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to convert to hex
|
||||||
|
*/
|
||||||
|
function hexify(octet) {
|
||||||
|
return Number(octet).toString(16).padStart(2, "0");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to convert Hex to Int
|
||||||
|
*/
|
||||||
|
function intify(hex) {
|
||||||
|
return parseInt(hex, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function converts IPv4 to IPv6 Transtion address
|
||||||
|
*/
|
||||||
|
function ipTransition(input, range) {
|
||||||
|
let output = "";
|
||||||
|
const HEXIP = input.split(".");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 6to4
|
||||||
|
*/
|
||||||
|
if (!args[1]) {
|
||||||
|
output += "6to4: ";
|
||||||
|
}
|
||||||
|
output += "2002:" + hexify(HEXIP[0]) + hexify(HEXIP[1]) + ":" + hexify(HEXIP[2]);
|
||||||
|
if (range) {
|
||||||
|
output += "00::/40\n";
|
||||||
|
} else {
|
||||||
|
output += hexify(HEXIP[3]) + "::/48\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapped
|
||||||
|
*/
|
||||||
|
if (!args[1]) {
|
||||||
|
output += "IPv4 Mapped: ";
|
||||||
|
}
|
||||||
|
output += "::ffff:" + hexify(HEXIP[0]) + hexify(HEXIP[1]) + ":" + hexify(HEXIP[2]);
|
||||||
|
if (range) {
|
||||||
|
output += "00/120\n";
|
||||||
|
} else {
|
||||||
|
output += hexify(HEXIP[3]) + "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translated
|
||||||
|
*/
|
||||||
|
if (!args[1]) {
|
||||||
|
output += "IPv4 Translated: ";
|
||||||
|
}
|
||||||
|
output += "::ffff:0:" + hexify(HEXIP[0]) + hexify(HEXIP[1]) + ":" + hexify(HEXIP[2]);
|
||||||
|
if (range) {
|
||||||
|
output += "00/120\n";
|
||||||
|
} else {
|
||||||
|
output += hexify(HEXIP[3]) + "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nat64
|
||||||
|
*/
|
||||||
|
if (!args[1]) {
|
||||||
|
output += "Nat 64: ";
|
||||||
|
}
|
||||||
|
output += "64:ff9b::" + hexify(HEXIP[0]) + hexify(HEXIP[1]) + ":" + hexify(HEXIP[2]);
|
||||||
|
if (range) {
|
||||||
|
output += "00/120\n";
|
||||||
|
} else {
|
||||||
|
output += hexify(HEXIP[3]) + "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert MAC to EUI-64
|
||||||
|
*/
|
||||||
|
function macTransition(input) {
|
||||||
|
let output = "";
|
||||||
|
const MACPARTS = input.split(":");
|
||||||
|
if (!args[1]) {
|
||||||
|
output += "EUI-64 Interface ID: ";
|
||||||
|
}
|
||||||
|
const MAC = MACPARTS[0] + MACPARTS[1] + ":" + MACPARTS[2] + "ff:fe" + MACPARTS[3] + ":" + MACPARTS[4] + MACPARTS[5];
|
||||||
|
output += MAC.slice(0, 1) + XOR[MAC.slice(1, 2)] + MAC.slice(2);
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert IPv6 address to its original IPv4 or MAC address
|
||||||
|
*/
|
||||||
|
function unTransition(input) {
|
||||||
|
let output = "";
|
||||||
|
let hextets = "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 6to4
|
||||||
|
*/
|
||||||
|
if (input.startsWith("2002:")) {
|
||||||
|
if (!args[1]) {
|
||||||
|
output += "IPv4: ";
|
||||||
|
}
|
||||||
|
output += String(intify(input.slice(5, 7))) + "." + String(intify(input.slice(7, 9)))+ "." + String(intify(input.slice(10, 12)))+ "." + String(intify(input.slice(12, 14))) + "\n";
|
||||||
|
} else if (input.startsWith("::ffff:") || input.startsWith("0000:0000:0000:0000:0000:ffff:") || input.startsWith("::ffff:0000:") || input.startsWith("0000:0000:0000:0000:ffff:0000:") || input.startsWith("64:ff9b::") || input.startsWith("0064:ff9b:0000:0000:0000:0000:")) {
|
||||||
|
/**
|
||||||
|
* Mapped/Translated/Nat64
|
||||||
|
*/
|
||||||
|
hextets = /:([0-9a-z]{1,4}):[0-9a-z]{1,4}$/.exec(input)[1].padStart(4, "0") + /:([0-9a-z]{1,4})$/.exec(input)[1].padStart(4, "0");
|
||||||
|
if (!args[1]) {
|
||||||
|
output += "IPv4: ";
|
||||||
|
}
|
||||||
|
output += intify(hextets.slice(-8, -7) + hextets.slice(-7, -6)) + "." +intify(hextets.slice(-6, -5) + hextets.slice(-5, -4)) + "." +intify(hextets.slice(-4, -3) + hextets.slice(-3, -2)) + "." +intify(hextets.slice(-2, -1) + hextets.slice(-1,)) + "\n";
|
||||||
|
} else if (input.slice(-12, -7).toUpperCase() === "FF:FE") {
|
||||||
|
/**
|
||||||
|
* EUI-64
|
||||||
|
*/
|
||||||
|
if (!args[1]) {
|
||||||
|
output += "Mac Address: ";
|
||||||
|
}
|
||||||
|
const MAC = (input.slice(-19, -17) + ":" + input.slice(-17, -15) + ":" + input.slice(-14, -12) + ":" + input.slice(-7, -5) + ":" + input.slice(-4, -2) + ":" + input.slice(-2,)).toUpperCase();
|
||||||
|
output += MAC.slice(0, 1) + XOR[MAC.slice(1, 2)] + MAC.slice(2) + "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main
|
||||||
|
*/
|
||||||
|
let output = "";
|
||||||
|
let inputs = input.split("\n");
|
||||||
|
// Remove blank rows
|
||||||
|
inputs = inputs.filter(Boolean);
|
||||||
|
|
||||||
|
for (let i = 0; i < inputs.length; i++) {
|
||||||
|
// if ignore ranges is checked and input is a range, skip
|
||||||
|
if ((args[0] && !inputs[i].includes("/")) || (!args[0])) {
|
||||||
|
if (/^[0-9]{1,3}(?:\.[0-9]{1,3}){3}$/.test(inputs[i])) {
|
||||||
|
output += ipTransition(inputs[i], false);
|
||||||
|
} else if (/\/24$/.test(inputs[i])) {
|
||||||
|
output += ipTransition(inputs[i], true);
|
||||||
|
} else if (/^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$/.test(inputs[i])) {
|
||||||
|
output += macTransition(inputs[i]);
|
||||||
|
} else if (/^((?:[0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,7}:|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?:(?::[0-9a-fA-F]{1,4}){1,6})|:(?:(?::[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(?::[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(?:ffff(?::0{1,4}){0,1}:){0,1}(?:(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])|(?:[0-9a-fA-F]{1,4}:){1,4}:(?:(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/.test(inputs[i])) {
|
||||||
|
output += unTransition(inputs[i]);
|
||||||
|
} else {
|
||||||
|
output = "Enter compressed or expanded IPv6 address, IPv4 address or MAC Address.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IPv6TransitionAddresses;
|
||||||
@ -9,13 +9,12 @@ import OperationError from "../errors/OperationError.mjs";
|
|||||||
import { isImage } from "../lib/FileType.mjs";
|
import { isImage } from "../lib/FileType.mjs";
|
||||||
import { toBase64 } from "../lib/Base64.mjs";
|
import { toBase64 } from "../lib/Base64.mjs";
|
||||||
import { isWorkerEnvironment } from "../Utils.mjs";
|
import { isWorkerEnvironment } from "../Utils.mjs";
|
||||||
import Jimp from "jimp/es/index.js";
|
import { Jimp, JimpMime } from "jimp";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Image Brightness / Contrast operation
|
* Image Brightness / Contrast operation
|
||||||
*/
|
*/
|
||||||
class ImageBrightnessContrast extends Operation {
|
class ImageBrightnessContrast extends Operation {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ImageBrightnessContrast constructor
|
* ImageBrightnessContrast constructor
|
||||||
*/
|
*/
|
||||||
@ -35,15 +34,15 @@ class ImageBrightnessContrast extends Operation {
|
|||||||
type: "number",
|
type: "number",
|
||||||
value: 0,
|
value: 0,
|
||||||
min: -100,
|
min: -100,
|
||||||
max: 100
|
max: 100,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Contrast",
|
name: "Contrast",
|
||||||
type: "number",
|
type: "number",
|
||||||
value: 0,
|
value: 0,
|
||||||
min: -100,
|
min: -100,
|
||||||
max: 100
|
max: 100,
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,14 +76,16 @@ class ImageBrightnessContrast extends Operation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let imageBuffer;
|
let imageBuffer;
|
||||||
if (image.getMIME() === "image/gif") {
|
if (image.mime === "image/gif") {
|
||||||
imageBuffer = await image.getBufferAsync(Jimp.MIME_PNG);
|
imageBuffer = await image.getBuffer(JimpMime.png);
|
||||||
} else {
|
} else {
|
||||||
imageBuffer = await image.getBufferAsync(Jimp.AUTO);
|
imageBuffer = await image.getBuffer(image.mime);
|
||||||
}
|
}
|
||||||
return imageBuffer.buffer;
|
return imageBuffer.buffer;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new OperationError(`Error adjusting image brightness or contrast. (${err})`);
|
throw new OperationError(
|
||||||
|
`Error adjusting image brightness or contrast. (${err})`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,7 +105,6 @@ class ImageBrightnessContrast extends Operation {
|
|||||||
|
|
||||||
return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
|
return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ImageBrightnessContrast;
|
export default ImageBrightnessContrast;
|
||||||
|
|||||||
@ -9,13 +9,12 @@ import OperationError from "../errors/OperationError.mjs";
|
|||||||
import { isImage } from "../lib/FileType.mjs";
|
import { isImage } from "../lib/FileType.mjs";
|
||||||
import { toBase64 } from "../lib/Base64.mjs";
|
import { toBase64 } from "../lib/Base64.mjs";
|
||||||
import { isWorkerEnvironment } from "../Utils.mjs";
|
import { isWorkerEnvironment } from "../Utils.mjs";
|
||||||
import Jimp from "jimp/es/index.js";
|
import { Jimp, JimpMime } from "jimp";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Image Filter operation
|
* Image Filter operation
|
||||||
*/
|
*/
|
||||||
class ImageFilter extends Operation {
|
class ImageFilter extends Operation {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ImageFilter constructor
|
* ImageFilter constructor
|
||||||
*/
|
*/
|
||||||
@ -33,11 +32,8 @@ class ImageFilter extends Operation {
|
|||||||
{
|
{
|
||||||
name: "Filter type",
|
name: "Filter type",
|
||||||
type: "option",
|
type: "option",
|
||||||
value: [
|
value: ["Greyscale", "Sepia"],
|
||||||
"Greyscale",
|
},
|
||||||
"Sepia"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,7 +56,11 @@ class ImageFilter extends Operation {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (isWorkerEnvironment())
|
if (isWorkerEnvironment())
|
||||||
self.sendStatusMessage("Applying " + filterType.toLowerCase() + " filter to image...");
|
self.sendStatusMessage(
|
||||||
|
"Applying " +
|
||||||
|
filterType.toLowerCase() +
|
||||||
|
" filter to image...",
|
||||||
|
);
|
||||||
if (filterType === "Greyscale") {
|
if (filterType === "Greyscale") {
|
||||||
image.greyscale();
|
image.greyscale();
|
||||||
} else {
|
} else {
|
||||||
@ -68,14 +68,16 @@ class ImageFilter extends Operation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let imageBuffer;
|
let imageBuffer;
|
||||||
if (image.getMIME() === "image/gif") {
|
if (image.mime === "image/gif") {
|
||||||
imageBuffer = await image.getBufferAsync(Jimp.MIME_PNG);
|
imageBuffer = await image.getBuffer(JimpMime.png);
|
||||||
} else {
|
} else {
|
||||||
imageBuffer = await image.getBufferAsync(Jimp.AUTO);
|
imageBuffer = await image.getBuffer(image.mime);
|
||||||
}
|
}
|
||||||
return imageBuffer.buffer;
|
return imageBuffer.buffer;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new OperationError(`Error applying filter to image. (${err})`);
|
throw new OperationError(
|
||||||
|
`Error applying filter to image. (${err})`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,7 +97,6 @@ class ImageFilter extends Operation {
|
|||||||
|
|
||||||
return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
|
return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ImageFilter;
|
export default ImageFilter;
|
||||||
|
|||||||
@ -9,13 +9,12 @@ import OperationError from "../errors/OperationError.mjs";
|
|||||||
import { isImage } from "../lib/FileType.mjs";
|
import { isImage } from "../lib/FileType.mjs";
|
||||||
import { toBase64 } from "../lib/Base64.mjs";
|
import { toBase64 } from "../lib/Base64.mjs";
|
||||||
import { isWorkerEnvironment } from "../Utils.mjs";
|
import { isWorkerEnvironment } from "../Utils.mjs";
|
||||||
import Jimp from "jimp/es/index.js";
|
import { Jimp, JimpMime } from "jimp";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Image Hue/Saturation/Lightness operation
|
* Image Hue/Saturation/Lightness operation
|
||||||
*/
|
*/
|
||||||
class ImageHueSaturationLightness extends Operation {
|
class ImageHueSaturationLightness extends Operation {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ImageHueSaturationLightness constructor
|
* ImageHueSaturationLightness constructor
|
||||||
*/
|
*/
|
||||||
@ -24,7 +23,8 @@ class ImageHueSaturationLightness extends Operation {
|
|||||||
|
|
||||||
this.name = "Image Hue/Saturation/Lightness";
|
this.name = "Image Hue/Saturation/Lightness";
|
||||||
this.module = "Image";
|
this.module = "Image";
|
||||||
this.description = "Adjusts the hue / saturation / lightness (HSL) values of an image.";
|
this.description =
|
||||||
|
"Adjusts the hue / saturation / lightness (HSL) values of an image.";
|
||||||
this.infoURL = "";
|
this.infoURL = "";
|
||||||
this.inputType = "ArrayBuffer";
|
this.inputType = "ArrayBuffer";
|
||||||
this.outputType = "ArrayBuffer";
|
this.outputType = "ArrayBuffer";
|
||||||
@ -35,22 +35,22 @@ class ImageHueSaturationLightness extends Operation {
|
|||||||
type: "number",
|
type: "number",
|
||||||
value: 0,
|
value: 0,
|
||||||
min: -360,
|
min: -360,
|
||||||
max: 360
|
max: 360,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Saturation",
|
name: "Saturation",
|
||||||
type: "number",
|
type: "number",
|
||||||
value: 0,
|
value: 0,
|
||||||
min: -100,
|
min: -100,
|
||||||
max: 100
|
max: 100,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Lightness",
|
name: "Lightness",
|
||||||
type: "number",
|
type: "number",
|
||||||
value: 0,
|
value: 0,
|
||||||
min: -100,
|
min: -100,
|
||||||
max: 100
|
max: 100,
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,43 +76,45 @@ class ImageHueSaturationLightness extends Operation {
|
|||||||
if (hue !== 0) {
|
if (hue !== 0) {
|
||||||
if (isWorkerEnvironment())
|
if (isWorkerEnvironment())
|
||||||
self.sendStatusMessage("Changing image hue...");
|
self.sendStatusMessage("Changing image hue...");
|
||||||
image.colour([
|
image.color([
|
||||||
{
|
{
|
||||||
apply: "hue",
|
apply: "hue",
|
||||||
params: [hue]
|
params: [hue],
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
if (saturation !== 0) {
|
if (saturation !== 0) {
|
||||||
if (isWorkerEnvironment())
|
if (isWorkerEnvironment())
|
||||||
self.sendStatusMessage("Changing image saturation...");
|
self.sendStatusMessage("Changing image saturation...");
|
||||||
image.colour([
|
image.color([
|
||||||
{
|
{
|
||||||
apply: "saturate",
|
apply: "saturate",
|
||||||
params: [saturation]
|
params: [saturation],
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
if (lightness !== 0) {
|
if (lightness !== 0) {
|
||||||
if (isWorkerEnvironment())
|
if (isWorkerEnvironment())
|
||||||
self.sendStatusMessage("Changing image lightness...");
|
self.sendStatusMessage("Changing image lightness...");
|
||||||
image.colour([
|
image.color([
|
||||||
{
|
{
|
||||||
apply: "lighten",
|
apply: "lighten",
|
||||||
params: [lightness]
|
params: [lightness],
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
let imageBuffer;
|
let imageBuffer;
|
||||||
if (image.getMIME() === "image/gif") {
|
if (image.mime === "image/gif") {
|
||||||
imageBuffer = await image.getBufferAsync(Jimp.MIME_PNG);
|
imageBuffer = await image.getBuffer(JimpMime.png);
|
||||||
} else {
|
} else {
|
||||||
imageBuffer = await image.getBufferAsync(Jimp.AUTO);
|
imageBuffer = await image.getBuffer(image.mime);
|
||||||
}
|
}
|
||||||
return imageBuffer.buffer;
|
return imageBuffer.buffer;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new OperationError(`Error adjusting image hue / saturation / lightness. (${err})`);
|
throw new OperationError(
|
||||||
|
`Error adjusting image hue / saturation / lightness. (${err})`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,13 +9,12 @@ import OperationError from "../errors/OperationError.mjs";
|
|||||||
import { isImage } from "../lib/FileType.mjs";
|
import { isImage } from "../lib/FileType.mjs";
|
||||||
import { toBase64 } from "../lib/Base64.mjs";
|
import { toBase64 } from "../lib/Base64.mjs";
|
||||||
import { isWorkerEnvironment } from "../Utils.mjs";
|
import { isWorkerEnvironment } from "../Utils.mjs";
|
||||||
import Jimp from "jimp/es/index.js";
|
import { Jimp, JimpMime } from "jimp";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Image Opacity operation
|
* Image Opacity operation
|
||||||
*/
|
*/
|
||||||
class ImageOpacity extends Operation {
|
class ImageOpacity extends Operation {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ImageOpacity constructor
|
* ImageOpacity constructor
|
||||||
*/
|
*/
|
||||||
@ -35,8 +34,8 @@ class ImageOpacity extends Operation {
|
|||||||
type: "number",
|
type: "number",
|
||||||
value: 100,
|
value: 100,
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 100
|
max: 100,
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,10 +62,10 @@ class ImageOpacity extends Operation {
|
|||||||
image.opacity(opacity / 100);
|
image.opacity(opacity / 100);
|
||||||
|
|
||||||
let imageBuffer;
|
let imageBuffer;
|
||||||
if (image.getMIME() === "image/gif") {
|
if (image.mime === "image/gif") {
|
||||||
imageBuffer = await image.getBufferAsync(Jimp.MIME_PNG);
|
imageBuffer = await image.getBuffer(JimpMime.png);
|
||||||
} else {
|
} else {
|
||||||
imageBuffer = await image.getBufferAsync(Jimp.AUTO);
|
imageBuffer = await image.getBuffer(image.mime);
|
||||||
}
|
}
|
||||||
return imageBuffer.buffer;
|
return imageBuffer.buffer;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -90,7 +89,6 @@ class ImageOpacity extends Operation {
|
|||||||
|
|
||||||
return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
|
return `<img src="data:${type};base64,${toBase64(dataArray)}">`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ImageOpacity;
|
export default ImageOpacity;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user