diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..add0a8d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,57 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + check: + name: Lint, typecheck, test, build (Node ${{ matrix.node-version }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node-version: [20, 22] + steps: + - uses: actions/checkout@v6 + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v6 + with: + node-version: ${{ matrix.node-version }} + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Typecheck + run: npm run typecheck + + - name: Lint + run: npm run lint + + - name: Test + run: npm test + + - name: Build + run: npm run build + + - name: Browser bundle hygiene check + run: | + # Only flag actual imports/requires of node: modules — not string + # literals or comments that happen to contain "node:". + if grep -E "require\(['\"](node:|crypto|buffer|fs)['\"]|import.*from.*['\"]node:|import\s+['\"]node:" dist/index.browser.js; then + echo "::error::Browser bundle contains forbidden Node imports" + exit 1 + fi + + - name: Bundle size budget + run: npm run check:bundle-size + + - name: Smoke test ESM example + run: cd examples/node-esm && npm install --no-package-lock && npm start + + - name: Smoke test CJS example + run: cd examples/node-cjs && npm install --no-package-lock && npm start diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..87c9f3b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,110 @@ +name: Release + +on: + release: + types: [published] + workflow_dispatch: + inputs: + tag: + description: "Release tag to publish (e.g. v2.0.0). Must already exist as a git tag." + required: true + type: string + dry-run: + description: "Skip the actual npm publish (build + verify only)." + required: false + type: boolean + default: false + +permissions: + contents: read + +jobs: + publish: + name: Publish to npm + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ github.event.inputs.tag || github.ref }} + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: 24 + cache: npm + registry-url: https://registry.npmjs.org + + - name: Install dependencies + run: npm ci + + - name: Verify tag matches package.json version and determine npm dist-tag + env: + RELEASE_PRERELEASE: ${{ github.event.release.prerelease }} + run: | + PKG_VERSION="$(node -p "require('./package.json').version")" + RAW_REF="${{ github.event.inputs.tag || github.ref_name }}" + TAG_VERSION="${RAW_REF#v}" + echo "package.json version: $PKG_VERSION" + echo "git tag version: $TAG_VERSION" + if [ "$PKG_VERSION" != "$TAG_VERSION" ]; then + echo "::error::package.json version ($PKG_VERSION) does not match release tag ($TAG_VERSION)" + exit 1 + fi + + # Pre-release semver (contains a hyphen, e.g. 2.0.0-rc.0) → publish under "next". + # Stable (e.g. 2.0.0) → publish under "latest", which is npm's default install target. + if [[ "$PKG_VERSION" == *-* ]]; then + NPM_TAG=next + else + NPM_TAG=latest + fi + echo "npm dist-tag: $NPM_TAG" + echo "NPM_TAG=$NPM_TAG" >> "$GITHUB_ENV" + + # If this run was triggered by a GitHub Release, the "prerelease" flag + # on the release must agree with the version string. This catches the + # easy mistake of forgetting to tick "Set as a pre-release" (or vice + # versa) before publishing the release. + if [ "${{ github.event_name }}" = "release" ]; then + if [ "$NPM_TAG" = "next" ] && [ "$RELEASE_PRERELEASE" != "true" ]; then + echo "::error::Version $PKG_VERSION looks like a pre-release but the GitHub Release is not marked as prerelease" + exit 1 + fi + if [ "$NPM_TAG" = "latest" ] && [ "$RELEASE_PRERELEASE" = "true" ]; then + echo "::error::Version $PKG_VERSION is stable but the GitHub Release is marked as prerelease" + exit 1 + fi + fi + + - name: Typecheck + run: npm run typecheck + + - name: Lint + run: npm run lint + + - name: Test + run: npm test + + - name: Build + run: npm run build + + - name: Browser bundle hygiene check + run: | + if grep -E "require\(['\"](node:|crypto|buffer|fs)['\"]|import.*from.*['\"]node:|import\s+['\"]node:" dist/index.browser.js; then + echo "::error::Browser bundle contains forbidden Node imports" + exit 1 + fi + + - name: Bundle size budget + run: npm run check:bundle-size + + - name: Publish to npm (dry-run) + if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.dry-run == 'true' }} + run: npm publish --provenance --access public --tag "$NPM_TAG" --dry-run + + - name: Publish to npm + if: ${{ github.event_name != 'workflow_dispatch' || github.event.inputs.dry-run != 'true' }} + run: npm publish --provenance --access public --tag "$NPM_TAG" diff --git a/.gitignore b/.gitignore index 9cf9541..8e3bd1f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,17 @@ .DS_Store .idea .tmp -node_modules/ .nyc_output -nbproject/ \ No newline at end of file +nbproject/ +node_modules/ +examples/*/node_modules/ +examples/*/package-lock.json +examples/*/dist/ +examples/*/test-results/ +examples/*/playwright-report/ +examples/*/.playwright/ +dist/ +coverage/ +*.log +.env +.env.local diff --git a/.npmignore b/.npmignore index 925efd3..e1a3713 100644 --- a/.npmignore +++ b/.npmignore @@ -1,6 +1,17 @@ +src +src.legacy test -.travis.yml -.nyc_output -.tmp +examples +.github .idea -.DS_Store \ No newline at end of file +.tmp +.nyc_output +.DS_Store +.nvmrc +.gitignore +biome.json +tsconfig.json +tsconfig.test.json +tsup.config.ts +vitest.workspace.ts +vitest.config.ts diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..2bd5a0a --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 2164c7c..0000000 --- a/.travis.yml +++ /dev/null @@ -1,12 +0,0 @@ -language: node_js -node_js: - - '8' - - 'stable' - -sudo: false - -before_install: - - npm install -g npm@latest - - npm install -g grunt-cli -install: - - npm install diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3d01ea6 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,162 @@ +# Changelog + +## 2.0.0 — TypeScript rewrite, native `node:crypto` fast paths, security audit fixes + +Full rewrite of the v1 library in TypeScript with the same public API. The +node bundle now routes RSA primitives through `node:crypto` whenever +possible, and the browser bundle defaults to native `BigInt`. + +### Performance — node bundle uses `node:crypto` natively + +- **Keygen** uses `crypto.generateKeyPairSync`. 2048-bit drops from ~2.3 s + to ~50 ms (~45× faster) on modern hardware; 1024-bit from ~240 ms to + ~10 ms. +- **PKCS#1 v1.5 and PSS sign/verify** use `crypto.sign` / `crypto.verify`. + PSS-SHA256 sign on 2048-bit drops from ~17 ms to sub-millisecond. +- OAEP encrypt / PKCS#1 v1.5 encrypt route through `NodeNativeEngine` — + also `node:crypto`-backed. + +### Performance — browser bundle defaults to native `BigInt` + +A drop-in BigInteger implementation lives at +[src/bigint/big-integer-native.ts](src/bigint/big-integer-native.ts) and +uses ES2020's native `BigInt`. The browser bundle picks it at load time; +the node bundle stays on the audited jsbn implementation. Round-trips +identically through every API; switch back to jsbn with +`new NodeRSA(key, { bigIntImpl: 'jsbn' })` if you ever need to. + +| 2048-bit, JS path | jsbn | native | speedup | +|---|---|---|---| +| PSS-SHA256 sign | ~16 ms | ~4 ms | **~4×** | +| PSS-SHA256 verify | ~0.4 ms | ~0.08 ms | **~5×** | + +The `bigIntImpl` option (also accepted by `setOptions`) must be set +BEFORE the key is imported or generated; switching it on an instance +that already has key components throws, since the two implementations +produce incompatible BigInteger instances. + +The browser bundle silently falls back to jsbn on runtimes without +`globalThis.BigInt` (i.e. pre-2020 environments). No user action needed. + +### Breaking changes + +- **Min Node.js is now 20**. v1 worked back to Node 8.11; v2 requires Node 20+ + for `node:crypto`, `globalThis.crypto`, and modern ESM features. +- **Module shape**: ESM-first. `package.json#exports` provides a dual ESM/CJS + layout — `import NodeRSA from 'node-rsa'` for ESM, + `require('node-rsa').default` for CommonJS. +- **Browser default return type is `Uint8Array`** (was `Buffer` via polyfill). + Node return type stays `Buffer` (which extends `Uint8Array`, so most + existing consumers continue to work). Internal byte handling is `Uint8Array` + end-to-end; the Node entry wraps results as `Buffer` at the API boundary. +- **No more `Buffer` or `crypto` shims for browsers**. The browser bundle + contains zero Node-builtin imports — verified in CI by a `grep` over + `dist/index.browser.js`. Bundlers (Vite, Webpack 5, Rollup, esbuild, Parcel) + resolve the browser entry via package.json conditional exports. +- **`setOptions({environment})` is a deprecated no-op**. Build-time platform + conditions decide the runtime now. The option still forces the pure-JS + engine path when set to `'browser'`, preserving the v1 semantic that the + 61-case test suite relies on. A one-time `console.warn` is emitted on use. +- **MD4 is Node-only and provider-gated**. OpenSSL 3 (Node 17+) doesn't load + the legacy provider by default, so `crypto.createHash('md4')` throws. v2 + probes at module load and reports md4 as unsupported when the provider is + absent. The browser bundle never supports MD4. +- **`asn1` npm dependency removed**. PKCS#1, PKCS#8, and OpenSSH formats now + use a small in-tree DER reader/writer (~150 lines, under + [`src/asn1/`](src/asn1)). Byte-identical to v1 output for every fixture key. +- **Native PKCS#1 v1.5 `privateDecrypt` is routed through the JS engine on + modern Node**. Node has security-deprecated raw PKCS#1 v1.5 decryption (CVE + response); v2 transparently falls back to the pure-JS implementation so the + call still succeeds. The byte-for-byte plaintext is identical. +- **Default signing scheme switched from `pkcs1` (PKCS#1 v1.5) to `pss` + (RSASSA-PSS).** PSS is the modern best-practice signing scheme — it has + a tighter security reduction and is preferred by RFC 8017 / NIST for new + code. Existing signatures produced under the v1 default remain verifiable + by passing `signingScheme: 'pkcs1'` explicitly. + + ```ts + // To keep v1's PKCS#1 v1.5 default explicit: + const key = new NodeRSA(null, { signingScheme: 'pkcs1' }); + const sig = key.sign('msg'); + ``` + + The bare-hash shorthand `setOptions({ signingScheme: 'sha256' })` + also resolves to `pss-sha256` (was `pkcs1-sha256` in v1). Set + `signingScheme: 'pkcs1-sha256'` explicitly to keep v1 behaviour. +- **Custom MGF for PSS now throws on the node bundle.** `node:crypto` + only supports MGF1 with hash equal to the signing hash. If you need a + non-default MGF, force the pure-JS path with + `setOptions({ environment: 'browser' })`. +- **Hash algorithms unsupported by the local OpenSSL build now throw at + sign/verify time on the node bundle.** Functionally equivalent to v1 + (the JS scheme delegated to `nodeBackend.digest` which also threw) — + only the error wording and call-site changed. + +### Security fixes (no API change) + +- **OAEP decode is now constant-time** (RFC 8017 §7.1.2). Closes a Manger- + style padding-oracle (~10⁵ queries to recover plaintext given a timing + oracle). Includes a missing `Y == 0x00` check on the leading byte and a + post-decode message-length bound. +- **PKCS#1 v1.5 decode is now constant-time** internally (RFC 8017 §7.2.2, + Bleichenbacher / ROBOT). Closes the internal differential timing oracle; + the valid/invalid binary oracle inherent to PKCS#1 v1.5 remains — use + OAEP for untrusted ciphertexts (the README has a security note). +- **PSS verify is now constant-time** (RFC 8017 §9.1.2 step 11). +- **Private-key operations are blinded** (Kocher 1996 / Brumley-Boneh + 2003 defence). Fresh `r ← random coprime to n` masks the variable-time + `modPow` from any timing leak on `d`, `dmp1`, or `dmq1`. +- **Miller-Rabin uses CSPRNG witnesses** in [2, n-2] (was `Math.random()` + over a 168-element fixed table — adversarial-pseudoprime risk) and now + honours the caller's full round count (was silently halved). Keygen + picks adaptive rounds by bit length per FIPS 186-4 Table C.3. +- **Public exponent validated on import**: `1 < e` with e odd + (RFC 8017 §3.1). +- **RSA primitive bounds-check**: `0 ≤ x < n` enforced in both + `$doPrivate` and `$doPublic` (RFC 8017 §3.2). `verify()` translates + the resulting out-of-range error to "invalid signature" per §8.x. +- **Imported private keys are CRT-consistency-checked**: `n = p·q`, + `dp ≡ d mod (p−1)`, `dq ≡ d mod (q−1)`, `q·coeff ≡ 1 mod p`, + `e·dp ≡ 1 mod (p−1)`, `e·dq ≡ 1 mod (q−1)`. Closes a Boneh-DeMillo- + Lipton fault-injection vector on crafted PEM/PKCS#8/OpenSSH files. +- **`generate(B)` refuses `B < 512`** (cryptographically broken) and + emits a one-shot `console.warn` for `B < 2048` (below NIST SP 800-56B + §6.1.6.2 minimum). +- **Fermat-distance defence**: keygen rejects p, q pairs with + `|p − q| < 2^(B/2 − 100)` (FIPS 186-4 §B.3.6). +- **CRT recombination is branch-free**: removed the data-dependent + `while (xp < xq) xp += p` loop. +- **OpenSSH parser hardening**: `SshReader.readString` bounds-checks + before `subarray`; the two private-section checkints (`checkint1`, + `checkint2`) are now validated for equality. +- **PKCS#8 parser hardening**: outer version validated against + {0, 1} (RFC 5958 §2); inner PKCS#1 version restricted to two-prime + (RFC 8017 §A.1.2); algorithm OID whitelist with clear diagnostics for + PSS-only (1.2.840.113549.1.1.10) and OAEP-only (.1.1.7) misuse. + +### Added + +- TypeScript types for every public surface (`NodeRSAOptions`, + `EncryptionSchemeOptions`, `SigningSchemeOptions`, `HashAlg`, format + string union types). +- `@noble/hashes` runtime dependency for synchronous SHA/MD/RIPEMD digests + in the browser bundle. ~6 KB gzipped, audited, zero-dep. +- Bundle size budget (CI-enforced): + - `dist/index.browser.js`: <100 KB raw / <30 KB gzipped (currently 90/21) + - `dist/index.node.{js,cjs}`: <120 KB raw / <35 KB gzipped (currently 94/22) + +### Internal + +- Modern tooling: `tsup` for build (esbuild), `vitest` for tests (with a + workspace running every spec in two projects — `node` and + `browser-emulated`), `biome` for lint+format, strict TypeScript with + `noUncheckedIndexedAccess` / `exactOptionalPropertyTypes` / + `noImplicitOverride` etc. +- 1006 test cases across 27 files. The v1 mocha suite of 61 `it()` blocks is + ported verbatim and runs in both vitest projects. +- The legacy v1 source is preserved in `src.legacy/` during the port and + deleted on the v2.0.0 release commit. + +## 1.1.1 and earlier + +See git history. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..534fe7e --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2014-2026 rzcoder +Copyright (c) 2003-2009 Tom Wu (BigInteger / RSA primitives, see src/bigint/big-integer.ts) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 0000000..5793af5 --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,260 @@ +# Migrating from node-rsa v1 to v2.0 + +## TL;DR + +Bump Node to ≥ 20; follow Steps 1–10 below. The biggest behavioural change +to watch for is the **default signing scheme switch from PKCS#1 v1.5 to +RSASSA-PSS**. If you rely on the default (i.e. call `key.sign(...)` without +an explicit `signingScheme`), either accept the switch (recommended — PSS +is modern best practice) or pin to v1.5 explicitly. See +[Step 7](#step-7-adjust-to-the-new-default-signing-scheme). + +For browser bundlers (Vite, Webpack 5, Rollup, esbuild, Parcel), **delete +any Buffer/crypto/process shims** you set up for v1 — they're no longer +needed and may interfere. + +## Behaviour changes at a glance + +| Concern | v1 | v2 | +|---|---|---| +| Return types on Node | `Buffer` | `Buffer` (unchanged; `Buffer` extends `Uint8Array`) | +| Return types on browser | needed Buffer polyfill | `Uint8Array` | +| Module system | CJS | ESM + CJS dual | +| Min Node version | 8.11 | 20 | +| Browser crypto | `crypto-browserify` shim required | Built-in: `@noble/hashes` + `globalThis.crypto.getRandomValues` | +| `setOptions({environment})` | controls runtime branching | Deprecated no-op (still forces JS engine when set to `'browser'`) | +| MD4 in browser | available via shim | not available (Web Crypto subset) | +| `asn1` npm dependency | required | replaced with in-tree DER reader/writer | +| Default signing scheme | `pkcs1` (PKCS#1 v1.5) | `pss` (RSASSA-PSS) | +| Custom MGF for PSS on Node | accepted (pure-JS path) | throws — force JS path via `setOptions({environment:'browser'})` | + +## Step 1: bump Node + +```jsonc +// package.json (yours) +"engines": { "node": ">=20" } +``` + +v2 uses `node:crypto`, `globalThis.crypto`, ESM `import.meta`, and a strict +TypeScript configuration that targets ES2022. Node 18 reached end-of-life on +2025-04-30; v2 drops it. + +## Step 2: update the import + +```ts +// v1 (CommonJS) +const NodeRSA = require('node-rsa'); + +// v2 ESM +import NodeRSA from 'node-rsa'; + +// v2 CJS still works +const NodeRSA = require('node-rsa').default; +``` + +The CJS `.default` is the standard ESM-to-CJS interop shape. + +## Step 3: review return types + +If you call `.toString(...)` on the result of `encrypt`/`decrypt`/`sign`, +keep going — `Buffer` is still returned on Node. For browser bundles, the +return type is `Uint8Array`, which does not have `.toString('base64')`. +Replace with explicit encoding: + +```ts +// v1 (browser, with polyfill) +const b64 = key.encrypt('hi').toString('base64'); + +// v2 (browser, no polyfill) +const b64 = key.encrypt('hi', 'base64'); +// or +const bytes = key.encrypt('hi'); +const b64 = btoa(String.fromCharCode(...bytes)); +``` + +The encoding parameter has always existed on v1 too — using it now is +forward-compatible with both. + +## Step 4: remove Buffer / crypto shims from your bundler + +For Vite: + +```diff +// vite.config.ts +- import { nodePolyfills } from 'vite-plugin-node-polyfills'; + export default defineConfig({ +- plugins: [nodePolyfills({ include: ['buffer', 'crypto'] })], ++ plugins: [], + }); +``` + +For Webpack: + +```diff +// webpack.config.js + resolve: { +- fallback: { buffer: require.resolve('buffer/'), crypto: require.resolve('crypto-browserify') }, ++ fallback: { buffer: false, crypto: false }, + }, +``` + +The browser entry of `node-rsa@2` has no Node-builtin imports — CI greps the +bundle to keep it that way. + +## Step 5: drop the `environment` option (optional) + +`setOptions({ environment: 'browser' })` still works as a force-JS-engine +hint, but it logs a one-time deprecation warning. If you only need that +because you used to run `environment: 'browser'` in a Node test for cross- +compat checks, the new vitest workspace pattern is a better fit. + +If you genuinely relied on `'iojs'` as an environment value, switch to +`'node'`. v2 has no third platform. + +## Step 6: re-check your hash algorithm selection + +* **MD4 in browser**: was never supported in v1's browser whitelist either + — no change. +* **MD4 on Node**: v2 probes for OpenSSL legacy-provider availability at + module load. If your Node runtime doesn't load it, MD4 throws. Switch to + SHA-256 for any signing scheme that's not pinned by a wire-protocol + requirement. + +The node bundle additionally routes sign/verify through +`node:crypto.{sign,verify}`, which **throws synchronously** for any hash +the local OpenSSL build doesn't support (most commonly `md4`, sometimes +`ripemd160`). v1 and v2's pure-JS schemes already threw at digest time — +only the error wording and call-site differ. If you need a hash OpenSSL +doesn't support but `@noble/hashes` does, force the JS path with +`setOptions({ environment: 'browser' })`. + +## Step 7: adjust to the new default signing scheme + +`DEFAULT_SIGNING_SCHEME` is `'pss'` in v2 (was `'pkcs1'` in v1). This +matters in two cases: + +1. **You call `key.sign()` without an explicit scheme and someone else + verifies the signature.** They'll be expecting PSS, not PKCS#1 v1.5. + Either coordinate the switch or pin explicitly: + + ```ts + const key = new NodeRSA(pem, { signingScheme: 'pkcs1' }); + // ^^^^^^^^^^^^^^^^^^^^^^^^ + // keeps v1's PKCS#1 v1.5 default; remove this line to accept the v2 default + ``` + +2. **You used the bare-hash shorthand** `signingScheme: 'sha256'`. The + shorthand maps to "default scheme + that hash", so in v1 it meant + `pkcs1-sha256`; in v2 it means `pss-sha256`. Spell out the scheme to + keep behaviour: + + ```ts + new NodeRSA(null, { signingScheme: 'pkcs1-sha256' }); + ``` + +Round-trip in-process (`key.sign()` then `key.verify()` on the same +`NodeRSA` instance, no `setOptions` between them) is unaffected — both +sides see the same default and round-trip cleanly. Cross-version +verification (sign in v1, verify in v2, or vice versa) requires an +explicit scheme on at least one side. + +## Step 8: if you used a custom MGF for PSS + +The node bundle calls `node:crypto.sign` / `verify` for PSS, and +`node:crypto` only supports MGF1 with hash equal to the signing hash. +Passing `signingScheme: { scheme: 'pss', mgf: ... }` on Node throws at +scheme construction. To keep a custom MGF, opt back into the pure-JS path: + +```ts +key.setOptions({ environment: 'browser' }); // forces JsEngine + JS schemes +``` + +If you forced `environment: 'browser'` at runtime, sign/verify revert to +the pure-JS schemes alongside the engine — that path is unchanged. + +## Step 9: re-run your tests + +The 61-case mocha suite from v1 is ported 1-to-1 in v2's +`test/node-rsa.spec.ts` (run on both Node and browser-emulated workspaces) +and is green. If your tests still pass, you're done. + +## Step 10: TypeScript types — drop `@types/node-rsa` + +v2 ships native TypeScript types. **Uninstall `@types/node-rsa`** — keeping +it shadows the bundled `.d.ts` and produces stale errors: + +```sh +npm uninstall @types/node-rsa +``` + +The runtime and value-level API is unchanged, but the type surface differs +from `@types/node-rsa@1.1.4` in a few places. The fixes are mechanical. + +### Module shape + +DT used `export = NodeRSA`, which carried a namespace alongside the class. +v2 uses `export default NodeRSA` plus named type exports. + +```ts +// v1 + @types/node-rsa +import NodeRSA = require('node-rsa'); +const opts: NodeRSA.Options = { signingScheme: 'pkcs1-sha256' }; +const key: NodeRSA.Key = pemString; + +// v2 +import NodeRSA, { type NodeRSAOptions, type Key } from 'node-rsa'; +const opts: NodeRSAOptions = { signingScheme: 'pkcs1-sha256' }; +const key: Key = pemString; +``` + +The `NodeRSA.` namespace pattern no longer resolves — every type +must be imported by name. + +### One renamed type + +Only the `Options` interface is renamed — DT scoped it under the namespace +(`NodeRSA.Options`), v2 exports it flat with the class-prefix: + +| `@types/node-rsa@1.1.4` | v2 | +|---|---| +| `NodeRSA.Options` | `NodeRSAOptions` | + +Every other DT type name is preserved as-is: `Key`, `Data`, `KeyBits`, +`KeyComponentsPrivate`, `KeyComponentsPublic`, `Format`, `FormatPem`, +`FormatDer`, `FormatComponentsPrivate`, `FormatComponentsPublic`, `Encoding`, +`EncryptionScheme`, `SigningScheme`, `SigningSchemeHash`, `HashingAlgorithm`, +`AdvancedSigningScheme`, `AdvancedSigningSchemePSS`, `AdvancedSigningSchemePKCS1`, +`AdvancedEncryptionScheme`, `AdvancedEncryptionSchemePKCS1`, +`AdvancedEncryptionSchemePKCS1OAEP`. Import them by name. + +### `Encoding` is narrower + +DT declared `Encoding = "ascii" | "utf8" | "utf16le" | "ucs2" | "latin1" | +"base64" | "hex" | "binary" | "buffer"`. v2 declares `Encoding = 'buffer' +| 'binary' | 'latin1' | 'hex' | 'base64' | 'utf8'`. + +The dropped values (`ascii`, `utf16le`, `ucs2`) were not actually wired +end-to-end in v1 — passing them ran the data through a base64 fallback that +mangled non-ASCII input. v2 removes the type so the silent fallback can't +be reached. If you were genuinely using `'utf16le'` and getting expected +results, you weren't; switch to `'utf8'` or pre-encode the buffer yourself. + +`'binary'` and `'latin1'` are interchangeable in v2 and map to the same +runtime path. + +### Return types + +`Buffer` on Node, `Uint8Array` on browser — already covered in +[Step 3](#step-3-review-return-types). DT always returned `Buffer`; if you +relied on Buffer-only methods (`.toString('base64')`, `.write`, etc.) on a +browser build, switch to the explicit-encoding overloads or polyfill `Buffer`. + +## When to keep using v1 + +- You depend on `node-rsa` working under Node ≤ 18. +- You import from `node-rsa/src/...` deep-paths. v2 doesn't expose that + layout. +- You patched the v1 source for a private fix. The v2 file structure is + different; reapply against v2 or wait for the v2.x port of your patch. + +`npm install node-rsa@^1.1` continues to work for those cases. diff --git a/README.md b/README.md index bb666d2..e59cd88 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,47 @@ # Node-RSA -Node.js RSA library
-Based on jsbn library from Tom Wu http://www-cs-students.stanford.edu/~tjw/jsbn/ +[![npm version](https://img.shields.io/npm/v/node-rsa.svg)](https://www.npmjs.com/package/node-rsa) +[![CI](https://github.com/rzcoder/node-rsa/actions/workflows/ci.yml/badge.svg)](https://github.com/rzcoder/node-rsa/actions/workflows/ci.yml) +[![npm downloads](https://img.shields.io/npm/dm/node-rsa.svg)](https://www.npmjs.com/package/node-rsa) +[![license](https://img.shields.io/npm/l/node-rsa.svg)](https://github.com/rzcoder/node-rsa/blob/master/LICENSE) -* Pure JavaScript -* No needed OpenSSL +RSA library for Node.js and browsers. + +* Pure TypeScript +* Works in Node.js and modern browsers (no Buffer/crypto polyfills needed) * Generating keys -* Supports long messages for encrypt/decrypt +* Encrypting and decrypting, with long-message support * Signing and verifying +## Table of contents + +* [Example](#example) +* [Installing](#installing) +* [Usage](#usage) + * [Create instance](#create-instance) + * [Import/Export keys](#importexport-keys) + * [Properties](#properties) + * [Encrypting/Decrypting](#encryptingdecrypting) + * [Signing/Verifying](#signingverifying) +* [Browser usage](#browser-usage) +* [Security notes](#security-notes) +* [Migrating](#migrating) +* [Changelog](#changelog) +* [License](#license) +* [Acknowledgements](#acknowledgements) + ## Example -```javascript -const NodeRSA = require('node-rsa'); -const key = new NodeRSA({b: 512}); +```ts +import NodeRSA from 'node-rsa'; + +const key = new NodeRSA({ b: 2048 }); const text = 'Hello RSA!'; const encrypted = key.encrypt(text, 'base64'); -console.log('encrypted: ', encrypted); +console.log('encrypted:', encrypted); const decrypted = key.decrypt(encrypted, 'utf8'); -console.log('decrypted: ', decrypted); +console.log('decrypted:', decrypted); ``` ## Installing @@ -27,7 +49,7 @@ console.log('decrypted: ', decrypted); ```shell npm install node-rsa ``` -> Requires nodejs >= 8.11.1 +> Requires Node.js >= 20. For browsers, any bundler with conditional-exports support (Vite, Webpack 5, Rollup, esbuild, Parcel) picks the browser entry automatically. ### Testing @@ -35,308 +57,242 @@ npm install node-rsa npm test ``` -## Work environment - -This library developed and tested primary for Node.js, but it still can work in browsers with [browserify](http://browserify.org/). - ## Usage ### Create instance -```javascript -const NodeRSA = require('node-rsa'); + +```ts +import NodeRSA from 'node-rsa'; const key = new NodeRSA([keyData, [format]], [options]); ``` -* keyData — `{string|buffer|object}` — parameters for generating key or the key in one of supported formats.
-* format — `{string}` — format for importing key. See more details about formats in [Export/Import](#importexport-keys) section.
-* options — `{object}` — additional settings. +* `keyData` — `string | Uint8Array | object` — key data in one of the supported formats, or a generation spec. +* `format` — `string` — format id for importing the key. See [Import/Export](#importexport-keys). +* `options` — `object` — additional settings (below). #### Options -You can specify some options by second/third constructor argument, or over `key.setOptions()` method. -* environment — working environment (default autodetect): - * `'browser'` — will run pure js implementation of RSA algorithms. - * `'node'` for `nodejs >= 0.10.x or io.js >= 1.x` — provide some native methods like sign/verify and encrypt/decrypt. -* encryptionScheme — padding scheme for encrypt/decrypt. Can be `'pkcs1_oaep'` or `'pkcs1'`. Default `'pkcs1_oaep'`. -* signingScheme — scheme used for signing and verifying. Can be `'pkcs1'` or `'pss'` or 'scheme-hash' format string (eg `'pss-sha1'`). Default `'pkcs1-sha256'`, or, if chosen pss: `'pss-sha1'`. +You can pass options as the second/third constructor argument, or later via `key.setOptions()`. -> *Notice:* This lib supporting next hash algorithms: `'md5'`, `'ripemd160'`, `'sha1'`, `'sha256'`, `'sha512'` in browser and node environment and additional `'md4'`, `'sha'`, `'sha224'`, `'sha384'` in node only. +* `environment` — `'node'` or `'browser'`. Auto-detected from the loaded bundle; the option mainly exists to force the pure-JS engine on Node (`setOptions({ environment: 'browser' })`), bypassing the `node:crypto` fast path — useful if you need PSS with a custom MGF. +* `bigIntImpl` — `'native'` or `'jsbn'`. The browser bundle defaults to native ES2020 `BigInt`; the Node bundle uses jsbn. Switch only **before** importing/generating a key; switching on a populated instance throws. +* `signingScheme` — scheme used for `sign` / `verify`. One of `'pss'` (default), `'pkcs1'`, or a `'scheme-hash'` shorthand (e.g. `'pkcs1-sha512'`). Object form is also accepted: `{ scheme: 'pss', hash: 'sha256', saltLength?: number, mgf?: MaskGenerationFunction }`. Default hash is `sha256`. +* `encryptionScheme` — padding scheme for `encrypt` / `decrypt`. One of `'pkcs1_oaep'` (default) or `'pkcs1'`. Object form: `{ scheme: 'pkcs1_oaep', hash: 'sha1', mgf?, label? }`. Default OAEP hash is `sha1`. -Some [advanced options info](https://github.com/rzcoder/node-rsa/wiki/Advanced-options) +> *Note:* Supported hash algorithms are `'md5'`, `'ripemd160'`, `'sha1'`, `'sha256'`, `'sha512'` in both environments, plus `'md4'`, `'sha224'`, `'sha384'` on Node. `'md4'` additionally requires running Node with `--openssl-legacy-provider`. -#### Creating "empty" key -```javascript +#### Creating an "empty" key + +```ts const key = new NodeRSA(); ``` -#### Generate new 512bit-length key -```javascript -const key = new NodeRSA({b: 512}); +#### Generate new 2048-bit key + +```ts +const key = new NodeRSA({ b: 2048 }); ``` -Also you can use next method: +Or: -```javascript +```ts key.generateKeyPair([bits], [exp]); ``` -* bits — `{int}` — key size in bits. 2048 by default. -* exp — `{int}` — public exponent. 65537 by default. +* `bits` — `number` — key size in bits. 2048 by default. +* `exp` — `number` — public exponent. 65537 by default. #### Load key from PEM string -```javascript -const key = new NodeRSA('-----BEGIN RSA PRIVATE KEY-----\n'+ - 'MIIBOQIBAAJAVY6quuzCwyOWzymJ7C4zXjeV/232wt2ZgJZ1kHzjI73wnhQ3WQcL\n'+ - 'DFCSoi2lPUW8/zspk0qWvPdtp6Jg5Lu7hwIDAQABAkBEws9mQahZ6r1mq2zEm3D/\n'+ - 'VM9BpV//xtd6p/G+eRCYBT2qshGx42ucdgZCYJptFoW+HEx/jtzWe74yK6jGIkWJ\n'+ - 'AiEAoNAMsPqwWwTyjDZCo9iKvfIQvd3MWnmtFmjiHoPtjx0CIQCIMypAEEkZuQUi\n'+ - 'pMoreJrOlLJWdc0bfhzNAJjxsTv/8wIgQG0ZqI3GubBxu9rBOAM5EoA4VNjXVigJ\n'+ - 'QEEk1jTkp8ECIQCHhsoq90mWM/p9L5cQzLDWkTYoPI49Ji+Iemi2T5MRqwIgQl07\n'+ - 'Es+KCn25OKXR/FJ5fu6A6A+MptABL3r8SEjlpLc=\n'+ - '-----END RSA PRIVATE KEY-----'); +```ts +const key = new NodeRSA( + '-----BEGIN RSA PRIVATE KEY-----\n' + + 'MIIBOQIBAAJAVY6quuzCwyOWzymJ7C4zXjeV/232wt2ZgJZ1kHzjI73wnhQ3WQcL\n' + + 'DFCSoi2lPUW8/zspk0qWvPdtp6Jg5Lu7hwIDAQABAkBEws9mQahZ6r1mq2zEm3D/\n' + + 'VM9BpV//xtd6p/G+eRCYBT2qshGx42ucdgZCYJptFoW+HEx/jtzWe74yK6jGIkWJ\n' + + 'AiEAoNAMsPqwWwTyjDZCo9iKvfIQvd3MWnmtFmjiHoPtjx0CIQCIMypAEEkZuQUi\n' + + 'pMoreJrOlLJWdc0bfhzNAJjxsTv/8wIgQG0ZqI3GubBxu9rBOAM5EoA4VNjXVigJ\n' + + 'QEEk1jTkp8ECIQCHhsoq90mWM/p9L5cQzLDWkTYoPI49Ji+Iemi2T5MRqwIgQl07\n' + + 'Es+KCn25OKXR/FJ5fu6A6A+MptABL3r8SEjlpLc=\n' + + '-----END RSA PRIVATE KEY-----', +); ``` ### Import/Export keys -```javascript + +```ts key.importKey(keyData, [format]); key.exportKey([format]); ``` -* keyData — `{string|buffer}` — may be: - * key in PEM string - * Buffer containing PEM string - * Buffer containing DER encoded data - * Object contains key components -* format — `{string}` — format id for export/import. +* `keyData` — may be: + * PEM string (or a `Uint8Array`/`Buffer` containing one) + * `Uint8Array` containing raw DER + * object with raw key components +* `format` — `string` — format id for import/export. #### Format string syntax -Format string composed of several parts: `scheme-[key_type]-[output_type]`
-Scheme — NodeRSA supports multiple format schemes for import/export keys: +`scheme-[key_type]-[output_type]` - * `'pkcs1'` — public key starts from `'-----BEGIN RSA PUBLIC KEY-----'` header and private key starts from `'-----BEGIN RSA PRIVATE KEY-----'` header - * `'pkcs8'` — public key starts from `'-----BEGIN PUBLIC KEY-----'` header and private key starts from `'-----BEGIN PRIVATE KEY-----'` header - * `'openssh'` — public key starts from `'ssh-rsa'` header and private key starts from `'-----BEGIN OPENSSH PRIVATE KEY-----'` header - * `'components'` — use it for import/export key from/to raw components (see example below). For private key, importing data should contain all private key components, for public key: only public exponent (`e`) and modulus (`n`). All components (except `e`) should be Buffer, `e` could be Buffer or just normal Number. +**Scheme** — node-rsa supports several: -Key type — can be `'private'` or `'public'`. Default `'private'`
-Output type — can be: + * `'pkcs1'` — public PEM starts with `-----BEGIN RSA PUBLIC KEY-----`, private with `-----BEGIN RSA PRIVATE KEY-----`. + * `'pkcs8'` — public PEM starts with `-----BEGIN PUBLIC KEY-----`, private with `-----BEGIN PRIVATE KEY-----`. + * `'openssh'` — public starts with `ssh-rsa`, private with `-----BEGIN OPENSSH PRIVATE KEY-----`. + * `'components'` — raw modulus/exponent and CRT params. For a private key all components must be present; for a public key only `n` and `e`. All components are `Uint8Array` except `e`, which may be `Uint8Array` or a plain `number`. - * `'pem'` — Base64 encoded string with header and footer. Used by default. - * `'der'` — Binary encoded key data. +**Key type** — `'private'` (default) or `'public'`. -> *Notice:* For import, if *keyData* is PEM string or buffer containing string, you can do not specify format, but if you provide *keyData* as DER you must specify it in format string. +**Output type**: + + * `'pem'` — base64 PEM string with header/footer. Used by default. + * `'der'` — `Uint8Array` of binary DER. + +> *Note:* For import, if `keyData` is a PEM string (or a `Uint8Array` containing PEM), you can omit `format`. If it's raw DER, you must specify the format string. **Shortcuts and examples** - * `'private'` or `'pkcs1'` or `'pkcs1-private'` == `'pkcs1-private-pem'` — private key encoded in pcks1 scheme as pem string. - * `'public'` or `'pkcs8-public'` == `'pkcs8-public-pem'` — public key encoded in pcks8 scheme as pem string. - * `'pkcs8'` or `'pkcs8-private'` == `'pkcs8-private-pem'` — private key encoded in pcks8 scheme as pem string. - * `'pkcs1-der'` == `'pkcs1-private-der'` — private key encoded in pcks1 scheme as binary buffer. - * `'pkcs8-public-der'` — public key encoded in pcks8 scheme as binary buffer. + + * `'private'` ≡ `'pkcs1'` ≡ `'pkcs1-private'` ≡ `'pkcs1-private-pem'` — private key, PKCS#1, PEM. + * `'public'` ≡ `'pkcs8-public'` ≡ `'pkcs8-public-pem'` — public key, PKCS#8, PEM. + * `'pkcs8'` ≡ `'pkcs8-private'` ≡ `'pkcs8-private-pem'` — private key, PKCS#8, PEM. + * `'pkcs1-der'` ≡ `'pkcs1-private-der'` — private key, PKCS#1, binary DER. + * `'pkcs8-public-der'` — public key, PKCS#8, binary DER. **Code example** -```javascript +```ts const keyData = '-----BEGIN PUBLIC KEY----- ... -----END PUBLIC KEY-----'; key.importKey(keyData, 'pkcs8'); const publicDer = key.exportKey('pkcs8-public-der'); const privateDer = key.exportKey('pkcs1-der'); ``` -```javascript +```ts +const hex = (s: string) => + Uint8Array.from(s.match(/.{2}/g)!.map((b) => parseInt(b, 16))); + key.importKey({ - n: Buffer.from('0086fa9ba066685845fc03833a9699c8baefb53cfbf19052a7f10f1eaa30488cec1ceb752bdff2df9fad6c64b3498956e7dbab4035b4823c99a44cc57088a23783', 'hex'), - e: 65537, - d: Buffer.from('5d2f0dd982596ef781affb1cab73a77c46985c6da2aafc252cea3f4546e80f40c0e247d7d9467750ea1321cc5aa638871b3ed96d19dcc124916b0bcb296f35e1', 'hex'), - p: Buffer.from('00c59419db615e56b9805cc45673a32d278917534804171edcf925ab1df203927f', 'hex'), - q: Buffer.from('00aee3f86b66087abc069b8b1736e38ad6af624f7ea80e70b95f4ff2bf77cd90fd', 'hex'), - dmp1: Buffer.from('008112f5a969fcb56f4e3a4c51a60dcdebec157ee4a7376b843487b53844e8ac85', 'hex'), - dmq1: Buffer.from('1a7370470e0f8a4095df40922a430fe498720e03e1f70d257c3ce34202249d21', 'hex'), - coeff: Buffer.from('00b399675e5e81506b729a777cc03026f0b2119853dfc5eb124610c0ab82999e45', 'hex') + n: hex('0086fa9ba066685845fc03833a9699c8baefb53cfbf19052a7f10f1eaa30488cec1ceb752bdff2df9fad6c64b3498956e7dbab4035b4823c99a44cc57088a23783'), + e: 65537, + d: hex('5d2f0dd982596ef781affb1cab73a77c46985c6da2aafc252cea3f4546e80f40c0e247d7d9467750ea1321cc5aa638871b3ed96d19dcc124916b0bcb296f35e1'), + p: hex('00c59419db615e56b9805cc45673a32d278917534804171edcf925ab1df203927f'), + q: hex('00aee3f86b66087abc069b8b1736e38ad6af624f7ea80e70b95f4ff2bf77cd90fd'), + dmp1: hex('008112f5a969fcb56f4e3a4c51a60dcdebec157ee4a7376b843487b53844e8ac85'), + dmq1: hex('1a7370470e0f8a4095df40922a430fe498720e03e1f70d257c3ce34202249d21'), + coeff: hex('00b399675e5e81506b729a777cc03026f0b2119853dfc5eb124610c0ab82999e45'), }, 'components'); + const publicComponents = key.exportKey('components-public'); console.log(publicComponents); /* -{ n: , - e: 65537 -} +{ n: Uint8Array(65) [0, 134, 250, 155, 160, 102, 104, 88, 69, 252, 3, 131, 58, ... ], + e: 65537 } */ ``` -If you want to only import the public key use `'components-public'` as an option: +To import only the public part use `'components-public'`: -```javascript +```ts key.importKey({ - n: Buffer.from('0086fa9ba066685845fc03833a9699c8baefb53cfbf19052a7f10f1eaa30488cec1ceb752bdff2df9fad6c64b3498956e7dbab4035b4823c99a44cc57088a23783', 'hex'), - e: 65537, + n: hex('0086fa9ba066685845fc03833a9699c8baefb53cfbf19052a7f10f1eaa30488cec1ceb752bdff2df9fad6c64b3498956e7dbab4035b4823c99a44cc57088a23783'), + e: 65537, }, 'components-public'); ``` +> *Note:* `Buffer` is a `Uint8Array` subclass on Node, so any code that passes `Buffer.from(...)` still works — the types document the cross-platform shape. + ### Properties #### Key testing -```javascript +```ts key.isPrivate(); key.isPublic([strict]); ``` -strict — `{boolean}` — if true method will return false if key pair have private exponent. Default `false`. +`strict` — `boolean` — if `true`, returns `false` when the key pair also contains a private exponent. Default `false`. -```javascript +```ts key.isEmpty(); ``` -Return `true` if key pair doesn't have any data. +Returns `true` if the instance has no key data. #### Key info -```javascript +```ts key.getKeySize(); ``` -Return key size in bits. +Returns key size in bits. -```javascript +```ts key.getMaxMessageSize(); ``` -Return max data size for encrypt in bytes. +Returns the max data size for a single encrypt operation, in bytes (scheme-dependent). -### Encrypting/decrypting +### Encrypting/Decrypting -```javascript +```ts key.encrypt(buffer, [encoding], [source_encoding]); -key.encryptPrivate(buffer, [encoding], [source_encoding]); // use private key for encryption +key.encryptPrivate(buffer, [encoding], [source_encoding]); // encrypt with private key ``` -Return encrypted data.
+Returns the encrypted data. -* buffer — `{buffer}` — data for encrypting, may be string, Buffer, or any object/array. Arrays and objects will encoded to JSON string first.
-* encoding — `{string}` — encoding for output result, may be `'buffer'`, `'binary'`, `'hex'` or `'base64'`. Default `'buffer'`.
-* source_encoding — `{string}` — source encoding, works only with string buffer. Can take standard Node.js Buffer encodings (hex, utf8, base64, etc). `'utf8'` by default.
+* `buffer` — data to encrypt. May be `string`, `Uint8Array` (or `Buffer` on Node), `number`, plain object, or array. Objects and arrays are JSON-stringified first. +* `encoding` — output encoding: `'buffer'` (default — returns `Uint8Array`), `'binary'`, `'hex'`, or `'base64'`. +* `source_encoding` — only used when `buffer` is a string; how to interpret its bytes. Accepts `'utf8'` (default), `'hex'`, `'base64'`, `'binary'`. -```javascript +```ts key.decrypt(buffer, [encoding]); -key.decryptPublic(buffer, [encoding]); // use public key for decryption +key.decryptPublic(buffer, [encoding]); // decrypt with public key ``` -Return decrypted data.
+Returns the decrypted data. -* buffer — `{buffer}` — data for decrypting. Takes Buffer object or base64 encoded string.
-* encoding — `{string}` — encoding for result string. Can also take `'buffer'` for raw Buffer object, or `'json'` for automatic JSON.parse result. Default `'buffer'`. +* `buffer` — `Uint8Array` or base64-encoded string. +* `encoding` — output: `'buffer'` (default, raw `Uint8Array`), `'utf8'`, `'hex'`, `'base64'`, `'binary'`, or `'json'` (UTF-8 decoded + `JSON.parse`). -> *Notice:* `encryptPrivate` and `decryptPublic` using only pkcs1 padding type 1 (not random) +> *Note:* `encryptPrivate` / `decryptPublic` always use PKCS#1 v1.5 type-1 padding (deterministic), regardless of the configured `encryptionScheme`. ### Signing/Verifying -```javascript +```ts key.sign(buffer, [encoding], [source_encoding]); ``` -Return signature for buffer. All the arguments are the same as for `encrypt` method. +Returns the signature. All arguments behave like `encrypt`. -```javascript -key.verify(buffer, signature, [source_encoding], [signature_encoding]) +```ts +key.verify(buffer, signature, [source_encoding], [signature_encoding]); ``` -Return result of check, `true` or `false`.
+Returns `true` / `false`. -* buffer — `{buffer}` — data for check, same as `encrypt` method.
-* signature — `{string}` — signature for check, result of `sign` method.
-* source_encoding — `{string}` — same as for `encrypt` method.
-* signature_encoding — `{string}` — encoding of given signature. May be `'buffer'`, `'binary'`, `'hex'` or `'base64'`. Default `'buffer'`. +* `buffer` — data that was signed; same shape as for `encrypt`. +* `signature` — `Uint8Array` or string, as produced by `sign`. +* `source_encoding` — encoding for `buffer` if it's a string. Default `'utf8'`. +* `signature_encoding` — encoding of `signature`. One of `'buffer'` (default), `'binary'`, `'hex'`, `'base64'`. -## Contributing +## Browser usage -Questions, comments, bug reports, and pull requests are all welcome. +The browser bundle (`dist/index.browser.js`) is published as ESM only and contains no Node-builtin imports — you don't need to polyfill Buffer, crypto, or process. Bundlers that honour the `"browser"` export condition (Vite, Webpack 5, Rollup, esbuild, Parcel) pick it up automatically. The bundle weighs ~114 KB raw / ~28 KB gzipped. -## Changelog +If your bundler doesn't resolve conditional exports, import the browser entry directly: + +```ts +import NodeRSA from 'node-rsa/dist/index.browser.js'; +``` -### 1.1.0 - * Added OpenSSH key format support. - -### 1.0.2 - * Importing keys from PEM now is less dependent on non-key data in files. - -### 1.0.1 - * `importKey()` now returns `this` - -### 1.0.0 - * Using semver now 🎉 - * **Breaking change**: Drop support nodejs < 8.11.1 - * **Possible breaking change**: `new Buffer()` call as deprecated was replaced by `Buffer.from` & `Buffer.alloc`. - * **Possible breaking change**: Drop support for hash scheme `sha` (was removed in node ~10). `sha1`, `sha256` and others still works. - * **Possible breaking change**: Little change in environment detect algorithm. - -### 0.4.2 - * `no padding` scheme will padded data with zeros on all environments. - -### 0.4.1 - * `PKCS1 no padding` scheme support. - -### 0.4.0 - * License changed from BSD to MIT. - * Some changes in internal api. - -### 0.3.3 - * Fixed PSS encode/verify methods with max salt length. - -### 0.3.2 - * Fixed environment detection in web worker. - -### 0.3.0 - * Added import/export from/to raw key components. - * Removed lodash from dependencies. - -### 0.2.30 - * Fixed a issue when the key was generated by 1 bit smaller than specified. It may slow down the generation of large keys. - -### 0.2.24 - * Now used old hash APIs for webpack compatible. - -### 0.2.22 - * `encryptPrivate` and `decryptPublic` now using only pkcs1 (type 1) padding. - -### 0.2.20 - * Added `.encryptPrivate()` and `.decryptPublic()` methods. - * Encrypt/decrypt methods in nodejs 0.12.x and io.js using native implementation (> 40x speed boost). - * Fixed some regex issue causing catastrophic backtracking. - -### 0.2.10 - * **Methods `.exportPrivate()` and `.exportPublic()` was replaced by `.exportKey([format])`.** - * By default `.exportKey()` returns private key as `.exportPrivate()`, if you need public key from `.exportPublic()` you must specify format as `'public'` or `'pkcs8-public-pem'`. - * Method `.importKey(key, [format])` now has second argument. - -### 0.2.0 - * **`.getPublicPEM()` method was renamed to `.exportPublic()`** - * **`.getPrivatePEM()` method was renamed to `.exportPrivate()`** - * **`.loadFromPEM()` method was renamed to `.importKey()`** - * Added PKCS1_OAEP encrypting/decrypting support. - * **PKCS1_OAEP now default scheme, you need to specify 'encryptingScheme' option to 'pkcs1' for compatibility with 0.1.x version of NodeRSA.** - * Added PSS signing/verifying support. - * Signing now supports `'md5'`, `'ripemd160'`, `'sha1'`, `'sha256'`, `'sha512'` hash algorithms in both environments - and additional `'md4'`, `'sha'`, `'sha224'`, `'sha384'` for nodejs env. - * **`options.signingAlgorithm` was renamed to `options.signingScheme`** - * Added `encryptingScheme` option. - * Property `key.options` now mark as private. Added `key.setOptions(options)` method. - - -### 0.1.54 - * Added support for loading PEM key from Buffer (`fs.readFileSync()` output). - * Added `isEmpty()` method. - -### 0.1.52 - * Improve work with not properly trimming PEM strings. - -### 0.1.50 - * Implemented native js signing and verifying for browsers. - * `options.signingAlgorithm` now takes only hash-algorithm name. - * Added `.getKeySize()` and `.getMaxMessageSize()` methods. - * `.loadFromPublicPEM` and `.loadFromPrivatePEM` methods marked as private. - -### 0.1.40 - * Added signing/verifying. - -### 0.1.30 - * Added long message support. +## Security notes +* **PKCS#1 v1.5 encryption** (`encryptionScheme: 'pkcs1'`) is vulnerable to Bleichenbacher-style padding-oracle attacks when used to decrypt attacker-controlled ciphertexts. The library closes the internal differential timing channel but cannot eliminate the binary valid/invalid oracle inherent to the scheme. Use the default `'pkcs1_oaep'` for new code and for any path that touches attacker-controlled ciphertext. + +## Migrating + +Migrating from 1.x? See [MIGRATION.md](MIGRATION.md) for the behaviour-change summary and step-by-step walkthrough. + +## Changelog + +Release notes and per-version changes are tracked in [CHANGELOG.md](CHANGELOG.md). ## License -Copyright (c) 2014 rzcoder
+Copyright (c) 2014 rzcoder Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: @@ -344,9 +300,9 @@ The above copyright notice and this permission notice shall be included in all c THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -## Licensing for code used in rsa.js and jsbn.js +## Licensing for code used in rsa.ts and jsbn -Copyright (c) 2003-2005 Tom Wu
+Copyright (c) 2003-2005 Tom Wu All Rights Reserved. Permission is hereby granted, free of charge, to any person obtaining @@ -374,4 +330,7 @@ In addition, the following condition applies: All redistributions must retain an intact copy of this copyright notice and disclaimer. -[![Build Status](https://travis-ci.org/rzcoder/node-rsa.svg?branch=master)](https://travis-ci.org/rzcoder/node-rsa) +## Acknowledgements + +* Tom Wu — original [jsbn](http://www-cs-students.stanford.edu/~tjw/jsbn/) BigInteger and RSA implementations +* Paul Miller — [@noble/hashes](https://github.com/paulmillr/noble-hashes) v2.x, audited synchronous hash functions (MD5, RIPEMD-160, SHA-1/2 family) used by the browser bundle diff --git a/bench/fixtures.ts b/bench/fixtures.ts new file mode 100644 index 0000000..22429e1 --- /dev/null +++ b/bench/fixtures.ts @@ -0,0 +1,67 @@ +import NodeRSA from 'node-rsa-bench-entry'; + +// Three distinct math implementations: +// +// node → node:crypto everywhere (OpenSSL primitives; BigInteger unused). +// js-jsbn → JS engine + JS schemes + jsbn BigInteger (28-bit digits). +// js-native → JS engine + JS schemes + native ES2020 BigInt. +// +// Digest/RNG backend is always native to the runtime (node:crypto in Node, +// crypto.getRandomValues + @noble/hashes in browser), so it isn't a separate +// comparison axis here. +export type Mode = 'node' | 'js-jsbn' | 'js-native'; + +const RAW_MODES = process.env.NODE_RSA_BENCH_MODES ?? 'node'; +export const MODES: ReadonlyArray = RAW_MODES.split(',').map((s) => s.trim()) as Mode[]; + +// One canonical payload (32B) — typical RSA input (digest, symmetric key, +// short message). Larger payloads / chunking aren't the comparison the +// suite is for. +export const PAYLOAD = new Uint8Array(32).map((_, i) => i & 0xff); + +// One canonical key size — 2048-bit is the industry baseline. Fresh per +// process, memoized so the bench measures crypto ops, not keygen. +let _key2048: NodeRSA | undefined; +export function key2048(): NodeRSA { + if (!_key2048) _key2048 = new NodeRSA({ b: 2048 }); + return _key2048; +} + +/** Constructor options that pin a NodeRSA instance to the requested mode. */ +export function ctorOptionsFor( + mode: Mode, +): { environment: 'browser'; bigIntImpl: 'jsbn' | 'native' } | undefined { + if (mode === 'js-jsbn') return { environment: 'browser', bigIntImpl: 'jsbn' }; + if (mode === 'js-native') return { environment: 'browser', bigIntImpl: 'native' }; + // 'node': node bundle defaults (no override needed). + return undefined; +} + +/** + * Build a configured NodeRSA for the given (mode, scheme). Re-imports the + * shared 2048-bit key as PKCS1-PEM with the mode's pinning options applied + * via the constructor — bigIntImpl must be set BEFORE key components are + * built, so setOptions on a populated key would throw. + */ +export function buildKey( + mode: Mode, + opts: { scheme: 'pkcs1' | 'pkcs1_oaep' | 'pss'; hash: string; kind: 'enc' | 'sign' }, +): NodeRSA { + const pem = key2048().exportKey('pkcs1-private-pem') as string; + const pin = ctorOptionsFor(mode); + const key = pin + ? new NodeRSA(pem, 'pkcs1-private-pem', pin) + : new NodeRSA(pem, 'pkcs1-private-pem'); + if (opts.kind === 'enc') { + key.setOptions({ + encryptionScheme: { scheme: opts.scheme as 'pkcs1' | 'pkcs1_oaep', hash: opts.hash as never }, + }); + } else { + key.setOptions({ + signingScheme: { scheme: opts.scheme as 'pkcs1' | 'pss', hash: opts.hash as never }, + }); + } + return key; +} + +export { NodeRSA }; diff --git a/bench/operations.bench.ts b/bench/operations.bench.ts new file mode 100644 index 0000000..ab63fe0 --- /dev/null +++ b/bench/operations.bench.ts @@ -0,0 +1,66 @@ +import { bench, describe } from 'vitest'; +import { buildKey, ctorOptionsFor, MODES, NodeRSA, PAYLOAD } from './fixtures.js'; + +// One canonical configuration — 2048-bit RSA, SHA-256, 32B input — so the +// matrix is "environment vs typical operation" and nothing else: +// +// keygen / encrypt / decrypt / sign / verify +// +// Each `describe` is one operation. The benches inside are the modes that +// run in this workspace project (node project: node-native + node-js; +// browser project: browser). Vitest's per-describe summary then compares +// modes against each other for the SAME operation — which is the only +// comparison worth printing. +describe('keygen 2048-bit', () => { + for (const mode of MODES) { + const pin = ctorOptionsFor(mode); + bench( + mode, + () => { + // Pin the impl via constructor options BEFORE generateKeyPair — the + // BigInteger swap must happen before any prime-search arithmetic. + const k = pin ? new NodeRSA(null, pin) : new NodeRSA(); + k.generateKeyPair(2048); + }, + { iterations: 5, time: 60_000, warmupIterations: 0, warmupTime: 0 }, + ); + } +}); + +describe('encrypt OAEP-SHA256 2048-bit', () => { + for (const mode of MODES) { + const key = buildKey(mode, { kind: 'enc', scheme: 'pkcs1_oaep', hash: 'sha256' }); + bench(mode, () => { + key.encrypt(PAYLOAD); + }); + } +}); + +describe('decrypt OAEP-SHA256 2048-bit', () => { + for (const mode of MODES) { + const key = buildKey(mode, { kind: 'enc', scheme: 'pkcs1_oaep', hash: 'sha256' }); + const ct = key.encrypt(PAYLOAD) as Uint8Array; + bench(mode, () => { + key.decrypt(ct); + }); + } +}); + +describe('sign PSS-SHA256 2048-bit', () => { + for (const mode of MODES) { + const key = buildKey(mode, { kind: 'sign', scheme: 'pss', hash: 'sha256' }); + bench(mode, () => { + key.sign(PAYLOAD); + }); + } +}); + +describe('verify PSS-SHA256 2048-bit', () => { + for (const mode of MODES) { + const key = buildKey(mode, { kind: 'sign', scheme: 'pss', hash: 'sha256' }); + const sig = key.sign(PAYLOAD) as Uint8Array; + bench(mode, () => { + key.verify(PAYLOAD, sig); + }); + } +}); diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..320e833 --- /dev/null +++ b/biome.json @@ -0,0 +1,100 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.4.15/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "includes": [ + "**", + "!**/dist", + "!**/src.legacy", + "!**/node_modules", + "!**/coverage", + "!**/.tmp", + "!**/.nyc_output", + "!**/package-lock.json", + "!**/test/tests.js", + "!**/test/keys" + ] + }, + "assist": { "actions": { "source": { "organizeImports": "on" } } }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "suspicious": { + "noExplicitAny": "error", + "noConsole": { "level": "warn", "options": { "allow": ["warn", "error"] } } + }, + "style": { + "useImportType": "error", + "useNodejsImportProtocol": "error", + "useConst": "error", + "noNonNullAssertion": "warn" + }, + "complexity": { + "noBannedTypes": "error" + } + } + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 100, + "lineEnding": "lf" + }, + "javascript": { + "formatter": { + "quoteStyle": "single", + "jsxQuoteStyle": "double", + "semicolons": "always", + "trailingCommas": "all", + "arrowParentheses": "always", + "bracketSpacing": true + } + }, + "json": { + "formatter": { + "indentWidth": 2 + } + }, + "overrides": [ + { + "includes": ["**/src/bigint/**"], + "linter": { + "rules": { + "style": { + "noNonNullAssertion": "off", + "noParameterAssign": "off" + }, + "suspicious": { + "noAssignInExpressions": "off" + } + } + } + }, + { + "includes": ["**/examples/**", "**/scripts/**"], + "linter": { + "rules": { + "suspicious": { + "noConsole": "off" + } + } + } + }, + { + "includes": ["**/test/**"], + "linter": { + "rules": { + "style": { + "noNonNullAssertion": "off" + } + } + } + } + ] +} diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..090b51a --- /dev/null +++ b/examples/README.md @@ -0,0 +1,31 @@ +# Examples + +Three runnable consumers that import the built `node-rsa@2` package from the +parent directory. Use them as smoke tests after `npm run build`. + +``` +node-cjs/ CommonJS via require('node-rsa').default +node-esm/ ESM via import NodeRSA from 'node-rsa' +vite-browser/ Real-browser end-to-end via Vite + Playwright Chromium +``` + +To run the Node consumers: + +```bash +cd ../ # repo root +npm run build +cd examples/node-esm && npm install && npm start +cd ../node-cjs && npm install && npm start +``` + +To run the browser end-to-end (see [vite-browser/README.md](vite-browser/README.md) +for the long form): + +```bash +cd ../ # repo root +npm run build +cd examples/vite-browser +npm install +npm run playwright:install # one-time: download Chromium +npm test +``` diff --git a/examples/node-cjs/index.cjs b/examples/node-cjs/index.cjs new file mode 100644 index 0000000..cafe6a9 --- /dev/null +++ b/examples/node-cjs/index.cjs @@ -0,0 +1,22 @@ +const NodeRSA = require('node-rsa').default; + +console.log('=== node-rsa CJS example ==='); + +const key = new NodeRSA({ b: 1024 }); +console.log(`Generated key size: ${key.getKeySize()} bits`); + +const ct = key.encrypt('hello from CJS'); +console.log(`Ciphertext length: ${ct.length} bytes`); + +const pt = key.decrypt(ct, 'utf8'); +console.log(`Decrypted: ${pt}`); + +const sig = key.sign('signed payload'); +const ok = key.verify('signed payload', sig); +console.log(`Signature verify: ${ok}`); + +if (pt !== 'hello from CJS' || !ok) { + console.error('FAILED'); + process.exit(1); +} +console.log('OK'); diff --git a/examples/node-cjs/package.json b/examples/node-cjs/package.json new file mode 100644 index 0000000..ce60a76 --- /dev/null +++ b/examples/node-cjs/package.json @@ -0,0 +1,11 @@ +{ + "name": "node-rsa-example-cjs", + "version": "0.0.0", + "private": true, + "scripts": { + "start": "node index.cjs" + }, + "dependencies": { + "node-rsa": "file:../.." + } +} diff --git a/examples/node-esm/index.mjs b/examples/node-esm/index.mjs new file mode 100644 index 0000000..d4f9a71 --- /dev/null +++ b/examples/node-esm/index.mjs @@ -0,0 +1,22 @@ +import NodeRSA from 'node-rsa'; + +console.log('=== node-rsa ESM example ==='); + +const key = new NodeRSA({ b: 1024 }); +console.log(`Generated key size: ${key.getKeySize()} bits`); + +const ct = key.encrypt('hello from ESM'); +console.log(`Ciphertext length: ${ct.length} bytes`); + +const pt = key.decrypt(ct, 'utf8'); +console.log(`Decrypted: ${pt}`); + +const sig = key.sign('signed payload'); +const ok = key.verify('signed payload', sig); +console.log(`Signature verify: ${ok}`); + +if (pt !== 'hello from ESM' || !ok) { + console.error('FAILED'); + process.exit(1); +} +console.log('OK'); diff --git a/examples/node-esm/package.json b/examples/node-esm/package.json new file mode 100644 index 0000000..654a7fd --- /dev/null +++ b/examples/node-esm/package.json @@ -0,0 +1,12 @@ +{ + "name": "node-rsa-example-esm", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "start": "node index.mjs" + }, + "dependencies": { + "node-rsa": "file:../.." + } +} diff --git a/examples/vite-browser/.gitignore b/examples/vite-browser/.gitignore new file mode 100644 index 0000000..0ca82f2 --- /dev/null +++ b/examples/vite-browser/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +.playwright/ +playwright-report/ +test-results/ +*.log diff --git a/examples/vite-browser/README.md b/examples/vite-browser/README.md new file mode 100644 index 0000000..4653760 --- /dev/null +++ b/examples/vite-browser/README.md @@ -0,0 +1,50 @@ +# node-rsa — Vite + Playwright browser example + +End-to-end smoke for the **browser bundle** of `node-rsa`: a Vite dev server +serves a page that runs keygen / encrypt+decrypt / sign+verify against the +public API, and Playwright drives Chromium to verify everything round-trips +without any Node-builtin shim slipping in. + +## What this proves + +1. `import NodeRSA from 'node-rsa'` resolves to `dist/index.browser.js` via + the package's `exports` map — no `Buffer`/`crypto` polyfill required. +2. Native ES2020 `BigInt` is the default impl (the browser entry calls + `setBigIntegerImpl('native')` at module load); the assertion in + `tests/rsa.spec.ts` pins `bigIntImpl === 'native'`. +3. Four operations round-trip end-to-end: 1024-bit keygen, OAEP-SHA1 + encrypt/decrypt, PSS-SHA256 sign/verify, PKCS#1 PEM export/import. +4. No `pageerror` / `console.error` fires during the run — a guard against + silent regressions like a `Buffer.from` sneaking back into the bundle. + +## Running locally + +```sh +# from the repo root, build the browser bundle first +npm run build + +# then install + drive the example +cd examples/vite-browser +npm install +npm run playwright:install # one-time: download Chromium +npm test # spins up Vite + runs the Playwright spec +``` + +To eyeball the page yourself, `npm run dev` and open +. The on-page status flips to **All steps passed.** +once every step has succeeded. + +## Wiring notes + +- `node-rsa` is consumed via `"node-rsa": "file:../.."`. Vite resolves it + through the workspace's `package.json#exports` map — the `browser` + condition picks `dist/index.browser.js`. Run `npm run build` in the + repo root before `npm install` here so the dist files exist. +- `playwright.config.ts` boots Vite via its `webServer` block on port 5174; + the same port is pinned in `vite.config.ts`. If a stray Vite instance is + already on that port locally, `reuseExistingServer: !process.env.CI` + re-uses it; CI always launches a fresh one. +- `src/main.ts` exposes its results on `window.__rsaResults`. The + Playwright spec asserts on both the visible DOM (`#status[data-state]`) + and that structured payload, so a regression that silently corrupts a + step still surfaces via the assertion message. diff --git a/examples/vite-browser/index.html b/examples/vite-browser/index.html new file mode 100644 index 0000000..8bff25b --- /dev/null +++ b/examples/vite-browser/index.html @@ -0,0 +1,57 @@ + + + + + + node-rsa — browser bundle smoke test + + + +

node-rsa — browser bundle smoke test

+

Running…

+ + + + + + + + +
StepResult
+ + + diff --git a/examples/vite-browser/package.json b/examples/vite-browser/package.json new file mode 100644 index 0000000..32534e0 --- /dev/null +++ b/examples/vite-browser/package.json @@ -0,0 +1,23 @@ +{ + "name": "node-rsa-example-vite-browser", + "version": "0.0.0", + "private": true, + "type": "module", + "description": "Real-browser end-to-end test for node-rsa: Vite dev server + Playwright drives a page that exercises keygen / encrypt+decrypt / sign+verify against the browser bundle, with native BigInt as the default impl.", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "test": "playwright test", + "test:ui": "playwright test --ui", + "playwright:install": "playwright install chromium" + }, + "dependencies": { + "node-rsa": "file:../.." + }, + "devDependencies": { + "@playwright/test": "^1.49.0", + "typescript": "^5.7.0", + "vite": "^6.0.0" + } +} diff --git a/examples/vite-browser/playwright.config.ts b/examples/vite-browser/playwright.config.ts new file mode 100644 index 0000000..3c7d3bc --- /dev/null +++ b/examples/vite-browser/playwright.config.ts @@ -0,0 +1,28 @@ +import { defineConfig, devices } from '@playwright/test'; + +// Single-project Playwright config: spin up Vite, point the test at the +// served page, run on Chromium. The CI flag flips reuseExistingServer so a +// developer running `npm test` locally with a live `npm run dev` instance +// gets quick re-runs instead of port-conflict errors. +export default defineConfig({ + testDir: './tests', + timeout: 30_000, + retries: 0, + reporter: process.env.CI ? 'github' : 'list', + use: { + baseURL: 'http://localhost:5174', + trace: 'retain-on-failure', + }, + webServer: { + command: 'npm run dev', + port: 5174, + reuseExistingServer: !process.env.CI, + timeout: 30_000, + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}); diff --git a/examples/vite-browser/src/main.ts b/examples/vite-browser/src/main.ts new file mode 100644 index 0000000..5c79707 --- /dev/null +++ b/examples/vite-browser/src/main.ts @@ -0,0 +1,113 @@ +import NodeRSA from 'node-rsa'; + +// This is the smoke target for the Playwright suite. It exercises the +// public NodeRSA surface in a real browser to prove: +// 1. The browser bundle has no implicit Node-builtin imports (Vite would +// otherwise complain about un-polyfilled `Buffer`/`crypto`). +// 2. Native ES2020 `BigInt` is selected by default (the browser bundle's +// module-load `setBigIntegerImpl('native')` call). +// 3. Keygen / encrypt+decrypt / sign+verify all round-trip. +// +// Results land in: +// - the on-page table (for human eyeballing during `npm run dev`), +// - the `data-state` attribute on `#status` (the Playwright assertion), +// - `window.__rsaResults` (for richer assertions if needed). + +declare global { + interface Window { + __rsaResults?: Record; + } +} + +type Step = { name: string; ok: boolean; detail: string }; + +function renderRow(step: Step): void { + const tbody = document.querySelector('#results tbody'); + if (!tbody) return; + const tr = document.createElement('tr'); + const tdName = document.createElement('td'); + const tdResult = document.createElement('td'); + tdName.textContent = step.name; + tdResult.textContent = `${step.ok ? 'OK' : 'FAIL'} — ${step.detail}`; + tdResult.style.color = step.ok ? '#15803d' : '#b91c1c'; + tr.append(tdName, tdResult); + tbody.append(tr); +} + +function setStatus(state: 'ok' | 'fail', message: string): void { + const el = document.querySelector('#status'); + if (!el) return; + el.dataset.state = state; + el.textContent = message; +} + +async function run(): Promise { + const steps: Step[] = []; + const results: Record = {}; + + try { + // Step 1 — keygen. 1024-bit keeps the example responsive (~100 ms on + // a modern laptop with native BigInt); production code should use 2048+. + const t0 = performance.now(); + const key = new NodeRSA({ b: 1024 }); + const keygenMs = performance.now() - t0; + const bigIntImpl = key.$options.bigIntImpl; + steps.push({ + name: '1. keygen 1024-bit', + ok: key.isPrivate() && key.isPublic(), + detail: `${keygenMs.toFixed(0)} ms, bigIntImpl=${bigIntImpl}`, + }); + results.keygenMs = keygenMs; + results.bigIntImpl = bigIntImpl; + + // Step 2 — OAEP encrypt + decrypt round-trip. + const plaintext = 'hello from a real browser'; + const ct = key.encrypt(plaintext) as Uint8Array; + const pt = key.decrypt(ct, 'utf8') as string; + steps.push({ + name: '2. OAEP encrypt + decrypt round-trip', + ok: pt === plaintext, + detail: `ciphertext ${ct.byteLength}B, plaintext "${pt}"`, + }); + results.ciphertextLength = ct.byteLength; + results.decryptedPlaintext = pt; + + // Step 3 — PSS sign + verify round-trip. + const payload = new TextEncoder().encode('signed payload'); + const sig = key.sign(payload) as Uint8Array; + const ok = key.verify(payload, sig); + steps.push({ + name: '3. PSS sign + verify round-trip', + ok, + detail: `signature ${sig.byteLength}B, verify=${ok}`, + }); + results.signatureLength = sig.byteLength; + results.verifyOk = ok; + + // Step 4 — PEM export round-trip — proves the format layer survives + // the trip through `globalThis.btoa` / TextEncoder without Node Buffer. + const pem = key.exportKey('pkcs1-private-pem') as string; + const reimported = new NodeRSA(pem, 'pkcs1-private-pem'); + const pemMatch = (reimported.exportKey('pkcs1-private-pem') as string).trim() === pem.trim(); + steps.push({ + name: '4. PEM export → import → re-export equality', + ok: pemMatch, + detail: `pem length ${pem.length}, byte-identical=${pemMatch}`, + }); + results.pemLength = pem.length; + results.pemRoundtripOk = pemMatch; + + for (const step of steps) renderRow(step); + const allOk = steps.every((s) => s.ok); + setStatus(allOk ? 'ok' : 'fail', allOk ? 'All steps passed.' : 'One or more steps failed.'); + window.__rsaResults = { ok: allOk, steps, ...results }; + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + for (const step of steps) renderRow(step); + renderRow({ name: 'unhandled error', ok: false, detail: message }); + setStatus('fail', `Threw: ${message}`); + window.__rsaResults = { ok: false, error: message, steps }; + } +} + +void run(); diff --git a/examples/vite-browser/tests/rsa.spec.ts b/examples/vite-browser/tests/rsa.spec.ts new file mode 100644 index 0000000..3966b3e --- /dev/null +++ b/examples/vite-browser/tests/rsa.spec.ts @@ -0,0 +1,53 @@ +import { expect, test } from '@playwright/test'; + +// End-to-end smoke: load the page, wait for main.ts to finish its run, then +// assert the on-page status flipped to "ok" and that every individual step +// succeeded according to `window.__rsaResults`. + +interface RsaResults { + ok: boolean; + steps: Array<{ name: string; ok: boolean; detail: string }>; + bigIntImpl?: string; + decryptedPlaintext?: string; + verifyOk?: boolean; + pemRoundtripOk?: boolean; + error?: string; +} + +test('runs node-rsa keygen / encrypt+decrypt / sign+verify in a real browser', async ({ page }) => { + // Surface any console-level errors so a regression in the browser bundle + // (e.g. an accidental `Buffer.from` slipping back in) shows up here + // instead of as a mysterious "results never arrived" timeout. + const consoleErrors: string[] = []; + page.on('pageerror', (err) => consoleErrors.push(err.message)); + page.on('console', (msg) => { + if (msg.type() === 'error') consoleErrors.push(msg.text()); + }); + + await page.goto('/'); + + // Wait for the page script to finish — it flips `#status[data-state]` + // from 'pending' to 'ok' (or 'fail') exactly once. + await expect(page.locator('#status')).toHaveAttribute('data-state', 'ok', { timeout: 15_000 }); + + const results = (await page.evaluate(() => window.__rsaResults)) as RsaResults | undefined; + expect(results, 'window.__rsaResults should be populated by main.ts').toBeDefined(); + if (!results) return; + + expect(results.error, `unexpected JS error: ${results.error}`).toBeUndefined(); + expect(results.ok, 'all steps reported success').toBe(true); + + // Sanity-check the individual round-trips end-to-end. + expect(results.bigIntImpl).toBe('native'); + expect(results.decryptedPlaintext).toBe('hello from a real browser'); + expect(results.verifyOk).toBe(true); + expect(results.pemRoundtripOk).toBe(true); + + // Every step must have ok=true; if any failed, the assertion message + // surfaces which step + its detail string. + for (const step of results.steps) { + expect(step.ok, `${step.name}: ${step.detail}`).toBe(true); + } + + expect(consoleErrors, 'no JS errors should fire during the run').toEqual([]); +}); diff --git a/examples/vite-browser/tsconfig.json b/examples/vite-browser/tsconfig.json new file mode 100644 index 0000000..014d907 --- /dev/null +++ b/examples/vite-browser/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "lib": ["ES2022", "DOM"], + "types": [] + }, + "include": ["src/**/*", "tests/**/*", "*.ts"] +} diff --git a/examples/vite-browser/vite.config.ts b/examples/vite-browser/vite.config.ts new file mode 100644 index 0000000..7fb25ec --- /dev/null +++ b/examples/vite-browser/vite.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from 'vite'; + +// Pin a non-default port so this example never clashes with whatever the +// host machine has on 5173. Playwright's webServer config below points at +// the same port — keep them in sync. +export default defineConfig({ + server: { + port: 5174, + strictPort: true, + }, + // Tells Vite to surface a clear error if we accidentally import a + // Node-builtin from the browser entry — this whole example exists to + // prove the browser bundle has zero Node deps. (Vite would normally + // try to polyfill some of them silently.) + optimizeDeps: { + include: ['node-rsa'], + }, +}); diff --git a/gruntfile.js b/gruntfile.js deleted file mode 100644 index ecdb7f1..0000000 --- a/gruntfile.js +++ /dev/null @@ -1,33 +0,0 @@ -module.exports = function (grunt) { - grunt.initConfig({ - jshint: { - options: {}, - default: { - files: { - src: ['gruntfile.js', 'src/**/*.js', '!src/libs/jsbn.js'] - } - }, - libs: { - files: { - src: ['src/libs/**/*'] - } - } - }, - - simplemocha: { - options: { - reporter: 'list' - }, - all: {src: ['test/**/*.js']} - } - }); - - require('jit-grunt')(grunt, { - 'simplemocha': 'grunt-simple-mocha' - }); - - grunt.registerTask('lint', ['jshint:default']); - grunt.registerTask('test', ['simplemocha']); - - grunt.registerTask('default', ['lint', 'test']); -}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index dc7b214..bb4d65a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,4283 +1,2819 @@ { "name": "node-rsa", - "version": "1.1.1", - "lockfileVersion": 1, + "version": "2.0.0-rc.0", + "lockfileVersion": 3, "requires": true, - "dependencies": { - "@babel/code-frame": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", - "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", - "dev": true, - "requires": { - "@babel/highlight": "^7.8.3" - } - }, - "@babel/core": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.9.0.tgz", - "integrity": "sha512-kWc7L0fw1xwvI0zi8OKVBuxRVefwGOrKSQMvrQ3dW+bIIavBY3/NpXmpjMy7bQnLgwgzWQZ8TlM57YHpHNHz4w==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.8.3", - "@babel/generator": "^7.9.0", - "@babel/helper-module-transforms": "^7.9.0", - "@babel/helpers": "^7.9.0", - "@babel/parser": "^7.9.0", - "@babel/template": "^7.8.6", - "@babel/traverse": "^7.9.0", - "@babel/types": "^7.9.0", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.1", - "json5": "^2.1.2", - "lodash": "^4.17.13", - "resolve": "^1.3.2", - "semver": "^5.4.1", - "source-map": "^0.5.0" - }, + "packages": { + "": { + "name": "node-rsa", + "version": "2.0.0-rc.0", + "license": "MIT", "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } + "@noble/hashes": "^2.2.0" + }, + "devDependencies": { + "@biomejs/biome": "^2.4.15", + "@types/node": "^22.10.0", + "chai": "^6.2.2", + "tsup": "^8.5.1", + "typescript": "^6.0.3", + "vitest": "^4.1.6" + }, + "engines": { + "node": ">=20" } }, - "@babel/generator": { - "version": "7.9.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.9.4.tgz", - "integrity": "sha512-rjP8ahaDy/ouhrvCoU1E5mqaitWrxwuNGU+dy1EpaoK48jZay4MdkskKGIMHLZNewg8sAsqpGSREJwP0zH3YQA==", + "node_modules/@biomejs/biome": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.4.15.tgz", + "integrity": "sha512-j5VH3a/h/HXTKBM50MDMxRCzkeLv9S2XJcW2WgnZT1+xyisi+0bISrXR82gCX+8S9lvK0skEvHJRN+3Ktr2hlw==", "dev": true, - "requires": { - "@babel/types": "^7.9.0", - "jsesc": "^2.5.1", - "lodash": "^4.17.13", - "source-map": "^0.5.0" + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.4.15", + "@biomejs/cli-darwin-x64": "2.4.15", + "@biomejs/cli-linux-arm64": "2.4.15", + "@biomejs/cli-linux-arm64-musl": "2.4.15", + "@biomejs/cli-linux-x64": "2.4.15", + "@biomejs/cli-linux-x64-musl": "2.4.15", + "@biomejs/cli-win32-arm64": "2.4.15", + "@biomejs/cli-win32-x64": "2.4.15" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.4.15.tgz", + "integrity": "sha512-rF3PPqLq1yoST79zaQbDjVJwsuIeci/O+9bgNmC5QpgOqz6aqYuzA4abyAGx+mgyiDXn4A049xAN8gijbuR1Qg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.4.15.tgz", + "integrity": "sha512-/5KHXYMfSJs1fNXiX30xFtI8JcCFV6zaVVLxOa0M2sfqBKHkpQhRTv94yxQWxeTY2lzo2OuTlNvPC+hDQt2wcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.4.15.tgz", + "integrity": "sha512-owaAMZD/T4LrD0ELNCk0Km3qrRHuM0X6EAyVE1FSqGY0rbLoiDLrO4Us2tllm6cAeB2Ioa9C2C08NZPdr8+0Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.4.15.tgz", + "integrity": "sha512-ZPcxznxm0pogHBLZhYntyR3sR+MrZjqJIKEr7ZqVen0Rl+P/4upVmfYXjftizi9RoqZntg33fv/1fbdhbYXpEQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.4.15.tgz", + "integrity": "sha512-0jj7THz12GbUOLmMibktK6DZjqz2zV64KFxyBtcFTKPiiOIY0a7vns1elpO1dERvxpsZ5ik0oFfz0oGwFde1+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.4.15.tgz", + "integrity": "sha512-CNq/9W38SYSH023lfcQ4KKU8K0YX8T//FZUhcgtMMRABDojx5XsMV7jlweAvGSl389wJQB29Qo6Zb/a+jdvt+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.4.15.tgz", + "integrity": "sha512-ouhkYdlhp/1GghEJPdWwD/Vi3gQ1nFxuSpMolWsbq3Lsq3QUR4jl6UdhhscdCugKU5vOEuMiJhvKj66O0OCq+w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.4.15.tgz", + "integrity": "sha512-zBrGq5mx5wwpnow4+2BxUvleDM+GNd4sLbPaMapsSLQLD0NGRCquqPBTgN+7XkUteHvj7M+BstuI8tmnV7+HgQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" } }, - "@babel/helper-function-name": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz", - "integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==", + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.8.3", - "@babel/template": "^7.8.3", - "@babel/types": "^7.8.3" + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" } }, - "@babel/helper-get-function-arity": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", - "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", "dev": true, - "requires": { - "@babel/types": "^7.8.3" + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" } }, - "@babel/helper-member-expression-to-functions": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.8.3.tgz", - "integrity": "sha512-fO4Egq88utkQFjbPrSHGmGLFqmrshs11d46WI+WZDESt7Wu7wN2G2Iu+NMMZJFDOVRHAMIkB5SNh30NtwCA7RA==", + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, - "requires": { - "@babel/types": "^7.8.3" + "license": "MIT", + "engines": { + "node": ">=6.0.0" } }, - "@babel/helper-module-imports": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.8.3.tgz", - "integrity": "sha512-R0Bx3jippsbAEtzkpZ/6FIiuzOURPcMjHp+Z6xPe6DtApDJx+w7UYyOLanZqO8+wKR9G10s/FmHXvxaMd9s6Kg==", + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, - "requires": { - "@babel/types": "^7.8.3" - } + "license": "MIT" }, - "@babel/helper-module-transforms": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.9.0.tgz", - "integrity": "sha512-0FvKyu0gpPfIQ8EkxlrAydOWROdHpBmiCiRwLkUiBGhCUPRRbVD2/tm3sFr/c/GWFrQ/ffutGUAnx7V0FzT2wA==", + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.8.3", - "@babel/helper-replace-supers": "^7.8.6", - "@babel/helper-simple-access": "^7.8.3", - "@babel/helper-split-export-declaration": "^7.8.3", - "@babel/template": "^7.8.6", - "@babel/types": "^7.9.0", - "lodash": "^4.17.13" + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "@babel/helper-optimise-call-expression": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.8.3.tgz", - "integrity": "sha512-Kag20n86cbO2AvHca6EJsvqAd82gc6VMGule4HwebwMlwkpXuVqrNRj6CkCV2sKxgi9MyAUnZVnZ6lJ1/vKhHQ==", + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", "dev": true, - "requires": { - "@babel/types": "^7.8.3" + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" } }, - "@babel/helper-replace-supers": { - "version": "7.8.6", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.8.6.tgz", - "integrity": "sha512-PeMArdA4Sv/Wf4zXwBKPqVj7n9UF/xg6slNRtZW84FM7JpE1CbG8B612FyM4cxrf4fMAMGO0kR7voy1ForHHFA==", - "dev": true, - "requires": { - "@babel/helper-member-expression-to-functions": "^7.8.3", - "@babel/helper-optimise-call-expression": "^7.8.3", - "@babel/traverse": "^7.8.6", - "@babel/types": "^7.8.6" + "node_modules/@noble/hashes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "@babel/helper-simple-access": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.8.3.tgz", - "integrity": "sha512-VNGUDjx5cCWg4vvCTR8qQ7YJYZ+HBjxOgXEl7ounz+4Sn7+LMD3CFrCTEU6/qXKbA2nKg21CwhhBzO0RpRbdCw==", + "node_modules/@oxc-project/types": { + "version": "0.130.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.130.0.tgz", + "integrity": "sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==", "dev": true, - "requires": { - "@babel/template": "^7.8.3", - "@babel/types": "^7.8.3" + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" } }, - "@babel/helper-split-export-declaration": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", - "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", - "dev": true, - "requires": { - "@babel/types": "^7.8.3" + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.1.tgz", + "integrity": "sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.1.tgz", + "integrity": "sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.1.tgz", + "integrity": "sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.1.tgz", + "integrity": "sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.1.tgz", + "integrity": "sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.1.tgz", + "integrity": "sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.1.tgz", + "integrity": "sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.1.tgz", + "integrity": "sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.1.tgz", + "integrity": "sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.1.tgz", + "integrity": "sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.1.tgz", + "integrity": "sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.1.tgz", + "integrity": "sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.1.tgz", + "integrity": "sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "@babel/helper-validator-identifier": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.0.tgz", - "integrity": "sha512-6G8bQKjOh+of4PV/ThDm/rRqlU7+IGoJuofpagU5GlEl29Vv0RGqqt86ZGRV8ZuSOY3o+8yXl5y782SMcG7SHw==", - "dev": true + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.1.tgz", + "integrity": "sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.1.tgz", + "integrity": "sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz", + "integrity": "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.3.tgz", + "integrity": "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.3.tgz", + "integrity": "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.3.tgz", + "integrity": "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.3.tgz", + "integrity": "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.3.tgz", + "integrity": "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.3.tgz", + "integrity": "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.3.tgz", + "integrity": "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.3.tgz", + "integrity": "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.3.tgz", + "integrity": "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.3.tgz", + "integrity": "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.3.tgz", + "integrity": "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.3.tgz", + "integrity": "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.3.tgz", + "integrity": "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.3.tgz", + "integrity": "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.3.tgz", + "integrity": "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.3.tgz", + "integrity": "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.3.tgz", + "integrity": "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.3.tgz", + "integrity": "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.3.tgz", + "integrity": "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.3.tgz", + "integrity": "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.3.tgz", + "integrity": "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.3.tgz", + "integrity": "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.3.tgz", + "integrity": "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.3.tgz", + "integrity": "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" }, - "@babel/helpers": { - "version": "7.9.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.9.2.tgz", - "integrity": "sha512-JwLvzlXVPjO8eU9c/wF9/zOIN7X6h8DYf7mG4CiFRZRvZNKEF5dQ3H3V+ASkHoIB3mWhatgl5ONhyqHRI6MppA==", + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", "dev": true, - "requires": { - "@babel/template": "^7.8.3", - "@babel/traverse": "^7.9.0", - "@babel/types": "^7.9.0" + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" } }, - "@babel/highlight": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz", - "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==", + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.9.0", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" } }, - "@babel/parser": { - "version": "7.9.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.9.4.tgz", - "integrity": "sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA==", - "dev": true + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" }, - "@babel/template": { - "version": "7.8.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz", - "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==", + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, - "requires": { - "@babel/code-frame": "^7.8.3", - "@babel/parser": "^7.8.6", - "@babel/types": "^7.8.6" - } - }, - "@babel/traverse": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.9.0.tgz", - "integrity": "sha512-jAZQj0+kn4WTHO5dUZkZKhbFrqZE7K5LAQ5JysMnmvGij+wOdr+8lWqPeW0BcF4wFwrEXXtdGO7wcV6YPJcf3w==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", + "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", "dev": true, - "requires": { - "@babel/code-frame": "^7.8.3", - "@babel/generator": "^7.9.0", - "@babel/helper-function-name": "^7.8.3", - "@babel/helper-split-export-declaration": "^7.8.3", - "@babel/parser": "^7.9.0", - "@babel/types": "^7.9.0", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.13" - }, + "license": "MIT", "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } + "undici-types": "~6.21.0" } }, - "@babel/types": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.0.tgz", - "integrity": "sha512-BS9JKfXkzzJl8RluW4JGknzpiUV7ZrvTayM6yfqLTVBEnFtyowVIOu6rqxRd5cVO6yGoWf4T8u8dgK9oB+GCng==", + "node_modules/@vitest/expect": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.6.tgz", + "integrity": "sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg==", "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.9.0", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.6", + "@vitest/utils": "4.1.6", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "@istanbuljs/load-nyc-config": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.0.0.tgz", - "integrity": "sha512-ZR0rq/f/E4f4XcgnDvtMWXCUJpi8eO0rssVhmztsZqLIEFA9UUP9zmpE0VxlM+kv/E1ul2I876Fwil2ayptDVg==", + "node_modules/@vitest/mocker": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.6.tgz", + "integrity": "sha512-MCFc63czMjEInOlcY2cpQCvCN+KgbAn+60xu9cMgP4sKaLC5JNAKw7JH8QdAnoAC88hW1IiSNZ+GgVXlN1UcMQ==", "dev": true, - "requires": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, + "license": "MIT", "dependencies": { - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } + "@vitest/spy": "4.1.6", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true + "vite": { + "optional": true } } }, - "@istanbuljs/schema": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz", - "integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==", - "dev": true - }, - "@types/color-name": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", - "dev": true - }, - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true - }, - "aggregate-error": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz", - "integrity": "sha512-quoaXsZ9/BLNae5yiNoUz+Nhkwz83GhWwtYFglcjEQB2NDHCIpApbqXxIFnm4Pq/Nvhrsq5sYJFyohrrxnTGAA==", - "dev": true, - "requires": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.6.tgz", + "integrity": "sha512-h5SxD/IzNhZYnrSZRsUZQIC+vD0GY8cUvq0iwsmkFKixRCKLLWqCXa/FIQ4S1R+sI+PGoojkHsdNrbZiM9Qpgw==", + "dev": true, + "license": "MIT", "dependencies": { - "indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true - } + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "ansi-colors": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", - "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==", - "dev": true - }, - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "node_modules/@vitest/runner": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.6.tgz", + "integrity": "sha512-nOPCmn2+yD0ZNmKdsXGv/UxMMWbMuKeD6GyYncNwdkYDxpQvrPSKYj2rWuDjC2Y4b6w6hjip5dBKFzEUuZe3vA==", "dev": true, - "requires": { - "color-convert": "^1.9.0" + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.6", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "anymatch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", - "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "node_modules/@vitest/snapshot": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.6.tgz", + "integrity": "sha512-YhsdE6xAVfTDmzjxL2ZDUvjj+ZsgyOKe+TdQzqkD72wIOmHka8NuGQ6NpTNZv9D2Z63fbwWKJPeVpEw4EQgYxw==", "dev": true, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.6", + "@vitest/utils": "4.1.6", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "append-transform": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", - "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", + "node_modules/@vitest/spy": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.6.tgz", + "integrity": "sha512-JFKxMx6udhwKh/Ldo270e17QX710vgunMkuPAvXjHSvC6oqLWAHhVhjg/I71q0u0CBSErIODV1Kjv0FQNSWjdg==", "dev": true, - "requires": { - "default-require-extensions": "^3.0.0" + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" } }, - "archy": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", - "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", - "dev": true - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "node_modules/@vitest/utils": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.6.tgz", + "integrity": "sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ==", "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - }, + "license": "MIT", "dependencies": { - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true - } - } - }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", - "dev": true - }, - "arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", - "dev": true - }, - "array-each": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", - "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=", - "dev": true - }, - "array-slice": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", - "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "requires": { - "safer-buffer": "~2.1.0" + "@vitest/pretty-format": "4.1.6", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true - }, - "assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", - "dev": true - }, - "async": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz", - "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==", - "dev": true - }, - "atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", - "dev": true - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "dev": true, - "requires": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } + "engines": { + "node": ">=0.4.0" } }, - "binary-extensions": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", - "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } + "license": "MIT" }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, - "requires": { - "fill-range": "^7.0.1" + "license": "MIT", + "engines": { + "node": ">=12" } }, - "browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", - "dev": true - }, - "cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "dev": true, - "requires": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - } - }, - "caching-transform": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", - "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==", - "dev": true, - "requires": { - "hasha": "^5.0.0", - "make-dir": "^3.0.0", - "package-hash": "^4.0.0", - "write-file-atomic": "^3.0.0" - } - }, - "chai": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", - "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", - "dev": true, - "requires": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^3.0.1", - "get-func-name": "^2.0.0", - "pathval": "^1.1.0", - "type-detect": "^4.0.5" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", - "dev": true - }, - "chokidar": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.0.tgz", - "integrity": "sha512-dGmKLDdT3Gdl7fBUe8XK+gAtGmzy5Fn0XkkWQuYxGIgWVPPse2CxFA5mtrlD0TOHaHjEUqkWNyP1XdHoJES/4A==", - "dev": true, - "requires": { - "anymatch": "~3.1.1", - "braces": "~3.0.2", - "fsevents": "~2.1.1", - "glob-parent": "~5.1.0", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.2.0" - } - }, - "class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, + "node_modules/bundle-require": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", + "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", + "dev": true, + "license": "MIT", "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" } }, - "clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true - }, - "cli": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cli/-/cli-1.0.1.tgz", - "integrity": "sha1-IoF1NPJL+klQw01TLUjsvGIbjBQ=", + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", "dev": true, - "requires": { - "exit": "0.1.2", - "glob": "^7.1.1" - }, - "dependencies": { - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - } + "license": "MIT", + "engines": { + "node": ">=8" } }, - "cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" + "license": "MIT", + "engines": { + "node": ">=18" } }, - "collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, - "requires": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", "dev": true, - "requires": { - "color-name": "1.1.3" + "license": "MIT", + "engines": { + "node": ">= 6" } }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "colors": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", - "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", - "dev": true - }, - "commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", - "dev": true - }, - "component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "console-browserify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", - "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", "dev": true, - "requires": { - "date-now": "^0.1.4" - } + "license": "MIT" }, - "convert-source-map": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", - "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", "dev": true, - "requires": { - "safe-buffer": "~5.1.1" + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" } }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" }, - "cross-spawn": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.1.tgz", - "integrity": "sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg==", + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, + "license": "MIT", "dependencies": { - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true } } }, - "date-now": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", - "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=", - "dev": true - }, - "dateformat": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", - "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==", - "dev": true - }, - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "dev": true, - "requires": { - "ms": "^2.1.1" + "license": "Apache-2.0", + "engines": { + "node": ">=8" } }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true - }, - "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", - "dev": true - }, - "deep-eql": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", - "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", "dev": true, - "requires": { - "type-detect": "^4.0.0" - } + "license": "MIT" }, - "default-require-extensions": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.0.tgz", - "integrity": "sha512-ek6DpXq/SCpvjhpFsLFRVtIxJCRw6fUR42lYMVZuUMK7n8eMz4Uh5clckdBjEpLhn/gEBZo7hDJnJcwdKLKQjg==", + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", "dev": true, - "requires": { - "strip-bom": "^4.0.0" + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", "dependencies": { - "strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true - } + "@types/estree": "^1.0.0" } }, - "define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", "dev": true, - "requires": { - "object-keys": "^1.0.12" + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" } }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" + "license": "MIT", + "engines": { + "node": ">=12.0.0" }, - "dependencies": { - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true } } }, - "detect-file": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", - "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=", - "dev": true - }, - "diff": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", - "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", - "dev": true - }, - "dom-serializer": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", - "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", + "node_modules/fix-dts-default-cjs-exports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", + "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", "dev": true, - "requires": { - "domelementtype": "^2.0.1", - "entities": "^2.0.0" - }, + "license": "MIT", "dependencies": { - "domelementtype": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz", - "integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==", - "dev": true - }, - "entities": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz", - "integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==", - "dev": true - } + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "rollup": "^4.34.8" } }, - "domelementtype": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", - "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", - "dev": true - }, - "domhandler": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.3.0.tgz", - "integrity": "sha1-LeWaCCLVAn+r/28DLCsloqir5zg=", - "dev": true, - "requires": { - "domelementtype": "1" - } - }, - "domutils": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", - "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", - "dev": true, - "requires": { - "dom-serializer": "0", - "domelementtype": "1" - } - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "entities": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.0.0.tgz", - "integrity": "sha1-sph6o4ITR/zeZCsk/fyeT7cSvyY=", - "dev": true - }, - "es-abstract": { - "version": "1.17.5", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz", - "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==", - "dev": true, - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.1.5", - "is-regex": "^1.0.5", - "object-inspect": "^1.7.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.0", - "string.prototype.trimleft": "^2.1.1", - "string.prototype.trimright": "^2.1.1" - } - }, - "es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - } - }, - "es6-error": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", - "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true - }, - "eventemitter2": { - "version": "0.4.14", - "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", - "integrity": "sha1-j2G3XN4BKy6esoTUVFWDtWQ7Yas=", - "dev": true - }, - "exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", - "dev": true - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "expand-tilde": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", - "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", "dev": true, - "requires": { - "homedir-polyfill": "^1.0.1" + "license": "MIT", + "engines": { + "node": ">=10" } }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", "dev": true, - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, + "license": "MPL-2.0", "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } + "funding": { + "url": "https://github.com/sponsors/antonk52" } }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } + "license": "MIT" }, - "find-cache-dir": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz", - "integrity": "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==", + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", "dev": true, - "requires": { - "commondir": "^1.0.1", - "make-dir": "^3.0.2", - "pkg-dir": "^4.1.0" + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, - "findup-sync": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.3.0.tgz", - "integrity": "sha1-N5MKpdgWt3fANEXhlmzGeQpMCxY=", + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, - "requires": { - "glob": "~5.0.0" - }, + "license": "MIT", "dependencies": { - "glob": { - "version": "5.0.15", - "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", - "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", - "dev": true, - "requires": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } + "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "fined": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz", - "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==", + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", "dev": true, - "requires": { - "expand-tilde": "^2.0.2", - "is-plain-object": "^2.0.3", - "object.defaults": "^1.1.0", - "object.pick": "^1.2.0", - "parse-filepath": "^1.0.1" + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" } }, - "flagged-respawn": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz", - "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==", - "dev": true - }, - "flat": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz", - "integrity": "sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw==", + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, - "requires": { - "is-buffer": "~2.0.3" - } + "license": "MIT" }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", - "dev": true - }, - "for-own": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", - "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", "dev": true, - "requires": { - "for-in": "^1.0.1" + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" } }, - "foreground-child": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", - "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "dev": true, - "requires": { - "cross-spawn": "^7.0.0", - "signal-exit": "^3.0.2" + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "dev": true, - "requires": { - "map-cache": "^0.2.2" + "license": "MIT", + "engines": { + "node": ">=0.10.0" } }, - "fromentries": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.2.0.tgz", - "integrity": "sha512-33X7H/wdfO99GdRLLgkjUrD4geAFdq/Uv0kl3HD4da6HDixd2GUg8Mw7dahLCV9r/EARkmtYBB6Tch4EEokFTQ==", - "dev": true - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" }, - "fsevents": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.2.tgz", - "integrity": "sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA==", + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "dev": true, - "optional": true + "license": "MIT" }, - "function-bind": { + "node_modules/picocolors": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "gensync": { - "version": "1.0.0-beta.1", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz", - "integrity": "sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==", - "dev": true - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true - }, - "get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", - "dev": true - }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", - "dev": true - }, - "getobject": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/getobject/-/getobject-1.0.0.tgz", - "integrity": "sha512-tbUz6AKKKr2YiMB+fLWIgq5ZeBOobop9YMMAU9dC54/ot2ksMXt3DOFyBuhZw6ptcVszEykgByK20j7W9jHFag==", - "dev": true - }, - "glob": { - "version": "7.1.7", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", - "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" }, - "dependencies": { - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - } + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "glob-parent": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", - "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", "dev": true, - "requires": { - "is-glob": "^4.0.1" + "license": "MIT", + "engines": { + "node": ">= 6" } }, - "global-modules": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", - "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", "dev": true, - "requires": { - "global-prefix": "^1.0.1", - "is-windows": "^1.0.1", - "resolve-dir": "^1.0.0" + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" } }, - "global-prefix": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", - "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", "dev": true, - "requires": { - "expand-tilde": "^2.0.2", - "homedir-polyfill": "^1.0.1", - "ini": "^1.3.4", - "is-windows": "^1.0.1", - "which": "^1.2.14" + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" } }, - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true - }, - "graceful-fs": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", - "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==", - "dev": true - }, - "growl": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", - "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", - "dev": true - }, - "grunt": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.3.0.tgz", - "integrity": "sha512-6ILlMXv11/4cxuhSMfSU+SfvbxrPuqZrAtLN64+tZpQ3DAKfSQPQHRbTjSbdtxfyQhGZPtN0bDZJ/LdCM5WXXA==", - "dev": true, - "requires": { - "dateformat": "~3.0.3", - "eventemitter2": "~0.4.13", - "exit": "~0.1.2", - "findup-sync": "~0.3.0", - "glob": "~7.1.6", - "grunt-cli": "~1.3.2", - "grunt-known-options": "~1.1.0", - "grunt-legacy-log": "~3.0.0", - "grunt-legacy-util": "~2.0.0", - "iconv-lite": "~0.4.13", - "js-yaml": "~3.14.0", - "minimatch": "~3.0.4", - "mkdirp": "~1.0.4", - "nopt": "~3.0.6", - "rimraf": "~3.0.2" - }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", "dependencies": { - "grunt-cli": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.3.2.tgz", - "integrity": "sha512-8OHDiZZkcptxVXtMfDxJvmN7MVJNE8L/yIcPb4HB7TlyFD1kDvjHrb62uhySsU14wJx9ORMnTuhRMQ40lH/orQ==", - "dev": true, - "requires": { - "grunt-known-options": "~1.1.0", - "interpret": "~1.1.0", - "liftoff": "~2.5.0", - "nopt": "~4.0.1", - "v8flags": "~3.1.1" - }, - "dependencies": { - "nopt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", - "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", - "dev": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - } - } + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true }, - "js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } + "postcss": { + "optional": true }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } + "tsx": { + "optional": true }, - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true + "yaml": { + "optional": true } } }, - "grunt-contrib-jshint": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/grunt-contrib-jshint/-/grunt-contrib-jshint-2.1.0.tgz", - "integrity": "sha512-65S2/C/6RfjY/umTxfwXXn+wVvaYmykHkHSsW6Q6rhkbv3oudTEgqnFFZvWzWCoHUb+3GMZLbP3oSrNyvshmIQ==", + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "dev": true, - "requires": { - "chalk": "^2.4.2", - "hooker": "^0.2.3", - "jshint": "~2.10.2" + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, - "grunt-known-options": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/grunt-known-options/-/grunt-known-options-1.1.1.tgz", - "integrity": "sha512-cHwsLqoighpu7TuYj5RonnEuxGVFnztcUqTqp5rXFGYL4OuPFofwC4Ycg7n9fYwvK6F5WbYgeVOwph9Crs2fsQ==", - "dev": true - }, - "grunt-legacy-log": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-3.0.0.tgz", - "integrity": "sha512-GHZQzZmhyq0u3hr7aHW4qUH0xDzwp2YXldLPZTCjlOeGscAOWWPftZG3XioW8MasGp+OBRIu39LFx14SLjXRcA==", - "dev": true, - "requires": { - "colors": "~1.1.2", - "grunt-legacy-log-utils": "~2.1.0", - "hooker": "~0.2.3", - "lodash": "~4.17.19" - } - }, - "grunt-legacy-log-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-2.1.0.tgz", - "integrity": "sha512-lwquaPXJtKQk0rUM1IQAop5noEpwFqOXasVoedLeNzaibf/OPWjKYvvdqnEHNmU+0T0CaReAXIbGo747ZD+Aaw==", - "dev": true, - "requires": { - "chalk": "~4.1.0", - "lodash": "~4.17.19" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", - "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "grunt-legacy-util": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-2.0.1.tgz", - "integrity": "sha512-2bQiD4fzXqX8rhNdXkAywCadeqiPiay0oQny77wA2F3WF4grPJXCvAcyoWUJV+po/b15glGkxuSiQCK299UC2w==", - "dev": true, - "requires": { - "async": "~3.2.0", - "exit": "~0.1.2", - "getobject": "~1.0.0", - "hooker": "~0.2.3", - "lodash": "~4.17.21", - "underscore.string": "~3.3.5", - "which": "~2.0.2" - }, - "dependencies": { - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } - } - }, - "grunt-simple-mocha": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/grunt-simple-mocha/-/grunt-simple-mocha-0.4.1.tgz", - "integrity": "sha1-V5RJJJ6vCoGHj6cvPtq1FF1F/Xc=", - "dev": true, - "requires": { - "mocha": "*" - } - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true, - "requires": { - "function-bind": "^1.1.1" + "license": "MIT", + "engines": { + "node": ">=8" } }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "has-symbols": { + "node_modules/rolldown": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", - "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", - "dev": true - }, - "has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.1.tgz", + "integrity": "sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==", "dev": true, - "requires": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - } - }, - "has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, + "license": "MIT", "dependencies": { - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "hasha": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.0.tgz", - "integrity": "sha512-2W+jKdQbAdSIrggA8Q35Br8qKadTrqCTC8+XZvBWepKDK6m9XkX6Iz1a2yh2KP01kzAR/dpuMeUnocoLYDcskw==", - "dev": true, - "requires": { - "is-stream": "^2.0.0", - "type-fest": "^0.8.0" - } - }, - "he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true - }, - "homedir-polyfill": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", - "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", - "dev": true, - "requires": { - "parse-passwd": "^1.0.0" - } - }, - "hooker": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", - "integrity": "sha1-uDT3I8xKJCqmWWNFnfbZhMXT2Vk=", - "dev": true - }, - "html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true - }, - "htmlparser2": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz", - "integrity": "sha1-mWwosZFRaovoZQGn15dX5ccMEGg=", - "dev": true, - "requires": { - "domelementtype": "1", - "domhandler": "2.3", - "domutils": "1.5", - "entities": "1.0", - "readable-stream": "1.1" - } - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, - "ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true - }, - "interpret": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", - "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=", - "dev": true - }, - "is-absolute": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", - "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", - "dev": true, - "requires": { - "is-relative": "^1.0.0", - "is-windows": "^1.0.1" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" + "@oxc-project/types": "=0.130.0", + "@rolldown/pluginutils": "^1.0.0" }, - "dependencies": { - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "requires": { - "binary-extensions": "^2.0.0" - } - }, - "is-buffer": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", - "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==", - "dev": true - }, - "is-callable": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", - "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", - "dev": true - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" + "bin": { + "rolldown": "bin/cli.mjs" }, - "dependencies": { - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-date-object": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", - "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", - "dev": true - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" + "engines": { + "node": "^20.19.0 || >=22.12.0" }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.1", + "@rolldown/binding-darwin-arm64": "1.0.1", + "@rolldown/binding-darwin-x64": "1.0.1", + "@rolldown/binding-freebsd-x64": "1.0.1", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.1", + "@rolldown/binding-linux-arm64-gnu": "1.0.1", + "@rolldown/binding-linux-arm64-musl": "1.0.1", + "@rolldown/binding-linux-ppc64-gnu": "1.0.1", + "@rolldown/binding-linux-s390x-gnu": "1.0.1", + "@rolldown/binding-linux-x64-gnu": "1.0.1", + "@rolldown/binding-linux-x64-musl": "1.0.1", + "@rolldown/binding-openharmony-arm64": "1.0.1", + "@rolldown/binding-wasm32-wasi": "1.0.1", + "@rolldown/binding-win32-arm64-msvc": "1.0.1", + "@rolldown/binding-win32-x64-msvc": "1.0.1" + } + }, + "node_modules/rollup": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.3.tgz", + "integrity": "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==", + "dev": true, + "license": "MIT", "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.3", + "@rollup/rollup-android-arm64": "4.60.3", + "@rollup/rollup-darwin-arm64": "4.60.3", + "@rollup/rollup-darwin-x64": "4.60.3", + "@rollup/rollup-freebsd-arm64": "4.60.3", + "@rollup/rollup-freebsd-x64": "4.60.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.3", + "@rollup/rollup-linux-arm-musleabihf": "4.60.3", + "@rollup/rollup-linux-arm64-gnu": "4.60.3", + "@rollup/rollup-linux-arm64-musl": "4.60.3", + "@rollup/rollup-linux-loong64-gnu": "4.60.3", + "@rollup/rollup-linux-loong64-musl": "4.60.3", + "@rollup/rollup-linux-ppc64-gnu": "4.60.3", + "@rollup/rollup-linux-ppc64-musl": "4.60.3", + "@rollup/rollup-linux-riscv64-gnu": "4.60.3", + "@rollup/rollup-linux-riscv64-musl": "4.60.3", + "@rollup/rollup-linux-s390x-gnu": "4.60.3", + "@rollup/rollup-linux-x64-gnu": "4.60.3", + "@rollup/rollup-linux-x64-musl": "4.60.3", + "@rollup/rollup-openbsd-x64": "4.60.3", + "@rollup/rollup-openharmony-arm64": "4.60.3", + "@rollup/rollup-win32-arm64-msvc": "4.60.3", + "@rollup/rollup-win32-ia32-msvc": "4.60.3", + "@rollup/rollup-win32-x64-gnu": "4.60.3", + "@rollup/rollup-win32-x64-msvc": "4.60.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true + "license": "ISC" }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", "dev": true, - "requires": { - "isobject": "^3.0.1" + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" } }, - "is-regex": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", - "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, - "requires": { - "has": "^1.0.3" + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" } }, - "is-relative": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", - "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true, - "requires": { - "is-unc-path": "^1.0.0" - } + "license": "MIT" }, - "is-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", - "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", - "dev": true - }, - "is-symbol": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", - "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", "dev": true, - "requires": { - "has-symbols": "^1.0.1" - } + "license": "MIT" }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true - }, - "is-unc-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", - "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", "dev": true, - "requires": { - "unc-path-regex": "^0.1.2" - } - }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true - }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "istanbul-lib-coverage": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", - "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", - "dev": true - }, - "istanbul-lib-hook": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", - "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", - "dev": true, - "requires": { - "append-transform": "^2.0.0" - } - }, - "istanbul-lib-instrument": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.1.tgz", - "integrity": "sha512-imIchxnodll7pvQBYOqUu88EufLCU56LMeFPZZM/fJZ1irYcYdqroaV+ACK1Ila8ls09iEYArp+nqyC6lW1Vfg==", - "dev": true, - "requires": { - "@babel/core": "^7.7.5", - "@babel/parser": "^7.7.5", - "@babel/template": "^7.7.4", - "@babel/traverse": "^7.7.4", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.0.0", - "semver": "^6.3.0" - }, + "license": "MIT", "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "istanbul-lib-processinfo": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.2.tgz", - "integrity": "sha512-kOwpa7z9hme+IBPZMzQ5vdQj8srYgAtaRqeI48NGmAQ+/5yKiHLV0QbYqQpxsdEF0+w14SoB8YbnHKcXE2KnYw==", - "dev": true, - "requires": { - "archy": "^1.0.0", - "cross-spawn": "^7.0.0", - "istanbul-lib-coverage": "^3.0.0-alpha.1", - "make-dir": "^3.0.0", - "p-map": "^3.0.0", - "rimraf": "^3.0.0", - "uuid": "^3.3.3" + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" }, - "dependencies": { - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } - } - }, - "istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", - "dev": true, - "requires": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^3.0.0", - "supports-color": "^7.1.0" + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" }, - "dependencies": { - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } + "engines": { + "node": ">=16 || 14 >=14.17" } }, - "istanbul-lib-source-maps": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz", - "integrity": "sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg==", + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", "dev": true, - "requires": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - }, + "license": "MIT", "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } + "any-promise": "^1.0.0" } }, - "istanbul-reports": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.1.tgz", - "integrity": "sha512-Vm9xwCiQ8t2cNNnckyeAV0UdxKpcQUz4nMxsBvIu8n2kmPSiyb5uaF/8LpmKr+yqL/MdOXaX2Nmdo4Qyxium9Q==", + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", "dev": true, - "requires": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" } }, - "jit-grunt": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/jit-grunt/-/jit-grunt-0.10.0.tgz", - "integrity": "sha1-AIw6f+Hpa9DYTiYOofoXg0V/ecI=", - "dev": true - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true + "license": "MIT" }, - "jshint": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/jshint/-/jshint-2.10.3.tgz", - "integrity": "sha512-d8AoXcNNYzmm7cdmulQ3dQApbrPYArtVBO6n4xOICe4QsXGNHCAKDcFORzqP52LhK61KX0VhY39yYzCsNq+bxQ==", + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", "dev": true, - "requires": { - "cli": "~1.0.0", - "console-browserify": "1.1.x", - "exit": "0.1.x", - "htmlparser2": "3.8.x", - "lodash": "~4.17.11", - "minimatch": "~3.0.2", - "shelljs": "0.3.x", - "strip-json-comments": "1.0.x" - } + "license": "MIT" }, - "json5": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.2.tgz", - "integrity": "sha512-MoUOQ4WdiN3yxhm7NEVJSJrieAo5hNSLQ5sj05OTRHPL9HOBy8u4Bu88jsC1jvqAdN+E1bJmsUcZH+1HQxliqQ==", + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - }, - "liftoff": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-2.5.0.tgz", - "integrity": "sha1-IAkpG7Mc6oYbvxCnwVooyvdcMew=", - "dev": true, - "requires": { - "extend": "^3.0.0", - "findup-sync": "^2.0.0", - "fined": "^1.0.1", - "flagged-respawn": "^1.0.0", - "is-plain-object": "^2.0.4", - "object.map": "^1.0.0", - "rechoir": "^0.6.2", - "resolve": "^1.1.7" - }, + "license": "MIT", "dependencies": { - "findup-sync": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz", - "integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=", - "dev": true, - "requires": { - "detect-file": "^1.0.0", - "is-glob": "^3.1.0", - "micromatch": "^3.0.4", - "resolve-dir": "^1.0.1" - } - }, - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "requires": { - "is-extglob": "^2.1.0" - } - } + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" } }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", "dev": true, - "requires": { - "p-locate": "^4.1.0" + "license": "MIT", + "engines": { + "node": ">=14.0.0" } }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, - "lodash.flattendeep": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", - "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", - "dev": true - }, - "log-symbols": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", - "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==", + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", "dev": true, - "requires": { - "chalk": "^2.4.2" + "license": "MIT", + "bin": { + "tree-kill": "cli.js" } }, - "make-dir": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.2.tgz", - "integrity": "sha512-rYKABKutXa6vXTXhoV18cBE7PaewPXHe/Bdq4v+ZLMhxbWApkFFplT0LcbMW+6BbjnQXzZ/sAvSE/JdguApG5w==", + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", "dev": true, - "requires": { - "semver": "^6.0.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "make-iterator": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", - "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==", - "dev": true, - "requires": { - "kind-of": "^6.0.2" - } - }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", - "dev": true - }, - "map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", - "dev": true, - "requires": { - "object-visit": "^1.0.0" - } - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - }, - "dependencies": { - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - } - } - } + "license": "Apache-2.0" }, - "minimatch": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz", - "integrity": "sha1-Kk5AkLlrLbBqnX3wEFWmKnfJt3Q=", + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "requires": { - "brace-expansion": "^1.0.0" - } - }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true + "license": "0BSD", + "optional": true }, - "mixin-deep": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", - "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "node_modules/tsup": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", + "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", "dev": true, - "requires": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, + "license": "MIT", "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "mkdirp": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.3.tgz", - "integrity": "sha512-P+2gwrFqx8lhew375MQHHeTlY8AuOJSrGf0R5ddkEndUkmwpgUob/vQuBD1V22/Cw1/lJr4x+EjllSezBThzBg==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "mocha": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-7.1.1.tgz", - "integrity": "sha512-3qQsu3ijNS3GkWcccT5Zw0hf/rWvu1fTN9sPvEd81hlwsr30GX2GcDSSoBxo24IR8FelmrAydGC6/1J5QQP4WA==", - "dev": true, - "requires": { - "ansi-colors": "3.2.3", - "browser-stdout": "1.3.1", - "chokidar": "3.3.0", - "debug": "3.2.6", - "diff": "3.5.0", - "escape-string-regexp": "1.0.5", - "find-up": "3.0.0", - "glob": "7.1.3", - "growl": "1.10.5", - "he": "1.2.0", - "js-yaml": "3.13.1", - "log-symbols": "3.0.0", - "minimatch": "3.0.4", - "mkdirp": "0.5.3", - "ms": "2.1.1", - "node-environment-flags": "1.0.6", - "object.assign": "4.1.0", - "strip-json-comments": "2.0.1", - "supports-color": "6.0.0", - "which": "1.3.1", - "wide-align": "1.1.3", - "yargs": "13.3.2", - "yargs-parser": "13.1.2", - "yargs-unparser": "1.6.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - }, - "cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", - "dev": true, - "requires": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" - } - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true - }, - "supports-color": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", - "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" - } - }, - "yargs": { - "version": "13.3.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", - "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", - "dev": true, - "requires": { - "cliui": "^5.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.1.2" - } - }, - "yargs-parser": { - "version": "13.1.2", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", - "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - } - }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", - "dev": true - }, - "nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - } - }, - "node-environment-flags": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.6.tgz", - "integrity": "sha512-5Evy2epuL+6TM0lCQGpFIj6KwiEsGh1SrHUhTbNX+sLbBtjidPZFAnVK9y5yU1+h//RitLbRHTIMyxQPtxMdHw==", - "dev": true, - "requires": { - "object.getownpropertydescriptors": "^2.0.3", - "semver": "^5.7.0" - } - }, - "node-preload": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", - "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==", - "dev": true, - "requires": { - "process-on-spawn": "^1.0.0" - } - }, - "nopt": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", - "dev": true, - "requires": { - "abbrev": "1" - } - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "nyc": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.0.0.tgz", - "integrity": "sha512-qcLBlNCKMDVuKb7d1fpxjPR8sHeMVX0CHarXAVzrVWoFrigCkYR8xcrjfXSPi5HXM7EU78L6ywO7w1c5rZNCNg==", - "dev": true, - "requires": { - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "caching-transform": "^4.0.0", - "convert-source-map": "^1.7.0", - "decamelize": "^1.2.0", - "find-cache-dir": "^3.2.0", - "find-up": "^4.1.0", - "foreground-child": "^2.0.0", - "glob": "^7.1.6", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-hook": "^3.0.0", - "istanbul-lib-instrument": "^4.0.0", - "istanbul-lib-processinfo": "^2.0.2", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.0.0", - "js-yaml": "^3.13.1", - "make-dir": "^3.0.0", - "node-preload": "^0.2.0", - "p-map": "^3.0.0", - "process-on-spawn": "^1.0.0", + "bundle-require": "^5.1.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "consola": "^3.4.0", + "debug": "^4.4.0", + "esbuild": "^0.27.0", + "fix-dts-default-cjs-exports": "^1.0.0", + "joycon": "^3.1.1", + "picocolors": "^1.1.1", + "postcss-load-config": "^6.0.1", "resolve-from": "^5.0.0", - "rimraf": "^3.0.0", - "signal-exit": "^3.0.2", - "spawn-wrap": "^2.0.0", - "test-exclude": "^6.0.0", - "uuid": "^3.3.3", - "yargs": "^15.0.2" + "rollup": "^4.34.8", + "source-map": "^0.7.6", + "sucrase": "^3.35.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.11", + "tree-kill": "^1.2.2" }, - "dependencies": { - "balanced-match": { - "version": "1.0.0", - "resolved": false, - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": false, - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true }, - "minimatch": { - "version": "3.0.4", - "resolved": false, - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } + "@swc/core": { + "optional": true }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true + "postcss": { + "optional": true }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } + "typescript": { + "optional": true } } }, - "object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, - "requires": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } + "engines": { + "node": ">=14.17" } }, - "object-inspect": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", - "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", - "dev": true - }, - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true - }, - "object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", - "dev": true, - "requires": { - "isobject": "^3.0.0" - } - }, - "object.assign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", - "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "function-bind": "^1.1.1", - "has-symbols": "^1.0.0", - "object-keys": "^1.0.11" - } - }, - "object.defaults": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", - "integrity": "sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8=", - "dev": true, - "requires": { - "array-each": "^1.0.1", - "array-slice": "^1.0.0", - "for-own": "^1.0.0", - "isobject": "^3.0.0" - } - }, - "object.getownpropertydescriptors": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz", - "integrity": "sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1" - } - }, - "object.map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz", - "integrity": "sha1-z4Plncj8wK1fQlDh94s7gb2AHTc=", + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", "dev": true, - "requires": { - "for-own": "^1.0.0", - "make-iterator": "^1.0.0" - } + "license": "MIT" }, - "object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, - "requires": { - "isobject": "^3.0.1" - } + "license": "MIT" }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "node_modules/vite": { + "version": "8.0.13", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.13.tgz", + "integrity": "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==", "dev": true, - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "dev": true - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true - }, - "osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "dev": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "p-limit": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz", - "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "p-map": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", - "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", - "dev": true, - "requires": { - "aggregate-error": "^3.0.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "package-hash": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", - "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.15", - "hasha": "^5.0.0", - "lodash.flattendeep": "^4.4.0", - "release-zalgo": "^1.0.0" - } - }, - "parse-filepath": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", - "integrity": "sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE=", - "dev": true, - "requires": { - "is-absolute": "^1.0.0", - "map-cache": "^0.2.0", - "path-root": "^0.1.1" - } - }, - "parse-passwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", - "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", - "dev": true - }, - "pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true - }, - "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", - "dev": true - }, - "path-root": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", - "integrity": "sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc=", - "dev": true, - "requires": { - "path-root-regex": "^0.1.0" - } - }, - "path-root-regex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", - "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=", - "dev": true - }, - "pathval": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", - "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", - "dev": true - }, - "picomatch": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", - "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", - "dev": true - }, - "pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "requires": { - "find-up": "^4.0.0" - }, + "license": "MIT", "dependencies": { - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - } - } - }, - "posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", - "dev": true - }, - "process-on-spawn": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", - "integrity": "sha512-1WsPDsUSMmZH5LeMLegqkPDrsGgsWwk1Exipy2hvB0o/F0ASzbpIctSCcZIK1ykJvtTJULEH+20WOFjMvGnCTg==", - "dev": true, - "requires": { - "fromentries": "^1.2.0" - } - }, - "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "readdirp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.2.0.tgz", - "integrity": "sha512-crk4Qu3pmXwgxdSgGhgA/eXiJAPQiX4GMOZZMXnqKxHX7TaoL+3gQVo/WeuAiogr07DpnfjIMpXXa+PAIvwPGQ==", - "dev": true, - "requires": { - "picomatch": "^2.0.4" - } - }, - "rechoir": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", - "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", - "dev": true, - "requires": { - "resolve": "^1.1.6" - } - }, - "regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - } - }, - "release-zalgo": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", - "integrity": "sha1-CXALflB0Mpc5Mw5TXFqQ+2eFFzA=", - "dev": true, - "requires": { - "es6-error": "^4.0.1" - } - }, - "repeat-element": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz", - "integrity": "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==", - "dev": true - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "dev": true - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true - }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true - }, - "resolve": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz", - "integrity": "sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==", - "dev": true, - "requires": { - "path-parse": "^1.0.6" - } - }, - "resolve-dir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", - "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", - "dev": true, - "requires": { - "expand-tilde": "^2.0.0", - "global-modules": "^1.0.0" - } - }, - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true - }, - "resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", - "dev": true - }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", - "dev": true, - "requires": { - "ret": "~0.1.10" - } - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true - }, - "set-value": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", - "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.14", + "rolldown": "1.0.1", + "tinyglobby": "^0.2.16" }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true - }, - "shelljs": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.3.0.tgz", - "integrity": "sha1-NZbmMHp4FUT1kfN9phg2DzHbV7E=", - "dev": true - }, - "signal-exit": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", - "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", - "dev": true - }, - "snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "dev": true, - "requires": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" + "bin": { + "vite": "bin/vite.js" }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true }, - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } + "@vitejs/devtools": { + "optional": true }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } + "esbuild": { + "optional": true }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, - "snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "dev": true, - "requires": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } + "jiti": { + "optional": true }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } + "less": { + "optional": true }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "dev": true, - "requires": { - "kind-of": "^3.2.0" - }, - "dependencies": { - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true + "sass": { + "optional": true }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - }, - "source-map-resolve": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", - "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", - "dev": true, - "requires": { - "atob": "^2.1.2", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, - "source-map-url": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", - "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==", - "dev": true - }, - "spawn-wrap": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", - "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", - "dev": true, - "requires": { - "foreground-child": "^2.0.0", - "is-windows": "^1.0.2", - "make-dir": "^3.0.0", - "rimraf": "^3.0.0", - "signal-exit": "^3.0.2", - "which": "^2.0.1" - }, - "dependencies": { - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true + "sass-embedded": { + "optional": true }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } + "stylus": { + "optional": true }, - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } + "sugarss": { + "optional": true }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } + "terser": { + "optional": true }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } + "tsx": { + "optional": true }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } + "yaml": { + "optional": true } } }, - "split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "node_modules/vitest": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.6.tgz", + "integrity": "sha512-6lvjbS3p9b4CrdCmguzbh2/4uoXhGE2q71R4OX5sqF9R1bo9Xd6fGrMAfvp5wnCzlBnFVdCOp6onuTQVbo8iUQ==", "dev": true, - "requires": { - "extend-shallow": "^3.0.0" - } - }, - "sprintf-js": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", - "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", - "dev": true - }, - "static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", - "dev": true, - "requires": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, + "license": "MIT", "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - } - }, - "string.prototype.trimleft": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", - "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" - } - }, - "string.prototype.trimright": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", - "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - }, - "strip-json-comments": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz", - "integrity": "sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E=", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" + "@vitest/expect": "4.1.6", + "@vitest/mocker": "4.1.6", + "@vitest/pretty-format": "4.1.6", + "@vitest/runner": "4.1.6", + "@vitest/snapshot": "4.1.6", + "@vitest/spy": "4.1.6", + "@vitest/utils": "4.1.6", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" }, - "dependencies": { - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - } - } - }, - "test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "requires": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" + "bin": { + "vitest": "vitest.mjs" }, - "dependencies": { - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.6", + "@vitest/browser-preview": "4.1.6", + "@vitest/browser-webdriverio": "4.1.6", + "@vitest/coverage-istanbul": "4.1.6", + "@vitest/coverage-v8": "4.1.6", + "@vitest/ui": "4.1.6", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } + "@opentelemetry/api": { + "optional": true }, - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } + "@types/node": { + "optional": true }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - } - } - }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true - }, - "to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true + "@vitest/browser-playwright": { + "optional": true }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "dev": true, - "requires": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true - }, - "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true - }, - "typedarray-to-buffer": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "dev": true, - "requires": { - "is-typedarray": "^1.0.0" - } - }, - "unc-path-regex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", - "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=", - "dev": true - }, - "underscore.string": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.5.tgz", - "integrity": "sha512-g+dpmgn+XBneLmXXo+sGlW5xQEt4ErkS3mgeN2GFbremYeMBSJKr9Wf2KJplQVaiPY/f7FN6atosWYNm9ovrYg==", - "dev": true, - "requires": { - "sprintf-js": "^1.0.3", - "util-deprecate": "^1.0.2" - } - }, - "union-value": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", - "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^2.0.1" - } - }, - "unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", - "dev": true, - "requires": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "dependencies": { - "has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", - "dev": true, - "requires": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", - "dev": true + "@vitest/browser-preview": { + "optional": true }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - } - } - }, - "urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", - "dev": true - }, - "use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", - "dev": true - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true - }, - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "dev": true - }, - "v8flags": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.1.3.tgz", - "integrity": "sha512-amh9CCg3ZxkzQ48Mhcb8iX7xpAfYJgePHxWMQCBWECpOSqJUXgY26ncA61UTV0BkPqfhcy6mzwCIoP4ygxpW8w==", - "dev": true, - "requires": { - "homedir-polyfill": "^1.0.1" - } - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", - "dev": true - }, - "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "dev": true, - "requires": { - "string-width": "^1.0.2 || 2" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true + "@vitest/browser-webdriverio": { + "optional": true }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true + "@vitest/coverage-istanbul": { + "optional": true }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } + "@vitest/coverage-v8": { + "optional": true }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } + "@vitest/ui": { + "optional": true }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } + "happy-dom": { + "optional": true }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - } - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "write-file-atomic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", - "dev": true, - "requires": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" - } - }, - "y18n": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", - "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==", - "dev": true - }, - "yargs": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.1.tgz", - "integrity": "sha512-92O1HWEjw27sBfgmXiixJWT5hRBp2eobqXicLtPBIDBhYB+1HpwZlXmbW2luivBJHBzki+7VyCLRtAkScbTBQA==", - "dev": true, - "requires": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.1" - }, - "dependencies": { - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } + "jsdom": { + "optional": true }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true + "vite": { + "optional": false } } }, - "yargs-parser": { - "version": "18.1.2", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.2.tgz", - "integrity": "sha512-hlIPNR3IzC1YuL1c2UwwDKpXlNFBqD1Fswwh1khz5+d8Cq/8yc/Mn0i+rQXduu8hcrFKvO7Eryk+09NecTQAAQ==", + "node_modules/vitest/node_modules/tinyexec": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz", + "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==", "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - }, - "dependencies": { - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - } + "license": "MIT", + "engines": { + "node": ">=18" } }, - "yargs-unparser": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.0.tgz", - "integrity": "sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw==", + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", "dev": true, - "requires": { - "flat": "^4.1.0", - "lodash": "^4.17.15", - "yargs": "^13.3.0" - }, + "license": "MIT", "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - }, - "cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", - "dev": true, - "requires": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" - } - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - }, - "wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" - } - }, - "yargs": { - "version": "13.3.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", - "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", - "dev": true, - "requires": { - "cliui": "^5.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.1.2" - } - }, - "yargs-parser": { - "version": "13.1.2", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", - "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" } } } diff --git a/package.json b/package.json index 6ee65ef..ea338f4 100644 --- a/package.json +++ b/package.json @@ -1,27 +1,66 @@ { "name": "node-rsa", - "version": "1.1.1", - "description": "Node.js RSA library", - "main": "src/NodeRSA.js", + "version": "2.0.0-rc.0", + "description": "RSA library for Node.js and browsers", + "type": "module", + "engines": { + "node": ">=20" + }, + "main": "./dist/index.node.cjs", + "module": "./dist/index.node.js", + "browser": "./dist/index.browser.js", + "types": "./dist/index.node.d.ts", + "exports": { + ".": { + "types": "./dist/index.node.d.ts", + "browser": { + "import": "./dist/index.browser.js" + }, + "node": { + "import": "./dist/index.node.js", + "require": "./dist/index.node.cjs" + }, + "default": "./dist/index.browser.js" + }, + "./package.json": "./package.json" + }, + "sideEffects": false, + "files": [ + "dist", + "README.md", + "CHANGELOG.md", + "MIGRATION.md", + "LICENSE" + ], "scripts": { - "test": "grunt test" + "build": "tsup", + "test": "vitest run", + "test:watch": "vitest", + "bench": "vitest bench --run --config=vitest.bench.config.ts", + "lint": "biome check .", + "lint:fix": "biome check --write .", + "format": "biome format --write .", + "typecheck": "tsc --noEmit", + "check": "npm run typecheck && npm run lint && npm run test && npm run build && npm run check:bundle-size", + "check:bundle-size": "node scripts/check-bundle-size.mjs" }, "repository": { "type": "git", "url": "https://github.com/rzcoder/node-rsa.git" }, "keywords": [ - "node", "rsa", "crypto", - "assymetric", + "asymmetric", "encryption", "decryption", "sign", "verify", "pkcs1", "oaep", - "pss" + "pss", + "typescript", + "browser" ], "author": "rzcoder", "license": "MIT", @@ -29,16 +68,15 @@ "url": "https://github.com/rzcoder/node-rsa/issues" }, "homepage": "https://github.com/rzcoder/node-rsa", - "devDependencies": { - "chai": "^4.2.0", - "grunt": "^1.1.0", - "grunt-contrib-jshint": "^2.1.0", - "grunt-simple-mocha": "0.4.1", - "jit-grunt": "0.10.0", - "lodash": "^4.17.15", - "nyc": "^15.0.0" - }, "dependencies": { - "asn1": "^0.2.4" + "@noble/hashes": "^2.2.0" + }, + "devDependencies": { + "@biomejs/biome": "^2.4.15", + "@types/node": "^22.10.0", + "chai": "^6.2.2", + "tsup": "^8.5.1", + "typescript": "^6.0.3", + "vitest": "^4.1.6" } } diff --git a/scripts/check-bundle-size.mjs b/scripts/check-bundle-size.mjs new file mode 100644 index 0000000..f1261b4 --- /dev/null +++ b/scripts/check-bundle-size.mjs @@ -0,0 +1,37 @@ +#!/usr/bin/env node +import { readFileSync, statSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { gzipSync } from 'node:zlib'; + +const root = resolve(import.meta.dirname, '..'); + +const BUDGETS = { + 'dist/index.browser.js': { raw: 120_000, gz: 30_000 }, + 'dist/index.node.js': { raw: 130_000, gz: 35_000 }, + 'dist/index.node.cjs': { raw: 130_000, gz: 35_000 }, +}; + +let failed = false; +for (const [rel, budget] of Object.entries(BUDGETS)) { + const path = resolve(root, rel); + const raw = statSync(path).size; + const gz = gzipSync(readFileSync(path)).length; + const rawOk = raw <= budget.raw; + const gzOk = gz <= budget.gz; + const mark = (ok) => (ok ? '✓' : '✗'); + console.log( + `${mark(rawOk && gzOk)} ${rel.padEnd(28)} raw=${kb(raw)} (${mark(rawOk)} ≤${kb(budget.raw)}) gz=${kb(gz)} (${mark(gzOk)} ≤${kb(budget.gz)})`, + ); + if (!rawOk || !gzOk) failed = true; +} + +if (failed) { + console.error( + '\nBundle size budget exceeded. Update the budget in scripts/check-bundle-size.mjs or trim the build.', + ); + process.exit(1); +} + +function kb(n) { + return `${(n / 1024).toFixed(1)} KB`; +} diff --git a/src/NodeRSA.js b/src/NodeRSA.js deleted file mode 100644 index 190fd66..0000000 --- a/src/NodeRSA.js +++ /dev/null @@ -1,398 +0,0 @@ -/*! - * RSA library for Node.js - * - * Author: rzcoder - * License MIT - */ - -var constants = require('constants'); -var rsa = require('./libs/rsa.js'); -var crypt = require('crypto'); -var ber = require('asn1').Ber; -var _ = require('./utils')._; -var utils = require('./utils'); -var schemes = require('./schemes/schemes.js'); -var formats = require('./formats/formats.js'); - -if (typeof constants.RSA_NO_PADDING === "undefined") { - //patch for node v0.10.x, constants do not defined - constants.RSA_NO_PADDING = 3; -} - -module.exports = (function () { - var SUPPORTED_HASH_ALGORITHMS = { - node10: ['md4', 'md5', 'ripemd160', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512'], - node: ['md4', 'md5', 'ripemd160', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512'], - iojs: ['md4', 'md5', 'ripemd160', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512'], - browser: ['md5', 'ripemd160', 'sha1', 'sha256', 'sha512'] - }; - - var DEFAULT_ENCRYPTION_SCHEME = 'pkcs1_oaep'; - var DEFAULT_SIGNING_SCHEME = 'pkcs1'; - - var DEFAULT_EXPORT_FORMAT = 'private'; - var EXPORT_FORMAT_ALIASES = { - 'private': 'pkcs1-private-pem', - 'private-der': 'pkcs1-private-der', - 'public': 'pkcs8-public-pem', - 'public-der': 'pkcs8-public-der', - }; - - /** - * @param key {string|buffer|object} Key in PEM format, or data for generate key {b: bits, e: exponent} - * @constructor - */ - function NodeRSA(key, format, options) { - if (!(this instanceof NodeRSA)) { - return new NodeRSA(key, format, options); - } - - if (_.isObject(format)) { - options = format; - format = undefined; - } - - this.$options = { - signingScheme: DEFAULT_SIGNING_SCHEME, - signingSchemeOptions: { - hash: 'sha256', - saltLength: null - }, - encryptionScheme: DEFAULT_ENCRYPTION_SCHEME, - encryptionSchemeOptions: { - hash: 'sha1', - label: null - }, - environment: utils.detectEnvironment(), - rsaUtils: this - }; - this.keyPair = new rsa.Key(); - this.$cache = {}; - - if (Buffer.isBuffer(key) || _.isString(key)) { - this.importKey(key, format); - } else if (_.isObject(key)) { - this.generateKeyPair(key.b, key.e); - } - - this.setOptions(options); - } - - /** - * Set and validate options for key instance - * @param options - */ - NodeRSA.prototype.setOptions = function (options) { - options = options || {}; - if (options.environment) { - this.$options.environment = options.environment; - } - - if (options.signingScheme) { - if (_.isString(options.signingScheme)) { - var signingScheme = options.signingScheme.toLowerCase().split('-'); - if (signingScheme.length == 1) { - if (SUPPORTED_HASH_ALGORITHMS.node.indexOf(signingScheme[0]) > -1) { - this.$options.signingSchemeOptions = { - hash: signingScheme[0] - }; - this.$options.signingScheme = DEFAULT_SIGNING_SCHEME; - } else { - this.$options.signingScheme = signingScheme[0]; - this.$options.signingSchemeOptions = { - hash: null - }; - } - } else { - this.$options.signingSchemeOptions = { - hash: signingScheme[1] - }; - this.$options.signingScheme = signingScheme[0]; - } - } else if (_.isObject(options.signingScheme)) { - this.$options.signingScheme = options.signingScheme.scheme || DEFAULT_SIGNING_SCHEME; - this.$options.signingSchemeOptions = _.omit(options.signingScheme, 'scheme'); - } - - if (!schemes.isSignature(this.$options.signingScheme)) { - throw Error('Unsupported signing scheme'); - } - - if (this.$options.signingSchemeOptions.hash && - SUPPORTED_HASH_ALGORITHMS[this.$options.environment].indexOf(this.$options.signingSchemeOptions.hash) === -1) { - throw Error('Unsupported hashing algorithm for ' + this.$options.environment + ' environment'); - } - } - - if (options.encryptionScheme) { - if (_.isString(options.encryptionScheme)) { - this.$options.encryptionScheme = options.encryptionScheme.toLowerCase(); - this.$options.encryptionSchemeOptions = {}; - } else if (_.isObject(options.encryptionScheme)) { - this.$options.encryptionScheme = options.encryptionScheme.scheme || DEFAULT_ENCRYPTION_SCHEME; - this.$options.encryptionSchemeOptions = _.omit(options.encryptionScheme, 'scheme'); - } - - if (!schemes.isEncryption(this.$options.encryptionScheme)) { - throw Error('Unsupported encryption scheme'); - } - - if (this.$options.encryptionSchemeOptions.hash && - SUPPORTED_HASH_ALGORITHMS[this.$options.environment].indexOf(this.$options.encryptionSchemeOptions.hash) === -1) { - throw Error('Unsupported hashing algorithm for ' + this.$options.environment + ' environment'); - } - } - - this.keyPair.setOptions(this.$options); - }; - - /** - * Generate private/public keys pair - * - * @param bits {int} length key in bits. Default 2048. - * @param exp {int} public exponent. Default 65537. - * @returns {NodeRSA} - */ - NodeRSA.prototype.generateKeyPair = function (bits, exp) { - bits = bits || 2048; - exp = exp || 65537; - - if (bits % 8 !== 0) { - throw Error('Key size must be a multiple of 8.'); - } - - this.keyPair.generate(bits, exp.toString(16)); - this.$cache = {}; - return this; - }; - - /** - * Importing key - * @param keyData {string|buffer|Object} - * @param format {string} - */ - NodeRSA.prototype.importKey = function (keyData, format) { - if (!keyData) { - throw Error("Empty key given"); - } - - if (format) { - format = EXPORT_FORMAT_ALIASES[format] || format; - } - - if (!formats.detectAndImport(this.keyPair, keyData, format) && format === undefined) { - throw Error("Key format must be specified"); - } - - this.$cache = {}; - - return this; - }; - - /** - * Exporting key - * @param [format] {string} - */ - NodeRSA.prototype.exportKey = function (format) { - format = format || DEFAULT_EXPORT_FORMAT; - format = EXPORT_FORMAT_ALIASES[format] || format; - - if (!this.$cache[format]) { - this.$cache[format] = formats.detectAndExport(this.keyPair, format); - } - - return this.$cache[format]; - }; - - /** - * Check if key pair contains private key - */ - NodeRSA.prototype.isPrivate = function () { - return this.keyPair.isPrivate(); - }; - - /** - * Check if key pair contains public key - * @param [strict] {boolean} - public key only, return false if have private exponent - */ - NodeRSA.prototype.isPublic = function (strict) { - return this.keyPair.isPublic(strict); - }; - - /** - * Check if key pair doesn't contains any data - */ - NodeRSA.prototype.isEmpty = function (strict) { - return !(this.keyPair.n || this.keyPair.e || this.keyPair.d); - }; - - /** - * Encrypting data method with public key - * - * @param buffer {string|number|object|array|Buffer} - data for encrypting. Object and array will convert to JSON string. - * @param encoding {string} - optional. Encoding for output result, may be 'buffer', 'binary', 'hex' or 'base64'. Default 'buffer'. - * @param source_encoding {string} - optional. Encoding for given string. Default utf8. - * @returns {string|Buffer} - */ - NodeRSA.prototype.encrypt = function (buffer, encoding, source_encoding) { - return this.$$encryptKey(false, buffer, encoding, source_encoding); - }; - - /** - * Decrypting data method with private key - * - * @param buffer {Buffer} - buffer for decrypting - * @param encoding - encoding for result string, can also take 'json' or 'buffer' for the automatic conversion of this type - * @returns {Buffer|object|string} - */ - NodeRSA.prototype.decrypt = function (buffer, encoding) { - return this.$$decryptKey(false, buffer, encoding); - }; - - /** - * Encrypting data method with private key - * - * Parameters same as `encrypt` method - */ - NodeRSA.prototype.encryptPrivate = function (buffer, encoding, source_encoding) { - return this.$$encryptKey(true, buffer, encoding, source_encoding); - }; - - /** - * Decrypting data method with public key - * - * Parameters same as `decrypt` method - */ - NodeRSA.prototype.decryptPublic = function (buffer, encoding) { - return this.$$decryptKey(true, buffer, encoding); - }; - - /** - * Encrypting data method with custom key - */ - NodeRSA.prototype.$$encryptKey = function (usePrivate, buffer, encoding, source_encoding) { - try { - var res = this.keyPair.encrypt(this.$getDataForEncrypt(buffer, source_encoding), usePrivate); - - if (encoding == 'buffer' || !encoding) { - return res; - } else { - return res.toString(encoding); - } - } catch (e) { - throw Error('Error during encryption. Original error: ' + e); - } - }; - - /** - * Decrypting data method with custom key - */ - NodeRSA.prototype.$$decryptKey = function (usePublic, buffer, encoding) { - try { - buffer = _.isString(buffer) ? Buffer.from(buffer, 'base64') : buffer; - var res = this.keyPair.decrypt(buffer, usePublic); - - if (res === null) { - throw Error('Key decrypt method returns null.'); - } - - return this.$getDecryptedData(res, encoding); - } catch (e) { - throw Error('Error during decryption (probably incorrect key). Original error: ' + e); - } - }; - - /** - * Signing data - * - * @param buffer {string|number|object|array|Buffer} - data for signing. Object and array will convert to JSON string. - * @param encoding {string} - optional. Encoding for output result, may be 'buffer', 'binary', 'hex' or 'base64'. Default 'buffer'. - * @param source_encoding {string} - optional. Encoding for given string. Default utf8. - * @returns {string|Buffer} - */ - NodeRSA.prototype.sign = function (buffer, encoding, source_encoding) { - if (!this.isPrivate()) { - throw Error("This is not private key"); - } - - var res = this.keyPair.sign(this.$getDataForEncrypt(buffer, source_encoding)); - - if (encoding && encoding != 'buffer') { - res = res.toString(encoding); - } - - return res; - }; - - /** - * Verifying signed data - * - * @param buffer - signed data - * @param signature - * @param source_encoding {string} - optional. Encoding for given string. Default utf8. - * @param signature_encoding - optional. Encoding of given signature. May be 'buffer', 'binary', 'hex' or 'base64'. Default 'buffer'. - * @returns {*} - */ - NodeRSA.prototype.verify = function (buffer, signature, source_encoding, signature_encoding) { - if (!this.isPublic()) { - throw Error("This is not public key"); - } - signature_encoding = (!signature_encoding || signature_encoding == 'buffer' ? null : signature_encoding); - return this.keyPair.verify(this.$getDataForEncrypt(buffer, source_encoding), signature, signature_encoding); - }; - - /** - * Returns key size in bits - * @returns {int} - */ - NodeRSA.prototype.getKeySize = function () { - return this.keyPair.keySize; - }; - - /** - * Returns max message length in bytes (for 1 chunk) depending on current encryption scheme - * @returns {int} - */ - NodeRSA.prototype.getMaxMessageSize = function () { - return this.keyPair.maxMessageLength; - }; - - /** - * Preparing given data for encrypting/signing. Just make new/return Buffer object. - * - * @param buffer {string|number|object|array|Buffer} - data for encrypting. Object and array will convert to JSON string. - * @param encoding {string} - optional. Encoding for given string. Default utf8. - * @returns {Buffer} - */ - NodeRSA.prototype.$getDataForEncrypt = function (buffer, encoding) { - if (_.isString(buffer) || _.isNumber(buffer)) { - return Buffer.from('' + buffer, encoding || 'utf8'); - } else if (Buffer.isBuffer(buffer)) { - return buffer; - } else if (_.isObject(buffer)) { - return Buffer.from(JSON.stringify(buffer)); - } else { - throw Error("Unexpected data type"); - } - }; - - /** - * - * @param buffer {Buffer} - decrypted data. - * @param encoding - optional. Encoding for result output. May be 'buffer', 'json' or any of Node.js Buffer supported encoding. - * @returns {*} - */ - NodeRSA.prototype.$getDecryptedData = function (buffer, encoding) { - encoding = encoding || 'buffer'; - - if (encoding == 'buffer') { - return buffer; - } else if (encoding == 'json') { - return JSON.parse(buffer.toString()); - } else { - return buffer.toString(encoding); - } - }; - - return NodeRSA; -})(); diff --git a/src/asn1/index.ts b/src/asn1/index.ts new file mode 100644 index 0000000..aa39fd0 --- /dev/null +++ b/src/asn1/index.ts @@ -0,0 +1,4 @@ +export { OID } from './oids.js'; +export { DerReader } from './reader.js'; +export { Tag } from './tags.js'; +export { DerWriter } from './writer.js'; diff --git a/src/asn1/oids.ts b/src/asn1/oids.ts new file mode 100644 index 0000000..ad1d4aa --- /dev/null +++ b/src/asn1/oids.ts @@ -0,0 +1,4 @@ +export const OID = { + /** rsaEncryption — used in PKCS#8 AlgorithmIdentifier */ + RSA_ENCRYPTION: '1.2.840.113549.1.1.1', +} as const; diff --git a/src/asn1/reader.ts b/src/asn1/reader.ts new file mode 100644 index 0000000..74d7f25 --- /dev/null +++ b/src/asn1/reader.ts @@ -0,0 +1,202 @@ +import { Tag, tagName } from './tags.js'; + +export class DerReader { + private pos = 0; + private readonly bytes: Uint8Array; + + constructor(bytes: Uint8Array) { + this.bytes = bytes; + } + + get position(): number { + return this.pos; + } + + get remaining(): number { + return this.bytes.length - this.pos; + } + + hasMore(): boolean { + return this.pos < this.bytes.length; + } + + /** + * Read a generic TLV. If `expectedTag` is supplied, asserts the tag matches. + * Returns the value bytes (no tag, no length octets). + */ + readTlv(expectedTag?: number): { tag: number; value: Uint8Array } { + if (this.pos >= this.bytes.length) { + throw new Error('DerReader: unexpected end of input'); + } + const tag = this.bytes[this.pos++] as number; + if (expectedTag !== undefined && tag !== expectedTag) { + throw new Error( + `DerReader: expected ${tagName(expectedTag)} (0x${expectedTag.toString(16)}) but got ${tagName(tag)} (0x${tag.toString(16)})`, + ); + } + const length = this.readLength(); + const end = this.pos + length; + if (end > this.bytes.length) { + throw new Error( + `DerReader: TLV length ${length} exceeds buffer (pos=${this.pos}, len=${this.bytes.length})`, + ); + } + const value = this.bytes.subarray(this.pos, end); + this.pos = end; + return { tag, value }; + } + + private readLength(): number { + if (this.pos >= this.bytes.length) { + throw new Error('DerReader: missing length octet'); + } + const first = this.bytes[this.pos++] as number; + if ((first & 0x80) === 0) return first; + const numBytes = first & 0x7f; + if (numBytes === 0) { + throw new Error('DerReader: indefinite length not permitted in DER'); + } + if (numBytes > 4) { + throw new Error(`DerReader: unsupported length width ${numBytes}`); + } + let len = 0; + for (let i = 0; i < numBytes; i++) { + if (this.pos >= this.bytes.length) { + throw new Error('DerReader: truncated length'); + } + const b = this.bytes[this.pos++] as number; + // X.690 §10.1: long-form length octets must be the minimum number + // necessary — no leading zeros. + if (i === 0 && b === 0 && numBytes > 1) { + throw new Error('DerReader: non-canonical length (leading zero in long-form)'); + } + len = (len << 8) | b; + } + // X.690 §10.1: short-form is required when the value is < 128. + if (len < 128) { + throw new Error(`DerReader: non-canonical length (long-form used for length ${len} < 128)`); + } + return len; + } + + /** Read a SEQUENCE and return a sub-reader scoped to its contents. */ + readSequence(): DerReader { + return new DerReader(this.readTlv(Tag.SEQUENCE).value); + } + + /** Read an INTEGER and return its raw value bytes (DER content). */ + readInteger(): Uint8Array { + const bytes = this.readTlv(Tag.INTEGER).value; + if (bytes.length === 0) { + throw new Error('DerReader: INTEGER must have at least one content octet'); + } + // X.690 §8.3.2: a leading 0x00 is allowed only when the next byte's MSB + // is set (sign-bit padding); a leading 0xff only when it's clear. + if (bytes.length >= 2) { + const b0 = bytes[0]; + const b1 = bytes[1]; + if (b1 !== undefined) { + if (b0 === 0x00 && (b1 & 0x80) === 0) { + throw new Error('DerReader: non-canonical INTEGER (redundant leading 0x00)'); + } + if (b0 === 0xff && (b1 & 0x80) !== 0) { + throw new Error('DerReader: non-canonical INTEGER (redundant leading 0xff)'); + } + } + } + return bytes; + } + + /** + * Read an INTEGER, decoding it as an unsigned JavaScript number. + * Throws if the value doesn't fit in a safe-integer. + */ + readSmallInteger(): number { + const bytes = this.readInteger(); + // Skip any leading zero used to indicate sign (positive) + let i = 0; + while (i < bytes.length - 1 && bytes[i] === 0) i++; + const meaningful = bytes.subarray(i); + if (meaningful.length > 6) { + throw new Error(`DerReader: integer too large for safe number (${meaningful.length} bytes)`); + } + let n = 0; + for (const b of meaningful) { + n = n * 256 + b; + } + return n; + } + + /** Read an OBJECT IDENTIFIER and return its dotted-string form. */ + readOid(): string { + return decodeOid(this.readTlv(Tag.OBJECT_IDENTIFIER).value); + } + + /** Read a NULL TLV. Throws if the value is non-empty. */ + readNull(): void { + const { value } = this.readTlv(Tag.NULL); + if (value.length !== 0) { + throw new Error(`DerReader: NULL must be zero-length, got ${value.length}`); + } + } + + /** Read a BIT STRING and return its value bytes INCLUDING the leading unused-bits octet. */ + readBitStringRaw(): Uint8Array { + return this.readTlv(Tag.BIT_STRING).value; + } + + /** + * Read a BIT STRING and return its content octets (after the unused-bits byte). + * Asserts unused-bits is zero. + */ + readBitString(): Uint8Array { + const raw = this.readBitStringRaw(); + if (raw.length === 0) { + throw new Error('DerReader: empty BIT STRING'); + } + if (raw[0] !== 0) { + throw new Error(`DerReader: non-zero unused bits (${raw[0]}) not supported`); + } + return raw.subarray(1); + } + + /** Read an OCTET STRING and return its value bytes. */ + readOctetString(): Uint8Array { + return this.readTlv(Tag.OCTET_STRING).value; + } +} + +function decodeOid(bytes: Uint8Array): string { + if (bytes.length === 0) { + throw new Error('DerReader: empty OID'); + } + let i = 0; + // First base-128 sequence encodes (40*arc0 + arc1). + let combined = 0; + let b: number; + do { + if (i >= bytes.length) throw new Error('DerReader: truncated OID'); + b = bytes[i++] as number; + combined = combined * 128 + (b & 0x7f); + } while ((b & 0x80) !== 0); + + const arcs: number[] = []; + if (combined < 40) { + arcs.push(0, combined); + } else if (combined < 80) { + arcs.push(1, combined - 40); + } else { + arcs.push(2, combined - 80); + } + + while (i < bytes.length) { + let arc = 0; + do { + if (i >= bytes.length) throw new Error('DerReader: truncated OID arc'); + b = bytes[i++] as number; + arc = arc * 128 + (b & 0x7f); + } while ((b & 0x80) !== 0); + arcs.push(arc); + } + return arcs.join('.'); +} diff --git a/src/asn1/tags.ts b/src/asn1/tags.ts new file mode 100644 index 0000000..d46645a --- /dev/null +++ b/src/asn1/tags.ts @@ -0,0 +1,27 @@ +export const Tag = { + INTEGER: 0x02, + BIT_STRING: 0x03, + OCTET_STRING: 0x04, + NULL: 0x05, + OBJECT_IDENTIFIER: 0x06, + SEQUENCE: 0x30, +} as const; + +export function tagName(tag: number): string { + switch (tag) { + case Tag.INTEGER: + return 'INTEGER'; + case Tag.BIT_STRING: + return 'BIT STRING'; + case Tag.OCTET_STRING: + return 'OCTET STRING'; + case Tag.NULL: + return 'NULL'; + case Tag.OBJECT_IDENTIFIER: + return 'OBJECT IDENTIFIER'; + case Tag.SEQUENCE: + return 'SEQUENCE'; + default: + return `tag 0x${tag.toString(16).padStart(2, '0')}`; + } +} diff --git a/src/asn1/writer.ts b/src/asn1/writer.ts new file mode 100644 index 0000000..eed677f --- /dev/null +++ b/src/asn1/writer.ts @@ -0,0 +1,173 @@ +import { concat } from '../crypto/bytes.js'; +import { Tag } from './tags.js'; + +export class DerWriter { + private chunks: Uint8Array[] = []; + private sequenceStack: Uint8Array[][] = []; + + /** Write a generic TLV with the given tag and value bytes. */ + writeTlv(tag: number, value: Uint8Array): void { + this.chunks.push(new Uint8Array([tag])); + this.chunks.push(encodeLength(value.length)); + this.chunks.push(value); + } + + /** + * Write an INTEGER. Accepts: + * - a positive JS number, + * - an unsigned big-endian byte array (a leading zero will be prepended + * if the MSB is set, to preserve positive sign). + */ + writeInteger(value: number | Uint8Array): void { + if (typeof value === 'number') { + this.writeTlv(Tag.INTEGER, encodeSmallInteger(value)); + } else { + this.writeTlv(Tag.INTEGER, normalizePositiveInteger(value)); + } + } + + writeOid(oid: string): void { + this.writeTlv(Tag.OBJECT_IDENTIFIER, encodeOid(oid)); + } + + writeNull(): void { + this.writeTlv(Tag.NULL, new Uint8Array(0)); + } + + /** Write a BIT STRING. Always emits an unused-bits prefix byte of 0x00. */ + writeBitString(content: Uint8Array): void { + const body = new Uint8Array(content.length + 1); + body[0] = 0; + body.set(content, 1); + this.writeTlv(Tag.BIT_STRING, body); + } + + /** + * Write a BIT STRING whose value bytes (including the leading unused-bits + * octet) are supplied directly. Mirrors callers that build the bit-string + * payload externally. + */ + writeBitStringRaw(valueIncludingUnusedBitsByte: Uint8Array): void { + this.writeTlv(Tag.BIT_STRING, valueIncludingUnusedBitsByte); + } + + writeOctetString(content: Uint8Array): void { + this.writeTlv(Tag.OCTET_STRING, content); + } + + /** Begin a nested SEQUENCE; subsequent writes go into it until endSequence(). */ + startSequence(): void { + this.sequenceStack.push(this.chunks); + this.chunks = []; + } + + /** Close the most recently opened SEQUENCE, emitting it as a TLV in the parent. */ + endSequence(): void { + if (this.sequenceStack.length === 0) { + throw new Error('DerWriter: endSequence without startSequence'); + } + const inner = concat(...this.chunks); + this.chunks = this.sequenceStack.pop() as Uint8Array[]; + this.writeTlv(Tag.SEQUENCE, inner); + } + + /** Return the assembled DER bytes. Throws if any SEQUENCE is unclosed. */ + toBytes(): Uint8Array { + if (this.sequenceStack.length > 0) { + throw new Error(`DerWriter: ${this.sequenceStack.length} SEQUENCE(s) unclosed`); + } + return concat(...this.chunks); + } +} + +function encodeLength(n: number): Uint8Array { + if (n < 0) throw new Error(`DerWriter: negative length ${n}`); + if (n < 0x80) return new Uint8Array([n]); + const bytes: number[] = []; + let temp = n; + while (temp > 0) { + bytes.unshift(temp & 0xff); + temp = Math.floor(temp / 256); + } + if (bytes.length > 0x7f) { + throw new Error(`DerWriter: length ${n} exceeds DER limits`); + } + return new Uint8Array([0x80 | bytes.length, ...bytes]); +} + +function encodeSmallInteger(n: number): Uint8Array { + if (n < 0) throw new Error(`DerWriter: negative integers not supported (got ${n})`); + if (n === 0) return new Uint8Array([0]); + if (!Number.isSafeInteger(n)) { + throw new Error(`DerWriter: integer ${n} not a safe integer`); + } + const bytes: number[] = []; + let temp = n; + while (temp > 0) { + bytes.unshift(temp & 0xff); + temp = Math.floor(temp / 256); + } + // Prepend 0x00 if MSB is set so the value is parsed as unsigned/positive. + if ((bytes[0] as number) & 0x80) bytes.unshift(0); + return new Uint8Array(bytes); +} + +function normalizePositiveInteger(value: Uint8Array): Uint8Array { + // Strip leading zeros, but keep one if MSB of the next byte is set. + let i = 0; + while (i < value.length - 1 && value[i] === 0 && ((value[i + 1] as number) & 0x80) === 0) { + i++; + } + const trimmed = value.subarray(i); + // Prepend a 0x00 if MSB is set, to signal positive. + if (trimmed.length > 0 && (trimmed[0] as number) & 0x80) { + const out = new Uint8Array(trimmed.length + 1); + out[0] = 0; + out.set(trimmed, 1); + return out; + } + return trimmed.length === 0 ? new Uint8Array([0]) : trimmed; +} + +function encodeOid(oid: string): Uint8Array { + const arcs = oid.split('.').map((s) => { + const n = Number(s); + if (!Number.isFinite(n) || n < 0 || !Number.isInteger(n)) { + throw new Error(`DerWriter: invalid OID arc "${s}"`); + } + return n; + }); + if (arcs.length < 2) { + throw new Error(`DerWriter: OID must have at least 2 arcs, got "${oid}"`); + } + const arc0 = arcs[0] as number; + const arc1 = arcs[1] as number; + if (arc0 > 2 || (arc0 < 2 && arc1 >= 40)) { + throw new Error(`DerWriter: invalid leading arcs ${arc0}.${arc1}`); + } + // The first byte encodes (40*arc0 + arc1) using base-128 — needed when + // arc0 = 2 and arc1 >= 48, where the combined value exceeds a single octet. + const out: number[] = []; + encodeBase128Into(arc0 * 40 + arc1, out); + for (let i = 2; i < arcs.length; i++) { + encodeBase128Into(arcs[i] as number, out); + } + return new Uint8Array(out); +} + +function encodeBase128Into(n: number, out: number[]): void { + if (n === 0) { + out.push(0); + return; + } + const bytes: number[] = []; + let temp = n; + while (temp > 0) { + bytes.unshift(temp & 0x7f); + temp = Math.floor(temp / 128); + } + for (let i = 0; i < bytes.length - 1; i++) { + bytes[i] = (bytes[i] as number) | 0x80; + } + out.push(...bytes); +} diff --git a/src/bigint/big-integer-jsbn.ts b/src/bigint/big-integer-jsbn.ts new file mode 100644 index 0000000..40cc6ca --- /dev/null +++ b/src/bigint/big-integer-jsbn.ts @@ -0,0 +1,1390 @@ +/* + * TypeScript port of Tom Wu's jsbn BigInteger. + * + * Original copyright (c) 2003-2009 Tom Wu — MIT-style license preserved in + * src.legacy/libs/jsbn.js. Buffer support added by rzcoder (2014). + * + * This port preserves the original digit representation, function names, and + * algorithm structure 1-to-1 with the legacy implementation so that all + * keygen RNG and primality-test paths produce byte-identical results. + */ + +import type { CryptoBackend } from '../crypto/types.js'; + +// Backend injection for RNG +let _backend: CryptoBackend | undefined; + +export function setBigIntegerBackend(backend: CryptoBackend): void { + _backend = backend; +} + +function getBackend(): CryptoBackend { + if (!_backend) { + throw new Error( + 'BigInteger crypto backend not initialized. Did you import from src/index.node.ts or src/index.browser.ts?', + ); + } + return _backend; +} + +// Digit-base constants +const DB = 28; // bits per digit +const DM = (1 << DB) - 1; +const DV = 1 << DB; +const BI_FP = 52; +const FV = 2 ** BI_FP; +const F1 = BI_FP - DB; +const F2 = 2 * DB - BI_FP; + +// Reducer interface (Classic / Montgomery / Barrett / NullExp) +interface Reducer { + convert(x: BigInteger): BigInteger; + revert(x: BigInteger): BigInteger; + reduce(x: BigInteger): void; + mulTo(x: BigInteger, y: BigInteger, r: BigInteger): void; + sqrTo(x: BigInteger, r: BigInteger): void; +} + +// Radix-conversion tables +const BI_RM = '0123456789abcdefghijklmnopqrstuvwxyz'; +const BI_RC: number[] = []; +{ + let rr = '0'.charCodeAt(0); + for (let vv = 0; vv <= 9; ++vv) BI_RC[rr++] = vv; + rr = 'a'.charCodeAt(0); + for (let vv = 10; vv < 36; ++vv) BI_RC[rr++] = vv; + rr = 'A'.charCodeAt(0); + for (let vv = 10; vv < 36; ++vv) BI_RC[rr++] = vv; +} + +function int2char(n: number): string { + return BI_RM.charAt(n); +} + +function intAt(s: string, i: number): number { + const c = BI_RC[s.charCodeAt(i)]; + return c == null ? -1 : c; +} + +function nbits(x: number): number { + let r = 1; + let t: number; + if ((t = x >>> 16) !== 0) { + x = t; + r += 16; + } + if ((t = x >> 8) !== 0) { + x = t; + r += 8; + } + if ((t = x >> 4) !== 0) { + x = t; + r += 4; + } + if ((t = x >> 2) !== 0) { + x = t; + r += 2; + } + if ((t = x >> 1) !== 0) { + x = t; + r += 1; + } + return r; +} + +function lbit(x: number): number { + if (x === 0) return -1; + let r = 0; + if ((x & 0xffff) === 0) { + x >>= 16; + r += 16; + } + if ((x & 0xff) === 0) { + x >>= 8; + r += 8; + } + if ((x & 0xf) === 0) { + x >>= 4; + r += 4; + } + if ((x & 3) === 0) { + x >>= 2; + r += 2; + } + if ((x & 1) === 0) ++r; + return r; +} + +function cbit(x: number): number { + let r = 0; + while (x !== 0) { + x &= x - 1; + ++r; + } + return r; +} + +// BigInteger class +export class BigInteger { + /** @internal digit array via numeric index — jsbn-style storage */ + [n: number]: number; + /** @internal */ + t = 0; + /** @internal */ + s = 0; + + // Mirror legacy `this.DB`/`this.DM`/etc. access patterns + /** @internal */ + readonly DB = DB; + /** @internal */ + readonly DM = DM; + /** @internal */ + readonly DV = DV; + /** @internal */ + readonly FV = FV; + /** @internal */ + readonly F1 = F1; + /** @internal */ + readonly F2 = F2; + + static ZERO: BigInteger; + static ONE: BigInteger; + /** @internal */ + static readonly int2char = int2char; + + constructor( + a?: number | Uint8Array | number[] | string | null, + b?: number | string, + unsigned?: boolean, + ) { + if (a == null) return; + if (typeof a === 'number') { + this.fromNumber(a, b as number | undefined); + } else if (a instanceof Uint8Array) { + this.fromBuffer(a); + } else if (typeof a === 'string') { + this.fromString(a, b as number, unsigned); + } else if (Array.isArray(a)) { + this.fromByteArray(a, unsigned); + } + } + + // am3: multiply-accumulate (digit-base 2^28) + /** @internal */ + am(i: number, x: number, w: BigInteger, j: number, c: number, n: number): number { + const xl = x & 0x3fff; + const xh = x >> 14; + while (--n >= 0) { + let l = this[i]! & 0x3fff; + const h = this[i++]! >> 14; + const m = xh * l + h * xl; + l = xl * l + ((m & 0x3fff) << 14) + w[j]! + c; + c = (l >> 28) + (m >> 14) + xh * h; + w[j++] = l & 0xfffffff; + } + return c; + } + + // protected: digit/byte initialisation + /** @internal */ + copyTo(r: BigInteger): void { + for (let i = this.t - 1; i >= 0; --i) r[i] = this[i]!; + r.t = this.t; + r.s = this.s; + } + + /** @internal */ + fromInt(x: number): void { + this.t = 1; + this.s = x < 0 ? -1 : 0; + if (x > 0) this[0] = x; + else if (x < -1) this[0] = x + DV; + else this.t = 0; + } + + /** @internal */ + fromString(data: string | number[] | Uint8Array, radix?: number, unsigned?: boolean): void { + let k: number; + switch (radix) { + case 2: + k = 1; + break; + case 4: + k = 2; + break; + case 8: + k = 3; + break; + case 16: + k = 4; + break; + case 32: + k = 5; + break; + case 256: + k = 8; + break; + default: + this.fromRadix(data as string, radix); + return; + } + this.t = 0; + this.s = 0; + const dataAny = data as { [n: number]: number; length: number; charAt?(i: number): string }; + let i = dataAny.length; + let mi = false; + let sh = 0; + while (--i >= 0) { + const x = k === 8 ? (dataAny[i] as number) & 0xff : intAt(data as string, i); + if (x < 0) { + if (dataAny.charAt && dataAny.charAt(i) === '-') mi = true; + continue; + } + mi = false; + if (sh === 0) this[this.t++] = x; + else if (sh + k > this.DB) { + this[this.t - 1] = (this[this.t - 1]! | ((x & ((1 << (this.DB - sh)) - 1)) << sh)) >>> 0; + this[this.t++] = x >> (this.DB - sh); + } else { + this[this.t - 1] = (this[this.t - 1]! | (x << sh)) >>> 0; + } + sh += k; + if (sh >= this.DB) sh -= this.DB; + } + if (!unsigned && k === 8 && ((dataAny[0] as number) & 0x80) !== 0) { + this.s = -1; + if (sh > 0) + this[this.t - 1] = (this[this.t - 1]! | (((1 << (this.DB - sh)) - 1) << sh)) >>> 0; + } + this.clamp(); + if (mi) BigInteger.ZERO.subTo(this, this); + } + + /** @internal */ + fromByteArray(a: number[], unsigned?: boolean): void { + this.fromString(a, 256, unsigned); + } + + /** @internal */ + fromBuffer(a: Uint8Array): void { + this.fromString(a, 256, true); + } + + /** @internal */ + clamp(): void { + const c = this.s & this.DM; + while (this.t > 0 && this[this.t - 1] === c) --this.t; + } + + // arithmetic on internal digits + /** @internal */ + dlShiftTo(n: number, r: BigInteger): void { + let i: number; + for (i = this.t - 1; i >= 0; --i) r[i + n] = this[i]!; + for (i = n - 1; i >= 0; --i) r[i] = 0; + r.t = this.t + n; + r.s = this.s; + } + + /** @internal */ + drShiftTo(n: number, r: BigInteger): void { + for (let i = n; i < this.t; ++i) r[i - n] = this[i]!; + r.t = Math.max(this.t - n, 0); + r.s = this.s; + } + + /** @internal */ + lShiftTo(n: number, r: BigInteger): void { + const bs = n % this.DB; + const cbs = this.DB - bs; + const bm = (1 << cbs) - 1; + const ds = Math.floor(n / this.DB); + let c = (this.s << bs) & this.DM; + let i: number; + for (i = this.t - 1; i >= 0; --i) { + r[i + ds + 1] = (this[i]! >> cbs) | c; + c = (this[i]! & bm) << bs; + } + for (i = ds - 1; i >= 0; --i) r[i] = 0; + r[ds] = c; + r.t = this.t + ds + 1; + r.s = this.s; + r.clamp(); + } + + /** @internal */ + rShiftTo(n: number, r: BigInteger): void { + r.s = this.s; + const ds = Math.floor(n / this.DB); + if (ds >= this.t) { + r.t = 0; + return; + } + const bs = n % this.DB; + const cbs = this.DB - bs; + const bm = (1 << bs) - 1; + r[0] = this[ds]! >> bs; + for (let i = ds + 1; i < this.t; ++i) { + r[i - ds - 1] = (r[i - ds - 1] ?? 0) | ((this[i]! & bm) << cbs); + r[i - ds] = this[i]! >> bs; + } + if (bs > 0) r[this.t - ds - 1] = (r[this.t - ds - 1] ?? 0) | ((this.s & bm) << cbs); + r.t = this.t - ds; + r.clamp(); + } + + /** @internal */ + subTo(a: BigInteger, r: BigInteger): void { + let i = 0; + let c = 0; + const m = Math.min(a.t, this.t); + while (i < m) { + c += this[i]! - a[i]!; + r[i++] = c & this.DM; + c >>= this.DB; + } + if (a.t < this.t) { + c -= a.s; + while (i < this.t) { + c += this[i]!; + r[i++] = c & this.DM; + c >>= this.DB; + } + c += this.s; + } else { + c += this.s; + while (i < a.t) { + c -= a[i]!; + r[i++] = c & this.DM; + c >>= this.DB; + } + c -= a.s; + } + r.s = c < 0 ? -1 : 0; + if (c < -1) r[i++] = this.DV + c; + else if (c > 0) r[i++] = c; + r.t = i; + r.clamp(); + } + + /** @internal */ + multiplyTo(a: BigInteger, r: BigInteger): void { + const x = this.abs(); + const y = a.abs(); + let i = x.t; + r.t = i + y.t; + while (--i >= 0) r[i] = 0; + for (i = 0; i < y.t; ++i) r[i + x.t] = x.am(0, y[i]!, r, i, 0, x.t); + r.s = 0; + r.clamp(); + if (this.s !== a.s) BigInteger.ZERO.subTo(r, r); + } + + /** @internal */ + squareTo(r: BigInteger): void { + const x = this.abs(); + let i = (r.t = 2 * x.t); + while (--i >= 0) r[i] = 0; + for (i = 0; i < x.t - 1; ++i) { + const c = x.am(i, x[i]!, r, 2 * i, 0, 1); + if ( + (r[i + x.t] = (r[i + x.t] ?? 0) + x.am(i + 1, 2 * x[i]!, r, 2 * i + 1, c, x.t - i - 1)) >= + x.DV + ) { + r[i + x.t] = r[i + x.t]! - x.DV; + r[i + x.t + 1] = 1; + } + } + if (r.t > 0) r[r.t - 1] = (r[r.t - 1] ?? 0) + x.am(i, x[i]!, r, 2 * i, 0, 1); + r.s = 0; + r.clamp(); + } + + /** @internal */ + divRemTo(m: BigInteger, q: BigInteger | null, r: BigInteger | null): void { + const pm = m.abs(); + if (pm.t <= 0) return; + const pt = this.abs(); + if (pt.t < pm.t) { + if (q != null) q.fromInt(0); + if (r != null) this.copyTo(r); + return; + } + if (r == null) r = nbi(); + const y = nbi(); + const ts = this.s; + const ms = m.s; + const nsh = this.DB - nbits(pm[pm.t - 1]!); + if (nsh > 0) { + pm.lShiftTo(nsh, y); + pt.lShiftTo(nsh, r); + } else { + pm.copyTo(y); + pt.copyTo(r); + } + const ys = y.t; + const y0 = y[ys - 1]!; + if (y0 === 0) return; + const yt = y0 * (1 << this.F1) + (ys > 1 ? y[ys - 2]! >> this.F2 : 0); + const d1 = this.FV / yt; + const d2 = (1 << this.F1) / yt; + const e = 1 << this.F2; + let i = r.t; + let j = i - ys; + const t = q == null ? nbi() : q; + y.dlShiftTo(j, t); + if (r.compareTo(t) >= 0) { + r[r.t++] = 1; + r.subTo(t, r); + } + BigInteger.ONE.dlShiftTo(ys, t); + t.subTo(y, y); + while (y.t < ys) y[y.t++] = 0; + while (--j >= 0) { + let qd = r[--i]! === y0 ? this.DM : Math.floor(r[i]! * d1 + (r[i - 1]! + e) * d2); + if ((r[i] = r[i]! + y.am(0, qd, r, j, 0, ys)) < qd) { + y.dlShiftTo(j, t); + r.subTo(t, r); + while (r[i]! < --qd) r.subTo(t, r); + } + } + if (q != null) { + r.drShiftTo(ys, q); + if (ts !== ms) BigInteger.ZERO.subTo(q, q); + } + r.t = ys; + r.clamp(); + if (nsh > 0) r.rShiftTo(nsh, r); + if (ts < 0) BigInteger.ZERO.subTo(r, r); + } + + /** @internal */ + invDigit(): number { + if (this.t < 1) return 0; + const x = this[0]!; + if ((x & 1) === 0) return 0; + let y = x & 3; + y = (y * (2 - (x & 0xf) * y)) & 0xf; + y = (y * (2 - (x & 0xff) * y)) & 0xff; + y = (y * (2 - (((x & 0xffff) * y) & 0xffff))) & 0xffff; + y = (y * (2 - ((x * y) % this.DV))) % this.DV; + return y > 0 ? this.DV - y : -y; + } + + isEven(): boolean { + return ((this.t > 0 ? this[0]! & 1 : this.s) & 1) === 0; + } + + /** @internal */ + exp(e: number, z: Reducer): BigInteger { + if (e > 0xffffffff || e < 1) return BigInteger.ONE; + let r = nbi(); + let r2 = nbi(); + const g = z.convert(this); + let i = nbits(e) - 1; + g.copyTo(r); + while (--i >= 0) { + z.sqrTo(r, r2); + if ((e & (1 << i)) > 0) z.mulTo(r2, g, r); + else { + const tmp = r; + r = r2; + r2 = tmp; + } + } + return z.revert(r); + } + + // public arithmetic & comparisons + toString(b?: number): string { + if (this.s < 0) return `-${this.negate().toString(b)}`; + let k: number; + if (b === 16) k = 4; + else if (b === 8) k = 3; + else if (b === 2) k = 1; + else if (b === 32) k = 5; + else if (b === 4) k = 2; + else return this.toRadix(b); + const km = (1 << k) - 1; + let d: number; + let m = false; + let r = ''; + let i = this.t; + let p = this.DB - ((i * this.DB) % k); + if (i-- > 0) { + if (p < this.DB && (d = this[i]! >> p) > 0) { + m = true; + r = int2char(d); + } + while (i >= 0) { + if (p < k) { + d = (this[i]! & ((1 << p) - 1)) << (k - p); + d |= this[--i]! >> (p += this.DB - k); + } else { + d = (this[i]! >> (p -= k)) & km; + if (p <= 0) { + p += this.DB; + --i; + } + } + if (d > 0) m = true; + if (m) r += int2char(d); + } + } + return m ? r : '0'; + } + + /** @internal */ + negate(): BigInteger { + const r = nbi(); + BigInteger.ZERO.subTo(this, r); + return r; + } + + abs(): BigInteger { + return this.s < 0 ? this.negate() : this; + } + + compareTo(a: BigInteger): number { + let r = this.s - a.s; + if (r !== 0) return r; + let i = this.t; + r = i - a.t; + if (r !== 0) return this.s < 0 ? -r : r; + while (--i >= 0) if ((r = this[i]! - a[i]!) !== 0) return r; + return 0; + } + + bitLength(): number { + if (this.t <= 0) return 0; + return this.DB * (this.t - 1) + nbits(this[this.t - 1]! ^ (this.s & this.DM)); + } + + mod(a: BigInteger): BigInteger { + const r = nbi(); + this.abs().divRemTo(a, null, r); + if (this.s < 0 && r.compareTo(BigInteger.ZERO) > 0) a.subTo(r, r); + return r; + } + + modPowInt(e: number, m: BigInteger): BigInteger { + const z: Reducer = e < 256 || m.isEven() ? new Classic(m) : new Montgomery(m); + return this.exp(e, z); + } + + // extended functions + /** @internal */ + clone(): BigInteger { + const r = nbi(); + this.copyTo(r); + return r; + } + + /** @internal */ + intValue(): number { + if (this.s < 0) { + if (this.t === 1) return this[0]! - this.DV; + if (this.t === 0) return -1; + } else if (this.t === 1) return this[0]!; + else if (this.t === 0) return 0; + return ((this[1]! & ((1 << (32 - this.DB)) - 1)) << this.DB) | this[0]!; + } + + /** @internal */ + byteValue(): number { + return this.t === 0 ? this.s : (this[0]! << 24) >> 24; + } + + /** @internal */ + shortValue(): number { + return this.t === 0 ? this.s : (this[0]! << 16) >> 16; + } + + /** @internal */ + chunkSize(r: number): number { + return Math.floor((Math.LN2 * this.DB) / Math.log(r)); + } + + signum(): number { + if (this.s < 0) return -1; + if (this.t <= 0 || (this.t === 1 && this[0]! <= 0)) return 0; + return 1; + } + + /** @internal */ + toRadix(b?: number): string { + const base = b ?? 10; + if (this.signum() === 0 || base < 2 || base > 36) return '0'; + const cs = this.chunkSize(base); + const a = base ** cs; + const d = nbv(a); + const y = nbi(); + const z = nbi(); + let r = ''; + this.divRemTo(d, y, z); + while (y.signum() > 0) { + r = (a + z.intValue()).toString(base).slice(1) + r; + y.divRemTo(d, y, z); + } + return z.intValue().toString(base) + r; + } + + /** @internal */ + fromRadix(s: string, b?: number): void { + this.fromInt(0); + const base = b ?? 10; + const cs = this.chunkSize(base); + const d = base ** cs; + let mi = false; + let j = 0; + let w = 0; + for (let i = 0; i < s.length; ++i) { + const x = intAt(s, i); + if (x < 0) { + if (s.charAt(i) === '-' && this.signum() === 0) mi = true; + continue; + } + w = base * w + x; + if (++j >= cs) { + this.dMultiply(d); + this.dAddOffset(w, 0); + j = 0; + w = 0; + } + } + if (j > 0) { + this.dMultiply(base ** j); + this.dAddOffset(w, 0); + } + if (mi) BigInteger.ZERO.subTo(this, this); + } + + /** @internal */ + fromNumber(a: number, b?: number): void { + if (typeof b === 'number') { + // (bits, certainty) → generate probable prime + if (a < 2) this.fromInt(1); + else { + this.fromNumber(a); + if (!this.testBit(a - 1)) { + this.bitwiseTo(BigInteger.ONE.shiftLeft(a - 1), op_or, this); + } + if (this.isEven()) this.dAddOffset(1, 0); + while (!this.isProbablePrime(b)) { + this.dAddOffset(2, 0); + if (this.bitLength() > a) this.subTo(BigInteger.ONE.shiftLeft(a - 1), this); + } + } + } else { + // (bits) → random a-bit integer + const x = getBackend().randomBytes((a >> 3) + 1); + const t = a & 7; + const bytes = new Uint8Array(x); + if (t > 0) bytes[0] = bytes[0]! & ((1 << t) - 1); + else bytes[0] = 0; + this.fromByteArray(Array.from(bytes)); + } + } + + /** @internal */ + toByteArray(): number[] { + let i = this.t; + const r: number[] = []; + r[0] = this.s; + let p = this.DB - ((i * this.DB) % 8); + let d: number; + let k = 0; + if (i-- > 0) { + if (p < this.DB && (d = this[i]! >> p) !== (this.s & this.DM) >> p) { + r[k++] = d | (this.s << (this.DB - p)); + } + while (i >= 0) { + if (p < 8) { + d = (this[i]! & ((1 << p) - 1)) << (8 - p); + d |= this[--i]! >> (p += this.DB - 8); + } else { + d = (this[i]! >> (p -= 8)) & 0xff; + if (p <= 0) { + p += this.DB; + --i; + } + } + if ((d & 0x80) !== 0) d |= -256; + if (k === 0 && (this.s & 0x80) !== (d & 0x80)) ++k; + if (k > 0 || d !== this.s) r[k++] = d; + } + } + return r; + } + + /** + * Return a Uint8Array of this integer in big-endian unsigned form. + * + * - `trimOrSize === true`: drop a leading 0x00 sign byte if present. + * - `trimOrSize` is a positive integer: left-pad or trim leading zeros to + * produce exactly `trimOrSize` bytes. Returns null if trimming would + * discard a non-zero byte (i.e., the value doesn't fit). + * - Otherwise: return the raw two's-complement byte array with possible + * leading 0x00 sign byte. + */ + toBuffer(trimOrSize?: boolean | number): Uint8Array | null { + let res = Uint8Array.from(this.toByteArray().map((b) => b & 0xff)); + if (trimOrSize === true && res.length > 0 && res[0] === 0) { + res = res.subarray(1); + } else if (typeof trimOrSize === 'number') { + if (res.length > trimOrSize) { + const excess = res.length - trimOrSize; + for (let i = 0; i < excess; i++) { + if (res[i] !== 0) return null; + } + return res.subarray(excess).slice(); + } + if (res.length < trimOrSize) { + const padded = new Uint8Array(trimOrSize); + padded.set(res, trimOrSize - res.length); + return padded; + } + } + return res.slice(); + } + + /** @internal */ + equals(a: BigInteger): boolean { + return this.compareTo(a) === 0; + } + + /** @internal */ + min(a: BigInteger): BigInteger { + return this.compareTo(a) < 0 ? this : a; + } + + /** @internal */ + max(a: BigInteger): BigInteger { + return this.compareTo(a) > 0 ? this : a; + } + + /** @internal */ + bitwiseTo(a: BigInteger, op: (x: number, y: number) => number, r: BigInteger): void { + let i: number; + let f: number; + const m = Math.min(a.t, this.t); + for (i = 0; i < m; ++i) r[i] = op(this[i]!, a[i]!); + if (a.t < this.t) { + f = a.s & this.DM; + for (i = m; i < this.t; ++i) r[i] = op(this[i]!, f); + r.t = this.t; + } else { + f = this.s & this.DM; + for (i = m; i < a.t; ++i) r[i] = op(f, a[i]!); + r.t = a.t; + } + r.s = op(this.s, a.s); + r.clamp(); + } + + /** @internal */ + and(a: BigInteger): BigInteger { + const r = nbi(); + this.bitwiseTo(a, op_and, r); + return r; + } + /** @internal */ + or(a: BigInteger): BigInteger { + const r = nbi(); + this.bitwiseTo(a, op_or, r); + return r; + } + /** @internal */ + xor(a: BigInteger): BigInteger { + const r = nbi(); + this.bitwiseTo(a, op_xor, r); + return r; + } + /** @internal */ + andNot(a: BigInteger): BigInteger { + const r = nbi(); + this.bitwiseTo(a, op_andnot, r); + return r; + } + /** @internal */ + not(): BigInteger { + const r = nbi(); + for (let i = 0; i < this.t; ++i) r[i] = this.DM & ~this[i]!; + r.t = this.t; + r.s = ~this.s; + return r; + } + + shiftLeft(n: number): BigInteger { + const r = nbi(); + if (n < 0) this.rShiftTo(-n, r); + else this.lShiftTo(n, r); + return r; + } + + shiftRight(n: number): BigInteger { + const r = nbi(); + if (n < 0) this.lShiftTo(-n, r); + else this.rShiftTo(n, r); + return r; + } + + /** @internal */ + getLowestSetBit(): number { + for (let i = 0; i < this.t; ++i) if (this[i] !== 0) return i * this.DB + lbit(this[i]!); + if (this.s < 0) return this.t * this.DB; + return -1; + } + + /** @internal */ + bitCount(): number { + let r = 0; + const x = this.s & this.DM; + for (let i = 0; i < this.t; ++i) r += cbit(this[i]! ^ x); + return r; + } + + testBit(n: number): boolean { + const j = Math.floor(n / this.DB); + if (j >= this.t) return this.s !== 0; + return (this[j]! & (1 << (n % this.DB))) !== 0; + } + + /** @internal */ + changeBit(n: number, op: (x: number, y: number) => number): BigInteger { + const r = BigInteger.ONE.shiftLeft(n); + this.bitwiseTo(r, op, r); + return r; + } + /** @internal */ + setBit(n: number): BigInteger { + return this.changeBit(n, op_or); + } + /** @internal */ + clearBit(n: number): BigInteger { + return this.changeBit(n, op_andnot); + } + /** @internal */ + flipBit(n: number): BigInteger { + return this.changeBit(n, op_xor); + } + + /** @internal */ + addTo(a: BigInteger, r: BigInteger): void { + let i = 0; + let c = 0; + const m = Math.min(a.t, this.t); + while (i < m) { + c += this[i]! + a[i]!; + r[i++] = c & this.DM; + c >>= this.DB; + } + if (a.t < this.t) { + c += a.s; + while (i < this.t) { + c += this[i]!; + r[i++] = c & this.DM; + c >>= this.DB; + } + c += this.s; + } else { + c += this.s; + while (i < a.t) { + c += a[i]!; + r[i++] = c & this.DM; + c >>= this.DB; + } + c += a.s; + } + r.s = c < 0 ? -1 : 0; + if (c > 0) r[i++] = c; + else if (c < -1) r[i++] = this.DV + c; + r.t = i; + r.clamp(); + } + + add(a: BigInteger): BigInteger { + const r = nbi(); + this.addTo(a, r); + return r; + } + subtract(a: BigInteger): BigInteger { + const r = nbi(); + this.subTo(a, r); + return r; + } + multiply(a: BigInteger): BigInteger { + const r = nbi(); + this.multiplyTo(a, r); + return r; + } + square(): BigInteger { + const r = nbi(); + this.squareTo(r); + return r; + } + /** @internal */ + divide(a: BigInteger): BigInteger { + const r = nbi(); + this.divRemTo(a, r, null); + return r; + } + /** @internal */ + remainder(a: BigInteger): BigInteger { + const r = nbi(); + this.divRemTo(a, null, r); + return r; + } + divideAndRemainder(a: BigInteger): [BigInteger, BigInteger] { + const q = nbi(); + const r = nbi(); + this.divRemTo(a, q, r); + return [q, r]; + } + + /** @internal */ + dMultiply(n: number): void { + this[this.t] = this.am(0, n - 1, this, 0, 0, this.t); + ++this.t; + this.clamp(); + } + + /** @internal */ + dAddOffset(n: number, w: number): void { + if (n === 0) return; + while (this.t <= w) this[this.t++] = 0; + this[w] = this[w]! + n; + while (this[w]! >= this.DV) { + this[w] = this[w]! - this.DV; + if (++w >= this.t) this[this.t++] = 0; + this[w] = (this[w] ?? 0) + 1; + } + } + + /** @internal */ + pow(e: number): BigInteger { + return this.exp(e, new NullExp()); + } + + /** @internal */ + multiplyLowerTo(a: BigInteger, n: number, r: BigInteger): void { + let i = Math.min(this.t + a.t, n); + r.s = 0; + r.t = i; + while (i > 0) r[--i] = 0; + let j: number; + for (j = r.t - this.t; i < j; ++i) r[i + this.t] = this.am(0, a[i]!, r, i, 0, this.t); + for (j = Math.min(a.t, n); i < j; ++i) this.am(0, a[i]!, r, i, 0, n - i); + r.clamp(); + } + + /** @internal */ + multiplyUpperTo(a: BigInteger, n: number, r: BigInteger): void { + --n; + let i = (r.t = this.t + a.t - n); + r.s = 0; + while (--i >= 0) r[i] = 0; + for (i = Math.max(n - this.t, 0); i < a.t; ++i) { + r[this.t + i - n] = this.am(n - i, a[i]!, r, 0, 0, this.t + i - n); + } + r.clamp(); + r.drShiftTo(1, r); + } + + modPow(e: BigInteger, m: BigInteger): BigInteger { + let i = e.bitLength(); + let k: number; + let r = nbv(1); + let z: Reducer; + if (i <= 0) return r; + if (i < 18) k = 1; + else if (i < 48) k = 3; + else if (i < 144) k = 4; + else if (i < 768) k = 5; + else k = 6; + if (i < 8) z = new Classic(m); + else if (m.isEven()) z = new Barrett(m); + else z = new Montgomery(m); + + const g: BigInteger[] = []; + let n = 3; + const k1 = k - 1; + const km = (1 << k) - 1; + g[1] = z.convert(this); + if (k > 1) { + const g2 = nbi(); + z.sqrTo(g[1]!, g2); + while (n <= km) { + g[n] = nbi(); + z.mulTo(g2, g[n - 2]!, g[n]!); + n += 2; + } + } + + let j = e.t - 1; + let w: number; + let is1 = true; + let r2 = nbi(); + let t: BigInteger; + i = nbits(e[j]!) - 1; + while (j >= 0) { + if (i >= k1) w = (e[j]! >> (i - k1)) & km; + else { + w = (e[j]! & ((1 << (i + 1)) - 1)) << (k1 - i); + if (j > 0) w |= e[j - 1]! >> (this.DB + i - k1); + } + n = k; + while ((w & 1) === 0) { + w >>= 1; + --n; + } + if ((i -= n) < 0) { + i += this.DB; + --j; + } + if (is1) { + g[w]!.copyTo(r); + is1 = false; + } else { + while (n > 1) { + z.sqrTo(r, r2); + z.sqrTo(r2, r); + n -= 2; + } + if (n > 0) z.sqrTo(r, r2); + else { + t = r; + r = r2; + r2 = t; + } + z.mulTo(r2, g[w]!, r); + } + while (j >= 0 && (e[j]! & (1 << i)) === 0) { + z.sqrTo(r, r2); + t = r; + r = r2; + r2 = t; + if (--i < 0) { + i = this.DB - 1; + --j; + } + } + } + return z.revert(r); + } + + gcd(a: BigInteger): BigInteger { + let x = this.s < 0 ? this.negate() : this.clone(); + let y = a.s < 0 ? a.negate() : a.clone(); + if (x.compareTo(y) < 0) { + const t = x; + x = y; + y = t; + } + let i = x.getLowestSetBit(); + let g = y.getLowestSetBit(); + if (g < 0) return x; + if (i < g) g = i; + if (g > 0) { + x.rShiftTo(g, x); + y.rShiftTo(g, y); + } + while (x.signum() > 0) { + if ((i = x.getLowestSetBit()) > 0) x.rShiftTo(i, x); + if ((i = y.getLowestSetBit()) > 0) y.rShiftTo(i, y); + if (x.compareTo(y) >= 0) { + x.subTo(y, x); + x.rShiftTo(1, x); + } else { + y.subTo(x, y); + y.rShiftTo(1, y); + } + } + if (g > 0) y.lShiftTo(g, y); + return y; + } + + /** @internal */ + modInt(n: number): number { + if (n <= 0) return 0; + const d = this.DV % n; + let r = this.s < 0 ? n - 1 : 0; + if (this.t > 0) { + if (d === 0) r = this[0]! % n; + else for (let i = this.t - 1; i >= 0; --i) r = (d * r + this[i]!) % n; + } + return r; + } + + modInverse(m: BigInteger): BigInteger { + const ac = m.isEven(); + if ((this.isEven() && ac) || m.signum() === 0) return BigInteger.ZERO; + const u = m.clone(); + const v = this.clone(); + const a = nbv(1); + const b = nbv(0); + const c = nbv(0); + const d = nbv(1); + while (u.signum() !== 0) { + while (u.isEven()) { + u.rShiftTo(1, u); + if (ac) { + if (!a.isEven() || !b.isEven()) { + a.addTo(this, a); + b.subTo(m, b); + } + a.rShiftTo(1, a); + } else if (!b.isEven()) b.subTo(m, b); + b.rShiftTo(1, b); + } + while (v.isEven()) { + v.rShiftTo(1, v); + if (ac) { + if (!c.isEven() || !d.isEven()) { + c.addTo(this, c); + d.subTo(m, d); + } + c.rShiftTo(1, c); + } else if (!d.isEven()) d.subTo(m, d); + d.rShiftTo(1, d); + } + if (u.compareTo(v) >= 0) { + u.subTo(v, u); + if (ac) a.subTo(c, a); + b.subTo(d, b); + } else { + v.subTo(u, v); + if (ac) c.subTo(a, c); + d.subTo(b, d); + } + } + if (v.compareTo(BigInteger.ONE) !== 0) return BigInteger.ZERO; + if (d.compareTo(m) >= 0) return d.subtract(m); + // Normalize sign — extended Euclidean can leave d in (-m, m), or + // occasionally further negative; the legacy jsbn applies +m up to twice. + if (d.signum() < 0) d.addTo(m, d); + else return d; + if (d.signum() < 0) return d.add(m); + return d; + } + + isProbablePrime(t: number): boolean { + let i: number; + const x = this.abs(); + if (x.t === 1 && x[0]! <= lowprimes[lowprimes.length - 1]!) { + for (i = 0; i < lowprimes.length; ++i) if (x[0] === lowprimes[i]) return true; + return false; + } + if (x.isEven()) return false; + i = 1; + while (i < lowprimes.length) { + let m = lowprimes[i]!; + let j = i + 1; + while (j < lowprimes.length && m < lplim) m *= lowprimes[j++]!; + m = x.modInt(m); + while (i < j) if (m % lowprimes[i++]! === 0) return false; + } + return x.millerRabin(t); + } + + /** @internal */ + millerRabin(t: number): boolean { + const n1 = this.subtract(BigInteger.ONE); + const k = n1.getLowestSetBit(); + if (k <= 0) return false; + const r = n1.shiftRight(k); + // Witnesses must come from a CSPRNG over the full [2, n-2] range — not + // a small fixed table sampled with Math.random() — or an adversary can + // construct pseudoprimes tuned to the specific witnesses we'll pick. + // The caller's round count is honoured directly; see FIPS 186-4 Table + // C.3 for the minimums (40/28/16 rounds at 1024/1536/≥2048 bits). + const two = nbv(2); + const nMinus3 = n1.subtract(two); // n - 3 = range size for [2, n-2] + const byteLen = ((this.bitLength() + 7) >> 3) + 1; // +1 byte to keep modulo bias < 2^-8 + for (let i = 0; i < t; ++i) { + const rb = getBackend().randomBytes(byteLen); + const a = new BigInteger(rb).mod(nMinus3).add(two); + let y = a.modPow(r, this); + if (y.compareTo(BigInteger.ONE) !== 0 && y.compareTo(n1) !== 0) { + let j = 1; + while (j++ < k && y.compareTo(n1) !== 0) { + y = y.modPowInt(2, this); + if (y.compareTo(BigInteger.ONE) === 0) return false; + } + if (y.compareTo(n1) !== 0) return false; + } + } + return true; + } +} + +// helpers and reducers +function nbi(): BigInteger { + return new BigInteger(null); +} + +function nbv(i: number): BigInteger { + const r = nbi(); + r.fromInt(i); + return r; +} + +function op_and(x: number, y: number): number { + return x & y; +} +function op_or(x: number, y: number): number { + return x | y; +} +function op_xor(x: number, y: number): number { + return x ^ y; +} +function op_andnot(x: number, y: number): number { + return x & ~y; +} + +class Classic implements Reducer { + constructor(private readonly m: BigInteger) {} + convert(x: BigInteger): BigInteger { + if (x.s < 0 || x.compareTo(this.m) >= 0) return x.mod(this.m); + return x; + } + revert(x: BigInteger): BigInteger { + return x; + } + reduce(x: BigInteger): void { + x.divRemTo(this.m, null, x); + } + mulTo(x: BigInteger, y: BigInteger, r: BigInteger): void { + x.multiplyTo(y, r); + this.reduce(r); + } + sqrTo(x: BigInteger, r: BigInteger): void { + x.squareTo(r); + this.reduce(r); + } +} + +class Montgomery implements Reducer { + m: BigInteger; + mp: number; + mpl: number; + mph: number; + um: number; + mt2: number; + constructor(m: BigInteger) { + this.m = m; + this.mp = m.invDigit(); + this.mpl = this.mp & 0x7fff; + this.mph = this.mp >> 15; + this.um = (1 << (m.DB - 15)) - 1; + this.mt2 = 2 * m.t; + } + convert(x: BigInteger): BigInteger { + const r = nbi(); + x.abs().dlShiftTo(this.m.t, r); + r.divRemTo(this.m, null, r); + if (x.s < 0 && r.compareTo(BigInteger.ZERO) > 0) this.m.subTo(r, r); + return r; + } + revert(x: BigInteger): BigInteger { + const r = nbi(); + x.copyTo(r); + this.reduce(r); + return r; + } + reduce(x: BigInteger): void { + while (x.t <= this.mt2) x[x.t++] = 0; + for (let i = 0; i < this.m.t; ++i) { + let j = x[i]! & 0x7fff; + const u0 = + (j * this.mpl + (((j * this.mph + (x[i]! >> 15) * this.mpl) & this.um) << 15)) & x.DM; + j = i + this.m.t; + x[j] = (x[j] ?? 0) + this.m.am(0, u0, x, i, 0, this.m.t); + while (x[j]! >= x.DV) { + x[j] = x[j]! - x.DV; + x[++j] = (x[j] ?? 0) + 1; + } + } + x.clamp(); + x.drShiftTo(this.m.t, x); + if (x.compareTo(this.m) >= 0) x.subTo(this.m, x); + } + mulTo(x: BigInteger, y: BigInteger, r: BigInteger): void { + x.multiplyTo(y, r); + this.reduce(r); + } + sqrTo(x: BigInteger, r: BigInteger): void { + x.squareTo(r); + this.reduce(r); + } +} + +class Barrett implements Reducer { + r2: BigInteger; + q3: BigInteger; + mu: BigInteger; + m: BigInteger; + constructor(m: BigInteger) { + this.r2 = nbi(); + this.q3 = nbi(); + BigInteger.ONE.dlShiftTo(2 * m.t, this.r2); + this.mu = this.r2.divide(m); + this.m = m; + } + convert(x: BigInteger): BigInteger { + if (x.s < 0 || x.t > 2 * this.m.t) return x.mod(this.m); + if (x.compareTo(this.m) < 0) return x; + const r = nbi(); + x.copyTo(r); + this.reduce(r); + return r; + } + revert(x: BigInteger): BigInteger { + return x; + } + reduce(x: BigInteger): void { + x.drShiftTo(this.m.t - 1, this.r2); + if (x.t > this.m.t + 1) { + x.t = this.m.t + 1; + x.clamp(); + } + this.mu.multiplyUpperTo(this.r2, this.m.t + 1, this.q3); + this.m.multiplyLowerTo(this.q3, this.m.t + 1, this.r2); + while (x.compareTo(this.r2) < 0) x.dAddOffset(1, this.m.t + 1); + x.subTo(this.r2, x); + while (x.compareTo(this.m) >= 0) x.subTo(this.m, x); + } + mulTo(x: BigInteger, y: BigInteger, r: BigInteger): void { + x.multiplyTo(y, r); + this.reduce(r); + } + sqrTo(x: BigInteger, r: BigInteger): void { + x.squareTo(r); + this.reduce(r); + } +} + +class NullExp implements Reducer { + convert(x: BigInteger): BigInteger { + return x; + } + revert(x: BigInteger): BigInteger { + return x; + } + reduce(_x: BigInteger): void {} + mulTo(x: BigInteger, y: BigInteger, r: BigInteger): void { + x.multiplyTo(y, r); + } + sqrTo(x: BigInteger, r: BigInteger): void { + x.squareTo(r); + } +} + +// lowprimes table for primality testing +// biome-ignore format: keep the lowprimes table on one line for clarity vs the legacy file +const lowprimes: number[] = [2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97,101,103,107,109,113,127,131,137,139,149,151,157,163,167,173,179,181,191,193,197,199,211,223,227,229,233,239,241,251,257,263,269,271,277,281,283,293,307,311,313,317,331,337,347,349,353,359,367,373,379,383,389,397,401,409,419,421,431,433,439,443,449,457,461,463,467,479,487,491,499,503,509,521,523,541,547,557,563,569,571,577,587,593,599,601,607,613,617,619,631,641,643,647,653,659,661,673,677,683,691,701,709,719,727,733,739,743,751,757,761,769,773,787,797,809,811,821,823,827,829,839,853,857,859,863,877,881,883,887,907,911,919,929,937,941,947,953,967,971,977,983,991,997]; +const lplim = (1 << 26) / lowprimes[lowprimes.length - 1]!; + +// constants — defined after the class because they call nbv +BigInteger.ZERO = nbv(0); +BigInteger.ONE = nbv(1); diff --git a/src/bigint/big-integer-native.ts b/src/bigint/big-integer-native.ts new file mode 100644 index 0000000..f91c4fd --- /dev/null +++ b/src/bigint/big-integer-native.ts @@ -0,0 +1,396 @@ +import type { CryptoBackend } from '../crypto/types.js'; + +// Native-BigInt implementation of the jsbn BigInteger surface used by node-rsa. +// Drop-in for src/bigint/big-integer-jsbn.ts — same exported class name and +// public methods. Selected by src/bigint/big-integer.ts (selector). +// +// Backend injection (random_bytes for primality testing) mirrors the jsbn file. + +let _backend: CryptoBackend | undefined; + +export function setBigIntegerBackend(backend: CryptoBackend): void { + _backend = backend; +} + +function getBackend(): CryptoBackend { + if (!_backend) { + throw new Error( + 'BigInteger (native): backend not set. Did you import the package via its main entry?', + ); + } + return _backend; +} + +// helpers +const ZERO_BI = 0n; +const ONE_BI = 1n; +const TWO_BI = 2n; + +/** big-endian bytes → unsigned bigint (signed if `unsigned === false`). */ +function bytesToBigInt(bytes: Uint8Array, unsigned: boolean): bigint { + if (bytes.length === 0) return ZERO_BI; + if (!unsigned && (bytes[0]! & 0x80) !== 0) { + // signed two's complement → negative magnitude + let inv = ZERO_BI; + for (let i = 0; i < bytes.length; i++) { + inv = (inv << 8n) | BigInt(bytes[i]! ^ 0xff); + } + return -(inv + ONE_BI); + } + let v = ZERO_BI; + for (let i = 0; i < bytes.length; i++) { + v = (v << 8n) | BigInt(bytes[i]!); + } + return v; +} + +/** + * bigint → big-endian bytes, matching jsbn `toBuffer` semantics: + * - If `length` is given, output is exactly that many bytes (left-padded + * with zeros, truncated from the left if too long). + * - If `length` is omitted, the magnitude is emitted with a leading 0x00 + * when the high bit would otherwise be set — so the bytes survive an + * ASN.1 two's-complement round-trip without sign confusion. + */ +function bigIntToBytes(v: bigint, length?: number): Uint8Array { + if (v < ZERO_BI) throw new Error('BigInteger.toBuffer: negative value'); + if (v === ZERO_BI) return new Uint8Array(length ?? 1); + let hex = v.toString(16); + if (hex.length & 1) hex = `0${hex}`; + const raw = new Uint8Array(hex.length / 2); + for (let i = 0; i < raw.length; i++) { + raw[i] = Number.parseInt(hex.substring(i * 2, i * 2 + 2), 16); + } + if (length === undefined) { + // Prepend 0x00 if the high bit is set, matching jsbn's sign-preserving + // output. ASN.1 writers in this codebase normalize either form, so this + // is for byte-level parity rather than functional correctness. + if ((raw[0] as number) & 0x80) { + const padded = new Uint8Array(raw.length + 1); + padded.set(raw, 1); + return padded; + } + return raw; + } + if (length === raw.length) return raw; + if (length < raw.length) { + let cut = 0; + while (cut < raw.length - length && raw[cut] === 0) cut++; + if (raw.length - cut === length) return raw.slice(cut); + return raw.slice(raw.length - length); + } + const out = new Uint8Array(length); + out.set(raw, length - raw.length); + return out; +} + +function bitLengthOf(v: bigint): number { + if (v === ZERO_BI) return 0; + const x = v < ZERO_BI ? -v : v; + return x.toString(2).length; +} + +/** square-and-multiply modular exponentiation. */ +function modPowBI(base: bigint, exp: bigint, mod: bigint): bigint { + if (mod === ONE_BI) return ZERO_BI; + if (exp < ZERO_BI) { + // a^-e mod n = (a^-1)^e mod n + return modPowBI(modInverseBI(base, mod), -exp, mod); + } + let b = base % mod; + if (b < ZERO_BI) b += mod; + let result = ONE_BI; + let e = exp; + while (e > ZERO_BI) { + if (e & ONE_BI) result = (result * b) % mod; + e >>= ONE_BI; + b = (b * b) % mod; + } + return result; +} + +/** Extended Euclidean inverse; returns 0n if gcd(a, m) ≠ 1 (jsbn behaviour). */ +function modInverseBI(a: bigint, m: bigint): bigint { + if (m <= ZERO_BI) throw new Error('BigInteger.modInverse: modulus must be positive'); + let aNorm = a % m; + if (aNorm < ZERO_BI) aNorm += m; + let oldR = aNorm; + let r = m; + let oldS = ONE_BI; + let s = ZERO_BI; + while (r !== ZERO_BI) { + const q = oldR / r; + [oldR, r] = [r, oldR - q * r]; + [oldS, s] = [s, oldS - q * s]; + } + if (oldR !== ONE_BI) return ZERO_BI; // no inverse + return oldS < ZERO_BI ? oldS + m : oldS; +} + +function gcdBI(a: bigint, b: bigint): bigint { + let x = a < ZERO_BI ? -a : a; + let y = b < ZERO_BI ? -b : b; + while (y !== ZERO_BI) { + [x, y] = [y, x % y]; + } + return x; +} + +// 168 primes below 1000; matches jsbn's sieve table. +// biome-ignore format: dense table reads better unwrapped +const SMALL_PRIMES: ReadonlyArray = [ + 2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71, + 73,79,83,89,97,101,103,107,109,113,127,131,137,139,149,151,157,163,167,173, + 179,181,191,193,197,199,211,223,227,229,233,239,241,251,257,263,269,271,277,281, + 283,293,307,311,313,317,331,337,347,349,353,359,367,373,379,383,389,397,401,409, + 419,421,431,433,439,443,449,457,461,463,467,479,487,491,499,503,509,521,523,541, + 547,557,563,569,571,577,587,593,599,601,607,613,617,619,631,641,643,647,653,659, + 661,673,677,683,691,701,709,719,727,733,739,743,751,757,761,769,773,787,797,809, + 811,821,823,827,829,839,853,857,859,863,877,881,883,887,907,911,919,929,937,941, + 947,953,967,971,977,983,991,997, +]; +const SMALL_PRIMES_BI: ReadonlyArray = SMALL_PRIMES.map((p) => BigInt(p)); + +/** + * Miller-Rabin primality test with CSPRNG witnesses in [2, n-2]. + * Matches src/bigint/big-integer-jsbn.ts's audited semantics. + */ +function millerRabin(n: bigint, rounds: number): boolean { + if (n < TWO_BI) return false; + if (n === TWO_BI || n === 3n) return true; + if ((n & ONE_BI) === ZERO_BI) return false; + + const nMinus1 = n - ONE_BI; + let s = 0; + let d = nMinus1; + while ((d & ONE_BI) === ZERO_BI) { + d >>= ONE_BI; + s++; + } + + const byteLen = ((bitLengthOf(n) + 7) >> 3) + 1; + const backend = getBackend(); + const nMinus3 = n - 3n; // range size for [0, n-4]; we add 2 → [2, n-2] + + witnessLoop: for (let i = 0; i < rounds; i++) { + let a: bigint; + for (;;) { + a = bytesToBigInt(backend.randomBytes(byteLen), true) % nMinus3; + a += TWO_BI; + if (a >= TWO_BI && a <= nMinus1 - ONE_BI) break; + } + let x = modPowBI(a, d, n); + if (x === ONE_BI || x === nMinus1) continue; + for (let r = 1; r < s; r++) { + x = (x * x) % n; + if (x === nMinus1) continue witnessLoop; + } + return false; + } + return true; +} + +function probablePrime(v: bigint, rounds: number): boolean { + if (v < TWO_BI) return false; + for (const p of SMALL_PRIMES_BI) { + if (v === p) return true; + if (v % p === ZERO_BI) return false; + } + return millerRabin(v, rounds); +} + +/** + * Generate a random `bits`-bit number that passes a single Miller-Rabin round. + * Matches jsbn fromNumber(bits, 1): the caller in RSAKey.generate runs a + * second isProbablePrime(mrRounds) for full FIPS 186-4 validation. + */ +function generateProbablePrime(bits: number): bigint { + if (bits < 2) throw new Error('BigInteger: cannot generate prime with < 2 bits'); + const byteLen = (bits + 7) >> 3; + const backend = getBackend(); + while (true) { + const x = backend.randomBytes(byteLen); + // Mask off unused high bits, then force top bit (exact bit length) and bottom bit (odd). + const tailBits = bits & 7; + if (tailBits > 0) x[0] = (x[0]! & ((1 << tailBits) - 1)) as number; + let v = bytesToBigInt(x, true); + v |= ONE_BI << BigInt(bits - 1); + v |= ONE_BI; + // Sequential search; bail and retry with fresh randomness if we'd exceed bit length. + for (let step = 0; step < 1 << 15; step += 2) { + if (bitLengthOf(v) > bits) break; + if (probablePrime(v, 1)) return v; + v += TWO_BI; + } + } +} + +function parseFromString(s: string, radix: number): bigint { + if (s.length === 0) return ZERO_BI; + let str = s; + let neg = false; + if (str[0] === '-') { + neg = true; + str = str.substring(1); + } + if (str.length === 0) return ZERO_BI; + let v: bigint; + if (radix === 10) { + v = BigInt(str); + } else if (radix === 16) { + v = BigInt(`0x${str}`); + } else { + const r = BigInt(radix); + v = ZERO_BI; + for (let i = 0; i < str.length; i++) { + const code = str.charCodeAt(i); + let d: number; + if (code >= 48 && code <= 57) d = code - 48; + else if (code >= 65 && code <= 90) d = code - 55; + else if (code >= 97 && code <= 122) d = code - 87; + else continue; + if (d < 0 || d >= radix) continue; + v = v * r + BigInt(d); + } + } + return neg ? -v : v; +} + +// the public class +export class BigInteger { + static readonly ONE: BigInteger = new BigInteger(1); + static readonly ZERO: BigInteger = new BigInteger(0); + + private _v: bigint; + + constructor(a?: number | string | Uint8Array | bigint | null, b?: number, unsigned?: boolean) { + if (a == null) { + this._v = ZERO_BI; + } else if (typeof a === 'bigint') { + this._v = a; + } else if (typeof a === 'number') { + if (b === 1) { + this._v = generateProbablePrime(a); + } else { + this._v = BigInt(a); + } + } else if (typeof a === 'string') { + this._v = parseFromString(a, b ?? 10); + } else if (a instanceof Uint8Array) { + // Match jsbn's fromBuffer default: bytes are treated as unsigned + // big-endian unless the caller explicitly says otherwise. RSA + // components (n, p, q, …) almost always have the high bit set, so + // any other default flips them to negative. + this._v = bytesToBigInt(a, unsigned ?? true); + } else { + throw new Error(`BigInteger: unsupported input type ${typeof a}`); + } + } + + signum(): -1 | 0 | 1 { + return this._v === ZERO_BI ? 0 : this._v > ZERO_BI ? 1 : -1; + } + + compareTo(o: BigInteger): -1 | 0 | 1 { + if (this._v === o._v) return 0; + return this._v > o._v ? 1 : -1; + } + + bitLength(): number { + return bitLengthOf(this._v); + } + + testBit(n: number): boolean { + return ((this._v >> BigInt(n)) & ONE_BI) === ONE_BI; + } + + isEven(): boolean { + return (this._v & ONE_BI) === ZERO_BI; + } + + /** @internal */ + negate(): BigInteger { + return new BigInteger(-this._v); + } + + abs(): BigInteger { + return new BigInteger(this._v < ZERO_BI ? -this._v : this._v); + } + + add(o: BigInteger): BigInteger { + return new BigInteger(this._v + o._v); + } + + subtract(o: BigInteger): BigInteger { + return new BigInteger(this._v - o._v); + } + + multiply(o: BigInteger): BigInteger { + return new BigInteger(this._v * o._v); + } + + square(): BigInteger { + return new BigInteger(this._v * this._v); + } + + /** @internal */ + divide(o: BigInteger): BigInteger { + return new BigInteger(this._v / o._v); + } + + /** Returns [quotient, remainder]. Matches jsbn divideAndRemainder. */ + divideAndRemainder(o: BigInteger): [BigInteger, BigInteger] { + return [new BigInteger(this._v / o._v), new BigInteger(this._v % o._v)]; + } + + /** Always non-negative result for positive modulus (Java/jsbn semantics). */ + mod(o: BigInteger): BigInteger { + const m = o._v; + if (m === ZERO_BI) throw new Error('BigInteger.mod: divide by zero'); + let r = this._v % m; + const absM = m < ZERO_BI ? -m : m; + if (r < ZERO_BI) r += absM; + return new BigInteger(r); + } + + modPow(e: BigInteger, m: BigInteger): BigInteger { + return new BigInteger(modPowBI(this._v, e._v, m._v)); + } + + modPowInt(e: number, m: BigInteger): BigInteger { + return new BigInteger(modPowBI(this._v, BigInt(e), m._v)); + } + + modInverse(m: BigInteger): BigInteger { + return new BigInteger(modInverseBI(this._v, m._v)); + } + + gcd(o: BigInteger): BigInteger { + return new BigInteger(gcdBI(this._v, o._v)); + } + + shiftLeft(n: number): BigInteger { + return new BigInteger(n >= 0 ? this._v << BigInt(n) : this._v >> BigInt(-n)); + } + + shiftRight(n: number): BigInteger { + return new BigInteger(n >= 0 ? this._v >> BigInt(n) : this._v << BigInt(-n)); + } + + isProbablePrime(rounds: number): boolean { + return probablePrime(this._v, rounds); + } + + toString(radix?: number): string { + return this._v.toString(radix ?? 10); + } + + /** Unsigned big-endian bytes; pads/truncates to `length` if given (jsbn parity). */ + toBuffer(length?: number): Uint8Array | null { + if (this._v < ZERO_BI) { + // jsbn returns null on negative; matches callers that check `if (!out) throw`. + return null; + } + return bigIntToBytes(this._v, length); + } +} diff --git a/src/bigint/big-integer.ts b/src/bigint/big-integer.ts new file mode 100644 index 0000000..38831e8 --- /dev/null +++ b/src/bigint/big-integer.ts @@ -0,0 +1,52 @@ +import type { CryptoBackend } from '../crypto/types.js'; +import { + BigInteger as JsbnBigInteger, + setBigIntegerBackend as setJsbnBackend, +} from './big-integer-jsbn.js'; +import { + BigInteger as NativeBigInteger, + setBigIntegerBackend as setNativeBackend, +} from './big-integer-native.js'; + +export type BigIntegerImpl = 'jsbn' | 'native'; + +// The instance type stays jsbn-shaped at the type level (both impls satisfy +// the same public surface); a single `BigInteger` identifier serves as both +// value (live binding to the active class) and type alias for instances. +export type BigInteger = JsbnBigInteger; +export let BigInteger: typeof JsbnBigInteger = JsbnBigInteger; + +let _currentImpl: BigIntegerImpl = 'jsbn'; +let _currentBackend: CryptoBackend | undefined; + +/** + * Switch the active BigInteger implementation. `'native'` falls back to + * `'jsbn'` silently if `globalThis.BigInt` is unavailable. + */ +export function setBigIntegerImpl(impl: BigIntegerImpl): BigIntegerImpl { + if (impl === 'native' && typeof BigInt === 'function') { + BigInteger = NativeBigInteger as unknown as typeof JsbnBigInteger; + _currentImpl = 'native'; + } else { + BigInteger = JsbnBigInteger; + _currentImpl = 'jsbn'; + } + // Re-apply the most recent backend to both impls so the next prime search + // works regardless of which one the user just flipped to. + if (_currentBackend) { + setJsbnBackend(_currentBackend); + setNativeBackend(_currentBackend); + } + return _currentImpl; +} + +/** + * Inject the crypto backend that BigInteger uses for RNG (primality + * testing). Applied to both impls so a later `setBigIntegerImpl` doesn't + * lose the binding. + */ +export function setBigIntegerBackend(backend: CryptoBackend): void { + _currentBackend = backend; + setJsbnBackend(backend); + setNativeBackend(backend); +} diff --git a/src/bigint/index.ts b/src/bigint/index.ts new file mode 100644 index 0000000..b56a1c8 --- /dev/null +++ b/src/bigint/index.ts @@ -0,0 +1 @@ +export { BigInteger, setBigIntegerBackend } from './big-integer.js'; diff --git a/src/crypto/backend.node.ts b/src/crypto/backend.node.ts new file mode 100644 index 0000000..09482a9 --- /dev/null +++ b/src/crypto/backend.node.ts @@ -0,0 +1,53 @@ +import { createHash, randomBytes as nodeRandomBytes } from 'node:crypto'; +import type { CryptoBackend, HashingAlgorithm } from './types.js'; + +// MD4 lives in OpenSSL's legacy provider, which is not loaded by default in +// OpenSSL 3 (Node 17+). Probe once at module load to decide if it's usable. +const CANDIDATE: readonly HashingAlgorithm[] = [ + 'md4', + 'md5', + 'ripemd160', + 'sha1', + 'sha224', + 'sha256', + 'sha384', + 'sha512', +]; + +const SUPPORTED: ReadonlySet = (() => { + const set = new Set(); + for (const alg of CANDIDATE) { + try { + createHash(alg); + set.add(alg); + } catch { + // Skip: provider not loaded (e.g., MD4 in OpenSSL 3). + } + } + return set; +})(); + +function bufferToU8(buf: Uint8Array): Uint8Array { + return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength); +} + +export const nodeBackend: CryptoBackend = { + name: 'node', + + randomBytes(n) { + return bufferToU8(nodeRandomBytes(n)); + }, + + digest(alg, data) { + if (!SUPPORTED.has(alg)) { + throw new Error(`Unsupported hash algorithm: ${alg}`); + } + const h = createHash(alg); + h.update(data); + return bufferToU8(h.digest()); + }, + + supportsHash(alg) { + return SUPPORTED.has(alg); + }, +}; diff --git a/src/crypto/backend.web.ts b/src/crypto/backend.web.ts new file mode 100644 index 0000000..ea6993f --- /dev/null +++ b/src/crypto/backend.web.ts @@ -0,0 +1,54 @@ +import { md5, ripemd160, sha1 } from '@noble/hashes/legacy.js'; +import { sha224, sha256, sha384, sha512 } from '@noble/hashes/sha2.js'; +import type { CryptoBackend, HashingAlgorithm } from './types.js'; + +type HashFn = (data: Uint8Array) => Uint8Array; + +const HASHES: Readonly, HashFn>> = { + md5: (d) => md5(d), + ripemd160: (d) => ripemd160(d), + sha1: (d) => sha1(d), + sha224: (d) => sha224(d), + sha256: (d) => sha256(d), + sha384: (d) => sha384(d), + sha512: (d) => sha512(d), +}; + +function getWebCrypto(): Crypto { + const c = globalThis.crypto; + if (!c || typeof c.getRandomValues !== 'function') { + throw new Error( + 'Web Crypto getRandomValues unavailable. Are you running in an environment without secure RNG?', + ); + } + return c; +} + +export const webBackend: CryptoBackend = { + name: 'web', + + randomBytes(n) { + const out = new Uint8Array(n); + let off = 0; + const c = getWebCrypto(); + while (off < n) { + const chunk = Math.min(n - off, 65536); + c.getRandomValues(out.subarray(off, off + chunk)); + off += chunk; + } + return out; + }, + + digest(alg, data) { + if (alg === 'md4') { + throw new Error('MD4 is not supported in the browser backend (Node-only)'); + } + const fn = HASHES[alg]; + if (!fn) throw new Error(`Unsupported hash algorithm: ${alg}`); + return fn(data); + }, + + supportsHash(alg) { + return alg !== 'md4' && alg in HASHES; + }, +}; diff --git a/src/crypto/bytes.ts b/src/crypto/bytes.ts new file mode 100644 index 0000000..374a6d1 --- /dev/null +++ b/src/crypto/bytes.ts @@ -0,0 +1,127 @@ +const HEX_CHARS = '0123456789abcdef'; +const utf8Encoder = new TextEncoder(); +const utf8Decoder = new TextDecoder('utf-8', { fatal: false }); + +export function concat(...arrays: readonly Uint8Array[]): Uint8Array { + let total = 0; + for (const a of arrays) total += a.length; + const out = new Uint8Array(total); + let off = 0; + for (const a of arrays) { + out.set(a, off); + off += a.length; + } + return out; +} + +// Length is treated as public: callers (OAEP lHash, PSS H/H', PKCS#1 padded +// block) compare buffers whose size is derived from public modulus/hash +// parameters, not from secret data. Do not use on variable-length secrets. +export function constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean { + if (a.length !== b.length) return false; + let diff = 0; + for (let i = 0; i < a.length; i++) { + diff |= (a[i] as number) ^ (b[i] as number); + } + return diff === 0; +} + +export function toHex(bytes: Uint8Array): string { + let out = ''; + for (let i = 0; i < bytes.length; i++) { + const b = bytes[i] as number; + out += HEX_CHARS[b >>> 4]; + out += HEX_CHARS[b & 0x0f]; + } + return out; +} + +export function fromHex(hex: string): Uint8Array { + const clean = hex.startsWith('0x') ? hex.slice(2) : hex; + if (clean.length % 2 !== 0) { + throw new Error(`Invalid hex: odd length ${clean.length}`); + } + const out = new Uint8Array(clean.length / 2); + for (let i = 0; i < out.length; i++) { + const hi = parseHexNibble(clean.charCodeAt(i * 2)); + const lo = parseHexNibble(clean.charCodeAt(i * 2 + 1)); + out[i] = (hi << 4) | lo; + } + return out; +} + +function parseHexNibble(c: number): number { + if (c >= 0x30 && c <= 0x39) return c - 0x30; + if (c >= 0x61 && c <= 0x66) return c - 0x61 + 10; + if (c >= 0x41 && c <= 0x46) return c - 0x41 + 10; + throw new Error(`Invalid hex character: 0x${c.toString(16).padStart(2, '0')}`); +} + +export function toBase64(bytes: Uint8Array): string { + let binary = ''; + const chunk = 0x8000; + for (let i = 0; i < bytes.length; i += chunk) { + const slice = bytes.subarray(i, Math.min(i + chunk, bytes.length)); + binary += String.fromCharCode(...slice); + } + return btoa(binary); +} + +export function fromBase64(b64: string): Uint8Array { + const binary = atob(b64); + const out = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + out[i] = binary.charCodeAt(i); + } + return out; +} + +export function fromUtf8(s: string): Uint8Array { + return utf8Encoder.encode(s); +} + +export function toUtf8(bytes: Uint8Array): string { + return utf8Decoder.decode(bytes); +} + +// latin1 (a.k.a. legacy Node "binary") — 1:1 mapping between byte 0x00-0xFF +// and code points U+0000-U+00FF. Use for raw-byte string transport, never for +// human-readable text. +export function fromLatin1(s: string): Uint8Array { + const out = new Uint8Array(s.length); + for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i) & 0xff; + return out; +} + +export function toLatin1(bytes: Uint8Array): string { + let out = ''; + const chunk = 0x8000; + for (let i = 0; i < bytes.length; i += chunk) { + const slice = bytes.subarray(i, Math.min(i + chunk, bytes.length)); + out += String.fromCharCode(...slice); + } + return out; +} + +export function readUInt32BE(bytes: Uint8Array, offset = 0): number { + if (offset + 4 > bytes.length) { + throw new RangeError(`readUInt32BE: out of range (offset=${offset}, length=${bytes.length})`); + } + return ( + (((bytes[offset] as number) << 24) | + ((bytes[offset + 1] as number) << 16) | + ((bytes[offset + 2] as number) << 8) | + (bytes[offset + 3] as number)) >>> + 0 + ); +} + +export function writeUInt32BE(value: number, target: Uint8Array, offset = 0): void { + if (offset + 4 > target.length) { + throw new RangeError(`writeUInt32BE: out of range (offset=${offset}, length=${target.length})`); + } + target[offset] = (value >>> 24) & 0xff; + target[offset + 1] = (value >>> 16) & 0xff; + target[offset + 2] = (value >>> 8) & 0xff; + target[offset + 3] = value & 0xff; +} diff --git a/src/crypto/digest-length.ts b/src/crypto/digest-length.ts new file mode 100644 index 0000000..7117a3d --- /dev/null +++ b/src/crypto/digest-length.ts @@ -0,0 +1,12 @@ +import type { HashingAlgorithm } from './types.js'; + +export const DIGEST_LENGTH: Readonly> = Object.freeze({ + md4: 16, + md5: 16, + ripemd160: 20, + sha1: 20, + sha224: 28, + sha256: 32, + sha384: 48, + sha512: 64, +}); diff --git a/src/crypto/types.ts b/src/crypto/types.ts new file mode 100644 index 0000000..f7c37ae --- /dev/null +++ b/src/crypto/types.ts @@ -0,0 +1,19 @@ +export type HashingAlgorithm = + | 'md4' + | 'md5' + | 'ripemd160' + | 'sha1' + | 'sha224' + | 'sha256' + | 'sha384' + | 'sha512'; + +export interface CryptoBackend { + readonly name: 'node' | 'web'; + + randomBytes(n: number): Uint8Array; + + digest(alg: HashingAlgorithm, data: Uint8Array): Uint8Array; + + supportsHash(alg: HashingAlgorithm): boolean; +} diff --git a/src/encryptEngines/encryptEngines.js b/src/encryptEngines/encryptEngines.js deleted file mode 100644 index d359452..0000000 --- a/src/encryptEngines/encryptEngines.js +++ /dev/null @@ -1,17 +0,0 @@ -var crypt = require('crypto'); - -module.exports = { - getEngine: function (keyPair, options) { - var engine = require('./js.js'); - if (options.environment === 'node') { - if (typeof crypt.publicEncrypt === 'function' && typeof crypt.privateDecrypt === 'function') { - if (typeof crypt.privateEncrypt === 'function' && typeof crypt.publicDecrypt === 'function') { - engine = require('./io.js'); - } else { - engine = require('./node12.js'); - } - } - } - return engine(keyPair, options); - } -}; \ No newline at end of file diff --git a/src/encryptEngines/io.js b/src/encryptEngines/io.js deleted file mode 100644 index 799ae1d..0000000 --- a/src/encryptEngines/io.js +++ /dev/null @@ -1,72 +0,0 @@ -var crypto = require('crypto'); -var constants = require('constants'); -var schemes = require('../schemes/schemes.js'); - -module.exports = function (keyPair, options) { - var pkcs1Scheme = schemes.pkcs1.makeScheme(keyPair, options); - - return { - encrypt: function (buffer, usePrivate) { - var padding; - if (usePrivate) { - padding = constants.RSA_PKCS1_PADDING; - if (options.encryptionSchemeOptions && options.encryptionSchemeOptions.padding) { - padding = options.encryptionSchemeOptions.padding; - } - return crypto.privateEncrypt({ - key: options.rsaUtils.exportKey('private'), - padding: padding - }, buffer); - } else { - padding = constants.RSA_PKCS1_OAEP_PADDING; - if (options.encryptionScheme === 'pkcs1') { - padding = constants.RSA_PKCS1_PADDING; - } - if (options.encryptionSchemeOptions && options.encryptionSchemeOptions.padding) { - padding = options.encryptionSchemeOptions.padding; - } - - var data = buffer; - if (padding === constants.RSA_NO_PADDING) { - data = pkcs1Scheme.pkcs0pad(buffer); - } - - return crypto.publicEncrypt({ - key: options.rsaUtils.exportKey('public'), - padding: padding - }, data); - } - }, - - decrypt: function (buffer, usePublic) { - var padding; - if (usePublic) { - padding = constants.RSA_PKCS1_PADDING; - if (options.encryptionSchemeOptions && options.encryptionSchemeOptions.padding) { - padding = options.encryptionSchemeOptions.padding; - } - return crypto.publicDecrypt({ - key: options.rsaUtils.exportKey('public'), - padding: padding - }, buffer); - } else { - padding = constants.RSA_PKCS1_OAEP_PADDING; - if (options.encryptionScheme === 'pkcs1') { - padding = constants.RSA_PKCS1_PADDING; - } - if (options.encryptionSchemeOptions && options.encryptionSchemeOptions.padding) { - padding = options.encryptionSchemeOptions.padding; - } - var res = crypto.privateDecrypt({ - key: options.rsaUtils.exportKey('private'), - padding: padding - }, buffer); - - if (padding === constants.RSA_NO_PADDING) { - return pkcs1Scheme.pkcs0unpad(res); - } - return res; - } - } - }; -}; \ No newline at end of file diff --git a/src/encryptEngines/js.js b/src/encryptEngines/js.js deleted file mode 100644 index f148441..0000000 --- a/src/encryptEngines/js.js +++ /dev/null @@ -1,34 +0,0 @@ -var BigInteger = require('../libs/jsbn.js'); -var schemes = require('../schemes/schemes.js'); - -module.exports = function (keyPair, options) { - var pkcs1Scheme = schemes.pkcs1.makeScheme(keyPair, options); - - return { - encrypt: function (buffer, usePrivate) { - var m, c; - if (usePrivate) { - /* Type 1: zeros padding for private key encrypt */ - m = new BigInteger(pkcs1Scheme.encPad(buffer, {type: 1})); - c = keyPair.$doPrivate(m); - } else { - m = new BigInteger(keyPair.encryptionScheme.encPad(buffer)); - c = keyPair.$doPublic(m); - } - return c.toBuffer(keyPair.encryptedDataLength); - }, - - decrypt: function (buffer, usePublic) { - var m, c = new BigInteger(buffer); - - if (usePublic) { - m = keyPair.$doPublic(c); - /* Type 1: zeros padding for private key decrypt */ - return pkcs1Scheme.encUnPad(m.toBuffer(keyPair.encryptedDataLength), {type: 1}); - } else { - m = keyPair.$doPrivate(c); - return keyPair.encryptionScheme.encUnPad(m.toBuffer(keyPair.encryptedDataLength)); - } - } - }; -}; \ No newline at end of file diff --git a/src/encryptEngines/node12.js b/src/encryptEngines/node12.js deleted file mode 100644 index 86e5836..0000000 --- a/src/encryptEngines/node12.js +++ /dev/null @@ -1,56 +0,0 @@ -var crypto = require('crypto'); -var constants = require('constants'); -var schemes = require('../schemes/schemes.js'); - -module.exports = function (keyPair, options) { - var jsEngine = require('./js.js')(keyPair, options); - var pkcs1Scheme = schemes.pkcs1.makeScheme(keyPair, options); - - return { - encrypt: function (buffer, usePrivate) { - if (usePrivate) { - return jsEngine.encrypt(buffer, usePrivate); - } - var padding = constants.RSA_PKCS1_OAEP_PADDING; - if (options.encryptionScheme === 'pkcs1') { - padding = constants.RSA_PKCS1_PADDING; - } - if (options.encryptionSchemeOptions && options.encryptionSchemeOptions.padding) { - padding = options.encryptionSchemeOptions.padding; - } - - var data = buffer; - if (padding === constants.RSA_NO_PADDING) { - data = pkcs1Scheme.pkcs0pad(buffer); - } - - return crypto.publicEncrypt({ - key: options.rsaUtils.exportKey('public'), - padding: padding - }, data); - }, - - decrypt: function (buffer, usePublic) { - if (usePublic) { - return jsEngine.decrypt(buffer, usePublic); - } - var padding = constants.RSA_PKCS1_OAEP_PADDING; - if (options.encryptionScheme === 'pkcs1') { - padding = constants.RSA_PKCS1_PADDING; - } - if (options.encryptionSchemeOptions && options.encryptionSchemeOptions.padding) { - padding = options.encryptionSchemeOptions.padding; - } - - var res = crypto.privateDecrypt({ - key: options.rsaUtils.exportKey('private'), - padding: padding - }, buffer); - - if (padding === constants.RSA_NO_PADDING) { - return pkcs1Scheme.pkcs0unpad(res); - } - return res; - } - }; -}; \ No newline at end of file diff --git a/src/formats/components.js b/src/formats/components.js deleted file mode 100644 index a275314..0000000 --- a/src/formats/components.js +++ /dev/null @@ -1,71 +0,0 @@ -var _ = require('../utils')._; -var utils = require('../utils'); - -module.exports = { - privateExport: function (key, options) { - return { - n: key.n.toBuffer(), - e: key.e, - d: key.d.toBuffer(), - p: key.p.toBuffer(), - q: key.q.toBuffer(), - dmp1: key.dmp1.toBuffer(), - dmq1: key.dmq1.toBuffer(), - coeff: key.coeff.toBuffer() - }; - }, - - privateImport: function (key, data, options) { - if (data.n && data.e && data.d && data.p && data.q && data.dmp1 && data.dmq1 && data.coeff) { - key.setPrivate( - data.n, - data.e, - data.d, - data.p, - data.q, - data.dmp1, - data.dmq1, - data.coeff - ); - } else { - throw Error("Invalid key data"); - } - }, - - publicExport: function (key, options) { - return { - n: key.n.toBuffer(), - e: key.e - }; - }, - - publicImport: function (key, data, options) { - if (data.n && data.e) { - key.setPublic( - data.n, - data.e - ); - } else { - throw Error("Invalid key data"); - } - }, - - /** - * Trying autodetect and import key - * @param key - * @param data - */ - autoImport: function (key, data) { - if (data.n && data.e) { - if (data.d && data.p && data.q && data.dmp1 && data.dmq1 && data.coeff) { - module.exports.privateImport(key, data); - return true; - } else { - module.exports.publicImport(key, data); - return true; - } - } - - return false; - } -}; diff --git a/src/formats/components.ts b/src/formats/components.ts new file mode 100644 index 0000000..a4904da --- /dev/null +++ b/src/formats/components.ts @@ -0,0 +1,72 @@ +import type { RSAKey } from '../rsa/key.js'; +import type { ExportOptions, FormatProvider, ImportOptions } from './types.js'; + +export interface PrivateComponents { + n: Uint8Array; + e: number | Uint8Array; + d: Uint8Array; + p: Uint8Array; + q: Uint8Array; + dmp1: Uint8Array; + dmq1: Uint8Array; + coeff: Uint8Array; +} + +export interface PublicComponents { + n: Uint8Array; + e: number | Uint8Array; +} + +/** + * Raw component object — plain JS object with `n`, `e`, `d`, `p`, `q`, + * `dmp1`, `dmq1`, `coeff` (private) or just `n`, `e` (public). + * No encoding step; intended for direct programmatic input. + */ +export const componentsFormat: FormatProvider = { + privateExport(key: RSAKey, _options: ExportOptions = {}): PrivateComponents { + if (!key.n || !key.d || !key.p || !key.q || !key.dmp1 || !key.dmq1 || !key.coeff) { + throw new Error('components export: incomplete private key'); + } + return { + n: key.n.toBuffer() as Uint8Array, + e: key.e, + d: key.d.toBuffer() as Uint8Array, + p: key.p.toBuffer() as Uint8Array, + q: key.q.toBuffer() as Uint8Array, + dmp1: key.dmp1.toBuffer() as Uint8Array, + dmq1: key.dmq1.toBuffer() as Uint8Array, + coeff: key.coeff.toBuffer() as Uint8Array, + }; + }, + + privateImport(key: RSAKey, data: unknown, _options: ImportOptions = {}): void { + const d = data as Partial; + if (!d.n || !d.e || !d.d || !d.p || !d.q || !d.dmp1 || !d.dmq1 || !d.coeff) { + throw new Error('Invalid key data'); + } + key.setPrivate(d.n, d.e, d.d, d.p, d.q, d.dmp1, d.dmq1, d.coeff); + }, + + publicExport(key: RSAKey, _options: ExportOptions = {}): PublicComponents { + if (!key.n) throw new Error('components export: missing modulus'); + return { n: key.n.toBuffer() as Uint8Array, e: key.e }; + }, + + publicImport(key: RSAKey, data: unknown, _options: ImportOptions = {}): void { + const d = data as Partial; + if (!d.n || d.e == null) throw new Error('Invalid key data'); + key.setPublic(d.n, d.e); + }, + + autoImport(key: RSAKey, data: unknown): boolean { + if (typeof data !== 'object' || data === null) return false; + const d = data as Partial; + if (!d.n || d.e == null) return false; + if (d.d && d.p && d.q && d.dmp1 && d.dmq1 && d.coeff) { + componentsFormat.privateImport?.(key, data); + return true; + } + componentsFormat.publicImport?.(key, data); + return true; + }, +}; diff --git a/src/formats/formats.js b/src/formats/formats.js deleted file mode 100644 index e1fc3fb..0000000 --- a/src/formats/formats.js +++ /dev/null @@ -1,97 +0,0 @@ -var _ = require('../utils')._; - -function formatParse(format) { - format = format.split('-'); - var keyType = 'private'; - var keyOpt = {type: 'default'}; - - for (var i = 1; i < format.length; i++) { - if (format[i]) { - switch (format[i]) { - case 'public': - keyType = format[i]; - break; - case 'private': - keyType = format[i]; - break; - case 'pem': - keyOpt.type = format[i]; - break; - case 'der': - keyOpt.type = format[i]; - break; - } - } - } - - return {scheme: format[0], keyType: keyType, keyOpt: keyOpt}; -} - -module.exports = { - pkcs1: require('./pkcs1'), - pkcs8: require('./pkcs8'), - components: require('./components'), - openssh: require('./openssh'), - - isPrivateExport: function (format) { - return module.exports[format] && typeof module.exports[format].privateExport === 'function'; - }, - - isPrivateImport: function (format) { - return module.exports[format] && typeof module.exports[format].privateImport === 'function'; - }, - - isPublicExport: function (format) { - return module.exports[format] && typeof module.exports[format].publicExport === 'function'; - }, - - isPublicImport: function (format) { - return module.exports[format] && typeof module.exports[format].publicImport === 'function'; - }, - - detectAndImport: function (key, data, format) { - if (format === undefined) { - for (var scheme in module.exports) { - if (typeof module.exports[scheme].autoImport === 'function' && module.exports[scheme].autoImport(key, data)) { - return true; - } - } - } else if (format) { - var fmt = formatParse(format); - - if (module.exports[fmt.scheme]) { - if (fmt.keyType === 'private') { - module.exports[fmt.scheme].privateImport(key, data, fmt.keyOpt); - } else { - module.exports[fmt.scheme].publicImport(key, data, fmt.keyOpt); - } - } else { - throw Error('Unsupported key format'); - } - } - - return false; - }, - - detectAndExport: function (key, format) { - if (format) { - var fmt = formatParse(format); - - if (module.exports[fmt.scheme]) { - if (fmt.keyType === 'private') { - if (!key.isPrivate()) { - throw Error("This is not private key"); - } - return module.exports[fmt.scheme].privateExport(key, fmt.keyOpt); - } else { - if (!key.isPublic()) { - throw Error("This is not public key"); - } - return module.exports[fmt.scheme].publicExport(key, fmt.keyOpt); - } - } else { - throw Error('Unsupported key format'); - } - } - } -}; \ No newline at end of file diff --git a/src/formats/index.ts b/src/formats/index.ts new file mode 100644 index 0000000..254986a --- /dev/null +++ b/src/formats/index.ts @@ -0,0 +1,89 @@ +import type { RSAKey } from '../rsa/key.js'; +import { componentsFormat } from './components.js'; +import { opensshFormat } from './openssh.js'; +import { pkcs1Format } from './pkcs1.js'; +import { pkcs8Format } from './pkcs8.js'; +import type { FormatProvider, ImportOptions } from './types.js'; + +/** Registry of built-in format providers, keyed by scheme name. */ +export const FORMATS: Record = { + pkcs1: pkcs1Format, + pkcs8: pkcs8Format, + components: componentsFormat, + openssh: opensshFormat, +}; + +interface ParsedFormat { + scheme: string; + keyType: 'private' | 'public'; + keyOpt: ImportOptions; +} + +function formatParse(format: string): ParsedFormat { + const parts = format.split('-'); + let keyType: 'private' | 'public' = 'private'; + const keyOpt: ImportOptions = { type: 'default' }; + for (let i = 1; i < parts.length; i++) { + const p = parts[i]; + if (p === 'public' || p === 'private') keyType = p; + else if (p === 'pem' || p === 'der') keyOpt.type = p; + } + return { scheme: parts[0] ?? '', keyType, keyOpt }; +} + +/** + * Import `data` into `key`. If `format` is omitted, each provider's + * `autoImport` is tried in registration order. Returns false only on + * the no-format auto path when nothing matched; the explicit-format path + * throws on unknown scheme or missing provider method. + * + * Format string is `[-public|-private][-pem|-der]`, e.g. + * `"pkcs1-private-pem"`. Defaults: keyType=private, type=default. + */ +export function detectAndImport(key: RSAKey, data: unknown, format?: string): boolean { + if (!format) { + for (const scheme of Object.values(FORMATS)) { + if (scheme.autoImport?.(key, data)) return true; + } + return false; + } + const fmt = formatParse(format); + const provider = FORMATS[fmt.scheme]; + if (!provider) throw new Error('Unsupported key format'); + if (fmt.keyType === 'private') { + if (!provider.privateImport) throw new Error(`Format ${fmt.scheme} has no privateImport`); + provider.privateImport(key, data, fmt.keyOpt); + } else { + if (!provider.publicImport) throw new Error(`Format ${fmt.scheme} has no publicImport`); + provider.publicImport(key, data, fmt.keyOpt); + } + return true; +} + +/** + * Export `key` in the given format. Returns undefined if `format` is omitted. + * Throws if the scheme is unknown, the key lacks the requested half + * (private/public), or the provider doesn't implement that direction. + * Format string syntax matches {@link detectAndImport}. + */ +export function detectAndExport( + key: RSAKey, + format?: string, +): Uint8Array | string | object | undefined { + if (!format) return undefined; + const fmt = formatParse(format); + const provider = FORMATS[fmt.scheme]; + if (!provider) throw new Error('Unsupported key format'); + if (fmt.keyType === 'private') { + if (!key.isPrivate()) throw new Error('This is not private key'); + if (!provider.privateExport) throw new Error(`Format ${fmt.scheme} has no privateExport`); + return provider.privateExport(key, fmt.keyOpt); + } + if (!key.isPublic()) throw new Error('This is not public key'); + if (!provider.publicExport) throw new Error(`Format ${fmt.scheme} has no publicExport`); + return provider.publicExport(key, fmt.keyOpt); +} + +export type { PrivateComponents, PublicComponents } from './components.js'; +export type { ExportOptions, FormatProvider, ImportOptions } from './types.js'; +export { componentsFormat, opensshFormat, pkcs1Format, pkcs8Format }; diff --git a/src/formats/openssh.js b/src/formats/openssh.js deleted file mode 100644 index 60aa146..0000000 --- a/src/formats/openssh.js +++ /dev/null @@ -1,292 +0,0 @@ -var _ = require("../utils")._; -var utils = require("../utils"); -var BigInteger = require("../libs/jsbn"); - -const PRIVATE_OPENING_BOUNDARY = "-----BEGIN OPENSSH PRIVATE KEY-----"; -const PRIVATE_CLOSING_BOUNDARY = "-----END OPENSSH PRIVATE KEY-----"; - -module.exports = { - privateExport: function (key, options) { - const nbuf = key.n.toBuffer(); - - let ebuf = Buffer.alloc(4) - ebuf.writeUInt32BE(key.e, 0); - //Slice leading zeroes - while (ebuf[0] === 0) ebuf = ebuf.slice(1); - - const dbuf = key.d.toBuffer(); - const coeffbuf = key.coeff.toBuffer(); - const pbuf = key.p.toBuffer(); - const qbuf = key.q.toBuffer(); - let commentbuf; - if (typeof key.sshcomment !== "undefined") { - commentbuf = Buffer.from(key.sshcomment); - } else { - commentbuf = Buffer.from([]); - } - - const pubkeyLength = - 11 + // 32bit length, 'ssh-rsa' - 4 + ebuf.byteLength + - 4 + nbuf.byteLength; - - const privateKeyLength = - 8 + //64bit unused checksum - 11 + // 32bit length, 'ssh-rsa' - 4 + nbuf.byteLength + - 4 + ebuf.byteLength + - 4 + dbuf.byteLength + - 4 + coeffbuf.byteLength + - 4 + pbuf.byteLength + - 4 + qbuf.byteLength + - 4 + commentbuf.byteLength; - - let length = - 15 + //openssh-key-v1,0x00, - 16 + // 2*(32bit length, 'none') - 4 + // 32bit length, empty string - 4 + // 32bit number of keys - 4 + // 32bit pubkey length - pubkeyLength + - 4 + //32bit private+checksum+comment+padding length - privateKeyLength; - - const paddingLength = Math.ceil(privateKeyLength / 8) * 8 - privateKeyLength; - length += paddingLength; - - const buf = Buffer.alloc(length); - const writer = {buf: buf, off: 0}; - buf.write("openssh-key-v1", "utf8"); - buf.writeUInt8(0, 14); - writer.off += 15; - - writeOpenSSHKeyString(writer, Buffer.from("none")); - writeOpenSSHKeyString(writer, Buffer.from("none")); - writeOpenSSHKeyString(writer, Buffer.from("")); - - writer.off = writer.buf.writeUInt32BE(1, writer.off); - writer.off = writer.buf.writeUInt32BE(pubkeyLength, writer.off); - - writeOpenSSHKeyString(writer, Buffer.from("ssh-rsa")); - writeOpenSSHKeyString(writer, ebuf); - writeOpenSSHKeyString(writer, nbuf); - - writer.off = writer.buf.writeUInt32BE( - length - 47 - pubkeyLength, - writer.off - ); - writer.off += 8; - - writeOpenSSHKeyString(writer, Buffer.from("ssh-rsa")); - writeOpenSSHKeyString(writer, nbuf); - writeOpenSSHKeyString(writer, ebuf); - writeOpenSSHKeyString(writer, dbuf); - writeOpenSSHKeyString(writer, coeffbuf); - writeOpenSSHKeyString(writer, pbuf); - writeOpenSSHKeyString(writer, qbuf); - writeOpenSSHKeyString(writer, commentbuf); - - let pad = 0x01; - while (writer.off < length) { - writer.off = writer.buf.writeUInt8(pad++, writer.off); - } - - if (options.type === "der") { - return writer.buf - } else { - return PRIVATE_OPENING_BOUNDARY + "\n" + utils.linebrk(buf.toString("base64"), 70) + "\n" + PRIVATE_CLOSING_BOUNDARY + "\n"; - } - }, - - privateImport: function (key, data, options) { - options = options || {}; - var buffer; - - if (options.type !== "der") { - if (Buffer.isBuffer(data)) { - data = data.toString("utf8"); - } - - if (_.isString(data)) { - var pem = utils.trimSurroundingText(data, PRIVATE_OPENING_BOUNDARY, PRIVATE_CLOSING_BOUNDARY) - .replace(/\s+|\n\r|\n|\r$/gm, ""); - buffer = Buffer.from(pem, "base64"); - } else { - throw Error("Unsupported key format"); - } - } else if (Buffer.isBuffer(data)) { - buffer = data; - } else { - throw Error("Unsupported key format"); - } - - const reader = {buf: buffer, off: 0}; - - if (buffer.slice(0, 14).toString("ascii") !== "openssh-key-v1") - throw "Invalid file format."; - - reader.off += 15; - - //ciphername - if (readOpenSSHKeyString(reader).toString("ascii") !== "none") - throw Error("Unsupported key type"); - //kdfname - if (readOpenSSHKeyString(reader).toString("ascii") !== "none") - throw Error("Unsupported key type"); - //kdf - if (readOpenSSHKeyString(reader).toString("ascii") !== "") - throw Error("Unsupported key type"); - //keynum - reader.off += 4; - - //sshpublength - reader.off += 4; - - //keytype - if (readOpenSSHKeyString(reader).toString("ascii") !== "ssh-rsa") - throw Error("Unsupported key type"); - readOpenSSHKeyString(reader); - readOpenSSHKeyString(reader); - - reader.off += 12; - if (readOpenSSHKeyString(reader).toString("ascii") !== "ssh-rsa") - throw Error("Unsupported key type"); - - const n = readOpenSSHKeyString(reader); - const e = readOpenSSHKeyString(reader); - const d = readOpenSSHKeyString(reader); - const coeff = readOpenSSHKeyString(reader); - const p = readOpenSSHKeyString(reader); - const q = readOpenSSHKeyString(reader); - - //Calculate missing values - const dint = new BigInteger(d); - const qint = new BigInteger(q); - const pint = new BigInteger(p); - const dp = dint.mod(pint.subtract(BigInteger.ONE)); - const dq = dint.mod(qint.subtract(BigInteger.ONE)); - - key.setPrivate( - n, // modulus - e, // publicExponent - d, // privateExponent - p, // prime1 - q, // prime2 - dp.toBuffer(), // exponent1 -- d mod (p1) - dq.toBuffer(), // exponent2 -- d mod (q-1) - coeff // coefficient -- (inverse of q) mod p - ); - - key.sshcomment = readOpenSSHKeyString(reader).toString("ascii"); - }, - - publicExport: function (key, options) { - let ebuf = Buffer.alloc(4) - ebuf.writeUInt32BE(key.e, 0); - //Slice leading zeroes - while (ebuf[0] === 0) ebuf = ebuf.slice(1); - const nbuf = key.n.toBuffer(); - const buf = Buffer.alloc( - ebuf.byteLength + 4 + - nbuf.byteLength + 4 + - "ssh-rsa".length + 4 - ); - - const writer = {buf: buf, off: 0}; - writeOpenSSHKeyString(writer, Buffer.from("ssh-rsa")); - writeOpenSSHKeyString(writer, ebuf); - writeOpenSSHKeyString(writer, nbuf); - - let comment = key.sshcomment || ""; - - if (options.type === "der") { - return writer.buf - } else { - return "ssh-rsa " + buf.toString("base64") + " " + comment + "\n"; - } - }, - - publicImport: function (key, data, options) { - options = options || {}; - var buffer; - - if (options.type !== "der") { - if (Buffer.isBuffer(data)) { - data = data.toString("utf8"); - } - - if (_.isString(data)) { - if (data.substring(0, 8) !== "ssh-rsa ") - throw Error("Unsupported key format"); - let pemEnd = data.indexOf(" ", 8); - - //Handle keys with no comment - if (pemEnd === -1) { - pemEnd = data.length; - } else { - key.sshcomment = data.substring(pemEnd + 1) - .replace(/\s+|\n\r|\n|\r$/gm, ""); - } - - const pem = data.substring(8, pemEnd) - .replace(/\s+|\n\r|\n|\r$/gm, ""); - buffer = Buffer.from(pem, "base64"); - } else { - throw Error("Unsupported key format"); - } - } else if (Buffer.isBuffer(data)) { - buffer = data; - } else { - throw Error("Unsupported key format"); - } - - const reader = {buf: buffer, off: 0}; - - const type = readOpenSSHKeyString(reader).toString("ascii"); - - if (type !== "ssh-rsa") - throw Error("Invalid key type: " + type); - - const e = readOpenSSHKeyString(reader); - const n = readOpenSSHKeyString(reader); - - key.setPublic( - n, - e - ); - }, - - /** - * Trying autodetect and import key - * @param key - * @param data - */ - autoImport: function (key, data) { - // [\S\s]* matches zero or more of any character - if (/^[\S\s]*-----BEGIN OPENSSH PRIVATE KEY-----\s*(?=(([A-Za-z0-9+/=]+\s*)+))\1-----END OPENSSH PRIVATE KEY-----[\S\s]*$/g.test(data)) { - module.exports.privateImport(key, data); - return true; - } - - if (/^[\S\s]*ssh-rsa \s*(?=(([A-Za-z0-9+/=]+\s*)+))\1[\S\s]*$/g.test(data)) { - module.exports.publicImport(key, data); - return true; - } - - return false; - } -}; - -function readOpenSSHKeyString(reader) { - const len = reader.buf.readInt32BE(reader.off); - reader.off += 4; - const res = reader.buf.slice(reader.off, reader.off + len); - reader.off += len; - return res; -} - -function writeOpenSSHKeyString(writer, data) { - writer.buf.writeInt32BE(data.byteLength, writer.off); - writer.off += 4; - writer.off += data.copy(writer.buf, writer.off); -} \ No newline at end of file diff --git a/src/formats/openssh.ts b/src/formats/openssh.ts new file mode 100644 index 0000000..f982695 --- /dev/null +++ b/src/formats/openssh.ts @@ -0,0 +1,294 @@ +import { BigInteger } from '../bigint/big-integer.js'; +import { + fromBase64, + fromUtf8, + readUInt32BE, + toBase64, + toUtf8, + writeUInt32BE, +} from '../crypto/bytes.js'; +import type { RSAKey } from '../rsa/key.js'; +import { linebrk, trimSurroundingText } from '../utils/text-utils.js'; +import type { ExportOptions, FormatProvider, ImportOptions } from './types.js'; + +const PRIVATE_OPENING = '-----BEGIN OPENSSH PRIVATE KEY-----'; +const PRIVATE_CLOSING = '-----END OPENSSH PRIVATE KEY-----'; + +/** + * OpenSSH — `ssh-rsa AAAA…` single-line public format, and + * `OPENSSH PRIVATE KEY` PEM (openssh-key-v1). Only unencrypted keys + * (cipher="none", kdf="none"); encrypted private keys are not supported. + * The trailing comment field is preserved on import as `key.sshcomment`. + */ +export const opensshFormat: FormatProvider = { + /** OpenSSH private-key export. The two checkint placeholders are left as zero (no integrity field is written). */ + privateExport(key: RSAKey, options: ExportOptions = {}): Uint8Array | string { + if (!key.n || !key.d || !key.p || !key.q || !key.coeff) { + throw new Error('OpenSSH export: incomplete private key'); + } + + const nbuf = key.n.toBuffer() as Uint8Array; + let ebuf = new Uint8Array(4); + writeUInt32BE(key.e, ebuf, 0); + // Strip leading zero bytes + while (ebuf.length > 0 && ebuf[0] === 0) ebuf = ebuf.subarray(1); + + const dbuf = key.d.toBuffer() as Uint8Array; + const coeffbuf = key.coeff.toBuffer() as Uint8Array; + const pbuf = key.p.toBuffer() as Uint8Array; + const qbuf = key.q.toBuffer() as Uint8Array; + const commentbuf = key.sshcomment ? fromUtf8(key.sshcomment) : new Uint8Array(0); + + const pubkeyLength = + 11 + // length-prefixed 'ssh-rsa' (4-byte uint32 length + 7 chars) + 4 + + ebuf.byteLength + // 4 = length prefix for e + 4 + + nbuf.byteLength; // 4 = length prefix for n + + const privateKeyLength = + 8 + // two uint32 checkints (file-corruption / wrong-passphrase detector) + 11 + // length-prefixed 'ssh-rsa' (4 + 7) + 4 + + nbuf.byteLength + // 4 = length prefix for n + 4 + + ebuf.byteLength + // 4 = length prefix for e + 4 + + dbuf.byteLength + // 4 = length prefix for d + 4 + + coeffbuf.byteLength + // 4 = length prefix for iqmp (coeff) + 4 + + pbuf.byteLength + // 4 = length prefix for p + 4 + + qbuf.byteLength + // 4 = length prefix for q + 4 + + commentbuf.byteLength; // 4 = length prefix for comment + const paddingLength = Math.ceil(privateKeyLength / 8) * 8 - privateKeyLength; + + const totalLength = + 15 + // 'openssh-key-v1\0' magic + 16 + // two length-prefixed 'none' strings (cipher + kdf), 2*(4+4) + 4 + // empty kdfoptions (length prefix only, zero bytes of payload) + 4 + // numkeys (uint32 = 1) + 4 + // pubkey-blob length prefix + pubkeyLength + + 4 + // private-section length prefix + privateKeyLength + + paddingLength; + + const buf = new Uint8Array(totalLength); + const writer = new SshWriter(buf); + + // "openssh-key-v1\0" + buf.set(fromUtf8('openssh-key-v1'), 0); + buf[14] = 0; + writer.off = 15; + + writer.writeString(fromUtf8('none')); + writer.writeString(fromUtf8('none')); + writer.writeString(new Uint8Array(0)); + + writer.writeUInt32(1); // number of keys + writer.writeUInt32(pubkeyLength); + + writer.writeString(fromUtf8('ssh-rsa')); + writer.writeString(ebuf); + writer.writeString(nbuf); + + writer.writeUInt32(privateKeyLength + paddingLength); // length prefix for the private section + writer.off += 8; // skip two uint32 checkints (left as zero — no integrity field is written) + + writer.writeString(fromUtf8('ssh-rsa')); + writer.writeString(nbuf); + writer.writeString(ebuf); + writer.writeString(dbuf); + writer.writeString(coeffbuf); + writer.writeString(pbuf); + writer.writeString(qbuf); + writer.writeString(commentbuf); + + let pad = 0x01; + while (writer.off < totalLength) { + buf[writer.off++] = pad++; + } + + if (options.type === 'der') return buf; + return `${PRIVATE_OPENING}\n${linebrk(toBase64(buf), 70)}\n${PRIVATE_CLOSING}\n`; + }, + + /** OpenSSH private-key import. The format omits CRT exponents, so `dp` and `dq` are derived from `d mod (p−1)` and `d mod (q−1)`. */ + privateImport(key: RSAKey, data: Uint8Array | string, options: ImportOptions = {}): void { + let buffer: Uint8Array; + if (options.type !== 'der') { + const text = data instanceof Uint8Array ? toUtf8(data) : (data as string); + const trimmed = trimSurroundingText(text, PRIVATE_OPENING, PRIVATE_CLOSING).replace( + /\s+/g, + '', + ); + buffer = fromBase64(trimmed); + } else if (data instanceof Uint8Array) { + buffer = data; + } else { + throw new Error('Unsupported key format'); + } + + const magic = toUtf8(buffer.subarray(0, 14)); + if (magic !== 'openssh-key-v1') throw new Error('Invalid file format.'); + + const reader = new SshReader(buffer); + reader.off = 15; + + if (toUtf8(reader.readString()) !== 'none') throw new Error('Unsupported key type'); + if (toUtf8(reader.readString()) !== 'none') throw new Error('Unsupported key type'); + if (toUtf8(reader.readString()) !== '') throw new Error('Unsupported key type'); + + reader.off += 4; // keynum + reader.off += 4; // sshpublength + + if (toUtf8(reader.readString()) !== 'ssh-rsa') throw new Error('Unsupported key type'); + reader.readString(); // public e + reader.readString(); // public n + + // Private section: `length || checkint1 || checkint2 || keydata` — + // the two checkints MUST be identical (file corruption / wrong + // passphrase detector per the OpenSSH key format). + reader.off += 4; // private section length (subsequent reads bounds-check) + const checkInt1 = readUInt32BE(reader.buf, reader.off); + reader.off += 4; + const checkInt2 = readUInt32BE(reader.buf, reader.off); + reader.off += 4; + if (checkInt1 !== checkInt2) { + throw new Error( + 'OpenSSH private key: checksum mismatch (file may be corrupted or encrypted)', + ); + } + if (toUtf8(reader.readString()) !== 'ssh-rsa') throw new Error('Unsupported key type'); + + const n = reader.readString(); + const e = reader.readString(); + const d = reader.readString(); + const coeff = reader.readString(); + const p = reader.readString(); + const q = reader.readString(); + + // Derive dp = d mod (p-1) and dq = d mod (q-1) + const dint = new BigInteger(d); + const pint = new BigInteger(p); + const qint = new BigInteger(q); + const dp = dint.mod(pint.subtract(BigInteger.ONE)).toBuffer() as Uint8Array; + const dq = dint.mod(qint.subtract(BigInteger.ONE)).toBuffer() as Uint8Array; + + key.setPrivate(n, e, d, p, q, dp, dq, coeff); + key.sshcomment = toUtf8(reader.readString()); + }, + + publicExport(key: RSAKey, options: ExportOptions = {}): Uint8Array | string { + if (!key.n) throw new Error('OpenSSH export: missing modulus'); + let ebuf = new Uint8Array(4); + writeUInt32BE(key.e, ebuf, 0); + while (ebuf.length > 0 && ebuf[0] === 0) ebuf = ebuf.subarray(1); + const nbuf = key.n.toBuffer() as Uint8Array; + + const buf = new Uint8Array(ebuf.byteLength + 4 + nbuf.byteLength + 4 + 'ssh-rsa'.length + 4); + const writer = new SshWriter(buf); + writer.writeString(fromUtf8('ssh-rsa')); + writer.writeString(ebuf); + writer.writeString(nbuf); + + if (options.type === 'der') return buf; + const comment = key.sshcomment ?? ''; + return `ssh-rsa ${toBase64(buf)} ${comment}\n`; + }, + + publicImport(key: RSAKey, data: Uint8Array | string, options: ImportOptions = {}): void { + let buffer: Uint8Array; + if (options.type !== 'der') { + const text = data instanceof Uint8Array ? toUtf8(data) : (data as string); + if (text.substring(0, 8) !== 'ssh-rsa ') throw new Error('Unsupported key format'); + let pemEnd = text.indexOf(' ', 8); + if (pemEnd === -1) { + pemEnd = text.length; + } else { + // Legacy strips ALL whitespace (not just trailing) — multi-word + // comments are lossy on round-trip. Preserving that for 1-to-1. + key.sshcomment = text.substring(pemEnd + 1).replace(/\s+|\n\r|\n|\r$/gm, ''); + } + const pem = text.substring(8, pemEnd).replace(/\s+/g, ''); + buffer = fromBase64(pem); + } else if (data instanceof Uint8Array) { + buffer = data; + } else { + throw new Error('Unsupported key format'); + } + + const reader = new SshReader(buffer); + const type = toUtf8(reader.readString()); + if (type !== 'ssh-rsa') throw new Error(`Invalid key type: ${type}`); + const e = reader.readString(); + const n = reader.readString(); + key.setPublic(n, e); + }, + + autoImport(key: RSAKey, data: unknown): boolean { + const text = + typeof data === 'string' + ? data + : data instanceof Uint8Array + ? new TextDecoder().decode(data) + : null; + if (text === null) return false; + if ( + /^[\S\s]*-----BEGIN OPENSSH PRIVATE KEY-----\s*(?=(([A-Za-z0-9+/=]+\s*)+))\1-----END OPENSSH PRIVATE KEY-----[\S\s]*$/g.test( + text, + ) + ) { + opensshFormat.privateImport?.(key, text); + return true; + } + if (/^[\S\s]*ssh-rsa \s*(?=(([A-Za-z0-9+/=]+\s*)+))\1[\S\s]*$/g.test(text)) { + opensshFormat.publicImport?.(key, text); + return true; + } + return false; + }, +}; + +class SshReader { + off = 0; + + constructor(readonly buf: Uint8Array) {} + + readString(): Uint8Array { + const len = readUInt32BE(this.buf, this.off); + // Uint8Array.subarray silently truncates on OOB rather than throwing, + // so a forged length field would deliver a short buffer deep into the + // key parser with an opaque failure mode. Bound-check explicitly. + if (this.off + 4 + len > this.buf.length) { + throw new Error( + `OpenSSH: string length ${len} exceeds buffer (offset=${this.off}, buffer=${this.buf.length})`, + ); + } + this.off += 4; + const out = this.buf.subarray(this.off, this.off + len); + this.off += len; + return out; + } +} + +class SshWriter { + off = 0; + + constructor(readonly buf: Uint8Array) {} + + writeString(data: Uint8Array): void { + writeUInt32BE(data.byteLength, this.buf, this.off); + this.off += 4; + this.buf.set(data, this.off); + this.off += data.byteLength; + } + + writeUInt32(value: number): void { + writeUInt32BE(value, this.buf, this.off); + this.off += 4; + } +} diff --git a/src/formats/pem.ts b/src/formats/pem.ts new file mode 100644 index 0000000..cd9e46c --- /dev/null +++ b/src/formats/pem.ts @@ -0,0 +1,46 @@ +import { fromBase64, toBase64 } from '../crypto/bytes.js'; +import { linebrk, trimSurroundingText } from '../utils/text-utils.js'; +import type { ImportOptions } from './types.js'; + +/** Wrap raw DER bytes in a PEM container (base64 body line-wrapped at `lineLength`, default 64). */ +export function encodePem( + body: Uint8Array, + opening: string, + closing: string, + lineLength = 64, +): string { + return `${opening}\n${linebrk(toBase64(body), lineLength)}\n${closing}`; +} + +/** + * Decode a PEM-wrapped block into raw bytes. Tolerates leading and + * trailing noise around the boundaries (matches v1 behaviour). + */ +export function decodePem(text: string, opening: string, closing: string): Uint8Array { + const trimmed = trimSurroundingText(text, opening, closing).replace(/\s+/g, ''); + return fromBase64(trimmed); +} + +/** + * Normalize import input to raw bytes. `options.type === 'der'` requires + * a `Uint8Array`; otherwise the input is treated as PEM text (string or + * UTF-8-decoded bytes) and routed through {@link decodePem}. + */ +export function resolveBytes( + data: Uint8Array | string, + options: ImportOptions, + opening: string, + closing: string, +): Uint8Array { + if (options.type === 'der') { + if (data instanceof Uint8Array) return data; + throw new Error('Unsupported key format'); + } + if (data instanceof Uint8Array) { + return decodePem(new TextDecoder().decode(data), opening, closing); + } + if (typeof data === 'string') { + return decodePem(data, opening, closing); + } + throw new Error('Unsupported key format'); +} diff --git a/src/formats/pkcs1.js b/src/formats/pkcs1.js deleted file mode 100644 index 5fba246..0000000 --- a/src/formats/pkcs1.js +++ /dev/null @@ -1,148 +0,0 @@ -var ber = require('asn1').Ber; -var _ = require('../utils')._; -var utils = require('../utils'); - -const PRIVATE_OPENING_BOUNDARY = '-----BEGIN RSA PRIVATE KEY-----'; -const PRIVATE_CLOSING_BOUNDARY = '-----END RSA PRIVATE KEY-----'; - -const PUBLIC_OPENING_BOUNDARY = '-----BEGIN RSA PUBLIC KEY-----'; -const PUBLIC_CLOSING_BOUNDARY = '-----END RSA PUBLIC KEY-----'; - -module.exports = { - privateExport: function (key, options) { - options = options || {}; - - var n = key.n.toBuffer(); - var d = key.d.toBuffer(); - var p = key.p.toBuffer(); - var q = key.q.toBuffer(); - var dmp1 = key.dmp1.toBuffer(); - var dmq1 = key.dmq1.toBuffer(); - var coeff = key.coeff.toBuffer(); - - var length = n.length + d.length + p.length + q.length + dmp1.length + dmq1.length + coeff.length + 512; // magic - var writer = new ber.Writer({size: length}); - - writer.startSequence(); - writer.writeInt(0); - writer.writeBuffer(n, 2); - writer.writeInt(key.e); - writer.writeBuffer(d, 2); - writer.writeBuffer(p, 2); - writer.writeBuffer(q, 2); - writer.writeBuffer(dmp1, 2); - writer.writeBuffer(dmq1, 2); - writer.writeBuffer(coeff, 2); - writer.endSequence(); - - if (options.type === 'der') { - return writer.buffer; - } else { - return PRIVATE_OPENING_BOUNDARY + '\n' + utils.linebrk(writer.buffer.toString('base64'), 64) + '\n' + PRIVATE_CLOSING_BOUNDARY; - } - }, - - privateImport: function (key, data, options) { - options = options || {}; - var buffer; - - if (options.type !== 'der') { - if (Buffer.isBuffer(data)) { - data = data.toString('utf8'); - } - - if (_.isString(data)) { - var pem = utils.trimSurroundingText(data, PRIVATE_OPENING_BOUNDARY, PRIVATE_CLOSING_BOUNDARY) - .replace(/\s+|\n\r|\n|\r$/gm, ''); - buffer = Buffer.from(pem, 'base64'); - } else { - throw Error('Unsupported key format'); - } - } else if (Buffer.isBuffer(data)) { - buffer = data; - } else { - throw Error('Unsupported key format'); - } - - var reader = new ber.Reader(buffer); - reader.readSequence(); - reader.readString(2, true); // just zero - key.setPrivate( - reader.readString(2, true), // modulus - reader.readString(2, true), // publicExponent - reader.readString(2, true), // privateExponent - reader.readString(2, true), // prime1 - reader.readString(2, true), // prime2 - reader.readString(2, true), // exponent1 -- d mod (p1) - reader.readString(2, true), // exponent2 -- d mod (q-1) - reader.readString(2, true) // coefficient -- (inverse of q) mod p - ); - }, - - publicExport: function (key, options) { - options = options || {}; - - var n = key.n.toBuffer(); - var length = n.length + 512; // magic - - var bodyWriter = new ber.Writer({size: length}); - bodyWriter.startSequence(); - bodyWriter.writeBuffer(n, 2); - bodyWriter.writeInt(key.e); - bodyWriter.endSequence(); - - if (options.type === 'der') { - return bodyWriter.buffer; - } else { - return PUBLIC_OPENING_BOUNDARY + '\n' + utils.linebrk(bodyWriter.buffer.toString('base64'), 64) + '\n' + PUBLIC_CLOSING_BOUNDARY; - } - }, - - publicImport: function (key, data, options) { - options = options || {}; - var buffer; - - if (options.type !== 'der') { - if (Buffer.isBuffer(data)) { - data = data.toString('utf8'); - } - - if (_.isString(data)) { - var pem = utils.trimSurroundingText(data, PUBLIC_OPENING_BOUNDARY, PUBLIC_CLOSING_BOUNDARY) - .replace(/\s+|\n\r|\n|\r$/gm, ''); - buffer = Buffer.from(pem, 'base64'); - } - } else if (Buffer.isBuffer(data)) { - buffer = data; - } else { - throw Error('Unsupported key format'); - } - - var body = new ber.Reader(buffer); - body.readSequence(); - key.setPublic( - body.readString(0x02, true), // modulus - body.readString(0x02, true) // publicExponent - ); - }, - - /** - * Trying autodetect and import key - * @param key - * @param data - */ - autoImport: function (key, data) { - // [\S\s]* matches zero or more of any character - if (/^[\S\s]*-----BEGIN RSA PRIVATE KEY-----\s*(?=(([A-Za-z0-9+/=]+\s*)+))\1-----END RSA PRIVATE KEY-----[\S\s]*$/g.test(data)) { - module.exports.privateImport(key, data); - return true; - } - - if (/^[\S\s]*-----BEGIN RSA PUBLIC KEY-----\s*(?=(([A-Za-z0-9+/=]+\s*)+))\1-----END RSA PUBLIC KEY-----[\S\s]*$/g.test(data)) { - module.exports.publicImport(key, data); - return true; - } - - return false; - } -}; \ No newline at end of file diff --git a/src/formats/pkcs1.ts b/src/formats/pkcs1.ts new file mode 100644 index 0000000..93ee0fe --- /dev/null +++ b/src/formats/pkcs1.ts @@ -0,0 +1,93 @@ +import { DerReader, DerWriter } from '../asn1/index.js'; +import type { RSAKey } from '../rsa/key.js'; +import { encodePem, resolveBytes } from './pem.js'; +import type { ExportOptions, FormatProvider, ImportOptions } from './types.js'; + +const PRIVATE_OPENING = '-----BEGIN RSA PRIVATE KEY-----'; +const PRIVATE_CLOSING = '-----END RSA PRIVATE KEY-----'; +const PUBLIC_OPENING = '-----BEGIN RSA PUBLIC KEY-----'; +const PUBLIC_CLOSING = '-----END RSA PUBLIC KEY-----'; + +/** PKCS#1 (RFC 8017 §A.1) — `RSA PRIVATE KEY` / `RSA PUBLIC KEY` PEM, or raw DER. */ +export const pkcs1Format: FormatProvider = { + privateExport(key: RSAKey, options: ExportOptions = {}): Uint8Array | string { + if (!key.n || !key.d || !key.p || !key.q || !key.dmp1 || !key.dmq1 || !key.coeff) { + throw new Error('PKCS#1 export: incomplete private key'); + } + const w = new DerWriter(); + w.startSequence(); + w.writeInteger(0); + w.writeInteger(key.n.toBuffer() as Uint8Array); + w.writeInteger(key.e); + w.writeInteger(key.d.toBuffer() as Uint8Array); + w.writeInteger(key.p.toBuffer() as Uint8Array); + w.writeInteger(key.q.toBuffer() as Uint8Array); + w.writeInteger(key.dmp1.toBuffer() as Uint8Array); + w.writeInteger(key.dmq1.toBuffer() as Uint8Array); + w.writeInteger(key.coeff.toBuffer() as Uint8Array); + w.endSequence(); + const bytes = w.toBytes(); + return options.type === 'der' ? bytes : encodePem(bytes, PRIVATE_OPENING, PRIVATE_CLOSING); + }, + + privateImport(key: RSAKey, data: Uint8Array | string, options: ImportOptions = {}): void { + const buffer = resolveBytes(data, options, PRIVATE_OPENING, PRIVATE_CLOSING); + const seq = new DerReader(buffer).readSequence(); + seq.readSmallInteger(); // version + const n = seq.readInteger(); + const e = seq.readSmallInteger(); + const d = seq.readInteger(); + const p = seq.readInteger(); + const q = seq.readInteger(); + const dmp1 = seq.readInteger(); + const dmq1 = seq.readInteger(); + const coeff = seq.readInteger(); + key.setPrivate(n, e, d, p, q, dmp1, dmq1, coeff); + }, + + publicExport(key: RSAKey, options: ExportOptions = {}): Uint8Array | string { + if (!key.n) throw new Error('PKCS#1 export: missing modulus'); + const w = new DerWriter(); + w.startSequence(); + w.writeInteger(key.n.toBuffer() as Uint8Array); + w.writeInteger(key.e); + w.endSequence(); + const bytes = w.toBytes(); + return options.type === 'der' ? bytes : encodePem(bytes, PUBLIC_OPENING, PUBLIC_CLOSING); + }, + + publicImport(key: RSAKey, data: Uint8Array | string, options: ImportOptions = {}): void { + const buffer = resolveBytes(data, options, PUBLIC_OPENING, PUBLIC_CLOSING); + const seq = new DerReader(buffer).readSequence(); + const n = seq.readInteger(); + const e = seq.readSmallInteger(); + key.setPublic(n, e); + }, + + autoImport(key: RSAKey, data: unknown): boolean { + const text = + typeof data === 'string' + ? data + : data instanceof Uint8Array + ? new TextDecoder().decode(data) + : null; + if (text === null) return false; + if ( + /^[\S\s]*-----BEGIN RSA PRIVATE KEY-----\s*(?=(([A-Za-z0-9+/=]+\s*)+))\1-----END RSA PRIVATE KEY-----[\S\s]*$/g.test( + text, + ) + ) { + pkcs1Format.privateImport?.(key, text); + return true; + } + if ( + /^[\S\s]*-----BEGIN RSA PUBLIC KEY-----\s*(?=(([A-Za-z0-9+/=]+\s*)+))\1-----END RSA PUBLIC KEY-----[\S\s]*$/g.test( + text, + ) + ) { + pkcs1Format.publicImport?.(key, text); + return true; + } + return false; + }, +}; diff --git a/src/formats/pkcs8.js b/src/formats/pkcs8.js deleted file mode 100644 index 3dd1a3c..0000000 --- a/src/formats/pkcs8.js +++ /dev/null @@ -1,187 +0,0 @@ -var ber = require('asn1').Ber; -var _ = require('../utils')._; -var PUBLIC_RSA_OID = '1.2.840.113549.1.1.1'; -var utils = require('../utils'); - -const PRIVATE_OPENING_BOUNDARY = '-----BEGIN PRIVATE KEY-----'; -const PRIVATE_CLOSING_BOUNDARY = '-----END PRIVATE KEY-----'; - -const PUBLIC_OPENING_BOUNDARY = '-----BEGIN PUBLIC KEY-----'; -const PUBLIC_CLOSING_BOUNDARY = '-----END PUBLIC KEY-----'; - -module.exports = { - privateExport: function (key, options) { - options = options || {}; - - var n = key.n.toBuffer(); - var d = key.d.toBuffer(); - var p = key.p.toBuffer(); - var q = key.q.toBuffer(); - var dmp1 = key.dmp1.toBuffer(); - var dmq1 = key.dmq1.toBuffer(); - var coeff = key.coeff.toBuffer(); - - var length = n.length + d.length + p.length + q.length + dmp1.length + dmq1.length + coeff.length + 512; // magic - var bodyWriter = new ber.Writer({size: length}); - - bodyWriter.startSequence(); - bodyWriter.writeInt(0); - bodyWriter.writeBuffer(n, 2); - bodyWriter.writeInt(key.e); - bodyWriter.writeBuffer(d, 2); - bodyWriter.writeBuffer(p, 2); - bodyWriter.writeBuffer(q, 2); - bodyWriter.writeBuffer(dmp1, 2); - bodyWriter.writeBuffer(dmq1, 2); - bodyWriter.writeBuffer(coeff, 2); - bodyWriter.endSequence(); - - var writer = new ber.Writer({size: length}); - writer.startSequence(); - writer.writeInt(0); - writer.startSequence(); - writer.writeOID(PUBLIC_RSA_OID); - writer.writeNull(); - writer.endSequence(); - writer.writeBuffer(bodyWriter.buffer, 4); - writer.endSequence(); - - if (options.type === 'der') { - return writer.buffer; - } else { - return PRIVATE_OPENING_BOUNDARY + '\n' + utils.linebrk(writer.buffer.toString('base64'), 64) + '\n' + PRIVATE_CLOSING_BOUNDARY; - } - }, - - privateImport: function (key, data, options) { - options = options || {}; - var buffer; - - if (options.type !== 'der') { - if (Buffer.isBuffer(data)) { - data = data.toString('utf8'); - } - - if (_.isString(data)) { - var pem = utils.trimSurroundingText(data, PRIVATE_OPENING_BOUNDARY, PRIVATE_CLOSING_BOUNDARY) - .replace('-----END PRIVATE KEY-----', '') - .replace(/\s+|\n\r|\n|\r$/gm, ''); - buffer = Buffer.from(pem, 'base64'); - } else { - throw Error('Unsupported key format'); - } - } else if (Buffer.isBuffer(data)) { - buffer = data; - } else { - throw Error('Unsupported key format'); - } - - var reader = new ber.Reader(buffer); - reader.readSequence(); - reader.readInt(0); - var header = new ber.Reader(reader.readString(0x30, true)); - - if (header.readOID(0x06, true) !== PUBLIC_RSA_OID) { - throw Error('Invalid Public key format'); - } - - var body = new ber.Reader(reader.readString(0x04, true)); - body.readSequence(); - body.readString(2, true); // just zero - key.setPrivate( - body.readString(2, true), // modulus - body.readString(2, true), // publicExponent - body.readString(2, true), // privateExponent - body.readString(2, true), // prime1 - body.readString(2, true), // prime2 - body.readString(2, true), // exponent1 -- d mod (p1) - body.readString(2, true), // exponent2 -- d mod (q-1) - body.readString(2, true) // coefficient -- (inverse of q) mod p - ); - }, - - publicExport: function (key, options) { - options = options || {}; - - var n = key.n.toBuffer(); - var length = n.length + 512; // magic - - var bodyWriter = new ber.Writer({size: length}); - bodyWriter.writeByte(0); - bodyWriter.startSequence(); - bodyWriter.writeBuffer(n, 2); - bodyWriter.writeInt(key.e); - bodyWriter.endSequence(); - - var writer = new ber.Writer({size: length}); - writer.startSequence(); - writer.startSequence(); - writer.writeOID(PUBLIC_RSA_OID); - writer.writeNull(); - writer.endSequence(); - writer.writeBuffer(bodyWriter.buffer, 3); - writer.endSequence(); - - if (options.type === 'der') { - return writer.buffer; - } else { - return PUBLIC_OPENING_BOUNDARY + '\n' + utils.linebrk(writer.buffer.toString('base64'), 64) + '\n' + PUBLIC_CLOSING_BOUNDARY; - } - }, - - publicImport: function (key, data, options) { - options = options || {}; - var buffer; - - if (options.type !== 'der') { - if (Buffer.isBuffer(data)) { - data = data.toString('utf8'); - } - - if (_.isString(data)) { - var pem = utils.trimSurroundingText(data, PUBLIC_OPENING_BOUNDARY, PUBLIC_CLOSING_BOUNDARY) - .replace(/\s+|\n\r|\n|\r$/gm, ''); - buffer = Buffer.from(pem, 'base64'); - } - } else if (Buffer.isBuffer(data)) { - buffer = data; - } else { - throw Error('Unsupported key format'); - } - - var reader = new ber.Reader(buffer); - reader.readSequence(); - var header = new ber.Reader(reader.readString(0x30, true)); - - if (header.readOID(0x06, true) !== PUBLIC_RSA_OID) { - throw Error('Invalid Public key format'); - } - - var body = new ber.Reader(reader.readString(0x03, true)); - body.readByte(); - body.readSequence(); - key.setPublic( - body.readString(0x02, true), // modulus - body.readString(0x02, true) // publicExponent - ); - }, - - /** - * Trying autodetect and import key - * @param key - * @param data - */ - autoImport: function (key, data) { - if (/^[\S\s]*-----BEGIN PRIVATE KEY-----\s*(?=(([A-Za-z0-9+/=]+\s*)+))\1-----END PRIVATE KEY-----[\S\s]*$/g.test(data)) { - module.exports.privateImport(key, data); - return true; - } - - if (/^[\S\s]*-----BEGIN PUBLIC KEY-----\s*(?=(([A-Za-z0-9+/=]+\s*)+))\1-----END PUBLIC KEY-----[\S\s]*$/g.test(data)) { - module.exports.publicImport(key, data); - return true; - } - - return false; - } -}; diff --git a/src/formats/pkcs8.ts b/src/formats/pkcs8.ts new file mode 100644 index 0000000..b16c3fb --- /dev/null +++ b/src/formats/pkcs8.ts @@ -0,0 +1,167 @@ +import { DerReader, DerWriter, OID } from '../asn1/index.js'; +import type { RSAKey } from '../rsa/key.js'; +import { encodePem, resolveBytes } from './pem.js'; +import type { ExportOptions, FormatProvider, ImportOptions } from './types.js'; + +const PRIVATE_OPENING = '-----BEGIN PRIVATE KEY-----'; +const PRIVATE_CLOSING = '-----END PRIVATE KEY-----'; +const PUBLIC_OPENING = '-----BEGIN PUBLIC KEY-----'; +const PUBLIC_CLOSING = '-----END PUBLIC KEY-----'; + +/** + * PKCS#8 (RFC 5958) — `PRIVATE KEY` / `PUBLIC KEY` PEM, or raw DER. Wraps a + * PKCS#1 body inside an algorithm-id envelope; only `rsaEncryption` OID is + * accepted (RSASSA-PSS / RSAES-OAEP variants are rejected with a clear error). + */ +export const pkcs8Format: FormatProvider = { + privateExport(key: RSAKey, options: ExportOptions = {}): Uint8Array | string { + if (!key.n || !key.d || !key.p || !key.q || !key.dmp1 || !key.dmq1 || !key.coeff) { + throw new Error('PKCS#8 export: incomplete private key'); + } + // Inner: PKCS#1 private key body + const body = new DerWriter(); + body.startSequence(); + body.writeInteger(0); + body.writeInteger(key.n.toBuffer() as Uint8Array); + body.writeInteger(key.e); + body.writeInteger(key.d.toBuffer() as Uint8Array); + body.writeInteger(key.p.toBuffer() as Uint8Array); + body.writeInteger(key.q.toBuffer() as Uint8Array); + body.writeInteger(key.dmp1.toBuffer() as Uint8Array); + body.writeInteger(key.dmq1.toBuffer() as Uint8Array); + body.writeInteger(key.coeff.toBuffer() as Uint8Array); + body.endSequence(); + + const w = new DerWriter(); + w.startSequence(); + w.writeInteger(0); // version + w.startSequence(); + w.writeOid(OID.RSA_ENCRYPTION); + w.writeNull(); + w.endSequence(); + w.writeOctetString(body.toBytes()); + w.endSequence(); + + const bytes = w.toBytes(); + return options.type === 'der' ? bytes : encodePem(bytes, PRIVATE_OPENING, PRIVATE_CLOSING); + }, + + privateImport(key: RSAKey, data: Uint8Array | string, options: ImportOptions = {}): void { + const buffer = resolveBytes(data, options, PRIVATE_OPENING, PRIVATE_CLOSING); + const outer = new DerReader(buffer).readSequence(); + // RFC 5958 §2: PrivateKeyInfo / OneAsymmetricKey version ∈ {0, 1}. + const outerVersion = outer.readSmallInteger(); + if (outerVersion !== 0 && outerVersion !== 1) { + throw new Error(`PKCS#8: unsupported version ${outerVersion} (RFC 5958 §2 requires 0 or 1)`); + } + const header = outer.readSequence(); + const oid = header.readOid(); + if (oid !== OID.RSA_ENCRYPTION) { + throw pkcs8OidError(oid, 'private'); + } + header.readNull(); + const body = new DerReader(outer.readOctetString()).readSequence(); + // RFC 8017 §A.1.2: 0 = two-prime, 1 = multi-prime. Two-prime only. + const innerVersion = body.readSmallInteger(); + if (innerVersion !== 0) { + throw new Error( + `PKCS#8: PKCS#1 multi-prime keys (version ${innerVersion}) are not supported`, + ); + } + const n = body.readInteger(); + const e = body.readSmallInteger(); + const d = body.readInteger(); + const p = body.readInteger(); + const q = body.readInteger(); + const dmp1 = body.readInteger(); + const dmq1 = body.readInteger(); + const coeff = body.readInteger(); + key.setPrivate(n, e, d, p, q, dmp1, dmq1, coeff); + }, + + publicExport(key: RSAKey, options: ExportOptions = {}): Uint8Array | string { + if (!key.n) throw new Error('PKCS#8 export: missing modulus'); + // Inner: SEQUENCE { n, e } + const inner = new DerWriter(); + inner.startSequence(); + inner.writeInteger(key.n.toBuffer() as Uint8Array); + inner.writeInteger(key.e); + inner.endSequence(); + + const w = new DerWriter(); + w.startSequence(); + w.startSequence(); + w.writeOid(OID.RSA_ENCRYPTION); + w.writeNull(); + w.endSequence(); + w.writeBitString(inner.toBytes()); + w.endSequence(); + + const bytes = w.toBytes(); + return options.type === 'der' ? bytes : encodePem(bytes, PUBLIC_OPENING, PUBLIC_CLOSING); + }, + + publicImport(key: RSAKey, data: Uint8Array | string, options: ImportOptions = {}): void { + const buffer = resolveBytes(data, options, PUBLIC_OPENING, PUBLIC_CLOSING); + const outer = new DerReader(buffer).readSequence(); + const header = outer.readSequence(); + const oid = header.readOid(); + if (oid !== OID.RSA_ENCRYPTION) { + throw pkcs8OidError(oid, 'public'); + } + header.readNull(); + const inner = new DerReader(outer.readBitString()).readSequence(); + const n = inner.readInteger(); + const e = inner.readSmallInteger(); + key.setPublic(n, e); + }, + + autoImport(key: RSAKey, data: unknown): boolean { + const text = + typeof data === 'string' + ? data + : data instanceof Uint8Array + ? new TextDecoder().decode(data) + : null; + if (text === null) return false; + if ( + /^[\S\s]*-----BEGIN PRIVATE KEY-----\s*(?=(([A-Za-z0-9+/=]+\s*)+))\1-----END PRIVATE KEY-----[\S\s]*$/g.test( + text, + ) + ) { + pkcs8Format.privateImport?.(key, text); + return true; + } + if ( + /^[\S\s]*-----BEGIN PUBLIC KEY-----\s*(?=(([A-Za-z0-9+/=]+\s*)+))\1-----END PUBLIC KEY-----[\S\s]*$/g.test( + text, + ) + ) { + pkcs8Format.publicImport?.(key, text); + return true; + } + return false; + }, +}; + +/** + * RFC 5958 §2 and RFC 8017 require rsaEncryption (1.2.840.113549.1.1.1) + * in the PKCS#8 privateKeyAlgorithm field — not PSS/OAEP-specific OIDs. + * Some implementations get this wrong; surface a clear diagnostic instead + * of a generic "invalid format" that tempts maintainers to relax the check. + */ +function pkcs8OidError(oid: string, kind: 'private' | 'public'): Error { + if (oid === '1.2.840.113549.1.1.10') { + return new Error( + `PKCS#8 ${kind} key: RSASSA-PSS-only keys (1.2.840.113549.1.1.10) are not supported; expected rsaEncryption`, + ); + } + if (oid === '1.2.840.113549.1.1.7') { + return new Error( + `PKCS#8 ${kind} key: RSAES-OAEP-only keys (1.2.840.113549.1.1.7) are not supported; expected rsaEncryption`, + ); + } + return new Error( + `PKCS#8 ${kind} key: unsupported algorithm OID ${oid}; expected rsaEncryption (1.2.840.113549.1.1.1)`, + ); +} diff --git a/src/formats/types.ts b/src/formats/types.ts new file mode 100644 index 0000000..39a338c --- /dev/null +++ b/src/formats/types.ts @@ -0,0 +1,27 @@ +import type { RSAKey } from '../rsa/key.js'; + +export interface ExportOptions { + /** "pem" (default) or "der". */ + type?: 'pem' | 'der' | 'default'; +} + +export interface ImportOptions { + type?: 'pem' | 'der' | 'default'; +} + +/** + * One key-encoding format (PKCS#1, PKCS#8, OpenSSH, components). + * `components` returns a plain object; the rest return PEM string or DER bytes. + */ +export interface FormatProvider { + /** Serialize the private half. Throws if the key lacks private components. */ + privateExport(key: RSAKey, options?: ExportOptions): Uint8Array | string | object; + /** Parse `data` into `key` as a private key. */ + privateImport(key: RSAKey, data: unknown, options?: ImportOptions): void; + /** Serialize the public half. */ + publicExport(key: RSAKey, options?: ExportOptions): Uint8Array | string | object; + /** Parse `data` into `key` as a public key. */ + publicImport(key: RSAKey, data: unknown, options?: ImportOptions): void; + /** Sniff `data` and route to private/public import if the format matches. Returns false if not recognised. */ + autoImport(key: RSAKey, data: unknown): boolean; +} diff --git a/src/index.browser.ts b/src/index.browser.ts new file mode 100644 index 0000000..0df607e --- /dev/null +++ b/src/index.browser.ts @@ -0,0 +1,23 @@ +import { setBigIntegerImpl } from './bigint/big-integer.js'; +import { webBackend } from './crypto/backend.web.js'; +import { bootstrap, NodeRSA } from './node-rsa.js'; + +// Modern browsers (Chrome 67+, Firefox 68+, Safari 14+, Edge 79+) support +// native BigInt. The selector falls back to jsbn silently if `BigInt` is +// missing, so this is safe everywhere. Callers can flip back via +// `setBigIntegerImpl('jsbn')` before constructing any NodeRSA instance. +setBigIntegerImpl('native'); + +bootstrap({ + environment: 'browser', + backend: webBackend, + // Browser bundle ships only the pure-JS engine — there is no node:crypto. +}); + +export { NodeRSA }; +export default NodeRSA; +// Browser bundle defaults to native BigInt (with silent jsbn fallback on +// runtimes without globalThis.BigInt). Users who need to force one or the +// other pass `{ bigIntImpl: 'jsbn' | 'native' }` to the NodeRSA constructor +// or to setOptions BEFORE the key is imported/generated. +export * from './types.js'; diff --git a/src/index.node.ts b/src/index.node.ts new file mode 100644 index 0000000..90a6279 --- /dev/null +++ b/src/index.node.ts @@ -0,0 +1,35 @@ +import { nodeBackend } from './crypto/backend.node.js'; +import { bootstrap, NodeRSA } from './node-rsa.js'; +import { JsEngine } from './rsa/engine.js'; +import { NodeNativeEngine } from './rsa/native-engine.js'; +import { nodeNativeKeygen } from './rsa/native-keygen.js'; +import { nodeNativeSchemes } from './rsa/native-signatures.js'; +import type { ResolvedOptions } from './types.js'; + +bootstrap({ + environment: 'node', + backend: nodeBackend, + // node:crypto.generateKeyPairSync — ~50× faster than RSAKey.generate + // for 2048-bit keys, used unless the caller forces environment:'browser'. + keygenFor: nodeNativeKeygen, + // PKCS#1 v1.5 and PSS sign/verify go through node:crypto.sign / verify. + // OAEP is unchanged here — the encrypt side is already handled by + // NodeNativeEngine below. + schemes: nodeNativeSchemes, + engineFor: (key, options: ResolvedOptions) => { + // Native path supports the two padding schemes node:crypto knows about. + // For everything else (and for setOptions({environment:'browser'}) which + // is checked at the call site), fall back to the JS engine. + if (options.encryptionScheme === 'pkcs1' || options.encryptionScheme === 'pkcs1_oaep') { + return new NodeNativeEngine(key, options); + } + return new JsEngine(key); + }, +}); + +export { NodeRSA }; +export default NodeRSA; +// Node bundle defaults to the jsbn BigInteger impl. Users who want native +// BigInt instead pass `{ bigIntImpl: 'native' }` to the NodeRSA constructor +// or to setOptions BEFORE the key is imported/generated. +export * from './types.js'; diff --git a/src/libs/jsbn.js b/src/libs/jsbn.js deleted file mode 100644 index 6eb3cd4..0000000 --- a/src/libs/jsbn.js +++ /dev/null @@ -1,1540 +0,0 @@ -/* - * Basic JavaScript BN library - subset useful for RSA encryption. - * - * Copyright (c) 2003-2005 Tom Wu - * All Rights Reserved. - * - * Permission is hereby granted, free of charge, to any person obtaining - * a copy of this software and associated documentation files (the - * "Software"), to deal in the Software without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, sublicense, and/or sell copies of the Software, and to - * permit persons to whom the Software is furnished to do so, subject to - * the following conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND, - * EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY - * WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. - * - * IN NO EVENT SHALL TOM WU BE LIABLE FOR ANY SPECIAL, INCIDENTAL, - * INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND, OR ANY DAMAGES WHATSOEVER - * RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER OR NOT ADVISED OF - * THE POSSIBILITY OF DAMAGE, AND ON ANY THEORY OF LIABILITY, ARISING OUT - * OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - * - * In addition, the following condition applies: - * - * All redistributions must retain an intact copy of this copyright notice - * and disclaimer. - */ - -/* - * Added Node.js Buffers support - * 2014 rzcoder - */ - -var crypt = require('crypto'); -var _ = require('../utils')._; - -// Bits per digit -var dbits; - -// JavaScript engine analysis -var canary = 0xdeadbeefcafe; -var j_lm = ((canary & 0xffffff) == 0xefcafe); - -// (public) Constructor -function BigInteger(a, b) { - if (a != null) { - if ("number" == typeof a) { - this.fromNumber(a, b); - } else if (Buffer.isBuffer(a)) { - this.fromBuffer(a); - } else if (b == null && "string" != typeof a) { - this.fromByteArray(a); - } else { - this.fromString(a, b); - } - } -} - -// return new, unset BigInteger -function nbi() { - return new BigInteger(null); -} - -// am: Compute w_j += (x*this_i), propagate carries, -// c is initial carry, returns final carry. -// c < 3*dvalue, x < 2*dvalue, this_i < dvalue -// We need to select the fastest one that works in this environment. - -// am1: use a single mult and divide to get the high bits, -// max digit bits should be 26 because -// max internal value = 2*dvalue^2-2*dvalue (< 2^53) -function am1(i, x, w, j, c, n) { - while (--n >= 0) { - var v = x * this[i++] + w[j] + c; - c = Math.floor(v / 0x4000000); - w[j++] = v & 0x3ffffff; - } - return c; -} -// am2 avoids a big mult-and-extract completely. -// Max digit bits should be <= 30 because we do bitwise ops -// on values up to 2*hdvalue^2-hdvalue-1 (< 2^31) -function am2(i, x, w, j, c, n) { - var xl = x & 0x7fff, xh = x >> 15; - while (--n >= 0) { - var l = this[i] & 0x7fff; - var h = this[i++] >> 15; - var m = xh * l + h * xl; - l = xl * l + ((m & 0x7fff) << 15) + w[j] + (c & 0x3fffffff); - c = (l >>> 30) + (m >>> 15) + xh * h + (c >>> 30); - w[j++] = l & 0x3fffffff; - } - return c; -} -// Alternately, set max digit bits to 28 since some -// browsers slow down when dealing with 32-bit numbers. -function am3(i, x, w, j, c, n) { - var xl = x & 0x3fff, xh = x >> 14; - while (--n >= 0) { - var l = this[i] & 0x3fff; - var h = this[i++] >> 14; - var m = xh * l + h * xl; - l = xl * l + ((m & 0x3fff) << 14) + w[j] + c; - c = (l >> 28) + (m >> 14) + xh * h; - w[j++] = l & 0xfffffff; - } - return c; -} - -// We need to select the fastest one that works in this environment. -//if (j_lm && (navigator.appName == "Microsoft Internet Explorer")) { -// BigInteger.prototype.am = am2; -// dbits = 30; -//} else if (j_lm && (navigator.appName != "Netscape")) { -// BigInteger.prototype.am = am1; -// dbits = 26; -//} else { // Mozilla/Netscape seems to prefer am3 -// BigInteger.prototype.am = am3; -// dbits = 28; -//} - -// For node.js, we pick am3 with max dbits to 28. -BigInteger.prototype.am = am3; -dbits = 28; - -BigInteger.prototype.DB = dbits; -BigInteger.prototype.DM = ((1 << dbits) - 1); -BigInteger.prototype.DV = (1 << dbits); - -var BI_FP = 52; -BigInteger.prototype.FV = Math.pow(2, BI_FP); -BigInteger.prototype.F1 = BI_FP - dbits; -BigInteger.prototype.F2 = 2 * dbits - BI_FP; - -// Digit conversions -var BI_RM = "0123456789abcdefghijklmnopqrstuvwxyz"; -var BI_RC = new Array(); -var rr, vv; -rr = "0".charCodeAt(0); -for (vv = 0; vv <= 9; ++vv) BI_RC[rr++] = vv; -rr = "a".charCodeAt(0); -for (vv = 10; vv < 36; ++vv) BI_RC[rr++] = vv; -rr = "A".charCodeAt(0); -for (vv = 10; vv < 36; ++vv) BI_RC[rr++] = vv; - -function int2char(n) { - return BI_RM.charAt(n); -} -function intAt(s, i) { - var c = BI_RC[s.charCodeAt(i)]; - return (c == null) ? -1 : c; -} - -// (protected) copy this to r -function bnpCopyTo(r) { - for (var i = this.t - 1; i >= 0; --i) r[i] = this[i]; - r.t = this.t; - r.s = this.s; -} - -// (protected) set from integer value x, -DV <= x < DV -function bnpFromInt(x) { - this.t = 1; - this.s = (x < 0) ? -1 : 0; - if (x > 0) this[0] = x; - else if (x < -1) this[0] = x + DV; - else this.t = 0; -} - -// return bigint initialized to value -function nbv(i) { - var r = nbi(); - r.fromInt(i); - return r; -} - -// (protected) set from string and radix -function bnpFromString(data, radix, unsigned) { - var k; - switch (radix) { - case 2: - k = 1; - break; - case 4: - k = 2; - break; - case 8: - k = 3; - break; - case 16: - k = 4; - break; - case 32: - k = 5; - break; - case 256: - k = 8; - break; - default: - this.fromRadix(data, radix); - return; - } - - this.t = 0; - this.s = 0; - - var i = data.length; - var mi = false; - var sh = 0; - - while (--i >= 0) { - var x = (k == 8) ? data[i] & 0xff : intAt(data, i); - if (x < 0) { - if (data.charAt(i) == "-") mi = true; - continue; - } - mi = false; - if (sh === 0) - this[this.t++] = x; - else if (sh + k > this.DB) { - this[this.t - 1] |= (x & ((1 << (this.DB - sh)) - 1)) << sh; - this[this.t++] = (x >> (this.DB - sh)); - } - else - this[this.t - 1] |= x << sh; - sh += k; - if (sh >= this.DB) sh -= this.DB; - } - if ((!unsigned) && k == 8 && (data[0] & 0x80) != 0) { - this.s = -1; - if (sh > 0) this[this.t - 1] |= ((1 << (this.DB - sh)) - 1) << sh; - } - this.clamp(); - if (mi) BigInteger.ZERO.subTo(this, this); -} - -function bnpFromByteArray(a, unsigned) { - this.fromString(a, 256, unsigned) -} - -function bnpFromBuffer(a) { - this.fromString(a, 256, true) -} - -// (protected) clamp off excess high words -function bnpClamp() { - var c = this.s & this.DM; - while (this.t > 0 && this[this.t - 1] == c) --this.t; -} - -// (public) return string representation in given radix -function bnToString(b) { - if (this.s < 0) return "-" + this.negate().toString(b); - var k; - if (b == 16) k = 4; - else if (b == 8) k = 3; - else if (b == 2) k = 1; - else if (b == 32) k = 5; - else if (b == 4) k = 2; - else return this.toRadix(b); - var km = (1 << k) - 1, d, m = false, r = "", i = this.t; - var p = this.DB - (i * this.DB) % k; - if (i-- > 0) { - if (p < this.DB && (d = this[i] >> p) > 0) { - m = true; - r = int2char(d); - } - while (i >= 0) { - if (p < k) { - d = (this[i] & ((1 << p) - 1)) << (k - p); - d |= this[--i] >> (p += this.DB - k); - } - else { - d = (this[i] >> (p -= k)) & km; - if (p <= 0) { - p += this.DB; - --i; - } - } - if (d > 0) m = true; - if (m) r += int2char(d); - } - } - return m ? r : "0"; -} - -// (public) -this -function bnNegate() { - var r = nbi(); - BigInteger.ZERO.subTo(this, r); - return r; -} - -// (public) |this| -function bnAbs() { - return (this.s < 0) ? this.negate() : this; -} - -// (public) return + if this > a, - if this < a, 0 if equal -function bnCompareTo(a) { - var r = this.s - a.s; - if (r != 0) return r; - var i = this.t; - r = i - a.t; - if (r != 0) return (this.s < 0) ? -r : r; - while (--i >= 0) if ((r = this[i] - a[i]) != 0) return r; - return 0; -} - -// returns bit length of the integer x -function nbits(x) { - var r = 1, t; - if ((t = x >>> 16) != 0) { - x = t; - r += 16; - } - if ((t = x >> 8) != 0) { - x = t; - r += 8; - } - if ((t = x >> 4) != 0) { - x = t; - r += 4; - } - if ((t = x >> 2) != 0) { - x = t; - r += 2; - } - if ((t = x >> 1) != 0) { - x = t; - r += 1; - } - return r; -} - -// (public) return the number of bits in "this" -function bnBitLength() { - if (this.t <= 0) return 0; - return this.DB * (this.t - 1) + nbits(this[this.t - 1] ^ (this.s & this.DM)); -} - -// (protected) r = this << n*DB -function bnpDLShiftTo(n, r) { - var i; - for (i = this.t - 1; i >= 0; --i) r[i + n] = this[i]; - for (i = n - 1; i >= 0; --i) r[i] = 0; - r.t = this.t + n; - r.s = this.s; -} - -// (protected) r = this >> n*DB -function bnpDRShiftTo(n, r) { - for (var i = n; i < this.t; ++i) r[i - n] = this[i]; - r.t = Math.max(this.t - n, 0); - r.s = this.s; -} - -// (protected) r = this << n -function bnpLShiftTo(n, r) { - var bs = n % this.DB; - var cbs = this.DB - bs; - var bm = (1 << cbs) - 1; - var ds = Math.floor(n / this.DB), c = (this.s << bs) & this.DM, i; - for (i = this.t - 1; i >= 0; --i) { - r[i + ds + 1] = (this[i] >> cbs) | c; - c = (this[i] & bm) << bs; - } - for (i = ds - 1; i >= 0; --i) r[i] = 0; - r[ds] = c; - r.t = this.t + ds + 1; - r.s = this.s; - r.clamp(); -} - -// (protected) r = this >> n -function bnpRShiftTo(n, r) { - r.s = this.s; - var ds = Math.floor(n / this.DB); - if (ds >= this.t) { - r.t = 0; - return; - } - var bs = n % this.DB; - var cbs = this.DB - bs; - var bm = (1 << bs) - 1; - r[0] = this[ds] >> bs; - for (var i = ds + 1; i < this.t; ++i) { - r[i - ds - 1] |= (this[i] & bm) << cbs; - r[i - ds] = this[i] >> bs; - } - if (bs > 0) r[this.t - ds - 1] |= (this.s & bm) << cbs; - r.t = this.t - ds; - r.clamp(); -} - -// (protected) r = this - a -function bnpSubTo(a, r) { - var i = 0, c = 0, m = Math.min(a.t, this.t); - while (i < m) { - c += this[i] - a[i]; - r[i++] = c & this.DM; - c >>= this.DB; - } - if (a.t < this.t) { - c -= a.s; - while (i < this.t) { - c += this[i]; - r[i++] = c & this.DM; - c >>= this.DB; - } - c += this.s; - } - else { - c += this.s; - while (i < a.t) { - c -= a[i]; - r[i++] = c & this.DM; - c >>= this.DB; - } - c -= a.s; - } - r.s = (c < 0) ? -1 : 0; - if (c < -1) r[i++] = this.DV + c; - else if (c > 0) r[i++] = c; - r.t = i; - r.clamp(); -} - -// (protected) r = this * a, r != this,a (HAC 14.12) -// "this" should be the larger one if appropriate. -function bnpMultiplyTo(a, r) { - var x = this.abs(), y = a.abs(); - var i = x.t; - r.t = i + y.t; - while (--i >= 0) r[i] = 0; - for (i = 0; i < y.t; ++i) r[i + x.t] = x.am(0, y[i], r, i, 0, x.t); - r.s = 0; - r.clamp(); - if (this.s != a.s) BigInteger.ZERO.subTo(r, r); -} - -// (protected) r = this^2, r != this (HAC 14.16) -function bnpSquareTo(r) { - var x = this.abs(); - var i = r.t = 2 * x.t; - while (--i >= 0) r[i] = 0; - for (i = 0; i < x.t - 1; ++i) { - var c = x.am(i, x[i], r, 2 * i, 0, 1); - if ((r[i + x.t] += x.am(i + 1, 2 * x[i], r, 2 * i + 1, c, x.t - i - 1)) >= x.DV) { - r[i + x.t] -= x.DV; - r[i + x.t + 1] = 1; - } - } - if (r.t > 0) r[r.t - 1] += x.am(i, x[i], r, 2 * i, 0, 1); - r.s = 0; - r.clamp(); -} - -// (protected) divide this by m, quotient and remainder to q, r (HAC 14.20) -// r != q, this != m. q or r may be null. -function bnpDivRemTo(m, q, r) { - var pm = m.abs(); - if (pm.t <= 0) return; - var pt = this.abs(); - if (pt.t < pm.t) { - if (q != null) q.fromInt(0); - if (r != null) this.copyTo(r); - return; - } - if (r == null) r = nbi(); - var y = nbi(), ts = this.s, ms = m.s; - var nsh = this.DB - nbits(pm[pm.t - 1]); // normalize modulus - if (nsh > 0) { - pm.lShiftTo(nsh, y); - pt.lShiftTo(nsh, r); - } - else { - pm.copyTo(y); - pt.copyTo(r); - } - var ys = y.t; - var y0 = y[ys - 1]; - if (y0 === 0) return; - var yt = y0 * (1 << this.F1) + ((ys > 1) ? y[ys - 2] >> this.F2 : 0); - var d1 = this.FV / yt, d2 = (1 << this.F1) / yt, e = 1 << this.F2; - var i = r.t, j = i - ys, t = (q == null) ? nbi() : q; - y.dlShiftTo(j, t); - if (r.compareTo(t) >= 0) { - r[r.t++] = 1; - r.subTo(t, r); - } - BigInteger.ONE.dlShiftTo(ys, t); - t.subTo(y, y); // "negative" y so we can replace sub with am later - while (y.t < ys) y[y.t++] = 0; - while (--j >= 0) { - // Estimate quotient digit - var qd = (r[--i] == y0) ? this.DM : Math.floor(r[i] * d1 + (r[i - 1] + e) * d2); - if ((r[i] += y.am(0, qd, r, j, 0, ys)) < qd) { // Try it out - y.dlShiftTo(j, t); - r.subTo(t, r); - while (r[i] < --qd) r.subTo(t, r); - } - } - if (q != null) { - r.drShiftTo(ys, q); - if (ts != ms) BigInteger.ZERO.subTo(q, q); - } - r.t = ys; - r.clamp(); - if (nsh > 0) r.rShiftTo(nsh, r); // Denormalize remainder - if (ts < 0) BigInteger.ZERO.subTo(r, r); -} - -// (public) this mod a -function bnMod(a) { - var r = nbi(); - this.abs().divRemTo(a, null, r); - if (this.s < 0 && r.compareTo(BigInteger.ZERO) > 0) a.subTo(r, r); - return r; -} - -// Modular reduction using "classic" algorithm -function Classic(m) { - this.m = m; -} -function cConvert(x) { - if (x.s < 0 || x.compareTo(this.m) >= 0) return x.mod(this.m); - else return x; -} -function cRevert(x) { - return x; -} -function cReduce(x) { - x.divRemTo(this.m, null, x); -} -function cMulTo(x, y, r) { - x.multiplyTo(y, r); - this.reduce(r); -} -function cSqrTo(x, r) { - x.squareTo(r); - this.reduce(r); -} - -Classic.prototype.convert = cConvert; -Classic.prototype.revert = cRevert; -Classic.prototype.reduce = cReduce; -Classic.prototype.mulTo = cMulTo; -Classic.prototype.sqrTo = cSqrTo; - -// (protected) return "-1/this % 2^DB"; useful for Mont. reduction -// justification: -// xy == 1 (mod m) -// xy = 1+km -// xy(2-xy) = (1+km)(1-km) -// x[y(2-xy)] = 1-k^2m^2 -// x[y(2-xy)] == 1 (mod m^2) -// if y is 1/x mod m, then y(2-xy) is 1/x mod m^2 -// should reduce x and y(2-xy) by m^2 at each step to keep size bounded. -// JS multiply "overflows" differently from C/C++, so care is needed here. -function bnpInvDigit() { - if (this.t < 1) return 0; - var x = this[0]; - if ((x & 1) === 0) return 0; - var y = x & 3; // y == 1/x mod 2^2 - y = (y * (2 - (x & 0xf) * y)) & 0xf; // y == 1/x mod 2^4 - y = (y * (2 - (x & 0xff) * y)) & 0xff; // y == 1/x mod 2^8 - y = (y * (2 - (((x & 0xffff) * y) & 0xffff))) & 0xffff; // y == 1/x mod 2^16 - // last step - calculate inverse mod DV directly; - // assumes 16 < DB <= 32 and assumes ability to handle 48-bit ints - y = (y * (2 - x * y % this.DV)) % this.DV; // y == 1/x mod 2^dbits - // we really want the negative inverse, and -DV < y < DV - return (y > 0) ? this.DV - y : -y; -} - -// Montgomery reduction -function Montgomery(m) { - this.m = m; - this.mp = m.invDigit(); - this.mpl = this.mp & 0x7fff; - this.mph = this.mp >> 15; - this.um = (1 << (m.DB - 15)) - 1; - this.mt2 = 2 * m.t; -} - -// xR mod m -function montConvert(x) { - var r = nbi(); - x.abs().dlShiftTo(this.m.t, r); - r.divRemTo(this.m, null, r); - if (x.s < 0 && r.compareTo(BigInteger.ZERO) > 0) this.m.subTo(r, r); - return r; -} - -// x/R mod m -function montRevert(x) { - var r = nbi(); - x.copyTo(r); - this.reduce(r); - return r; -} - -// x = x/R mod m (HAC 14.32) -function montReduce(x) { - while (x.t <= this.mt2) // pad x so am has enough room later - x[x.t++] = 0; - for (var i = 0; i < this.m.t; ++i) { - // faster way of calculating u0 = x[i]*mp mod DV - var j = x[i] & 0x7fff; - var u0 = (j * this.mpl + (((j * this.mph + (x[i] >> 15) * this.mpl) & this.um) << 15)) & x.DM; - // use am to combine the multiply-shift-add into one call - j = i + this.m.t; - x[j] += this.m.am(0, u0, x, i, 0, this.m.t); - // propagate carry - while (x[j] >= x.DV) { - x[j] -= x.DV; - x[++j]++; - } - } - x.clamp(); - x.drShiftTo(this.m.t, x); - if (x.compareTo(this.m) >= 0) x.subTo(this.m, x); -} - -// r = "x^2/R mod m"; x != r -function montSqrTo(x, r) { - x.squareTo(r); - this.reduce(r); -} - -// r = "xy/R mod m"; x,y != r -function montMulTo(x, y, r) { - x.multiplyTo(y, r); - this.reduce(r); -} - -Montgomery.prototype.convert = montConvert; -Montgomery.prototype.revert = montRevert; -Montgomery.prototype.reduce = montReduce; -Montgomery.prototype.mulTo = montMulTo; -Montgomery.prototype.sqrTo = montSqrTo; - -// (protected) true iff this is even -function bnpIsEven() { - return ((this.t > 0) ? (this[0] & 1) : this.s) === 0; -} - -// (protected) this^e, e < 2^32, doing sqr and mul with "r" (HAC 14.79) -function bnpExp(e, z) { - if (e > 0xffffffff || e < 1) return BigInteger.ONE; - var r = nbi(), r2 = nbi(), g = z.convert(this), i = nbits(e) - 1; - g.copyTo(r); - while (--i >= 0) { - z.sqrTo(r, r2); - if ((e & (1 << i)) > 0) z.mulTo(r2, g, r); - else { - var t = r; - r = r2; - r2 = t; - } - } - return z.revert(r); -} - -// (public) this^e % m, 0 <= e < 2^32 -function bnModPowInt(e, m) { - var z; - if (e < 256 || m.isEven()) z = new Classic(m); else z = new Montgomery(m); - return this.exp(e, z); -} - -// Copyright (c) 2005-2009 Tom Wu -// All Rights Reserved. -// See "LICENSE" for details. - -// Extended JavaScript BN functions, required for RSA private ops. - -// Version 1.1: new BigInteger("0", 10) returns "proper" zero -// Version 1.2: square() API, isProbablePrime fix - -//(public) -function bnClone() { - var r = nbi(); - this.copyTo(r); - return r; -} - -//(public) return value as integer -function bnIntValue() { - if (this.s < 0) { - if (this.t == 1) return this[0] - this.DV; - else if (this.t === 0) return -1; - } - else if (this.t == 1) return this[0]; - else if (this.t === 0) return 0; -// assumes 16 < DB < 32 - return ((this[1] & ((1 << (32 - this.DB)) - 1)) << this.DB) | this[0]; -} - -//(public) return value as byte -function bnByteValue() { - return (this.t == 0) ? this.s : (this[0] << 24) >> 24; -} - -//(public) return value as short (assumes DB>=16) -function bnShortValue() { - return (this.t == 0) ? this.s : (this[0] << 16) >> 16; -} - -//(protected) return x s.t. r^x < DV -function bnpChunkSize(r) { - return Math.floor(Math.LN2 * this.DB / Math.log(r)); -} - -//(public) 0 if this === 0, 1 if this > 0 -function bnSigNum() { - if (this.s < 0) return -1; - else if (this.t <= 0 || (this.t == 1 && this[0] <= 0)) return 0; - else return 1; -} - -//(protected) convert to radix string -function bnpToRadix(b) { - if (b == null) b = 10; - if (this.signum() === 0 || b < 2 || b > 36) return "0"; - var cs = this.chunkSize(b); - var a = Math.pow(b, cs); - var d = nbv(a), y = nbi(), z = nbi(), r = ""; - this.divRemTo(d, y, z); - while (y.signum() > 0) { - r = (a + z.intValue()).toString(b).substr(1) + r; - y.divRemTo(d, y, z); - } - return z.intValue().toString(b) + r; -} - -//(protected) convert from radix string -function bnpFromRadix(s, b) { - this.fromInt(0); - if (b == null) b = 10; - var cs = this.chunkSize(b); - var d = Math.pow(b, cs), mi = false, j = 0, w = 0; - for (var i = 0; i < s.length; ++i) { - var x = intAt(s, i); - if (x < 0) { - if (s.charAt(i) == "-" && this.signum() === 0) mi = true; - continue; - } - w = b * w + x; - if (++j >= cs) { - this.dMultiply(d); - this.dAddOffset(w, 0); - j = 0; - w = 0; - } - } - if (j > 0) { - this.dMultiply(Math.pow(b, j)); - this.dAddOffset(w, 0); - } - if (mi) BigInteger.ZERO.subTo(this, this); -} - -//(protected) alternate constructor -function bnpFromNumber(a, b) { - if ("number" == typeof b) { - // new BigInteger(int,int,RNG) - if (a < 2) this.fromInt(1); - else { - this.fromNumber(a); - if (!this.testBit(a - 1)) // force MSB set - this.bitwiseTo(BigInteger.ONE.shiftLeft(a - 1), op_or, this); - if (this.isEven()) this.dAddOffset(1, 0); // force odd - while (!this.isProbablePrime(b)) { - this.dAddOffset(2, 0); - if (this.bitLength() > a) this.subTo(BigInteger.ONE.shiftLeft(a - 1), this); - } - } - } else { - // new BigInteger(int,RNG) - var x = crypt.randomBytes((a >> 3) + 1) - var t = a & 7; - - if (t > 0) - x[0] &= ((1 << t) - 1); - else - x[0] = 0; - - this.fromByteArray(x); - } -} - -//(public) convert to bigendian byte array -function bnToByteArray() { - var i = this.t, r = new Array(); - r[0] = this.s; - var p = this.DB - (i * this.DB) % 8, d, k = 0; - if (i-- > 0) { - if (p < this.DB && (d = this[i] >> p) != (this.s & this.DM) >> p) - r[k++] = d | (this.s << (this.DB - p)); - while (i >= 0) { - if (p < 8) { - d = (this[i] & ((1 << p) - 1)) << (8 - p); - d |= this[--i] >> (p += this.DB - 8); - } - else { - d = (this[i] >> (p -= 8)) & 0xff; - if (p <= 0) { - p += this.DB; - --i; - } - } - if ((d & 0x80) != 0) d |= -256; - if (k === 0 && (this.s & 0x80) != (d & 0x80)) ++k; - if (k > 0 || d != this.s) r[k++] = d; - } - } - return r; -} - -/** - * return Buffer object - * @param trim {boolean} slice buffer if first element == 0 - * @returns {Buffer} - */ -function bnToBuffer(trimOrSize) { - var res = Buffer.from(this.toByteArray()); - if (trimOrSize === true && res[0] === 0) { - res = res.slice(1); - } else if (_.isNumber(trimOrSize)) { - if (res.length > trimOrSize) { - for (var i = 0; i < res.length - trimOrSize; i++) { - if (res[i] !== 0) { - return null; - } - } - return res.slice(res.length - trimOrSize); - } else if (res.length < trimOrSize) { - var padded = Buffer.alloc(trimOrSize); - padded.fill(0, 0, trimOrSize - res.length); - res.copy(padded, trimOrSize - res.length); - return padded; - } - } - return res; -} - -function bnEquals(a) { - return (this.compareTo(a) == 0); -} -function bnMin(a) { - return (this.compareTo(a) < 0) ? this : a; -} -function bnMax(a) { - return (this.compareTo(a) > 0) ? this : a; -} - -//(protected) r = this op a (bitwise) -function bnpBitwiseTo(a, op, r) { - var i, f, m = Math.min(a.t, this.t); - for (i = 0; i < m; ++i) r[i] = op(this[i], a[i]); - if (a.t < this.t) { - f = a.s & this.DM; - for (i = m; i < this.t; ++i) r[i] = op(this[i], f); - r.t = this.t; - } - else { - f = this.s & this.DM; - for (i = m; i < a.t; ++i) r[i] = op(f, a[i]); - r.t = a.t; - } - r.s = op(this.s, a.s); - r.clamp(); -} - -//(public) this & a -function op_and(x, y) { - return x & y; -} -function bnAnd(a) { - var r = nbi(); - this.bitwiseTo(a, op_and, r); - return r; -} - -//(public) this | a -function op_or(x, y) { - return x | y; -} -function bnOr(a) { - var r = nbi(); - this.bitwiseTo(a, op_or, r); - return r; -} - -//(public) this ^ a -function op_xor(x, y) { - return x ^ y; -} -function bnXor(a) { - var r = nbi(); - this.bitwiseTo(a, op_xor, r); - return r; -} - -//(public) this & ~a -function op_andnot(x, y) { - return x & ~y; -} -function bnAndNot(a) { - var r = nbi(); - this.bitwiseTo(a, op_andnot, r); - return r; -} - -//(public) ~this -function bnNot() { - var r = nbi(); - for (var i = 0; i < this.t; ++i) r[i] = this.DM & ~this[i]; - r.t = this.t; - r.s = ~this.s; - return r; -} - -//(public) this << n -function bnShiftLeft(n) { - var r = nbi(); - if (n < 0) this.rShiftTo(-n, r); else this.lShiftTo(n, r); - return r; -} - -//(public) this >> n -function bnShiftRight(n) { - var r = nbi(); - if (n < 0) this.lShiftTo(-n, r); else this.rShiftTo(n, r); - return r; -} - -//return index of lowest 1-bit in x, x < 2^31 -function lbit(x) { - if (x === 0) return -1; - var r = 0; - if ((x & 0xffff) === 0) { - x >>= 16; - r += 16; - } - if ((x & 0xff) === 0) { - x >>= 8; - r += 8; - } - if ((x & 0xf) === 0) { - x >>= 4; - r += 4; - } - if ((x & 3) === 0) { - x >>= 2; - r += 2; - } - if ((x & 1) === 0) ++r; - return r; -} - -//(public) returns index of lowest 1-bit (or -1 if none) -function bnGetLowestSetBit() { - for (var i = 0; i < this.t; ++i) - if (this[i] != 0) return i * this.DB + lbit(this[i]); - if (this.s < 0) return this.t * this.DB; - return -1; -} - -//return number of 1 bits in x -function cbit(x) { - var r = 0; - while (x != 0) { - x &= x - 1; - ++r; - } - return r; -} - -//(public) return number of set bits -function bnBitCount() { - var r = 0, x = this.s & this.DM; - for (var i = 0; i < this.t; ++i) r += cbit(this[i] ^ x); - return r; -} - -//(public) true iff nth bit is set -function bnTestBit(n) { - var j = Math.floor(n / this.DB); - if (j >= this.t) return (this.s != 0); - return ((this[j] & (1 << (n % this.DB))) != 0); -} - -//(protected) this op (1<>= this.DB; - } - if (a.t < this.t) { - c += a.s; - while (i < this.t) { - c += this[i]; - r[i++] = c & this.DM; - c >>= this.DB; - } - c += this.s; - } - else { - c += this.s; - while (i < a.t) { - c += a[i]; - r[i++] = c & this.DM; - c >>= this.DB; - } - c += a.s; - } - r.s = (c < 0) ? -1 : 0; - if (c > 0) r[i++] = c; - else if (c < -1) r[i++] = this.DV + c; - r.t = i; - r.clamp(); -} - -//(public) this + a -function bnAdd(a) { - var r = nbi(); - this.addTo(a, r); - return r; -} - -//(public) this - a -function bnSubtract(a) { - var r = nbi(); - this.subTo(a, r); - return r; -} - -//(public) this * a -function bnMultiply(a) { - var r = nbi(); - this.multiplyTo(a, r); - return r; -} - -// (public) this^2 -function bnSquare() { - var r = nbi(); - this.squareTo(r); - return r; -} - -//(public) this / a -function bnDivide(a) { - var r = nbi(); - this.divRemTo(a, r, null); - return r; -} - -//(public) this % a -function bnRemainder(a) { - var r = nbi(); - this.divRemTo(a, null, r); - return r; -} - -//(public) [this/a,this%a] -function bnDivideAndRemainder(a) { - var q = nbi(), r = nbi(); - this.divRemTo(a, q, r); - return new Array(q, r); -} - -//(protected) this *= n, this >= 0, 1 < n < DV -function bnpDMultiply(n) { - this[this.t] = this.am(0, n - 1, this, 0, 0, this.t); - ++this.t; - this.clamp(); -} - -//(protected) this += n << w words, this >= 0 -function bnpDAddOffset(n, w) { - if (n === 0) return; - while (this.t <= w) this[this.t++] = 0; - this[w] += n; - while (this[w] >= this.DV) { - this[w] -= this.DV; - if (++w >= this.t) this[this.t++] = 0; - ++this[w]; - } -} - -//A "null" reducer -function NullExp() { -} -function nNop(x) { - return x; -} -function nMulTo(x, y, r) { - x.multiplyTo(y, r); -} -function nSqrTo(x, r) { - x.squareTo(r); -} - -NullExp.prototype.convert = nNop; -NullExp.prototype.revert = nNop; -NullExp.prototype.mulTo = nMulTo; -NullExp.prototype.sqrTo = nSqrTo; - -//(public) this^e -function bnPow(e) { - return this.exp(e, new NullExp()); -} - -//(protected) r = lower n words of "this * a", a.t <= n -//"this" should be the larger one if appropriate. -function bnpMultiplyLowerTo(a, n, r) { - var i = Math.min(this.t + a.t, n); - r.s = 0; // assumes a,this >= 0 - r.t = i; - while (i > 0) r[--i] = 0; - var j; - for (j = r.t - this.t; i < j; ++i) r[i + this.t] = this.am(0, a[i], r, i, 0, this.t); - for (j = Math.min(a.t, n); i < j; ++i) this.am(0, a[i], r, i, 0, n - i); - r.clamp(); -} - -//(protected) r = "this * a" without lower n words, n > 0 -//"this" should be the larger one if appropriate. -function bnpMultiplyUpperTo(a, n, r) { - --n; - var i = r.t = this.t + a.t - n; - r.s = 0; // assumes a,this >= 0 - while (--i >= 0) r[i] = 0; - for (i = Math.max(n - this.t, 0); i < a.t; ++i) - r[this.t + i - n] = this.am(n - i, a[i], r, 0, 0, this.t + i - n); - r.clamp(); - r.drShiftTo(1, r); -} - -//Barrett modular reduction -function Barrett(m) { -// setup Barrett - this.r2 = nbi(); - this.q3 = nbi(); - BigInteger.ONE.dlShiftTo(2 * m.t, this.r2); - this.mu = this.r2.divide(m); - this.m = m; -} - -function barrettConvert(x) { - if (x.s < 0 || x.t > 2 * this.m.t) return x.mod(this.m); - else if (x.compareTo(this.m) < 0) return x; - else { - var r = nbi(); - x.copyTo(r); - this.reduce(r); - return r; - } -} - -function barrettRevert(x) { - return x; -} - -//x = x mod m (HAC 14.42) -function barrettReduce(x) { - x.drShiftTo(this.m.t - 1, this.r2); - if (x.t > this.m.t + 1) { - x.t = this.m.t + 1; - x.clamp(); - } - this.mu.multiplyUpperTo(this.r2, this.m.t + 1, this.q3); - this.m.multiplyLowerTo(this.q3, this.m.t + 1, this.r2); - while (x.compareTo(this.r2) < 0) x.dAddOffset(1, this.m.t + 1); - x.subTo(this.r2, x); - while (x.compareTo(this.m) >= 0) x.subTo(this.m, x); -} - -//r = x^2 mod m; x != r -function barrettSqrTo(x, r) { - x.squareTo(r); - this.reduce(r); -} - -//r = x*y mod m; x,y != r -function barrettMulTo(x, y, r) { - x.multiplyTo(y, r); - this.reduce(r); -} - -Barrett.prototype.convert = barrettConvert; -Barrett.prototype.revert = barrettRevert; -Barrett.prototype.reduce = barrettReduce; -Barrett.prototype.mulTo = barrettMulTo; -Barrett.prototype.sqrTo = barrettSqrTo; - -//(public) this^e % m (HAC 14.85) -function bnModPow(e, m) { - var i = e.bitLength(), k, r = nbv(1), z; - if (i <= 0) return r; - else if (i < 18) k = 1; - else if (i < 48) k = 3; - else if (i < 144) k = 4; - else if (i < 768) k = 5; - else k = 6; - if (i < 8) - z = new Classic(m); - else if (m.isEven()) - z = new Barrett(m); - else - z = new Montgomery(m); - -// precomputation - var g = new Array(), n = 3, k1 = k - 1, km = (1 << k) - 1; - g[1] = z.convert(this); - if (k > 1) { - var g2 = nbi(); - z.sqrTo(g[1], g2); - while (n <= km) { - g[n] = nbi(); - z.mulTo(g2, g[n - 2], g[n]); - n += 2; - } - } - - var j = e.t - 1, w, is1 = true, r2 = nbi(), t; - i = nbits(e[j]) - 1; - while (j >= 0) { - if (i >= k1) w = (e[j] >> (i - k1)) & km; - else { - w = (e[j] & ((1 << (i + 1)) - 1)) << (k1 - i); - if (j > 0) w |= e[j - 1] >> (this.DB + i - k1); - } - - n = k; - while ((w & 1) === 0) { - w >>= 1; - --n; - } - if ((i -= n) < 0) { - i += this.DB; - --j; - } - if (is1) { // ret == 1, don't bother squaring or multiplying it - g[w].copyTo(r); - is1 = false; - } - else { - while (n > 1) { - z.sqrTo(r, r2); - z.sqrTo(r2, r); - n -= 2; - } - if (n > 0) z.sqrTo(r, r2); else { - t = r; - r = r2; - r2 = t; - } - z.mulTo(r2, g[w], r); - } - - while (j >= 0 && (e[j] & (1 << i)) === 0) { - z.sqrTo(r, r2); - t = r; - r = r2; - r2 = t; - if (--i < 0) { - i = this.DB - 1; - --j; - } - } - } - return z.revert(r); -} - -//(public) gcd(this,a) (HAC 14.54) -function bnGCD(a) { - var x = (this.s < 0) ? this.negate() : this.clone(); - var y = (a.s < 0) ? a.negate() : a.clone(); - if (x.compareTo(y) < 0) { - var t = x; - x = y; - y = t; - } - var i = x.getLowestSetBit(), g = y.getLowestSetBit(); - if (g < 0) return x; - if (i < g) g = i; - if (g > 0) { - x.rShiftTo(g, x); - y.rShiftTo(g, y); - } - while (x.signum() > 0) { - if ((i = x.getLowestSetBit()) > 0) x.rShiftTo(i, x); - if ((i = y.getLowestSetBit()) > 0) y.rShiftTo(i, y); - if (x.compareTo(y) >= 0) { - x.subTo(y, x); - x.rShiftTo(1, x); - } - else { - y.subTo(x, y); - y.rShiftTo(1, y); - } - } - if (g > 0) y.lShiftTo(g, y); - return y; -} - -//(protected) this % n, n < 2^26 -function bnpModInt(n) { - if (n <= 0) return 0; - var d = this.DV % n, r = (this.s < 0) ? n - 1 : 0; - if (this.t > 0) - if (d === 0) r = this[0] % n; - else for (var i = this.t - 1; i >= 0; --i) r = (d * r + this[i]) % n; - return r; -} - -//(public) 1/this % m (HAC 14.61) -function bnModInverse(m) { - var ac = m.isEven(); - if ((this.isEven() && ac) || m.signum() === 0) return BigInteger.ZERO; - var u = m.clone(), v = this.clone(); - var a = nbv(1), b = nbv(0), c = nbv(0), d = nbv(1); - while (u.signum() != 0) { - while (u.isEven()) { - u.rShiftTo(1, u); - if (ac) { - if (!a.isEven() || !b.isEven()) { - a.addTo(this, a); - b.subTo(m, b); - } - a.rShiftTo(1, a); - } - else if (!b.isEven()) b.subTo(m, b); - b.rShiftTo(1, b); - } - while (v.isEven()) { - v.rShiftTo(1, v); - if (ac) { - if (!c.isEven() || !d.isEven()) { - c.addTo(this, c); - d.subTo(m, d); - } - c.rShiftTo(1, c); - } - else if (!d.isEven()) d.subTo(m, d); - d.rShiftTo(1, d); - } - if (u.compareTo(v) >= 0) { - u.subTo(v, u); - if (ac) a.subTo(c, a); - b.subTo(d, b); - } - else { - v.subTo(u, v); - if (ac) c.subTo(a, c); - d.subTo(b, d); - } - } - if (v.compareTo(BigInteger.ONE) != 0) return BigInteger.ZERO; - if (d.compareTo(m) >= 0) return d.subtract(m); - if (d.signum() < 0) d.addTo(m, d); else return d; - if (d.signum() < 0) return d.add(m); else return d; -} - -var lowprimes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509, 521, 523, 541, 547, 557, 563, 569, 571, 577, 587, 593, 599, 601, 607, 613, 617, 619, 631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691, 701, 709, 719, 727, 733, 739, 743, 751, 757, 761, 769, 773, 787, 797, 809, 811, 821, 823, 827, 829, 839, 853, 857, 859, 863, 877, 881, 883, 887, 907, 911, 919, 929, 937, 941, 947, 953, 967, 971, 977, 983, 991, 997]; -var lplim = (1 << 26) / lowprimes[lowprimes.length - 1]; - -//(public) test primality with certainty >= 1-.5^t -function bnIsProbablePrime(t) { - var i, x = this.abs(); - if (x.t == 1 && x[0] <= lowprimes[lowprimes.length - 1]) { - for (i = 0; i < lowprimes.length; ++i) - if (x[0] == lowprimes[i]) return true; - return false; - } - if (x.isEven()) return false; - i = 1; - while (i < lowprimes.length) { - var m = lowprimes[i], j = i + 1; - while (j < lowprimes.length && m < lplim) m *= lowprimes[j++]; - m = x.modInt(m); - while (i < j) if (m % lowprimes[i++] === 0) return false; - } - return x.millerRabin(t); -} - -//(protected) true if probably prime (HAC 4.24, Miller-Rabin) -function bnpMillerRabin(t) { - var n1 = this.subtract(BigInteger.ONE); - var k = n1.getLowestSetBit(); - if (k <= 0) return false; - var r = n1.shiftRight(k); - t = (t + 1) >> 1; - if (t > lowprimes.length) t = lowprimes.length; - var a = nbi(); - for (var i = 0; i < t; ++i) { - //Pick bases at random, instead of starting at 2 - a.fromInt(lowprimes[Math.floor(Math.random() * lowprimes.length)]); - var y = a.modPow(r, this); - if (y.compareTo(BigInteger.ONE) != 0 && y.compareTo(n1) != 0) { - var j = 1; - while (j++ < k && y.compareTo(n1) != 0) { - y = y.modPowInt(2, this); - if (y.compareTo(BigInteger.ONE) === 0) return false; - } - if (y.compareTo(n1) != 0) return false; - } - } - return true; -} - -// protected -BigInteger.prototype.copyTo = bnpCopyTo; -BigInteger.prototype.fromInt = bnpFromInt; -BigInteger.prototype.fromString = bnpFromString; -BigInteger.prototype.fromByteArray = bnpFromByteArray; -BigInteger.prototype.fromBuffer = bnpFromBuffer; -BigInteger.prototype.clamp = bnpClamp; -BigInteger.prototype.dlShiftTo = bnpDLShiftTo; -BigInteger.prototype.drShiftTo = bnpDRShiftTo; -BigInteger.prototype.lShiftTo = bnpLShiftTo; -BigInteger.prototype.rShiftTo = bnpRShiftTo; -BigInteger.prototype.subTo = bnpSubTo; -BigInteger.prototype.multiplyTo = bnpMultiplyTo; -BigInteger.prototype.squareTo = bnpSquareTo; -BigInteger.prototype.divRemTo = bnpDivRemTo; -BigInteger.prototype.invDigit = bnpInvDigit; -BigInteger.prototype.isEven = bnpIsEven; -BigInteger.prototype.exp = bnpExp; - -BigInteger.prototype.chunkSize = bnpChunkSize; -BigInteger.prototype.toRadix = bnpToRadix; -BigInteger.prototype.fromRadix = bnpFromRadix; -BigInteger.prototype.fromNumber = bnpFromNumber; -BigInteger.prototype.bitwiseTo = bnpBitwiseTo; -BigInteger.prototype.changeBit = bnpChangeBit; -BigInteger.prototype.addTo = bnpAddTo; -BigInteger.prototype.dMultiply = bnpDMultiply; -BigInteger.prototype.dAddOffset = bnpDAddOffset; -BigInteger.prototype.multiplyLowerTo = bnpMultiplyLowerTo; -BigInteger.prototype.multiplyUpperTo = bnpMultiplyUpperTo; -BigInteger.prototype.modInt = bnpModInt; -BigInteger.prototype.millerRabin = bnpMillerRabin; - - -// public -BigInteger.prototype.toString = bnToString; -BigInteger.prototype.negate = bnNegate; -BigInteger.prototype.abs = bnAbs; -BigInteger.prototype.compareTo = bnCompareTo; -BigInteger.prototype.bitLength = bnBitLength; -BigInteger.prototype.mod = bnMod; -BigInteger.prototype.modPowInt = bnModPowInt; - -BigInteger.prototype.clone = bnClone; -BigInteger.prototype.intValue = bnIntValue; -BigInteger.prototype.byteValue = bnByteValue; -BigInteger.prototype.shortValue = bnShortValue; -BigInteger.prototype.signum = bnSigNum; -BigInteger.prototype.toByteArray = bnToByteArray; -BigInteger.prototype.toBuffer = bnToBuffer; -BigInteger.prototype.equals = bnEquals; -BigInteger.prototype.min = bnMin; -BigInteger.prototype.max = bnMax; -BigInteger.prototype.and = bnAnd; -BigInteger.prototype.or = bnOr; -BigInteger.prototype.xor = bnXor; -BigInteger.prototype.andNot = bnAndNot; -BigInteger.prototype.not = bnNot; -BigInteger.prototype.shiftLeft = bnShiftLeft; -BigInteger.prototype.shiftRight = bnShiftRight; -BigInteger.prototype.getLowestSetBit = bnGetLowestSetBit; -BigInteger.prototype.bitCount = bnBitCount; -BigInteger.prototype.testBit = bnTestBit; -BigInteger.prototype.setBit = bnSetBit; -BigInteger.prototype.clearBit = bnClearBit; -BigInteger.prototype.flipBit = bnFlipBit; -BigInteger.prototype.add = bnAdd; -BigInteger.prototype.subtract = bnSubtract; -BigInteger.prototype.multiply = bnMultiply; -BigInteger.prototype.divide = bnDivide; -BigInteger.prototype.remainder = bnRemainder; -BigInteger.prototype.divideAndRemainder = bnDivideAndRemainder; -BigInteger.prototype.modPow = bnModPow; -BigInteger.prototype.modInverse = bnModInverse; -BigInteger.prototype.pow = bnPow; -BigInteger.prototype.gcd = bnGCD; -BigInteger.prototype.isProbablePrime = bnIsProbablePrime; -BigInteger.int2char = int2char; - -// "constants" -BigInteger.ZERO = nbv(0); -BigInteger.ONE = nbv(1); - -// JSBN-specific extension -BigInteger.prototype.square = bnSquare; - -//BigInteger interfaces not implemented in jsbn: - -//BigInteger(int signum, byte[] magnitude) -//double doubleValue() -//float floatValue() -//int hashCode() -//long longValue() -//static BigInteger valueOf(long val) - -module.exports = BigInteger; \ No newline at end of file diff --git a/src/libs/rsa.js b/src/libs/rsa.js deleted file mode 100644 index 158f745..0000000 --- a/src/libs/rsa.js +++ /dev/null @@ -1,316 +0,0 @@ -/* - * RSA Encryption / Decryption with PKCS1 v2 Padding. - * - * Copyright (c) 2003-2005 Tom Wu - * All Rights Reserved. - * - * Permission is hereby granted, free of charge, to any person obtaining - * a copy of this software and associated documentation files (the - * "Software"), to deal in the Software without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, sublicense, and/or sell copies of the Software, and to - * permit persons to whom the Software is furnished to do so, subject to - * the following conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND, - * EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY - * WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. - * - * IN NO EVENT SHALL TOM WU BE LIABLE FOR ANY SPECIAL, INCIDENTAL, - * INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND, OR ANY DAMAGES WHATSOEVER - * RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER OR NOT ADVISED OF - * THE POSSIBILITY OF DAMAGE, AND ON ANY THEORY OF LIABILITY, ARISING OUT - * OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - * - * In addition, the following condition applies: - * - * All redistributions must retain an intact copy of this copyright notice - * and disclaimer. - */ - -/* - * Node.js adaptation - * long message support implementation - * signing/verifying - * - * 2014 rzcoder - */ - -var _ = require('../utils')._; -var crypt = require('crypto'); -var BigInteger = require('./jsbn.js'); -var utils = require('../utils.js'); -var schemes = require('../schemes/schemes.js'); -var encryptEngines = require('../encryptEngines/encryptEngines.js'); - -exports.BigInteger = BigInteger; -module.exports.Key = (function () { - /** - * RSA key constructor - * - * n - modulus - * e - publicExponent - * d - privateExponent - * p - prime1 - * q - prime2 - * dmp1 - exponent1 -- d mod (p1) - * dmq1 - exponent2 -- d mod (q-1) - * coeff - coefficient -- (inverse of q) mod p - */ - function RSAKey() { - this.n = null; - this.e = 0; - this.d = null; - this.p = null; - this.q = null; - this.dmp1 = null; - this.dmq1 = null; - this.coeff = null; - } - - RSAKey.prototype.setOptions = function (options) { - var signingSchemeProvider = schemes[options.signingScheme]; - var encryptionSchemeProvider = schemes[options.encryptionScheme]; - - if (signingSchemeProvider === encryptionSchemeProvider) { - this.signingScheme = this.encryptionScheme = encryptionSchemeProvider.makeScheme(this, options); - } else { - this.encryptionScheme = encryptionSchemeProvider.makeScheme(this, options); - this.signingScheme = signingSchemeProvider.makeScheme(this, options); - } - - this.encryptEngine = encryptEngines.getEngine(this, options); - }; - - /** - * Generate a new random private key B bits long, using public expt E - * @param B - * @param E - */ - RSAKey.prototype.generate = function (B, E) { - var qs = B >> 1; - this.e = parseInt(E, 16); - var ee = new BigInteger(E, 16); - while (true) { - while (true) { - this.p = new BigInteger(B - qs, 1); - if (this.p.subtract(BigInteger.ONE).gcd(ee).compareTo(BigInteger.ONE) === 0 && this.p.isProbablePrime(10)) - break; - } - while (true) { - this.q = new BigInteger(qs, 1); - if (this.q.subtract(BigInteger.ONE).gcd(ee).compareTo(BigInteger.ONE) === 0 && this.q.isProbablePrime(10)) - break; - } - if (this.p.compareTo(this.q) <= 0) { - var t = this.p; - this.p = this.q; - this.q = t; - } - var p1 = this.p.subtract(BigInteger.ONE); - var q1 = this.q.subtract(BigInteger.ONE); - var phi = p1.multiply(q1); - if (phi.gcd(ee).compareTo(BigInteger.ONE) === 0) { - this.n = this.p.multiply(this.q); - if (this.n.bitLength() < B) { - continue; - } - this.d = ee.modInverse(phi); - this.dmp1 = this.d.mod(p1); - this.dmq1 = this.d.mod(q1); - this.coeff = this.q.modInverse(this.p); - break; - } - } - this.$$recalculateCache(); - }; - - /** - * Set the private key fields N, e, d and CRT params from buffers - * - * @param N - * @param E - * @param D - * @param P - * @param Q - * @param DP - * @param DQ - * @param C - */ - RSAKey.prototype.setPrivate = function (N, E, D, P, Q, DP, DQ, C) { - if (N && E && D && N.length > 0 && (_.isNumber(E) || E.length > 0) && D.length > 0) { - this.n = new BigInteger(N); - this.e = _.isNumber(E) ? E : utils.get32IntFromBuffer(E, 0); - this.d = new BigInteger(D); - - if (P && Q && DP && DQ && C) { - this.p = new BigInteger(P); - this.q = new BigInteger(Q); - this.dmp1 = new BigInteger(DP); - this.dmq1 = new BigInteger(DQ); - this.coeff = new BigInteger(C); - } else { - // TODO: re-calculate any missing CRT params - } - this.$$recalculateCache(); - } else { - throw Error("Invalid RSA private key"); - } - }; - - /** - * Set the public key fields N and e from hex strings - * @param N - * @param E - */ - RSAKey.prototype.setPublic = function (N, E) { - if (N && E && N.length > 0 && (_.isNumber(E) || E.length > 0)) { - this.n = new BigInteger(N); - this.e = _.isNumber(E) ? E : utils.get32IntFromBuffer(E, 0); - this.$$recalculateCache(); - } else { - throw Error("Invalid RSA public key"); - } - }; - - /** - * private - * Perform raw private operation on "x": return x^d (mod n) - * - * @param x - * @returns {*} - */ - RSAKey.prototype.$doPrivate = function (x) { - if (this.p || this.q) { - return x.modPow(this.d, this.n); - } - - // TODO: re-calculate any missing CRT params - var xp = x.mod(this.p).modPow(this.dmp1, this.p); - var xq = x.mod(this.q).modPow(this.dmq1, this.q); - - while (xp.compareTo(xq) < 0) { - xp = xp.add(this.p); - } - return xp.subtract(xq).multiply(this.coeff).mod(this.p).multiply(this.q).add(xq); - }; - - /** - * private - * Perform raw public operation on "x": return x^e (mod n) - * - * @param x - * @returns {*} - */ - RSAKey.prototype.$doPublic = function (x) { - return x.modPowInt(this.e, this.n); - }; - - /** - * Return the PKCS#1 RSA encryption of buffer - * @param buffer {Buffer} - * @returns {Buffer} - */ - RSAKey.prototype.encrypt = function (buffer, usePrivate) { - var buffers = []; - var results = []; - var bufferSize = buffer.length; - var buffersCount = Math.ceil(bufferSize / this.maxMessageLength) || 1; // total buffers count for encrypt - var dividedSize = Math.ceil(bufferSize / buffersCount || 1); // each buffer size - - if (buffersCount == 1) { - buffers.push(buffer); - } else { - for (var bufNum = 0; bufNum < buffersCount; bufNum++) { - buffers.push(buffer.slice(bufNum * dividedSize, (bufNum + 1) * dividedSize)); - } - } - - for (var i = 0; i < buffers.length; i++) { - results.push(this.encryptEngine.encrypt(buffers[i], usePrivate)); - } - - return Buffer.concat(results); - }; - - /** - * Return the PKCS#1 RSA decryption of buffer - * @param buffer {Buffer} - * @returns {Buffer} - */ - RSAKey.prototype.decrypt = function (buffer, usePublic) { - if (buffer.length % this.encryptedDataLength > 0) { - throw Error('Incorrect data or key'); - } - - var result = []; - var offset = 0; - var length = 0; - var buffersCount = buffer.length / this.encryptedDataLength; - - for (var i = 0; i < buffersCount; i++) { - offset = i * this.encryptedDataLength; - length = offset + this.encryptedDataLength; - result.push(this.encryptEngine.decrypt(buffer.slice(offset, Math.min(length, buffer.length)), usePublic)); - } - - return Buffer.concat(result); - }; - - RSAKey.prototype.sign = function (buffer) { - return this.signingScheme.sign.apply(this.signingScheme, arguments); - }; - - RSAKey.prototype.verify = function (buffer, signature, signature_encoding) { - return this.signingScheme.verify.apply(this.signingScheme, arguments); - }; - - /** - * Check if key pair contains private key - */ - RSAKey.prototype.isPrivate = function () { - return this.n && this.e && this.d && true || false; - }; - - /** - * Check if key pair contains public key - * @param strict {boolean} - public key only, return false if have private exponent - */ - RSAKey.prototype.isPublic = function (strict) { - return this.n && this.e && !(strict && this.d) || false; - }; - - Object.defineProperty(RSAKey.prototype, 'keySize', { - get: function () { - return this.cache.keyBitLength; - } - }); - - Object.defineProperty(RSAKey.prototype, 'encryptedDataLength', { - get: function () { - return this.cache.keyByteLength; - } - }); - - Object.defineProperty(RSAKey.prototype, 'maxMessageLength', { - get: function () { - return this.encryptionScheme.maxMessageLength(); - } - }); - - /** - * Caching key data - */ - RSAKey.prototype.$$recalculateCache = function () { - this.cache = this.cache || {}; - // Bit & byte length - this.cache.keyBitLength = this.n.bitLength(); - this.cache.keyByteLength = (this.cache.keyBitLength + 6) >> 3; - }; - - return RSAKey; -})(); - diff --git a/src/node-rsa.ts b/src/node-rsa.ts new file mode 100644 index 0000000..7af8714 --- /dev/null +++ b/src/node-rsa.ts @@ -0,0 +1,394 @@ +import { setBigIntegerBackend } from './bigint/big-integer.js'; +import { + fromBase64, + fromLatin1, + fromUtf8, + toBase64, + toHex, + toLatin1, + toUtf8, +} from './crypto/bytes.js'; +import type { CryptoBackend } from './crypto/types.js'; +import { detectAndExport, detectAndImport } from './formats/index.js'; +import { applyOptions, EXPORT_FORMAT_ALIASES, makeDefaultOptions } from './options.js'; +import { type Engine, JsEngine } from './rsa/engine.js'; +import { RSAKey } from './rsa/key.js'; +import { SCHEMES } from './schemes/index.js'; +import type { EncryptionSchemeImpl, SchemeOptions, SignatureScheme } from './schemes/types.js'; +import type { + Data, + Encoding, + Environment, + Format, + FormatComponentsPrivate, + FormatComponentsPublic, + FormatDer, + FormatPem, + Key, + KeyBits, + KeyComponentsPrivate, + KeyComponentsPublic, + NodeRSAGenerateOptions, + NodeRSAOptions, + ResolvedOptions, +} from './types.js'; + +interface SchemeProviderLike { + isEncryption: boolean; + isSignature: boolean; + makeScheme(key: RSAKey, options: SchemeOptions): EncryptionSchemeImpl | SignatureScheme; +} + +interface NodeRSAInternal { + environment: Environment; + backend: CryptoBackend; + /** Optional engine factory (e.g. NodeNativeEngine). Falls back to JsEngine. */ + engineFor?: (key: RSAKey, options: ResolvedOptions) => Engine; + /** + * Optional native key generator (e.g. node:crypto.generateKeyPairSync). + * If absent, NodeRSA.generateKeyPair falls back to the pure-JS + * RSAKey.generate path. + */ + keygenFor?: (key: RSAKey, bits: number, expHex: string) => void; + /** + * Optional override of the default SCHEMES registry. The node bundle + * passes a map with PKCS1 + PSS replaced by node:crypto-backed wrappers. + * Bypassed when the user forces environment:'browser' at runtime so + * setOptions can route back to the JS implementations. + */ + schemes?: Record; +} + +let internal: NodeRSAInternal | undefined; + +/** Called by the platform entry (src/index.node.ts or .browser.ts) at module load. */ +export function bootstrap(config: NodeRSAInternal): void { + internal = config; + setBigIntegerBackend(config.backend); +} + +function getInternal(): NodeRSAInternal { + if (!internal) { + throw new Error( + 'NodeRSA: backend not initialized. Import the package via its main entry, not by deep-importing internals.', + ); + } + return internal; +} + +export class NodeRSA { + $options: ResolvedOptions; + keyPair: RSAKey; + private engine: Engine | null = null; + private $cache: Record = {}; + + constructor(key?: KeyBits); + constructor(key: Key, format?: Format, options?: NodeRSAOptions); + constructor(key: null | undefined, format?: NodeRSAOptions); + constructor( + key?: Key | KeyBits | NodeRSAGenerateOptions | null, + format?: Format | string | NodeRSAOptions, + options?: NodeRSAOptions, + ) { + let opts: NodeRSAOptions | undefined; + let fmt: string | undefined; + if (typeof format === 'object' && format !== null) { + opts = format; + fmt = undefined; + } else { + fmt = format as string | undefined; + opts = options; + } + + const env = getInternal().environment; + this.$options = makeDefaultOptions(env); + this.keyPair = new RSAKey(); + + // Apply user options BEFORE touching BigInteger so settings like + // `bigIntImpl` take effect during importKey/generateKeyPair. The keyPair + // is still empty here, so rewireScheme is a no-op-ish wire-up and safe. + if (opts) { + applyOptions(this.$options, opts); + this.rewireScheme(); + } + + if (key instanceof Uint8Array || typeof key === 'string') { + this.importKey(key as Key, fmt as Format | undefined); + } else if (key && typeof key === 'object') { + const gen = key as NodeRSAGenerateOptions; + this.generateKeyPair(gen.b, gen.e); + } + + if (!opts && !key) this.rewireScheme(); + } + + setOptions(options: NodeRSAOptions): this { + if ( + options.bigIntImpl && + options.bigIntImpl !== this.$options.bigIntImpl && + this.keyPair.n != null + ) { + // Existing BigInteger components carry the old impl's class identity. + // Switching now would mix impls inside one key — broken arithmetic. + throw new Error( + 'NodeRSA: bigIntImpl can only be set on a fresh instance (before importKey / generateKeyPair). Pass it in the constructor options, or set it before importing.', + ); + } + applyOptions(this.$options, options); + this.rewireScheme(); + return this; + } + + generateKeyPair(bits = 2048, exp = 65537): this { + if (bits % 8 !== 0) throw new Error('Key size must be a multiple of 8.'); + const cfg = getInternal(); + const expHex = exp.toString(16); + // Native fast-path (node bundle wires keygenFor → node:crypto.generateKeyPairSync, + // ~20–50× faster than RSAKey.generate for keys ≥ 2048 bits). The browser bundle + // doesn't wire it and falls back to the pure-JS path. + if (cfg.keygenFor && this.$options.environment !== 'browser') { + cfg.keygenFor(this.keyPair, bits, expHex); + } else { + this.keyPair.generate(bits, expHex); + } + this.$cache = {}; + this.rewireScheme(); + return this; + } + + importKey(keyData: Key, format?: Format | string): this; + importKey(keyData: Uint8Array | string | object, format?: string): this { + if (keyData == null || (typeof keyData === 'string' && keyData.length === 0)) { + throw new Error('Empty key given'); + } + const resolvedFormat = format ? (EXPORT_FORMAT_ALIASES[format] ?? format) : format; + const imported = detectAndImport(this.keyPair, keyData, resolvedFormat); + if (!imported && resolvedFormat === undefined) { + throw new Error('Key format must be specified'); + } + this.$cache = {}; + this.rewireScheme(); + return this; + } + + exportKey(format?: FormatPem): string; + exportKey(format: FormatDer): Uint8Array; + exportKey(format: FormatComponentsPrivate): KeyComponentsPrivate; + exportKey(format: FormatComponentsPublic): KeyComponentsPublic; + exportKey(format?: string): Uint8Array | string | object; + exportKey(format = 'private'): Uint8Array | string | object { + const resolved = EXPORT_FORMAT_ALIASES[format] ?? format; + if (!this.$cache[resolved]) { + const exported = detectAndExport(this.keyPair, resolved); + if (exported === undefined) throw new Error('Export failed'); + this.$cache[resolved] = exported; + } + return this.$cache[resolved] as Uint8Array | string | object; + } + + isPrivate(): boolean { + return this.keyPair.isPrivate(); + } + + isPublic(strict?: boolean): boolean { + return this.keyPair.isPublic(strict); + } + + isEmpty(): boolean { + return !(this.keyPair.n || this.keyPair.e || this.keyPair.d); + } + + getKeySize(): number { + return this.keyPair.keySize; + } + + getMaxMessageSize(): number { + return this.keyPair.maxMessageLength; + } + + encrypt(data: Data | Uint8Array, encoding?: 'buffer', sourceEncoding?: Encoding): Uint8Array; + encrypt(data: Data | Uint8Array, encoding: Encoding, sourceEncoding?: Encoding): string; + encrypt(buffer: unknown, encoding?: Encoding, sourceEncoding?: string): Uint8Array | string { + return this.$$encryptKey(false, buffer, encoding, sourceEncoding); + } + + decrypt(data: Uint8Array | string, encoding?: 'buffer'): Uint8Array; + decrypt(data: Uint8Array | string, encoding: Encoding): string; + decrypt(data: Uint8Array | string, encoding: 'json'): T; + decrypt(buffer: Uint8Array | string, encoding?: Encoding | 'json'): Uint8Array | string | object { + return this.$$decryptKey(false, buffer, encoding as Encoding); + } + + encryptPrivate( + data: Data | Uint8Array, + encoding?: 'buffer', + sourceEncoding?: Encoding, + ): Uint8Array; + encryptPrivate(data: Data | Uint8Array, encoding: Encoding, sourceEncoding?: Encoding): string; + encryptPrivate( + buffer: unknown, + encoding?: Encoding, + sourceEncoding?: string, + ): Uint8Array | string { + return this.$$encryptKey(true, buffer, encoding, sourceEncoding); + } + + decryptPublic(data: Uint8Array | string, encoding?: 'buffer'): Uint8Array; + decryptPublic(data: Uint8Array | string, encoding: Encoding): string; + decryptPublic(data: Uint8Array | string, encoding: 'json'): T; + decryptPublic( + buffer: Uint8Array | string, + encoding?: Encoding | 'json', + ): Uint8Array | string | object { + return this.$$decryptKey(true, buffer, encoding as Encoding); + } + + sign(data: Data | Uint8Array, encoding?: 'buffer', sourceEncoding?: Encoding): Uint8Array; + sign(data: Data | Uint8Array, encoding: Encoding, sourceEncoding?: Encoding): string; + sign(buffer: unknown, encoding?: Encoding, sourceEncoding?: string): Uint8Array | string { + if (!this.isPrivate()) throw new Error('This is not private key'); + const data = this.$getDataForEncrypt(buffer, sourceEncoding); + const res = this.keyPair.signingScheme.sign(data); + return encoding && encoding !== 'buffer' ? encodeBytes(res, encoding) : res; + } + + verify(data: Data | Uint8Array, signature: Uint8Array, sourceEncoding?: Encoding): boolean; + verify( + data: Data | Uint8Array, + signature: string, + sourceEncoding: Encoding | undefined, + signatureEncoding: Encoding, + ): boolean; + verify( + buffer: unknown, + signature: Uint8Array | string, + sourceEncoding?: string, + signatureEncoding?: string, + ): boolean { + if (!this.isPublic()) throw new Error('This is not public key'); + const data = this.$getDataForEncrypt(buffer, sourceEncoding); + const sig = + typeof signature === 'string' ? decodeBytes(signature, signatureEncoding) : signature; + return this.keyPair.signingScheme.verify(data, sig); + } + + // internals + $$encryptKey( + usePrivate: boolean, + buffer: unknown, + encoding?: Encoding, + sourceEncoding?: string, + ): Uint8Array | string { + try { + const data = this.$getDataForEncrypt(buffer, sourceEncoding); + const res = this.ensureEngine().encrypt(data, usePrivate); + return encoding && encoding !== 'buffer' ? encodeBytes(res, encoding) : res; + } catch { + throw new Error('Error during encryption'); + } + } + + $$decryptKey( + usePublic: boolean, + buffer: Uint8Array | string, + encoding?: Encoding, + ): Uint8Array | string | object { + try { + const bytes = typeof buffer === 'string' ? fromBase64(buffer) : buffer; + const res = this.ensureEngine().decrypt(bytes, usePublic); + return this.$getDecryptedData(res, encoding); + } catch { + throw new Error('Error during decryption'); + } + } + + $getDataForEncrypt(buffer: unknown, encoding?: string): Uint8Array { + if (typeof buffer === 'string') { + return encoding && encoding !== 'utf8' ? decodeBytes(buffer, encoding) : fromUtf8(buffer); + } + if (typeof buffer === 'number') return fromUtf8(String(buffer)); + if (buffer instanceof Uint8Array) return buffer; + if (buffer !== null && typeof buffer === 'object') return fromUtf8(JSON.stringify(buffer)); + throw new Error('Unexpected data type'); + } + + $getDecryptedData(bytes: Uint8Array, encoding?: Encoding | 'json'): Uint8Array | string | object { + const enc = encoding ?? 'buffer'; + if (enc === 'buffer') return bytes; + if (enc === 'json') return JSON.parse(toUtf8(bytes)); + return encodeBytes(bytes, enc); + } + + private rewireScheme(): void { + const cfg = getInternal(); + const opts: SchemeOptions = { + signingScheme: this.$options.signingScheme, + encryptionScheme: this.$options.encryptionScheme, + signingSchemeOptions: this.$options.signingSchemeOptions, + encryptionSchemeOptions: this.$options.encryptionSchemeOptions, + environment: this.$options.environment, + backend: cfg.backend, + }; + // When the user forces environment:'browser' on the node bundle, revert + // to the pure-JS SCHEMES so signing also goes through the JS path — + // otherwise sign/verify would still use node:crypto while the engine + // uses JsEngine, defeating the override. + const forcedJs = this.$options.environment === 'browser'; + const schemes = forcedJs ? SCHEMES : (cfg.schemes ?? SCHEMES); + this.keyPair.setOptions(opts, schemes); + this.engine = null; + } + + private ensureEngine(): Engine { + if (this.engine) return this.engine; + const cfg = getInternal(); + const forcedJs = this.$options.environment === 'browser'; + if (!forcedJs && cfg.engineFor) { + this.engine = cfg.engineFor(this.keyPair, this.$options); + } else { + this.engine = new JsEngine(this.keyPair); + } + return this.engine; + } +} + +function encodeBytes(bytes: Uint8Array, encoding: string): string { + switch (encoding) { + case 'hex': + return toHex(bytes); + case 'base64': + return toBase64(bytes); + case 'utf8': + return toUtf8(bytes); + case 'binary': + case 'latin1': + return toLatin1(bytes); + default: + // Best-effort: treat as base64 fallback + return toBase64(bytes); + } +} + +function decodeBytes(s: string, encoding?: string): Uint8Array { + switch (encoding) { + case 'hex': { + if (s.length % 2 !== 0) throw new Error('Invalid hex string'); + const out = new Uint8Array(s.length / 2); + for (let i = 0; i < out.length; i++) + out[i] = Number.parseInt(s.substring(i * 2, i * 2 + 2), 16); + return out; + } + case 'utf8': + return fromUtf8(s); + case 'binary': + case 'latin1': + return fromLatin1(s); + case undefined: + case null: + case 'buffer': + case 'base64': + return fromBase64(s); + default: + return fromBase64(s); + } +} diff --git a/src/options.ts b/src/options.ts new file mode 100644 index 0000000..1fc19bc --- /dev/null +++ b/src/options.ts @@ -0,0 +1,160 @@ +import { setBigIntegerImpl } from './bigint/big-integer.js'; +import type { HashingAlgorithm } from './crypto/types.js'; +import { SCHEMES } from './schemes/index.js'; +import type { EncryptionSchemeOptions, SigningSchemeOptions } from './schemes/types.js'; +import type { + EncryptionScheme, + Environment, + NodeRSAOptions, + ResolvedOptions, + SigningScheme, +} from './types.js'; + +const NODE_HASHES: ReadonlyArray = [ + 'md4', + 'md5', + 'ripemd160', + 'sha1', + 'sha224', + 'sha256', + 'sha384', + 'sha512', +]; + +// Legacy v1 exposed four environment values: 'node', 'browser', plus +// 'node10' and 'iojs' as Node aliases (each retaining the same hash list). +// v2 treats 'node10' and 'iojs' as 'node'-equivalents for the hash whitelist +// so that any user setOptions({environment:'iojs'}) keeps working. +export const SUPPORTED_HASH_ALGORITHMS: Record> = { + node: NODE_HASHES, + node10: NODE_HASHES, + iojs: NODE_HASHES, + browser: ['md5', 'ripemd160', 'sha1', 'sha256', 'sha512'], +}; + +function allowedHashes(env: string): ReadonlyArray { + return SUPPORTED_HASH_ALGORITHMS[env] ?? NODE_HASHES; +} + +export const DEFAULT_ENCRYPTION_SCHEME: EncryptionScheme = 'pkcs1_oaep'; +// PSS (RSASSA-PSS) is the modern best-practice signing scheme — +// probabilistic, with a provable security reduction. Callers needing the +// v1-era PKCS#1 v1.5 default must set `signingScheme: 'pkcs1'` explicitly. +export const DEFAULT_SIGNING_SCHEME: SigningScheme = 'pss'; + +export const EXPORT_FORMAT_ALIASES: Record = { + private: 'pkcs1-private-pem', + 'private-der': 'pkcs1-private-der', + public: 'pkcs8-public-pem', + 'public-der': 'pkcs8-public-der', +}; + +export function makeDefaultOptions(environment: Environment): ResolvedOptions { + return { + signingScheme: DEFAULT_SIGNING_SCHEME, + signingSchemeOptions: { hash: 'sha256' }, + encryptionScheme: DEFAULT_ENCRYPTION_SCHEME, + encryptionSchemeOptions: { hash: 'sha1' }, + environment, + // Mirrors the per-bundle default flipped by the entry module + // (index.browser.ts switches to 'native' at load; index.node.ts leaves + // 'jsbn'). Stored on ResolvedOptions so callers can read the active + // setting back off the NodeRSA instance. + bigIntImpl: environment === 'browser' ? 'native' : 'jsbn', + }; +} + +let warnedEnvironment = false; + +/** + * Apply user-supplied options on top of the resolved defaults, mutating + * `target` in-place. Mirrors v1's setOptions string-parsing rules: + * + * - "pkcs1" → scheme = pkcs1, no hash override + * - "sha256" → scheme = default (pkcs1), hash = sha256 + * - "pss-sha512" → scheme = pss, hash = sha512 + * - { scheme, hash, ... } → object form, scheme defaults to default + */ +export function applyOptions(target: ResolvedOptions, options: NodeRSAOptions): void { + if (options.bigIntImpl) { + // Side effect: globally swap the BigInteger implementation. The selector + // module guards the fallback for runtimes without globalThis.BigInt. + setBigIntegerImpl(options.bigIntImpl); + target.bigIntImpl = options.bigIntImpl; + } + + if (options.environment) { + if (options.environment !== target.environment && !warnedEnvironment) { + // eslint-disable-next-line no-console + console.warn( + 'NodeRSA: setOptions({environment}) is deprecated. Build-time platform conditions decide the runtime; the option now only forces the pure-JS engine path.', + ); + warnedEnvironment = true; + } + target.environment = options.environment; + } + + if (options.signingScheme !== undefined) { + if (typeof options.signingScheme === 'string') { + const parts = options.signingScheme.toLowerCase().split('-'); + if (parts.length === 1) { + if (NODE_HASHES.includes(parts[0] as HashingAlgorithm)) { + target.signingSchemeOptions = { hash: parts[0] as HashingAlgorithm }; + target.signingScheme = DEFAULT_SIGNING_SCHEME; + } else { + target.signingScheme = parts[0] as SigningScheme; + target.signingSchemeOptions = {}; + } + } else { + target.signingScheme = parts[0] as SigningScheme; + target.signingSchemeOptions = { hash: parts[1] as HashingAlgorithm }; + } + } else { + const obj = options.signingScheme; + target.signingScheme = (obj.scheme ?? DEFAULT_SIGNING_SCHEME) as SigningScheme; + const { scheme: _scheme, ...rest } = obj; + target.signingSchemeOptions = rest as SigningSchemeOptions; + } + + if (!SCHEMES[target.signingScheme]?.isSignature) { + throw new Error('Unsupported signing scheme'); + } + if ( + target.signingSchemeOptions.hash && + !allowedHashes(target.environment).includes(target.signingSchemeOptions.hash) + ) { + throw new Error(`Unsupported hashing algorithm for ${target.environment} environment`); + } + if ( + target.signingSchemeOptions.hash && + (target.signingSchemeOptions.hash === 'md4' || target.signingSchemeOptions.hash === 'md5') + ) { + // eslint-disable-next-line no-console + console.warn( + `node-rsa: ${target.signingSchemeOptions.hash} is cryptographically broken for signatures; use sha256 or stronger`, + ); + } + } + + if (options.encryptionScheme !== undefined) { + if (typeof options.encryptionScheme === 'string') { + target.encryptionScheme = options.encryptionScheme.toLowerCase() as EncryptionScheme; + target.encryptionSchemeOptions = {}; + } else { + const obj = options.encryptionScheme; + target.encryptionScheme = (obj.scheme ?? DEFAULT_ENCRYPTION_SCHEME) as EncryptionScheme; + const { scheme: _scheme, ...rest } = obj; + target.encryptionSchemeOptions = rest as EncryptionSchemeOptions; + } + + if (!SCHEMES[target.encryptionScheme]?.isEncryption) { + throw new Error('Unsupported encryption scheme'); + } + if ( + target.encryptionSchemeOptions.hash && + !allowedHashes(target.environment).includes(target.encryptionSchemeOptions.hash) + ) { + throw new Error(`Unsupported hashing algorithm for ${target.environment} environment`); + } + } +} diff --git a/src/rsa/engine.ts b/src/rsa/engine.ts new file mode 100644 index 0000000..edab4a9 --- /dev/null +++ b/src/rsa/engine.ts @@ -0,0 +1,98 @@ +import { concat } from '../crypto/bytes.js'; +import { pkcs1Scheme as pkcs1Provider } from '../schemes/pkcs1.js'; +import type { EncryptionSchemeImpl, SchemeOptions, SignatureScheme } from '../schemes/types.js'; +import type { RSAKey } from './key.js'; + +/** + * Engine handles full encrypt/decrypt for arbitrarily-long buffers by + * chunking, applying the encryption scheme's padding, and invoking the + * RSA primitive (key.$doPublic / $doPrivate). + * + * Type-1 path (encryptPrivate, decryptPublic) is *always* PKCS#1 v1.5, + * even when the configured encryptionScheme is OAEP. + */ +export interface Engine { + /** + * Pad and encrypt `buffer`, splitting into key-size chunks as needed. + * `usePrivate=true` selects the "sign-with-PKCS#1-type-1" path (always + * PKCS#1 v1.5, regardless of the configured encryption scheme). + */ + encrypt(buffer: Uint8Array, usePrivate?: boolean): Uint8Array; + /** + * Decrypt and unpad. `usePublic=true` mirrors `encrypt`'s type-1 path — + * verifies a public-decryptable PKCS#1 v1.5 message. Throws on length + * mismatch or invalid padding. + */ + decrypt(buffer: Uint8Array, usePublic?: boolean): Uint8Array; +} + +/** Pure-JS RSA encrypt/decrypt — runs the primitive via `RSAKey.$doPublic`/`$doPrivate`. */ +export class JsEngine implements Engine { + /** Always a PKCS#1 v1.5 scheme — used for usePrivate / usePublic paths. */ + private readonly pkcs1: EncryptionSchemeImpl; + + constructor(private readonly key: RSAKey) { + this.pkcs1 = pkcs1Provider.makeScheme(key, key.options) as EncryptionSchemeImpl & + SignatureScheme; + } + + encrypt(buffer: Uint8Array, usePrivate = false): Uint8Array { + const max = this.key.maxMessageLength; + if (max <= 0) throw new Error('Engine: key not initialised'); + const buffersCount = Math.ceil(buffer.length / max) || 1; + const dividedSize = Math.ceil(buffer.length / buffersCount) || 1; + + const chunks: Uint8Array[] = []; + if (buffersCount === 1) { + chunks.push(buffer); + } else { + for (let i = 0; i < buffersCount; i++) { + chunks.push(buffer.subarray(i * dividedSize, (i + 1) * dividedSize)); + } + } + + const out: Uint8Array[] = []; + for (const chunk of chunks) { + const padded = usePrivate + ? this.pkcs1.encPad(chunk, { type: 1 }) + : this.key.encryptionScheme.encPad(chunk); + const bi = new this.key.BI(padded); + const result = usePrivate ? this.key.$doPrivate(bi) : this.key.$doPublic(bi); + const bytes = result.toBuffer(this.key.encryptedDataLength); + if (!bytes) throw new Error('Engine: RSA primitive returned oversize integer'); + out.push(bytes); + } + return concat(...out); + } + + decrypt(buffer: Uint8Array, usePublic = false): Uint8Array { + const chunkLen = this.key.encryptedDataLength; + if (buffer.length % chunkLen !== 0) { + throw new Error('Incorrect data or key'); + } + const count = buffer.length / chunkLen; + const parts: Uint8Array[] = []; + let bad = 0; + + for (let i = 0; i < count; i++) { + const off = i * chunkLen; + const ct = buffer.subarray(off, off + chunkLen); + const bi = new this.key.BI(ct); + const result = usePublic ? this.key.$doPublic(bi) : this.key.$doPrivate(bi); + const padded = result.toBuffer(chunkLen); + if (!padded) throw new Error('Engine: RSA primitive returned oversize integer'); + const unpadded = usePublic + ? this.pkcs1.encUnPad(padded, { type: 1 }) + : this.key.encryptionScheme.encUnPad(padded); + // Always perform equivalent work regardless of padding validity + // to prevent timing side-channels (Bleichenbacher-style attacks). + parts.push(unpadded ?? padded.subarray(0, 0)); + bad |= unpadded ? 0 : 1; + } + if (bad) throw new Error('Decryption failed'); + return concat(...parts); + } +} + +// Re-export type for the SchemeOptions import that's used elsewhere +export type { SchemeOptions }; diff --git a/src/rsa/key.ts b/src/rsa/key.ts new file mode 100644 index 0000000..6b036bc --- /dev/null +++ b/src/rsa/key.ts @@ -0,0 +1,401 @@ +import { BigInteger } from '../bigint/big-integer.js'; +import type { CryptoBackend } from '../crypto/types.js'; +import type { EncryptionSchemeImpl, SchemeOptions, SignatureScheme } from '../schemes/types.js'; + +// One-shot guard so repeated small-key calls don't spam stderr. +let warnedSmallKey = false; + +/** + * Asymmetric RSA key (public or private). + * + * Field semantics (RFC 3447): + * n — modulus + * e — public exponent + * d — private exponent + * p, q — prime factors of n (n = p * q) + * dmp1 — d mod (p - 1) + * dmq1 — d mod (q - 1) + * coeff — (q^-1) mod p, used by CRT decryption + */ +export class RSAKey { + n: BigInteger | null = null; + e = 0; + d: BigInteger | null = null; + p: BigInteger | null = null; + q: BigInteger | null = null; + dmp1: BigInteger | null = null; + dmq1: BigInteger | null = null; + coeff: BigInteger | null = null; + + // Cached per-update key metrics. + cache: { keyBitLength: number; keyByteLength: number } = { + keyBitLength: 0, + keyByteLength: 0, + }; + + // Scheme bindings — populated by setOptions(). + encryptionScheme!: EncryptionSchemeImpl; + signingScheme!: SignatureScheme; + options!: SchemeOptions; + + /** OpenSSH key comment field (preserved across import/export). */ + sshcomment?: string; + + /** + * BigInteger constructor that owns this key's components. Read off + * `n.constructor` so a later `setBigIntegerImpl()` swap by another + * NodeRSA instance can't corrupt operations on this key — fresh + * BigIntegers spawned during sign/encrypt/blinding stay the same class + * as `n`, `d`, `p`, `q` etc. + */ + get BI(): typeof BigInteger { + if (!this.n) throw new Error('RSAKey: no key components'); + return this.n.constructor as typeof BigInteger; + } + + /** + * Bind encryption + signing scheme instances to this key. If both schemes + * resolve to the same provider (PKCS#1 v1.5 covers both), one instance is + * shared so internal padding state stays consistent. Throws on unknown + * scheme names. + */ + setOptions( + options: SchemeOptions, + schemes: Record< + string, + { makeScheme(key: RSAKey, opts: SchemeOptions): EncryptionSchemeImpl | SignatureScheme } + >, + ): void { + this.options = options; + const sigProvider = schemes[options.signingScheme]; + const encProvider = schemes[options.encryptionScheme]; + if (!sigProvider) throw new Error(`Unknown signing scheme: ${options.signingScheme}`); + if (!encProvider) throw new Error(`Unknown encryption scheme: ${options.encryptionScheme}`); + + if (sigProvider === encProvider) { + const scheme = sigProvider.makeScheme(this, options) as EncryptionSchemeImpl & + SignatureScheme; + this.signingScheme = scheme; + this.encryptionScheme = scheme; + } else { + this.encryptionScheme = encProvider.makeScheme(this, options) as EncryptionSchemeImpl; + this.signingScheme = sigProvider.makeScheme(this, options) as SignatureScheme; + } + } + + /** + * Generate a fresh `B`-bit private key with public exponent E (hex string). + * Matches v1's algorithm and RNG call pattern exactly. + */ + generate(B: number, E: string): void { + if (B < 512) { + throw new Error( + `Key size ${B} bits is cryptographically broken (< 512); refusing to generate`, + ); + } + if (B < 2048 && !warnedSmallKey) { + warnedSmallKey = true; + // Below NIST SP 800-56B §6.1.6.2's 2048-bit minimum. + // eslint-disable-next-line no-console + console.warn( + `node-rsa: generating ${B}-bit RSA key — below NIST SP 800-56B §6.1.6.2 minimum (2048 bits); not recommended for production`, + ); + } + const qs = B >> 1; + this.e = Number.parseInt(E, 16); + const ee = new BigInteger(E, 16); + // FIPS 186-4 Table C.3 Miller-Rabin minimums by half-modulus bit length. + const mrRounds = B >= 4096 ? 16 : B >= 3072 ? 28 : 40; + // FIPS 186-4 §B.3.6 Fermat-factoring defence: require |p − q| > 2^(B/2 − 100). + // With CSPRNG primes the rejection rate is ≈ 2⁻¹⁰⁰ per pair. + const minPQDiff = BigInteger.ONE.shiftLeft((B >> 1) - 100); + while (true) { + while (true) { + // `BigInteger(bits, 1)` is fromNumber's sequential prime search with + // one Miller-Rabin round per candidate — combined with trial division + // by 168 small primes, fast enough for the sieve. The outer + // isProbablePrime(mrRounds) below does the strong validation. + this.p = new BigInteger(B - qs, 1); + if ( + this.p.subtract(BigInteger.ONE).gcd(ee).compareTo(BigInteger.ONE) === 0 && + this.p.isProbablePrime(mrRounds) + ) { + break; + } + } + while (true) { + this.q = new BigInteger(qs, 1); + if ( + this.q.subtract(BigInteger.ONE).gcd(ee).compareTo(BigInteger.ONE) === 0 && + this.q.isProbablePrime(mrRounds) + ) { + break; + } + } + if (this.p.compareTo(this.q) <= 0) { + const t = this.p; + this.p = this.q; + this.q = t; + } + // Regenerate the pair if p and q are too close (Fermat defence). + if (this.p.subtract(this.q).compareTo(minPQDiff) < 0) continue; + const p1 = this.p.subtract(BigInteger.ONE); + const q1 = this.q.subtract(BigInteger.ONE); + const phi = p1.multiply(q1); + if (phi.gcd(ee).compareTo(BigInteger.ONE) === 0) { + this.n = this.p.multiply(this.q); + if (this.n.bitLength() < B) continue; + this.d = ee.modInverse(phi); + this.dmp1 = this.d.mod(p1); + this.dmq1 = this.d.mod(q1); + this.coeff = this.q.modInverse(this.p); + break; + } + } + this.recalculateCache(); + } + + /** + * Install private-key components (raw big-endian bytes; E may be a number). + * If any CRT field (P/Q/DP/DQ/C) is omitted the key works without CRT — + * slower decrypt but valid. Throws if N/E/D are missing or if CRT fields + * are present but mathematically inconsistent (Boneh-DeMillo-Lipton + * fault-attack guard). + */ + setPrivate( + N: Uint8Array, + E: number | Uint8Array, + D: Uint8Array, + P?: Uint8Array, + Q?: Uint8Array, + DP?: Uint8Array, + DQ?: Uint8Array, + C?: Uint8Array, + ): void { + if (!N || N.length === 0) throw new Error('Invalid RSA private key'); + if (typeof E !== 'number' && (!E || E.length === 0)) throw new Error('Invalid RSA private key'); + if (!D || D.length === 0) throw new Error('Invalid RSA private key'); + + this.n = new BigInteger(N); + this.e = typeof E === 'number' ? E : readBigEndianUInt(E); + this.d = new BigInteger(D); + + if (P && Q && DP && DQ && C) { + this.p = new BigInteger(P); + this.q = new BigInteger(Q); + this.dmp1 = new BigInteger(DP); + this.dmq1 = new BigInteger(DQ); + this.coeff = new BigInteger(C); + } + this.validateExponent(); + this.validatePrivateConsistency(); + this.recalculateCache(); + } + + /** Install public-key components (raw big-endian bytes; E may be a number). Throws if N/E are missing or E is invalid. */ + setPublic(N: Uint8Array, E: number | Uint8Array): void { + if (!N || N.length === 0) throw new Error('Invalid RSA public key'); + if (typeof E !== 'number' && (!E || E.length === 0)) throw new Error('Invalid RSA public key'); + + this.n = new BigInteger(N); + this.e = typeof E === 'number' ? E : readBigEndianUInt(E); + this.validateExponent(); + this.recalculateCache(); + } + + /** + * RFC 8017 §3.1 requires 1 < e < n with e odd. e=1 makes ciphertext == + * plaintext; even e breaks RSA invertibility entirely. The e < n side + * is implicit (n ≥ 2^512 ≫ any JS-number-encodable e). + */ + private validateExponent(): void { + if (this.e <= 1) { + throw new Error('Invalid RSA exponent: e must be > 1'); + } + if ((this.e & 1) === 0) { + throw new Error('Invalid RSA exponent: e must be odd'); + } + } + + /** + * Cross-check CRT invariants for an imported private key. Inconsistent + * components (n ≠ p·q, mismatched dp/dq, bad coeff) don't just produce + * garbage on decrypt — they enable Boneh-DeMillo-Lipton fault attacks + * where a single faulted signature reveals gcd(s_correct − s_faulted, n) + * and factors n. Skipped when CRT components are absent (basic n, e, d + * key still works, just without CRT). + */ + private validatePrivateConsistency(): void { + if (!this.n || !this.d || !this.p || !this.q || !this.dmp1 || !this.dmq1 || !this.coeff) { + return; + } + if (this.p.multiply(this.q).compareTo(this.n) !== 0) { + throw new Error('RSA private key inconsistent: n ≠ p × q'); + } + const p1 = this.p.subtract(BigInteger.ONE); + const q1 = this.q.subtract(BigInteger.ONE); + if (this.d.mod(p1).compareTo(this.dmp1) !== 0) { + throw new Error('RSA private key inconsistent: dp ≠ d mod (p − 1)'); + } + if (this.d.mod(q1).compareTo(this.dmq1) !== 0) { + throw new Error('RSA private key inconsistent: dq ≠ d mod (q − 1)'); + } + if (this.q.multiply(this.coeff).mod(this.p).compareTo(BigInteger.ONE) !== 0) { + throw new Error('RSA private key inconsistent: q × coeff ≢ 1 (mod p)'); + } + const eBig = new BigInteger(this.e.toString(16), 16); + if (eBig.multiply(this.dmp1).mod(p1).compareTo(BigInteger.ONE) !== 0) { + throw new Error('RSA private key inconsistent: e × dp ≢ 1 (mod p − 1)'); + } + if (eBig.multiply(this.dmq1).mod(q1).compareTo(BigInteger.ONE) !== 0) { + throw new Error('RSA private key inconsistent: e × dq ≢ 1 (mod q − 1)'); + } + } + + /** x^d mod n, using CRT if p/q are available, otherwise direct. */ + $doPrivate(x: BigInteger): BigInteger { + if (!this.n || !this.d) throw new Error('No private key'); + // RFC 8017 §5.1.2 / §3.2 mandate inputs in [0, n-1]. Without this + // check, ciphertext c and c+kn would decrypt the same (malleability) + // and negative intermediates would corrupt CRT recombination. + if (x.signum() < 0 || x.compareTo(this.n) >= 0) { + throw new Error('RSA: input out of range (must be 0 ≤ x < n)'); + } + + // Base blinding (Kocher 1996): the variable-time modPow leaks d-bits + // unless its input is masked from the attacker. Pre-multiply by r^e, + // post-multiply by r^-1, with r ← random coprime to n: + // (x · r^e)^d = x^d · r^(e·d) = x^d · r (mod n) + // then × r^-1 mod n = x^d mod n + const blinding = this.makeBlinding(); + const inputX = blinding ? x.multiply(blinding.re).mod(this.n) : x; + + let result: BigInteger; + if (!this.p || !this.q || !this.dmp1 || !this.dmq1 || !this.coeff) { + result = inputX.modPow(this.d, this.n); + } else { + const xp = inputX.mod(this.p).modPow(this.dmp1, this.p); + const xq = inputX.mod(this.q).modPow(this.dmq1, this.q); + // Garner recombination without a data-dependent `while (xp < xq)` + // loop: BigInteger.mod normalises any negative dividend to [0, p). + result = xp.subtract(xq).multiply(this.coeff).mod(this.p).multiply(this.q).add(xq); + } + + if (blinding) { + result = result.multiply(blinding.rInv).mod(this.n); + } + return result; + } + + /** + * Produce a fresh blinding pair (r^e mod n, r^-1 mod n) for one private + * operation. Returns null only in the astronomically rare case that the + * RNG keeps producing r with gcd(r, n) ≠ 1 — probability ≈ 2/√n per + * attempt; 10 attempts is overkill safety. + * + * Returns null also if there's no backend yet (e.g., key without + * setOptions() — only happens in some test setups). + */ + private makeBlinding(): { re: BigInteger; rInv: BigInteger } | null { + if (!this.n || !this.options) return null; + const n = this.n; + const BI = this.BI; + const byteLen = ((n.bitLength() + 7) >> 3) + 1; + const two = new BI(Uint8Array.of(2)); + const nMinus3 = n.subtract(BI.ONE).subtract(two); // range size for [2, n-2] + + for (let attempt = 0; attempt < 10; attempt++) { + const rb = this.options.backend.randomBytes(byteLen); + const r = new BI(rb).mod(nMinus3).add(two); + const rInv = r.modInverse(n); + if (rInv.signum() === 0) continue; // gcd(r, n) ≠ 1; retry + const re = r.modPowInt(this.e, n); + return { re, rInv }; + } + return null; + } + + /** x^e mod n. */ + $doPublic(x: BigInteger): BigInteger { + if (!this.n) throw new Error('No public key'); + // RFC 8017 §5.2.2 / §3.2 mandate inputs in [0, n-1]; rejects s ≥ n + // on verify and m ≥ n on encrypt. + if (x.signum() < 0 || x.compareTo(this.n) >= 0) { + throw new Error('RSA: input out of range (must be 0 ≤ x < n)'); + } + return x.modPowInt(this.e, this.n); + } + + /** True iff `d` is loaded (n, e implied). */ + isPrivate(): boolean { + return !!(this.n && this.e && this.d); + } + + /** True iff `n` and `e` are set. With `strict=true` additionally requires `d` to be absent. */ + isPublic(strict?: boolean): boolean { + if (!this.n || !this.e) return false; + if (strict && this.d) return false; + return true; + } + + /** Modulus size in bits (0 if no key loaded). */ + get keySize(): number { + return this.cache.keyBitLength; + } + + /** Ciphertext block size in bytes. */ + get encryptedDataLength(): number { + return this.cache.keyByteLength; + } + + /** Largest single-chunk plaintext the configured encryption scheme will accept. */ + get maxMessageLength(): number { + return this.encryptionScheme.maxMessageLength(); + } + + /** Recompute cached key-size metrics. */ + recalculateCache(): void { + if (!this.n) { + this.cache = { keyBitLength: 0, keyByteLength: 0 }; + return; + } + const keyBitLength = this.n.bitLength(); + this.cache = { + keyBitLength, + keyByteLength: (keyBitLength + 6) >> 3, + }; + } + + /** + * Clear all key material from this instance. Call when the key is no + * longer needed to reduce the window in which private components are + * reachable from the JS heap (heap snapshots, core dumps, swap). + * + * JavaScript has no guaranteed deterministic memory zeroing — GC-managed + * BigInteger internals may linger until collected. This method removes + * references as early as possible, which is the strongest guarantee the + * language offers. + */ + destroy(): void { + this.n = null; + this.e = 0; + this.d = null; + this.p = null; + this.q = null; + this.dmp1 = null; + this.dmq1 = null; + this.coeff = null; + this.cache = { keyBitLength: 0, keyByteLength: 0 }; + } + + /** Convenience: get the backend bound via setOptions. */ + get backend(): CryptoBackend { + return this.options.backend; + } +} + +function readBigEndianUInt(buf: Uint8Array): number { + let n = 0; + for (let i = 0; i < buf.length; i++) n = n * 256 + (buf[i] as number); + return n; +} diff --git a/src/rsa/native-engine.ts b/src/rsa/native-engine.ts new file mode 100644 index 0000000..0113c72 --- /dev/null +++ b/src/rsa/native-engine.ts @@ -0,0 +1,139 @@ +import { + constants as nodeConstants, + privateDecrypt as nodePrivateDecrypt, + privateEncrypt as nodePrivateEncrypt, + publicDecrypt as nodePublicDecrypt, + publicEncrypt as nodePublicEncrypt, +} from 'node:crypto'; +import { detectAndExport } from '../formats/index.js'; +import { RSA_NO_PADDING } from '../schemes/index.js'; +import type { ResolvedOptions } from '../types.js'; +import { type Engine, JsEngine } from './engine.js'; +import type { RSAKey } from './key.js'; + +/** + * NodeNativeEngine — uses node:crypto.{publicEncrypt, privateDecrypt, + * privateEncrypt, publicDecrypt} when the scheme is one of: + * - pkcs1 (RSA_PKCS1_PADDING) + * - pkcs1_oaep (RSA_PKCS1_OAEP_PADDING) + * - RSA_NO_PADDING (when set in encryptionSchemeOptions.padding) + * + * Falls back to the JS engine for unsupported combinations. + */ +export class NodeNativeEngine implements Engine { + private readonly fallback: JsEngine; + constructor( + private readonly key: RSAKey, + private readonly options: ResolvedOptions, + ) { + this.fallback = new JsEngine(key); + } + + /** + * Routes back to the JS engine for combinations OpenSSL doesn't accept: + * - `privateEncrypt` + OAEP padding ("illegal or unsupported padding mode") + * - any RSA_NO_PADDING operation (Node would require pre-padded fixed-size + * chunks; the JS engine handles padding/unpadding internally). + * - PKCS#1 v1.5 privateDecrypt on modern Node (security-deprecated since + * CVE-2024-PEND — Node throws unless --security-revert is set). + * + * `decrypt` is the parameter "reversed" for `usePublic=true` callers and + * "not reversed" for the canonical `privateDecrypt` path. The arg name in + * encrypt() means usePrivate; in decrypt() it means usePublic. + */ + private nativeAvailableForEncrypt(usePrivate: boolean): boolean { + if (this.options.encryptionSchemeOptions.padding === RSA_NO_PADDING) return false; + if (usePrivate && this.options.encryptionScheme === 'pkcs1_oaep') return false; + return true; + } + + private nativeAvailableForDecrypt(usePublic: boolean): boolean { + if (this.options.encryptionSchemeOptions.padding === RSA_NO_PADDING) return false; + if (usePublic && this.options.encryptionScheme === 'pkcs1_oaep') return false; + // PKCS#1 v1.5 privateDecrypt has been disabled in modern Node by default. + if (!usePublic && this.options.encryptionScheme === 'pkcs1') return false; + return true; + } + + private padding(): number { + const p = this.options.encryptionSchemeOptions.padding; + if (p === RSA_NO_PADDING) return nodeConstants.RSA_NO_PADDING; + if (this.options.encryptionScheme === 'pkcs1_oaep') return nodeConstants.RSA_PKCS1_OAEP_PADDING; + return nodeConstants.RSA_PKCS1_PADDING; + } + + private oaepHashOption(): { oaepHash?: string } { + if (this.options.encryptionScheme === 'pkcs1_oaep') { + const h = this.options.encryptionSchemeOptions.hash; + if (h) return { oaepHash: h }; + } + return {}; + } + + encrypt(buffer: Uint8Array, usePrivate = false): Uint8Array { + if (!this.nativeAvailableForEncrypt(usePrivate)) + return this.fallback.encrypt(buffer, usePrivate); + const max = this.key.maxMessageLength; + if (max <= 0) throw new Error('Engine: key not initialised'); + const buffersCount = Math.ceil(buffer.length / max) || 1; + const dividedSize = Math.ceil(buffer.length / buffersCount) || 1; + + const chunks: Uint8Array[] = []; + if (buffersCount === 1) { + chunks.push(buffer); + } else { + for (let i = 0; i < buffersCount; i++) { + chunks.push(buffer.subarray(i * dividedSize, (i + 1) * dividedSize)); + } + } + + const exportFormat = usePrivate ? 'pkcs1-private-pem' : 'pkcs8-public-pem'; + const keyPem = detectAndExport(this.key, exportFormat) as string; + const oaep = this.oaepHashOption(); + const padding = this.padding(); + + const out: Uint8Array[] = []; + for (const chunk of chunks) { + const ct = usePrivate + ? nodePrivateEncrypt({ key: keyPem, padding, ...oaep }, Buffer.from(chunk)) + : nodePublicEncrypt({ key: keyPem, padding, ...oaep }, Buffer.from(chunk)); + out.push(new Uint8Array(ct.buffer, ct.byteOffset, ct.byteLength)); + } + + return concatU8(out); + } + + decrypt(buffer: Uint8Array, usePublic = false): Uint8Array { + if (!this.nativeAvailableForDecrypt(usePublic)) return this.fallback.decrypt(buffer, usePublic); + const chunkLen = this.key.encryptedDataLength; + if (buffer.length % chunkLen !== 0) throw new Error('Incorrect data or key'); + const count = buffer.length / chunkLen; + + const exportFormat = usePublic ? 'pkcs8-public-pem' : 'pkcs1-private-pem'; + const keyPem = detectAndExport(this.key, exportFormat) as string; + const oaep = this.oaepHashOption(); + const padding = this.padding(); + + const out: Uint8Array[] = []; + for (let i = 0; i < count; i++) { + const slice = Buffer.from(buffer.subarray(i * chunkLen, (i + 1) * chunkLen)); + const pt = usePublic + ? nodePublicDecrypt({ key: keyPem, padding, ...oaep }, slice) + : nodePrivateDecrypt({ key: keyPem, padding, ...oaep }, slice); + out.push(new Uint8Array(pt.buffer, pt.byteOffset, pt.byteLength)); + } + return concatU8(out); + } +} + +function concatU8(parts: Uint8Array[]): Uint8Array { + let total = 0; + for (const p of parts) total += p.length; + const out = new Uint8Array(total); + let off = 0; + for (const p of parts) { + out.set(p, off); + off += p.length; + } + return out; +} diff --git a/src/rsa/native-keygen.ts b/src/rsa/native-keygen.ts new file mode 100644 index 0000000..d27933c --- /dev/null +++ b/src/rsa/native-keygen.ts @@ -0,0 +1,64 @@ +import { generateKeyPairSync } from 'node:crypto'; +import { fromBase64 } from '../crypto/bytes.js'; +import type { RSAKey } from './key.js'; + +// One-shot guard mirroring RSAKey.generate's warning behaviour. +let warnedSmallKey = false; + +function fromBase64Url(b64url: string): Uint8Array { + const b64 = b64url.replace(/-/g, '+').replace(/_/g, '/'); + const pad = (4 - (b64.length % 4)) % 4; + return fromBase64(b64 + '='.repeat(pad)); +} + +/** + * Populate `key` with a freshly-generated `bits`-bit RSA key whose public + * exponent is `expHex` (hex string, e.g. `"010001"`). Uses + * `node:crypto.generateKeyPairSync` — orders of magnitude faster than the + * pure-JS Miller-Rabin path for keys ≥ 2048 bits (~50 ms vs ~2 s for + * 2048-bit). + * + * Browser bundle has no equivalent; src/index.browser.ts doesn't wire this + * factory and NodeRSA.generateKeyPair falls back to RSAKey.generate. + */ +export function nodeNativeKeygen(key: RSAKey, bits: number, expHex: string): void { + if (bits < 512) { + throw new Error( + `Key size ${bits} bits is cryptographically broken (< 512); refusing to generate`, + ); + } + if (bits < 2048 && !warnedSmallKey) { + warnedSmallKey = true; + // eslint-disable-next-line no-console + console.warn( + `node-rsa: generating ${bits}-bit RSA key — below NIST SP 800-56B §6.1.6.2 minimum (2048 bits); not recommended for production`, + ); + } + + const exp = Number.parseInt(expHex, 16); + const { privateKey } = generateKeyPairSync('rsa', { + modulusLength: bits, + publicExponent: exp, + }); + const jwk = privateKey.export({ format: 'jwk' }) as { + n: string; + e: string; + d: string; + p: string; + q: string; + dp: string; + dq: string; + qi: string; + }; + + key.setPrivate( + fromBase64Url(jwk.n), + exp, + fromBase64Url(jwk.d), + fromBase64Url(jwk.p), + fromBase64Url(jwk.q), + fromBase64Url(jwk.dp), + fromBase64Url(jwk.dq), + fromBase64Url(jwk.qi), + ); +} diff --git a/src/rsa/native-signatures.ts b/src/rsa/native-signatures.ts new file mode 100644 index 0000000..3c00628 --- /dev/null +++ b/src/rsa/native-signatures.ts @@ -0,0 +1,188 @@ +import { + createPrivateKey, + createPublicKey, + type KeyObject, + constants as nodeConstants, + sign as nodeSign, + verify as nodeVerify, +} from 'node:crypto'; +import type { HashingAlgorithm } from '../crypto/types.js'; +import { pkcs1Format } from '../formats/pkcs1.js'; +import type { SchemeProvider } from '../schemes/index.js'; +import { oaepScheme } from '../schemes/oaep.js'; +import { pkcs1Scheme } from '../schemes/pkcs1.js'; +import type { EncryptionSchemeImpl, SchemeOptions, SignatureScheme } from '../schemes/types.js'; +import type { RSAKey } from './key.js'; + +const DEFAULT_PKCS1_HASH: HashingAlgorithm = 'sha256'; +const DEFAULT_PSS_HASH: HashingAlgorithm = 'sha1'; +const DEFAULT_PSS_SALT = 20; + +function bufferToU8(buf: Buffer): Uint8Array { + return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength); +} + +function privateKeyObjectFor(key: RSAKey): KeyObject { + if (!key.isPrivate()) throw new Error('Native signing requires a private key'); + const pem = pkcs1Format.privateExport?.(key, { type: 'pem' }) as string; + return createPrivateKey({ key: pem, format: 'pem', type: 'pkcs1' }); +} + +function publicKeyObjectFor(key: RSAKey): KeyObject { + if (!key.isPublic()) throw new Error('Native verifying requires a public key'); + const pem = pkcs1Format.publicExport?.(key, { type: 'pem' }) as string; + return createPublicKey({ key: pem, format: 'pem', type: 'pkcs1' }); +} + +function assertHashSupported(backend: SchemeOptions['backend'], hash: HashingAlgorithm): void { + if (!backend.supportsHash(hash)) { + throw new Error( + `node-rsa: hash "${hash}" not available in node:crypto on this build (OpenSSL 3 may need the legacy provider for md4/ripemd160). Use setOptions({environment:"browser"}) to force the pure-JS path.`, + ); + } +} + +/** + * PKCS#1 v1.5 — keeps the JS scheme for encryption padding (encPad/encUnPad), + * delegates sign/verify to node:crypto. NodeNativeEngine handles the + * encryption RSA primitive separately, so the JS scheme's sign/verify path + * (which is what we override here) is the only thing still going through + * BigInteger.modPow today. + */ +class NodeNativePkcs1Scheme implements EncryptionSchemeImpl, SignatureScheme { + private privateKeyObj?: KeyObject; + private publicKeyObj?: KeyObject; + + constructor( + private readonly inner: EncryptionSchemeImpl & SignatureScheme, + private readonly key: RSAKey, + private readonly options: SchemeOptions, + ) {} + + maxMessageLength(): number { + return this.inner.maxMessageLength(); + } + encPad(buf: Uint8Array, opts?: { type?: number }): Uint8Array { + return this.inner.encPad(buf, opts); + } + encUnPad(buf: Uint8Array, opts?: { type?: number }): Uint8Array | null { + return this.inner.encUnPad(buf, opts); + } + + sign(buffer: Uint8Array): Uint8Array { + const hash = this.options.signingSchemeOptions.hash ?? DEFAULT_PKCS1_HASH; + assertHashSupported(this.options.backend, hash); + if (!this.privateKeyObj) this.privateKeyObj = privateKeyObjectFor(this.key); + const sig = nodeSign(hash, buffer, { + key: this.privateKeyObj, + padding: nodeConstants.RSA_PKCS1_PADDING, + }); + return bufferToU8(sig); + } + + verify(buffer: Uint8Array, signature: Uint8Array): boolean { + const hash = this.options.signingSchemeOptions.hash ?? DEFAULT_PKCS1_HASH; + assertHashSupported(this.options.backend, hash); + if (!this.publicKeyObj) this.publicKeyObj = publicKeyObjectFor(this.key); + // RFC 8017 §8.2.2 step 2.b: out-of-range signature representative + // (or any other RSA-primitive failure) yields "invalid signature", + // not a thrown error. + try { + return nodeVerify( + hash, + buffer, + { + key: this.publicKeyObj, + padding: nodeConstants.RSA_PKCS1_PADDING, + }, + signature, + ); + } catch { + return false; + } + } +} + +/** + * PSS — node:crypto only supports MGF1 with the same hash as the message + * digest. A custom MGF or a different MGF-hash configuration cannot be + * expressed natively; we throw at scheme construction so the failure is + * loud and early rather than silent at sign time. + */ +class NodeNativePssScheme implements SignatureScheme { + private privateKeyObj?: KeyObject; + private publicKeyObj?: KeyObject; + + constructor( + private readonly key: RSAKey, + private readonly options: SchemeOptions, + ) { + if (options.signingSchemeOptions.mgf) { + throw new Error( + 'node-rsa: custom MGF for PSS is not supported in the node-native engine ' + + '(node:crypto only does MGF1 with hash = signing hash). ' + + 'Use setOptions({environment:"browser"}) to force the pure-JS path.', + ); + } + } + + sign(buffer: Uint8Array): Uint8Array { + const hash = this.options.signingSchemeOptions.hash ?? DEFAULT_PSS_HASH; + assertHashSupported(this.options.backend, hash); + const saltLength = this.options.signingSchemeOptions.saltLength ?? DEFAULT_PSS_SALT; + if (!this.privateKeyObj) this.privateKeyObj = privateKeyObjectFor(this.key); + const sig = nodeSign(hash, buffer, { + key: this.privateKeyObj, + padding: nodeConstants.RSA_PKCS1_PSS_PADDING, + saltLength, + }); + return bufferToU8(sig); + } + + verify(buffer: Uint8Array, signature: Uint8Array): boolean { + const hash = this.options.signingSchemeOptions.hash ?? DEFAULT_PSS_HASH; + assertHashSupported(this.options.backend, hash); + const saltLength = this.options.signingSchemeOptions.saltLength ?? DEFAULT_PSS_SALT; + if (!this.publicKeyObj) this.publicKeyObj = publicKeyObjectFor(this.key); + try { + return nodeVerify( + hash, + buffer, + { + key: this.publicKeyObj, + padding: nodeConstants.RSA_PKCS1_PSS_PADDING, + saltLength, + }, + signature, + ); + } catch { + return false; + } + } +} + +/** + * Drop-in replacement for the default `SCHEMES` map used by the Node bundle: + * pkcs1 + pss sign/verify go through `node:crypto` (faster, FIPS-friendly); + * pkcs1_oaep is unchanged because OAEP encryption already routes through + * NodeNativeEngine. Constructing a PSS scheme with a custom MGF throws — + * see `NodeNativePssScheme`. + */ +export const nodeNativeSchemes: Record = { + pkcs1: { + isEncryption: true, + isSignature: true, + makeScheme(key: RSAKey, options: SchemeOptions): EncryptionSchemeImpl & SignatureScheme { + const inner = pkcs1Scheme.makeScheme(key, options) as EncryptionSchemeImpl & SignatureScheme; + return new NodeNativePkcs1Scheme(inner, key, options); + }, + }, + pss: { + isEncryption: false, + isSignature: true, + makeScheme(key: RSAKey, options: SchemeOptions): SignatureScheme { + return new NodeNativePssScheme(key, options); + }, + }, + pkcs1_oaep: oaepScheme, +}; diff --git a/src/schemes/index.ts b/src/schemes/index.ts new file mode 100644 index 0000000..a63b7f2 --- /dev/null +++ b/src/schemes/index.ts @@ -0,0 +1,27 @@ +import type { RSAKey } from '../rsa/key.js'; +import { oaepScheme } from './oaep.js'; +import { pkcs1Scheme, RSA_NO_PADDING } from './pkcs1.js'; +import { pssScheme } from './pss.js'; +import type { EncryptionSchemeImpl, SchemeOptions, SignatureScheme } from './types.js'; + +export interface SchemeProvider { + isEncryption: boolean; + isSignature: boolean; + makeScheme(key: RSAKey, options: SchemeOptions): EncryptionSchemeImpl | SignatureScheme; +} + +export const SCHEMES: Record = { + pkcs1: pkcs1Scheme, + pkcs1_oaep: oaepScheme, + pss: pssScheme, +}; + +export type { + EncryptionSchemeImpl, + EncryptionSchemeOptions, + MaskGenerationFunction, + SchemeOptions, + SignatureScheme, + SigningSchemeOptions, +} from './types.js'; +export { oaepScheme, pkcs1Scheme, pssScheme, RSA_NO_PADDING }; diff --git a/src/schemes/oaep.js b/src/schemes/oaep.js deleted file mode 100644 index 30ef0c1..0000000 --- a/src/schemes/oaep.js +++ /dev/null @@ -1,179 +0,0 @@ -/** - * PKCS_OAEP signature scheme - */ - -var BigInteger = require('../libs/jsbn'); -var crypt = require('crypto'); - -module.exports = { - isEncryption: true, - isSignature: false -}; - -module.exports.digestLength = { - md4: 16, - md5: 16, - ripemd160: 20, - rmd160: 20, - sha1: 20, - sha224: 28, - sha256: 32, - sha384: 48, - sha512: 64 -}; - -var DEFAULT_HASH_FUNCTION = 'sha1'; - -/* - * OAEP Mask Generation Function 1 - * Generates a buffer full of pseudorandom bytes given seed and maskLength. - * Giving the same seed, maskLength, and hashFunction will result in the same exact byte values in the buffer. - * - * https://tools.ietf.org/html/rfc3447#appendix-B.2.1 - * - * Parameters: - * seed [Buffer] The pseudo random seed for this function - * maskLength [int] The length of the output - * hashFunction [String] The hashing function to use. Will accept any valid crypto hash. Default "sha1" - * Supports "sha1" and "sha256". - * To add another algorythm the algorythem must be accepted by crypto.createHash, and then the length of the output of the hash function (the digest) must be added to the digestLength object below. - * Most RSA implementations will be expecting sha1 - */ -module.exports.eme_oaep_mgf1 = function (seed, maskLength, hashFunction) { - hashFunction = hashFunction || DEFAULT_HASH_FUNCTION; - var hLen = module.exports.digestLength[hashFunction]; - var count = Math.ceil(maskLength / hLen); - var T = Buffer.alloc(hLen * count); - var c = Buffer.alloc(4); - for (var i = 0; i < count; ++i) { - var hash = crypt.createHash(hashFunction); - hash.update(seed); - c.writeUInt32BE(i, 0); - hash.update(c); - hash.digest().copy(T, i * hLen); - } - return T.slice(0, maskLength); -}; - -module.exports.makeScheme = function (key, options) { - function Scheme(key, options) { - this.key = key; - this.options = options; - } - - Scheme.prototype.maxMessageLength = function () { - return this.key.encryptedDataLength - 2 * module.exports.digestLength[this.options.encryptionSchemeOptions.hash || DEFAULT_HASH_FUNCTION] - 2; - }; - - /** - * Pad input - * alg: PKCS1_OAEP - * - * https://tools.ietf.org/html/rfc3447#section-7.1.1 - */ - Scheme.prototype.encPad = function (buffer) { - var hash = this.options.encryptionSchemeOptions.hash || DEFAULT_HASH_FUNCTION; - var mgf = this.options.encryptionSchemeOptions.mgf || module.exports.eme_oaep_mgf1; - var label = this.options.encryptionSchemeOptions.label || Buffer.alloc(0); - var emLen = this.key.encryptedDataLength; - - var hLen = module.exports.digestLength[hash]; - - // Make sure we can put message into an encoded message of emLen bytes - if (buffer.length > emLen - 2 * hLen - 2) { - throw new Error("Message is too long to encode into an encoded message with a length of " + emLen + " bytes, increase" + - "emLen to fix this error (minimum value for given parameters and options: " + (emLen - 2 * hLen - 2) + ")"); - } - - var lHash = crypt.createHash(hash); - lHash.update(label); - lHash = lHash.digest(); - - var PS = Buffer.alloc(emLen - buffer.length - 2 * hLen - 1); // Padding "String" - PS.fill(0); // Fill the buffer with octets of 0 - PS[PS.length - 1] = 1; - - var DB = Buffer.concat([lHash, PS, buffer]); - var seed = crypt.randomBytes(hLen); - - // mask = dbMask - var mask = mgf(seed, DB.length, hash); - // XOR DB and dbMask together. - for (var i = 0; i < DB.length; i++) { - DB[i] ^= mask[i]; - } - // DB = maskedDB - - // mask = seedMask - mask = mgf(DB, hLen, hash); - // XOR seed and seedMask together. - for (i = 0; i < seed.length; i++) { - seed[i] ^= mask[i]; - } - // seed = maskedSeed - - var em = Buffer.alloc(1 + seed.length + DB.length); - em[0] = 0; - seed.copy(em, 1); - DB.copy(em, 1 + seed.length); - - return em; - }; - - /** - * Unpad input - * alg: PKCS1_OAEP - * - * Note: This method works within the buffer given and modifies the values. It also returns a slice of the EM as the return Message. - * If the implementation requires that the EM parameter be unmodified then the implementation should pass in a clone of the EM buffer. - * - * https://tools.ietf.org/html/rfc3447#section-7.1.2 - */ - Scheme.prototype.encUnPad = function (buffer) { - var hash = this.options.encryptionSchemeOptions.hash || DEFAULT_HASH_FUNCTION; - var mgf = this.options.encryptionSchemeOptions.mgf || module.exports.eme_oaep_mgf1; - var label = this.options.encryptionSchemeOptions.label || Buffer.alloc(0); - - var hLen = module.exports.digestLength[hash]; - - // Check to see if buffer is a properly encoded OAEP message - if (buffer.length < 2 * hLen + 2) { - throw new Error("Error decoding message, the supplied message is not long enough to be a valid OAEP encoded message"); - } - - var seed = buffer.slice(1, hLen + 1); // seed = maskedSeed - var DB = buffer.slice(1 + hLen); // DB = maskedDB - - var mask = mgf(DB, hLen, hash); // seedMask - // XOR maskedSeed and seedMask together to get the original seed. - for (var i = 0; i < seed.length; i++) { - seed[i] ^= mask[i]; - } - - mask = mgf(seed, DB.length, hash); // dbMask - // XOR DB and dbMask together to get the original data block. - for (i = 0; i < DB.length; i++) { - DB[i] ^= mask[i]; - } - - var lHash = crypt.createHash(hash); - lHash.update(label); - lHash = lHash.digest(); - - var lHashEM = DB.slice(0, hLen); - if (lHashEM.toString("hex") != lHash.toString("hex")) { - throw new Error("Error decoding message, the lHash calculated from the label provided and the lHash in the encrypted data do not match."); - } - - // Filter out padding - i = hLen; - while (DB[i++] === 0 && i < DB.length); - if (DB[i - 1] != 1) { - throw new Error("Error decoding message, there is no padding message separator byte"); - } - - return DB.slice(i); // Message - }; - - return new Scheme(key, options); -}; diff --git a/src/schemes/oaep.ts b/src/schemes/oaep.ts new file mode 100644 index 0000000..1deb6be --- /dev/null +++ b/src/schemes/oaep.ts @@ -0,0 +1,154 @@ +import { concat, constantTimeEqual, writeUInt32BE } from '../crypto/bytes.js'; +import { DIGEST_LENGTH } from '../crypto/digest-length.js'; +import type { CryptoBackend, HashingAlgorithm } from '../crypto/types.js'; +import type { RSAKey } from '../rsa/key.js'; +import type { EncryptionSchemeImpl, MaskGenerationFunction, SchemeOptions } from './types.js'; + +const DEFAULT_HASH: HashingAlgorithm = 'sha1'; + +/** Default MGF1 implementation bound to a backend. */ +export function mgf1( + seed: Uint8Array, + maskLength: number, + hash: HashingAlgorithm, + backend: CryptoBackend, +): Uint8Array { + const hLen = DIGEST_LENGTH[hash]; + const count = Math.ceil(maskLength / hLen); + const out = new Uint8Array(hLen * count); + const counter = new Uint8Array(4); + for (let i = 0; i < count; i++) { + writeUInt32BE(i, counter, 0); + const h = backend.digest(hash, concat(seed, counter)); + out.set(h, i * hLen); + } + return out.subarray(0, maskLength); +} + +class OaepScheme implements EncryptionSchemeImpl { + constructor( + private readonly key: RSAKey, + private readonly options: SchemeOptions, + ) {} + + private hash(): HashingAlgorithm { + return this.options.encryptionSchemeOptions.hash ?? DEFAULT_HASH; + } + + private mgf(): MaskGenerationFunction { + const userMgf = this.options.encryptionSchemeOptions.mgf; + if (userMgf) return userMgf; + const backend = this.options.backend; + return (seed, maskLength, hash) => mgf1(seed, maskLength, hash, backend); + } + + maxMessageLength(): number { + return this.key.encryptedDataLength - 2 * DIGEST_LENGTH[this.hash()] - 2; + } + + encPad(buffer: Uint8Array): Uint8Array { + const hash = this.hash(); + const mgf = this.mgf(); + const label = this.options.encryptionSchemeOptions.label ?? new Uint8Array(0); + const emLen = this.key.encryptedDataLength; + const hLen = DIGEST_LENGTH[hash]; + + if (buffer.length > emLen - 2 * hLen - 2) { + throw new Error( + `Message is too long to encode into an encoded message with a length of ${emLen} bytes, increaseemLen to fix this error (minimum size: ${emLen - 2 * hLen - 2})`, + ); + } + + const lHash = this.options.backend.digest(hash, label); + const PS = new Uint8Array(emLen - buffer.length - 2 * hLen - 1); + PS[PS.length - 1] = 1; + const DB = concat(lHash, PS, buffer); + const seed = this.options.backend.randomBytes(hLen); + + const dbMask = mgf(seed, DB.length, hash); + for (let i = 0; i < DB.length; i++) DB[i] = (DB[i] as number) ^ (dbMask[i] as number); + + const seedMask = mgf(DB, hLen, hash); + for (let i = 0; i < seed.length; i++) seed[i] = (seed[i] as number) ^ (seedMask[i] as number); + + const em = new Uint8Array(1 + seed.length + DB.length); + em[0] = 0; + em.set(seed, 1); + em.set(DB, 1 + seed.length); + return em; + } + + /** + * Constant-time OAEP decode per RFC 8017 §7.1.2: all failure modes — + * wrong Y byte, lHash mismatch, no 0x01 separator, message length over + * the geometric maximum — must be indistinguishable in timing or a + * Manger oracle recovers plaintext in ~10⁵ queries. We accumulate a + * single `bad` flag without branches and return null once at the end. + */ + encUnPad(buffer: Uint8Array): Uint8Array | null { + const hash = this.hash(); + const mgf = this.mgf(); + const label = this.options.encryptionSchemeOptions.label ?? new Uint8Array(0); + const hLen = DIGEST_LENGTH[hash]; + + // Length precondition — public-known info (key size), safe to branch on. + if (buffer.length < 2 * hLen + 2) return null; + + // From here on, all checks are constant-time and accumulate into `bad`. + const work = buffer.slice(); + + // RFC 8017 §7.1.2 step 3: Y (the leading byte) must equal 0x00. + let bad = work[0] === 0x00 ? 0 : 1; + + const seed = work.subarray(1, hLen + 1); + const DB = work.subarray(1 + hLen); + + const seedMask = mgf(DB, hLen, hash); + for (let i = 0; i < seed.length; i++) seed[i] = (seed[i] as number) ^ (seedMask[i] as number); + + const dbMask = mgf(seed, DB.length, hash); + for (let i = 0; i < DB.length; i++) DB[i] = (DB[i] as number) ^ (dbMask[i] as number); + + // lHash compare in constant time. + const lHash = this.options.backend.digest(hash, label); + const lHashEM = DB.subarray(0, hLen); + bad |= constantTimeEqual(lHashEM, lHash) ? 0 : 1; + + // Constant-iteration separator scan. Walk the entire DB from hLen onward, + // recording the position of the first 0x01 byte. Any non-{0x00,0x01} byte + // before the separator, or no separator at all, marks `bad`. + let found = 0; + let msgStart = 0; + for (let j = hLen; j < DB.length; j++) { + const b = DB[j] as number; + // isOne = 1 if b==0x01 else 0 (constant-time, no branch). + const isOne = (((b ^ 0x01) - 1) >>> 31) & 1; + // isZero = 1 if b==0x00 else 0. + const isZero = (((b | -b) >>> 31) ^ 1) & 1; + const notFoundYet = (1 - found) & 1; + // Record msgStart = j+1 (first byte after the separator) the first + // time we see 0x01. Use arithmetic mask, not branch. + const recordMask = -(notFoundYet & isOne); + msgStart = (msgStart & ~recordMask) | ((j + 1) & recordMask); + // Before separator, any byte ≠ 0x00 and ≠ 0x01 marks bad. + bad |= notFoundYet & (1 - isOne) & (1 - isZero); + found |= isOne; + } + bad |= 1 - found; + + if (bad) return null; + + const msg = DB.subarray(msgStart).slice(); + // RFC 8017 §7.1.1 step 1.b: enforce mLen ≤ k − 2hLen − 2. + if (msg.length > this.maxMessageLength()) return null; + return msg; + } +} + +export const oaepScheme = { + isEncryption: true as const, + isSignature: false as const, + makeScheme(key: RSAKey, options: SchemeOptions): EncryptionSchemeImpl { + return new OaepScheme(key, options); + }, +}; diff --git a/src/schemes/pkcs1.js b/src/schemes/pkcs1.js deleted file mode 100644 index 86e55de..0000000 --- a/src/schemes/pkcs1.js +++ /dev/null @@ -1,238 +0,0 @@ -/** - * PKCS1 padding and signature scheme - */ - -var BigInteger = require('../libs/jsbn'); -var crypt = require('crypto'); -var constants = require('constants'); -var SIGN_INFO_HEAD = { - md2: Buffer.from('3020300c06082a864886f70d020205000410', 'hex'), - md5: Buffer.from('3020300c06082a864886f70d020505000410', 'hex'), - sha1: Buffer.from('3021300906052b0e03021a05000414', 'hex'), - sha224: Buffer.from('302d300d06096086480165030402040500041c', 'hex'), - sha256: Buffer.from('3031300d060960864801650304020105000420', 'hex'), - sha384: Buffer.from('3041300d060960864801650304020205000430', 'hex'), - sha512: Buffer.from('3051300d060960864801650304020305000440', 'hex'), - ripemd160: Buffer.from('3021300906052b2403020105000414', 'hex'), - rmd160: Buffer.from('3021300906052b2403020105000414', 'hex') -}; - -var SIGN_ALG_TO_HASH_ALIASES = { - 'ripemd160': 'rmd160' -}; - -var DEFAULT_HASH_FUNCTION = 'sha256'; - -module.exports = { - isEncryption: true, - isSignature: true -}; - -module.exports.makeScheme = function (key, options) { - function Scheme(key, options) { - this.key = key; - this.options = options; - } - - Scheme.prototype.maxMessageLength = function () { - if (this.options.encryptionSchemeOptions && this.options.encryptionSchemeOptions.padding == constants.RSA_NO_PADDING) { - return this.key.encryptedDataLength; - } - return this.key.encryptedDataLength - 11; - }; - - /** - * Pad input Buffer to encryptedDataLength bytes, and return Buffer.from - * alg: PKCS#1 - * @param buffer - * @returns {Buffer} - */ - Scheme.prototype.encPad = function (buffer, options) { - options = options || {}; - var filled; - if (buffer.length > this.key.maxMessageLength) { - throw new Error("Message too long for RSA (n=" + this.key.encryptedDataLength + ", l=" + buffer.length + ")"); - } - if (this.options.encryptionSchemeOptions && this.options.encryptionSchemeOptions.padding == constants.RSA_NO_PADDING) { - //RSA_NO_PADDING treated like JAVA left pad with zero character - filled = Buffer.alloc(this.key.maxMessageLength - buffer.length); - filled.fill(0); - return Buffer.concat([filled, buffer]); - } - - /* Type 1: zeros padding for private key encrypt */ - if (options.type === 1) { - filled = Buffer.alloc(this.key.encryptedDataLength - buffer.length - 1); - filled.fill(0xff, 0, filled.length - 1); - filled[0] = 1; - filled[filled.length - 1] = 0; - - return Buffer.concat([filled, buffer]); - } else { - /* random padding for public key encrypt */ - filled = Buffer.alloc(this.key.encryptedDataLength - buffer.length); - filled[0] = 0; - filled[1] = 2; - var rand = crypt.randomBytes(filled.length - 3); - for (var i = 0; i < rand.length; i++) { - var r = rand[i]; - while (r === 0) { // non-zero only - r = crypt.randomBytes(1)[0]; - } - filled[i + 2] = r; - } - filled[filled.length - 1] = 0; - return Buffer.concat([filled, buffer]); - } - }; - - /** - * Unpad input Buffer and, if valid, return the Buffer object - * alg: PKCS#1 (type 2, random) - * @param buffer - * @returns {Buffer} - */ - Scheme.prototype.encUnPad = function (buffer, options) { - options = options || {}; - var i = 0; - - if (this.options.encryptionSchemeOptions && this.options.encryptionSchemeOptions.padding == constants.RSA_NO_PADDING) { - //RSA_NO_PADDING treated like JAVA left pad with zero character - var unPad; - if (typeof buffer.lastIndexOf == "function") { //patch for old node version - unPad = buffer.slice(buffer.lastIndexOf('\0') + 1, buffer.length); - } else { - unPad = buffer.slice(String.prototype.lastIndexOf.call(buffer, '\0') + 1, buffer.length); - } - return unPad; - } - - if (buffer.length < 4) { - return null; - } - - /* Type 1: zeros padding for private key decrypt */ - if (options.type === 1) { - if (buffer[0] !== 0 || buffer[1] !== 1) { - return null; - } - i = 3; - while (buffer[i] !== 0) { - if (buffer[i] != 0xFF || ++i >= buffer.length) { - return null; - } - } - } else { - /* random padding for public key decrypt */ - if (buffer[0] !== 0 || buffer[1] !== 2) { - return null; - } - i = 3; - while (buffer[i] !== 0) { - if (++i >= buffer.length) { - return null; - } - } - } - return buffer.slice(i + 1, buffer.length); - }; - - Scheme.prototype.sign = function (buffer) { - var hashAlgorithm = this.options.signingSchemeOptions.hash || DEFAULT_HASH_FUNCTION; - if (this.options.environment === 'browser') { - hashAlgorithm = SIGN_ALG_TO_HASH_ALIASES[hashAlgorithm] || hashAlgorithm; - - var hasher = crypt.createHash(hashAlgorithm); - hasher.update(buffer); - var hash = this.pkcs1pad(hasher.digest(), hashAlgorithm); - var res = this.key.$doPrivate(new BigInteger(hash)).toBuffer(this.key.encryptedDataLength); - - return res; - } else { - var signer = crypt.createSign('RSA-' + hashAlgorithm.toUpperCase()); - signer.update(buffer); - return signer.sign(this.options.rsaUtils.exportKey('private')); - } - }; - - Scheme.prototype.verify = function (buffer, signature, signature_encoding) { - if (this.options.encryptionSchemeOptions && this.options.encryptionSchemeOptions.padding == constants.RSA_NO_PADDING) { - //RSA_NO_PADDING has no verify data - return false; - } - var hashAlgorithm = this.options.signingSchemeOptions.hash || DEFAULT_HASH_FUNCTION; - if (this.options.environment === 'browser') { - hashAlgorithm = SIGN_ALG_TO_HASH_ALIASES[hashAlgorithm] || hashAlgorithm; - - if (signature_encoding) { - signature = Buffer.from(signature, signature_encoding); - } - - var hasher = crypt.createHash(hashAlgorithm); - hasher.update(buffer); - var hash = this.pkcs1pad(hasher.digest(), hashAlgorithm); - var m = this.key.$doPublic(new BigInteger(signature)); - - return m.toBuffer().toString('hex') == hash.toString('hex'); - } else { - var verifier = crypt.createVerify('RSA-' + hashAlgorithm.toUpperCase()); - verifier.update(buffer); - return verifier.verify(this.options.rsaUtils.exportKey('public'), signature, signature_encoding); - } - }; - - /** - * PKCS#1 zero pad input buffer to max data length - * @param hashBuf - * @param hashAlgorithm - * @returns {*} - */ - Scheme.prototype.pkcs0pad = function (buffer) { - var filled = Buffer.alloc(this.key.maxMessageLength - buffer.length); - filled.fill(0); - return Buffer.concat([filled, buffer]); - }; - - Scheme.prototype.pkcs0unpad = function (buffer) { - var unPad; - if (typeof buffer.lastIndexOf == "function") { //patch for old node version - unPad = buffer.slice(buffer.lastIndexOf('\0') + 1, buffer.length); - } else { - unPad = buffer.slice(String.prototype.lastIndexOf.call(buffer, '\0') + 1, buffer.length); - } - - return unPad; - }; - - /** - * PKCS#1 pad input buffer to max data length - * @param hashBuf - * @param hashAlgorithm - * @returns {*} - */ - Scheme.prototype.pkcs1pad = function (hashBuf, hashAlgorithm) { - var digest = SIGN_INFO_HEAD[hashAlgorithm]; - if (!digest) { - throw Error('Unsupported hash algorithm'); - } - - var data = Buffer.concat([digest, hashBuf]); - - if (data.length + 10 > this.key.encryptedDataLength) { - throw Error('Key is too short for signing algorithm (' + hashAlgorithm + ')'); - } - - var filled = Buffer.alloc(this.key.encryptedDataLength - data.length - 1); - filled.fill(0xff, 0, filled.length - 1); - filled[0] = 1; - filled[filled.length - 1] = 0; - - var res = Buffer.concat([filled, data]); - - return res; - }; - - return new Scheme(key, options); -}; - - diff --git a/src/schemes/pkcs1.ts b/src/schemes/pkcs1.ts new file mode 100644 index 0000000..cd16164 --- /dev/null +++ b/src/schemes/pkcs1.ts @@ -0,0 +1,188 @@ +import { concat, constantTimeEqual, fromHex } from '../crypto/bytes.js'; +import type { HashingAlgorithm } from '../crypto/types.js'; +import type { RSAKey } from '../rsa/key.js'; +import type { EncryptionSchemeImpl, SchemeOptions, SignatureScheme } from './types.js'; + +export const RSA_NO_PADDING = 3; + +const SIGN_INFO_HEAD: Partial> = { + md5: fromHex('3020300c06082a864886f70d020505000410'), + sha1: fromHex('3021300906052b0e03021a05000414'), + sha224: fromHex('302d300d06096086480165030402040500041c'), + sha256: fromHex('3031300d060960864801650304020105000420'), + sha384: fromHex('3041300d060960864801650304020205000430'), + sha512: fromHex('3051300d060960864801650304020305000440'), + ripemd160: fromHex('3021300906052b2403020105000414'), +}; + +const DEFAULT_HASH: HashingAlgorithm = 'sha256'; + +class Pkcs1Scheme implements EncryptionSchemeImpl, SignatureScheme { + constructor( + private readonly key: RSAKey, + private readonly options: SchemeOptions, + ) {} + + private noPadding(): boolean { + return this.options.encryptionSchemeOptions.padding === RSA_NO_PADDING; + } + + maxMessageLength(): number { + if (this.noPadding()) return this.key.encryptedDataLength; + return this.key.encryptedDataLength - 11; + } + + encPad(buffer: Uint8Array, opts?: { type?: number }): Uint8Array { + const { type } = opts ?? {}; + + if (buffer.length > this.maxMessageLength()) { + throw new Error( + `Message too long for RSA (n=${this.key.encryptedDataLength}, l=${buffer.length})`, + ); + } + + if (this.noPadding()) { + const filled = new Uint8Array(this.maxMessageLength() - buffer.length); + return concat(filled, buffer); + } + + if (type === 1) { + // Type 1: zeros padding for private-key encrypt (signing) + const filled = new Uint8Array(this.key.encryptedDataLength - buffer.length - 1); + filled.fill(0xff, 0, filled.length - 1); + filled[0] = 1; + filled[filled.length - 1] = 0; + return concat(filled, buffer); + } + + // Type 2: random non-zero padding for public-key encrypt + const filled = new Uint8Array(this.key.encryptedDataLength - buffer.length); + filled[0] = 0; + filled[1] = 2; + const rand = this.options.backend.randomBytes(filled.length - 3); + for (let i = 0; i < rand.length; i++) { + let r = rand[i] as number; + while (r === 0) { + r = this.options.backend.randomBytes(1)[0] as number; + } + filled[i + 2] = r; + } + filled[filled.length - 1] = 0; + return concat(filled, buffer); + } + + /** + * Constant-time PKCS#1 v1.5 decode per RFC 8017 §7.2.2: header byte, + * padding-type byte, PS validity, and minimum PS length all accumulate + * into a single bitwise `bad` flag with no early return; one `return + * null` for all failure modes. + * + * Full Bleichenbacher mitigation (RFC §7.2.2 NOTE — return synthetic + * plaintext instead of null) would require session-key plumbing and an + * API change (callers expect a throw). This closes only the internal + * differential timing oracle; the valid/invalid binary oracle inherent + * to PKCS#1 v1.5 remains — use OAEP for untrusted ciphertexts. + */ + encUnPad(buffer: Uint8Array, opts?: { type?: number }): Uint8Array | null { + const { type } = opts ?? {}; + + if (this.noPadding()) { + // RSA_NO_PADDING: strip leading zero pad — matches legacy + // lastIndexOf('\0') semantics. Not security-sensitive (no padding). + let lastZero = -1; + for (let j = buffer.length - 1; j >= 0; j--) { + if (buffer[j] === 0) { + lastZero = j; + break; + } + } + return buffer.subarray(lastZero + 1).slice(); + } + + // Length precondition — public-known (= key chunk size); safe to branch. + if (buffer.length < 11) return null; + + const expectedType = type === 1 ? 1 : 2; + + // From here on: all checks accumulate into `bad`; no branch on data. + let bad = buffer[0] as number; // must be 0x00 + bad |= (buffer[1] as number) ^ expectedType; // must match type + + let found = 0; + let sepPos = 0; + for (let i = 2; i < buffer.length; i++) { + const b = buffer[i] as number; + const isZero = (((b | -b) >>> 31) ^ 1) & 1; // 1 if b == 0 + const notFoundYet = (1 - found) & 1; + if (expectedType === 1) { + // PS bytes must be 0xff. Mark `bad` if a byte before separator is + // neither 0xff (continue PS) nor 0x00 (separator). + const isNotFF = ((b ^ 0xff) === 0 ? 0 : 1) & 1; + bad |= notFoundYet & (1 - isZero) & isNotFF; + } + // For type 2: PS bytes are random non-zero; first 0x00 is the separator. + // No per-byte check needed beyond the separator-position validation below. + + // Record sepPos = i the first time we see 0x00. + const recordMask = -(notFoundYet & isZero); + sepPos = (sepPos & ~recordMask) | (i & recordMask); + found |= isZero; + } + bad |= 1 - found; + // PS must be ≥ 8 bytes (RFC 8017 §7.2.1) → sepPos ≥ 10 (indices 2..9 inclusive are PS). + bad |= ((sepPos - 10) >>> 31) & 1; + + if (bad) return null; + return buffer.subarray(sepPos + 1).slice(); + } + + sign(buffer: Uint8Array): Uint8Array { + const hashAlgorithm = this.options.signingSchemeOptions.hash ?? DEFAULT_HASH; + const hash = this.options.backend.digest(hashAlgorithm, buffer); + const padded = this.pkcs1pad(hash, hashAlgorithm); + const signed = this.key.$doPrivate(new this.key.BI(padded)); + const out = signed.toBuffer(this.key.encryptedDataLength); + if (!out) throw new Error('PKCS#1 sign: output overflow'); + return out; + } + + verify(buffer: Uint8Array, signature: Uint8Array): boolean { + if (this.noPadding()) return false; // RSA_NO_PADDING has no verify data + const hashAlgorithm = this.options.signingSchemeOptions.hash ?? DEFAULT_HASH; + const hash = this.options.backend.digest(hashAlgorithm, buffer); + const padded = this.pkcs1pad(hash, hashAlgorithm); + // RFC 8017 §8.2.2 step 2.b: an out-of-range signature representative + // (or any other RSA-primitive failure) must yield "invalid signature", + // not a thrown error. + let m: Uint8Array | null; + try { + m = this.key.$doPublic(new this.key.BI(signature)).toBuffer(); + } catch { + return false; + } + if (!m) return false; + return constantTimeEqual(m, padded); + } + + pkcs1pad(hashBuf: Uint8Array, hashAlgorithm: HashingAlgorithm): Uint8Array { + const digest = SIGN_INFO_HEAD[hashAlgorithm]; + if (!digest) throw new Error(`Unsupported hash algorithm: ${hashAlgorithm}`); + const data = concat(digest, hashBuf); + if (data.length + 10 > this.key.encryptedDataLength) { + throw new Error(`Key is too short for signing algorithm (${hashAlgorithm})`); + } + const filled = new Uint8Array(this.key.encryptedDataLength - data.length - 1); + filled.fill(0xff, 0, filled.length - 1); + filled[0] = 1; + filled[filled.length - 1] = 0; + return concat(filled, data); + } +} + +export const pkcs1Scheme = { + isEncryption: true as const, + isSignature: true as const, + makeScheme(key: RSAKey, options: SchemeOptions): EncryptionSchemeImpl & SignatureScheme { + return new Pkcs1Scheme(key, options); + }, +}; diff --git a/src/schemes/pss.js b/src/schemes/pss.js deleted file mode 100644 index c6e037f..0000000 --- a/src/schemes/pss.js +++ /dev/null @@ -1,183 +0,0 @@ -/** - * PSS signature scheme - */ - -var BigInteger = require('../libs/jsbn'); -var crypt = require('crypto'); - -module.exports = { - isEncryption: false, - isSignature: true -}; - -var DEFAULT_HASH_FUNCTION = 'sha1'; -var DEFAULT_SALT_LENGTH = 20; - -module.exports.makeScheme = function (key, options) { - var OAEP = require('./schemes').pkcs1_oaep; - - /** - * @param key - * @param options - * options [Object] An object that contains the following keys that specify certain options for encoding. - * └>signingSchemeOptions - * ├>hash [String] Hash function to use when encoding and generating masks. Must be a string accepted by node's crypto.createHash function. (default = "sha1") - * ├>mgf [function] The mask generation function to use when encoding. (default = mgf1SHA1) - * └>sLen [uint] The length of the salt to generate. (default = 20) - * @constructor - */ - function Scheme(key, options) { - this.key = key; - this.options = options; - } - - Scheme.prototype.sign = function (buffer) { - var mHash = crypt.createHash(this.options.signingSchemeOptions.hash || DEFAULT_HASH_FUNCTION); - mHash.update(buffer); - - var encoded = this.emsa_pss_encode(mHash.digest(), this.key.keySize - 1); - return this.key.$doPrivate(new BigInteger(encoded)).toBuffer(this.key.encryptedDataLength); - }; - - Scheme.prototype.verify = function (buffer, signature, signature_encoding) { - if (signature_encoding) { - signature = Buffer.from(signature, signature_encoding); - } - signature = new BigInteger(signature); - - var emLen = Math.ceil((this.key.keySize - 1) / 8); - var m = this.key.$doPublic(signature).toBuffer(emLen); - - var mHash = crypt.createHash(this.options.signingSchemeOptions.hash || DEFAULT_HASH_FUNCTION); - mHash.update(buffer); - - return this.emsa_pss_verify(mHash.digest(), m, this.key.keySize - 1); - }; - - /* - * https://tools.ietf.org/html/rfc3447#section-9.1.1 - * - * mHash [Buffer] Hashed message to encode - * emBits [uint] Maximum length of output in bits. Must be at least 8hLen + 8sLen + 9 (hLen = Hash digest length in bytes | sLen = length of salt in bytes) - * @returns {Buffer} The encoded message - */ - Scheme.prototype.emsa_pss_encode = function (mHash, emBits) { - var hash = this.options.signingSchemeOptions.hash || DEFAULT_HASH_FUNCTION; - var mgf = this.options.signingSchemeOptions.mgf || OAEP.eme_oaep_mgf1; - var sLen = this.options.signingSchemeOptions.saltLength || DEFAULT_SALT_LENGTH; - - var hLen = OAEP.digestLength[hash]; - var emLen = Math.ceil(emBits / 8); - - if (emLen < hLen + sLen + 2) { - throw new Error("Output length passed to emBits(" + emBits + ") is too small for the options " + - "specified(" + hash + ", " + sLen + "). To fix this issue increase the value of emBits. (minimum size: " + - (8 * hLen + 8 * sLen + 9) + ")" - ); - } - - var salt = crypt.randomBytes(sLen); - - var Mapostrophe = Buffer.alloc(8 + hLen + sLen); - Mapostrophe.fill(0, 0, 8); - mHash.copy(Mapostrophe, 8); - salt.copy(Mapostrophe, 8 + mHash.length); - - var H = crypt.createHash(hash); - H.update(Mapostrophe); - H = H.digest(); - - var PS = Buffer.alloc(emLen - salt.length - hLen - 2); - PS.fill(0); - - var DB = Buffer.alloc(PS.length + 1 + salt.length); - PS.copy(DB); - DB[PS.length] = 0x01; - salt.copy(DB, PS.length + 1); - - var dbMask = mgf(H, DB.length, hash); - - // XOR DB and dbMask together - var maskedDB = Buffer.alloc(DB.length); - for (var i = 0; i < dbMask.length; i++) { - maskedDB[i] = DB[i] ^ dbMask[i]; - } - - var bits = 8 * emLen - emBits; - var mask = 255 ^ (255 >> 8 - bits << 8 - bits); - maskedDB[0] = maskedDB[0] & mask; - - var EM = Buffer.alloc(maskedDB.length + H.length + 1); - maskedDB.copy(EM, 0); - H.copy(EM, maskedDB.length); - EM[EM.length - 1] = 0xbc; - - return EM; - }; - - /* - * https://tools.ietf.org/html/rfc3447#section-9.1.2 - * - * mHash [Buffer] Hashed message - * EM [Buffer] Signature - * emBits [uint] Length of EM in bits. Must be at least 8hLen + 8sLen + 9 to be a valid signature. (hLen = Hash digest length in bytes | sLen = length of salt in bytes) - * @returns {Boolean} True if signature(EM) matches message(M) - */ - Scheme.prototype.emsa_pss_verify = function (mHash, EM, emBits) { - var hash = this.options.signingSchemeOptions.hash || DEFAULT_HASH_FUNCTION; - var mgf = this.options.signingSchemeOptions.mgf || OAEP.eme_oaep_mgf1; - var sLen = this.options.signingSchemeOptions.saltLength || DEFAULT_SALT_LENGTH; - - var hLen = OAEP.digestLength[hash]; - var emLen = Math.ceil(emBits / 8); - - if (emLen < hLen + sLen + 2 || EM[EM.length - 1] != 0xbc) { - return false; - } - - var DB = Buffer.alloc(emLen - hLen - 1); - EM.copy(DB, 0, 0, emLen - hLen - 1); - - var mask = 0; - for (var i = 0, bits = 8 * emLen - emBits; i < bits; i++) { - mask |= 1 << (7 - i); - } - - if ((DB[0] & mask) !== 0) { - return false; - } - - var H = EM.slice(emLen - hLen - 1, emLen - 1); - var dbMask = mgf(H, DB.length, hash); - - // Unmask DB - for (i = 0; i < DB.length; i++) { - DB[i] ^= dbMask[i]; - } - - bits = 8 * emLen - emBits; - mask = 255 ^ (255 >> 8 - bits << 8 - bits); - DB[0] = DB[0] & mask; - - // Filter out padding - for (i = 0; DB[i] === 0 && i < DB.length; i++); - if (DB[i] != 1) { - return false; - } - - var salt = DB.slice(DB.length - sLen); - - var Mapostrophe = Buffer.alloc(8 + hLen + sLen); - Mapostrophe.fill(0, 0, 8); - mHash.copy(Mapostrophe, 8); - salt.copy(Mapostrophe, 8 + mHash.length); - - var Hapostrophe = crypt.createHash(hash); - Hapostrophe.update(Mapostrophe); - Hapostrophe = Hapostrophe.digest(); - - return H.toString("hex") === Hapostrophe.toString("hex"); - }; - - return new Scheme(key, options); -}; diff --git a/src/schemes/pss.ts b/src/schemes/pss.ts new file mode 100644 index 0000000..c9a6a03 --- /dev/null +++ b/src/schemes/pss.ts @@ -0,0 +1,172 @@ +import { constantTimeEqual } from '../crypto/bytes.js'; +import { DIGEST_LENGTH } from '../crypto/digest-length.js'; +import type { HashingAlgorithm } from '../crypto/types.js'; +import type { RSAKey } from '../rsa/key.js'; +import { mgf1 } from './oaep.js'; +import type { MaskGenerationFunction, SchemeOptions, SignatureScheme } from './types.js'; + +const DEFAULT_HASH: HashingAlgorithm = 'sha1'; +const DEFAULT_SALT_LENGTH = 20; + +class PssScheme implements SignatureScheme { + constructor( + private readonly key: RSAKey, + private readonly options: SchemeOptions, + ) {} + + private hash(): HashingAlgorithm { + return this.options.signingSchemeOptions.hash ?? DEFAULT_HASH; + } + + private mgf(): MaskGenerationFunction { + const userMgf = this.options.signingSchemeOptions.mgf; + if (userMgf) return userMgf; + const backend = this.options.backend; + return (seed, maskLength, hash) => mgf1(seed, maskLength, hash, backend); + } + + private saltLen(): number { + return this.options.signingSchemeOptions.saltLength ?? DEFAULT_SALT_LENGTH; + } + + sign(buffer: Uint8Array): Uint8Array { + const hash = this.hash(); + const mHash = this.options.backend.digest(hash, buffer); + const encoded = this.emsaPssEncode(mHash, this.key.keySize - 1); + const signed = this.key.$doPrivate(new this.key.BI(encoded)); + const out = signed.toBuffer(this.key.encryptedDataLength); + if (!out) throw new Error('PSS sign: output overflow'); + return out; + } + + verify(buffer: Uint8Array, signature: Uint8Array): boolean { + const hash = this.hash(); + const emLen = Math.ceil((this.key.keySize - 1) / 8); + // RFC 8017 §8.1.2 step 2.b: signature-representative out of range + // (or any other RSA-primitive failure) yields "invalid signature", + // not a thrown error. + let m: Uint8Array | null; + try { + m = this.key.$doPublic(new this.key.BI(signature)).toBuffer(emLen); + } catch { + return false; + } + if (!m) return false; + const mHash = this.options.backend.digest(hash, buffer); + return this.emsaPssVerify(mHash, m, this.key.keySize - 1); + } + + /** EMSA-PSS-ENCODE — RFC 3447 §9.1.1 */ + private emsaPssEncode(mHash: Uint8Array, emBits: number): Uint8Array { + const hash = this.hash(); + const mgf = this.mgf(); + const sLen = this.saltLen(); + const hLen = DIGEST_LENGTH[hash]; + const emLen = Math.ceil(emBits / 8); + + if (emLen < hLen + sLen + 2) { + throw new Error( + `Output length passed to emBits(${emBits}) is too small for the options specified(${hash}, ${sLen}). To fix this issue increase the value of emBits. (minimum size: ${8 * hLen + 8 * sLen + 9})`, + ); + } + + const salt = this.options.backend.randomBytes(sLen); + + const mPrime = new Uint8Array(8 + hLen + sLen); + mPrime.set(mHash, 8); + mPrime.set(salt, 8 + mHash.length); + + const H = this.options.backend.digest(hash, mPrime); + + const DB = new Uint8Array(emLen - hLen - 1); + DB[emLen - hLen - 1 - sLen - 1] = 0x01; + DB.set(salt, emLen - hLen - 1 - sLen); + + const dbMask = mgf(H, DB.length, hash); + for (let i = 0; i < DB.length; i++) DB[i] = (DB[i] as number) ^ (dbMask[i] as number); + + const bits = 8 * emLen - emBits; + const mask = 0xff ^ (((0xff >> (8 - bits)) << (8 - bits)) & 0xff); + DB[0] = (DB[0] as number) & mask; + + const EM = new Uint8Array(emLen); + EM.set(DB, 0); + EM.set(H, DB.length); + EM[EM.length - 1] = 0xbc; + return EM; + } + + /** + * EMSA-PSS-VERIFY per RFC 8017 §9.1.2. All input-dependent checks + * (trailer byte, leftmost-bits zero, PS-zeros, separator 0x01, H == H') + * accumulate into a single `bad` flag with one `return bad === 0` at the + * end. PSS verify operates on public data, so this is hygiene rather + * than a tight side-channel requirement — but RFC step 11 mandates + * evaluating all checks before deciding. + */ + private emsaPssVerify(mHash: Uint8Array, EM: Uint8Array, emBits: number): boolean { + const hash = this.hash(); + const mgf = this.mgf(); + const sLen = this.saltLen(); + const hLen = DIGEST_LENGTH[hash]; + const emLen = Math.ceil(emBits / 8); + + // Geometry preconditions: configured by the caller, not derived from + // attacker input — early return is safe. + if (emLen < hLen + sLen + 2) return false; + if (EM.length !== emLen) return false; + + let bad = 0; + + // RFC step 4: trailer byte must be 0xbc. + bad |= (EM[EM.length - 1] as number) ^ 0xbc; + + const DB = EM.slice(0, emLen - hLen - 1); + const bits = 8 * emLen - emBits; + + // RFC step 6: leftmost (8*emLen - emBits) bits of maskedDB[0] must be 0. + let topMask = 0; + for (let i = 0; i < bits; i++) topMask |= 1 << (7 - i); + bad |= (DB[0] as number) & topMask; + + const H = EM.subarray(emLen - hLen - 1, emLen - 1); + const dbMask = mgf(H, DB.length, hash); + for (let i = 0; i < DB.length; i++) DB[i] = (DB[i] as number) ^ (dbMask[i] as number); + + // RFC step 9: zero the masked top bits of DB[0] after unmasking. + const adjustedMask = 0xff ^ (((0xff >> (8 - bits)) << (8 - bits)) & 0xff); + DB[0] = (DB[0] as number) & adjustedMask; + + // RFC step 10: DB = PS (all zeros) || 0x01 || salt, where + // |PS| = emLen - hLen - sLen - 2, so 0x01 sits at index |PS| of DB. + const sepIdx = emLen - hLen - sLen - 2; + for (let i = 0; i < DB.length; i++) { + const b = DB[i] as number; + if (i < sepIdx) { + bad |= b; // must be 0x00 + } else if (i === sepIdx) { + bad |= b ^ 0x01; // must be 0x01 + } + // i > sepIdx: salt, no check (validated via H' below) + } + + // RFC steps 12-13: recompute H' = Hash(0x00⁸ || mHash || salt) and compare. + const salt = DB.subarray(DB.length - sLen); + const mPrime = new Uint8Array(8 + hLen + sLen); + mPrime.set(mHash, 8); + mPrime.set(salt, 8 + mHash.length); + const HPrime = this.options.backend.digest(hash, mPrime); + + bad |= constantTimeEqual(H, HPrime) ? 0 : 1; + + return bad === 0; + } +} + +export const pssScheme = { + isEncryption: false as const, + isSignature: true as const, + makeScheme(key: RSAKey, options: SchemeOptions): SignatureScheme { + return new PssScheme(key, options); + }, +}; diff --git a/src/schemes/schemes.js b/src/schemes/schemes.js deleted file mode 100644 index 3eb8261..0000000 --- a/src/schemes/schemes.js +++ /dev/null @@ -1,23 +0,0 @@ -module.exports = { - pkcs1: require('./pkcs1'), - pkcs1_oaep: require('./oaep'), - pss: require('./pss'), - - /** - * Check if scheme has padding methods - * @param scheme {string} - * @returns {Boolean} - */ - isEncryption: function (scheme) { - return module.exports[scheme] && module.exports[scheme].isEncryption; - }, - - /** - * Check if scheme has sign/verify methods - * @param scheme {string} - * @returns {Boolean} - */ - isSignature: function (scheme) { - return module.exports[scheme] && module.exports[scheme].isSignature; - } -}; \ No newline at end of file diff --git a/src/schemes/types.ts b/src/schemes/types.ts new file mode 100644 index 0000000..69883c8 --- /dev/null +++ b/src/schemes/types.ts @@ -0,0 +1,49 @@ +import type { CryptoBackend, HashingAlgorithm } from '../crypto/types.js'; + +export type MaskGenerationFunction = ( + seed: Uint8Array, + maskLength: number, + hash: HashingAlgorithm, +) => Uint8Array; + +export interface EncryptionSchemeOptions { + /** RSA padding constant (PKCS#1 = 1, OAEP = 4, RSA_NO_PADDING = 3). */ + padding?: number; + /** Hash to use for OAEP (default sha1). */ + hash?: HashingAlgorithm; + /** Label byte string for OAEP (default empty). */ + label?: Uint8Array; + /** Custom MGF (default MGF1). */ + mgf?: MaskGenerationFunction; +} + +export interface SigningSchemeOptions { + /** Hash to use (default sha256 for PKCS#1, sha1 for PSS). */ + hash?: HashingAlgorithm; + /** Salt length for PSS (default 20). */ + saltLength?: number; + /** Custom MGF for PSS (default MGF1). */ + mgf?: MaskGenerationFunction; +} + +export interface SchemeOptions { + signingScheme: 'pkcs1' | 'pss'; + encryptionScheme: 'pkcs1' | 'pkcs1_oaep'; + signingSchemeOptions: SigningSchemeOptions; + encryptionSchemeOptions: EncryptionSchemeOptions; + environment: 'node' | 'browser'; + backend: CryptoBackend; +} + +/** Encryption-padding side of a scheme (PKCS#1 v1.5 type 2, or OAEP). */ +export interface EncryptionSchemeImpl { + maxMessageLength(): number; + encPad(buffer: Uint8Array, opts?: { type?: number }): Uint8Array; + encUnPad(buffer: Uint8Array, opts?: { type?: number }): Uint8Array | null; +} + +/** Signing side of a scheme (PKCS#1 v1.5 type 1 with DigestInfo, or PSS). */ +export interface SignatureScheme { + sign(buffer: Uint8Array): Uint8Array; + verify(buffer: Uint8Array, signature: Uint8Array): boolean; +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..6173042 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,203 @@ +import type { HashingAlgorithm } from './crypto/types.js'; +import type { + EncryptionSchemeOptions, + MaskGenerationFunction, + SigningSchemeOptions, +} from './schemes/types.js'; + +export type Environment = 'node' | 'browser'; + +export type EncryptionScheme = 'pkcs1' | 'pkcs1_oaep'; +export type SigningScheme = 'pkcs1' | 'pss'; + +/** + * Shorthand scheme-hash combinations accepted by `signingScheme`. + * Parsed at runtime as `-`. + */ +export type SigningSchemeHash = + | 'pkcs1-ripemd160' + | 'pkcs1-md4' + | 'pkcs1-md5' + | 'pkcs1-sha' + | 'pkcs1-sha1' + | 'pkcs1-sha224' + | 'pkcs1-sha256' + | 'pkcs1-sha384' + | 'pkcs1-sha512' + | 'pss-ripemd160' + | 'pss-md4' + | 'pss-md5' + | 'pss-sha' + | 'pss-sha1' + | 'pss-sha224' + | 'pss-sha256' + | 'pss-sha384' + | 'pss-sha512'; + +/** PEM-encoded key format identifiers (string output / input). */ +export type FormatPem = + | 'private' + | 'public' + | 'pkcs1' + | 'pkcs1-pem' + | 'pkcs1-private' + | 'pkcs1-private-pem' + | 'pkcs1-public' + | 'pkcs1-public-pem' + | 'pkcs8' + | 'pkcs8-pem' + | 'pkcs8-private' + | 'pkcs8-private-pem' + | 'pkcs8-public' + | 'pkcs8-public-pem' + | 'openssh-public' + | 'openssh-private'; + +/** DER-encoded key format identifiers (Uint8Array output / input). */ +export type FormatDer = + | 'pkcs1-der' + | 'pkcs1-private-der' + | 'pkcs1-public-der' + | 'pkcs8-der' + | 'pkcs8-private-der' + | 'pkcs8-public-der'; + +/** Raw private components format identifiers. */ +export type FormatComponentsPrivate = + | 'components' + | 'components-pem' + | 'components-der' + | 'components-private' + | 'components-private-pem' + | 'components-private-der'; + +/** Raw public components format identifiers. */ +export type FormatComponentsPublic = + | 'components-public' + | 'components-public-pem' + | 'components-public-der'; + +/** Any supported key format identifier. */ +export type Format = FormatPem | FormatDer | FormatComponentsPrivate | FormatComponentsPublic; + +export interface KeyComponentsPrivate { + n: Uint8Array; + e: Uint8Array | number; + d: Uint8Array; + p: Uint8Array; + q: Uint8Array; + dmp1: Uint8Array; + dmq1: Uint8Array; + coeff: Uint8Array; +} + +export interface KeyComponentsPublic { + n: Uint8Array; + e: Uint8Array | number; +} + +/** Key material accepted by `importKey` / the constructor. */ +export type Key = string | Uint8Array | KeyComponentsPrivate | KeyComponentsPublic; + +/** Plaintext data accepted by `encrypt` / `sign`. */ +export type Data = string | object | unknown[]; + +/** `{ b: bits }` shorthand for `new NodeRSA({ b: 2048 })`. */ +export interface KeyBits { + b: number; +} + +/** + * Encoding tags accepted by encrypt/decrypt/sign/verify for converting + * between strings and bytes. `'json'` is a decrypt-only sentinel and is + * declared separately on `decrypt` / `decryptPublic` overloads, not here. + * + * Note: legacy v1 accepted `'ascii'`, `'utf16le'`, `'ucs2'` by name but + * routed them through `Buffer.from` aliases that v2 no longer wires; only + * the encodings below are implemented end-to-end. + */ +export type Encoding = 'buffer' | 'binary' | 'latin1' | 'hex' | 'base64' | 'utf8'; + +export interface AdvancedEncryptionSchemePKCS1 { + scheme: 'pkcs1'; + /** OpenSSL RSA padding constant (currently informational; runtime uses scheme name). */ + padding?: number; +} + +export interface AdvancedEncryptionSchemePKCS1OAEP { + scheme: 'pkcs1_oaep'; + hash?: HashingAlgorithm; + label?: Uint8Array; + mgf?: MaskGenerationFunction; +} + +export type AdvancedEncryptionScheme = + | AdvancedEncryptionSchemePKCS1 + | AdvancedEncryptionSchemePKCS1OAEP; + +export interface AdvancedSigningSchemePSS { + scheme: 'pss'; + hash?: HashingAlgorithm; + saltLength?: number; + mgf?: MaskGenerationFunction; +} + +export interface AdvancedSigningSchemePKCS1 { + scheme: 'pkcs1'; + hash?: HashingAlgorithm; +} + +export type AdvancedSigningScheme = AdvancedSigningSchemePSS | AdvancedSigningSchemePKCS1; + +export interface NodeRSAGenerateOptions { + /** Bits in the modulus. */ + b?: number; + /** Public exponent. */ + e?: number; +} + +/** Which BigInteger implementation NodeRSA should use under the hood. */ +export type BigIntegerImpl = 'jsbn' | 'native'; + +export interface NodeRSAOptions { + signingScheme?: + | SigningScheme + | SigningSchemeHash + | AdvancedSigningScheme + | (SigningSchemeOptions & { scheme?: SigningScheme }); + encryptionScheme?: + | EncryptionScheme + | AdvancedEncryptionScheme + | (EncryptionSchemeOptions & { scheme?: EncryptionScheme }); + environment?: Environment; + /** + * Switch the BigInteger backend. Browser bundle defaults to `'native'`, + * Node bundle defaults to `'jsbn'`. `'native'` silently falls back to + * `'jsbn'` on runtimes without `globalThis.BigInt`. + * + * Must be set BEFORE the key is imported/generated — i.e. as part of the + * constructor's options or before any importKey/generateKeyPair call. + * Calling `setOptions({ bigIntImpl })` on a NodeRSA whose `keyPair` + * already has components throws, because the existing BigInteger objects + * carry the old implementation's class identity and can't interoperate. + */ + bigIntImpl?: BigIntegerImpl; + /** Used for tests; not part of the public API surface. */ + key?: unknown; +} + +export type { + EncryptionSchemeOptions, + HashingAlgorithm, + MaskGenerationFunction, + SigningSchemeOptions, +}; + +export interface ResolvedOptions { + signingScheme: SigningScheme; + signingSchemeOptions: SigningSchemeOptions; + encryptionScheme: EncryptionScheme; + encryptionSchemeOptions: EncryptionSchemeOptions; + environment: Environment; + bigIntImpl: BigIntegerImpl; +} diff --git a/src/utils.js b/src/utils.js deleted file mode 100644 index eac573f..0000000 --- a/src/utils.js +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Utils functions - * - */ - -var crypt = require('crypto'); - -/** - * Break string str each maxLen symbols - * @param str - * @param maxLen - * @returns {string} - */ -module.exports.linebrk = function (str, maxLen) { - var res = ''; - var i = 0; - while (i + maxLen < str.length) { - res += str.substring(i, i + maxLen) + "\n"; - i += maxLen; - } - return res + str.substring(i, str.length); -}; - -module.exports.detectEnvironment = function () { - if (typeof(window) !== 'undefined' && window && !(process && process.title === 'node')) { - return 'browser'; - } - - return 'node'; -}; - -/** - * Trying get a 32-bit unsigned integer from the partial buffer - * @param buffer - * @param offset - * @returns {Number} - */ -module.exports.get32IntFromBuffer = function (buffer, offset) { - offset = offset || 0; - var size = 0; - if ((size = buffer.length - offset) > 0) { - if (size >= 4) { - return buffer.readUIntBE(offset, size); - } else { - var res = 0; - for (var i = offset + size, d = 0; i > offset; i--, d += 2) { - res += buffer[i - 1] * Math.pow(16, d); - } - return res; - } - } else { - return NaN; - } -}; - -module.exports._ = { - isObject: function (value) { - var type = typeof value; - return !!value && (type == 'object' || type == 'function'); - }, - - isString: function (value) { - return typeof value == 'string' || value instanceof String; - }, - - isNumber: function (value) { - return typeof value == 'number' || !isNaN(parseFloat(value)) && isFinite(value); - }, - - /** - * Returns copy of `obj` without `removeProp` field. - * @param obj - * @param removeProp - * @returns Object - */ - omit: function (obj, removeProp) { - var newObj = {}; - for (var prop in obj) { - if (!obj.hasOwnProperty(prop) || prop === removeProp) { - continue; - } - newObj[prop] = obj[prop]; - } - - return newObj; - } -}; - -/** - * Strips everything around the opening and closing lines, including the lines - * themselves. - */ -module.exports.trimSurroundingText = function (data, opening, closing) { - var trimStartIndex = 0; - var trimEndIndex = data.length; - - var openingBoundaryIndex = data.indexOf(opening); - if (openingBoundaryIndex >= 0) { - trimStartIndex = openingBoundaryIndex + opening.length; - } - - var closingBoundaryIndex = data.indexOf(closing, openingBoundaryIndex); - if (closingBoundaryIndex >= 0) { - trimEndIndex = closingBoundaryIndex; - } - - return data.substring(trimStartIndex, trimEndIndex); -} \ No newline at end of file diff --git a/src/utils/text-utils.ts b/src/utils/text-utils.ts new file mode 100644 index 0000000..044cb3e --- /dev/null +++ b/src/utils/text-utils.ts @@ -0,0 +1,34 @@ +/** Insert `\n` every `maxLen` chars. */ +export function linebrk(str: string, maxLen: number): string { + let out = ''; + let i = 0; + while (i + maxLen < str.length) { + out += `${str.substring(i, i + maxLen)}\n`; + i += maxLen; + } + return out + str.substring(i); +} + +/** + * Extract the body between `opening` and `closing` markers. Returns the + * input unchanged if markers aren't found. Throws if a second `opening` + * appears after the first `closing` (multi-block input — RFC 7468 §3 + * forbids ambiguity for PEM-style inputs). + */ +export function trimSurroundingText(data: string, opening: string, closing: string): string { + let start = 0; + let end = data.length; + const openIdx = data.indexOf(opening); + const closeIdx = openIdx >= 0 ? data.indexOf(closing, openIdx) : -1; + // Reject ambiguous multi-block input: a second opening marker after the + // first close means the input contains more than one block. + if (openIdx >= 0 && closeIdx >= 0) { + const secondOpen = data.indexOf(opening, closeIdx + closing.length); + if (secondOpen >= 0) { + throw new Error(`multiple ${opening} blocks — refusing ambiguous input`); + } + } + if (openIdx >= 0) start = openIdx + opening.length; + if (closeIdx >= 0) end = closeIdx; + return data.substring(start, end); +} diff --git a/test/asn1/fixture-roundtrip.spec.ts b/test/asn1/fixture-roundtrip.spec.ts new file mode 100644 index 0000000..ece725a --- /dev/null +++ b/test/asn1/fixture-roundtrip.spec.ts @@ -0,0 +1,118 @@ +import { readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; +import { DerReader, DerWriter } from '../../src/asn1/index.js'; +import { toHex } from '../../src/crypto/bytes.js'; + +const here = dirname(fileURLToPath(import.meta.url)); +const keysDir = resolve(here, '../../test/keys'); + +function loadDer(name: string): Uint8Array { + const buf = readFileSync(resolve(keysDir, name)); + return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength); +} + +/** + * Generic DER tree walker. Recurses into SEQUENCE and copies primitive TLVs + * verbatim. Round-tripping a well-formed DER document through this should + * produce byte-identical output. + */ +function roundTrip(bytes: Uint8Array): Uint8Array { + const w = new DerWriter(); + copyAll(new DerReader(bytes), w); + return w.toBytes(); +} + +function copyAll(r: DerReader, w: DerWriter): void { + while (r.hasMore()) { + const { tag, value } = r.readTlv(); + if (tag === 0x30 /* SEQUENCE */) { + w.startSequence(); + copyAll(new DerReader(value), w); + w.endSequence(); + } else { + w.writeTlv(tag, value); + } + } +} + +describe('DER fixture round-trip', () => { + const FIXTURES = [ + 'private_pkcs1.der', + 'private_pkcs8.der', + 'public_pkcs1.der', + 'public_pkcs8.der', + ]; + + for (const name of FIXTURES) { + it(`${name} round-trips byte-identical`, () => { + const original = loadDer(name); + const out = roundTrip(original); + expect(toHex(out)).toBe(toHex(original)); + }); + } +}); + +describe('PKCS#1 public key fixture inspection', () => { + it('parses the public key structure', () => { + const bytes = loadDer('public_pkcs1.der'); + const seq = new DerReader(bytes).readSequence(); + const n = seq.readInteger(); + const e = seq.readSmallInteger(); + expect(e).toBe(65537); + // Modulus byte length (with optional 0x00 sign prefix) is multiple-of-8 + // bytes plus 0..1. For a 1024-bit fixture: 128 or 129. + expect(n.length).toBeGreaterThan(60); + expect(n.length % 8 === 0 || (n.length - 1) % 8 === 0).toBe(true); + }); +}); + +describe('PKCS#1 private key fixture inspection', () => { + it('parses the private key structure', () => { + const bytes = loadDer('private_pkcs1.der'); + const seq = new DerReader(bytes).readSequence(); + expect(seq.readSmallInteger()).toBe(0); // version + const n = seq.readInteger(); + const e = seq.readSmallInteger(); + seq.readInteger(); // d + const p = seq.readInteger(); + const q = seq.readInteger(); + seq.readInteger(); // dmp1 + seq.readInteger(); // dmq1 + seq.readInteger(); // coeff + expect(e).toBe(65537); + // p and q should each be roughly half the modulus length. + expect(Math.abs(p.length - q.length)).toBeLessThanOrEqual(1); + expect(p.length * 2).toBeGreaterThanOrEqual(n.length - 2); + }); +}); + +describe('PKCS#8 public key fixture inspection', () => { + it('contains the rsaEncryption OID and an embedded SubjectPublicKey BIT STRING', () => { + const bytes = loadDer('public_pkcs8.der'); + const outer = new DerReader(bytes).readSequence(); + const header = outer.readSequence(); + expect(header.readOid()).toBe('1.2.840.113549.1.1.1'); + header.readNull(); + const bitContent = outer.readBitString(); + const inner = new DerReader(bitContent).readSequence(); + inner.readInteger(); // n + expect(inner.readSmallInteger()).toBe(65537); + }); +}); + +describe('PKCS#8 private key fixture inspection', () => { + it('contains version=0, rsaEncryption OID, and an embedded OCTET STRING private key body', () => { + const bytes = loadDer('private_pkcs8.der'); + const outer = new DerReader(bytes).readSequence(); + expect(outer.readSmallInteger()).toBe(0); + const header = outer.readSequence(); + expect(header.readOid()).toBe('1.2.840.113549.1.1.1'); + header.readNull(); + const inner = new DerReader(outer.readOctetString()).readSequence(); + expect(inner.readSmallInteger()).toBe(0); + inner.readInteger(); // n + expect(inner.readSmallInteger()).toBe(65537); + }); +}); diff --git a/test/asn1/negative.spec.ts b/test/asn1/negative.spec.ts new file mode 100644 index 0000000..e006381 --- /dev/null +++ b/test/asn1/negative.spec.ts @@ -0,0 +1,144 @@ +import { describe, expect, it } from 'vitest'; +import { DerReader, Tag } from '../../src/asn1/index.js'; +import { fromHex } from '../../src/crypto/bytes.js'; + +/** + * Regression coverage for the strict-DER and bounds-check fixes in + * src/asn1/reader.ts. A future refactor that loosens any of these checks + * should make a test here fail. + */ + +describe('DerReader — TLV bounds and structural errors', () => { + it('rejects truncated TLV where length exceeds remaining buffer', () => { + // SEQUENCE, length=10, but only 2 content octets follow + const der = fromHex('300a0500'); + expect(() => new DerReader(der).readSequence()).toThrow(/exceeds buffer/); + }); + + it('rejects unexpected end of input', () => { + const der = new Uint8Array(0); + expect(() => new DerReader(der).readTlv()).toThrow(/unexpected end of input/); + }); + + it('rejects missing length octet', () => { + const der = fromHex('30'); // tag only, no length byte + expect(() => new DerReader(der).readSequence()).toThrow(/missing length octet/); + }); + + it('rejects truncated long-form length octets', () => { + // SEQUENCE with numBytes=2 but only 1 length byte follows + const der = fromHex('308201'); + expect(() => new DerReader(der).readSequence()).toThrow(/truncated length/); + }); + + it('rejects tag mismatch', () => { + // OCTET STRING (0x04) where SEQUENCE (0x30) expected + const der = fromHex('040100'); + expect(() => new DerReader(der).readSequence()).toThrow(/expected SEQUENCE/); + }); + + it('rejects unsupported length width (> 4 bytes)', () => { + // numBytes = 5 in long-form indicator + const der = fromHex('308500000000000000'); + expect(() => new DerReader(der).readSequence()).toThrow(/unsupported length width/); + }); + + it('rejects indefinite-length encoding (BER-only)', () => { + // 0x80 means indefinite length — illegal in DER + const der = fromHex('308000000000'); + expect(() => new DerReader(der).readSequence()).toThrow(/indefinite length/); + }); +}); + +describe('DerReader — non-canonical length (L5)', () => { + it('rejects long-form length for value < 128', () => { + // SEQUENCE { OCTET STRING ""; } with length 2 encoded as long-form 0x81 0x02 + const der = fromHex('3081020400'); + expect(() => new DerReader(der).readSequence()).toThrow(/non-canonical length/); + }); + + it('rejects long-form length with leading zero byte', () => { + // SEQUENCE length 255 with redundant leading zero in long-form: 0x82 0x00 0xff + // Minimum form would be 0x81 0xff. + const der = new Uint8Array([0x30, 0x82, 0x00, 0xff, ...new Uint8Array(255).fill(0)]); + expect(() => new DerReader(der).readSequence()).toThrow(/non-canonical length/); + }); + + it('accepts short-form (len=127) and long-form (len=128) at the boundary', () => { + // Short-form at 127: 0x7f + const short = new Uint8Array([0x04, 0x7f, ...new Uint8Array(127)]); + expect(() => new DerReader(short).readOctetString()).not.toThrow(); + // Long-form at 128: 0x81 0x80 + const long = new Uint8Array([0x04, 0x81, 0x80, ...new Uint8Array(128)]); + expect(() => new DerReader(long).readOctetString()).not.toThrow(); + }); +}); + +describe('DerReader — non-canonical INTEGER (L1)', () => { + it('rejects empty INTEGER content', () => { + // INTEGER, length=0 — illegal per X.690 §8.3.1 + const der = fromHex('0200'); + expect(() => new DerReader(der).readInteger()).toThrow(/at least one content octet/); + }); + + it('rejects redundant leading 0x00 on positive integer', () => { + // 0x00 0x42 — minimum form is 0x42 (since 0x42 has MSB clear, sign byte is redundant) + const der = fromHex('02020042'); + expect(() => new DerReader(der).readInteger()).toThrow(/non-canonical INTEGER/); + }); + + it('rejects redundant leading 0xff on negative integer', () => { + // 0xff 0x7f — minimum form is 0x7f? Actually 0xff means -1*256+0x7f = -129 in DER negatives. + // For negative: leading 0xff allowed only if next byte's MSB is clear. + // Here next byte 0x7f has MSB clear, so the 0xff is REQUIRED (would be -129). + // We want a redundant case: 0xff 0xff (= -1) — minimum is just 0xff. + const der = fromHex('0202ffff'); + expect(() => new DerReader(der).readInteger()).toThrow(/non-canonical INTEGER/); + }); + + it('accepts canonical 0x00 0x80 (positive 128 with required sign byte)', () => { + // 0x80 alone would be -128; the leading 0x00 is required to express positive 128. + const der = fromHex('02020080'); + expect(() => new DerReader(der).readInteger()).not.toThrow(); + }); + + it('accepts canonical single-byte integers', () => { + for (const hex of ['020100', '020101', '02017f', '0201ff']) { + const der = fromHex(hex); + expect(() => new DerReader(der).readInteger()).not.toThrow(); + } + }); +}); + +describe('DerReader — BIT STRING and OID edge cases', () => { + it('rejects BIT STRING with non-zero unused-bits octet', () => { + // 0x03 (BIT STRING), length 2, unused-bits=3, content 0xff + const der = fromHex('030203ff'); + expect(() => new DerReader(der).readBitString()).toThrow(/non-zero unused bits/); + }); + + it('rejects empty BIT STRING (no unused-bits octet)', () => { + const der = fromHex('0300'); + expect(() => new DerReader(der).readBitString()).toThrow(/empty BIT STRING/); + }); + + it('rejects NULL with non-empty content', () => { + // NULL must have length 0; here length 1 + const der = fromHex('050100'); + expect(() => new DerReader(der).readNull()).toThrow(/zero-length/); + }); + + it('rejects empty OID', () => { + const der = fromHex('0600'); + expect(() => new DerReader(der).readOid()).toThrow(/empty OID/); + }); +}); + +describe('DerReader — readTlv tag filtering', () => { + it('returns tag and value for unfiltered reads', () => { + const der = fromHex('040548656c6c6f'); // OCTET STRING "Hello" + const { tag, value } = new DerReader(der).readTlv(); + expect(tag).toBe(Tag.OCTET_STRING); + expect(value.length).toBe(5); + }); +}); diff --git a/test/asn1/primitives.spec.ts b/test/asn1/primitives.spec.ts new file mode 100644 index 0000000..edd7b8e --- /dev/null +++ b/test/asn1/primitives.spec.ts @@ -0,0 +1,264 @@ +import { describe, expect, it } from 'vitest'; +import { DerReader, DerWriter, OID, Tag } from '../../src/asn1/index.js'; +import { fromHex, toHex } from '../../src/crypto/bytes.js'; + +describe('INTEGER', () => { + it('encodes small numbers', () => { + const w = new DerWriter(); + w.writeInteger(0); + expect(toHex(w.toBytes())).toBe('020100'); + }); + + it('encodes 1 as 02 01 01', () => { + const w = new DerWriter(); + w.writeInteger(1); + expect(toHex(w.toBytes())).toBe('020101'); + }); + + it('encodes 127 without a leading zero', () => { + const w = new DerWriter(); + w.writeInteger(127); + expect(toHex(w.toBytes())).toBe('02017f'); + }); + + it('encodes 128 with a leading zero (positive marker)', () => { + const w = new DerWriter(); + w.writeInteger(128); + expect(toHex(w.toBytes())).toBe('02020080'); + }); + + it('encodes 65537 (standard RSA public exponent)', () => { + const w = new DerWriter(); + w.writeInteger(65537); + expect(toHex(w.toBytes())).toBe('0203010001'); + }); + + it('round-trips a multi-byte byte-array INTEGER', () => { + const value = fromHex('00deadbeef'); + const w = new DerWriter(); + w.writeInteger(value); + const r = new DerReader(w.toBytes()); + expect(toHex(r.readInteger())).toBe('00deadbeef'); + }); + + it('prepends a leading zero when the byte-array MSB is set', () => { + const value = fromHex('deadbeef'); + const w = new DerWriter(); + w.writeInteger(value); + const r = new DerReader(w.toBytes()); + // Reader returns the raw DER content bytes (with the leading zero) + expect(toHex(r.readInteger())).toBe('00deadbeef'); + }); + + it('readSmallInteger decodes back', () => { + for (const n of [0, 1, 127, 128, 255, 256, 65535, 65537, 1234567]) { + const w = new DerWriter(); + w.writeInteger(n); + const r = new DerReader(w.toBytes()); + expect(r.readSmallInteger()).toBe(n); + } + }); +}); + +describe('NULL', () => { + it('encodes as 05 00', () => { + const w = new DerWriter(); + w.writeNull(); + expect(toHex(w.toBytes())).toBe('0500'); + }); + + it('readNull consumes the TLV', () => { + const w = new DerWriter(); + w.writeNull(); + w.writeInteger(7); + const r = new DerReader(w.toBytes()); + r.readNull(); + expect(r.readSmallInteger()).toBe(7); + }); + + it('readNull rejects non-zero length', () => { + const r = new DerReader(new Uint8Array([0x05, 0x01, 0x00])); + expect(() => r.readNull()).toThrow(/zero-length/); + }); +}); + +describe('OBJECT IDENTIFIER', () => { + it('encodes rsaEncryption (1.2.840.113549.1.1.1)', () => { + const w = new DerWriter(); + w.writeOid(OID.RSA_ENCRYPTION); + // Known canonical DER for rsaEncryption: 06 09 2A 86 48 86 F7 0D 01 01 01 + expect(toHex(w.toBytes())).toBe('06092a864886f70d010101'); + }); + + it('round-trips arbitrary OIDs', () => { + const cases = [ + '1.2.840.113549.1.1.1', + '0.0', + '1.2.3', + '2.999.1234567', // tests large arc + '1.3.6.1.4.1.311.2.1.4', // Microsoft OID — multibyte arcs + ]; + for (const oid of cases) { + const w = new DerWriter(); + w.writeOid(oid); + const r = new DerReader(w.toBytes()); + expect(r.readOid()).toBe(oid); + } + }); + + it('rejects invalid leading arcs', () => { + expect(() => new DerWriter().writeOid('3.0')).toThrow(); + expect(() => new DerWriter().writeOid('0.40')).toThrow(); + }); +}); + +describe('SEQUENCE', () => { + it('encodes an empty SEQUENCE as 30 00', () => { + const w = new DerWriter(); + w.startSequence(); + w.endSequence(); + expect(toHex(w.toBytes())).toBe('3000'); + }); + + it('encodes a SEQUENCE { INTEGER, NULL }', () => { + const w = new DerWriter(); + w.startSequence(); + w.writeInteger(0); + w.writeNull(); + w.endSequence(); + // 30 05 02 01 00 05 00 + expect(toHex(w.toBytes())).toBe('30050201000500'); + }); + + it('supports nested sequences', () => { + const w = new DerWriter(); + w.startSequence(); + w.startSequence(); + w.writeOid(OID.RSA_ENCRYPTION); + w.writeNull(); + w.endSequence(); + w.writeInteger(42); + w.endSequence(); + + const outer = new DerReader(w.toBytes()).readSequence(); + const header = outer.readSequence(); + expect(header.readOid()).toBe(OID.RSA_ENCRYPTION); + header.readNull(); + expect(outer.readSmallInteger()).toBe(42); + }); + + it('throws when endSequence has no matching start', () => { + const w = new DerWriter(); + expect(() => w.endSequence()).toThrow(); + }); + + it('throws when toBytes called with unclosed sequences', () => { + const w = new DerWriter(); + w.startSequence(); + expect(() => w.toBytes()).toThrow(/unclosed/); + }); +}); + +describe('BIT STRING', () => { + it('writes with a leading unused-bits byte of 0', () => { + const w = new DerWriter(); + w.writeBitString(new Uint8Array([0xaa, 0xbb])); + expect(toHex(w.toBytes())).toBe('0303' + '00aabb'); + }); + + it('round-trips via readBitString (asserts unused=0)', () => { + const w = new DerWriter(); + w.writeBitString(new Uint8Array([1, 2, 3])); + const r = new DerReader(w.toBytes()); + expect(toHex(r.readBitString())).toBe('010203'); + }); + + it('readBitStringRaw includes the unused-bits byte', () => { + const w = new DerWriter(); + w.writeBitString(new Uint8Array([1, 2, 3])); + const r = new DerReader(w.toBytes()); + expect(toHex(r.readBitStringRaw())).toBe('00010203'); + }); + + it('readBitString rejects non-zero unused bits', () => { + // 03 02 04 ff — a BIT STRING with 4 unused bits and one content byte + const r = new DerReader(new Uint8Array([0x03, 0x02, 0x04, 0xff])); + expect(() => r.readBitString()).toThrow(/unused bits/); + }); +}); + +describe('OCTET STRING', () => { + it('round-trips', () => { + const w = new DerWriter(); + w.writeOctetString(fromHex('0102deadbeef')); + const r = new DerReader(w.toBytes()); + expect(toHex(r.readOctetString())).toBe('0102deadbeef'); + }); +}); + +describe('Length codec', () => { + it('short form (n < 128)', () => { + const w = new DerWriter(); + w.writeOctetString(new Uint8Array(127)); + const bytes = w.toBytes(); + expect(bytes[0]).toBe(Tag.OCTET_STRING); + expect(bytes[1]).toBe(127); // short form + }); + + it('long form 1-byte length (128..255)', () => { + const w = new DerWriter(); + w.writeOctetString(new Uint8Array(200)); + const bytes = w.toBytes(); + expect(bytes[0]).toBe(Tag.OCTET_STRING); + expect(bytes[1]).toBe(0x81); // 1 byte follows + expect(bytes[2]).toBe(200); + }); + + it('long form 2-byte length (256..65535)', () => { + const w = new DerWriter(); + w.writeOctetString(new Uint8Array(300)); + const bytes = w.toBytes(); + expect(bytes[1]).toBe(0x82); + expect((bytes[2] as number) * 256 + (bytes[3] as number)).toBe(300); + }); + + it('reader handles all length encodings', () => { + for (const size of [0, 1, 127, 128, 255, 256, 1000, 65535]) { + const w = new DerWriter(); + w.writeOctetString(new Uint8Array(size)); + const r = new DerReader(w.toBytes()); + expect(r.readOctetString().length).toBe(size); + } + }); + + it('reader rejects indefinite length', () => { + const r = new DerReader(new Uint8Array([0x04, 0x80, 0x00, 0x00])); + expect(() => r.readOctetString()).toThrow(/indefinite/); + }); +}); + +describe('Reader semantics', () => { + it('asserts the expected tag', () => { + const w = new DerWriter(); + w.writeInteger(42); + const r = new DerReader(w.toBytes()); + expect(() => r.readOid()).toThrow(/expected/); + }); + + it('tracks position and remaining', () => { + const w = new DerWriter(); + w.writeInteger(1); + w.writeInteger(2); + const r = new DerReader(w.toBytes()); + expect(r.hasMore()).toBe(true); + r.readSmallInteger(); + expect(r.hasMore()).toBe(true); + r.readSmallInteger(); + expect(r.hasMore()).toBe(false); + }); + + it('rejects truncated input', () => { + const r = new DerReader(new Uint8Array([0x02, 0x10])); // INTEGER tag claiming 16 bytes + expect(() => r.readInteger()).toThrow(/exceeds buffer/); + }); +}); diff --git a/test/bigint/basic.spec.ts b/test/bigint/basic.spec.ts new file mode 100644 index 0000000..ca510c0 --- /dev/null +++ b/test/bigint/basic.spec.ts @@ -0,0 +1,231 @@ +import { beforeAll, describe, expect, it } from 'vitest'; +import { BigInteger, setBigIntegerBackend } from '../../src/bigint/big-integer.js'; +import { nodeBackend } from '../../src/crypto/backend.node.js'; + +beforeAll(() => { + setBigIntegerBackend(nodeBackend); +}); + +describe('BigInteger constants', () => { + it('ZERO and ONE are defined and round-trip via toString', () => { + expect(BigInteger.ZERO.toString(10)).toBe('0'); + expect(BigInteger.ONE.toString(10)).toBe('1'); + expect(BigInteger.ZERO.signum()).toBe(0); + expect(BigInteger.ONE.signum()).toBe(1); + }); +}); + +describe('BigInteger from string', () => { + it('parses decimal', () => { + const x = new BigInteger('123456789012345678901234567890', 10); + expect(x.toString(10)).toBe('123456789012345678901234567890'); + }); + + it('parses hex (with leading sign byte stripped on unsigned import)', () => { + const x = new BigInteger('1abcdef0', 16); + expect(x.toString(16)).toBe('1abcdef0'); + }); + + it('parses negative decimals', () => { + const x = new BigInteger('-42', 10); + expect(x.toString(10)).toBe('-42'); + expect(x.signum()).toBe(-1); + }); +}); + +describe('BigInteger arithmetic', () => { + const a = new BigInteger('100000000000000000000', 10); + const b = new BigInteger('3', 10); + + it('add', () => { + expect(a.add(b).toString(10)).toBe('100000000000000000003'); + }); + + it('subtract', () => { + expect(a.subtract(b).toString(10)).toBe('99999999999999999997'); + }); + + it('multiply', () => { + expect(a.multiply(b).toString(10)).toBe('300000000000000000000'); + }); + + it('divide and remainder', () => { + const [q, r] = a.divideAndRemainder(b); + expect(q.toString(10)).toBe('33333333333333333333'); + expect(r.toString(10)).toBe('1'); + }); + + it('square == multiply(this, this)', () => { + expect(a.square().toString(10)).toBe(a.multiply(a).toString(10)); + }); + + it('compareTo', () => { + expect(a.compareTo(b)).toBeGreaterThan(0); + expect(b.compareTo(a)).toBeLessThan(0); + expect(a.compareTo(a)).toBe(0); + }); +}); + +describe('BigInteger bit operations', () => { + it('bitLength on small values', () => { + expect(new BigInteger('1', 10).bitLength()).toBe(1); + expect(new BigInteger('7', 10).bitLength()).toBe(3); + expect(new BigInteger('8', 10).bitLength()).toBe(4); + expect(new BigInteger('255', 10).bitLength()).toBe(8); + expect(new BigInteger('256', 10).bitLength()).toBe(9); + }); + + it('shiftLeft / shiftRight round-trip', () => { + const x = new BigInteger('12345', 10); + expect(x.shiftLeft(100).shiftRight(100).toString(10)).toBe('12345'); + }); + + it('testBit', () => { + const x = new BigInteger('10', 10); // binary 1010 + expect(x.testBit(0)).toBe(false); + expect(x.testBit(1)).toBe(true); + expect(x.testBit(2)).toBe(false); + expect(x.testBit(3)).toBe(true); + }); +}); + +describe('BigInteger mod / modPow / modInverse', () => { + const five = new BigInteger('5', 10); + const seven = new BigInteger('7', 10); + const thirteen = new BigInteger('13', 10); + + it('mod', () => { + expect(new BigInteger('1000', 10).mod(seven).toString(10)).toBe('6'); + }); + + it('modPow: 5^3 mod 13 = 8', () => { + expect(five.modPow(new BigInteger('3', 10), thirteen).toString(10)).toBe('8'); + }); + + it('modPowInt: 5^3 mod 13 = 8', () => { + expect(five.modPowInt(3, thirteen).toString(10)).toBe('8'); + }); + + it('modInverse: 3^-1 mod 11 = 4', () => { + const inv = new BigInteger('3', 10).modInverse(new BigInteger('11', 10)); + expect(inv.toString(10)).toBe('4'); + }); + + it('modInverse round trip: (a * a^-1) mod m == 1', () => { + const a = new BigInteger('1234567890', 10); + const m = new BigInteger('100000007', 10); // prime + const inv = a.modInverse(m); + const product = a.multiply(inv).mod(m); + expect(product.toString(10)).toBe('1'); + }); + + it('modInverse returns 0 when gcd(a, m) ≠ 1 (no inverse)', () => { + // RSA blinding (src/rsa/key.ts:294) treats `rInv.signum() === 0` as + // "retry with fresh r" — so this branch must stay reachable. + // 2 has no inverse mod 4 because gcd(2,4)=2. + const noInv = new BigInteger('2', 10).modInverse(new BigInteger('4', 10)); + expect(noInv.signum()).toBe(0); + // 6 has no inverse mod 9 (gcd=3). + const noInv2 = new BigInteger('6', 10).modInverse(new BigInteger('9', 10)); + expect(noInv2.signum()).toBe(0); + }); + + it('modInverse rejects or returns 0 for a non-positive modulus (impl-dependent)', () => { + // jsbn returns 0 (no inverse) when modulus is 0/negative; native throws. + // Both behaviours are safe for callers, who treat `signum() === 0` as + // "no inverse, retry" (see src/rsa/key.ts:294). + const result = (() => { + try { + return new BigInteger('3', 10).modInverse(BigInteger.ZERO); + } catch { + return null; + } + })(); + if (result !== null) expect(result.signum()).toBe(0); + }); + + // Note: `0.modInverse(m)` is mathematically undefined and the two backends + // diverge — jsbn loops forever (v=0 inside the binary-extended-gcd inner + // loop), native returns 0. We don't construct that input anywhere in the + // library, so the behaviour-pinning test below is for valid (non-zero) `a` + // only. +}); + +describe('BigInteger gcd', () => { + it('coprime → 1', () => { + expect(new BigInteger('21', 10).gcd(new BigInteger('4', 10)).toString(10)).toBe('1'); + }); + + it('gcd(48, 18) = 6', () => { + expect(new BigInteger('48', 10).gcd(new BigInteger('18', 10)).toString(10)).toBe('6'); + }); + + it('gcd(n, 0) = n', () => { + expect(new BigInteger('42', 10).gcd(BigInteger.ZERO).toString(10)).toBe('42'); + }); +}); + +describe('BigInteger primality', () => { + it('detects known small primes', () => { + for (const n of [2, 3, 5, 7, 11, 13, 17, 19, 23, 97]) { + expect(new BigInteger(String(n), 10).isProbablePrime(20)).toBe(true); + } + }); + + it('rejects composites', () => { + for (const n of [4, 9, 15, 21, 25, 100, 1000]) { + expect(new BigInteger(String(n), 10).isProbablePrime(20)).toBe(false); + } + }); + + it('recognises a known large prime (2^61 - 1, Mersenne)', () => { + const p = new BigInteger('2305843009213693951', 10); + expect(p.isProbablePrime(20)).toBe(true); + }); +}); + +describe('BigInteger Buffer round-trip', () => { + it('round-trips an unsigned big-endian byte array', () => { + const bytes = new Uint8Array([0xde, 0xad, 0xbe, 0xef]); + const x = new BigInteger(bytes); + expect(x.toString(16)).toBe('deadbeef'); + const out = x.toBuffer(true); + expect(out).toBeInstanceOf(Uint8Array); + expect([...(out as Uint8Array)]).toEqual([0xde, 0xad, 0xbe, 0xef]); + }); + + it('toBuffer with size parameter zero-pads', () => { + const x = new BigInteger('5', 10); + const out = x.toBuffer(4) as Uint8Array; + expect([...out]).toEqual([0, 0, 0, 5]); + }); + + it('toBuffer with size parameter trims leading zeros only', () => { + const x = new BigInteger(new Uint8Array([0x12, 0x34])); + const out = x.toBuffer(2) as Uint8Array; + expect([...out]).toEqual([0x12, 0x34]); + }); + + it('toBuffer returns null if value does not fit in requested size', () => { + const x = new BigInteger(new Uint8Array([0xff, 0xff, 0xff])); + expect(x.toBuffer(2)).toBe(null); + }); +}); + +describe('BigInteger keygen RNG hook', () => { + it('errors helpfully if no backend is set (and then recovers)', async () => { + setBigIntegerBackend(undefined as never); + expect(() => new BigInteger(64)).toThrow(/backend not initialized/); + setBigIntegerBackend(nodeBackend); + }); + + it('new BigInteger(n) generates an n-bit random integer', () => { + setBigIntegerBackend(nodeBackend); + for (const bits of [16, 64, 128]) { + const x = new BigInteger(bits); + // bitLength is between bits-7 (if top byte happens to be < 0x80) and bits (after the mask). + expect(x.bitLength()).toBeLessThanOrEqual(bits); + expect(x.signum()).not.toBe(-1); + } + }); +}); diff --git a/test/bigint/native-parity.spec.ts b/test/bigint/native-parity.spec.ts new file mode 100644 index 0000000..634c46d --- /dev/null +++ b/test/bigint/native-parity.spec.ts @@ -0,0 +1,343 @@ +import { beforeAll, describe, expect, it } from 'vitest'; +import { + BigInteger as JsbnBigInteger, + setBigIntegerBackend as setJsbnBackend, +} from '../../src/bigint/big-integer-jsbn.js'; +import { + BigInteger as NativeBigInteger, + setBigIntegerBackend as setNativeBackend, +} from '../../src/bigint/big-integer-native.js'; +import { nodeBackend } from '../../src/crypto/backend.node.js'; + +// Cross-impl parity: every method the rest of node-rsa actually calls +// must produce identical results on jsbn and native for representative +// inputs (positive, negative, zero, 2048-bit-ish modulus). + +beforeAll(() => { + setJsbnBackend(nodeBackend); + setNativeBackend(nodeBackend); +}); + +const cases = { + small: '12345', + // 2048-bit-ish modulus from an existing test fixture, hex + modulus: + 'c3d3c4f8de7f6b5a9c1b8a4f2e5d7c8b3a9f1e7d6c5b4a3e2d1c0f9e8d7c6b5a4938271605948372615a4b3c2d1e0f9876543210fedcba9876543210fedcba98', + exp: '10001', // 65537 +}; + +function toHexU8(s: string): Uint8Array { + const pad = s.length & 1 ? `0${s}` : s; + const out = new Uint8Array(pad.length / 2); + for (let i = 0; i < out.length; i++) + out[i] = Number.parseInt(pad.substring(i * 2, i * 2 + 2), 16); + return out; +} + +describe('jsbn / native parity', () => { + const J = (hex: string) => new JsbnBigInteger(hex, 16); + const N = (hex: string) => new NativeBigInteger(hex, 16); + const aJ = J(cases.small); + const aN = N(cases.small); + const bJ = J('100'); + const bN = N('100'); + const mJ = J(cases.modulus); + const mN = N(cases.modulus); + const eJ = J(cases.exp); + const eN = N(cases.exp); + + it('add', () => { + expect(aN.add(bN).toString(16)).toBe(aJ.add(bJ).toString(16)); + }); + it('subtract', () => { + expect(aN.subtract(bN).toString(16)).toBe(aJ.subtract(bJ).toString(16)); + }); + it('multiply', () => { + expect(aN.multiply(bN).toString(16)).toBe(aJ.multiply(bJ).toString(16)); + }); + it('mod (positive dividend)', () => { + expect(aN.mod(bN).toString(16)).toBe(aJ.mod(bJ).toString(16)); + }); + it('mod (negative dividend → non-negative result)', () => { + const negJ = J('-1234'); + const negN = N('-1234'); + expect(negN.mod(bN).toString(16)).toBe(negJ.mod(bJ).toString(16)); + }); + it('compareTo (sign equivalence)', () => { + // jsbn returns the magnitude of the diff; native normalizes to -1/0/1. + // All node-rsa call sites use === 0 / < 0 / > 0, so sign equivalence is + // what matters for downstream correctness. + const sign = (n: number) => (n > 0 ? 1 : n < 0 ? -1 : 0); + expect(sign(aN.compareTo(bN))).toBe(sign(aJ.compareTo(bJ))); + expect(sign(bN.compareTo(aN))).toBe(sign(bJ.compareTo(aJ))); + expect(aN.compareTo(aN)).toBe(0); + }); + it('bitLength', () => { + expect(mN.bitLength()).toBe(mJ.bitLength()); + }); + it('signum', () => { + expect(aN.signum()).toBe(aJ.signum()); + expect(N('-5').signum()).toBe(J('-5').signum()); + expect(N('0').signum()).toBe(J('0').signum()); + }); + it('gcd (coprimes → 1)', () => { + expect(N('15').gcd(N('28')).toString(16)).toBe(J('15').gcd(J('28')).toString(16)); + }); + it('gcd (shared factor)', () => { + expect(N('30').gcd(N('45')).toString(16)).toBe(J('30').gcd(J('45')).toString(16)); + }); + it('shiftLeft', () => { + expect(aN.shiftLeft(100).toString(16)).toBe(aJ.shiftLeft(100).toString(16)); + }); + it('modPow (RSA-shaped: 2048-bit modulus, e=65537)', () => { + const baseJ = J('beefcafe'); + const baseN = N('beefcafe'); + expect(baseN.modPow(eN, mN).toString(16)).toBe(baseJ.modPow(eJ, mJ).toString(16)); + }); + it('modInverse (coprime)', () => { + const pJ = J('100000000000000000000000000000000000000000000000000000000003'); + const pN = N('100000000000000000000000000000000000000000000000000000000003'); + expect(eN.modInverse(pN).toString(16)).toBe(eJ.modInverse(pJ).toString(16)); + }); + it('toBuffer matches', () => { + const jBuf = mJ.toBuffer() as Uint8Array; + const nBuf = mN.toBuffer() as Uint8Array; + expect([...nBuf]).toEqual([...jBuf]); + }); + it('toBuffer with explicit length pads correctly', () => { + const lenJ = (mJ.toBuffer(256) as Uint8Array).length; + const lenN = (mN.toBuffer(256) as Uint8Array).length; + expect(lenN).toBe(lenJ); + expect(lenN).toBe(256); + }); + it('fromBytes round-trips', () => { + const bytes = toHexU8(cases.modulus); + expect(new NativeBigInteger(bytes).toString(16)).toBe(new JsbnBigInteger(bytes).toString(16)); + }); + it('isProbablePrime: small composite rejected, small prime accepted', () => { + expect(N('15').isProbablePrime(20)).toBe(false); + expect(N('17').isProbablePrime(20)).toBe(true); + expect(N('15').isProbablePrime(20)).toBe(J('15').isProbablePrime(20)); + expect(N('17').isProbablePrime(20)).toBe(J('17').isProbablePrime(20)); + }); + it('isEven / testBit', () => { + expect(N('10').isEven()).toBe(J('10').isEven()); + expect(N('11').isEven()).toBe(J('11').isEven()); + expect(N('1010').testBit(1)).toBe(J('1010').testBit(1)); + }); +}); + +describe('native end-to-end: generate + sign + verify with NodeRSA', async () => { + it('forces native via constructor option and round-trips PSS-SHA256', async () => { + const { default: NodeRSA } = await import('../../src/index.node.js'); + const key = new NodeRSA({ b: 512 }, { bigIntImpl: 'native' }); + expect(key.$options.bigIntImpl).toBe('native'); + const sig = key.sign('hello', 'buffer') as Uint8Array; + expect(key.verify('hello', sig)).toBe(true); + }); + + it('throws if bigIntImpl is changed on a key with components', async () => { + const { default: NodeRSA } = await import('../../src/index.node.js'); + const key = new NodeRSA({ b: 512 }); + expect(() => key.setOptions({ bigIntImpl: 'native' })).toThrow(/fresh instance/); + }); +}); + +describe('output parity across all 3 impls: jsbn ≡ native ≡ node (OpenSSL)', () => { + // Three impls in this comparison: + // 'jsbn' — JS engine + JS schemes + jsbn BigInteger (force via env:'browser'). + // 'native' — JS engine + JS schemes + native BigInt (force via env:'browser'). + // 'node' — node bundle defaults: NodeNativeEngine + node:crypto for the + // RSA primitive (encrypt/decrypt/sign/verify). + // + // For deterministic ops (PKCS#1 v1.5 signing, key serialisation), all three + // must produce byte-identical output. For probabilistic ops (PSS, OAEP) + // bytes differ run-to-run by design, so we only check round-trip / + // cross-verify between every pair. + + type Impl = 'jsbn' | 'native' | 'node'; + const IMPLS: ReadonlyArray = ['jsbn', 'native', 'node']; + const PEM_FIXTURE_PATH = '../../test/keys/private_pkcs1.pem'; + + async function buildKey(impl: Impl) { + const { default: NodeRSA } = await import('../../src/index.node.js'); + const { readFileSync } = await import('node:fs'); + const { fileURLToPath } = await import('node:url'); + const { dirname, resolve } = await import('node:path'); + const here = dirname(fileURLToPath(import.meta.url)); + const pem = readFileSync(resolve(here, PEM_FIXTURE_PATH), 'utf8'); + if (impl === 'node') { + // Node bundle defaults: NodeNativeEngine + node:crypto-backed schemes. + // bigIntImpl stays at the node-bundle default ('jsbn'), but the RSA + // primitive never touches BigInteger here. + return new NodeRSA(pem, 'pkcs1-private-pem'); + } + // Force the JS path so the RSA primitive actually goes through + // BigInteger and the bigIntImpl swap matters end-to-end. + return new NodeRSA(pem, 'pkcs1-private-pem', { + environment: 'browser', + bigIntImpl: impl, + }); + } + + function toHex(b: Uint8Array): string { + let s = ''; + for (const x of b) s += x.toString(16).padStart(2, '0'); + return s; + } + + async function buildAll(setOptions?: (k: Awaited>) => void) { + const keys = {} as Record>>; + for (const impl of IMPLS) { + const k = await buildKey(impl); + if (setOptions) setOptions(k); + keys[impl] = k; + } + return keys; + } + + it('exportKey(pkcs1-private-der) is byte-identical across all 3 impls', async () => { + const keys = await buildAll(); + const ref = toHex(keys.jsbn.exportKey('pkcs1-private-der') as Uint8Array); + for (const impl of IMPLS) { + const der = keys[impl].exportKey('pkcs1-private-der') as Uint8Array; + expect(toHex(der), `DER from ${impl}`).toBe(ref); + } + }); + + it('exportKey(components-public) yields identical n/e bytes across all 3 impls', async () => { + const keys = await buildAll(); + const ref = keys.jsbn.exportKey('components-public') as { n: Uint8Array; e: number }; + for (const impl of IMPLS) { + const c = keys[impl].exportKey('components-public') as { n: Uint8Array; e: number }; + expect(toHex(c.n), `n from ${impl}`).toBe(toHex(ref.n)); + expect(c.e, `e from ${impl}`).toBe(ref.e); + } + }); + + it('PKCS1-SHA256 signatures are byte-identical across all 3 impls (deterministic)', async () => { + const keys = await buildAll((k) => k.setOptions({ signingScheme: 'pkcs1-sha256' })); + const ref = toHex(keys.jsbn.sign(PAYLOAD_MSG, 'buffer') as Uint8Array); + for (const impl of IMPLS) { + const sig = keys[impl].sign(PAYLOAD_MSG, 'buffer') as Uint8Array; + expect(toHex(sig), `PKCS1 sig from ${impl}`).toBe(ref); + } + }); + + it('PKCS1-SHA256 signatures cross-verify (3×3 matrix)', async () => { + const keys = await buildAll((k) => k.setOptions({ signingScheme: 'pkcs1-sha256' })); + const sigs: Record = { + jsbn: keys.jsbn.sign(PAYLOAD_MSG, 'buffer') as Uint8Array, + native: keys.native.sign(PAYLOAD_MSG, 'buffer') as Uint8Array, + node: keys.node.sign(PAYLOAD_MSG, 'buffer') as Uint8Array, + }; + for (const signer of IMPLS) { + for (const verifier of IMPLS) { + expect(keys[verifier].verify(PAYLOAD_MSG, sigs[signer]), `${signer} → ${verifier}`).toBe( + true, + ); + } + } + }); + + it('PSS-SHA256 signatures cross-verify (3×3 matrix; probabilistic so bytes differ)', async () => { + const keys = await buildAll((k) => k.setOptions({ signingScheme: 'pss-sha256' })); + const sigs: Record = { + jsbn: keys.jsbn.sign(PAYLOAD_MSG, 'buffer') as Uint8Array, + native: keys.native.sign(PAYLOAD_MSG, 'buffer') as Uint8Array, + node: keys.node.sign(PAYLOAD_MSG, 'buffer') as Uint8Array, + }; + for (const signer of IMPLS) { + for (const verifier of IMPLS) { + expect(keys[verifier].verify(PAYLOAD_MSG, sigs[signer]), `${signer} → ${verifier}`).toBe( + true, + ); + } + } + }); + + it('OAEP-SHA1 ciphertexts cross-decrypt (3×3 matrix; probabilistic)', async () => { + const keys = await buildAll((k) => k.setOptions({ encryptionScheme: 'pkcs1_oaep' })); + const cts: Record = { + jsbn: keys.jsbn.encrypt(PAYLOAD_MSG, 'buffer') as Uint8Array, + native: keys.native.encrypt(PAYLOAD_MSG, 'buffer') as Uint8Array, + node: keys.node.encrypt(PAYLOAD_MSG, 'buffer') as Uint8Array, + }; + for (const encryptor of IMPLS) { + for (const decryptor of IMPLS) { + const pt = keys[decryptor].decrypt(cts[encryptor]) as Uint8Array; + expect(toHex(pt), `${encryptor} → ${decryptor}`).toBe(toHex(PAYLOAD_MSG)); + } + } + }); +}); + +const PAYLOAD_MSG = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + +describe('bigIntImpl consistency: every BigInteger in the lifecycle uses the same class', () => { + // Verifies that no operation silently constructs a BigInteger of the + // wrong class. After the active impl is set, key components, derived + // values (blinding/CRT intermediates), and any new BigInteger spawned + // by the JS scheme path must all be `instanceof` the same impl class. + + type AnyBI = typeof JsbnBigInteger | typeof NativeBigInteger; + + async function probe(opts: { bigIntImpl?: 'native' | 'jsbn' }, expected: AnyBI) { + const { default: NodeRSA } = await import('../../src/index.node.js'); + // Force environment:'browser' so every path goes through BigInteger — + // node-native paths bypass BigInteger entirely and would tell us + // nothing about consistency. + const key = new NodeRSA( + { b: 512 }, + { + environment: 'browser', + ...opts, + }, + ); + + // Key components from RSAKey.generate + expect(key.keyPair.n).toBeInstanceOf(expected); + expect(key.keyPair.p).toBeInstanceOf(expected); + expect(key.keyPair.q).toBeInstanceOf(expected); + expect(key.keyPair.d).toBeInstanceOf(expected); + expect(key.keyPair.dmp1).toBeInstanceOf(expected); + expect(key.keyPair.dmq1).toBeInstanceOf(expected); + expect(key.keyPair.coeff).toBeInstanceOf(expected); + + // Round-trip sign/verify (PSS via JS scheme — modPow path) + const sig = key.sign('hello', 'buffer') as Uint8Array; + expect(key.verify('hello', sig)).toBe(true); + + // Round-trip encrypt/decrypt (OAEP via JsEngine — modPow path) + const ct = key.encrypt('msg', 'buffer') as Uint8Array; + const pt = key.decrypt(ct, 'utf8') as string; + expect(pt).toBe('msg'); + + // Confirm $options reflects the active impl + expect(key.$options.bigIntImpl).toBe(opts.bigIntImpl ?? 'jsbn'); + } + + it('default (jsbn on node) → every component is JsbnBigInteger', async () => { + await probe({ bigIntImpl: 'jsbn' }, JsbnBigInteger); + }); + + it('bigIntImpl: native → every component is NativeBigInteger', async () => { + await probe({ bigIntImpl: 'native' }, NativeBigInteger); + }); + + it('browser bundle (default native) → every component is NativeBigInteger', async () => { + // Import the browser entry; its module-load setBigIntegerImpl('native') + // runs once at first import. Subsequent constructors honour it as the + // baseline default. + const { default: NodeRSA } = await import('../../src/index.browser.js'); + const key = new NodeRSA({ b: 512 }); + expect(key.$options.bigIntImpl).toBe('native'); + expect(key.keyPair.n).toBeInstanceOf(NativeBigInteger); + expect(key.keyPair.p).toBeInstanceOf(NativeBigInteger); + expect(key.keyPair.dmp1).toBeInstanceOf(NativeBigInteger); + const sig = key.sign('hello', 'buffer') as Uint8Array; + expect(key.verify('hello', sig)).toBe(true); + const ct = key.encrypt('msg', 'buffer') as Uint8Array; + expect(key.decrypt(ct, 'utf8')).toBe('msg'); + }); +}); diff --git a/test/crypto/backend-parity.node-only.spec.ts b/test/crypto/backend-parity.node-only.spec.ts new file mode 100644 index 0000000..605df69 --- /dev/null +++ b/test/crypto/backend-parity.node-only.spec.ts @@ -0,0 +1,57 @@ +import { describe, expect, it } from 'vitest'; +import { nodeBackend } from '../../src/crypto/backend.node.js'; +import { webBackend } from '../../src/crypto/backend.web.js'; +import { toHex } from '../../src/crypto/bytes.js'; +import type { HashingAlgorithm } from '../../src/crypto/types.js'; + +// Only runs in the 'node' workspace project. The 'browser-emulated' project +// excludes *.node-only.spec.ts because its alias substitutes backend.node +// with backend.web, which would make the comparison trivial. + +const CROSS_PLATFORM_HASHES: HashingAlgorithm[] = [ + 'md5', + 'ripemd160', + 'sha1', + 'sha224', + 'sha256', + 'sha384', + 'sha512', +]; + +const CORPUS: Uint8Array[] = [ + new Uint8Array(0), + new TextEncoder().encode('a'), + new TextEncoder().encode('abc'), + new TextEncoder().encode('The quick brown fox jumps over the lazy dog'), + (() => { + const big = new Uint8Array(10_000); + for (let i = 0; i < big.length; i++) big[i] = (i * 7 + 3) & 0xff; + return big; + })(), +]; + +describe('node vs. web backend parity', () => { + for (const alg of CROSS_PLATFORM_HASHES) { + describe(alg, () => { + CORPUS.forEach((data, i) => { + it(`input #${i} (len=${data.length}) digests match`, () => { + const a = toHex(nodeBackend.digest(alg, data)); + const b = toHex(webBackend.digest(alg, data)); + expect(a).toBe(b); + }); + }); + }); + } + + it('md4 is never supported by the web backend', () => { + // Node support is OpenSSL-config dependent (OpenSSL 3 needs the legacy + // provider loaded). Web backend never supports MD4. + expect(webBackend.supportsHash('md4')).toBe(false); + expect(() => webBackend.digest('md4', new Uint8Array(0))).toThrow(/MD4/); + }); + + it('backends advertise their name', () => { + expect(nodeBackend.name).toBe('node'); + expect(webBackend.name).toBe('web'); + }); +}); diff --git a/test/crypto/bytes.spec.ts b/test/crypto/bytes.spec.ts new file mode 100644 index 0000000..4852f18 --- /dev/null +++ b/test/crypto/bytes.spec.ts @@ -0,0 +1,166 @@ +import { describe, expect, it } from 'vitest'; +import { + concat, + constantTimeEqual, + fromBase64, + fromHex, + fromLatin1, + fromUtf8, + readUInt32BE, + toBase64, + toHex, + toLatin1, + toUtf8, + writeUInt32BE, +} from '../../src/crypto/bytes.js'; + +describe('bytes.concat', () => { + it('concatenates multiple arrays', () => { + const out = concat(new Uint8Array([1, 2]), new Uint8Array([3]), new Uint8Array([4, 5, 6])); + expect(Array.from(out)).toEqual([1, 2, 3, 4, 5, 6]); + }); + + it('handles empty inputs', () => { + expect(concat().length).toBe(0); + expect(Array.from(concat(new Uint8Array([1]), new Uint8Array(0), new Uint8Array([2])))).toEqual( + [1, 2], + ); + }); +}); + +describe('bytes.constantTimeEqual', () => { + it('returns true for identical content', () => { + expect(constantTimeEqual(new Uint8Array([1, 2, 3]), new Uint8Array([1, 2, 3]))).toBe(true); + }); + + it('returns false for different content or length', () => { + expect(constantTimeEqual(new Uint8Array([1, 2]), new Uint8Array([1, 2, 3]))).toBe(false); + expect(constantTimeEqual(new Uint8Array([1, 2, 3]), new Uint8Array([1, 2, 4]))).toBe(false); + }); +}); + +describe('bytes.toHex / fromHex', () => { + it('round-trips arbitrary bytes', () => { + const data = new Uint8Array([0x00, 0x01, 0x7f, 0x80, 0xff, 0xab, 0xcd]); + const hex = toHex(data); + expect(hex).toBe('00017f80ffabcd'); + expect(Array.from(fromHex(hex))).toEqual(Array.from(data)); + }); + + it('accepts upper-case hex and 0x prefix', () => { + expect(Array.from(fromHex('DEADbeef'))).toEqual([0xde, 0xad, 0xbe, 0xef]); + expect(Array.from(fromHex('0xdeadbeef'))).toEqual([0xde, 0xad, 0xbe, 0xef]); + }); + + it('rejects odd-length hex and invalid characters', () => { + expect(() => fromHex('abc')).toThrow(/odd length/); + expect(() => fromHex('zz')).toThrow(/Invalid hex/); + }); +}); + +describe('bytes.toBase64 / fromBase64', () => { + it('round-trips arbitrary bytes', () => { + const data = new Uint8Array([0, 1, 2, 250, 100, 200, 50, 0xff]); + const b64 = toBase64(data); + expect(Array.from(fromBase64(b64))).toEqual(Array.from(data)); + }); + + it('matches known vectors', () => { + expect(toBase64(new Uint8Array([72, 101, 108, 108, 111]))).toBe('SGVsbG8='); + expect(Array.from(fromBase64('SGVsbG8='))).toEqual([72, 101, 108, 108, 111]); + }); + + it('handles large arrays (chunked path)', () => { + const data = new Uint8Array(100_000); + for (let i = 0; i < data.length; i++) data[i] = i & 0xff; + const round = fromBase64(toBase64(data)); + expect(round.length).toBe(data.length); + expect(round[99_999]).toBe(99_999 & 0xff); + }); + + it('rejects malformed base64 input with characters outside the alphabet', () => { + // The library wraps `atob`, which throws on illegal characters. Pin + // the throwing behaviour so a future refactor doesn't silently swap to + // a permissive decoder (which would let key fixtures with garbage in + // them decode to wrong bytes). + for (const bad of ['====', '!!!!', 'AB$D', '@@@@']) { + expect(() => fromBase64(bad), `input "${bad}"`).toThrow(); + } + }); + + it('round-trips empty input through base64', () => { + expect(toBase64(new Uint8Array(0))).toBe(''); + expect(Array.from(fromBase64(''))).toEqual([]); + }); + + it('round-trips length-1 / length-2 inputs (padding-edge cases)', () => { + // Single-byte → "XX==", two-byte → "XXX=" canonical padding shapes. + const one = new Uint8Array([0xab]); + const two = new Uint8Array([0xab, 0xcd]); + expect(Array.from(fromBase64(toBase64(one)))).toEqual([0xab]); + expect(Array.from(fromBase64(toBase64(two)))).toEqual([0xab, 0xcd]); + }); +}); + +describe('bytes.fromUtf8 / toUtf8', () => { + it('round-trips ASCII', () => { + expect(toUtf8(fromUtf8('hello world'))).toBe('hello world'); + }); + + it('round-trips multibyte unicode', () => { + const s = 'テスト тест 测试 🚀'; + expect(toUtf8(fromUtf8(s))).toBe(s); + }); +}); + +describe('bytes.fromLatin1 / toLatin1', () => { + it('round-trips every byte 0x00-0xFF without corruption', () => { + // The whole point of latin1 (vs UTF-8) is bytes ≥0x80 survive verbatim: + // through TextDecoder they would expand to multi-byte sequences or hit + // U+FFFD replacement on invalid runs. This is the regression guard for + // the legacy `'binary'` Node-RSA encoding. + const all = new Uint8Array(256); + for (let i = 0; i < 256; i++) all[i] = i; + const s = toLatin1(all); + expect(s.length).toBe(256); + expect(Array.from(fromLatin1(s))).toEqual(Array.from(all)); + }); + + it('handles chunk boundary at 0x8000', () => { + const big = new Uint8Array(0x8000 + 17); + for (let i = 0; i < big.length; i++) big[i] = (i * 31 + 7) & 0xff; + expect(Array.from(fromLatin1(toLatin1(big)))).toEqual(Array.from(big)); + }); + + it('round-trips empty input', () => { + expect(toLatin1(new Uint8Array(0))).toBe(''); + expect(Array.from(fromLatin1(''))).toEqual([]); + }); +}); + +describe('bytes.readUInt32BE / writeUInt32BE', () => { + it('round-trips values', () => { + const buf = new Uint8Array(4); + writeUInt32BE(0xdeadbeef, buf, 0); + expect(Array.from(buf)).toEqual([0xde, 0xad, 0xbe, 0xef]); + expect(readUInt32BE(buf, 0)).toBe(0xdeadbeef); + }); + + it('handles offset writes', () => { + const buf = new Uint8Array(8); + writeUInt32BE(0x01020304, buf, 2); + expect(Array.from(buf)).toEqual([0, 0, 1, 2, 3, 4, 0, 0]); + expect(readUInt32BE(buf, 2)).toBe(0x01020304); + }); + + it('handles zero', () => { + const buf = new Uint8Array(4); + writeUInt32BE(0, buf, 0); + expect(readUInt32BE(buf, 0)).toBe(0); + }); + + it('throws on out-of-range offsets', () => { + expect(() => readUInt32BE(new Uint8Array(4), 1)).toThrow(/out of range/); + expect(() => writeUInt32BE(1, new Uint8Array(4), 1)).toThrow(/out of range/); + }); +}); diff --git a/test/crypto/digest.spec.ts b/test/crypto/digest.spec.ts new file mode 100644 index 0000000..0505b25 --- /dev/null +++ b/test/crypto/digest.spec.ts @@ -0,0 +1,205 @@ +import { describe, expect, it } from 'vitest'; +import { nodeBackend } from '../../src/crypto/backend.node.js'; +import { webBackend } from '../../src/crypto/backend.web.js'; +import { toHex } from '../../src/crypto/bytes.js'; +import { DIGEST_LENGTH } from '../../src/crypto/digest-length.js'; +import type { HashingAlgorithm } from '../../src/crypto/types.js'; + +// The vitest workspace aliases backend.node → backend.web in the +// browser-emulated project, so `nodeBackend` here resolves to whichever +// backend the project under test should exercise. Test vectors are universal. + +const EMPTY = new Uint8Array(0); +const ABC = new TextEncoder().encode('abc'); +const QUICK = new TextEncoder().encode('The quick brown fox jumps over the lazy dog'); + +// Known test vectors from RFCs / FIPS publications. +const VECTORS: Partial> = { + md5: { + empty: 'd41d8cd98f00b204e9800998ecf8427e', + abc: '900150983cd24fb0d6963f7d28e17f72', + quick: '9e107d9d372bb6826bd81d3542a419d6', + }, + sha1: { + empty: 'da39a3ee5e6b4b0d3255bfef95601890afd80709', + abc: 'a9993e364706816aba3e25717850c26c9cd0d89d', + quick: '2fd4e1c67a2d28fced849ee1bb76e7391b93eb12', + }, + sha224: { + empty: 'd14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f', + abc: '23097d223405d8228642a477bda255b32aadbce4bda0b3f7e36c9da7', + quick: '730e109bd7a8a32b1cb9d9a09aa2325d2430587ddbc0c38bad911525', + }, + sha256: { + empty: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', + abc: 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad', + quick: 'd7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592', + }, + sha384: { + empty: + '38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b', + abc: 'cb00753f45a35e8bb5a03d699ac65007272c32ab0eded1631a8b605a43ff5bed8086072ba1e7cc2358baeca134c825a7', + quick: + 'ca737f1014a48f4c0b6dd43cb177b0afd9e5169367544c494011e3317dbf9a509cb1e5dc1e85a941bbee3d7f2afbc9b1', + }, + sha512: { + empty: + 'cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e', + abc: 'ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f', + quick: + '07e547d9586f6a73f73fbac0435ed76951218fb7d0c8d788a309d785436bbb642e93a252a954f23912547d1e8a3b5ed6e1bfd7097821233fa0538f3db854fee6', + }, + ripemd160: { + empty: '9c1185a5c5e9fc54612808977ee8f548b2258d31', + abc: '8eb208f7e05d987a9b044a8e98c6b087f15a0bfc', + quick: '37f332f68db77bd9d7edd4969571ad671cf9dd3b', + }, +}; + +describe('digest test vectors', () => { + for (const [alg, vec] of Object.entries(VECTORS) as Array< + [HashingAlgorithm, NonNullable<(typeof VECTORS)[HashingAlgorithm]>] + >) { + describe(alg, () => { + it.skipIf(!nodeBackend.supportsHash(alg))(`empty: ${vec.empty.slice(0, 16)}…`, () => { + const out = nodeBackend.digest(alg, EMPTY); + expect(out.length).toBe(DIGEST_LENGTH[alg]); + expect(toHex(out)).toBe(vec.empty); + }); + + it.skipIf(!nodeBackend.supportsHash(alg))(`"abc": ${vec.abc.slice(0, 16)}…`, () => { + const out = nodeBackend.digest(alg, ABC); + expect(toHex(out)).toBe(vec.abc); + }); + + it.skipIf(!nodeBackend.supportsHash(alg))(`fox: ${vec.quick.slice(0, 16)}…`, () => { + const out = nodeBackend.digest(alg, QUICK); + expect(toHex(out)).toBe(vec.quick); + }); + }); + } +}); + +describe('digest output length', () => { + for (const alg of Object.keys(DIGEST_LENGTH) as HashingAlgorithm[]) { + it.skipIf(!nodeBackend.supportsHash(alg))(`${alg} → ${DIGEST_LENGTH[alg]} bytes`, () => { + const out = nodeBackend.digest(alg, new Uint8Array([1, 2, 3, 4, 5])); + expect(out.length).toBe(DIGEST_LENGTH[alg]); + }); + } +}); + +describe('digest determinism', () => { + it('same input → same output (sha256)', () => { + const a = nodeBackend.digest('sha256', QUICK); + const b = nodeBackend.digest('sha256', QUICK); + expect(toHex(a)).toBe(toHex(b)); + }); +}); + +describe('digest error cases', () => { + it('rejects an unknown algorithm', () => { + expect(() => nodeBackend.digest('sha999' as HashingAlgorithm, EMPTY)).toThrow(); + }); +}); + +/** + * Hash functions process data in fixed-size blocks (SHA-1/224/256 = 64 B, + * SHA-384/512 = 128 B), buffering partial blocks internally. Bugs around + * the "exactly one block", "one block plus one byte", and "block-aligned + * across multiple blocks" cases are common when implementing or porting + * a hash (e.g., an off-by-one in the length-encoded padding step). + * + * We don't have NIST vectors at these exact sizes, but we can use the + * backend as its own oracle: the digest must equal a chunked re-hash + * implementation only if the impl is correct. Since we don't expose an + * update/finalize API, we just verify the digest is stable (same input + * → same output) and length-correct across block boundaries — a + * non-trivial assertion if a future refactor breaks the chunking. + */ +describe('digest block-boundary inputs', () => { + const BLOCK: Partial> = { + md5: 64, + sha1: 64, + sha224: 64, + sha256: 64, + sha384: 128, + sha512: 128, + ripemd160: 64, + }; + + function makeBuf(n: number, seed: number): Uint8Array { + // Deterministic pseudo-random fill (LCG) so the test is reproducible. + const out = new Uint8Array(n); + let s = seed | 0; + for (let i = 0; i < n; i++) { + s = (s * 1103515245 + 12345) & 0x7fffffff; + out[i] = s & 0xff; + } + return out; + } + + for (const [alg, block] of Object.entries(BLOCK) as Array<[HashingAlgorithm, number]>) { + it.skipIf(!nodeBackend.supportsHash(alg))( + `${alg}: ${block - 1}/${block}/${block + 1}/${2 * block} bytes — stable and correct length`, + () => { + for (const n of [block - 1, block, block + 1, 2 * block - 1, 2 * block, 2 * block + 1]) { + const data = makeBuf(n, n * 7); + const a = nodeBackend.digest(alg, data); + const b = nodeBackend.digest(alg, data); + expect(a.length, `length for n=${n}`).toBe(DIGEST_LENGTH[alg]); + expect(toHex(b)).toBe(toHex(a)); + // A truncated input must yield a different digest from the full + // one — catches a regression that drops trailing bytes. + if (n > 1) { + const cut = nodeBackend.digest(alg, data.subarray(0, n - 1)); + expect(toHex(cut)).not.toBe(toHex(a)); + } + } + }, + ); + } + + it.skipIf(!nodeBackend.supportsHash('sha256'))( + 'sha256: 1-byte difference at the block boundary produces a different digest', + () => { + // Last byte before the 64-byte boundary mutated vs first byte of the + // next block — both must produce digests distinct from the canonical. + const base = makeBuf(64, 1); + const ref = toHex(nodeBackend.digest('sha256', base)); + const a = new Uint8Array(base); + a[63] = (a[63] as number) ^ 0x01; + expect(toHex(nodeBackend.digest('sha256', a))).not.toBe(ref); + const extra = new Uint8Array(65); + extra.set(base); + // Adding a single zero byte yields a different hash too. + expect(toHex(nodeBackend.digest('sha256', extra))).not.toBe(ref); + }, + ); +}); + +/** + * The vitest workspace alias maps src/crypto/backend.node.ts → + * src/crypto/backend.web.ts in the `browser-emulated` project. A broken + * alias would silently let the node backend serve `browser-emulated`, + * defeating the dual-backend coverage. Detect the active workspace by + * importing both backends explicitly: when the alias is live, the two + * identifiers are the same reference; when it's not, they differ. + */ +describe('backend identity (workspace-alias smoke)', () => { + const aliasActive = nodeBackend === webBackend; + it('alias is active iff nodeBackend resolves to webBackend', () => { + // Both states are legitimate (one per workspace); we just confirm the + // identity matches the .name field — i.e. if alias is active, .name + // says 'web'; otherwise 'node'. + if (aliasActive) { + expect(nodeBackend.name).toBe('web'); + } else { + expect(nodeBackend.name).toBe('node'); + } + }); + + it('webBackend.name is always "web" (no aliasing in the other direction)', () => { + expect(webBackend.name).toBe('web'); + }); +}); diff --git a/test/crypto/random.spec.ts b/test/crypto/random.spec.ts new file mode 100644 index 0000000..5b534a6 --- /dev/null +++ b/test/crypto/random.spec.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from 'vitest'; +import { nodeBackend } from '../../src/crypto/backend.node.js'; + +// `nodeBackend` is aliased to webBackend in the browser-emulated project. +// Tests below exercise the active backend's randomBytes regardless. + +describe('randomBytes', () => { + it('returns a Uint8Array of the requested size', () => { + for (const n of [0, 1, 16, 32, 100, 1024]) { + const bytes = nodeBackend.randomBytes(n); + expect(bytes).toBeInstanceOf(Uint8Array); + expect(bytes.length).toBe(n); + } + }); + + it('produces different outputs across calls (sanity check)', () => { + const a = nodeBackend.randomBytes(32); + const b = nodeBackend.randomBytes(32); + // 256-bit collision probability is negligible + let same = true; + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) { + same = false; + break; + } + } + expect(same).toBe(false); + }); + + it('handles requests larger than a single getRandomValues chunk', () => { + // Web Crypto getRandomValues caps at 65536 bytes per call; backend chunks. + const big = nodeBackend.randomBytes(200_000); + expect(big.length).toBe(200_000); + // First and last byte are independent samples; should generally differ across runs. + const big2 = nodeBackend.randomBytes(200_000); + expect(big[0] === big2[0] && big[big.length - 1] === big2[big2.length - 1]).toBe(false); + }); +}); diff --git a/test/formats/openssh-negative.spec.ts b/test/formats/openssh-negative.spec.ts new file mode 100644 index 0000000..19e15f5 --- /dev/null +++ b/test/formats/openssh-negative.spec.ts @@ -0,0 +1,178 @@ +import { readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { beforeAll, describe, expect, it } from 'vitest'; +import { setBigIntegerBackend } from '../../src/bigint/big-integer.js'; +import { nodeBackend } from '../../src/crypto/backend.node.js'; +import { fromBase64, toBase64 } from '../../src/crypto/bytes.js'; +import NodeRSA from '../../src/index.node.js'; + +const here = dirname(fileURLToPath(import.meta.url)); +const keysDir = resolve(here, '../keys'); + +function readStr(name: string): string { + return readFileSync(resolve(keysDir, name), 'utf8'); +} + +beforeAll(() => { + setBigIntegerBackend(nodeBackend); +}); + +const OPEN = '-----BEGIN OPENSSH PRIVATE KEY-----'; +const CLOSE = '-----END OPENSSH PRIVATE KEY-----'; + +/** Decode an OpenSSH-PEM string into its raw binary body. */ +function decodeOpenSshPem(pem: string): Uint8Array { + const body = pem.substring(pem.indexOf(OPEN) + OPEN.length, pem.indexOf(CLOSE)); + return fromBase64(body.replace(/\s+/g, '')); +} + +/** Re-wrap raw binary back into an OpenSSH PEM container. */ +function encodeOpenSshPem(bytes: Uint8Array): string { + const b64 = toBase64(bytes); + const lines: string[] = []; + for (let i = 0; i < b64.length; i += 70) lines.push(b64.slice(i, i + 70)); + return `${OPEN}\n${lines.join('\n')}\n${CLOSE}\n`; +} + +/** + * Find the byte offset of the SECOND occurrence of the OpenSSH `ssh-rsa` + * string TLV (length-prefix `00 00 00 07` + `ssh-rsa`). The first + * occurrence is in the public-key block; the second sits in the private + * section immediately after checkint1 and checkint2. The 4 bytes + * preceding the match are checkint2. + */ +function findSecondSshRsa(bytes: Uint8Array): number { + const pattern = new Uint8Array([ + 0x00, 0x00, 0x00, 0x07, 0x73, 0x73, 0x68, 0x2d, 0x72, 0x73, 0x61, + ]); + let count = 0; + outer: for (let i = 0; i <= bytes.length - pattern.length; i++) { + for (let j = 0; j < pattern.length; j++) { + if (bytes[i + j] !== pattern[j]) continue outer; + } + count++; + if (count === 2) return i; + } + throw new Error('OpenSSH private section marker not found'); +} + +describe('OpenSSH — M5 checkint validation', () => { + it('rejects checkint2 with a single-byte flip', () => { + const validPem = readStr('id_rsa'); + const bytes = decodeOpenSshPem(validPem); + const sshRsaPos = findSecondSshRsa(bytes); + const mutated = new Uint8Array(bytes); + // checkint2 occupies bytes [sshRsaPos - 4 .. sshRsaPos]. Flip the LSB. + mutated[sshRsaPos - 1] = (mutated[sshRsaPos - 1] as number) ^ 0x01; + const badPem = encodeOpenSshPem(mutated); + expect(() => new NodeRSA(badPem)).toThrow(/checksum mismatch/); + }); + + it('rejects checkint2 wholly replaced with a different value', () => { + const validPem = readStr('id_rsa'); + const bytes = decodeOpenSshPem(validPem); + const sshRsaPos = findSecondSshRsa(bytes); + const mutated = new Uint8Array(bytes); + // Replace all 4 bytes of checkint2 — confirms the comparison covers the + // whole field, not just one byte. + for (let i = 0; i < 4; i++) { + mutated[sshRsaPos - 4 + i] = (mutated[sshRsaPos - 4 + i] as number) ^ 0xff; + } + const badPem = encodeOpenSshPem(mutated); + expect(() => new NodeRSA(badPem)).toThrow(/checksum mismatch/); + }); + + it('accepts the unmodified valid OpenSSH key', () => { + const validPem = readStr('id_rsa'); + expect(() => new NodeRSA(validPem)).not.toThrow(); + }); +}); + +describe('OpenSSH — M4 SshReader bounds-check', () => { + it('rejects OpenSSH private key with a forged oversized string length', () => { + const validPem = readStr('id_rsa'); + const bytes = decodeOpenSshPem(validPem); + // The first string in the file is "openssh-key-v1\0" (15 bytes, not + // length-prefixed — it's a magic), followed by the 4-byte length of + // "none" (cipher name). Offset 15 is where the first length-prefixed + // string begins. Forge it to a length larger than the buffer. + const mutated = new Uint8Array(bytes); + mutated[15] = 0xff; + mutated[16] = 0xff; + mutated[17] = 0xff; + mutated[18] = 0xff; + const badPem = encodeOpenSshPem(mutated); + expect(() => new NodeRSA(badPem)).toThrow(/exceeds buffer/); + }); + + it('rejects OpenSSH key truncated mid-string with a bounds-check error', () => { + const validPem = readStr('id_rsa'); + const bytes = decodeOpenSshPem(validPem); + // Truncate to first 100 bytes — the next length-prefixed read will see + // a length that exceeds the remaining buffer. The bounds check in + // SshReader.readString surfaces an "exceeds buffer" error. + const truncated = bytes.subarray(0, 100); + const badPem = encodeOpenSshPem(truncated); + expect(() => new NodeRSA(badPem)).toThrow(/exceeds buffer/); + }); +}); + +describe('OpenSSH — padding bytes', () => { + // The OpenSSH private-key format pads the inner section so its total + // length is a multiple of 8. The padding bytes are 0x01, 0x02, 0x03, … + // — the importer ignores them today, so corrupting padding is technically + // a tolerable input. These tests pin the *current* behaviour: a single + // padding-byte change must not corrupt the recovered key. If a future + // change adds strict padding validation, these tests will need to update. + it('current behaviour: padding-byte tampering does not corrupt the recovered key', () => { + const validPem = readStr('id_rsa'); + const bytes = decodeOpenSshPem(validPem); + // Padding sits at the tail. Replace last 1–3 trailing pad bytes (which + // should be 0x01, 0x02, 0x03 ordering) with 0xff and ensure the parsed + // public modulus is unchanged. + const mutated = new Uint8Array(bytes); + for (let off = 1; off <= Math.min(3, bytes.length); off++) { + mutated[bytes.length - off] = 0xff; + } + const badPem = encodeOpenSshPem(mutated); + const reference = new NodeRSA(validPem); + const candidate = new NodeRSA(badPem); + expect(candidate.keyPair.n?.toString(16)).toBe(reference.keyPair.n?.toString(16)); + }); + + it('truncating the final padding byte exposes the bounds checker', () => { + // The padding sits BEYOND the data the importer consumes — chopping it + // off therefore leaves a syntactically valid file. Pin that: importing + // a file with 1 byte less than the original (last byte = the largest + // pad byte) must still succeed. + const validPem = readStr('id_rsa'); + const bytes = decodeOpenSshPem(validPem); + // Drop a single padding byte from the tail. + const truncated = bytes.subarray(0, bytes.length - 1); + const badPem = encodeOpenSshPem(truncated); + expect(() => new NodeRSA(badPem)).not.toThrow(); + }); +}); + +describe('OpenSSH — magic and cipher header', () => { + it('rejects file with wrong magic prefix', () => { + const validPem = readStr('id_rsa'); + const bytes = decodeOpenSshPem(validPem); + const mutated = new Uint8Array(bytes); + mutated[0] = 0x58; // 'X' instead of 'o' + const badPem = encodeOpenSshPem(mutated); + expect(() => new NodeRSA(badPem)).toThrow(/Invalid file format/); + }); + + it('rejects file declaring a non-"none" cipher', () => { + const validPem = readStr('id_rsa'); + const bytes = decodeOpenSshPem(validPem); + // After 15-byte magic, the cipher-name string starts. + // "none" is 4 bytes; replace one of them with 'X' to make "Xone". + const mutated = new Uint8Array(bytes); + mutated[19] = 0x58; // first char of cipher name + const badPem = encodeOpenSshPem(mutated); + expect(() => new NodeRSA(badPem)).toThrow(/Unsupported key type/); + }); +}); diff --git a/test/formats/pem-negative.spec.ts b/test/formats/pem-negative.spec.ts new file mode 100644 index 0000000..1879a16 --- /dev/null +++ b/test/formats/pem-negative.spec.ts @@ -0,0 +1,106 @@ +import { readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { beforeAll, describe, expect, it } from 'vitest'; +import { setBigIntegerBackend } from '../../src/bigint/big-integer.js'; +import { nodeBackend } from '../../src/crypto/backend.node.js'; +import { decodePem } from '../../src/formats/pem.js'; +import NodeRSAClass from '../../src/index.node.js'; + +const here = dirname(fileURLToPath(import.meta.url)); +const keysDir = resolve(here, '../keys'); + +function readStr(name: string): string { + return readFileSync(resolve(keysDir, name), 'utf8'); +} + +beforeAll(() => { + setBigIntegerBackend(nodeBackend); +}); + +const OPEN = '-----BEGIN RSA PRIVATE KEY-----'; +const CLOSE = '-----END RSA PRIVATE KEY-----'; + +describe('PEM — multi-block input is rejected (L2)', () => { + it('rejects two RSA PRIVATE KEY blocks concatenated', () => { + const single = readStr('private_pkcs1.pem').trim(); + const dup = `${single}\n${single}\n`; + expect(() => decodePem(dup, OPEN, CLOSE)).toThrow(/multiple .* blocks/); + }); + + it('rejects two blocks even with garbage between them', () => { + const single = readStr('private_pkcs1.pem').trim(); + const dup = `${single}\n# random comment\n\n${single}\n`; + expect(() => decodePem(dup, OPEN, CLOSE)).toThrow(/multiple .* blocks/); + }); + + it('accepts a single block with surrounding text', () => { + const single = readStr('private_pkcs1.pem').trim(); + const noisy = `header line\n${single}\nfooter line\n`; + expect(() => decodePem(noisy, OPEN, CLOSE)).not.toThrow(); + }); + + it('accepts mixed-marker files (different opening strings coexist)', () => { + // L2 only rejects duplicate *same-opening* blocks. A file with both an + // RSA PRIVATE KEY block and (say) a CERTIFICATE block is unambiguous — + // each decodePem call sees only its own opening marker. + const key = readStr('private_pkcs1.pem').trim(); + const mixed = `-----BEGIN CERTIFICATE-----\nMIIB...\n-----END CERTIFICATE-----\n${key}\n`; + expect(() => decodePem(mixed, OPEN, CLOSE)).not.toThrow(); + }); +}); + +describe('PEM — robustness against malformed input', () => { + it('throws on bad base64 inside an otherwise well-formed block', () => { + const body = '!!!not base64!!!'; + const bad = `${OPEN}\n${body}\n${CLOSE}\n`; + expect(() => decodePem(bad, OPEN, CLOSE)).toThrow(); + }); + + it('trims leading and trailing whitespace around the body', () => { + const single = readStr('private_pkcs1.pem'); + // Add tab/CR noise around the body — the \s+ replace should strip them. + const padded = single.replace(/\n/g, '\t \r\n '); + expect(() => decodePem(padded, OPEN, CLOSE)).not.toThrow(); + }); + + it('decodes raw base64 when no PEM markers are present', () => { + // trimSurroundingText returns the input verbatim when no markers exist; + // the fallback fromBase64 either succeeds (raw base64 mode) or throws + // on garbage. Document the success branch. + expect(decodePem('AAAA', OPEN, CLOSE)).toEqual(new Uint8Array([0, 0, 0])); + }); +}); + +describe('PEM — header / footer mismatches', () => { + // decodePem uses trimSurroundingText with the caller's `opening`/`closing` + // strings — if the closing marker isn't present, trimSurroundingText + // falls back to the entire remainder of the input. That's normally + // fine because pkcs1/pkcs8 importers pin their own marker pairs, but + // we should pin the behaviour for swap-on-input cases so a future + // refactor that loosens the pair doesn't silently accept a corrupted + // file. + it('rejects body when BEGIN/END markers swapped or replaced with a different pair', () => { + const body = readStr('private_pkcs1.pem').trim(); + // BEGIN matches RSA PRIVATE KEY, but END is PUBLIC KEY → closing not + // found at the caller's expected position. Everything from BEGIN onward + // (including the "END PUBLIC KEY" line) is fed to fromBase64, which + // bails out on the dashes / words. Either failure is acceptable as + // long as the call throws (rather than returning a truncated key). + const swapped = body.replace('-----END RSA PRIVATE KEY-----', '-----END RSA PUBLIC KEY-----'); + expect(() => decodePem(swapped, OPEN, CLOSE)).toThrow(); + }); + + it('importing a PKCS#1 PEM where the body says PUBLIC fails through NodeRSA', () => { + // End-to-end: the high-level importer should reject obvious + // header-tampering rather than half-parse a body. Uses the public + // surface (NodeRSA constructor) so a regression that loosened the + // PEM detector would be caught here too. + const body = readStr('private_pkcs1.pem') + .trim() + .replace('-----BEGIN RSA PRIVATE KEY-----', '-----BEGIN RSA PUBLIC KEY-----'); + // Importing private fixture with a public BEGIN: detectors prefer + // public route → eventually the body fails to parse as a SubjectPublicKeyInfo. + expect(() => new NodeRSAClass(body)).toThrow(); + }); +}); diff --git a/test/formats/pkcs8-bitstring.spec.ts b/test/formats/pkcs8-bitstring.spec.ts new file mode 100644 index 0000000..4de0956 --- /dev/null +++ b/test/formats/pkcs8-bitstring.spec.ts @@ -0,0 +1,138 @@ +import { readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { beforeAll, describe, expect, it } from 'vitest'; +import { DerReader, DerWriter, OID, Tag } from '../../src/asn1/index.js'; +import { setBigIntegerBackend } from '../../src/bigint/big-integer.js'; +import { nodeBackend } from '../../src/crypto/backend.node.js'; +import { encodePem } from '../../src/formats/pem.js'; +import NodeRSA from '../../src/index.node.js'; + +const here = dirname(fileURLToPath(import.meta.url)); +const keysDir = resolve(here, '../keys'); + +function readStr(name: string): string { + return readFileSync(resolve(keysDir, name), 'utf8'); +} + +beforeAll(() => { + setBigIntegerBackend(nodeBackend); +}); + +// PKCS#8 BIT STRING corner case audit +// +// The legacy `asn1` npm package (used by node-rsa v1) silently masks any +// non-zero "unused bits" octet on read, and accepts an optional unused-bits +// parameter on write. The in-tree DER writer/reader (added in v2.0) is +// stricter: BIT STRING is always written with an unused-bits byte of 0, and +// any non-zero unused-bits byte on read is rejected. +// +// For RSA SubjectPublicKeyInfo specifically, RFC 5280 §4.1 mandates that the +// BIT STRING contents are a DER-encoded RSAPublicKey — a sequence of whole +// bytes, so the unused-bits octet MUST be 0. There is no legitimate input +// produced by a conformant encoder that would carry non-zero unused-bits. +// +// This audit pins three properties of the in-tree implementation that the +// TODO flagged as "differs from asn1 npm package on one corner case": +// +// 1. publicExport always emits the unused-bits byte as 0. +// 2. publicImport rejects PEMs whose SPKI BIT STRING carries non-zero +// unused-bits, with a clear diagnostic (no silent masking). +// 3. Round-trip is byte-identical: import → export → import yields the +// same DER as the v1 `asn1`-package-produced fixture. +describe('PKCS#8 SPKI BIT STRING audit', () => { + it('publicExport always emits unused-bits = 0 (no caller-tunable parameter)', () => { + const k = new NodeRSA(readStr('private_pkcs1.pem')); + const der = k.exportKey('pkcs8-public-der') as Uint8Array; + + // Walk the DER: SEQUENCE { algId-SEQUENCE, BIT STRING { ... } }. + const outer = new DerReader(der).readSequence(); + outer.readSequence(); // skip algId + // Read the raw BIT STRING value (including the leading unused-bits byte). + // The first byte of the value MUST be 0x00 for any RFC-conformant SPKI. + const raw = outer.readBitStringRaw(); + expect(raw.length).toBeGreaterThan(0); + expect(raw[0]).toBe(0); + }); + + it('publicImport rejects SPKI whose BIT STRING carries non-zero unused-bits', () => { + // Build a SubjectPublicKeyInfo by hand and craft the BIT STRING with + // unused-bits = 4 (a value that would otherwise be silently masked by + // the legacy `asn1` decoder). + const validKey = new NodeRSA(readStr('private_pkcs1.pem')); + const innerDer = (() => { + const w = new DerWriter(); + w.startSequence(); + w.writeInteger(validKey.keyPair.n!.toBuffer() as Uint8Array); + w.writeInteger(validKey.keyPair.e); + w.endSequence(); + return w.toBytes(); + })(); + + // Hand-write the SPKI with a deliberate non-zero unused-bits byte. + // Reuse DerWriter for the outer structure, then patch the BIT STRING + // payload manually via writeBitStringRaw. + const malformed = (() => { + const w = new DerWriter(); + w.startSequence(); + w.startSequence(); + w.writeOid(OID.RSA_ENCRYPTION); + w.writeNull(); + w.endSequence(); + // Raw BIT STRING value: . + const raw = new Uint8Array(innerDer.length + 1); + raw[0] = 4; // non-zero — this is the corner case the audit pins. + raw.set(innerDer, 1); + w.writeBitStringRaw(raw); + w.endSequence(); + return w.toBytes(); + })(); + const pem = encodePem(malformed, '-----BEGIN PUBLIC KEY-----', '-----END PUBLIC KEY-----'); + + // The strict reader must throw — never silently mask. + expect(() => new NodeRSA(pem, 'pkcs8-public-pem')).toThrow(/unused bits/); + }); + + it('publicImport rejects an empty BIT STRING (no unused-bits octet at all)', () => { + // Mirror the previous test but emit a zero-length BIT STRING value. + // This is malformed per X.690 §8.6 — the unused-bits octet is mandatory. + const w = new DerWriter(); + w.startSequence(); + w.startSequence(); + w.writeOid(OID.RSA_ENCRYPTION); + w.writeNull(); + w.endSequence(); + // BIT STRING tag with zero-length content (no unused-bits byte). + w.writeTlv(Tag.BIT_STRING, new Uint8Array(0)); + w.endSequence(); + const pem = encodePem(w.toBytes(), '-----BEGIN PUBLIC KEY-----', '-----END PUBLIC KEY-----'); + + expect(() => new NodeRSA(pem, 'pkcs8-public-pem')).toThrow(/empty BIT STRING/); + }); + + it('SPKI round-trip is byte-identical (export→import→export yields the same DER)', () => { + // Loads a fixture produced by v1 (via the asn1 npm package), re-exports + // through the in-tree writer, and asserts byte equality. This is the + // "no observable difference" property the TODO entry asked us to + // confirm for the standard case. + const k = new NodeRSA(readStr('private_pkcs1.pem')); + const der1 = k.exportKey('pkcs8-public-der') as Uint8Array; + const k2 = new NodeRSA(); + k2.importKey(der1, 'pkcs8-public-der'); + const der2 = k2.exportKey('pkcs8-public-der') as Uint8Array; + + expect(der2.length).toBe(der1.length); + expect([...der2]).toEqual([...der1]); + }); + + it('matches OpenSSL byte-for-byte: re-export of `public_pkcs8.pem` is identical', () => { + // public_pkcs8.pem was produced by `openssl pkey -pubout` from the + // same fixture private key. Importing then re-exporting must reproduce + // it byte-for-byte (including the all-zero unused-bits byte that + // OpenSSL always emits). + const reference = readStr('public_pkcs8.pem').trim(); + const k = new NodeRSA(reference); + const roundtripped = (k.exportKey('pkcs8-public-pem') as string).trim(); + expect(roundtripped).toBe(reference); + }); +}); diff --git a/test/formats/pkcs8-negative.spec.ts b/test/formats/pkcs8-negative.spec.ts new file mode 100644 index 0000000..2f41704 --- /dev/null +++ b/test/formats/pkcs8-negative.spec.ts @@ -0,0 +1,187 @@ +import { readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { beforeAll, describe, expect, it } from 'vitest'; +import { DerWriter, OID } from '../../src/asn1/index.js'; +import { type BigInteger, setBigIntegerBackend } from '../../src/bigint/big-integer.js'; +import { nodeBackend } from '../../src/crypto/backend.node.js'; +import { encodePem } from '../../src/formats/pem.js'; +import NodeRSA from '../../src/index.node.js'; + +const here = dirname(fileURLToPath(import.meta.url)); +const keysDir = resolve(here, '../keys'); + +function readStr(name: string): string { + return readFileSync(resolve(keysDir, name), 'utf8'); +} + +function toBytes(b: BigInteger): Uint8Array { + return b.toBuffer() as Uint8Array; +} + +beforeAll(() => { + setBigIntegerBackend(nodeBackend); +}); + +/** + * Build a PKCS#8 private-key PEM with caller-chosen `version`, algorithm + * `oid`, and `innerVersion`. Components are taken from a valid fixture + * so the only thing wrong with the resulting file is the chosen header. + */ +function buildPkcs8Private(opts: { version: number; oid: string; innerVersion: number }): string { + const k = new NodeRSA(readStr('private_pkcs1.pem')); + const kp = k.keyPair; + + const body = new DerWriter(); + body.startSequence(); + body.writeInteger(opts.innerVersion); + body.writeInteger(toBytes(kp.n!)); + body.writeInteger(kp.e); + body.writeInteger(toBytes(kp.d!)); + body.writeInteger(toBytes(kp.p!)); + body.writeInteger(toBytes(kp.q!)); + body.writeInteger(toBytes(kp.dmp1!)); + body.writeInteger(toBytes(kp.dmq1!)); + body.writeInteger(toBytes(kp.coeff!)); + body.endSequence(); + + const w = new DerWriter(); + w.startSequence(); + w.writeInteger(opts.version); + w.startSequence(); + w.writeOid(opts.oid); + w.writeNull(); + w.endSequence(); + w.writeOctetString(body.toBytes()); + w.endSequence(); + + return encodePem(w.toBytes(), '-----BEGIN PRIVATE KEY-----', '-----END PRIVATE KEY-----'); +} + +describe('PKCS#8 — H8 algorithm OID allowlist with clear diagnostics', () => { + it('rejects RSASSA-PSS-only OID (1.2.840.113549.1.1.10)', () => { + const pem = buildPkcs8Private({ + version: 0, + oid: '1.2.840.113549.1.1.10', + innerVersion: 0, + }); + expect(() => new NodeRSA(pem)).toThrow(/RSASSA-PSS-only/); + }); + + it('rejects RSAES-OAEP-only OID (1.2.840.113549.1.1.7)', () => { + const pem = buildPkcs8Private({ + version: 0, + oid: '1.2.840.113549.1.1.7', + innerVersion: 0, + }); + expect(() => new NodeRSA(pem)).toThrow(/RSAES-OAEP-only/); + }); + + it('rejects unknown algorithm OID with generic diagnostic', () => { + const pem = buildPkcs8Private({ + version: 0, + oid: '1.2.3.4.5', + innerVersion: 0, + }); + expect(() => new NodeRSA(pem)).toThrow(/unsupported algorithm OID/); + }); + + it('accepts the canonical rsaEncryption OID', () => { + const pem = buildPkcs8Private({ + version: 0, + oid: OID.RSA_ENCRYPTION, + innerVersion: 0, + }); + expect(() => new NodeRSA(pem)).not.toThrow(); + }); +}); + +describe('PKCS#8 — M7 version validation', () => { + it('rejects outer version = 2 (out of RFC 5958 set {0, 1})', () => { + const pem = buildPkcs8Private({ + version: 2, + oid: OID.RSA_ENCRYPTION, + innerVersion: 0, + }); + expect(() => new NodeRSA(pem)).toThrow(/unsupported version 2/); + }); + + it('rejects outer version = 42', () => { + const pem = buildPkcs8Private({ + version: 42, + oid: OID.RSA_ENCRYPTION, + innerVersion: 0, + }); + expect(() => new NodeRSA(pem)).toThrow(/unsupported version 42/); + }); + + it('rejects PKCS#1 multi-prime keys (inner version = 1)', () => { + const pem = buildPkcs8Private({ + version: 0, + oid: OID.RSA_ENCRYPTION, + innerVersion: 1, + }); + expect(() => new NodeRSA(pem)).toThrow(/multi-prime keys/); + }); + + it('accepts outer version = 0 (PrivateKeyInfo)', () => { + const pem = buildPkcs8Private({ + version: 0, + oid: OID.RSA_ENCRYPTION, + innerVersion: 0, + }); + expect(() => new NodeRSA(pem)).not.toThrow(); + }); + + it('accepts outer version = 1 (OneAsymmetricKey)', () => { + // Note: import treats v1 like v0 — we don't actually consume the + // optional public key field. RFC 5958 §2 permits version 1 for keys + // without a public-key component. + const pem = buildPkcs8Private({ + version: 1, + oid: OID.RSA_ENCRYPTION, + innerVersion: 0, + }); + expect(() => new NodeRSA(pem)).not.toThrow(); + }); +}); + +describe('PKCS#8 — encrypted PrivateKeyInfo rejection', () => { + // RFC 5958 §3 defines EncryptedPrivateKeyInfo as a separate ASN.1 + // structure wrapped in `-----BEGIN ENCRYPTED PRIVATE KEY-----`. We + // don't support encrypted PEMs (no passphrase API); importing one + // should fail cleanly rather than half-parse an OctetString that's + // actually ciphertext. + it('rejects -----BEGIN ENCRYPTED PRIVATE KEY----- wrapper', () => { + // We don't need a real OpenSSL-encrypted body — the PEM header alone + // should put detectAndImport on a path that yields no match. + const body = readStr('private_pkcs8.pem') + .replace('BEGIN PRIVATE KEY', 'BEGIN ENCRYPTED PRIVATE KEY') + .replace('END PRIVATE KEY', 'END ENCRYPTED PRIVATE KEY'); + expect(() => new NodeRSA(body)).toThrow(); + }); + + it('rejects an EncryptedPrivateKeyInfo PEM body even if header looks normal', () => { + // Build a fake EncryptedPrivateKeyInfo: SEQUENCE { algId(pbes2-OID), OCTET STRING garbage }. + // Wrap with the regular -----BEGIN PRIVATE KEY----- header so the + // detector tries pkcs8Format.privateImport — which should reject + // because the OID is not rsaEncryption. + const PBES2_OID = '1.2.840.113549.1.5.13'; // PBES2 (encrypted-key OID family) + const w = new DerWriter(); + w.startSequence(); + w.startSequence(); + w.writeOid(PBES2_OID); + w.writeNull(); + w.endSequence(); + w.writeOctetString(new Uint8Array([0xde, 0xad, 0xbe, 0xef])); + w.endSequence(); + const fakePem = encodePem( + w.toBytes(), + '-----BEGIN PRIVATE KEY-----', + '-----END PRIVATE KEY-----', + ); + // The pkcs8 importer reads outerVersion first, but we omitted it — so it + // will throw on the version read. Either error is acceptable; assert throw. + expect(() => new NodeRSA(fakePem)).toThrow(); + }); +}); diff --git a/test/formats/roundtrip.spec.ts b/test/formats/roundtrip.spec.ts new file mode 100644 index 0000000..e160cef --- /dev/null +++ b/test/formats/roundtrip.spec.ts @@ -0,0 +1,285 @@ +import { readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { beforeAll, describe, expect, it } from 'vitest'; +import { setBigIntegerBackend } from '../../src/bigint/big-integer.js'; +import { nodeBackend } from '../../src/crypto/backend.node.js'; +import { toHex, toUtf8 } from '../../src/crypto/bytes.js'; +import { + componentsFormat, + detectAndExport, + detectAndImport, + opensshFormat, + pkcs1Format, + pkcs8Format, +} from '../../src/formats/index.js'; +import { RSAKey } from '../../src/rsa/key.js'; + +const here = dirname(fileURLToPath(import.meta.url)); +const keysDir = resolve(here, '../../test/keys'); + +function readBin(name: string): Uint8Array { + const buf = readFileSync(resolve(keysDir, name)); + return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength); +} + +function readStr(name: string): string { + return readFileSync(resolve(keysDir, name), 'utf8'); +} + +beforeAll(() => { + setBigIntegerBackend(nodeBackend); +}); + +describe('PKCS#1 DER round-trip', () => { + it('private_pkcs1.der → import → export → byte-identical', () => { + const orig = readBin('private_pkcs1.der'); + const key = new RSAKey(); + pkcs1Format.privateImport?.(key, orig, { type: 'der' }); + expect(key.isPrivate()).toBe(true); + const re = pkcs1Format.privateExport?.(key, { type: 'der' }) as Uint8Array; + expect(toHex(re)).toBe(toHex(orig)); + }); + + it('public_pkcs1.der → import → export → byte-identical', () => { + const orig = readBin('public_pkcs1.der'); + const key = new RSAKey(); + pkcs1Format.publicImport?.(key, orig, { type: 'der' }); + expect(key.isPublic()).toBe(true); + const re = pkcs1Format.publicExport?.(key, { type: 'der' }) as Uint8Array; + expect(toHex(re)).toBe(toHex(orig)); + }); +}); + +describe('PKCS#1 PEM round-trip', () => { + it('private_pkcs1.pem → import → export → byte-equal trimmed', () => { + const orig = readStr('private_pkcs1.pem'); + const key = new RSAKey(); + pkcs1Format.privateImport?.(key, orig); + const re = pkcs1Format.privateExport?.(key) as string; + expect(stripPem(re)).toBe(stripPem(orig)); + }); + + it('public_pkcs1.pem → import → export → byte-equal trimmed', () => { + const orig = readStr('public_pkcs1.pem'); + const key = new RSAKey(); + pkcs1Format.publicImport?.(key, orig); + const re = pkcs1Format.publicExport?.(key) as string; + expect(stripPem(re)).toBe(stripPem(orig)); + }); +}); + +describe('PKCS#8 DER round-trip', () => { + it('private_pkcs8.der', () => { + const orig = readBin('private_pkcs8.der'); + const key = new RSAKey(); + pkcs8Format.privateImport?.(key, orig, { type: 'der' }); + const re = pkcs8Format.privateExport?.(key, { type: 'der' }) as Uint8Array; + expect(toHex(re)).toBe(toHex(orig)); + }); + + it('public_pkcs8.der', () => { + const orig = readBin('public_pkcs8.der'); + const key = new RSAKey(); + pkcs8Format.publicImport?.(key, orig, { type: 'der' }); + const re = pkcs8Format.publicExport?.(key, { type: 'der' }) as Uint8Array; + expect(toHex(re)).toBe(toHex(orig)); + }); +}); + +describe('PKCS#8 PEM round-trip', () => { + it('private_pkcs8.pem', () => { + const orig = readStr('private_pkcs8.pem'); + const key = new RSAKey(); + pkcs8Format.privateImport?.(key, orig); + const re = pkcs8Format.privateExport?.(key) as string; + expect(stripPem(re)).toBe(stripPem(orig)); + }); + + it('public_pkcs8.pem', () => { + const orig = readStr('public_pkcs8.pem'); + const key = new RSAKey(); + pkcs8Format.publicImport?.(key, orig); + const re = pkcs8Format.publicExport?.(key) as string; + expect(stripPem(re)).toBe(stripPem(orig)); + }); +}); + +describe('OpenSSH round-trip', () => { + it('id_rsa.pub (public key) parses and round-trips', () => { + const orig = readStr('id_rsa.pub'); + const key = new RSAKey(); + opensshFormat.publicImport?.(key, orig); + expect(key.isPublic()).toBe(true); + const re = opensshFormat.publicExport?.(key) as string; + // OpenSSH public format: "ssh-rsa BASE64 [comment]\n" + expect(re.startsWith('ssh-rsa ')).toBe(true); + // Round trip the base64 part + expect(extractB64(re)).toBe(extractB64(orig)); + }); + + it('id_rsa (private key) parses and round-trips', () => { + const orig = readStr('id_rsa'); + const key = new RSAKey(); + opensshFormat.privateImport?.(key, orig); + expect(key.isPrivate()).toBe(true); + const re = opensshFormat.privateExport?.(key) as string; + expect(stripPem(re)).toBe(stripPem(orig)); + }); + + it('id_rsa_comment preserves the comment field', () => { + const orig = readStr('id_rsa_comment.pub'); + const key = new RSAKey(); + opensshFormat.publicImport?.(key, orig); + expect(key.sshcomment).toBeDefined(); + expect(key.sshcomment).not.toBe(''); + }); +}); + +describe('components format', () => { + it('round-trips a key through { n, e, d, p, q, dmp1, dmq1, coeff }', () => { + const src = new RSAKey(); + pkcs1Format.privateImport?.(src, readBin('private_pkcs1.der'), { type: 'der' }); + const components = componentsFormat.privateExport?.(src) as Record; + const dst = new RSAKey(); + componentsFormat.privateImport?.(dst, components); + expect(dst.n?.toBuffer() as Uint8Array).toEqual(src.n?.toBuffer() as Uint8Array); + expect(dst.e).toBe(src.e); + expect(dst.d?.toBuffer() as Uint8Array).toEqual(src.d?.toBuffer() as Uint8Array); + }); + + it('rejects missing private fields', () => { + const key = new RSAKey(); + expect(() => componentsFormat.privateImport?.(key, { n: new Uint8Array([1, 2]) })).toThrow(); + }); + + it('round-trips a public key through { n, e }', () => { + const src = new RSAKey(); + pkcs1Format.publicImport?.(src, readBin('public_pkcs1.der'), { type: 'der' }); + expect(src.isPublic()).toBe(true); + const components = componentsFormat.publicExport?.(src) as Record; + expect(components).toHaveProperty('n'); + expect(components).toHaveProperty('e'); + const dst = new RSAKey(); + componentsFormat.publicImport?.(dst, components); + expect(dst.isPublic()).toBe(true); + expect(dst.isPrivate()).toBe(false); + expect(dst.n?.toBuffer() as Uint8Array).toEqual(src.n?.toBuffer() as Uint8Array); + expect(dst.e).toBe(src.e); + }); + + it('rejects missing public fields (no n)', () => { + const key = new RSAKey(); + expect(() => componentsFormat.publicImport?.(key, { e: 65537 })).toThrow(); + }); +}); + +describe('cross-format equivalence — same key parsed three ways yields identical components', () => { + // The PKCS#1, PKCS#8, and OpenSSH fixtures in test/keys/ are all + // serialisations of the same RSA key. A parse mismatch (a renamed CRT + // field, an off-by-one in OpenSSH's component order — which is + // n,e,d,coeff,p,q not n,e,d,p,q,coeff — etc.) would silently let one + // format diverge while the others keep round-tripping. This test pins + // the invariant directly. + it('PKCS#1 DER ≡ PKCS#8 DER ≡ OpenSSH (n,e,d,p,q,dmp1,dmq1,coeff)', () => { + const fromPkcs1 = new RSAKey(); + pkcs1Format.privateImport?.(fromPkcs1, readBin('private_pkcs1.der'), { type: 'der' }); + + const fromPkcs8 = new RSAKey(); + pkcs8Format.privateImport?.(fromPkcs8, readBin('private_pkcs8.der'), { type: 'der' }); + + const fromOpenssh = new RSAKey(); + opensshFormat.privateImport?.(fromOpenssh, readStr('id_rsa')); + + const fields: Array<'n' | 'd' | 'p' | 'q' | 'dmp1' | 'dmq1' | 'coeff'> = [ + 'n', + 'd', + 'p', + 'q', + 'dmp1', + 'dmq1', + 'coeff', + ]; + for (const f of fields) { + const a = fromPkcs1[f]?.toBuffer() as Uint8Array; + const b = fromPkcs8[f]?.toBuffer() as Uint8Array; + const c = fromOpenssh[f]?.toBuffer() as Uint8Array; + expect(toHex(b), `PKCS#1 vs PKCS#8: ${f}`).toBe(toHex(a)); + expect(toHex(c), `PKCS#1 vs OpenSSH: ${f}`).toBe(toHex(a)); + } + expect(fromPkcs8.e).toBe(fromPkcs1.e); + expect(fromOpenssh.e).toBe(fromPkcs1.e); + }); + + it('public PKCS#1 DER ≡ public PKCS#8 DER (n, e)', () => { + const fromPkcs1 = new RSAKey(); + pkcs1Format.publicImport?.(fromPkcs1, readBin('public_pkcs1.der'), { type: 'der' }); + const fromPkcs8 = new RSAKey(); + pkcs8Format.publicImport?.(fromPkcs8, readBin('public_pkcs8.der'), { type: 'der' }); + expect(toHex(fromPkcs8.n?.toBuffer() as Uint8Array)).toBe( + toHex(fromPkcs1.n?.toBuffer() as Uint8Array), + ); + expect(fromPkcs8.e).toBe(fromPkcs1.e); + }); + + it('id_rsa_comment.pub preserves a non-empty SSH comment', () => { + // id_rsa_comment.pub carries a non-empty trailing comment that gets + // routed into key.sshcomment. Lets us round-trip through publicExport + // and confirm the comment survives. + const key = new RSAKey(); + opensshFormat.publicImport?.(key, readStr('id_rsa_comment.pub')); + expect(key.sshcomment).toBeDefined(); + expect((key.sshcomment as string).length).toBeGreaterThan(0); + const re = opensshFormat.publicExport?.(key) as string; + // Export format: "ssh-rsa \n" — comment must appear. + expect(re).toContain(key.sshcomment as string); + }); +}); + +describe('detectAndImport / detectAndExport', () => { + it('detects PKCS#1 PEM private', () => { + const key = new RSAKey(); + expect(detectAndImport(key, readStr('private_pkcs1.pem'))).toBe(true); + expect(key.isPrivate()).toBe(true); + }); + + it('detects PKCS#8 PEM public', () => { + const key = new RSAKey(); + expect(detectAndImport(key, readStr('public_pkcs8.pem'))).toBe(true); + expect(key.isPublic()).toBe(true); + }); + + it('explicit format string drives export', () => { + const key = new RSAKey(); + pkcs1Format.privateImport?.(key, readBin('private_pkcs1.der'), { type: 'der' }); + const pem = detectAndExport(key, 'pkcs8-private-pem') as string; + expect(pem).toContain('BEGIN PRIVATE KEY'); + }); + + it('throws when exporting private from a public-only key', () => { + const key = new RSAKey(); + pkcs1Format.publicImport?.(key, readBin('public_pkcs1.der'), { type: 'der' }); + expect(() => detectAndExport(key, 'pkcs1-private-pem')).toThrow(/not private/); + }); + + it('throws for unsupported format', () => { + const key = new RSAKey(); + pkcs1Format.privateImport?.(key, readBin('private_pkcs1.der'), { type: 'der' }); + expect(() => detectAndExport(key, 'magic-private-pem')).toThrow(/Unsupported/); + }); +}); + +function stripPem(s: string): string { + return s.replace(/\r?\n/g, '\n').trim(); +} + +function extractB64(s: string): string { + // "ssh-rsa [comment]\n" → return + const after = s.replace(/^ssh-rsa\s+/, ''); + const space = after.indexOf(' '); + const b64 = space === -1 ? after : after.substring(0, space); + return b64.trim(); +} + +// Suppress unused warning +void toUtf8; diff --git a/test/node-rsa.smoke.spec.ts b/test/node-rsa.smoke.spec.ts new file mode 100644 index 0000000..22395ea --- /dev/null +++ b/test/node-rsa.smoke.spec.ts @@ -0,0 +1,165 @@ +import { readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; +import NodeRSA from '../src/index.node.js'; + +const here = dirname(fileURLToPath(import.meta.url)); +const keysDir = resolve(here, 'keys'); + +function readStr(name: string): string { + return readFileSync(resolve(keysDir, name), 'utf8'); +} + +describe('NodeRSA smoke', () => { + it('constructs empty', () => { + const key = new NodeRSA(); + expect(key.isEmpty()).toBe(true); + expect(key.isPrivate()).toBe(false); + expect(key.isPublic()).toBe(false); + }); + + it('imports a PKCS#1 private PEM and exports it back', () => { + const orig = readStr('private_pkcs1.pem'); + const key = new NodeRSA(orig); + expect(key.isPrivate()).toBe(true); + expect(key.getKeySize()).toBe(1024); + const pem = key.exportKey('pkcs1-private-pem'); + expect(pem).toContain('BEGIN RSA PRIVATE KEY'); + }); + + it('round-trips encrypt → decrypt with default OAEP scheme', () => { + const key = new NodeRSA(readStr('private_pkcs1.pem')); + const ct = key.encrypt('hello world'); + expect(ct).toBeInstanceOf(Uint8Array); + const pt = key.decrypt(ct as Uint8Array, 'utf8'); + expect(pt).toBe('hello world'); + }); + + it('round-trips sign → verify with PKCS#1', () => { + const key = new NodeRSA(readStr('private_pkcs1.pem')); + const sig = key.sign('signed data'); + expect(sig).toBeInstanceOf(Uint8Array); + expect(key.verify('signed data', sig as Uint8Array)).toBe(true); + expect(key.verify('tampered', sig as Uint8Array)).toBe(false); + }); + + it('setOptions parses combined format like "pss-sha512"', () => { + const key = new NodeRSA(readStr('private_pkcs1.pem')); + key.setOptions({ signingScheme: 'pss-sha512' }); + expect(key.$options.signingScheme).toBe('pss'); + expect(key.$options.signingSchemeOptions.hash).toBe('sha512'); + const sig = key.sign('payload'); + expect(key.verify('payload', sig as Uint8Array)).toBe(true); + }); + + it('encrypts with base64 output encoding', () => { + const key = new NodeRSA(readStr('private_pkcs1.pem')); + const b64 = key.encrypt('hi', 'base64') as string; + expect(typeof b64).toBe('string'); + expect(b64).toMatch(/^[A-Za-z0-9+/=]+$/); + const pt = key.decrypt(b64, 'utf8'); + expect(pt).toBe('hi'); + }); + + it('decrypts to JSON', () => { + const key = new NodeRSA(readStr('private_pkcs1.pem')); + const ct = key.encrypt({ x: 1, y: [2, 3] }); + const obj = key.decrypt(ct as Uint8Array, 'json'); + expect(obj).toEqual({ x: 1, y: [2, 3] }); + }); + + it('encryptPrivate ↔ decryptPublic', () => { + const key = new NodeRSA(readStr('private_pkcs1.pem')); + const ct = key.encryptPrivate('private-encrypted'); + const pt = key.decryptPublic(ct as Uint8Array, 'utf8'); + expect(pt).toBe('private-encrypted'); + }); + + it("round-trips bytes ≥0x80 through encoding: 'binary' (latin1)", () => { + // Regression: legacy node-rsa v1 treats 'binary' as latin1 (1:1 byte↔char + // over 0x00-0xFF). Routing it through UTF-8 corrupts any byte ≥0x80 — + // either by re-encoding it as a 2-byte sequence on the way in or by + // substituting U+FFFD on the way out. Use a string whose every charcode + // is in U+0080-U+00FF so the failure mode is unambiguous. Exercise both + // legs (input via sourceEncoding='binary'; output via encoding='binary'). + const key = new NodeRSA(readStr('private_pkcs1.pem')); + const highBytes = new Uint8Array([0x80, 0xc3, 0xff, 0xa9, 0xde, 0xad, 0xbe, 0xef]); + const asLatin1 = String.fromCharCode(...highBytes); + const ct = key.encrypt(asLatin1, undefined, 'binary') as Uint8Array; + const pt = key.decrypt(ct, 'binary') as string; + expect(pt.length).toBe(highBytes.length); + for (let i = 0; i < highBytes.length; i++) { + expect(pt.charCodeAt(i)).toBe(highBytes[i]); + } + }); + + it('throws on import of empty key', () => { + expect(() => new NodeRSA('')).toThrow(/Empty key/); + }); + + it('exportKey caches', () => { + const key = new NodeRSA(readStr('private_pkcs1.pem')); + const a = key.exportKey(); + const b = key.exportKey(); + expect(a).toBe(b); + }); + + it('generates a fresh 512-bit key and uses it', () => { + const key = new NodeRSA({ b: 512 }); + expect(key.getKeySize()).toBe(512); + const ct = key.encrypt('round-trip'); + const pt = key.decrypt(ct as Uint8Array, 'utf8'); + expect(pt).toBe('round-trip'); + }, 35_000); + + it('post-keygen comprehensive: 512-bit key works end-to-end for OAEP + PSS + PKCS#1', () => { + // Regression: a generated key must be usable across every scheme the + // public API exposes — not just the constructor default. Failures + // here mean keygen produced components that fail one of the schemes + // (a CRT inconsistency, a wrong d, etc.). + const key = new NodeRSA({ b: 512 }); + expect(key.isPrivate()).toBe(true); + expect(key.keyPair.n!.bitLength()).toBe(512); + + // Default scheme set (OAEP encryption, PSS signing) — verified above. + { + const ct = key.encrypt('default-schemes'); + const pt = key.decrypt(ct as Uint8Array, 'utf8'); + expect(pt).toBe('default-schemes'); + const sig = key.sign('default-schemes'); + expect(key.verify('default-schemes', sig as Uint8Array)).toBe(true); + } + + // Switch to PKCS#1 v1.5 encryption + signing (legacy path). + key.setOptions({ encryptionScheme: 'pkcs1', signingScheme: 'pkcs1' }); + { + const ct = key.encrypt('pkcs1-schemes'); + const pt = key.decrypt(ct as Uint8Array, 'utf8'); + expect(pt).toBe('pkcs1-schemes'); + const sig = key.sign('pkcs1-schemes'); + expect(key.verify('pkcs1-schemes', sig as Uint8Array)).toBe(true); + } + + // PSS with a non-default hash. 512-bit emLen=64 limits PSS to + // hashes where hLen + sLen + 2 ≤ 64; sha384/sha512 don't fit at + // the default saltLength=20. Use sha1 (hLen=20+20+2=42 ≤ 64) — and + // do a saltLength=0 variant for coverage of the deterministic path. + key.setOptions({ + signingScheme: { scheme: 'pss', hash: 'sha1' }, + }); + { + const sig = key.sign('pss-sha1'); + expect(key.verify('pss-sha1', sig as Uint8Array)).toBe(true); + } + + // encryptPrivate ↔ decryptPublic (signature-shaped path) — exercises + // the type-1 padding branch built from the generated p, q, dp, dq. + key.setOptions({ encryptionScheme: 'pkcs1', signingScheme: 'pkcs1' }); + { + const ct = key.encryptPrivate('private-encrypt-with-gen'); + const pt = key.decryptPublic(ct as Uint8Array, 'utf8'); + expect(pt).toBe('private-encrypt-with-gen'); + } + }, 45_000); +}); diff --git a/test/node-rsa.spec.ts b/test/node-rsa.spec.ts new file mode 100644 index 0000000..f7c6089 --- /dev/null +++ b/test/node-rsa.spec.ts @@ -0,0 +1,908 @@ +import { readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { assert } from 'chai'; +import { beforeAll, describe, it } from 'vitest'; +import { nodeBackend } from '../src/crypto/backend.node.js'; +import { toHex } from '../src/crypto/bytes.js'; +import { DIGEST_LENGTH } from '../src/crypto/digest-length.js'; +import NodeRSA from '../src/index.node.js'; +import type { HashingAlgorithm } from '../src/types.js'; + +// 1-to-1 port of v1's test/tests.js (mocha+chai → vitest+chai). Structure, +// describe/it titles, and assertions match the legacy file. Buffer-specific +// uses are translated to Uint8Array equivalents; deprecated environment +// "iojs" is removed (v2 supports only "node" and "browser"). +const here = dirname(fileURLToPath(import.meta.url)); +const keysFolder = resolve(here, 'keys'); +const RSA_NO_PADDING = 3; + +function readFile(name: string): Uint8Array { + const buf = readFileSync(resolve(keysFolder, name)); + return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength); +} + +function readStr(name: string): string { + return readFileSync(resolve(keysFolder, name), 'utf8'); +} + +function asHex(x: unknown): string { + if (x instanceof Uint8Array) return toHex(x); + if (typeof x === 'string') return toHex(new TextEncoder().encode(x)); + throw new Error(`Cannot toHex: ${typeof x}`); +} + +describe('NodeRSA', () => { + const keySizes = [ + { b: 512, e: 3 }, + { b: 512, e: 5 }, + { b: 512, e: 257 }, + { b: 512, e: 65537 }, + { b: 768 }, + { b: 1024 }, + { b: 2048 }, // exercises PSS-SHA512 with max salt length + ]; + + const environments = ['browser', 'node'] as const; + const encryptSchemes: Array = [ + 'pkcs1', + 'pkcs1_oaep', + { + scheme: 'pkcs1', + padding: RSA_NO_PADDING, + toString() { + return 'pkcs1-nopadding'; + }, + }, + ]; + const signingSchemes = ['pkcs1', 'pss'] as const; + const signHashAlgorithms: Record<'node' | 'browser', string[]> = { + node: ['MD4', 'MD5', 'RIPEMD160', 'SHA1', 'SHA224', 'SHA256', 'SHA384', 'SHA512'], + browser: ['MD5', 'RIPEMD160', 'SHA1', 'SHA256', 'SHA512'], + }; + + // MD4 lives in OpenSSL's legacy provider, not loaded by default in + // OpenSSL 3 (Node 17+). Skip MD4-specific cases when unsupported. + function shouldSkip(alg: string): boolean { + return alg.toLowerCase() === 'md4' && !nodeBackend.supportsHash('md4'); + } + + type DataKey = + | 'string' + | 'unicode string' + | 'empty string' + | 'long string' + | 'buffer' + | 'json object' + | 'json array'; + const dataBundle: Record = { + string: { data: 'ascii + 12345678', encoding: 'utf8' }, + 'unicode string': { data: 'ascii + юникод スラ ⑨', encoding: 'utf8' }, + 'empty string': { data: '', encoding: ['utf8', 'ascii', 'hex', 'base64'] }, + 'long string': { + data: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', + encoding: ['utf8', 'ascii'], + }, + buffer: { data: new TextEncoder().encode('ascii + юникод スラ ⑨'), encoding: 'buffer' }, + 'json object': { + data: { + str: 'string', + arr: ['a', 'r', 'r', 'a', 'y', true, '⑨'], + int: 42, + nested: { key: { key: 1 } }, + }, + encoding: 'json', + }, + 'json array': { + data: [1, 2, 3, 4, 5, 6, 7, 8, 9, [10, 11, 12, [13], 14, 15, [16, 17, [18]]]], + encoding: 'json', + }, + }; + + const privateKeyPKCS1 = readStr('private_pkcs1.pem').trim(); + const publicKeyPKCS8 = readStr('public_pkcs8.pem').trim(); + + const generatedKeys: NodeRSA[] = []; + let privateNodeRSA: NodeRSA; + let publicNodeRSA: NodeRSA; + + beforeAll(() => { + // Generate the matrix once. Used by encrypt/decrypt and sign/verify suites. + for (const size of keySizes) { + const key = new NodeRSA({ b: size.b, e: size.e }, { encryptionScheme: 'pkcs1' }); + generatedKeys.push(key); + } + }, 60_000); + + describe('Setup options', () => { + it('should use browser environment', () => { + assert.equal(new NodeRSA(null, { environment: 'browser' }).$options.environment, 'browser'); + }); + + it('should use io.js environment', () => { + // v2 doesn't support "iojs"; behaviour is to leave it as-set (no validation). + assert.equal( + new NodeRSA(null, { environment: 'iojs' as 'node' }).$options.environment as string, + 'iojs', + ); + }); + + it('should make empty key pair with default options', () => { + const key = new NodeRSA(null); + assert.equal(key.isEmpty(), true); + // Default signing scheme is 'pss' (was 'pkcs1' in v1). + assert.equal(key.$options.signingScheme, 'pss'); + assert.equal(key.$options.signingSchemeOptions.hash, 'sha256'); + assert.equal(key.$options.signingSchemeOptions.saltLength, undefined); + + assert.equal(key.$options.encryptionScheme, 'pkcs1_oaep'); + assert.equal(key.$options.encryptionSchemeOptions.hash, 'sha1'); + assert.equal(key.$options.encryptionSchemeOptions.label, undefined); + }); + + it('should make key pair with pss-md5 signing scheme via bare-hash shorthand', () => { + // Bare `'md5'` parses as "default scheme + md5 hash"; default scheme is 'pss'. + const key = new NodeRSA(null, { signingScheme: 'md5' }); + assert.equal(key.$options.signingScheme, 'pss'); + assert.equal(key.$options.signingSchemeOptions.hash, 'md5'); + }); + + it('should make key pair with pss-sha512 signing scheme', () => { + const key = new NodeRSA(null, { signingScheme: 'pss-sha512' }); + assert.equal(key.$options.signingScheme, 'pss'); + assert.equal(key.$options.signingSchemeOptions.hash, 'sha512'); + }); + + it('should make key pair with pkcs1 encryption scheme, and pss-sha1 signing scheme', () => { + const key = new NodeRSA(null, { encryptionScheme: 'pkcs1', signingScheme: 'pss' }); + assert.equal(key.$options.encryptionScheme, 'pkcs1'); + assert.equal(key.$options.signingScheme, 'pss'); + assert.equal(key.$options.signingSchemeOptions.hash, undefined); + }); + + it('change options', () => { + const key = new NodeRSA(null, { signingScheme: 'pss-sha1' }); + assert.equal(key.$options.signingScheme, 'pss'); + assert.equal(key.$options.signingSchemeOptions.hash, 'sha1'); + key.setOptions({ signingScheme: 'pkcs1' }); + assert.equal(key.$options.signingScheme, 'pkcs1'); + assert.equal(key.$options.signingSchemeOptions.hash, undefined); + key.setOptions({ signingScheme: 'pkcs1-sha256' }); + assert.equal(key.$options.signingScheme, 'pkcs1'); + assert.equal(key.$options.signingSchemeOptions.hash, 'sha256'); + }); + + it('advanced options change', () => { + const key = new NodeRSA(null); + key.setOptions({ + encryptionScheme: { + scheme: 'pkcs1_oaep', + hash: 'sha512', + label: new TextEncoder().encode('horay'), + }, + signingScheme: { scheme: 'pss', hash: 'md5', saltLength: 15 }, + }); + + assert.equal(key.$options.signingScheme, 'pss'); + assert.equal(key.$options.signingSchemeOptions.hash, 'md5'); + assert.equal(key.$options.signingSchemeOptions.saltLength, 15); + assert.equal(key.$options.encryptionScheme, 'pkcs1_oaep'); + assert.equal(key.$options.encryptionSchemeOptions.hash, 'sha512'); + assert.deepEqual( + key.$options.encryptionSchemeOptions.label, + new TextEncoder().encode('horay'), + ); + }); + + it("should throw 'unsupported hashing algorithm' exception", () => { + const key = new NodeRSA(null); + assert.equal(key.isEmpty(), true); + assert.equal(key.$options.signingScheme, 'pss'); + assert.equal(key.$options.signingSchemeOptions.hash, 'sha256'); + assert.throws(() => { + key.setOptions({ environment: 'browser', signingScheme: 'md4' }); + }, /Unsupported hashing algorithm/); + }); + }); + + describe('Base methods', () => { + it('importKey() should throw exception if key data not specified', () => { + const key = new NodeRSA(null); + assert.throws(() => { + (key as { importKey: (k?: unknown) => void }).importKey(); + }, /Empty key given/); + }); + + it('importKey() should return this', () => { + const key = new NodeRSA(null); + assert.equal(key.importKey(publicKeyPKCS8), key); + }); + }); + + describe('Work with keys', () => { + describe('Generating keys', () => { + for (const size of keySizes) { + it(`should make key pair ${size.b}-bit length and public exponent is ${size.e ?? `${size.e} and should be 65537`}`, () => { + const key = generatedKeys[keySizes.indexOf(size)]; + assert.isObject(key?.keyPair); + assert.equal(key?.isEmpty(), false); + assert.equal(key?.getKeySize(), size.b); + assert.equal(key?.getMaxMessageSize(), size.b / 8 - 11); + assert.equal(key?.keyPair.e, size.e ?? 65537); + }, 35_000); + } + }); + + describe('Import/Export keys', () => { + const privateKeyPEMNotTrimmed = `random \n\n data \n\n ${privateKeyPKCS1}\n \n \n\n random data `; + const publicKeyPEMNotTrimmed = `\n\n\n\nrandom \n\n data\n ${publicKeyPKCS8}\n \n random data\n\n `; + + const fileKeyPKCS1 = readStr('private_pkcs1.pem').trim(); + const keys_formats: Record = { + 'pkcs1-private-der': { public: false, der: true, file: 'private_pkcs1.der' }, + 'pkcs1-private-pem': { public: false, der: false, file: 'private_pkcs1.pem' }, + 'pkcs8-private-der': { public: false, der: true, file: 'private_pkcs8.der' }, + 'pkcs8-private-pem': { public: false, der: false, file: 'private_pkcs8.pem' }, + 'pkcs1-public-der': { public: true, der: true, file: 'public_pkcs1.der' }, + 'pkcs1-public-pem': { public: true, der: false, file: 'public_pkcs1.pem' }, + 'pkcs8-public-der': { public: true, der: true, file: 'public_pkcs8.der' }, + 'pkcs8-public-pem': { public: true, der: false, file: 'public_pkcs8.pem' }, + private: { public: false, der: false, file: 'private_pkcs1.pem' }, + public: { public: true, der: false, file: 'public_pkcs8.pem' }, + 'private-der': { public: false, der: true, file: 'private_pkcs1.der' }, + 'public-der': { public: true, der: true, file: 'public_pkcs8.der' }, + pkcs1: { public: false, der: false, file: 'private_pkcs1.pem' }, + 'pkcs1-private': { public: false, der: false, file: 'private_pkcs1.pem' }, + 'pkcs1-der': { public: false, der: true, file: 'private_pkcs1.der' }, + pkcs8: { public: false, der: false, file: 'private_pkcs8.pem' }, + 'pkcs8-private': { public: false, der: false, file: 'private_pkcs8.pem' }, + 'pkcs8-der': { public: false, der: true, file: 'private_pkcs8.der' }, + 'pkcs1-public': { public: true, der: false, file: 'public_pkcs1.pem' }, + 'pkcs8-public': { public: true, der: false, file: 'public_pkcs8.pem' }, + 'openssh-public': { public: true, der: false, file: 'id_rsa.pub' }, + 'openssh-private': { public: false, der: false, file: 'id_rsa' }, + }; + + describe('Good cases', () => { + describe('Common cases', () => { + it('should load private key from (not trimmed) PKCS1-PEM string', () => { + privateNodeRSA = new NodeRSA(privateKeyPEMNotTrimmed); + assert.isObject(privateNodeRSA.keyPair); + assert(privateNodeRSA.isPrivate()); + assert(privateNodeRSA.isPublic()); + assert(!privateNodeRSA.isPublic(true)); + }); + + it('should load public key from (not trimmed) PKCS8-PEM string', () => { + publicNodeRSA = new NodeRSA(publicKeyPEMNotTrimmed); + assert.isObject(publicNodeRSA.keyPair); + assert(publicNodeRSA.isPublic()); + assert(publicNodeRSA.isPublic(true)); + assert(!publicNodeRSA.isPrivate()); + }); + + it('.exportKey() should return private PEM string', () => { + const exported = privateNodeRSA.exportKey('private') as string; + assert.equal(stripWs(exported), stripWs(privateKeyPKCS1)); + const exportedDefault = privateNodeRSA.exportKey() as string; + assert.equal(stripWs(exportedDefault), stripWs(privateKeyPKCS1)); + }); + + it('.exportKey() from public key should return pkcs8 public PEM string', () => { + assert.equal( + stripWs(publicNodeRSA.exportKey('public') as string), + stripWs(publicKeyPKCS8), + ); + }); + + it('.exportKey() from private key should return pkcs8 public PEM string', () => { + assert.equal( + stripWs(privateNodeRSA.exportKey('public') as string), + stripWs(publicKeyPKCS8), + ); + }); + + it('should create and load key from buffer/fs.readFileSync output', () => { + const buf = readFile('private_pkcs1.pem'); + const key1 = new NodeRSA(buf); + assert.equal(stripWs(key1.exportKey() as string), stripWs(fileKeyPKCS1)); + const key2 = new NodeRSA(); + key2.importKey(buf); + assert.equal(stripWs(key2.exportKey() as string), stripWs(fileKeyPKCS1)); + }); + + it('should gracefully handle data outside of encapsulation boundaries for pkcs1 private keys', () => { + const noisy = `Lorem ipsum${readStr('private_pkcs1.pem')}dulce et decorum`; + const key = new NodeRSA(noisy); + assert.equal(stripWs(key.exportKey() as string), stripWs(fileKeyPKCS1)); + }); + + it('should gracefully handle data outside of encapsulation boundaries for pkcs1 public keys', () => { + const noisy = `Lorem ipsum${readStr('public_pkcs1.pem')}dulce et decorum`; + const pub = new NodeRSA(noisy); + assert.isObject(pub.keyPair); + assert(pub.isPublic()); + assert(pub.isPublic(true)); + assert(!pub.isPrivate()); + }); + + it('should gracefully handle data outside of encapsulation boundaries for pkcs8 private keys', () => { + const noisy = `Lorem ipsum${readStr('private_pkcs8.pem')}dulce et decorum`; + const key = new NodeRSA(noisy); + assert.equal(stripWs(key.exportKey() as string), stripWs(fileKeyPKCS1)); + }); + + it('should gracefully handle data outside of encapsulation boundaries for pkcs8 public keys', () => { + const noisy = `Lorem ipsum${readStr('public_pkcs8.pem')}dulce et decorum`; + const pub = new NodeRSA(noisy); + assert.isObject(pub.keyPair); + assert(pub.isPublic()); + assert(pub.isPublic(true)); + assert(!pub.isPrivate()); + }); + + it('should handle data without begin/end encapsulation boundaries for pkcs1 private keys', () => { + const file = readStr('private_pkcs1.pem'); + const inner = file.substring( + '-----BEGIN RSA PRIVATE KEY-----'.length, + file.indexOf('-----END RSA PRIVATE KEY-----'), + ); + const key = new NodeRSA(inner, 'pkcs1-private-pem'); + assert.equal(stripWs(key.exportKey() as string), stripWs(fileKeyPKCS1)); + }); + + it('should handle data without begin/end encapsulation boundaries for pkcs1 public keys', () => { + const file = readStr('public_pkcs1.pem'); + const inner = file.substring( + '-----BEGIN RSA PUBLIC KEY-----'.length, + file.indexOf('-----END RSA PUBLIC KEY-----'), + ); + const pub = new NodeRSA(inner, 'pkcs1-public-pem'); + assert.isObject(pub.keyPair); + assert(pub.isPublic()); + assert(pub.isPublic(true)); + assert(!pub.isPrivate()); + }); + + it('should handle data without begin/end encapsulation boundaries for pkcs8 private keys', () => { + const file = readStr('private_pkcs8.pem'); + const inner = file.substring( + '-----BEGIN PRIVATE KEY-----'.length, + file.indexOf('-----END PRIVATE KEY-----'), + ); + const key = new NodeRSA(inner, 'pkcs8-private-pem'); + assert.equal(stripWs(key.exportKey() as string), stripWs(fileKeyPKCS1)); + }); + + it('should handle data without begin/end encapsulation boundaries for pkcs8 public keys', () => { + const file = readStr('public_pkcs8.pem'); + const inner = file.substring( + '-----BEGIN PUBLIC KEY-----'.length, + file.indexOf('-----END PUBLIC KEY-----'), + ); + const pub = new NodeRSA(inner, 'pkcs8-public-pem'); + assert.isObject(pub.keyPair); + assert(pub.isPublic()); + assert(pub.isPublic(true)); + assert(!pub.isPrivate()); + }); + + it('.importKey() from private components', () => { + const components = privateNodeRSA.exportKey('components') as Record< + string, + Uint8Array | number + >; + const key = new NodeRSA(); + key.importKey(components as unknown as object, 'components'); + assert(key.isPrivate()); + assert.equal( + stripWs(key.exportKey('pkcs1-private') as string), + stripWs(privateKeyPKCS1), + ); + assert.equal(stripWs(key.exportKey('pkcs8-public') as string), stripWs(publicKeyPKCS8)); + }); + + it('.importKey() from public components', () => { + const components = publicNodeRSA.exportKey('components-public') as Record< + string, + Uint8Array | number + >; + const key = new NodeRSA(); + key.importKey(components as unknown as object, 'components-public'); + assert(key.isPublic(true)); + assert.equal(stripWs(key.exportKey('pkcs8-public') as string), stripWs(publicKeyPKCS8)); + }); + + it('.exportKey() private components', () => { + const key = new NodeRSA(privateKeyPKCS1); + const c = key.exportKey('components') as { n: Uint8Array; e: number; d: Uint8Array }; + assert(c.n instanceof Uint8Array); + assert.equal(c.e, 65537); + assert(c.d instanceof Uint8Array); + }); + + it('.exportKey() public components', () => { + const key = new NodeRSA(publicKeyPKCS8); + const c = key.exportKey('components-public') as { n: Uint8Array; e: number }; + assert(c.n instanceof Uint8Array); + assert.equal(c.e, 65537); + }); + }); + + describe('Different key formats', () => { + const sampleKey = new NodeRSA(fileKeyPKCS1); + for (const [format, options] of Object.entries(keys_formats)) { + it(`should load from ${options.file} (${format})`, () => { + const key = new NodeRSA(readFile(options.file), format); + if (options.public) { + assert.equal( + stripWs(key.exportKey('public') as string), + stripWs(sampleKey.exportKey('public') as string), + ); + } else { + assert.equal( + stripWs(key.exportKey() as string), + stripWs(sampleKey.exportKey() as string), + ); + } + }); + + it(`should export to '${format}' format`, () => { + const keyData = readFile(options.file); + const exported = sampleKey.exportKey(format); + if (options.der) { + assert(exported instanceof Uint8Array); + assert.equal(toHex(exported as Uint8Array), toHex(keyData)); + } else { + assert(typeof exported === 'string'); + assert.equal( + (exported as string).replace(/\s+|\n\r|\n|\r$/gm, ''), + new TextDecoder().decode(keyData).replace(/\s+|\n\r|\n|\r$/gm, ''), + ); + } + }); + } + }); + + describe('OpenSSH keys', () => { + it('key export should preserve key data including comment', () => { + const opensshPrivateKey = readStr('id_rsa_comment'); + const opensshPublicKey = readStr('id_rsa_comment.pub'); + const opensshPriv = new NodeRSA(opensshPrivateKey); + const opensshPub = new NodeRSA(opensshPublicKey); + + assert.equal( + stripWs(opensshPriv.exportKey('openssh-private') as string), + stripWs(opensshPrivateKey), + ); + assert.equal( + stripWs(opensshPriv.exportKey('openssh-public') as string), + stripWs(opensshPublicKey), + ); + assert.equal( + stripWs(opensshPub.exportKey('openssh-public') as string), + stripWs(opensshPublicKey), + ); + }); + }); + }); + + describe('Bad cases', () => { + it('not public key', () => { + const key = new NodeRSA(); + assert.throws(() => key.exportKey(), /This is not private key/); + assert.throws(() => key.exportKey('public'), /This is not public key/); + }); + + it('not private key', () => { + const key = new NodeRSA(publicKeyPKCS8); + assert.throws(() => key.exportKey(), /This is not private key/); + assert.doesNotThrow(() => key.exportKey('public')); + }); + }); + }); + }); + + describe('Encrypting & decrypting', () => { + for (const env of environments) { + for (const scheme of encryptSchemes) { + const schemeLabel = typeof scheme === 'string' ? scheme : scheme.toString(); + describe(`Environment: ${env}. Encryption scheme: ${schemeLabel}`, () => { + describe('Good cases', () => { + const encrypted: Record = {}; + for (const [name, suit] of Object.entries(dataBundle)) { + it(`\`encrypt()\` should encrypt ${name}`, () => { + const idx = Math.floor(Math.random() * generatedKeys.length); + const sourceKey = generatedKeys[idx] as NodeRSA; + const key = new NodeRSA(sourceKey.exportKey(), { + environment: env, + encryptionScheme: scheme as 'pkcs1', + }); + const result = key.encrypt(suit.data); + assert(result instanceof Uint8Array); + assert((result as Uint8Array).length > 0); + encrypted[name] = result as Uint8Array; + }); + + it(`\`decrypt()\` should decrypt ${name}`, () => { + const idx = Math.floor(Math.random() * generatedKeys.length); + const sourceKey = generatedKeys[idx] as NodeRSA; + const key = new NodeRSA(sourceKey.exportKey(), { + environment: env, + encryptionScheme: scheme as 'pkcs1', + }); + const reEncrypted = key.encrypt(suit.data) as Uint8Array; + const enc = Array.isArray(suit.encoding) ? suit.encoding[0] : suit.encoding; + const dec = key.decrypt(reEncrypted, enc); + if (dec instanceof Uint8Array) { + assert.equal(asHex(suit.data), asHex(dec)); + } else { + assert.deepEqual(suit.data, dec); + } + }); + } + }); + + describe('Bad cases', () => { + it('unsupported data types', () => { + const key = generatedKeys[0] as NodeRSA; + assert.throws( + () => key.encrypt(null as unknown as string), + /Error during encryption/, + ); + assert.throws( + () => key.encrypt(undefined as unknown as string), + /Error during encryption/, + ); + assert.throws( + () => key.encrypt(true as unknown as string), + /Error during encryption/, + ); + }); + + it('incorrect key for decrypting', () => { + const k0 = generatedKeys[0] as NodeRSA; + const k1 = generatedKeys[1] as NodeRSA; + const encrypted = k0.encrypt('data') as Uint8Array; + assert.throws(() => k1.decrypt(encrypted), /Error during decryption/); + }); + }); + }); + } + + describe(`Environment: ${env}. encryptPrivate & decryptPublic`, () => { + for (const [name, suit] of Object.entries(dataBundle)) { + it(`\`encryptPrivate()\` should encrypt ${name}`, () => { + const idx = Math.floor(Math.random() * generatedKeys.length); + const src = generatedKeys[idx] as NodeRSA; + const key = new NodeRSA(src.exportKey(), { environment: env }); + const result = key.encryptPrivate(suit.data); + assert(result instanceof Uint8Array); + assert((result as Uint8Array).length > 0); + }); + + it(`\`decryptPublic()\` should decrypt ${name}`, () => { + const idx = Math.floor(Math.random() * generatedKeys.length); + const src = generatedKeys[idx] as NodeRSA; + const key = new NodeRSA(src.exportKey(), { environment: env }); + const enc = key.encryptPrivate(suit.data) as Uint8Array; + const encStr = Array.isArray(suit.encoding) ? suit.encoding[0] : suit.encoding; + const dec = key.decryptPublic(enc, encStr); + if (dec instanceof Uint8Array) { + assert.equal(asHex(suit.data), asHex(dec)); + } else { + assert.deepEqual(suit.data, dec); + } + }); + } + }); + } + + // Cross-environment compatibility + // JsEngine (forced when env='browser') and NodeNativeEngine (env='node') + // must produce interoperable ciphertexts for the same key. + // + // The legacy used `(function (i) { var key1, key2; it(...); it(...); })(i)` + // — an IIFE per data-bundle iteration — so each (encrypt, decrypt) pair + // had its own key1/key2. We get the same effect by declaring `let` bindings + // *inside* the for-of body: ES6 block scoping gives each iteration fresh + // bindings, and the encrypt/decrypt it() callbacks both close over the + // iteration-local pair. Without this, random key picking across iterations + // overwrites the shared variables and the decrypt test reads a key that + // doesn't match the ciphertext. + describe('Compatibility of different environments', () => { + for (const scheme of encryptSchemes) { + const schemeLabel = typeof scheme === 'string' ? scheme : scheme.toString(); + + // browser-encrypt → node-decrypt + for (const [name, suit] of Object.entries(dataBundle)) { + let key2A: NodeRSA; + let encryptedA: Uint8Array; + it(`Encryption scheme: ${schemeLabel} \`encrypt()\` by browser ${name}`, () => { + const idx = Math.floor(Math.random() * generatedKeys.length); + const sourceKey = (generatedKeys[idx] as NodeRSA).exportKey(); + const key1 = new NodeRSA(sourceKey, { + environment: 'browser', + encryptionScheme: scheme as 'pkcs1', + }); + key2A = new NodeRSA(sourceKey, { + environment: 'node', + encryptionScheme: scheme as 'pkcs1', + }); + encryptedA = key1.encrypt(suit.data) as Uint8Array; + assert(encryptedA instanceof Uint8Array); + assert(encryptedA.length > 0); + }); + + it(`Encryption scheme: ${schemeLabel} \`decrypt()\` by node ${name}`, () => { + const enc = Array.isArray(suit.encoding) ? suit.encoding[0] : suit.encoding; + const dec = key2A.decrypt(encryptedA, enc); + if (dec instanceof Uint8Array) { + assert.equal(asHex(suit.data), asHex(dec)); + } else { + assert.deepEqual(suit.data, dec); + } + }); + } + + // node-encrypt → browser-decrypt + for (const [name, suit] of Object.entries(dataBundle)) { + let key2B: NodeRSA; + let encryptedB: Uint8Array; + it(`Encryption scheme: ${schemeLabel} \`encrypt()\` by node ${name}. Scheme`, () => { + const idx = Math.floor(Math.random() * generatedKeys.length); + const sourceKey = (generatedKeys[idx] as NodeRSA).exportKey(); + const key1 = new NodeRSA(sourceKey, { + environment: 'node', + encryptionScheme: scheme as 'pkcs1', + }); + key2B = new NodeRSA(sourceKey, { + environment: 'browser', + encryptionScheme: scheme as 'pkcs1', + }); + encryptedB = key1.encrypt(suit.data) as Uint8Array; + assert(encryptedB instanceof Uint8Array); + assert(encryptedB.length > 0); + }); + + it(`Encryption scheme: ${schemeLabel} \`decrypt()\` by browser ${name}`, () => { + const enc = Array.isArray(suit.encoding) ? suit.encoding[0] : suit.encoding; + const dec = key2B.decrypt(encryptedB, enc); + if (dec instanceof Uint8Array) { + assert.equal(asHex(suit.data), asHex(dec)); + } else { + assert.deepEqual(suit.data, dec); + } + }); + } + } + + describe('encryptPrivate & decryptPublic', () => { + // browser-encryptPrivate → node-decryptPublic + for (const [name, suit] of Object.entries(dataBundle)) { + let key2C: NodeRSA; + let encryptedC: Uint8Array; + it(`\`encryptPrivate()\` by browser ${name}`, () => { + const idx = Math.floor(Math.random() * generatedKeys.length); + const sourceKey = (generatedKeys[idx] as NodeRSA).exportKey(); + const key1 = new NodeRSA(sourceKey, { environment: 'browser' }); + key2C = new NodeRSA(sourceKey, { environment: 'node' }); + encryptedC = key1.encryptPrivate(suit.data) as Uint8Array; + assert(encryptedC instanceof Uint8Array); + assert(encryptedC.length > 0); + }); + + it(`\`decryptPublic()\` by node ${name}`, () => { + const enc = Array.isArray(suit.encoding) ? suit.encoding[0] : suit.encoding; + const dec = key2C.decryptPublic(encryptedC, enc); + if (dec instanceof Uint8Array) { + assert.equal(asHex(suit.data), asHex(dec)); + } else { + assert.deepEqual(suit.data, dec); + } + }); + } + + // node-encryptPrivate → browser-decryptPublic + for (const [name, suit] of Object.entries(dataBundle)) { + let key2D: NodeRSA; + let encryptedD: Uint8Array; + it(`\`encryptPrivate()\` by node ${name}`, () => { + const idx = Math.floor(Math.random() * generatedKeys.length); + const sourceKey = (generatedKeys[idx] as NodeRSA).exportKey(); + // Note: legacy uses environment:'browser' for key1 in this loop + // too — looks like a v1 test bug; porting verbatim. + const key1 = new NodeRSA(sourceKey, { environment: 'browser' }); + key2D = new NodeRSA(sourceKey, { environment: 'node' }); + encryptedD = key1.encryptPrivate(suit.data) as Uint8Array; + assert(encryptedD instanceof Uint8Array); + assert(encryptedD.length > 0); + }); + + it(`\`decryptPublic()\` by browser ${name}`, () => { + const enc = Array.isArray(suit.encoding) ? suit.encoding[0] : suit.encoding; + const dec = key2D.decryptPublic(encryptedD, enc); + if (dec instanceof Uint8Array) { + assert.equal(asHex(suit.data), asHex(dec)); + } else { + assert.deepEqual(suit.data, dec); + } + }); + } + }); + }); + }); + + describe('Signing & verifying', () => { + for (const scheme of signingSchemes) { + describe(`Signing scheme: ${scheme}`, () => { + const envs: Array<'node' | 'browser'> = scheme === 'pkcs1' ? ['node', 'browser'] : ['node']; + + for (const env of envs) { + describe(`Good cases${envs.length > 1 ? ` in ${env} environment` : ''}`, () => { + for (const [name, suit] of Object.entries(dataBundle)) { + it(`should sign ${name}`, () => { + const sourceKey = generatedKeys[generatedKeys.length - 1] as NodeRSA; + const key = new NodeRSA(sourceKey.exportKey(), { + signingScheme: `${scheme}-sha256`, + environment: env, + }); + const signed = key.sign(suit.data); + assert(signed instanceof Uint8Array); + assert((signed as Uint8Array).length > 0); + }); + + it(`should verify ${name}`, () => { + const sourceKey = generatedKeys[generatedKeys.length - 1] as NodeRSA; + const key = new NodeRSA(sourceKey.exportKey(), { + signingScheme: `${scheme}-sha256`, + environment: env, + }); + const signed = key.sign(suit.data) as Uint8Array; + assert(key.verify(suit.data, signed)); + }); + } + + for (const alg of signHashAlgorithms[env]) { + it.skipIf(shouldSkip(alg))(`signing with custom algorithm (${alg})`, () => { + const sourceKey = generatedKeys[generatedKeys.length - 1] as NodeRSA; + const key = new NodeRSA(sourceKey.exportKey(), { + signingScheme: `${scheme}-${alg}`, + environment: env, + }); + const signed = key.sign('data'); + assert(key.verify('data', signed as Uint8Array)); + }); + + if (scheme === 'pss') { + it.skipIf(shouldSkip(alg))( + `signing with custom algorithm (${alg}) with max salt length`, + () => { + const a = alg.toLowerCase() as HashingAlgorithm; + const sourceKey = generatedKeys[generatedKeys.length - 1] as NodeRSA; + const key = new NodeRSA(sourceKey.exportKey(), { + signingScheme: { scheme: scheme, hash: a, saltLength: DIGEST_LENGTH[a] }, + environment: env, + }); + const signed = key.sign('data'); + assert(key.verify('data', signed as Uint8Array)); + }, + ); + } + } + }); + + describe(`Bad cases${envs.length > 1 ? ` in ${env} environment` : ''}`, () => { + it('incorrect data for verifying', () => { + const src = generatedKeys[0] as NodeRSA; + const key = new NodeRSA(src.exportKey(), { + signingScheme: `${scheme}-sha256`, + environment: env, + }); + const signed = key.sign('data1'); + assert(!key.verify('data2', signed as Uint8Array)); + }); + + it('incorrect key for signing', () => { + const src = generatedKeys[0] as NodeRSA; + const key = new NodeRSA(src.exportKey('pkcs8-public'), { + signingScheme: `${scheme}-sha256`, + environment: env, + }); + assert.throws(() => key.sign('data'), /This is not private key/); + }); + + it('incorrect key for verifying', () => { + const src0 = generatedKeys[0] as NodeRSA; + const src1 = generatedKeys[1] as NodeRSA; + const key1 = new NodeRSA(src0.exportKey(), { + signingScheme: `${scheme}-sha256`, + environment: env, + }); + const key2 = new NodeRSA(src1.exportKey('pkcs8-public'), { + signingScheme: `${scheme}-sha256`, + environment: env, + }); + const signed = key1.sign('data'); + assert(!key2.verify('data', signed as Uint8Array)); + }); + + it('incorrect key for verifying (empty)', () => { + const key = new NodeRSA(null, { environment: env }); + assert.throws(() => key.verify('data', 'somesignature'), /This is not public key/); + }); + + it('different algorithms', () => { + const src = generatedKeys[0] as NodeRSA; + const signKey = new NodeRSA(src.exportKey(), { + signingScheme: `${scheme}-md5`, + environment: env, + }); + const verifyKey = new NodeRSA(src.exportKey(), { + signingScheme: `${scheme}-sha1`, + environment: env, + }); + const signed = signKey.sign('data'); + assert(!verifyKey.verify('data', signed as Uint8Array)); + }); + }); + } + + // Cross-environment compatibility + // PSS uses a random salt, so cross-env signature bytes don't match. + // Only PKCS#1 v1.5 (deterministic) gets cross-env equality testing. + if (scheme !== 'pkcs1') return; + + describe('Compatibility of different environments', () => { + for (const alg of signHashAlgorithms.browser) { + it.skipIf(shouldSkip(alg))( + `signing with custom algorithm (${alg}) (equal test)`, + () => { + const sourceKey = (generatedKeys[5] as NodeRSA).exportKey(); + const nodeKey = new NodeRSA(sourceKey, { + signingScheme: `${scheme}-${alg}`, + environment: 'node', + }); + const browserKey = new NodeRSA(sourceKey, { + signingScheme: `${scheme}-${alg}`, + environment: 'browser', + }); + assert.equal(nodeKey.sign('data', 'hex'), browserKey.sign('data', 'hex')); + }, + ); + + it.skipIf(shouldSkip(alg))(`sign in node & verify in browser (${alg})`, () => { + const sourceKey = (generatedKeys[5] as NodeRSA).exportKey(); + const nodeKey = new NodeRSA(sourceKey, { + signingScheme: `${scheme}-${alg}`, + environment: 'node', + }); + const browserKey = new NodeRSA(sourceKey, { + signingScheme: `${scheme}-${alg}`, + environment: 'browser', + }); + assert(browserKey.verify('data', nodeKey.sign('data') as Uint8Array)); + }); + + it.skipIf(shouldSkip(alg))(`sign in browser & verify in node (${alg})`, () => { + const sourceKey = (generatedKeys[5] as NodeRSA).exportKey(); + const nodeKey = new NodeRSA(sourceKey, { + signingScheme: `${scheme}-${alg}`, + environment: 'node', + }); + const browserKey = new NodeRSA(sourceKey, { + signingScheme: `${scheme}-${alg}`, + environment: 'browser', + }); + assert(nodeKey.verify('data', browserKey.sign('data') as Uint8Array)); + }); + } + }); + }); + } + }); +}); + +function stripWs(s: string): string { + return s.replace(/\s+/g, ''); +} diff --git a/test/private_pkcs1.pem b/test/private_pkcs1.pem deleted file mode 100644 index 2ea486c..0000000 --- a/test/private_pkcs1.pem +++ /dev/null @@ -1,15 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIICXAIBAAKBgQCCdY+EpDC/vPa335l751SBM8d5Lf4z4QZX4bc+DqTY9zVY/rmP -GbTkCueKnIKApuOGMXJOaCwNH9wUftNt7T0foEwjl16uIC8m4hwSjjNL5TKqMVey -Syv04oBuidv76u5yNiLC4J85lbmW3WAyYkTCbm/VJZAXNJuqCm7AVWmQMQIDAQAB -AoGAEYR3oPfrE9PrzQTZNyn4zuCFCGCEobK1h1dno42T1Q5cu3Z4tB5fi79rF9Gs -NFo0cvBwyNZ0E88TXi0pdrlEW6mdPgQFd3CFxrOgKt9AGpOtI1zzVOb1Uddywq/m -WBPyETwEKzq7lC2nAcMUr0rlFrrDmUT2dafHeuWnFMZ/1YECQQDCtftsH9/prbgu -Q4F2lOWsLz96aix/jnI8FhBmukKmfLMXjCZYYv+Dsr8TIl/iriGqcSgGkBHHoGe1 -nmLUZ4EHAkEAq4YcB8T9DLIYUeaS+JRWwLOejU6/rYdgxBIaGn2m0Ldp/z7lLM7g -b0H5Al+7POajkAdnDclBDhyxqInHO4VvBwJBAJ25jNEpgNhqQKg5RsYoF2RDYchn -+WPan+7McLzGZPc4TFrmzKkMiK7GPMHjNokJRXwr7aBjVAPBjEEy7BvjPEECQFOJ -4rcKAzEewGeLREObg9Eg6nTqSMLMb52vL1V9ozR+UDrHuDilnXuyhwPX+kqEDl+E -q3V0cqHb6c8rI4TizRsCQANIyhoJ33ughNzbCIknkMPKtgvLOUARnbya/bkfRexL -icyYzXPNuqZDY8JZQHlshN8cCcZcYjGPYYscd2LKB6o= ------END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/test/rsa/engine.spec.ts b/test/rsa/engine.spec.ts new file mode 100644 index 0000000..e37cf27 --- /dev/null +++ b/test/rsa/engine.spec.ts @@ -0,0 +1,221 @@ +import { readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { beforeAll, describe, expect, it } from 'vitest'; +import { DerReader } from '../../src/asn1/index.js'; +import { BigInteger, setBigIntegerBackend } from '../../src/bigint/big-integer.js'; +import { nodeBackend } from '../../src/crypto/backend.node.js'; +import { fromUtf8 } from '../../src/crypto/bytes.js'; +import { JsEngine } from '../../src/rsa/engine.js'; +import { RSAKey } from '../../src/rsa/key.js'; +import { SCHEMES } from '../../src/schemes/index.js'; +import type { SchemeOptions } from '../../src/schemes/types.js'; + +const here = dirname(fileURLToPath(import.meta.url)); +const keysDir = resolve(here, '../../test/keys'); + +function loadPrivateKey(): RSAKey { + const buf = readFileSync(resolve(keysDir, 'private_pkcs1.der')); + const bytes = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength); + const seq = new DerReader(bytes).readSequence(); + seq.readSmallInteger(); + const n = seq.readInteger(); + const e = seq.readSmallInteger(); + const d = seq.readInteger(); + const p = seq.readInteger(); + const q = seq.readInteger(); + const dmp1 = seq.readInteger(); + const dmq1 = seq.readInteger(); + const coeff = seq.readInteger(); + const key = new RSAKey(); + key.setPrivate(n, e, d, p, q, dmp1, dmq1, coeff); + return key; +} + +function configure(key: RSAKey, encryptionScheme: 'pkcs1' | 'pkcs1_oaep' = 'pkcs1'): void { + const options: SchemeOptions = { + signingScheme: 'pkcs1', + encryptionScheme, + signingSchemeOptions: {}, + encryptionSchemeOptions: {}, + environment: 'node', + backend: nodeBackend, + }; + key.setOptions(options, SCHEMES); +} + +beforeAll(() => { + setBigIntegerBackend(nodeBackend); +}); + +describe('JsEngine encrypt → decrypt (PKCS#1 v1.5)', () => { + it('round-trips short message', () => { + const key = loadPrivateKey(); + configure(key); + const engine = new JsEngine(key); + const msg = fromUtf8('hello engine'); + const ct = engine.encrypt(msg); + expect(ct.length).toBe(key.encryptedDataLength); + const dec = engine.decrypt(ct); + expect(dec).toEqual(msg); + }); + + it('chunks a long message across multiple blocks', () => { + const key = loadPrivateKey(); + configure(key); + const engine = new JsEngine(key); + // 1024-bit key → max plain = 117 bytes for PKCS#1 v1.5. Use 250 bytes. + const msg = new Uint8Array(250).map((_, i) => (i * 3 + 1) & 0xff); + const ct = engine.encrypt(msg); + expect(ct.length % key.encryptedDataLength).toBe(0); + expect(ct.length).toBeGreaterThan(key.encryptedDataLength); // at least 2 chunks + const dec = engine.decrypt(ct); + expect(dec).toEqual(msg); + }); + + it('rejects ciphertext whose length is not a multiple of encryptedDataLength', () => { + const key = loadPrivateKey(); + configure(key); + const engine = new JsEngine(key); + expect(() => engine.decrypt(new Uint8Array(key.encryptedDataLength + 5))).toThrow( + /Incorrect data or key/, + ); + }); + + it('encryptPrivate → decryptPublic (signature-shaped path)', () => { + const key = loadPrivateKey(); + configure(key); + const engine = new JsEngine(key); + const msg = fromUtf8('signed payload'); + const ct = engine.encrypt(msg, true); + const dec = engine.decrypt(ct, true); + expect(dec).toEqual(msg); + }); +}); + +describe('JsEngine — CRT vs non-CRT $doPrivate parity', () => { + // RSAKey.$doPrivate uses Garner CRT recombination when p/q/dmp1/dmq1/coeff + // are present; otherwise it falls back to modPow(d, n). Both paths must + // produce identical outputs for the same input — a CRT bug would only + // surface as wrong ciphertext for keys with CRT components, which most + // imported keys have. + it('CRT and non-CRT decrypt produce byte-identical plaintext for a fixed message', () => { + const fullKey = loadPrivateKey(); // has full CRT components + const basicKey = new RSAKey(); + // Only n, e, d → engine takes the modPow(d, n) branch. + basicKey.setPrivate( + fullKey.n!.toBuffer() as Uint8Array, + fullKey.e, + fullKey.d!.toBuffer() as Uint8Array, + ); + configure(fullKey); + configure(basicKey); + + const fullEng = new JsEngine(fullKey); + const basicEng = new JsEngine(basicKey); + const msg = fromUtf8('crt-vs-direct parity check'); + + // Encrypt with public (no CRT involvement) → both keys give same path. + const ctFull = fullEng.encrypt(msg); + // Decrypt with each key; the CRT branch (fullKey) and the non-CRT + // branch (basicKey) must agree on the plaintext. + const ptFull = fullEng.decrypt(ctFull); + const ptBasic = basicEng.decrypt(ctFull); + expect(ptBasic).toEqual(ptFull); + expect(ptFull).toEqual(msg); + }); + + it('CRT and non-CRT produce identical raw $doPrivate result for a fixed input < n', () => { + // Direct primitive-level parity check — bypasses padding so a CRT bug + // becomes a single-block mismatch. + const fullKey = loadPrivateKey(); + const basicKey = new RSAKey(); + basicKey.setPrivate( + fullKey.n!.toBuffer() as Uint8Array, + fullKey.e, + fullKey.d!.toBuffer() as Uint8Array, + ); + configure(fullKey); + configure(basicKey); + // Choose a small value (well under n) by building a BigInteger that + // matches the active impl through the same selector the keys use. + const seed = new Uint8Array(fullKey.encryptedDataLength); + seed.fill(0x42); + seed[0] = 0x00; // ensures value < n + const x = new BigInteger(seed); + const yFull = fullKey.$doPrivate(x).toString(16); + const yBasic = basicKey.$doPrivate(x).toString(16); + expect(yBasic).toBe(yFull); + }); +}); + +describe('JsEngine — message-size boundary handling (PKCS#1 v1.5)', () => { + it('encrypts a message of exactly maxMessageLength bytes in one block', () => { + const key = loadPrivateKey(); + configure(key); + const engine = new JsEngine(key); + const max = key.maxMessageLength; + expect(max).toBe(key.encryptedDataLength - 11); // 117 for 1024-bit PKCS#1 v1.5 + const msg = new Uint8Array(max).map((_, i) => (i * 7 + 5) & 0xff); + const ct = engine.encrypt(msg); + expect(ct.length).toBe(key.encryptedDataLength); // exactly one chunk + const dec = engine.decrypt(ct); + expect(dec).toEqual(msg); + }); + + it('encrypts a message of maxMessageLength + 1 bytes in two blocks', () => { + const key = loadPrivateKey(); + configure(key); + const engine = new JsEngine(key); + const max = key.maxMessageLength; + const msg = new Uint8Array(max + 1).map((_, i) => (i * 13) & 0xff); + const ct = engine.encrypt(msg); + expect(ct.length).toBe(key.encryptedDataLength * 2); + const dec = engine.decrypt(ct); + expect(dec).toEqual(msg); + }); + + it('encrypts an empty message into a single full-size block', () => { + const key = loadPrivateKey(); + configure(key); + const engine = new JsEngine(key); + const ct = engine.encrypt(new Uint8Array(0)); + expect(ct.length).toBe(key.encryptedDataLength); + const dec = engine.decrypt(ct); + expect(dec.length).toBe(0); + }); + + it('encrypts a single-byte message', () => { + const key = loadPrivateKey(); + configure(key); + const engine = new JsEngine(key); + const ct = engine.encrypt(new Uint8Array([0xff])); + expect(ct.length).toBe(key.encryptedDataLength); + const dec = engine.decrypt(ct); + expect(Array.from(dec)).toEqual([0xff]); + }); +}); + +describe('JsEngine encrypt → decrypt (OAEP)', () => { + it('round-trips with default SHA-1', () => { + const key = loadPrivateKey(); + configure(key, 'pkcs1_oaep'); + const engine = new JsEngine(key); + const msg = fromUtf8('oaep engine'); + const ct = engine.encrypt(msg); + const dec = engine.decrypt(ct); + expect(dec).toEqual(msg); + }); + + it('chunks across multiple OAEP blocks', () => { + const key = loadPrivateKey(); + configure(key, 'pkcs1_oaep'); + const engine = new JsEngine(key); + // 1024-bit key with OAEP/SHA-1 → max plain = 86 bytes. Try 250. + const msg = new Uint8Array(250).map((_, i) => i & 0xff); + const ct = engine.encrypt(msg); + expect(ct.length % key.encryptedDataLength).toBe(0); + const dec = engine.decrypt(ct); + expect(dec).toEqual(msg); + }); +}); diff --git a/test/rsa/key-validation.spec.ts b/test/rsa/key-validation.spec.ts new file mode 100644 index 0000000..e8f5175 --- /dev/null +++ b/test/rsa/key-validation.spec.ts @@ -0,0 +1,224 @@ +import { readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { beforeAll, describe, expect, it } from 'vitest'; +import { BigInteger, setBigIntegerBackend } from '../../src/bigint/big-integer.js'; +import { nodeBackend } from '../../src/crypto/backend.node.js'; +import NodeRSA from '../../src/index.node.js'; +import { RSAKey } from '../../src/rsa/key.js'; + +const here = dirname(fileURLToPath(import.meta.url)); +const keysDir = resolve(here, '../keys'); + +function readStr(name: string): string { + return readFileSync(resolve(keysDir, name), 'utf8'); +} + +beforeAll(() => { + setBigIntegerBackend(nodeBackend); +}); + +/** Convert a BigInteger to its big-endian byte representation. */ +function toBytes(b: BigInteger): Uint8Array { + return b.toBuffer() as Uint8Array; +} + +describe('H1 — public exponent validation on import', () => { + // We reuse the valid n from a real fixture; only e varies. + const validKey = (): NodeRSA => new NodeRSA(readStr('private_pkcs1.pem')); + const validN = (): Uint8Array => toBytes(validKey().keyPair.n!); + + it('rejects e = 0', () => { + const k = new RSAKey(); + expect(() => k.setPublic(validN(), 0)).toThrow(/e must be > 1/); + }); + + it('rejects e = 1 (ciphertext == plaintext)', () => { + const k = new RSAKey(); + expect(() => k.setPublic(validN(), 1)).toThrow(/e must be > 1/); + }); + + it('rejects even e = 2 (breaks RSA invertibility)', () => { + const k = new RSAKey(); + expect(() => k.setPublic(validN(), 2)).toThrow(/e must be odd/); + }); + + it('rejects even e = 4', () => { + const k = new RSAKey(); + expect(() => k.setPublic(validN(), 4)).toThrow(/e must be odd/); + }); + + it('accepts canonical e = 65537', () => { + const k = new RSAKey(); + expect(() => k.setPublic(validN(), 65537)).not.toThrow(); + }); + + it('accepts e = 3 (uncommon but legal)', () => { + const k = new RSAKey(); + expect(() => k.setPublic(validN(), 3)).not.toThrow(); + }); +}); + +describe('H2 — RSA primitive input bounds', () => { + it('$doPublic throws when x >= n', () => { + const k = new NodeRSA(readStr('private_pkcs1.pem')); + // x = n is the boundary; RFC requires 0 <= x < n. + expect(() => k.keyPair.$doPublic(k.keyPair.n!)).toThrow(/out of range/); + }); + + it('$doPublic throws when x = n + 1', () => { + const k = new NodeRSA(readStr('private_pkcs1.pem')); + const nPlusOne = k.keyPair.n!.add(BigInteger.ONE); + expect(() => k.keyPair.$doPublic(nPlusOne)).toThrow(/out of range/); + }); + + it('$doPrivate throws when x >= n', () => { + const k = new NodeRSA(readStr('private_pkcs1.pem')); + expect(() => k.keyPair.$doPrivate(k.keyPair.n!)).toThrow(/out of range/); + }); + + it('$doPublic accepts x in [0, n)', () => { + const k = new NodeRSA(readStr('private_pkcs1.pem')); + const small = new BigInteger(Uint8Array.of(0x42)); + expect(() => k.keyPair.$doPublic(small)).not.toThrow(); + }); +}); + +describe('H3 — private key CRT consistency validation', () => { + function loadComponents(): { + n: Uint8Array; + e: number; + d: Uint8Array; + p: Uint8Array; + q: Uint8Array; + dmp1: Uint8Array; + dmq1: Uint8Array; + coeff: Uint8Array; + } { + const k = new NodeRSA(readStr('private_pkcs1.pem')); + const kp = k.keyPair; + return { + n: toBytes(kp.n!), + e: kp.e, + d: toBytes(kp.d!), + p: toBytes(kp.p!), + q: toBytes(kp.q!), + dmp1: toBytes(kp.dmp1!), + dmq1: toBytes(kp.dmq1!), + coeff: toBytes(kp.coeff!), + }; + } + + function flipLastByte(bytes: Uint8Array): Uint8Array { + const out = new Uint8Array(bytes); + out[out.length - 1] ^= 0x01; + return out; + } + + it('rejects key where n != p × q', () => { + const c = loadComponents(); + const k = new RSAKey(); + expect(() => + k.setPrivate(flipLastByte(c.n), c.e, c.d, c.p, c.q, c.dmp1, c.dmq1, c.coeff), + ).toThrow(/n ≠ p × q/); + }); + + it('rejects key where dp != d mod (p − 1)', () => { + const c = loadComponents(); + const k = new RSAKey(); + expect(() => + k.setPrivate(c.n, c.e, c.d, c.p, c.q, flipLastByte(c.dmp1), c.dmq1, c.coeff), + ).toThrow(/dp ≠ d mod/); + }); + + it('rejects key where dq != d mod (q − 1)', () => { + const c = loadComponents(); + const k = new RSAKey(); + expect(() => + k.setPrivate(c.n, c.e, c.d, c.p, c.q, c.dmp1, flipLastByte(c.dmq1), c.coeff), + ).toThrow(/dq ≠ d mod/); + }); + + it('rejects key where q × coeff ≢ 1 (mod p)', () => { + const c = loadComponents(); + const k = new RSAKey(); + expect(() => + k.setPrivate(c.n, c.e, c.d, c.p, c.q, c.dmp1, c.dmq1, flipLastByte(c.coeff)), + ).toThrow(/q × coeff ≢ 1/); + }); + + it('accepts the unmodified valid key', () => { + const c = loadComponents(); + const k = new RSAKey(); + expect(() => k.setPrivate(c.n, c.e, c.d, c.p, c.q, c.dmp1, c.dmq1, c.coeff)).not.toThrow(); + }); + + it('skips CRT validation when CRT components are absent', () => { + // Basic n, e, d key (no p, q, dp, dq, coeff). Validation should be + // skipped since we can't cross-check without the primes. + const c = loadComponents(); + const k = new RSAKey(); + expect(() => k.setPrivate(c.n, c.e, c.d)).not.toThrow(); + }); + + it('rejects a key claiming p == q (fixture n ≠ p²)', () => { + // RFC 8017 implicitly requires distinct primes (p ≠ q): if they're + // equal, n factors trivially via square-root. The current consistency + // check catches this in two ways: + // 1. Trivially: fixture n ≠ p², so the `n ≠ p × q` test fires when + // we pass q = p. + // 2. Even with a forged n = p², modInverse(p, p) = 0, so the + // coeff check (q × coeff ≢ 1 mod p) fails. + // The first form is the one a user-typed degenerate key would hit. + const c = loadComponents(); + const k = new RSAKey(); + expect(() => + k.setPrivate(c.n, c.e, c.d, c.p, c.p /* q := p */, c.dmp1, c.dmq1, c.coeff), + ).toThrow(); + }); + + it('rejects a key with p == q and matching n = p² via the coeff check', () => { + // Forge n = p² so that the n ≠ p × q check doesn't fire — and confirm + // the coeff-consistency check rejects the degenerate key. + const c = loadComponents(); + const p = new BigInteger(c.p); + const nPP = p.multiply(p); + const k = new RSAKey(); + expect(() => + k.setPrivate( + nPP.toBuffer() as Uint8Array, + c.e, + c.d, + c.p, + c.p, + c.dmp1, + c.dmq1, + c.coeff, // coeff was computed for the real (p, q ≠ p); won't match + ), + ).toThrow(); + }); +}); + +describe('H5 — minimum key size guard on generate', () => { + it('refuses B = 256 (cryptographically broken)', () => { + expect(() => new NodeRSA({ b: 256 })).toThrow(/cryptographically broken/); + }); + + it('refuses B = 128', () => { + expect(() => new NodeRSA({ b: 128 })).toThrow(/cryptographically broken/); + }); + + it('refuses B = 504 (just below the 512 threshold, multiple of 8)', () => { + expect(() => new NodeRSA({ b: 504 })).toThrow(/cryptographically broken/); + }); + + it('accepts B = 512 (legal but warned) and actually produces a usable key', () => { + // 512-bit keys emit a one-shot console.warn but are not rejected; the + // legacy test suite uses them for speed. Verify the key is actually + // produced (not just that no error was thrown). + const k = new NodeRSA({ b: 512 }); + expect(k.keyPair.n).not.toBeNull(); + expect(k.keyPair.n!.bitLength()).toBe(512); + expect(k.isPrivate()).toBe(true); + }); +}); diff --git a/test/schemes/cross-validation.node-only.spec.ts b/test/schemes/cross-validation.node-only.spec.ts new file mode 100644 index 0000000..c782ea5 --- /dev/null +++ b/test/schemes/cross-validation.node-only.spec.ts @@ -0,0 +1,241 @@ +import { + constants as cryptoConstants, + sign as nodeSign, + verify as nodeVerify, + privateDecrypt, + publicEncrypt, + randomBytes, +} from 'node:crypto'; +import { readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { beforeAll, describe, expect, it } from 'vitest'; +import { setBigIntegerBackend } from '../../src/bigint/big-integer.js'; +import { nodeBackend } from '../../src/crypto/backend.node.js'; +import NodeRSA from '../../src/index.node.js'; + +const here = dirname(fileURLToPath(import.meta.url)); +const keysDir = resolve(here, '../keys'); + +function readStr(name: string): string { + return readFileSync(resolve(keysDir, name), 'utf8'); +} + +beforeAll(() => { + setBigIntegerBackend(nodeBackend); +}); + +/** + * Cross-validation suite. node-rsa and OpenSSL (via node:crypto) must agree + * for every supported scheme × hash combination. This file is .node-only: + * the browser-emulated workspace doesn't have node:crypto. + * + * Engine routing caveat: on Node, encrypt/decrypt for OAEP and PKCS#1 v1.5 + * route through NodeNativeEngine (OpenSSL) — NOT through the JS engine + * where the C4/C5/C2 constant-time fixes live. So the OAEP and PKCS#1 + * encrypt/decrypt cases below validate INTEROP (round-trip correctness), + * not the JS-engine security paths. The dedicated JS-engine coverage lives + * in test/schemes/js-engine-security.spec.ts (which forces JsEngine via + * `environment: 'browser'` and runs in both workspaces). + * + * Sign/verify paths for both PKCS#1 v1.5 and PSS *do* run through the JS + * engine on Node — no native-engine equivalent exists — so those tests + * exercise pkcs1Scheme/pssScheme directly. + * + * Each describe block runs N random message trials; with the fixture + * 1024-bit key the suite is fast enough to keep in CI. + */ + +const ITERATIONS = 20; + +function makeNodeRsa(scheme: string, hashOpt?: string): NodeRSA { + const pem = readStr('private_pkcs1.pem'); + const opts: { signingScheme?: string; encryptionScheme?: string } = {}; + if (scheme.startsWith('pkcs1-') || scheme.startsWith('pss-')) { + opts.signingScheme = scheme; + } else { + opts.encryptionScheme = scheme; + if (hashOpt) opts.encryptionScheme = scheme; + } + return new NodeRSA(pem, opts); +} + +const NODE_HASH: Record = { + sha1: 'sha1', + sha256: 'sha256', + sha384: 'sha384', + sha512: 'sha512', +}; + +describe('PKCS#1 v1.5 sign / verify ↔ node:crypto', () => { + for (const hash of ['sha1', 'sha256', 'sha384', 'sha512'] as const) { + it(`bit-identical signatures: pkcs1-${hash} (${ITERATIONS} trials)`, () => { + const key = makeNodeRsa(`pkcs1-${hash}`); + const pem = readStr('private_pkcs1.pem'); + for (let i = 0; i < ITERATIONS; i++) { + const msg = randomBytes(50 + Math.floor(Math.random() * 200)); + // node-rsa → node:crypto + const sig = key.sign(msg) as Uint8Array; + const ok = nodeVerify(NODE_HASH[hash], msg, pem, sig); + expect(ok, `node:crypto failed to verify node-rsa signature #${i}`).toBe(true); + // PKCS#1 v1.5 is deterministic — sign in node:crypto and compare bytes. + const sigNode = nodeSign(NODE_HASH[hash], msg, pem); + expect(Buffer.from(sig).equals(sigNode), `byte-equal signature #${i}`).toBe(true); + } + }); + + it(`accepts node:crypto-produced signatures: pkcs1-${hash}`, () => { + const key = makeNodeRsa(`pkcs1-${hash}`); + const pem = readStr('private_pkcs1.pem'); + for (let i = 0; i < ITERATIONS; i++) { + const msg = randomBytes(50 + Math.floor(Math.random() * 200)); + const sig = nodeSign(NODE_HASH[hash], msg, pem); + expect(key.verify(msg, sig as unknown as Uint8Array)).toBe(true); + } + }); + } +}); + +describe('PSS sign / verify ↔ node:crypto', () => { + for (const hash of ['sha1', 'sha256', 'sha384', 'sha512'] as const) { + it(`node-rsa signs, node:crypto verifies: pss-${hash}`, () => { + // PSS uses a random salt so signatures aren't bit-identical, but + // they must verify in both directions. + const key = makeNodeRsa(`pss-${hash}`); + const pem = readStr('private_pkcs1.pem'); + for (let i = 0; i < ITERATIONS; i++) { + const msg = randomBytes(50 + Math.floor(Math.random() * 200)); + const sig = key.sign(msg) as Uint8Array; + const ok = nodeVerify( + NODE_HASH[hash], + msg, + { + key: pem, + padding: cryptoConstants.RSA_PKCS1_PSS_PADDING, + saltLength: 20, // matches DEFAULT_SALT_LENGTH in src/schemes/pss.ts + }, + sig, + ); + expect(ok, `node:crypto failed to verify PSS-${hash} signature #${i}`).toBe(true); + } + }); + + it(`node:crypto signs, node-rsa verifies: pss-${hash}`, () => { + const key = makeNodeRsa(`pss-${hash}`); + const pem = readStr('private_pkcs1.pem'); + for (let i = 0; i < ITERATIONS; i++) { + const msg = randomBytes(50 + Math.floor(Math.random() * 200)); + const sig = nodeSign(NODE_HASH[hash], msg, { + key: pem, + padding: cryptoConstants.RSA_PKCS1_PSS_PADDING, + saltLength: 20, + }); + expect(key.verify(msg, sig as unknown as Uint8Array)).toBe(true); + } + }); + } +}); + +describe('OAEP encrypt / decrypt ↔ node:crypto', () => { + // OAEP defaults to SHA-1 in src/schemes/oaep.ts; node:crypto defaults to + // SHA-1 as well for RSA_PKCS1_OAEP_PADDING — they line up. + it('node-rsa encrypts, node:crypto decrypts (default SHA-1)', () => { + const key = new NodeRSA(readStr('private_pkcs1.pem')); + const pem = readStr('private_pkcs1.pem'); + const maxMsg = key.getMaxMessageSize(); + for (let i = 0; i < ITERATIONS; i++) { + const msg = randomBytes(1 + Math.floor(Math.random() * maxMsg)); + const ct = key.encrypt(msg) as Uint8Array; + const pt = privateDecrypt( + { key: pem, padding: cryptoConstants.RSA_PKCS1_OAEP_PADDING }, + Buffer.from(ct), + ); + expect(Buffer.from(pt).equals(Buffer.from(msg)), `OAEP round-trip #${i}`).toBe(true); + } + }); + + it('node:crypto encrypts, node-rsa decrypts (default SHA-1)', () => { + const key = new NodeRSA(readStr('private_pkcs1.pem')); + const pem = readStr('private_pkcs1.pem'); + const maxMsg = key.getMaxMessageSize(); + for (let i = 0; i < ITERATIONS; i++) { + const msg = randomBytes(1 + Math.floor(Math.random() * maxMsg)); + const ct = publicEncrypt( + { key: pem, padding: cryptoConstants.RSA_PKCS1_OAEP_PADDING }, + Buffer.from(msg), + ); + const pt = key.decrypt(new Uint8Array(ct)) as Uint8Array; + expect(Buffer.from(pt).equals(Buffer.from(msg)), `OAEP reverse round-trip #${i}`).toBe(true); + } + }); +}); + +describe('OAEP cross-validation with non-default hash', () => { + // OAEP with SHA-1 is the existing happy-path. node:crypto exposes oaepHash + // to align with our encryptionSchemeOptions.hash. Skip sha512 — it doesn't + // fit on a 1024-bit key (k=128 < 2·64+2). sha384 only just fits (max 30 B). + for (const hash of ['sha256', 'sha384'] as const) { + const maxByHash: Record = { sha256: 62, sha384: 30 }; + it(`node-rsa encrypts, node:crypto decrypts: oaepHash=${hash}`, () => { + const pem = readStr('private_pkcs1.pem'); + const key = new NodeRSA(pem, { + encryptionScheme: { scheme: 'pkcs1_oaep', hash }, + }); + const limit = maxByHash[hash] as number; + for (let i = 0; i < ITERATIONS; i++) { + const msg = randomBytes(1 + Math.floor(Math.random() * limit)); + const ct = key.encrypt(msg) as Uint8Array; + const pt = privateDecrypt( + { key: pem, padding: cryptoConstants.RSA_PKCS1_OAEP_PADDING, oaepHash: hash }, + Buffer.from(ct), + ); + expect(Buffer.from(pt).equals(Buffer.from(msg)), `${hash} round-trip #${i}`).toBe(true); + } + }); + + it(`node:crypto encrypts, node-rsa decrypts: oaepHash=${hash}`, () => { + const pem = readStr('private_pkcs1.pem'); + const key = new NodeRSA(pem, { + encryptionScheme: { scheme: 'pkcs1_oaep', hash }, + }); + const limit = maxByHash[hash] as number; + for (let i = 0; i < ITERATIONS; i++) { + const msg = randomBytes(1 + Math.floor(Math.random() * limit)); + const ct = publicEncrypt( + { key: pem, padding: cryptoConstants.RSA_PKCS1_OAEP_PADDING, oaepHash: hash }, + Buffer.from(msg), + ); + const pt = key.decrypt(new Uint8Array(ct)) as Uint8Array; + expect(Buffer.from(pt).equals(Buffer.from(msg)), `${hash} reverse #${i}`).toBe(true); + } + }); + } +}); + +describe('Negative interop: tampered ciphertext / signature is rejected', () => { + it('node-rsa rejects a PKCS#1 v1.5 signature with a flipped bit', () => { + const key = makeNodeRsa('pkcs1-sha256'); + const pem = readStr('private_pkcs1.pem'); + const msg = randomBytes(64); + const sig = nodeSign('sha256', msg, pem); + const tampered = new Uint8Array(sig); + tampered[Math.floor(tampered.length / 2)] ^= 0x01; + expect(key.verify(msg, tampered)).toBe(false); + }); + + it('node-rsa rejects an OAEP ciphertext with a flipped bit', () => { + const key = new NodeRSA(readStr('private_pkcs1.pem')); + const pem = readStr('private_pkcs1.pem'); + const msg = randomBytes(32); + const ct = publicEncrypt( + { key: pem, padding: cryptoConstants.RSA_PKCS1_OAEP_PADDING }, + Buffer.from(msg), + ); + const tampered = new Uint8Array(ct); + tampered[Math.floor(tampered.length / 2)] ^= 0x01; + // The error message varies: native-engine path surfaces OpenSSL's + // "oaep decoding error", JsEngine path surfaces our "invalid padding". + // Either way, decrypt MUST throw — not return garbage plaintext. + expect(() => key.decrypt(tampered)).toThrow(); + }); +}); diff --git a/test/schemes/js-engine-security.spec.ts b/test/schemes/js-engine-security.spec.ts new file mode 100644 index 0000000..7043d5d --- /dev/null +++ b/test/schemes/js-engine-security.spec.ts @@ -0,0 +1,159 @@ +import { readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { beforeAll, describe, expect, it } from 'vitest'; +import { setBigIntegerBackend } from '../../src/bigint/big-integer.js'; +import { nodeBackend } from '../../src/crypto/backend.node.js'; +import NodeRSA from '../../src/index.node.js'; + +const here = dirname(fileURLToPath(import.meta.url)); +const keysDir = resolve(here, '../keys'); + +function readStr(name: string): string { + return readFileSync(resolve(keysDir, name), 'utf8'); +} + +beforeAll(() => { + setBigIntegerBackend(nodeBackend); +}); + +/** + * The security-fix code paths in src/schemes/oaep.ts and src/schemes/pkcs1.ts + * (C4 constant-time OAEP decode, C5 PKCS#1 v1.5 decode, C2 RSA blinding) only + * run when the JS engine handles the primitive. On Node, encryption and OAEP + * decryption are normally routed through NodeNativeEngine to OpenSSL — which + * means the cross-validation.node-only suite cannot test them. + * + * This file forces the JS engine via `environment: 'browser'` and exercises + * the fixes directly. Runs in BOTH the `node` and `browser-emulated` vitest + * workspaces because the JS engine should behave identically. + */ + +function makeJsKey(opts: Record = {}): NodeRSA { + return new NodeRSA(readStr('private_pkcs1.pem'), { + environment: 'browser', // force the pure-JS engine + ...opts, + }); +} + +describe('OAEP — JS-engine round-trip and constant-time decode (C4)', () => { + it('round-trips a short message through encPad → public → private → encUnPad', () => { + const key = makeJsKey(); + const msg = new TextEncoder().encode('hello world'); + const ct = key.encrypt(msg) as Uint8Array; + const pt = key.decrypt(ct) as Uint8Array; + expect(new TextDecoder().decode(pt)).toBe('hello world'); + }); + + it('rejects mid-buffer tampered ciphertext with the JS-engine padding error', () => { + // C4 path: oaep.encUnPad returns null on bad padding; the engine wraps + // in this generic message. We mutate a middle byte (small XOR) to avoid + // pushing the integer value past `n` and tripping the H2 bounds check + // instead — RSA primitive is a permutation, so the resulting plaintext + // is pseudo-random and almost-certainly fails OAEP padding. + const key = makeJsKey(); + const ct = key.encrypt(new TextEncoder().encode('test')) as Uint8Array; + const tampered = new Uint8Array(ct); + tampered[Math.floor(tampered.length / 2)] ^= 0x01; + expect(() => key.decrypt(tampered)).toThrow('Error during decryption'); + }); + + it('all mid-buffer mutations yield the SAME generic error (constant error path)', () => { + // C4's downstream contract: every distinct OAEP failure mode (Y, lHash, + // PS, separator) reaches the same `return null` and the engine wraps in + // an identical message. The message itself must not be an oracle. + // + // We can't easily craft ciphertexts that hit each failure mode + // *specifically* (encrypt's public API doesn't expose padding internals), + // but we can demonstrate that several independent mid-buffer mutations + // all surface byte-identical errors — that's the observable property. + const key = makeJsKey(); + const valid = key.encrypt(new TextEncoder().encode('test')) as Uint8Array; + // Pick positions across the buffer: lHash region, mid-DB, near tail. + // Skip byte 0 (would push ct >= n on some keys and trip H2 instead). + const positions = [20, 40, 60, 80, valid.length - 5]; + const errors = positions.map((pos) => { + const t = new Uint8Array(valid); + t[pos] ^= 0x01; + try { + key.decrypt(t); + return null; + } catch (e) { + return (e as Error).message; + } + }); + expect(errors[0]).toBeTruthy(); + for (let i = 1; i < errors.length; i++) { + expect(errors[i], `position ${positions[i]} differs from position ${positions[0]}`).toBe( + errors[0], + ); + } + }); +}); + +describe('PKCS#1 v1.5 — JS-engine round-trip and constant-time decode (C5)', () => { + it('round-trips through encPad → public → private → encUnPad', () => { + const key = makeJsKey({ encryptionScheme: 'pkcs1' }); + const msg = new TextEncoder().encode('hello'); + const ct = key.encrypt(msg) as Uint8Array; + const pt = key.decrypt(ct) as Uint8Array; + expect(new TextDecoder().decode(pt)).toBe('hello'); + }); + + it('rejects mid-buffer tampered ciphertext with the JS-engine padding error', () => { + // Same rationale as the OAEP test: avoid byte 0 to keep the value < n. + const key = makeJsKey({ encryptionScheme: 'pkcs1' }); + const ct = key.encrypt(new TextEncoder().encode('test')) as Uint8Array; + const tampered = new Uint8Array(ct); + tampered[Math.floor(tampered.length / 2)] ^= 0x01; + expect(() => key.decrypt(tampered)).toThrow('Error during decryption'); + }); + + it('all mid-buffer mutations yield the SAME generic error (constant error path)', () => { + const key = makeJsKey({ encryptionScheme: 'pkcs1' }); + const valid = key.encrypt(new TextEncoder().encode('test')) as Uint8Array; + const positions = [20, 40, 60, 80, valid.length - 5]; + const errors = positions.map((pos) => { + const t = new Uint8Array(valid); + t[pos] ^= 0x01; + try { + key.decrypt(t); + return null; + } catch (e) { + return (e as Error).message; + } + }); + expect(errors[0]).toBeTruthy(); + for (let i = 1; i < errors.length; i++) { + expect(errors[i], `position ${positions[i]} differs from position ${positions[0]}`).toBe( + errors[0], + ); + } + }); +}); + +describe('RSA blinding — JS-engine private operation does not crash (C2)', () => { + it('private operation completes for arbitrary input in [0, n)', () => { + // Exercise $doPrivate through encryptPrivate (which calls $doPrivate + // through the JS engine). Without the blinding path working, this + // would crash or produce wrong output. + const key = makeJsKey({ encryptionScheme: 'pkcs1' }); + const msg = new TextEncoder().encode('blinded'); + // encryptPrivate uses $doPrivate; decryptPublic verifies the result. + const ct = key.encryptPrivate(msg) as Uint8Array; + const pt = key.decryptPublic(ct) as Uint8Array; + expect(new TextDecoder().decode(pt)).toBe('blinded'); + }); + + it('repeated private ops on the same input yield the same plaintext', () => { + // Blinding uses fresh r each call, but the un-blinded result must be + // deterministic for a fixed message. + const key = makeJsKey({ encryptionScheme: 'pkcs1' }); + const msg = new TextEncoder().encode('repeat'); + const ct = key.encryptPrivate(msg) as Uint8Array; + for (let i = 0; i < 5; i++) { + const pt = key.decryptPublic(ct) as Uint8Array; + expect(new TextDecoder().decode(pt)).toBe('repeat'); + } + }); +}); diff --git a/test/schemes/pkcs1-encunpad.spec.ts b/test/schemes/pkcs1-encunpad.spec.ts new file mode 100644 index 0000000..cc65334 --- /dev/null +++ b/test/schemes/pkcs1-encunpad.spec.ts @@ -0,0 +1,247 @@ +import { readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { beforeAll, describe, expect, it } from 'vitest'; +import { DerReader } from '../../src/asn1/index.js'; +import { setBigIntegerBackend } from '../../src/bigint/big-integer.js'; +import { nodeBackend } from '../../src/crypto/backend.node.js'; +import { RSAKey } from '../../src/rsa/key.js'; +import { SCHEMES } from '../../src/schemes/index.js'; +import type { EncryptionScheme, SchemeOptions } from '../../src/schemes/types.js'; + +const here = dirname(fileURLToPath(import.meta.url)); +const keysDir = resolve(here, '../../test/keys'); + +/** + * EM-shape negative tests for PKCS#1 v1.5 decode (src/schemes/pkcs1.ts). + * Goes one layer below cross-validation/js-engine-security: directly + * crafts the *decoded* EM and calls `encryptionScheme.encUnPad`. That + * isolates the constant-time `bad` flag from the surrounding RSA + * primitive (no need to encrypt → tamper → decrypt; we synthesise the + * post-RSA byte string the decoder sees). + * + * Lives in both vitest workspaces — the encUnPad code is pure-JS and + * identical across `node` and `browser-emulated`. + */ + +function loadDer(name: string): Uint8Array { + const buf = readFileSync(resolve(keysDir, name)); + return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength); +} + +function makeKey(): RSAKey { + const seq = new DerReader(loadDer('private_pkcs1.der')).readSequence(); + seq.readSmallInteger(); + const n = seq.readInteger(); + const e = seq.readSmallInteger(); + const d = seq.readInteger(); + const p = seq.readInteger(); + const q = seq.readInteger(); + const dmp1 = seq.readInteger(); + const dmq1 = seq.readInteger(); + const coeff = seq.readInteger(); + const key = new RSAKey(); + key.setPrivate(n, e, d, p, q, dmp1, dmq1, coeff); + const options: SchemeOptions = { + signingScheme: 'pkcs1', + encryptionScheme: 'pkcs1', + signingSchemeOptions: {}, + encryptionSchemeOptions: {}, + environment: 'node', + backend: nodeBackend, + }; + key.setOptions(options, SCHEMES); + return key; +} + +/** + * Build a well-formed PKCS#1 v1.5 type-2 EM: `00 02 PS 00 MSG`, with + * `PS` of arbitrary non-zero bytes long enough to satisfy the ≥ 8 + * minimum unless otherwise specified. + */ +function buildEm( + emLen: number, + msgLen: number, + opts: { type?: number; psLen?: number; corrupt?: (em: Uint8Array) => void } = {}, +): Uint8Array { + const em = new Uint8Array(emLen); + em[0] = 0x00; + em[1] = opts.type ?? 0x02; + const psLen = opts.psLen ?? emLen - msgLen - 3; + // Fill PS with non-zero bytes (0x42), then the separator 0x00, then message. + for (let i = 0; i < psLen; i++) em[2 + i] = 0x42; + em[2 + psLen] = 0x00; // separator + for (let i = 0; i < msgLen; i++) em[3 + psLen + i] = 0xa5; + opts.corrupt?.(em); + return em; +} + +beforeAll(() => { + setBigIntegerBackend(nodeBackend); +}); + +describe('PKCS#1 v1.5 encUnPad — direct EM negative tests (type=2)', () => { + it('accepts a canonical EM', () => { + const key = makeKey(); + const enc = key.encryptionScheme as EncryptionScheme; + const em = buildEm(key.encryptedDataLength, 10); + const out = enc.encUnPad(em); + expect(out).not.toBeNull(); + expect((out as Uint8Array).length).toBe(10); + }); + + it('rejects EM with leading byte ≠ 0x00', () => { + const key = makeKey(); + const enc = key.encryptionScheme as EncryptionScheme; + const em = buildEm(key.encryptedDataLength, 10, { + corrupt: (b) => { + b[0] = 0x01; + }, + }); + expect(enc.encUnPad(em)).toBeNull(); + }); + + it('rejects EM with type byte ≠ 0x02 (e.g., 0x01, 0x03, 0xff)', () => { + const key = makeKey(); + const enc = key.encryptionScheme as EncryptionScheme; + for (const t of [0x00, 0x01, 0x03, 0xff]) { + const em = buildEm(key.encryptedDataLength, 10, { + corrupt: (b) => { + b[1] = t; + }, + }); + expect(enc.encUnPad(em), `type=0x${t.toString(16)}`).toBeNull(); + } + }); + + it('rejects EM with no 0x00 separator anywhere after byte 2', () => { + const key = makeKey(); + const enc = key.encryptionScheme as EncryptionScheme; + const em = new Uint8Array(key.encryptedDataLength); + em[0] = 0x00; + em[1] = 0x02; + em.fill(0x42, 2); // No 0x00 → no separator found + expect(enc.encUnPad(em)).toBeNull(); + }); + + it('rejects EM with PS shorter than 8 bytes (RFC §7.2.1)', () => { + const key = makeKey(); + const enc = key.encryptionScheme as EncryptionScheme; + for (const psLen of [0, 1, 7]) { + const em = buildEm(key.encryptedDataLength, key.encryptedDataLength - psLen - 3, { + psLen, + }); + expect(enc.encUnPad(em), `PS=${psLen}`).toBeNull(); + } + }); + + it('accepts EM where PS is exactly 8 bytes (boundary)', () => { + const key = makeKey(); + const enc = key.encryptionScheme as EncryptionScheme; + const em = buildEm(key.encryptedDataLength, key.encryptedDataLength - 11, { psLen: 8 }); + const out = enc.encUnPad(em); + expect(out).not.toBeNull(); + expect((out as Uint8Array).length).toBe(key.encryptedDataLength - 11); + }); + + it('rejects EM whose buffer is shorter than 11 bytes (geometry)', () => { + const key = makeKey(); + const enc = key.encryptionScheme as EncryptionScheme; + for (const len of [0, 1, 10]) { + const em = new Uint8Array(len); + expect(enc.encUnPad(em), `len=${len}`).toBeNull(); + } + }); + + it('accepts EM with empty message (msg length = 0)', () => { + const key = makeKey(); + const enc = key.encryptionScheme as EncryptionScheme; + // PS = emLen - 3 (separator + leading 2 bytes; msg = 0) + const em = buildEm(key.encryptedDataLength, 0); + const out = enc.encUnPad(em); + expect(out).not.toBeNull(); + expect((out as Uint8Array).length).toBe(0); + }); +}); + +describe('PKCS#1 v1.5 encUnPad — direct EM negative tests (type=1, signature path)', () => { + it('accepts a canonical type-1 EM', () => { + const key = makeKey(); + const enc = key.encryptionScheme as EncryptionScheme; + const emLen = key.encryptedDataLength; + // type-1 EM: 0x00 0x01 0xff…0xff 0x00 MSG + const em = new Uint8Array(emLen); + em[0] = 0x00; + em[1] = 0x01; + const psLen = emLen - 10 - 3; // 10-byte message + for (let i = 0; i < psLen; i++) em[2 + i] = 0xff; + em[2 + psLen] = 0x00; + for (let i = 0; i < 10; i++) em[3 + psLen + i] = 0x5a; + const out = enc.encUnPad(em, { type: 1 }); + expect(out).not.toBeNull(); + expect((out as Uint8Array).length).toBe(10); + }); + + it('rejects type-1 EM where a PS byte is not 0xff and not the separator', () => { + const key = makeKey(); + const enc = key.encryptionScheme as EncryptionScheme; + const emLen = key.encryptedDataLength; + const em = new Uint8Array(emLen); + em[0] = 0x00; + em[1] = 0x01; + const psLen = emLen - 10 - 3; + for (let i = 0; i < psLen; i++) em[2 + i] = 0xff; + em[2 + 5] = 0xfe; // PS byte ≠ 0xff and ≠ 0x00 + em[2 + psLen] = 0x00; + for (let i = 0; i < 10; i++) em[3 + psLen + i] = 0x5a; + expect(enc.encUnPad(em, { type: 1 })).toBeNull(); + }); + + it('rejects type-1 EM with type byte ≠ 0x01', () => { + const key = makeKey(); + const enc = key.encryptionScheme as EncryptionScheme; + const emLen = key.encryptedDataLength; + const em = new Uint8Array(emLen); + em[0] = 0x00; + em[1] = 0x02; // wrong type for sig path + const psLen = emLen - 10 - 3; + for (let i = 0; i < psLen; i++) em[2 + i] = 0xff; + em[2 + psLen] = 0x00; + expect(enc.encUnPad(em, { type: 1 })).toBeNull(); + }); +}); + +describe('PKCS#1 v1.5 encUnPad — RSA_NO_PADDING raw mode', () => { + it('strips leading zero pad', () => { + const key = new RSAKey(); + const seq = new DerReader(loadDer('private_pkcs1.der')).readSequence(); + seq.readSmallInteger(); + const n = seq.readInteger(); + const e = seq.readSmallInteger(); + const d = seq.readInteger(); + const p = seq.readInteger(); + const q = seq.readInteger(); + const dmp1 = seq.readInteger(); + const dmq1 = seq.readInteger(); + const coeff = seq.readInteger(); + key.setPrivate(n, e, d, p, q, dmp1, dmq1, coeff); + const options: SchemeOptions = { + signingScheme: 'pkcs1', + encryptionScheme: 'pkcs1', + signingSchemeOptions: {}, + // RSA_NO_PADDING = 3 (matches src/schemes/pkcs1.ts). + encryptionSchemeOptions: { padding: 3 }, + environment: 'node', + backend: nodeBackend, + }; + key.setOptions(options, SCHEMES); + const enc = key.encryptionScheme as EncryptionScheme; + const em = new Uint8Array(key.encryptedDataLength); + // Zero-pad except for a small payload at the tail. + em[em.length - 3] = 0x00; // last zero is the boundary + em[em.length - 2] = 0x11; + em[em.length - 1] = 0x22; + const out = enc.encUnPad(em) as Uint8Array; + expect(Array.from(out)).toEqual([0x11, 0x22]); + }); +}); diff --git a/test/schemes/roundtrip.spec.ts b/test/schemes/roundtrip.spec.ts new file mode 100644 index 0000000..0c617c5 --- /dev/null +++ b/test/schemes/roundtrip.spec.ts @@ -0,0 +1,330 @@ +import { readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { beforeAll, describe, expect, it } from 'vitest'; +import { DerReader } from '../../src/asn1/index.js'; +import { BigInteger, setBigIntegerBackend } from '../../src/bigint/big-integer.js'; +import { nodeBackend } from '../../src/crypto/backend.node.js'; +import { fromUtf8 } from '../../src/crypto/bytes.js'; +import { RSAKey } from '../../src/rsa/key.js'; +import { oaepScheme, pkcs1Scheme, pssScheme, SCHEMES } from '../../src/schemes/index.js'; +import type { SchemeOptions } from '../../src/schemes/types.js'; + +const here = dirname(fileURLToPath(import.meta.url)); +const keysDir = resolve(here, '../../test/keys'); + +function loadDer(name: string): Uint8Array { + const buf = readFileSync(resolve(keysDir, name)); + return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength); +} + +function loadPrivateKey(): RSAKey { + const seq = new DerReader(loadDer('private_pkcs1.der')).readSequence(); + seq.readSmallInteger(); // version + const n = seq.readInteger(); + const e = seq.readSmallInteger(); + const d = seq.readInteger(); + const p = seq.readInteger(); + const q = seq.readInteger(); + const dmp1 = seq.readInteger(); + const dmq1 = seq.readInteger(); + const coeff = seq.readInteger(); + const key = new RSAKey(); + key.setPrivate(n, e, d, p, q, dmp1, dmq1, coeff); + return key; +} + +function makeOptions( + encryptionScheme: 'pkcs1' | 'pkcs1_oaep', + signingScheme: 'pkcs1' | 'pss' = 'pkcs1', +): SchemeOptions { + return { + signingScheme, + encryptionScheme, + signingSchemeOptions: {}, + encryptionSchemeOptions: {}, + environment: 'node', + backend: nodeBackend, + }; +} + +beforeAll(() => { + setBigIntegerBackend(nodeBackend); +}); + +describe('RSAKey loaded from PKCS#1 private fixture', () => { + it('has the expected metrics', () => { + const key = loadPrivateKey(); + expect(key.isPrivate()).toBe(true); + expect(key.isPublic()).toBe(true); + expect(key.isPublic(true)).toBe(false); + expect(key.keySize).toBe(1024); + expect(key.encryptedDataLength).toBe(128); + expect(key.e).toBe(65537); + }); +}); + +describe('PKCS#1 v1.5 encrypt → decrypt round-trip', () => { + const messages = [ + 'hello world', + '', // empty + 'a', + 'τέστ unicode 🚀', + 'A'.repeat(50), + ]; + + for (const text of messages) { + it(`message "${text.slice(0, 20)}${text.length > 20 ? '…' : ''}"`, () => { + const key = loadPrivateKey(); + const options = makeOptions('pkcs1'); + key.setOptions(options, SCHEMES); + + const msg = fromUtf8(text); + const padded = key.encryptionScheme.encPad(msg); + expect(padded.length).toBe(key.encryptedDataLength); + const ct = key.$doPublic(new BigInteger(padded)).toBuffer(key.encryptedDataLength); + expect(ct).not.toBeNull(); + expect((ct as Uint8Array).length).toBe(key.encryptedDataLength); + + const dec = key + .$doPrivate(new BigInteger(ct as Uint8Array)) + .toBuffer(key.encryptedDataLength); + expect(dec).not.toBeNull(); + const unpadded = key.encryptionScheme.encUnPad(dec as Uint8Array); + expect(unpadded).not.toBeNull(); + expect(unpadded as Uint8Array).toEqual(msg); + }); + } +}); + +describe('OAEP encrypt → decrypt round-trip', () => { + it('default SHA-1 OAEP', () => { + const key = loadPrivateKey(); + const options = makeOptions('pkcs1_oaep'); + key.setOptions(options, SCHEMES); + + const msg = fromUtf8('hello OAEP'); + const padded = key.encryptionScheme.encPad(msg); + expect(padded.length).toBe(key.encryptedDataLength); + const ct = key.$doPublic(new BigInteger(padded)).toBuffer(key.encryptedDataLength); + const dec = key.$doPrivate(new BigInteger(ct as Uint8Array)).toBuffer(key.encryptedDataLength); + const unpadded = key.encryptionScheme.encUnPad(dec as Uint8Array); + expect(unpadded as Uint8Array).toEqual(msg); + }); + + // 1024-bit key supports OAEP up to: k - 2*hLen - 2. + // sha1 → 86 bytes ✓ + // sha256 → 62 bytes ✓ + // sha384 → 30 bytes ✓ + // sha512 → −2 bytes ✗ (key too small; verified via expect-throw below) + it.each(['sha256', 'sha384'] as const)('OAEP hash=%s', (h) => { + const key = loadPrivateKey(); + const options: SchemeOptions = { + ...makeOptions('pkcs1_oaep'), + encryptionSchemeOptions: { hash: h }, + }; + key.setOptions(options, SCHEMES); + const msg = fromUtf8(`OAEP-${h}`); + const padded = key.encryptionScheme.encPad(msg); + expect(padded.length).toBe(key.encryptedDataLength); + const ct = key.$doPublic(new BigInteger(padded)).toBuffer(key.encryptedDataLength); + const dec = key.$doPrivate(new BigInteger(ct as Uint8Array)).toBuffer(key.encryptedDataLength); + const unpadded = key.encryptionScheme.encUnPad(dec as Uint8Array); + expect(unpadded as Uint8Array).toEqual(msg); + }); + + it('OAEP/sha512 refuses to pad on 1024-bit key (k=128 < 2·64+2)', () => { + // RFC 8017 §7.1.1: encoding requires k ≥ 2hLen + 2. With sha512/hLen=64 + // and a 1024-bit key (k=128) the minimum is 130, so any encPad call — + // even on empty input — must throw. Exercises the upper geometry guard. + const key = loadPrivateKey(); + const options: SchemeOptions = { + ...makeOptions('pkcs1_oaep'), + encryptionSchemeOptions: { hash: 'sha512' }, + }; + key.setOptions(options, SCHEMES); + expect(() => key.encryptionScheme.encPad(fromUtf8(''))).toThrow(); + }); + + it('OAEP with custom label', () => { + const key = loadPrivateKey(); + const label = fromUtf8('my-label'); + const options: SchemeOptions = { + ...makeOptions('pkcs1_oaep'), + encryptionSchemeOptions: { label }, + }; + key.setOptions(options, SCHEMES); + const msg = fromUtf8('labeled'); + const padded = key.encryptionScheme.encPad(msg); + const ct = key.$doPublic(new BigInteger(padded)).toBuffer(key.encryptedDataLength); + const dec = key.$doPrivate(new BigInteger(ct as Uint8Array)).toBuffer(key.encryptedDataLength); + const unpadded = key.encryptionScheme.encUnPad(dec as Uint8Array); + expect(unpadded as Uint8Array).toEqual(msg); + }); +}); + +describe('PKCS#1 v1.5 sign → verify round-trip', () => { + it.each(['sha1', 'sha256', 'sha384', 'sha512'] as const)('hash=%s', (h) => { + const key = loadPrivateKey(); + const options: SchemeOptions = { + ...makeOptions('pkcs1', 'pkcs1'), + signingSchemeOptions: { hash: h }, + }; + key.setOptions(options, SCHEMES); + + const msg = fromUtf8('hello pkcs1 signing'); + const sig = key.signingScheme.sign(msg); + expect(sig.length).toBe(key.encryptedDataLength); + expect(key.signingScheme.verify(msg, sig)).toBe(true); + expect(key.signingScheme.verify(fromUtf8('tampered'), sig)).toBe(false); + }); +}); + +describe('PSS sign → verify round-trip', () => { + it.each(['sha1', 'sha256', 'sha512'] as const)('hash=%s', (h) => { + const key = loadPrivateKey(); + const options: SchemeOptions = { + ...makeOptions('pkcs1', 'pss'), + signingSchemeOptions: { hash: h }, + }; + key.setOptions(options, SCHEMES); + + const msg = fromUtf8('hello pss signing'); + const sig = key.signingScheme.sign(msg); + expect(sig.length).toBe(key.encryptedDataLength); + expect(key.signingScheme.verify(msg, sig)).toBe(true); + expect(key.signingScheme.verify(fromUtf8('tampered'), sig)).toBe(false); + }); + + it('PSS with saltLength=0 is deterministic-ish', () => { + const key = loadPrivateKey(); + const options: SchemeOptions = { + ...makeOptions('pkcs1', 'pss'), + signingSchemeOptions: { hash: 'sha256', saltLength: 0 }, + }; + key.setOptions(options, SCHEMES); + const msg = fromUtf8('zero-salt'); + const sig1 = key.signingScheme.sign(msg); + const sig2 = key.signingScheme.sign(msg); + expect(sig1).toEqual(sig2); + expect(key.signingScheme.verify(msg, sig1)).toBe(true); + }); + + it.each( + // RFC 3447 §9.1: saltLength ∈ {0, hLen}. hLen depends on hash, so verify + // each explicitly — each branch traverses a different EM geometry (PS + // padding length, separator position) in emsaPssEncode / emsaPssVerify. + // sha512+saltLen=64 won't fit on a 1024-bit key (emLen=128 < 64+64+2); + // covered by the explicit-geometry-failure test below. + [ + ['sha1', 20], + ['sha256', 32], + ['sha384', 48], + ] as const, + )('PSS hash=%s saltLength=hLen=%i', (h, sLen) => { + const key = loadPrivateKey(); + const options: SchemeOptions = { + ...makeOptions('pkcs1', 'pss'), + signingSchemeOptions: { hash: h, saltLength: sLen }, + }; + key.setOptions(options, SCHEMES); + const msg = fromUtf8(`pss-${h}-saltLen-${sLen}`); + const sig = key.signingScheme.sign(msg); + expect(sig.length).toBe(key.encryptedDataLength); + expect(key.signingScheme.verify(msg, sig)).toBe(true); + }); + + it('rejects a PSS signature with a single-byte flip', () => { + // Hardens against any future refactor that swaps emsaPssVerify's + // accumulated `bad` flag for an early return — verify must still come + // back false for tampered signatures. + const key = loadPrivateKey(); + const options: SchemeOptions = { + ...makeOptions('pkcs1', 'pss'), + signingSchemeOptions: { hash: 'sha256' }, + }; + key.setOptions(options, SCHEMES); + const msg = fromUtf8('pss-tamper'); + const sig = key.signingScheme.sign(msg); + // Walk a few representative positions: start (masked-bits region), + // middle (PS / separator), tail (salt and trailer). + for (const pos of [0, sig.length >> 2, sig.length >> 1, sig.length - 2, sig.length - 1]) { + const tampered = new Uint8Array(sig); + tampered[pos] = (tampered[pos] as number) ^ 0x01; + expect(key.signingScheme.verify(msg, tampered), `flip at ${pos} should be rejected`).toBe( + false, + ); + } + }); + + it('rejects a PSS signature whose message was modified', () => { + const key = loadPrivateKey(); + const options: SchemeOptions = { + ...makeOptions('pkcs1', 'pss'), + signingSchemeOptions: { hash: 'sha256' }, + }; + key.setOptions(options, SCHEMES); + const msg = fromUtf8('original'); + const sig = key.signingScheme.sign(msg); + expect(key.signingScheme.verify(fromUtf8('original2'), sig)).toBe(false); + expect(key.signingScheme.verify(fromUtf8(''), sig)).toBe(false); + }); + + it('sha512+saltLen=64 fails to encode on 1024-bit key (geometry guard)', () => { + const key = loadPrivateKey(); + const options: SchemeOptions = { + ...makeOptions('pkcs1', 'pss'), + signingSchemeOptions: { hash: 'sha512', saltLength: 64 }, + }; + key.setOptions(options, SCHEMES); + expect(() => key.signingScheme.sign(fromUtf8('x'))).toThrow(); + }); + + it('rejects a truncated PSS signature without throwing', () => { + // RFC 8017 §8.1.2 step 2.b: representation-out-of-range yields + // "invalid signature" (not an exception). emsaPssVerify additionally + // checks EM.length === emLen and returns false. + const key = loadPrivateKey(); + const options: SchemeOptions = { + ...makeOptions('pkcs1', 'pss'), + signingSchemeOptions: { hash: 'sha256' }, + }; + key.setOptions(options, SCHEMES); + const msg = fromUtf8('truncate-me'); + const sig = key.signingScheme.sign(msg); + expect(() => key.signingScheme.verify(msg, sig.subarray(0, sig.length - 1))).not.toThrow(); + expect(key.signingScheme.verify(msg, sig.subarray(0, sig.length - 1))).toBe(false); + }); +}); + +describe('scheme registry', () => { + it('isEncryption', () => { + expect(SCHEMES.pkcs1?.isEncryption).toBe(true); + expect(SCHEMES.pkcs1_oaep?.isEncryption).toBe(true); + expect(SCHEMES.pss?.isEncryption).toBe(false); + }); + + it('isSignature', () => { + expect(SCHEMES.pkcs1?.isSignature).toBe(true); + expect(SCHEMES.pkcs1_oaep?.isSignature).toBe(false); + expect(SCHEMES.pss?.isSignature).toBe(true); + }); + + it('exports the scheme implementations', () => { + expect(SCHEMES.pkcs1).toBe(pkcs1Scheme); + expect(SCHEMES.pkcs1_oaep).toBe(oaepScheme); + expect(SCHEMES.pss).toBe(pssScheme); + }); +}); + +describe('RSAKey errors', () => { + it('setPrivate rejects empty modulus', () => { + const key = new RSAKey(); + expect(() => key.setPrivate(new Uint8Array(0), 65537, new Uint8Array([1]))).toThrow(); + }); + + it('setPublic rejects empty modulus', () => { + const key = new RSAKey(); + expect(() => key.setPublic(new Uint8Array(0), 65537)).toThrow(); + }); +}); diff --git a/test/tests.js b/test/tests.js deleted file mode 100644 index 6966ed9..0000000 --- a/test/tests.js +++ /dev/null @@ -1,938 +0,0 @@ -var fs = require('fs'); -var assert = require('chai').assert; -var _ = require('lodash'); -var NodeRSA = require('../src/NodeRSA'); -var OAEP = require('../src/schemes/oaep'); -var constants = require('constants'); - -describe('NodeRSA', function () { - var keySizes = [ - {b: 512, e: 3}, - {b: 512, e: 5}, - {b: 512, e: 257}, - {b: 512, e: 65537}, - {b: 768}, // 'e' should be 65537 - {b: 1024}, // 'e' should be 65537 - {b: 2048} // 'e' should be 65537 - ]; - - var environments = ['browser', 'node']; - var encryptSchemes = [ - 'pkcs1', - 'pkcs1_oaep', - { - scheme:'pkcs1', - padding: constants.RSA_NO_PADDING, - toString: function() { - return 'pkcs1-nopadding'; - } - } - ]; - var signingSchemes = ['pkcs1', 'pss']; - var signHashAlgorithms = { - 'node': ['MD4', 'MD5', 'RIPEMD160', 'SHA1', 'SHA224', 'SHA256', 'SHA384', 'SHA512'], - 'browser': ['MD5', 'RIPEMD160', 'SHA1', 'SHA256', 'SHA512'] - }; - - var dataBundle = { - 'string': { - data: 'ascii + 12345678', - encoding: 'utf8' - }, - 'unicode string': { - data: 'ascii + юникод スラ ⑨', - encoding: 'utf8' - }, - 'empty string': { - data: '', - encoding: ['utf8', 'ascii', 'hex', 'base64'] - }, - 'long string': { - data: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', - encoding: ['utf8', 'ascii'] - }, - 'buffer': { - data: Buffer.from('ascii + юникод スラ ⑨'), - encoding: 'buffer' - }, - 'json object': { - data: {str: 'string', arr: ['a', 'r', 'r', 'a', 'y', true, '⑨'], int: 42, nested: {key: {key: 1}}}, - encoding: 'json' - }, - 'json array': { - data: [1, 2, 3, 4, 5, 6, 7, 8, 9, [10, 11, 12, [13], 14, 15, [16, 17, [18]]]], - encoding: 'json' - } - }; - - var privateKeyPKCS1 = '-----BEGIN RSA PRIVATE KEY-----\n' + - 'MIIFwgIBAAKCAUEAsE1edyfToZRv6cFOkB0tAJ5qJor4YF5CccJAL0fS/o1Yk10V\n' + - 'SXH4Xx4peSJgYQKkO0HqO1hAz6k9dFQB4U1CnWtRjtNEcIfycqrZrhu6you5syb6\n' + - 'ScV3Zu/9bm7/DyaLlx/gJhUPR1OxOzaqsEvlu7hbDhNLIYo1zKFb/aUBbD6+UcaG\n' + - 'xH2BfFNdzVAtVSVpc/s2Y3sboMN7rByUj7937iQlaMINvVjyasynYuzHNw6ZRP9J\n' + - 'P9fwxrCyaxnTPWxVl0qvVaQO2+TtFMtDXH2OVZtWWeLHAL8cildw0G+u2qVqTqIG\n' + - 'EwNyJlsAHykaPFAMW0xLueumrSlB+JUJPrRvvw4nBCd4GOrNSlPCE/xlk1Cb8JaI\n' + - 'CTLvDUcYc3ZqL3jqAueBhkpw2uCz8xVJeOA1KY4kQIIx8JEBsAYzgyP2iy0CAwEA\n' + - 'AQKCAUAjBcudShkdgRpWSmNr94/IDrAxpeu/YRo79QXBHriIftW4uIYRCAX6B0jf\n' + - '2ndg7iBn8Skxzs9ZMVqW8FVLR4jTMs2J3Og8npUIOG5zyuhpciZas4SHASY+GbCz\n' + - 'rnMWtGaIh/mENyzI05RimfKAgSNLDk1wV17Wc9lKJEfc9Fl7Al/WaOS+xdviMcFx\n' + - 'ltrajksLkjz0uDD917eKskbE45lULfGqeI0kYDadWp88pw6ikXJln2p3Y1PNQF3e\n' + - 'y2cN+Snzd0jx/c5fD9B1zxKYv5bUo+UnTzBxV81e9xCJfkdXv+6D5qDn1gGLdZZa\n' + - '5FxtZbRgVh/ZlqP9xYr72as/WFmIA20wRgHPgWvLyHsh0XThqZf2/O3R8KmFv8aT\n' + - '+kmc5is6sVItIIi7ltorVapTkJai3zz/VSMBBaL+ytFN9jVl4QKBoQDfL8TMeZXu\n' + - 'gBTN7yq6zZWN8+60MUaxz0/lKdzmo35z32rpVKdsYd922pmcsNYaoj/H9L3j/NP4\n' + - '9z+SHfYpWvTa7AvJfNlXYc3BRXIarpfnXsm65IzKzHaF9i2xdXxkfTEYIvOQDMLF\n' + - 'SiiObWJMV+QqUxb3luu3/CR3IcbgeTOpdiC/T/Zl/YYl17JqZTHmLFZPq7xewttg\n' + - 'zQorDRWIFDtlAoGhAMo4+uM9f4BpOHSmayhLhHArIGs4386BkXSeOLeQitaQJ/2c\n' + - 'zb459O87XoCAonZbq+dI7XRnBU3toQvEsZgrtGkOFXCZJMWAQxD5BQ5vEYT6c86h\n' + - 'uGpX6h3ODlJ6UGi+5CWyMQ1cFlBkfffFAarjSYTVlyj736sOeDuJWX133z5VQBQ8\n' + - '1xSH23kNF95vxB4I1fXG8WL11YZU7VEwSLC4aCkCgaAKRj+wDhTZ4umSRWVZLiep\n' + - 'XkZp4y7W9q095nx13abvnKRmU3BVq/fGl++kZ/ujRD7dbKXlPflgJ7m0d06ivr4w\n' + - '6dbtEqNKw4TeVd0X31u82f89bFIS7/Cw4BFgbwEn+x9sdgdyZTP+MxjE3cI9s3oc\n' + - 'fLC8+ySk1qWzGkn2gX3gWkDNrdexAEfRrClZfokaiIX8qvJEBoJk5WuHadXI6u2F\n' + - 'AoGgByidOQ4kRVd0OCzr/jEuLwpXy3Pn+Fd93rL7LwRe5dmUkNXMMr+6e/2OCt6C\n' + - '4c28+CMMxOIgvfF7kf8Uil6BtHZbK/E/6/3uYdtu4mPsKtjy4I25CYqzLvrsZt8N\n' + - 'maeoS+1S7zYjVBU6oFrJBFOndpxZDYpdEKEigHkMQfTMYliCPDUrJ/7nNhHQln8+\n' + - 'YhHOATVZtjcdp/O5svYSnK7qgQKBoDd3lFWrPatgxpF1JXMEFFbaIRdNxHkKA4YY\n' + - 'gMTM4MPgViunYX/yJ7SaX8jWnC231A9uVn4+kb+DvKjc+ZuTQvnIUK2u6LvIinVF\n' + - 'snDEA+BbXwehAtwdHDMDtqYFdx4hvCWQwBNn4p3J0OO2tbYVMtvM5aOEfRSYagfm\n' + - 'RywhDUAjW8U0RBnzlmXhQQ6B9bjqooS2MsRrJrS5CU682fb3hBo=\n' + - '-----END RSA PRIVATE KEY-----'; - - var privateKeyComponents = { - n: 'ALBNXncn06GUb+nBTpAdLQCeaiaK+GBeQnHCQC9H0v6NWJNdFUlx+F8eKXkiYGECpDtB6jtYQM+pPXRUAeFNQp1rUY7TRHCH8nKq2a4busqLubMm+knFd2bv/W5u/w8mi5cf4CYVD0dTsTs2qrBL5bu4Ww4TSyGKNcyhW/2lAWw+vlHGhsR9gXxTXc1QLVUlaXP7NmN7G6DDe6wclI+/d+4kJWjCDb1Y8mrMp2LsxzcOmUT/ST/X8MawsmsZ0z1sVZdKr1WkDtvk7RTLQ1x9jlWbVlnixwC/HIpXcNBvrtqlak6iBhMDciZbAB8pGjxQDFtMS7nrpq0pQfiVCT60b78OJwQneBjqzUpTwhP8ZZNQm/CWiAky7w1HGHN2ai946gLngYZKcNrgs/MVSXjgNSmOJECCMfCRAbAGM4Mj9ost', - e: 65537, - d: 'IwXLnUoZHYEaVkpja/ePyA6wMaXrv2EaO/UFwR64iH7VuLiGEQgF+gdI39p3YO4gZ/EpMc7PWTFalvBVS0eI0zLNidzoPJ6VCDhuc8roaXImWrOEhwEmPhmws65zFrRmiIf5hDcsyNOUYpnygIEjSw5NcFde1nPZSiRH3PRZewJf1mjkvsXb4jHBcZba2o5LC5I89Lgw/de3irJGxOOZVC3xqniNJGA2nVqfPKcOopFyZZ9qd2NTzUBd3stnDfkp83dI8f3OXw/Qdc8SmL+W1KPlJ08wcVfNXvcQiX5HV7/ug+ag59YBi3WWWuRcbWW0YFYf2Zaj/cWK+9mrP1hZiANtMEYBz4Fry8h7IdF04amX9vzt0fCphb/Gk/pJnOYrOrFSLSCIu5baK1WqU5CWot88/1UjAQWi/srRTfY1ZeE=', - p: 'AN8vxMx5le6AFM3vKrrNlY3z7rQxRrHPT+Up3OajfnPfaulUp2xh33bamZyw1hqiP8f0veP80/j3P5Id9ila9NrsC8l82VdhzcFFchqul+deybrkjMrMdoX2LbF1fGR9MRgi85AMwsVKKI5tYkxX5CpTFveW67f8JHchxuB5M6l2IL9P9mX9hiXXsmplMeYsVk+rvF7C22DNCisNFYgUO2U=', - q: 'AMo4+uM9f4BpOHSmayhLhHArIGs4386BkXSeOLeQitaQJ/2czb459O87XoCAonZbq+dI7XRnBU3toQvEsZgrtGkOFXCZJMWAQxD5BQ5vEYT6c86huGpX6h3ODlJ6UGi+5CWyMQ1cFlBkfffFAarjSYTVlyj736sOeDuJWX133z5VQBQ81xSH23kNF95vxB4I1fXG8WL11YZU7VEwSLC4aCk=', - dmp1: 'CkY/sA4U2eLpkkVlWS4nqV5GaeMu1vatPeZ8dd2m75ykZlNwVav3xpfvpGf7o0Q+3Wyl5T35YCe5tHdOor6+MOnW7RKjSsOE3lXdF99bvNn/PWxSEu/wsOARYG8BJ/sfbHYHcmUz/jMYxN3CPbN6HHywvPskpNalsxpJ9oF94FpAza3XsQBH0awpWX6JGoiF/KryRAaCZOVrh2nVyOrthQ==', - dmq1: 'ByidOQ4kRVd0OCzr/jEuLwpXy3Pn+Fd93rL7LwRe5dmUkNXMMr+6e/2OCt6C4c28+CMMxOIgvfF7kf8Uil6BtHZbK/E/6/3uYdtu4mPsKtjy4I25CYqzLvrsZt8NmaeoS+1S7zYjVBU6oFrJBFOndpxZDYpdEKEigHkMQfTMYliCPDUrJ/7nNhHQln8+YhHOATVZtjcdp/O5svYSnK7qgQ==', - coeff: 'N3eUVas9q2DGkXUlcwQUVtohF03EeQoDhhiAxMzgw+BWK6dhf/IntJpfyNacLbfUD25Wfj6Rv4O8qNz5m5NC+chQra7ou8iKdUWycMQD4FtfB6EC3B0cMwO2pgV3HiG8JZDAE2fincnQ47a1thUy28zlo4R9FJhqB+ZHLCENQCNbxTREGfOWZeFBDoH1uOqihLYyxGsmtLkJTrzZ9veEGg==' - }; - - var publicKeyPKCS8 = '-----BEGIN PUBLIC KEY-----\n' + - 'MIIBYjANBgkqhkiG9w0BAQEFAAOCAU8AMIIBSgKCAUEAsE1edyfToZRv6cFOkB0t\n' + - 'AJ5qJor4YF5CccJAL0fS/o1Yk10VSXH4Xx4peSJgYQKkO0HqO1hAz6k9dFQB4U1C\n' + - 'nWtRjtNEcIfycqrZrhu6you5syb6ScV3Zu/9bm7/DyaLlx/gJhUPR1OxOzaqsEvl\n' + - 'u7hbDhNLIYo1zKFb/aUBbD6+UcaGxH2BfFNdzVAtVSVpc/s2Y3sboMN7rByUj793\n' + - '7iQlaMINvVjyasynYuzHNw6ZRP9JP9fwxrCyaxnTPWxVl0qvVaQO2+TtFMtDXH2O\n' + - 'VZtWWeLHAL8cildw0G+u2qVqTqIGEwNyJlsAHykaPFAMW0xLueumrSlB+JUJPrRv\n' + - 'vw4nBCd4GOrNSlPCE/xlk1Cb8JaICTLvDUcYc3ZqL3jqAueBhkpw2uCz8xVJeOA1\n' + - 'KY4kQIIx8JEBsAYzgyP2iy0CAwEAAQ==\n' + - '-----END PUBLIC KEY-----'; - - var generatedKeys = []; - var privateNodeRSA = null; - var publicNodeRSA = null; - - describe('Setup options', function () { - it('should use browser environment', function () { - assert.equal((new NodeRSA(null, {environment: 'browser'})).$options.environment, 'browser'); - }); - - it('should use io.js environment', function () { - assert.equal((new NodeRSA(null, {environment: 'iojs'})).$options.environment, 'iojs'); - }); - - it('should make empty key pair with default options', function () { - var key = new NodeRSA(null); - assert.equal(key.isEmpty(), true); - assert.equal(key.$options.signingScheme, 'pkcs1'); - assert.equal(key.$options.signingSchemeOptions.hash, 'sha256'); - assert.equal(key.$options.signingSchemeOptions.saltLength, null); - - assert.equal(key.$options.encryptionScheme, 'pkcs1_oaep'); - assert.equal(key.$options.encryptionSchemeOptions.hash, 'sha1'); - assert.equal(key.$options.encryptionSchemeOptions.label, null); - }); - - it('should make key pair with pkcs1-md5 signing scheme', function () { - var key = new NodeRSA(null, {signingScheme: 'md5'}); - assert.equal(key.$options.signingScheme, 'pkcs1'); - assert.equal(key.$options.signingSchemeOptions.hash, 'md5'); - }); - - it('should make key pair with pss-sha512 signing scheme', function () { - var key = new NodeRSA(null, {signingScheme: 'pss-sha512'}); - assert.equal(key.$options.signingScheme, 'pss'); - assert.equal(key.$options.signingSchemeOptions.hash, 'sha512'); - }); - - it('should make key pair with pkcs1 encryption scheme, and pss-sha1 signing scheme', function () { - var key = new NodeRSA(null, {encryptionScheme: 'pkcs1', signingScheme: 'pss'}); - assert.equal(key.$options.encryptionScheme, 'pkcs1'); - assert.equal(key.$options.signingScheme, 'pss'); - assert.equal(key.$options.signingSchemeOptions.hash, null); - }); - - it('change options', function () { - var key = new NodeRSA(null, {signingScheme: 'pss-sha1'}); - assert.equal(key.$options.signingScheme, 'pss'); - assert.equal(key.$options.signingSchemeOptions.hash, 'sha1'); - key.setOptions({signingScheme: 'pkcs1'}); - assert.equal(key.$options.signingScheme, 'pkcs1'); - assert.equal(key.$options.signingSchemeOptions.hash, null); - key.setOptions({signingScheme: 'pkcs1-sha256'}); - assert.equal(key.$options.signingScheme, 'pkcs1'); - assert.equal(key.$options.signingSchemeOptions.hash, 'sha256'); - }); - - it('advanced options change', function () { - var key = new NodeRSA(null); - key.setOptions({ - encryptionScheme: { - scheme: 'pkcs1_oaep', - hash: 'sha512', - label: 'horay' - }, - signingScheme: { - scheme: 'pss', - hash: 'md5', - saltLength: 15 - } - }); - - assert.equal(key.$options.signingScheme, 'pss'); - assert.equal(key.$options.signingSchemeOptions.hash, 'md5'); - assert.equal(key.$options.signingSchemeOptions.saltLength, 15); - assert.equal(key.$options.encryptionScheme, 'pkcs1_oaep'); - assert.equal(key.$options.encryptionSchemeOptions.hash, 'sha512'); - assert.equal(key.$options.encryptionSchemeOptions.label, 'horay'); - }); - - it('should throw \'unsupported hashing algorithm\' exception', function () { - var key = new NodeRSA(null); - assert.equal(key.isEmpty(), true); - assert.equal(key.$options.signingScheme, 'pkcs1'); - assert.equal(key.$options.signingSchemeOptions.hash, 'sha256'); - - assert.throw(function () { - key.setOptions({ - environment: 'browser', - signingScheme: 'md4' - }); - }, Error, 'Unsupported hashing algorithm'); - }); - }); - - describe('Base methods', function () { - it('importKey() should throw exception if key data not specified', function () { - var key = new NodeRSA(null); - - assert.throw(function () { - key.importKey(); - }, Error, 'Empty key given'); - }); - - it('importKey() should return this', function () { - var key = new NodeRSA(null); - assert.equal(key.importKey(publicKeyPKCS8), key); - }); - }); - - describe('Work with keys', function () { - describe('Generating keys', function () { - for (var size in keySizes) { - (function (size) { - it('should make key pair ' + size.b + '-bit length and public exponent is ' + (size.e ? size.e : size.e + ' and should be 65537'), function () { - this.timeout(35000); - generatedKeys.push(new NodeRSA({b: size.b, e: size.e}, {encryptionScheme: 'pkcs1'})); - assert.instanceOf(generatedKeys[generatedKeys.length - 1].keyPair, Object); - assert.equal(generatedKeys[generatedKeys.length - 1].isEmpty(), false); - assert.equal(generatedKeys[generatedKeys.length - 1].getKeySize(), size.b); - assert.equal(generatedKeys[generatedKeys.length - 1].getMaxMessageSize(), (size.b / 8 - 11)); - assert.equal(generatedKeys[generatedKeys.length - 1].keyPair.e, size.e || 65537); - }); - })(keySizes[size]); - } - }); - - describe('Import/Export keys', function () { - var publicKeyComponents = { - n: 'ALBNXncn06GUb+nBTpAdLQCeaiaK+GBeQnHCQC9H0v6NWJNdFUlx+F8eKXkiYGECpDtB6jtYQM+pPXRUAeFNQp1rUY7TRHCH8nKq2a4busqLubMm+knFd2bv/W5u/w8mi5cf4CYVD0dTsTs2qrBL5bu4Ww4TSyGKNcyhW/2lAWw+vlHGhsR9gXxTXc1QLVUlaXP7NmN7G6DDe6wclI+/d+4kJWjCDb1Y8mrMp2LsxzcOmUT/ST/X8MawsmsZ0z1sVZdKr1WkDtvk7RTLQ1x9jlWbVlnixwC/HIpXcNBvrtqlak6iBhMDciZbAB8pGjxQDFtMS7nrpq0pQfiVCT60b78OJwQneBjqzUpTwhP8ZZNQm/CWiAky7w1HGHN2ai946gLngYZKcNrgs/MVSXjgNSmOJECCMfCRAbAGM4Mj9ost', - e: 65537, - }; - - var privateKeyPEMNotTrimmed = 'random \n\n data \n\n ' + privateKeyPKCS1 + '\n \n \n\n random data '; - var publicKeyPEMNotTrimmed = '\n\n\n\nrandom \n\n data\n ' + publicKeyPKCS8 + '\n \n random data\n\n '; - - var fileKeyPKCS1 = '-----BEGIN RSA PRIVATE KEY-----\n' + - 'MIICXAIBAAKBgQCCdY+EpDC/vPa335l751SBM8d5Lf4z4QZX4bc+DqTY9zVY/rmP\n' + - 'GbTkCueKnIKApuOGMXJOaCwNH9wUftNt7T0foEwjl16uIC8m4hwSjjNL5TKqMVey\n' + - 'Syv04oBuidv76u5yNiLC4J85lbmW3WAyYkTCbm/VJZAXNJuqCm7AVWmQMQIDAQAB\n' + - 'AoGAEYR3oPfrE9PrzQTZNyn4zuCFCGCEobK1h1dno42T1Q5cu3Z4tB5fi79rF9Gs\n' + - 'NFo0cvBwyNZ0E88TXi0pdrlEW6mdPgQFd3CFxrOgKt9AGpOtI1zzVOb1Uddywq/m\n' + - 'WBPyETwEKzq7lC2nAcMUr0rlFrrDmUT2dafHeuWnFMZ/1YECQQDCtftsH9/prbgu\n' + - 'Q4F2lOWsLz96aix/jnI8FhBmukKmfLMXjCZYYv+Dsr8TIl/iriGqcSgGkBHHoGe1\n' + - 'nmLUZ4EHAkEAq4YcB8T9DLIYUeaS+JRWwLOejU6/rYdgxBIaGn2m0Ldp/z7lLM7g\n' + - 'b0H5Al+7POajkAdnDclBDhyxqInHO4VvBwJBAJ25jNEpgNhqQKg5RsYoF2RDYchn\n' + - '+WPan+7McLzGZPc4TFrmzKkMiK7GPMHjNokJRXwr7aBjVAPBjEEy7BvjPEECQFOJ\n' + - '4rcKAzEewGeLREObg9Eg6nTqSMLMb52vL1V9ozR+UDrHuDilnXuyhwPX+kqEDl+E\n' + - 'q3V0cqHb6c8rI4TizRsCQANIyhoJ33ughNzbCIknkMPKtgvLOUARnbya/bkfRexL\n' + - 'icyYzXPNuqZDY8JZQHlshN8cCcZcYjGPYYscd2LKB6o=\n' + - '-----END RSA PRIVATE KEY-----'; - var keysFolder = __dirname + '/keys/'; - var keys_formats = { - 'pkcs1-private-der': {public: false, der: true, file: 'private_pkcs1.der'}, - 'pkcs1-private-pem': {public: false, der: false, file: 'private_pkcs1.pem'}, - 'pkcs8-private-der': {public: false, der: true, file: 'private_pkcs8.der'}, - 'pkcs8-private-pem': {public: false, der: false, file: 'private_pkcs8.pem'}, - 'pkcs1-public-der': {public: true, der: true, file: 'public_pkcs1.der'}, - 'pkcs1-public-pem': {public: true, der: false, file: 'public_pkcs1.pem'}, - 'pkcs8-public-der': {public: true, der: true, file: 'public_pkcs8.der'}, - 'pkcs8-public-pem': {public: true, der: false, file: 'public_pkcs8.pem'}, - - 'private': {public: false, der: false, file: 'private_pkcs1.pem'}, - 'public': {public: true, der: false, file: 'public_pkcs8.pem'}, - 'private-der': {public: false, der: true, file: 'private_pkcs1.der'}, - 'public-der': {public: true, der: true, file: 'public_pkcs8.der'}, - - 'pkcs1': {public: false, der: false, file: 'private_pkcs1.pem'}, - 'pkcs1-private': {public: false, der: false, file: 'private_pkcs1.pem'}, - 'pkcs1-der': {public: false, der: true, file: 'private_pkcs1.der'}, - 'pkcs8': {public: false, der: false, file: 'private_pkcs8.pem'}, - 'pkcs8-private': {public: false, der: false, file: 'private_pkcs8.pem'}, - 'pkcs8-der': {public: false, der: true, file: 'private_pkcs8.der'}, - 'pkcs1-public': {public: true, der: false, file: 'public_pkcs1.pem'}, - 'pkcs8-public': {public: true, der: false, file: 'public_pkcs8.pem'}, - - 'openssh-public': {public: true, der: false, file: 'id_rsa.pub'}, - 'openssh-private': {public: false, der: false, file: 'id_rsa'} - }; - - describe('Good cases', function () { - describe('Common cases', function () { - it('should load private key from (not trimmed) PKCS1-PEM string', function () { - privateNodeRSA = new NodeRSA(privateKeyPEMNotTrimmed); - assert.instanceOf(privateNodeRSA.keyPair, Object); - assert(privateNodeRSA.isPrivate()); - assert(privateNodeRSA.isPublic()); - assert(!privateNodeRSA.isPublic(true)); - }); - - it('should load public key from (not trimmed) PKCS8-PEM string', function () { - publicNodeRSA = new NodeRSA(publicKeyPEMNotTrimmed); - assert.instanceOf(publicNodeRSA.keyPair, Object); - assert(publicNodeRSA.isPublic()); - assert(publicNodeRSA.isPublic(true)); - assert(!publicNodeRSA.isPrivate()); - }); - - it('.exportKey() should return private PEM string', function () { - assert.equal(privateNodeRSA.exportKey('private'), privateKeyPKCS1); - assert.equal(privateNodeRSA.exportKey(), privateKeyPKCS1); - }); - - it('.exportKey() from public key should return pkcs8 public PEM string', function () { - assert.equal(publicNodeRSA.exportKey('public'), publicKeyPKCS8); - }); - - it('.exportKey() from private key should return pkcs8 public PEM string', function () { - assert.equal(privateNodeRSA.exportKey('public'), publicKeyPKCS8); - }); - - it('should create and load key from buffer/fs.readFileSync output', function () { - var key = new NodeRSA(fs.readFileSync(keysFolder + 'private_pkcs1.pem')); - assert.equal(key.exportKey(), fileKeyPKCS1); - key = new NodeRSA(); - key.importKey(fs.readFileSync(keysFolder + 'private_pkcs1.pem')); - assert.equal(key.exportKey(), fileKeyPKCS1); - }); - - it('should gracefully handle data outside of encapsulation boundaries for pkcs1 private keys', function () { - let privateFileWithNoise = 'Lorem ipsum' + fs.readFileSync(keysFolder + 'private_pkcs1.pem') + 'dulce et decorum'; - let key = new NodeRSA(privateFileWithNoise); - assert.equal(key.exportKey(), fileKeyPKCS1); - }); - - it('should gracefully handle data outside of encapsulation boundaries for pkcs1 public keys', function () { - let publicFileWithNoise = 'Lorem ipsum' + fs.readFileSync(keysFolder + 'public_pkcs1.pem') + 'dulce et decorum'; - let publicNodeRSA = new NodeRSA(publicFileWithNoise); - assert.instanceOf(publicNodeRSA.keyPair, Object); - assert(publicNodeRSA.isPublic()); - assert(publicNodeRSA.isPublic(true)); - assert(!publicNodeRSA.isPrivate()); - }); - - it('should gracefully handle data outside of encapsulation boundaries for pkcs8 private keys', function () { - let privateFileWithNoise = 'Lorem ipsum' + fs.readFileSync(keysFolder + 'private_pkcs8.pem') + 'dulce et decorum'; - let key = new NodeRSA(privateFileWithNoise); - assert.equal(key.exportKey(), fileKeyPKCS1); - }); - - it('should gracefully handle data outside of encapsulation boundaries for pkcs8 public keys', function () { - let publicFileWithNoise = 'Lorem ipsum' + fs.readFileSync(keysFolder + 'public_pkcs8.pem') + 'dulce et decorum'; - let publicNodeRSA = new NodeRSA(publicFileWithNoise); - assert.instanceOf(publicNodeRSA.keyPair, Object); - assert(publicNodeRSA.isPublic()); - assert(publicNodeRSA.isPublic(true)); - assert(!publicNodeRSA.isPrivate()); - }); - - it('should handle data without begin/end encapsulation boundaries for pkcs1 private keys', function () { - let privateFile = fs.readFileSync(keysFolder + 'private_pkcs1.pem', "utf8"); - let privateFileNoBoundaries = privateFile.substring("-----BEGIN RSA PRIVATE KEY-----".length, privateFile.indexOf("-----END RSA PRIVATE KEY-----")); - let key = new NodeRSA(privateFileNoBoundaries, "pkcs1-private-pem"); - assert.equal(key.exportKey(), fileKeyPKCS1); - }); - - it('should handle data without begin/end encapsulation boundaries for pkcs1 public keys', function () { - let publicFile = fs.readFileSync(keysFolder + 'public_pkcs1.pem', "utf8"); - let publicFileNoBoundaries = publicFile.substring("-----BEGIN RSA PUBLIC KEY-----".length, publicFile.indexOf("-----END RSA PUBLIC KEY-----")); - let publicNodeRSA = new NodeRSA(publicFileNoBoundaries, "pkcs1-public-pem"); - assert.instanceOf(publicNodeRSA.keyPair, Object); - assert(publicNodeRSA.isPublic()); - assert(publicNodeRSA.isPublic(true)); - assert(!publicNodeRSA.isPrivate()); - }); - - it('should handle data without begin/end encapsulation boundaries for pkcs8 private keys', function () { - let privateFile = fs.readFileSync(keysFolder + 'private_pkcs8.pem', "utf8"); - let privateFileNoBoundaries = privateFile.substring('-----BEGIN PRIVATE KEY-----'.length, privateFile.indexOf('-----END PRIVATE KEY-----')); - let key = new NodeRSA(privateFileNoBoundaries, "pkcs8-private-pem"); - assert.equal(key.exportKey(), fileKeyPKCS1); - }); - - it('should handle data without begin/end encapsulation boundaries for pkcs8 public keys', function () { - let publicFile = fs.readFileSync(keysFolder + 'public_pkcs8.pem', "utf8"); - let publicFileNoBoundaries = publicFile.substring("-----BEGIN PUBLIC KEY-----".length, publicFile.indexOf("-----END PUBLIC KEY-----")); - let publicNodeRSA = new NodeRSA(publicFileNoBoundaries, "pkcs8-public-pem"); - assert.instanceOf(publicNodeRSA.keyPair, Object); - assert(publicNodeRSA.isPublic()); - assert(publicNodeRSA.isPublic(true)); - assert(!publicNodeRSA.isPrivate()); - }); - - it('.importKey() from private components', function () { - var key = new NodeRSA(); - key.importKey({ - n: Buffer.from(privateKeyComponents.n, 'base64'), - e: 65537, - d: Buffer.from(privateKeyComponents.d, 'base64'), - p: Buffer.from(privateKeyComponents.p, 'base64'), - q: Buffer.from(privateKeyComponents.q, 'base64'), - dmp1: Buffer.from(privateKeyComponents.dmp1, 'base64'), - dmq1: Buffer.from(privateKeyComponents.dmq1, 'base64'), - coeff: Buffer.from(privateKeyComponents.coeff, 'base64') - }, 'components'); - assert(key.isPrivate()); - assert.equal(key.exportKey('pkcs1-private'), privateKeyPKCS1); - assert.equal(key.exportKey('pkcs8-public'), publicKeyPKCS8); - }); - - it('.importKey() from public components', function () { - var key = new NodeRSA(); - key.importKey({ - n: Buffer.from(publicKeyComponents.n, 'base64'), - e: 65537 - }, 'components-public'); - assert(key.isPublic(true)); - assert.equal(key.exportKey('pkcs8-public'), publicKeyPKCS8); - }); - - it('.exportKey() private components', function () { - var key = new NodeRSA(privateKeyPKCS1); - var components = key.exportKey('components'); - assert(_.isEqual({ - n: components.n.toString('base64'), - e: components.e, - d: components.d.toString('base64'), - p: components.p.toString('base64'), - q: components.q.toString('base64'), - dmp1: components.dmp1.toString('base64'), - dmq1: components.dmq1.toString('base64'), - coeff: components.coeff.toString('base64') - }, privateKeyComponents)); - }); - - it('.exportKey() public components', function () { - var key = new NodeRSA(publicKeyPKCS8); - var components = key.exportKey('components-public'); - assert(_.isEqual({ - n: components.n.toString('base64'), - e: components.e - }, publicKeyComponents)); - }); - }); - - describe('Different key formats', function () { - var sampleKey = new NodeRSA(fileKeyPKCS1); - - for (var format in keys_formats) { - (function (format) { - var options = keys_formats[format]; - - it('should load from ' + options.file + ' (' + format + ')', function () { - var key = new NodeRSA(fs.readFileSync(keysFolder + options.file), format); - if (options.public) { - assert.equal(key.exportKey('public'), sampleKey.exportKey('public')); - } else { - assert.equal(key.exportKey(), sampleKey.exportKey()); - } - }); - - it('should export to \'' + format + '\' format', function () { - var keyData = fs.readFileSync(keysFolder + options.file); - var exported = sampleKey.exportKey(format); - - if (options.der) { - assert(Buffer.isBuffer(exported)); - assert.equal(exported.toString('hex'), keyData.toString('hex')); - } else { - assert(_.isString(exported)); - assert.equal(exported.replace(/\s+|\n\r|\n|\r$/gm, ''), keyData.toString('utf8').replace(/\s+|\n\r|\n|\r$/gm, '')); - } - }); - })(format); - } - }); - - describe('OpenSSH keys', function () { - /* - * Warning! - * OpenSSH private key contains unused 64bit value, this value is set by ssh-keygen, - * but it's not used. NodeRSA does NOT store this value, so importing and exporting key sets this value to 0. - * This value is 0 in test files, so the tests pass. - */ - it('key export should preserve key data including comment', function(){ - const opensshPrivateKey = fs.readFileSync(keysFolder + 'id_rsa_comment').toString(); - const opensshPublicKey = fs.readFileSync(keysFolder + 'id_rsa_comment.pub').toString(); - const opensshPriv = new NodeRSA(opensshPrivateKey); - const opensshPub = new NodeRSA(opensshPublicKey); - - assert.equal( - opensshPriv.exportKey('openssh-private'), - opensshPrivateKey - ); - - assert.equal( - opensshPriv.exportKey('openssh-public'), - opensshPublicKey - ); - - assert.equal( - opensshPub.exportKey('openssh-public'), - opensshPublicKey - ); - }); - }) - }); - - describe('Bad cases', function () { - it('not public key', function () { - var key = new NodeRSA(); - assert.throw(function () { - key.exportKey(); - }, Error, 'This is not private key'); - assert.throw(function () { - key.exportKey('public'); - }, Error, 'This is not public key'); - }); - - it('not private key', function () { - var key = new NodeRSA(publicKeyPKCS8); - assert.throw(function () { - key.exportKey(); - }, Error, 'This is not private key'); - assert.doesNotThrow(function () { - key.exportKey('public'); - }, Error, 'This is not public key'); - }); - }); - }); - }); - - describe('Encrypting & decrypting', function () { - for (var env in environments) { - (function (env) { - for (var scheme_i in encryptSchemes) { - (function (scheme) { - describe('Environment: ' + env + '. Encryption scheme: ' + scheme, function () { - describe('Good cases', function () { - var encrypted = {}; - var decrypted = {}; - for (var i in dataBundle) { - (function (i) { - var key = null; - var suit = dataBundle[i]; - - it('`encrypt()` should encrypt ' + i, function () { - key = new NodeRSA(generatedKeys[Math.round(Math.random() * 1000) % generatedKeys.length].exportKey(), { - environment: env, - encryptionScheme: scheme - }); - encrypted[i] = key.encrypt(suit.data); - assert(Buffer.isBuffer(encrypted[i])); - assert(encrypted[i].length > 0); - }); - - it('`decrypt()` should decrypt ' + i, function () { - decrypted[i] = key.decrypt(encrypted[i], _.isArray(suit.encoding) ? suit.encoding[0] : suit.encoding); - if (Buffer.isBuffer(decrypted[i])) { - assert.equal(suit.data.toString('hex'), decrypted[i].toString('hex')); - } else { - assert(_.isEqual(suit.data, decrypted[i])); - } - }); - })(i); - } - - - }); - - describe('Bad cases', function () { - it('unsupported data types', function () { - assert.throw(function () { - generatedKeys[0].encrypt(null); - }, Error, 'Unexpected data type'); - assert.throw(function () { - generatedKeys[0].encrypt(undefined); - }, Error, 'Unexpected data type'); - assert.throw(function () { - generatedKeys[0].encrypt(true); - }, Error, 'Unexpected data type'); - }); - - it('incorrect key for decrypting', function () { - var encrypted = generatedKeys[0].encrypt('data'); - assert.throw(function () { - generatedKeys[1].decrypt(encrypted); - }, Error, 'Error during decryption'); - }); - }); - }); - })(encryptSchemes[scheme_i]); - } - - describe('Environment: ' + env + '. encryptPrivate & decryptPublic', function () { - var encrypted = {}; - var decrypted = {}; - for (var i in dataBundle) { - (function (i) { - var key = null; - var suit = dataBundle[i]; - - it('`encryptPrivate()` should encrypt ' + i, function () { - key = new NodeRSA(generatedKeys[Math.round(Math.random() * 1000) % generatedKeys.length].exportKey(), { - environment: env - }); - encrypted[i] = key.encryptPrivate(suit.data); - assert(Buffer.isBuffer(encrypted[i])); - assert(encrypted[i].length > 0); - }); - - it('`decryptPublic()` should decrypt ' + i, function () { - decrypted[i] = key.decryptPublic(encrypted[i], _.isArray(suit.encoding) ? suit.encoding[0] : suit.encoding); - if (Buffer.isBuffer(decrypted[i])) { - assert.equal(suit.data.toString('hex'), decrypted[i].toString('hex')); - } else { - assert(_.isEqual(suit.data, decrypted[i])); - } - }); - })(i); - } - }); - })(environments[env]); - } - - describe('Compatibility of different environments', function () { - for (var scheme_i in encryptSchemes) { - (function (scheme) { - var encrypted = {}; - var decrypted = {}; - for (var i in dataBundle) { - (function (i) { - var key1 = null; - var key2 = null; - var suit = dataBundle[i]; - - it('Encryption scheme: ' + scheme + ' `encrypt()` by browser ' + i, function () { - var key = generatedKeys[Math.round(Math.random() * 1000) % generatedKeys.length].exportKey(); - key1 = new NodeRSA(key, { - environment: 'browser', - encryptionScheme: scheme - }); - key2 = new NodeRSA(key, { - environment: 'node', - encryptionScheme: scheme - }); - encrypted[i] = key1.encrypt(suit.data); - assert(Buffer.isBuffer(encrypted[i])); - assert(encrypted[i].length > 0); - }); - - it('Encryption scheme: ' + scheme + ' `decrypt()` by node ' + i, function () { - decrypted[i] = key2.decrypt(encrypted[i], _.isArray(suit.encoding) ? suit.encoding[0] : suit.encoding); - if (Buffer.isBuffer(decrypted[i])) { - assert.equal(suit.data.toString('hex'), decrypted[i].toString('hex')); - } else { - assert(_.isEqual(suit.data, decrypted[i])); - } - }); - })(i); - } - - encrypted = {}; - decrypted = {}; - for (var i in dataBundle) { - (function (i) { - var key1 = null; - var key2 = null; - var suit = dataBundle[i]; - - it('Encryption scheme: ' + scheme + ' `encrypt()` by node ' + i + '. Scheme', function () { - var key = generatedKeys[Math.round(Math.random() * 1000) % generatedKeys.length].exportKey(); - key1 = new NodeRSA(key, { - environment: 'node', - encryptionScheme: scheme - }); - key2 = new NodeRSA(key, { - environment: 'browser', - encryptionScheme: scheme - }); - encrypted[i] = key1.encrypt(suit.data); - assert(Buffer.isBuffer(encrypted[i])); - assert(encrypted[i].length > 0); - }); - - it('Encryption scheme: ' + scheme + ' `decrypt()` by browser ' + i, function () { - decrypted[i] = key2.decrypt(encrypted[i], _.isArray(suit.encoding) ? suit.encoding[0] : suit.encoding); - if (Buffer.isBuffer(decrypted[i])) { - assert.equal(suit.data.toString('hex'), decrypted[i].toString('hex')); - } else { - assert(_.isEqual(suit.data, decrypted[i])); - } - }); - })(i); - } - })(encryptSchemes[scheme_i]); - } - - describe('encryptPrivate & decryptPublic', function () { - var encrypted = {}; - var decrypted = {}; - for (var i in dataBundle) { - (function (i) { - var key1 = null; - var key2 = null; - var suit = dataBundle[i]; - - it('`encryptPrivate()` by browser ' + i, function () { - var key = generatedKeys[Math.round(Math.random() * 1000) % generatedKeys.length].exportKey(); - key1 = new NodeRSA(key, {environment: 'browser'}); - key2 = new NodeRSA(key, {environment: 'node'}); - encrypted[i] = key1.encryptPrivate(suit.data); - assert(Buffer.isBuffer(encrypted[i])); - assert(encrypted[i].length > 0); - }); - - it('`decryptPublic()` by node ' + i, function () { - decrypted[i] = key2.decryptPublic(encrypted[i], _.isArray(suit.encoding) ? suit.encoding[0] : suit.encoding); - if (Buffer.isBuffer(decrypted[i])) { - assert.equal(suit.data.toString('hex'), decrypted[i].toString('hex')); - } else { - assert(_.isEqual(suit.data, decrypted[i])); - } - }); - })(i); - } - - for (var i in dataBundle) { - (function (i) { - var key1 = null; - var key2 = null; - var suit = dataBundle[i]; - - it('`encryptPrivate()` by node ' + i, function () { - var key = generatedKeys[Math.round(Math.random() * 1000) % generatedKeys.length].exportKey(); - key1 = new NodeRSA(key, {environment: 'browser'}); - key2 = new NodeRSA(key, {environment: 'node'}); - encrypted[i] = key1.encryptPrivate(suit.data); - assert(Buffer.isBuffer(encrypted[i])); - assert(encrypted[i].length > 0); - }); - - it('`decryptPublic()` by browser ' + i, function () { - decrypted[i] = key2.decryptPublic(encrypted[i], _.isArray(suit.encoding) ? suit.encoding[0] : suit.encoding); - if (Buffer.isBuffer(decrypted[i])) { - assert.equal(suit.data.toString('hex'), decrypted[i].toString('hex')); - } else { - assert(_.isEqual(suit.data, decrypted[i])); - } - }); - })(i); - } - }); - }); - }); - - describe('Signing & verifying', function () { - for (var scheme_i in signingSchemes) { - (function (scheme) { - describe('Signing scheme: ' + scheme, function () { - var envs = ['node']; - if (scheme == 'pkcs1') { - envs = environments; - } - - for (var env in envs) { - (function (env) { - describe('Good cases ' + (envs.length > 1 ? ' in ' + env + ' environment' : ''), function () { - var signed = {}; - var key = null; - - for (var i in dataBundle) { - (function (i) { - var suit = dataBundle[i]; - it('should sign ' + i, function () { - key = new NodeRSA(generatedKeys[generatedKeys.length - 1].exportKey(), { - signingScheme: scheme + '-sha256', - environment: env - }); - signed[i] = key.sign(suit.data); - assert(Buffer.isBuffer(signed[i])); - assert(signed[i].length > 0); - }); - - it('should verify ' + i, function () { - assert(key.verify(suit.data, signed[i])); - }); - })(i); - } - - for (var alg in signHashAlgorithms[env]) { - (function (alg) { - it('signing with custom algorithm (' + alg + ')', function () { - var key = new NodeRSA(generatedKeys[generatedKeys.length - 1].exportKey(), { - signingScheme: scheme + '-' + alg, - environment: env - }); - var signed = key.sign('data'); - assert(key.verify('data', signed)); - }); - - if (scheme === 'pss') { - it('signing with custom algorithm (' + alg + ') with max salt length', function () { - var a = alg.toLowerCase(); - var key = new NodeRSA(generatedKeys[generatedKeys.length - 1].exportKey(), { - signingScheme: { scheme: scheme, hash: a, saltLength: OAEP.digestLength[a] }, - environment: env - }); - var signed = key.sign('data'); - assert(key.verify('data', signed)); - }); - } - })(signHashAlgorithms[env][alg]); - } - }); - - describe('Bad cases' + (envs.length > 1 ? ' in ' + env + ' environment' : ''), function () { - it('incorrect data for verifying', function () { - var key = new NodeRSA(generatedKeys[0].exportKey(), { - signingScheme: scheme + '-sha256', - environment: env - }); - var signed = key.sign('data1'); - assert(!key.verify('data2', signed)); - }); - - it('incorrect key for signing', function () { - var key = new NodeRSA(generatedKeys[0].exportKey('pkcs8-public'), { - signingScheme: scheme + '-sha256', - environment: env - }); - assert.throw(function () { - key.sign('data'); - }, Error, 'This is not private key'); - }); - - it('incorrect key for verifying', function () { - var key1 = new NodeRSA(generatedKeys[0].exportKey(), { - signingScheme: scheme + '-sha256', - environment: env - }); - var key2 = new NodeRSA(generatedKeys[1].exportKey('pkcs8-public'), { - signingScheme: scheme + '-sha256', - environment: env - }); - var signed = key1.sign('data'); - assert(!key2.verify('data', signed)); - }); - - it('incorrect key for verifying (empty)', function () { - var key = new NodeRSA(null, {environment: env}); - - assert.throw(function () { - key.verify('data', 'somesignature'); - }, Error, 'This is not public key'); - }); - - it('different algorithms', function () { - var singKey = new NodeRSA(generatedKeys[0].exportKey(), { - signingScheme: scheme + '-md5', - environment: env - }); - var verifyKey = new NodeRSA(generatedKeys[0].exportKey(), { - signingScheme: scheme + '-sha1', - environment: env - }); - var signed = singKey.sign('data'); - assert(!verifyKey.verify('data', signed)); - }); - }); - })(envs[env]); - } - - if (scheme !== 'pkcs1') { - return; - } - - describe('Compatibility of different environments', function () { - for (var alg in signHashAlgorithms['browser']) { - (function (alg) { - it('signing with custom algorithm (' + alg + ') (equal test)', function () { - var nodeKey = new NodeRSA(generatedKeys[5].exportKey(), { - signingScheme: scheme + '-' + alg, - environment: 'node' - }); - var browserKey = new NodeRSA(generatedKeys[5].exportKey(), { - signingScheme: scheme + '-' + alg, - environment: 'browser' - }); - - assert.equal(nodeKey.sign('data', 'hex'), browserKey.sign('data', 'hex')); - }); - - it('sign in node & verify in browser (' + alg + ')', function () { - var nodeKey = new NodeRSA(generatedKeys[5].exportKey(), { - signingScheme: scheme + '-' + alg, - environment: 'node' - }); - var browserKey = new NodeRSA(generatedKeys[5].exportKey(), { - signingScheme: scheme + '-' + alg, - environment: 'browser' - }); - - assert(browserKey.verify('data', nodeKey.sign('data'))); - }); - - it('sign in browser & verify in node (' + alg + ')', function () { - var nodeKey = new NodeRSA(generatedKeys[5].exportKey(), { - signingScheme: scheme + '-' + alg, - environment: 'node' - }); - var browserKey = new NodeRSA(generatedKeys[5].exportKey(), { - signingScheme: scheme + '-' + alg, - environment: 'browser' - }); - - assert(nodeKey.verify('data', browserKey.sign('data'))); - }); - })(signHashAlgorithms['browser'][alg]); - } - }); - }); - })(signingSchemes[scheme_i]); - } - }); -}); \ No newline at end of file diff --git a/test/utils/text-utils.spec.ts b/test/utils/text-utils.spec.ts new file mode 100644 index 0000000..75b3c75 --- /dev/null +++ b/test/utils/text-utils.spec.ts @@ -0,0 +1,101 @@ +import { describe, expect, it } from 'vitest'; +import { linebrk, trimSurroundingText } from '../../src/utils/text-utils.js'; + +describe('linebrk', () => { + it('returns the input unchanged when shorter than maxLen', () => { + expect(linebrk('abc', 10)).toBe('abc'); + }); + + it('returns the input unchanged when length equals maxLen', () => { + expect(linebrk('abcde', 5)).toBe('abcde'); + }); + + it('inserts a newline at every maxLen boundary', () => { + expect(linebrk('abcdefghij', 3)).toBe('abc\ndef\nghi\nj'); + }); + + it('does not emit a trailing newline when input length is a multiple of maxLen', () => { + expect(linebrk('abcdef', 3)).toBe('abc\ndef'); + }); + + it('returns empty string for empty input', () => { + expect(linebrk('', 4)).toBe(''); + }); + + it('handles maxLen = 1 (one char per line)', () => { + expect(linebrk('abc', 1)).toBe('a\nb\nc'); + }); +}); + +describe('trimSurroundingText', () => { + const OPEN = '<>'; + const CLOSE = '<>'; + + describe('boundary conditions', () => { + it('extracts content between first opening and first closing', () => { + expect(trimSurroundingText(`prefix${OPEN}body${CLOSE}suffix`, OPEN, CLOSE)).toBe('body'); + }); + + it('returns input verbatim when neither marker is present', () => { + expect(trimSurroundingText('plain text', OPEN, CLOSE)).toBe('plain text'); + }); + + it('keeps only the tail when only the opening is present', () => { + expect(trimSurroundingText(`prefix${OPEN}tail`, OPEN, CLOSE)).toBe('tail'); + }); + + it('returns input verbatim when only closing appears (no preceding opening)', () => { + // closing without opening: openIdx = -1 → closeIdx not computed → both + // bounds default to the whole string. + expect(trimSurroundingText(`head${CLOSE}suffix`, OPEN, CLOSE)).toBe(`head${CLOSE}suffix`); + }); + + it('extracts an empty body when opening and closing are adjacent', () => { + expect(trimSurroundingText(`${OPEN}${CLOSE}`, OPEN, CLOSE)).toBe(''); + }); + + it('uses the first closing after the first opening, not the last', () => { + const text = `${OPEN}first${CLOSE}middle${CLOSE}tail`; + expect(() => trimSurroundingText(text, OPEN, CLOSE)).not.toThrow(); + expect(trimSurroundingText(text, OPEN, CLOSE)).toBe('first'); + }); + }); + + describe('multi-block rejection', () => { + it('throws when a second opening appears after the first closing', () => { + const text = `${OPEN}one${CLOSE}${OPEN}two${CLOSE}`; + expect(() => trimSurroundingText(text, OPEN, CLOSE)).toThrow(/multiple .* blocks/); + }); + + it('throws even with arbitrary text between the two blocks', () => { + const text = `${OPEN}one${CLOSE}\n\n# noise here\n\n${OPEN}two${CLOSE}`; + expect(() => trimSurroundingText(text, OPEN, CLOSE)).toThrow(/multiple .* blocks/); + }); + + it('does not reject when a different opening marker appears after the close', () => { + // The guard is keyed on the *same* opening string, not any "BEGIN". + const text = `${OPEN}body${CLOSE}<>other body<>`; + expect(() => trimSurroundingText(text, OPEN, CLOSE)).not.toThrow(); + expect(trimSurroundingText(text, OPEN, CLOSE)).toBe('body'); + }); + + it('does not reject when a second opening appears *before* the first closing', () => { + // Two opening markers in a row → second one is just part of the body. + const text = `${OPEN}${OPEN}body${CLOSE}`; + expect(() => trimSurroundingText(text, OPEN, CLOSE)).not.toThrow(); + expect(trimSurroundingText(text, OPEN, CLOSE)).toBe(`${OPEN}body`); + }); + }); + + describe('arbitrary markers (not PEM)', () => { + it('works with multi-char generic markers', () => { + expect(trimSurroundingText('xxx[[start]]payload[[end]]yyy', '[[start]]', '[[end]]')).toBe( + 'payload', + ); + }); + + it('works with single-char markers', () => { + expect(trimSurroundingText('ab', '<', '>')).toBe('x'); + }); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..08c301f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,37 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2022", "DOM"], + "outDir": "./dist", + "rootDir": "./src", + + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "stripInternal": true, + + "strict": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "noFallthroughCasesInSwitch": true, + "exactOptionalPropertyTypes": true, + "noImplicitReturns": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + + "verbatimModuleSyntax": true, + "isolatedModules": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "types": ["node"], + + "ignoreDeprecations": "6.0" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "src.legacy", "test", "bench", "examples", ".tmp"] +} diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 0000000..0062903 --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": ".", + "noEmit": true, + "noUnusedLocals": false, + "noUnusedParameters": false + }, + "include": [ + "src/**/*", + "test/**/*", + "vitest.config.ts", + "vitest.bench.config.ts", + "tsup.config.ts" + ], + "exclude": ["node_modules", "dist", "src.legacy", ".tmp"] +} diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 0000000..bbb4fb6 --- /dev/null +++ b/tsup.config.ts @@ -0,0 +1,29 @@ +import { defineConfig } from 'tsup'; + +const shared = { + sourcemap: true, + treeshake: true, + splitting: false, + clean: true, + outDir: 'dist', +} as const; + +export default defineConfig([ + { + ...shared, + entry: { 'index.node': 'src/index.node.ts' }, + format: ['esm', 'cjs'], + platform: 'node', + target: 'node20', + dts: true, + }, + { + ...shared, + entry: { 'index.browser': 'src/index.browser.ts' }, + format: ['esm'], + platform: 'browser', + target: 'es2022', + dts: true, + clean: false, + }, +]); diff --git a/vitest.bench.config.ts b/vitest.bench.config.ts new file mode 100644 index 0000000..31739d2 --- /dev/null +++ b/vitest.bench.config.ts @@ -0,0 +1,25 @@ +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { defineConfig } from 'vitest/config'; + +const root = dirname(fileURLToPath(import.meta.url)); +const nodeEntry = resolve(root, 'src/index.node.ts'); + +// The bench imports the public API via the virtual `node-rsa-bench-entry` +// specifier, which resolves to the node bundle. The three modes (`node`, +// `js-jsbn`, `js-native`) are selected per-bench via constructor options — +// no separate project per bundle is needed, since the digest/RNG backend +// is always native to the runtime. +export default defineConfig({ + resolve: { + alias: { 'node-rsa-bench-entry': nodeEntry }, + }, + test: { + name: 'bench', + environment: 'node', + env: { NODE_RSA_BENCH_MODES: 'node,js-jsbn,js-native' }, + benchmark: { + include: ['bench/**/*.bench.ts'], + }, + }, +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..934dc2b --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,37 @@ +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { defineConfig } from 'vitest/config'; + +const root = dirname(fileURLToPath(import.meta.url)); +const nodeBackend = resolve(root, 'src/crypto/backend.node.ts'); +const webBackend = resolve(root, 'src/crypto/backend.web.ts'); + +export default defineConfig({ + test: { + projects: [ + { + test: { + name: 'node', + include: ['test/**/*.spec.ts'], + environment: 'node', + }, + }, + { + resolve: { + alias: { + [nodeBackend]: webBackend, + }, + }, + define: { + 'process.env.NODE_RSA_FORCE_BACKEND': JSON.stringify('web'), + }, + test: { + name: 'browser-emulated', + include: ['test/**/*.spec.ts'], + exclude: ['test/**/*.node-only.spec.ts'], + environment: 'node', + }, + }, + ], + }, +});