Merge branch 'master' into jwt-decode-verify

This commit is contained in:
hl6226 2026-07-03 21:51:53 -05:00 committed by GitHub
commit 3c56588f5a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 705 additions and 146 deletions

62
.github/workflows/cla-close-stale.yml vendored Normal file
View File

@ -0,0 +1,62 @@
name: Close Stale Unsigned CLA PRs
on:
schedule:
# Runs daily at 01:30 UTC.
- cron: '30 1 * * *'
workflow_dispatch: {}
permissions:
contents: read
pull-requests: write
issues: write
# Configurable intervals (days).
# DAYS_BEFORE_WARNING = grace period before the warning comment.
# DAYS_BEFORE_CLOSURE = further period after the warning before closing.
env:
DAYS_BEFORE_WARNING: 7
DAYS_BEFORE_CLOSURE: 21
jobs:
stale:
runs-on: ubuntu-latest
steps:
- name: Close stale unsigned-CLA PRs
uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 #v10.3.0
with:
# ---- Guards: only act on PRs carrying the CLA label ----
only-labels: 'awaiting cla'
# Never touch issues — PRs only.
days-before-issue-stale: -1
days-before-issue-close: -1
# ---- Timing ----
# DAYS_BEFORE_WARNING: days of inactivity before the warning comment.
days-before-pr-stale: ${{ env.DAYS_BEFORE_WARNING }}
# DAYS_BEFORE_CLOSURE: days after being marked stale before closing.
days-before-pr-close: ${{ env.DAYS_BEFORE_CLOSURE }}
# ---- Warning comment (posted once when marked stale) ----
stale-pr-message: >
As we are unable to accept contributions unless the CLA has
been signed, this PR will be automatically closed if the CLA
is not signed within ${{ env.DAYS_BEFORE_CLOSURE }} days.
# ---- Close comment ----
close-pr-message: >
This PR has been automatically closed as the CLA remains
unsigned. We will be happy to have it reopened if the CLA
is signed subsequently.
# A dedicated marker label so we can track stale state without
# interfering with the "awaiting cla" label.
stale-pr-label: 'cla-stale'
# If the PR is updated after being marked stale, remove the marker
# so the warning-then-close cycle restarts cleanly.
remove-pr-stale-when-updated: true
# Process enough PRs per run for busy repos.
operations-per-run: 200

87
.github/workflows/cla-label.yml vendored Normal file
View File

@ -0,0 +1,87 @@
name: CLA Label Sync
on:
issue_comment:
types: [created, edited]
pull_request_target:
types: [opened, synchronize, reopened]
permissions:
pull-requests: write
issues: write
contents: read
jobs:
sync-label:
# Only run for PRs (issue_comment fires for issues too)
if: >-
github.event_name == 'pull_request_target' ||
(github.event.issue.pull_request != null)
runs-on: ubuntu-latest
steps:
- name: Sync "awaiting cla" label
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 #v9.0.0
env:
AWAITING_LABEL: 'awaiting cla'
# Bot login that posts the CLA comment. Common values:
# 'github-actions[bot]', 'CLAassistant', 'cla-assistant[bot]'
CLA_BOT_LOGINS: 'CLAassistant'
# Regex (case-insensitive) that matches an UNSIGNED CLA comment
NOT_SIGNED_REGEX: 'cla-assistant.io/pull/badge/not_signed'
# Regex (case-insensitive) that matches a SIGNED CLA comment
SIGNED_REGEX: 'cla-assistant.io/pull/badge/signed'
with:
script: |
const awaitingLabel = process.env.AWAITING_LABEL;
const botLogins = process.env.CLA_BOT_LOGINS.split(',').map(s => s.trim().toLowerCase());
const notSigned = new RegExp(process.env.NOT_SIGNED_REGEX, 'i');
const signed = new RegExp(process.env.SIGNED_REGEX, 'i');
// Resolve PR number for either trigger
const prNumber = context.eventName === 'pull_request_target'
? context.payload.pull_request.number
: context.payload.issue.number;
const { owner, repo } = context.repo;
// Pull the full comment history to find the latest CLA bot comment
const comments = await github.paginate(github.rest.issues.listComments, {
owner, repo, issue_number: prNumber, per_page: 100,
});
const claComments = comments.filter(c =>
botLogins.includes((c.user?.login || '').toLowerCase()) &&
(notSigned.test(c.body) || signed.test(c.body))
);
if (claComments.length === 0) {
core.info('No CLA Assistant comment found yet; nothing to do.');
return;
}
const latest = claComments[claComments.length - 1];
const isSigned = signed.test(latest.body) && !notSigned.test(latest.body);
core.info(`Latest CLA comment (id ${latest.id}) => signed=${isSigned}`);
// Current labels
const { data: issue } = await github.rest.issues.get({
owner, repo, issue_number: prNumber,
});
const hasLabel = issue.labels.some(l =>
(typeof l === 'string' ? l : l.name) === awaitingLabel
);
if (isSigned && hasLabel) {
await github.rest.issues.removeLabel({
owner, repo, issue_number: prNumber, name: awaitingLabel,
}).catch(e => core.warning(`removeLabel failed: ${e.message}`));
core.info(`Removed "${awaitingLabel}".`);
} else if (!isSigned && !hasLabel) {
await github.rest.issues.addLabels({
owner, repo, issue_number: prNumber, labels: [awaitingLabel],
});
core.info(`Added "${awaitingLabel}".`);
} else {
core.info('Label already in the correct state.');
}

View File

@ -27,7 +27,7 @@ RUN npm run build
#########################################
# Package static build files into nginx #
#########################################
FROM nginxinc/nginx-unprivileged:stable-alpine@sha256:458ecbec226a23120713b35945bcdf0d6e4ea5bbec60c149ce1deca5d264071b AS cyberchef
FROM nginxinc/nginx-unprivileged:stable-alpine@sha256:fd3314e343bad2de4e1127ef58be122abbfa7e09572fa46ae62fcddb6b3f21c5 AS cyberchef
LABEL maintainer="GCHQ <oss@gchq.gov.uk>"

211
package-lock.json generated
View File

@ -27,7 +27,7 @@
"bootstrap-colorpicker": "^3.4.0",
"bootstrap-material-design": "^4.1.3",
"browserify-zlib": "^0.2.0",
"bson": "^7.3.0",
"bson": "^7.3.1",
"buffer": "^6.0.3",
"cbor": "10.0.12",
"chi-squared": "^1.1.0",
@ -115,12 +115,12 @@
"@babel/preset-env": "^7.29.7",
"@babel/runtime": "^7.29.7",
"@codemirror/commands": "^6.10.4",
"@codemirror/language": "^6.12.3",
"@codemirror/language": "^6.12.4",
"@codemirror/search": "^6.7.1",
"@codemirror/state": "^6.5.4",
"@codemirror/view": "^6.43.2",
"@puppeteer/browsers": "3.0.5",
"autoprefixer": "^10.5.1",
"@codemirror/view": "^6.43.4",
"@puppeteer/browsers": "3.0.6",
"autoprefixer": "^10.5.2",
"babel-loader": "^10.1.1",
"base64-loader": "^1.0.0",
"chromedriver": "^148.0.4",
@ -150,14 +150,14 @@
"mini-css-extract-plugin": "2.10.2",
"modify-source-webpack-plugin": "^4.1.0",
"nightwatch": "^3.16.0",
"postcss": "^8.5.15",
"postcss": "^8.5.16",
"postcss-css-variables": "^0.19.0",
"postcss-import": "^16.1.1",
"postcss-loader": "^8.2.1",
"prompt": "^1.3.0",
"sitemap": "^9.0.1",
"terser": "^5.48.0",
"webpack": "^5.107.2",
"webpack": "^5.108.3",
"webpack-bundle-analyzer": "^5.3.0",
"webpack-dev-server": "^5.2.5",
"webpack-node-externals": "^3.0.0",
@ -1861,9 +1861,9 @@
}
},
"node_modules/@codemirror/language": {
"version": "6.12.3",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.3.tgz",
"integrity": "sha512-QwCZW6Tt1siP37Jet9Tb02Zs81TQt6qQrZR2H+eGMcFsL1zMrk2/b9CLC7/9ieP1fjIUMgviLWMmgiHoJrj+ZA==",
"version": "6.12.4",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.4.tgz",
"integrity": "sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -1898,9 +1898,9 @@
}
},
"node_modules/@codemirror/view": {
"version": "6.43.2",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.2.tgz",
"integrity": "sha512-8kU6WNRYBKV9Sw3cxNz+uSvUvx3tt+1qgupGFPubnbLFDHOgh5qQdIGmXcD7bkA/PROK6LDKVhKMpcY7H++Amg==",
"version": "6.43.4",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.4.tgz",
"integrity": "sha512-YImu23iyKfncJzT7sRy+rEqEhSc8RhOHqDxwy4WzXRKJwYm6iwf/9OJk5ctCAdZ6yi2ZqaGEvmf55fSVqMDrgg==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -4417,9 +4417,9 @@
"license": "MIT"
},
"node_modules/@puppeteer/browsers": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.0.5.tgz",
"integrity": "sha512-xYXNuEQmHNIPWWcbL/skf2KF7seyp7c1xmKFRk3wmdFx7VwBsKVrtOLKs8ecaezsKPsWeF1YsgwIiElAscaryA==",
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.0.6.tgz",
"integrity": "sha512-B/gKoqlFkzhvzsI6jo9K1cZz9o5ypviVv/xu8CwA4grZzyVwN+XfkT+tu8T1zrauuEXv6VhS2oGX+6NL95WcKA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@ -4433,11 +4433,15 @@
"node": ">=22.12.0"
},
"peerDependencies": {
"proxy-agent": ">=8.0.1"
"proxy-agent": ">=8.0.1",
"yauzl": "^2.10.0 || ^3.4.0"
},
"peerDependenciesMeta": {
"proxy-agent": {
"optional": true
},
"yauzl": {
"optional": true
}
}
},
@ -5615,9 +5619,9 @@
"license": "MIT"
},
"node_modules/autoprefixer": {
"version": "10.5.1",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.1.tgz",
"integrity": "sha512-jwM2pcTuCWUoN70FEvf5XrXyDbUgRURK4FnU8v0jWZZYU/KkVvN9T33mu1sVLFY9JW3kTWzKheEpn6xYLRc/VA==",
"version": "10.5.2",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.2.tgz",
"integrity": "sha512-rD5t5DwOjJdmSORcTq64j8MawTC+tbQ+HHqjR4NDumamy/ambn1UJrlKL+KdwujWxMkFjPM3pPHOEA9tl4767Q==",
"dev": true,
"funding": [
{
@ -6377,9 +6381,9 @@
}
},
"node_modules/bson": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/bson/-/bson-7.3.0.tgz",
"integrity": "sha512-WmjjMEwFwZHmGnAb7wn90MhkiT+mTm4x/rLj7dvAPWfwnVWDXhLun2e+UM88MJoDGW624yzZglVX/zTBy9ZZMw==",
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/bson/-/bson-7.3.1.tgz",
"integrity": "sha512-h/C0qe6857pQhcSJHLfsR1uYGj98Ge3wKAD3Ed9KqH3wcVh+BM4Jq4xISD7vs9OPuT07n+q3QQVjslJ286j6ag==",
"license": "Apache-2.0",
"engines": {
"node": ">=20.19.0"
@ -8841,9 +8845,9 @@
}
},
"node_modules/enhanced-resolve": {
"version": "5.22.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.22.0.tgz",
"integrity": "sha512-xYcDWrpELkFzz9SpZ3PlI6Eu6eD93Yf0WLDRxikGhWJ3MAir2SNZTIVCVZqZ/NUyx8AdMc2gT9C0gPiw18kG+A==",
"version": "5.24.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.1.tgz",
"integrity": "sha512-7DdUaTjmNwMcH2gLr1qycesKII3BK4RLy/mdAb7x10Lq7bR4aNKHt1BR1ZALSv0rPM/hF5wYF0PhGop/rJm8vw==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -10257,13 +10261,6 @@
"tslib": "2"
}
},
"node_modules/glob-to-regexp": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
"integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
"dev": true,
"license": "BSD-2-Clause"
},
"node_modules/global-directory": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/global-directory/-/global-directory-5.0.0.tgz",
@ -13365,6 +13362,67 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/minimizer-webpack-plugin": {
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/minimizer-webpack-plugin/-/minimizer-webpack-plugin-5.6.1.tgz",
"integrity": "sha512-DoeAZz8Q1C1znwsUzej1fdoi4jCf7/+Em27ouLqfK/+3m8G+D7yDhUwrc3CNhjSzGUN1kn7Iv4sWmjflQHenpw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.25",
"jest-worker": "^27.4.5",
"schema-utils": "^4.3.0",
"terser": "^5.31.1"
},
"engines": {
"node": ">= 10.13.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
},
"peerDependencies": {
"webpack": "^5.1.0"
},
"peerDependenciesMeta": {
"@minify-html/node": {
"optional": true
},
"@swc/core": {
"optional": true
},
"@swc/css": {
"optional": true
},
"@swc/html": {
"optional": true
},
"clean-css": {
"optional": true
},
"cssnano": {
"optional": true
},
"csso": {
"optional": true
},
"esbuild": {
"optional": true
},
"html-minifier-terser": {
"optional": true
},
"lightningcss": {
"optional": true
},
"postcss": {
"optional": true
},
"uglify-js": {
"optional": true
}
}
},
"node_modules/mocha": {
"version": "10.8.2",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz",
@ -14927,9 +14985,9 @@
}
},
"node_modules/postcss": {
"version": "8.5.15",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
"version": "8.5.16",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz",
"integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==",
"dev": true,
"funding": [
{
@ -17222,67 +17280,6 @@
"node": ">=10"
}
},
"node_modules/terser-webpack-plugin": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.0.tgz",
"integrity": "sha512-Eum+5ajkaOhf5KbM26osvv21kLD7BaGqQ1UA4Ami4arYwylmGUQTgHFpHDdmJod1q4QXa66p0to/FBKID+J1vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.25",
"jest-worker": "^27.4.5",
"schema-utils": "^4.3.0",
"terser": "^5.31.1"
},
"engines": {
"node": ">= 10.13.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
},
"peerDependencies": {
"webpack": "^5.1.0"
},
"peerDependenciesMeta": {
"@minify-html/node": {
"optional": true
},
"@swc/core": {
"optional": true
},
"@swc/css": {
"optional": true
},
"@swc/html": {
"optional": true
},
"clean-css": {
"optional": true
},
"cssnano": {
"optional": true
},
"csso": {
"optional": true
},
"esbuild": {
"optional": true
},
"html-minifier-terser": {
"optional": true
},
"lightningcss": {
"optional": true
},
"postcss": {
"optional": true
},
"uglify-js": {
"optional": true
}
}
},
"node_modules/terser/node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
@ -18050,13 +18047,12 @@
"license": "Apache-2.0"
},
"node_modules/watchpack": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz",
"integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==",
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz",
"integrity": "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==",
"dev": true,
"license": "MIT",
"dependencies": {
"glob-to-regexp": "^0.4.1",
"graceful-fs": "^4.1.2"
},
"engines": {
@ -18094,9 +18090,9 @@
}
},
"node_modules/webpack": {
"version": "5.107.2",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.107.2.tgz",
"integrity": "sha512-v7RhXaJbpMlV0D7hC7lb2EbnxkoeUqf9qhKr6lozx3Q48pmFrqqNRmZFUEGmi7pSwm6fCQ2H1IjvCkHqdpVdjQ==",
"version": "5.108.3",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.108.3.tgz",
"integrity": "sha512-hOpaCHmQVVY66IVTjofnH14IgSdmod2aquSGHGuYig/OIdWge01Hk2Wt988DZcwXumFUT4+FvJY5N+ikl8o/ww==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -18109,19 +18105,18 @@
"acorn-import-phases": "^1.0.3",
"browserslist": "^4.28.1",
"chrome-trace-event": "^1.0.2",
"enhanced-resolve": "^5.22.0",
"enhanced-resolve": "^5.22.2",
"es-module-lexer": "^2.1.0",
"eslint-scope": "5.1.1",
"events": "^3.2.0",
"glob-to-regexp": "^0.4.1",
"graceful-fs": "^4.2.11",
"loader-runner": "^4.3.2",
"mime-db": "^1.54.0",
"minimizer-webpack-plugin": "^5.6.1",
"neo-async": "^2.6.2",
"schema-utils": "^4.3.3",
"tapable": "^2.3.0",
"terser-webpack-plugin": "^5.5.0",
"watchpack": "^2.5.1",
"watchpack": "^2.5.2",
"webpack-sources": "^3.5.0"
},
"bin": {

View File

@ -45,12 +45,12 @@
"@babel/preset-env": "^7.29.7",
"@babel/runtime": "^7.29.7",
"@codemirror/commands": "^6.10.4",
"@codemirror/language": "^6.12.3",
"@codemirror/language": "^6.12.4",
"@codemirror/search": "^6.7.1",
"@codemirror/state": "^6.5.4",
"@codemirror/view": "^6.43.2",
"@puppeteer/browsers": "3.0.5",
"autoprefixer": "^10.5.1",
"@codemirror/view": "^6.43.4",
"@puppeteer/browsers": "3.0.6",
"autoprefixer": "^10.5.2",
"babel-loader": "^10.1.1",
"base64-loader": "^1.0.0",
"chromedriver": "^148.0.4",
@ -80,14 +80,14 @@
"mini-css-extract-plugin": "2.10.2",
"modify-source-webpack-plugin": "^4.1.0",
"nightwatch": "^3.16.0",
"postcss": "^8.5.15",
"postcss": "^8.5.16",
"postcss-css-variables": "^0.19.0",
"postcss-import": "^16.1.1",
"postcss-loader": "^8.2.1",
"prompt": "^1.3.0",
"sitemap": "^9.0.1",
"terser": "^5.48.0",
"webpack": "^5.107.2",
"webpack": "^5.108.3",
"webpack-bundle-analyzer": "^5.3.0",
"webpack-dev-server": "^5.2.5",
"webpack-node-externals": "^3.0.0",
@ -111,7 +111,7 @@
"bootstrap-colorpicker": "^3.4.0",
"bootstrap-material-design": "^4.1.3",
"browserify-zlib": "^0.2.0",
"bson": "^7.3.0",
"bson": "^7.3.1",
"buffer": "^6.0.3",
"cbor": "10.0.12",
"chi-squared": "^1.1.0",

View File

@ -5,6 +5,7 @@
*/
import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs";
import bcrypt from "bcryptjs";
import { isWorkerEnvironment } from "../Utils.mjs";
@ -43,11 +44,16 @@ class BcryptCompare extends Operation {
async run(input, args) {
const hash = args[0];
const match = await bcrypt.compare(input, hash, undefined, p => {
// Progress callback
if (isWorkerEnvironment())
self.sendStatusMessage(`Progress: ${(p * 100).toFixed(0)}%`);
});
let match;
try {
match = await bcrypt.compare(input, hash, undefined, p => {
// Progress callback
if (isWorkerEnvironment())
self.sendStatusMessage(`Progress: ${(p * 100).toFixed(0)}%`);
});
} catch (err) {
throw new OperationError(err.toString());
}
return match ? "Match: " + input : "No match";

View File

@ -5,6 +5,7 @@
*/
import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs";
import * as OTPAuth from "otpauth";
/**
@ -19,7 +20,7 @@ class GenerateHOTP extends Operation {
this.name = "Generate HOTP";
this.module = "Default";
this.description = "The HMAC-based One-Time Password algorithm (HOTP) is an algorithm that computes a one-time password from a shared secret key and an incrementing counter. It has been adopted as Internet Engineering Task Force standard RFC 4226, is the cornerstone of Initiative For Open Authentication (OAUTH), and is used in a number of two-factor authentication systems.<br><br>Enter the secret as the input or leave it blank for a random secret to be generated.";
this.description = "The HMAC-based One-Time Password algorithm (HOTP) is an algorithm that computes a one-time password from a shared secret key and an incrementing counter. It has been adopted as Internet Engineering Task Force standard RFC 4226, is the cornerstone of Initiative For Open Authentication (OAUTH), and is used in a number of two-factor authentication systems.<br><br>Enter the secret as the input or leave it blank for a random secret to be generated. The secret must be a valid base32 string (characters AZ and 27).";
this.infoURL = "https://wikipedia.org/wiki/HMAC-based_One-time_Password_algorithm";
this.inputType = "ArrayBuffer";
this.outputType = "string";
@ -27,17 +28,23 @@ class GenerateHOTP extends Operation {
{
"name": "Name",
"type": "string",
"value": ""
"value": "Account",
"allowEmpty": false
},
{
"name": "Code length",
"type": "number",
"value": 6
"value": 6,
"min": 6,
"max": 8,
"integer": true
},
{
"name": "Counter",
"type": "number",
"value": 0
"value": 0,
"min": 0,
"integer": true
}
];
}
@ -47,7 +54,15 @@ class GenerateHOTP extends Operation {
*/
run(input, args) {
const secretStr = new TextDecoder("utf-8").decode(input).trim();
const secret = secretStr ? secretStr.toUpperCase().replace(/\s+/g, "") : "";
let secret;
try {
secret = secretStr ?
OTPAuth.Secret.fromBase32(secretStr.toUpperCase().replace(/\s+/g, "")) :
new OTPAuth.Secret();
} catch {
throw new OperationError("Invalid secret. The input must be a valid base32 string (characters AZ and 27).");
}
const hotp = new OTPAuth.HOTP({
issuer: "",
@ -55,7 +70,7 @@ class GenerateHOTP extends Operation {
algorithm: "SHA1",
digits: args[1],
counter: args[2],
secret: OTPAuth.Secret.fromBase32(secret)
secret
});
const uri = hotp.toString();

View File

@ -5,6 +5,7 @@
*/
import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs";
import * as OTPAuth from "otpauth";
/**
@ -18,7 +19,7 @@ class GenerateTOTP extends Operation {
super();
this.name = "Generate TOTP";
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. The secret must be a valid base32 string (characters AZ and 27). T0 and T1 are in seconds.";
this.infoURL = "https://wikipedia.org/wiki/Time-based_One-time_Password_algorithm";
this.inputType = "ArrayBuffer";
this.outputType = "string";
@ -26,22 +27,30 @@ class GenerateTOTP extends Operation {
{
"name": "Name",
"type": "string",
"value": ""
"value": "Account",
"allowEmpty": false
},
{
"name": "Code length",
"type": "number",
"value": 6
"value": 6,
"min": 6,
"max": 8,
"integer": true
},
{
"name": "Epoch offset (T0)",
"type": "number",
"value": 0
"value": 0,
"min": 0,
"integer": true
},
{
"name": "Interval (T1)",
"type": "number",
"value": 30
"value": 30,
"min": 1,
"integer": true
}
];
}
@ -51,7 +60,15 @@ class GenerateTOTP extends Operation {
*/
run(input, args) {
const secretStr = new TextDecoder("utf-8").decode(input).trim();
const secret = secretStr ? secretStr.toUpperCase().replace(/\s+/g, "") : "";
let secret;
try {
secret = secretStr ?
OTPAuth.Secret.fromBase32(secretStr.toUpperCase().replace(/\s+/g, "")) :
new OTPAuth.Secret();
} catch {
throw new OperationError("Invalid secret. The input must be a valid base32 string (characters AZ and 27).");
}
const totp = new OTPAuth.TOTP({
issuer: "",
@ -60,7 +77,7 @@ class GenerateTOTP extends Operation {
digits: args[1],
period: args[3],
epoch: args[2] * 1000, // Convert seconds to milliseconds
secret: OTPAuth.Secret.fromBase32(secret)
secret
});
const uri = totp.toString();

View File

@ -36,7 +36,8 @@ class ShowOnMap extends Operation {
{
name: "Input Format",
type: "option",
value: ["Auto"].concat(FORMATS)
value: ["Auto"].concat(FORMATS),
allowEmpty: false
},
{
name: "Input Delimiter",
@ -49,7 +50,8 @@ class ShowOnMap extends Operation {
"Comma",
"Semi-colon",
"Colon"
]
],
allowEmpty: false
}
];
}

View File

@ -43,7 +43,14 @@ class ToBase32 extends Operation {
if (!input) return "";
input = new Uint8Array(input);
const alphabet = args[0] ? Utils.expandAlphRange(args[0]).join("") : "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567=";
const alphabet = args[0] ?
Utils.expandAlphRange(args[0]).join("") :
"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567=";
// Unicode-safe alphabet handling
// Supports BMP + non-BMP characters (emoji, Mahjong tiles, etc.)
const alphabetChars = Array.from(alphabet);
let output = "",
chr1, chr2, chr3, chr4, chr5,
enc1, enc2, enc3, enc4, enc5, enc6, enc7, enc8,
@ -74,10 +81,19 @@ class ToBase32 extends Operation {
enc8 = 32;
}
output += alphabet.charAt(enc1) + alphabet.charAt(enc2) + alphabet.charAt(enc3) +
alphabet.charAt(enc4) + alphabet.charAt(enc5) + alphabet.charAt(enc6) +
alphabet.charAt(enc7) + alphabet.charAt(enc8);
// Preserve original charAt() behavior:
// out-of-range indexes return ""
output +=
(alphabetChars[enc1] || "") +
(alphabetChars[enc2] || "") +
(alphabetChars[enc3] || "") +
(alphabetChars[enc4] || "") +
(alphabetChars[enc5] || "") +
(alphabetChars[enc6] || "") +
(alphabetChars[enc7] || "") +
(alphabetChars[enc8] || "");
}
return output;
}

View File

@ -52,9 +52,15 @@ class ViewBitPlane extends Operation {
if (!isImage(input))
throw new OperationError("Please enter a valid image file.");
const [colour, bit] = args,
parsedImage = await Jimp.read(input),
width = parsedImage.bitmap.width,
const [colour, bit] = args;
let parsedImage;
try {
parsedImage = await Jimp.read(input);
} catch (err) {
throw new OperationError(`Error loading image. (${err})`);
}
const width = parsedImage.bitmap.width,
height = parsedImage.bitmap.height,
colourIndex = COLOUR_OPTIONS.indexOf(colour),
bitIndex = 7 - bit;

View File

@ -6,6 +6,8 @@
import Operation from "../Operation.mjs";
const MAX_LINE_WIDTH = 65536;
/**
* Wrap operation
*/
@ -27,6 +29,9 @@ class Wrap extends Operation {
"name": "Line Width",
"type": "number",
"value": 64,
"min": 1,
"max": MAX_LINE_WIDTH,
"integer": true,
},
];
}

View File

@ -65,6 +65,42 @@ TestRegister.addApiTests([
assert.strictEqual(result.toString(), "493e8136b759370a415ef2cf2f7a69690441ff86592aba082bc2e2e0");
}),
it("Composable Dish: toBase32 should support non-BMP Unicode alphabets", () => {
const alphabet = "🀇🀈🀉🀊🀋🀌🀍🀎🀏🀙🀚🀛🀜🀝🀞🀟🀠🀡🀐🀑🀒🀓🀔🀕🀖🀗🀘🀀🀁🀂🀃🀅";
const result = new Dish("hello")
.apply(toBase32, {alphabet})
.toString();
// Should not contain replacement characters
assert.equal(result.includes("<22>"), false);
// Should contain only symbols from the alphabet
for (const ch of Array.from(result)) {
assert.ok(Array.from(alphabet).includes(ch));
}
// "hello" => 8 Base32 symbols
assert.equal(Array.from(result).length, 8);
}),
it("Composable Dish: toBase32 should omit padding for 32-character Unicode alphabets", () => {
const alphabet = "🀇🀈🀉🀊🀋🀌🀍🀎🀏🀙🀚🀛🀜🀝🀞🀟🀠🀡🀐🀑🀒🀓🀔🀕🀖🀗🀘🀀🀁🀂🀃🀅";
const result = new Dish("hell")
.apply(toBase32, {alphabet})
.toString();
// Should not leak undefined from array indexing
assert.equal(result.includes("undefined"), false);
// Should not contain replacement characters
assert.equal(result.includes("<22>"), false);
// Unpadded Base32 output for 4-byte input should be 7 symbols
assert.equal(Array.from(result).length, 7);
}),
it("Dish translation: ArrayBuffer to ArrayBuffer", () => {
const dish = new Dish(new ArrayBuffer(10), 4);
dish.get("array buffer");

View File

@ -109,6 +109,38 @@ TestRegister.addApiTests([
assert.equal(3 + result, 35);
}),
it("toBase32: should support non-BMP Unicode alphabets", () => {
const alphabet = "🀇🀈🀉🀊🀋🀌🀍🀎🀏🀙🀚🀛🀜🀝🀞🀟🀠🀡🀐🀑🀒🀓🀔🀕🀖🀗🀘🀀🀁🀂🀃🀅";
const result = chef.toBase32("hello", {alphabet}).toString();
// Should not contain replacement characters
assert.equal(result.includes("<22>"), false);
// Should contain only symbols from the alphabet
for (const ch of Array.from(result)) {
assert.ok(Array.from(alphabet).includes(ch));
}
// "hello" => 8 Base32 symbols
assert.equal(Array.from(result).length, 8);
}),
it("toBase32: should omit padding for 32-character Unicode alphabets", () => {
const alphabet = "🀇🀈🀉🀊🀋🀌🀍🀎🀏🀙🀚🀛🀜🀝🀞🀟🀠🀡🀐🀑🀒🀓🀔🀕🀖🀗🀘🀀🀁🀂🀃🀅";
const result = chef.toBase32("hell", {alphabet}).toString();
// Should not leak undefined from array indexing
assert.equal(result.includes("undefined"), false);
// Should not contain replacement characters
assert.equal(result.includes("<22>"), false);
// Unpadded Base32 output for 4-byte input should be 7 symbols
assert.equal(Array.from(result).length, 7);
}),
it("chef.help: should exist", () => {
assert(chef.help);
}),

View File

@ -605,8 +605,9 @@ Top Drawer`, {
it("Generate HOTP", () => {
const result = chef.generateHOTP("JBSWY3DPEHPK3PXP", {
name: "Account",
});
const expected = `URI: otpauth://hotp/?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&counter=0
const expected = `URI: otpauth://hotp/Account?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&counter=0
Password: 282760`;
assert.strictEqual(result.toString(), expected);

View File

@ -172,5 +172,27 @@ TestRegister.addTests([
},
],
},
{
name: "To Base32: should support non-BMP Unicode alphabets",
input: "hello",
expectedOutput: "🀝🀈🀐🀔🀖🀀🀊🀟",
recipeConfig: [
{
op: "To Base32",
args: ["🀇🀈🀉🀊🀋🀌🀍🀎🀏🀙🀚🀛🀜🀝🀞🀟🀠🀡🀐🀑🀒🀓🀔🀕🀖🀗🀘🀀🀁🀂🀃🀅"],
},
],
},
{
name: "To Base32: should omit padding for 32-character Unicode alphabets",
input: "hell",
expectedOutput: "🀝🀈🀐🀔🀖🀀🀇",
recipeConfig: [
{
op: "To Base32",
args: ["🀇🀈🀉🀊🀋🀌🀍🀎🀏🀙🀚🀛🀜🀝🀞🀟🀠🀡🀐🀑🀒🀓🀔🀕🀖🀗🀘🀀🀁🀂🀃🀅"],
},
],
},
]);

View File

@ -993,6 +993,17 @@ TestRegister.addTests([
}
]
},
{
name: "Bcrypt compare: invalid salt version",
input: "password",
expectedOutput: "Error: Invalid salt version: $a",
recipeConfig: [
{
op: "Bcrypt compare",
args: ["$ab$04$K.H1WlFDQ/iIo/PiprT/puwluJ5rzuSE5q8D/Fk3NuLgU2aXiGR9m"]
}
]
},
{
name: "Scrypt: RFC test vector 1",
input: "",

View File

@ -241,6 +241,21 @@ TestRegister.addTests([
}
]
},
{
name: "View Bit Plane: malformed PNG",
input: PNG_HEX.replace("49484452", "49424452"),
expectedOutput: "Error loading image. (Error: unrecognised content at end of stream)",
recipeConfig: [
{
op: "From Hex",
args: ["None"]
},
{
op: "View Bit Plane",
args: ["Red", 0]
}
]
},
{
name: "Randomize Colour Palette",
"input": PNG_HEX,

View File

@ -12,11 +12,176 @@ TestRegister.addTests([
{
name: "Generate HOTP",
input: "JBSWY3DPEHPK3PXP",
expectedOutput: `URI: otpauth://hotp/?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&counter=0\n\nPassword: 282760`,
expectedOutput: `URI: otpauth://hotp/Account?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&counter=0\n\nPassword: 282760`,
recipeConfig: [
{
op: "Generate HOTP",
args: ["", 6, 0], // [Name, Code length, Counter]
args: ["Account", 6, 0], // [Name, Code length, Counter]
},
],
},
{
name: "Generate HOTP - empty name rejected",
input: "JBSWY3DPEHPK3PXP",
expectedOutput: "Name cannot be empty.",
recipeConfig: [
{
op: "Generate HOTP",
args: ["", 6, 0],
},
],
},
{
name: "Generate HOTP - code length below minimum rejected",
input: "JBSWY3DPEHPK3PXP",
expectedOutput: "Code length must be greater than or equal to 6.",
recipeConfig: [
{
op: "Generate HOTP",
args: ["Account", -6, 0],
},
],
},
{
name: "Generate HOTP - code length above maximum rejected",
input: "JBSWY3DPEHPK3PXP",
expectedOutput: "Code length must be less than or equal to 8.",
recipeConfig: [
{
op: "Generate HOTP",
args: ["Account", 9, 0],
},
],
},
{
name: "Generate HOTP - non-integer code length rejected",
input: "JBSWY3DPEHPK3PXP",
expectedOutput: "Code length must be an integer.",
recipeConfig: [
{
op: "Generate HOTP",
args: ["Account", 6.5, 0],
},
],
},
{
name: "Generate HOTP - negative counter rejected",
input: "JBSWY3DPEHPK3PXP",
expectedOutput: "Counter must be greater than or equal to 0.",
recipeConfig: [
{
op: "Generate HOTP",
args: ["Account", 6, -1],
},
],
},
{
name: "Generate HOTP - special characters in name are URI-encoded",
input: "JBSWY3DPEHPK3PXP",
expectedOutput: `URI: otpauth://hotp/user%40example.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&counter=0\n\nPassword: 282760`,
recipeConfig: [
{
op: "Generate HOTP",
args: ["user@example.com", 6, 0],
},
],
},
{
name: "Generate TOTP",
input: "JBSWY3DPEHPK3PXP",
expectedMatch: /^URI: otpauth:\/\/totp\/Account\?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&period=30\n\nPassword: \d{6}$/,
recipeConfig: [
{
op: "Generate TOTP",
args: ["Account", 6, 0, 30], // [Name, Code length, Epoch offset (T0), Interval (T1)]
},
],
},
{
name: "Generate TOTP - empty name rejected",
input: "JBSWY3DPEHPK3PXP",
expectedOutput: "Name cannot be empty.",
recipeConfig: [
{
op: "Generate TOTP",
args: ["", 6, 0, 30],
},
],
},
{
name: "Generate TOTP - code length below minimum rejected",
input: "JBSWY3DPEHPK3PXP",
expectedOutput: "Code length must be greater than or equal to 6.",
recipeConfig: [
{
op: "Generate TOTP",
args: ["Account", -6, 0, 30],
},
],
},
{
name: "Generate TOTP - code length above maximum rejected",
input: "JBSWY3DPEHPK3PXP",
expectedOutput: "Code length must be less than or equal to 8.",
recipeConfig: [
{
op: "Generate TOTP",
args: ["Account", 9, 0, 30],
},
],
},
{
name: "Generate TOTP - non-integer code length rejected",
input: "JBSWY3DPEHPK3PXP",
expectedOutput: "Code length must be an integer.",
recipeConfig: [
{
op: "Generate TOTP",
args: ["Account", 6.5, 0, 30],
},
],
},
{
name: "Generate TOTP - negative interval rejected",
input: "JBSWY3DPEHPK3PXP",
expectedOutput: "Interval (T1) must be greater than or equal to 1.",
recipeConfig: [
{
op: "Generate TOTP",
args: ["Account", 6, 0, -1],
},
],
},
{
name: "Generate TOTP - negative epoch offset rejected",
input: "JBSWY3DPEHPK3PXP",
expectedOutput: "Epoch offset (T0) must be greater than or equal to 0.",
recipeConfig: [
{
op: "Generate TOTP",
args: ["Account", 6, -1, 30],
},
],
},
{
name: "Generate HOTP - invalid base32 secret rejected",
input: "not,valid|base32;input",
expectedOutput: "Invalid secret. The input must be a valid base32 string (characters AZ and 27).",
recipeConfig: [
{
op: "Generate HOTP",
args: ["Account", 6, 0],
},
],
},
{
name: "Generate TOTP - invalid base32 secret rejected",
input: "not,valid|base32;input",
expectedOutput: "Invalid secret. The input must be a valid base32 string (characters AZ and 27).",
recipeConfig: [
{
op: "Generate TOTP",
args: ["Account", 6, 0, 30],
},
],
},

View File

@ -36,4 +36,26 @@ TestRegister.addTests([
},
],
},
{
name: "Show on map: empty input format is rejected",
input: "1, 24",
expectedOutput: "Input Format cannot be empty.",
recipeConfig: [
{
op: "Show on map",
args: [13, "", "Auto"]
},
],
},
{
name: "Show on map: empty input delimiter is rejected",
input: "1, 24",
expectedOutput: "Input Delimiter cannot be empty.",
recipeConfig: [
{
op: "Show on map",
args: [13, "Auto", ""]
},
],
},
]);

View File

@ -40,5 +40,49 @@ TestRegister.addTests([
"args": [10]
},
],
},
{
name: "Wrap rejects zero line width",
input: "hello",
expectedOutput: "Line Width must be greater than or equal to 1.",
recipeConfig: [
{
"op": "Wrap",
"args": [0]
},
],
},
{
name: "Wrap rejects negative line width",
input: "hello",
expectedOutput: "Line Width must be greater than or equal to 1.",
recipeConfig: [
{
"op": "Wrap",
"args": [-1]
},
],
},
{
name: "Wrap rejects non-integer line width",
input: "hello",
expectedOutput: "Line Width must be an integer.",
recipeConfig: [
{
"op": "Wrap",
"args": [1.1]
},
],
},
{
name: "Wrap rejects excessive line width",
input: "hello",
expectedOutput: "Line Width must be less than or equal to 65536.",
recipeConfig: [
{
"op": "Wrap",
"args": [65537]
},
],
}
]);