Skip to content

src: decouple KeyObject and CryptoKey and move CryptoKey to src#62924

Open
panva wants to merge 13 commits intonodejs:mainfrom
panva:native-cryptokey
Open

src: decouple KeyObject and CryptoKey and move CryptoKey to src#62924
panva wants to merge 13 commits intonodejs:mainfrom
panva:native-cryptokey

Conversation

@panva
Copy link
Copy Markdown
Member

@panva panva commented Apr 24, 2026

Refactors Node.js' Web Crypto CryptoKey implementation to decouple it from KeyObject by moving CryptoKey's internal-slot storage into a new native NativeCryptoKey base and updating internal crypto code to use slot accessors instead of JS-visible properties.

Paired with a future EOL #62321 deprecation (v26.x runtime-deprecation, v27.x EOL) this makes non-extractable CryptoKey key material unreachable in userland 🎉 thanks to removing public access to the [[handle]] internal slot.

Changes:

  • Introduces NativeCryptoKey (C++) plus JS-side slot helpers (getCryptoKey{Type,Extractable,Algorithm,Usages,Handle}) and updates Web Crypto/internal consumers accordingly.
  • Migrates Web Crypto key generation/import/export paths from KeyObject wrappers to KeyObjectHandle usage, including structured-clone/worker-transfer support.
  • Adds targeted regression tests for slot hiding / brand checks / clone-transfer and adds Web Crypto sign/verify benchmarks.

The PR is squash-merged, but the changes are split into logical commits to make review tractable.

Commits

Each commit has a description covering its scope and rationale, see individual commit messages for details. Commits are cumulative (except for fixups and applying feedback). No later commit from the ones listed below revises code introduced by an earlier one. In order:

  1. src: decouple KeyObject and CryptoKey and move CryptoKey to src (empty, just a squash target)
  2. src,crypto: add NativeCryptoKey
  3. lib,crypto: rewire CryptoKey on the native class
  4. lib,crypto: migrate algorithm modules to native CryptoKey
  5. src,crypto: relax RSA/EC keygen arg checks
  6. lib,crypto: validate HkdfParams info length early
  7. lib,crypto: add early structural JWK validation
  8. test: add CryptoKey class regression tests
  9. benchmark: add Web Crypto sign/verify benchmarks
  10. ++ ... rest is fixups and applying feedback

The PR's changes UI is your friend, either filter commits, or filter files, or both, mark reviewed files as Viewed to remember where you left off.

Benchmark

https://ci.nodejs.org/view/Node.js%20benchmark/job/benchmark-node-micro-benchmarks/1833/

No regression on any parallel configuration; RSASSA-PKCS1-v1_5 verify is ~5–6% faster (***) on both shared-key and unique-key-per-call. serial-mode rows have ±20–60% margins and are inconclusive. The lone ** regression candidate (EC sign, shared/parallel, −3.96%) falls within the multiple-comparison false-positive budget (≈0.62 expected at **+ across 30 comparisons).

Signed-off-by: Filip Skokan <panva.ip@gmail.com>

@panva panva added crypto Issues and PRs related to the crypto subsystem. lib / src Issues and PRs related to general changes in the lib or src directory. security Issues and PRs related to security. webcrypto commit-queue-squash Add this label to instruct the Commit Queue to squash all the PR commits into the first one. labels Apr 24, 2026
@nodejs-github-bot
Copy link
Copy Markdown
Collaborator

nodejs-github-bot commented Apr 24, 2026

Review requested:

  • @nodejs/crypto
  • @nodejs/performance
  • @nodejs/cpp-reviewers (for src,crypto: add NativeCryptoKey)

@nodejs-github-bot nodejs-github-bot added the needs-ci PRs that need a full CI run. label Apr 24, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 24, 2026

Codecov Report

❌ Patch coverage is 89.03002% with 95 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.62%. Comparing base (2428030) to head (74cba08).
⚠️ Report is 37 commits behind head on main.

Files with missing lines Patch % Lines
src/crypto/crypto_keys.cc 70.83% 18 Missing and 38 partials ⚠️
lib/internal/crypto/webcrypto_util.js 85.07% 10 Missing ⚠️
lib/internal/crypto/keys.js 96.77% 7 Missing ⚠️
lib/internal/crypto/webcrypto.js 93.02% 2 Missing and 4 partials ⚠️
src/crypto/crypto_ec.cc 55.55% 2 Missing and 2 partials ⚠️
src/crypto/crypto_keys.h 71.42% 4 Missing ⚠️
lib/internal/crypto/ml_kem.js 92.30% 3 Missing ⚠️
lib/internal/crypto/diffiehellman.js 81.81% 2 Missing ⚠️
lib/internal/crypto/cfrg.js 96.55% 1 Missing ⚠️
lib/internal/crypto/ml_dsa.js 97.22% 1 Missing ⚠️
... and 1 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #62924      +/-   ##
==========================================
- Coverage   89.63%   89.62%   -0.01%     
==========================================
  Files         706      706              
  Lines      219219   219432     +213     
  Branches    42004    42088      +84     
==========================================
+ Hits       196499   196671     +172     
- Misses      14622    14632      +10     
- Partials     8098     8129      +31     
Files with missing lines Coverage Δ
lib/internal/crypto/aes.js 92.48% <100.00%> (+2.58%) ⬆️
lib/internal/crypto/argon2.js 95.95% <100.00%> (+0.06%) ⬆️
lib/internal/crypto/chacha20_poly1305.js 98.42% <100.00%> (+6.37%) ⬆️
lib/internal/crypto/ec.js 95.59% <100.00%> (+1.41%) ⬆️
lib/internal/crypto/hkdf.js 96.04% <100.00%> (+0.06%) ⬆️
lib/internal/crypto/mac.js 98.98% <100.00%> (+6.28%) ⬆️
lib/internal/crypto/pbkdf2.js 94.89% <100.00%> (+0.15%) ⬆️
lib/internal/crypto/util.js 96.10% <100.00%> (+0.57%) ⬆️
lib/internal/crypto/webidl.js 98.39% <100.00%> (+<0.01%) ⬆️
lib/internal/util/comparisons.js 99.71% <100.00%> (+<0.01%) ⬆️
... and 13 more

... and 55 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread src/crypto/crypto_keys.cc
Comment thread src/crypto/crypto_keys.cc
Comment thread src/crypto/crypto_keys.h
@panva
Copy link
Copy Markdown
Member Author

panva commented Apr 24, 2026

@addaleax thank you for your notes, I hope 74cba08 65e5612addresses them.

@panva panva added the request-ci Add this label to start a Jenkins CI on a PR. label Apr 24, 2026
panva added 12 commits April 25, 2026 12:13
Signed-off-by: Filip Skokan <panva.ip@gmail.com>
Introduces a C++ `NativeCryptoKey` class that holds the real CryptoKey
slots (`handle_data_`, `algorithm_`, `usages_`, `extractable_`) and
provides structured-clone and worker-transfer support through a dedicated
`CryptoKeyTransferData`. New bindings `createCryptoKeyClass`,
`getCryptoKeyHandle`, and `getCryptoKeySlots` expose the class and
accessors to JS; a brand-tag class-ID pointer rejects spoofed receivers
on the accessors.

Signed-off-by: Filip Skokan <panva.ip@gmail.com>
JS-side `CryptoKey` now extends the native `NativeCryptoKey` created via
`createCryptoKeyClass`. `InternalCryptoKey` caches the
`[type, extractable, algorithm, usages, handle]` slot tuple in a
`#slots` private field on first access; the public getters and
`isCryptoKey` route through dedicated `getCryptoKey{Type,Extractable,
Algorithm,Usages,Handle}` helpers re-exported from this module. Symbol
property storage (`kKeyObject`, `kAlgorithm`, `kExtractable`,
`kKeyUsages`, `kCachedAlgorithm`, `kCachedKeyUsages`) is gone;
`kKeyObject` is dropped from `internal/crypto/util` exports.

`deepStrictEqual` on CryptoKey switches to the new accessors plus
`handle.equals()` (instead of structural compare on wrapper objects).
An ESLint rule forbids destructuring `getCryptoKeyHandle` from
`internalBinding('crypto')`; it must come from `internal/crypto/keys`
so the brand-check path is used.

Signed-off-by: Filip Skokan <panva.ip@gmail.com>
Every Web Crypto algorithm module drops `key[kKeyObject][kHandle]`
symbol access in favor of `getCryptoKey*` accessors and stores the raw
`KeyObjectHandle` directly (no more `SecretKeyObject`/`PublicKeyObject`/
`PrivateKeyObject` wrappers on the CryptoKey slot). Key generation
moves off the promisified `generateKey`/`generateKeyPair` paths onto
ones that return `KeyObjectHandle` (i.e. not wrapped in KeyObject):

  - `SecretKeyGenJob` for AES, ChaCha20-Poly1305, HMAC/CMAC (aes.js,
    chacha20_poly1305.js, mac.js).
  - `EcKeyPairGenJob` for ECDSA, ECDH (ec.js).
  - `NidKeyPairGenJob` for Ed25519/Ed448/X25519/X448, ML-DSA-{44,65,87},
    ML-KEM-{512,768,1024} (cfrg.js, ml_dsa.js, ml_kem.js).
  - `RsaKeyPairGenJob` for RSASSA-PKCS1-v1_5, RSA-PSS, RSA-OAEP (rsa.js).

`webcrypto_util.js` switches its import helpers (`importDerKey`,
`importJwkKey`, `importRawKey`) to return raw `KeyObjectHandle`s and
adds `importSecretKey` / `importJwkSecretKey` helpers for symmetric
imports. `webcrypto.js` follows the same pattern; its `getPublicKey`
builds a temporary wrapper and carries a TODO for future refactor.
`hkdf.js` switches from `promisify(hkdf)` to
`jobPromise(() => new HKDFJob(...))` directly. `diffiehellman.js`,
`argon2.js`, and `pbkdf2.js` are accessor-only migrations (the
`TODO(panva)` notes on argon2/pbkdf2 are orthogonal and left for a later
cleanup).

Signed-off-by: Filip Skokan <panva.ip@gmail.com>
Loosens two `AdditionalConfig` precondition checks so the new Web Crypto
keygen jobs added earlier (RsaKeyPairGenJob, EcKeyPairGenJob) can reuse
the shared traits without threading unused encoding args through the job
constructor.

  - `RsaKeyGenTraits::AdditionalConfig` now CHECKs RSA key
     type-dependant argument count accounting for being able to skip
     unused parameters.
  - `EcKeyGenTraits::AdditionalConfig` now defaults `param_encoding` to
    `OPENSSL_EC_NAMED_CURVE`, this is not observable in existing
    crypto.generateKeyPair(Sync) as its dispatch already applies the
    same default. This is just so that a stray OPENSSL_EC_NAMED_CURVE
    isn't needed in ec.js

Pure precondition relaxation — no new code paths. Existing
`generateKeyPair` callers still pass the same args and hit the same
branches.

Signed-off-by: Filip Skokan <panva.ip@gmail.com>
Enforces a 1024-byte maximum on `HkdfParams.info` at the WebIDL layer
using refactored `validateMaxBufferLength`. Its error message is also
fixed.

Oversized `info` was already rejected via a different code path. It just
relocates the rejection earlier into the WebIDL conversion step so the
failure reproduces the OpenSSL's limitation early.

Signed-off-by: Filip Skokan <panva.ip@gmail.com>
Tightens `validateJwk` to require the per-`kty` string members up front
(before switching to C++), short-circuiting with the same
`Invalid keyData` message and `DataError` when the JWK is missing
required fields or passes a non-string value for one. In theory non-strings
are already rejected by WebIDL's JWK converter but this doesn't hurt.

  - RSA: requires `n`, `e`; if `d` is present, also requires `p`, `q`,
    `dp`, `dq`, and `qi`.
  - EC:  requires `crv`, `x`, `y`; optional `d`.
  - OKP: requires `crv`, `x`; optional `d`.
  - oct: requires `k`.
  - AKP: requires `alg`, `pub`; optional `priv`.

Four export/import negative tests update their expected error text from
the later "Invalid JWK … Parameter and algorithm name mismatch" to the
new short-circuit "Invalid keyData" (for the case where `crv`/`alg` is
missing entirely). A new `{ kty: 'oct' }` missing-`k` negative is added
for ChaCha20-Poly1305. The tests check error messages but the error
class (DataError/DOMException) is the same everywhere.

Signed-off-by: Filip Skokan <panva.ip@gmail.com>
Adds four focused tests that check guarantees either introduced by the
native `CryptoKey` refactor or carried over from the existing state.

  - `test-webcrypto-cryptokey-brand-check.js` - each of the four
    prototype getters (`type`, `extractable`, `algorithm`, `usages`)
    throws `ERR_INVALID_THIS` for foreign receivers (plain objects,
    null-proto, primitives, null/undefined, functions, a subverted
    `Symbol.hasInstance`, and a real `BaseObject` of a different kind).
    `util.types.isCryptoKey()` remains accurate after a prototype
    swap, confirming it cannot be spoofed.
  - `test-webcrypto-cryptokey-clone-transfer.js` - exhaustive
    structured-clone, `MessagePort.postMessage`, and
    `Worker.postMessage` round-trips.
    Verifies slot preservation, inspect-output equivalence, and that
    crypto operations interoperate across clones including repeated
    round-trips.
  - `test-webcrypto-cryptokey-hidden-slots.js` - replaces all four
    prototype getters with forged versions and confirms internal
    consumers (export, inspect) still read the real native slots.
  - `test-webcrypto-cryptokey-no-own-symbols.js`, asserts CryptoKey
    instances expose no own symbol-keyed properties even after every
    public getter has been touched (proof the `#slots` private field
    plus native storage leaves the instance shape pristine).

Signed-off-by: Filip Skokan <panva.ip@gmail.com>
Adds two benchmarks exercising `subtle.sign` / `subtle.verify` across
the main algorithm families:

  - ECDSA P-256
  - RSASSA-PKCS1-v1_5
  - RSA-PSS
  - Ed25519
  - ML-DSA-44 (gated on OpenSSL >= 3.5)

Each benchmark runs two modes (serial / parallel) × two key-reuse
patterns (shared / unique per call) so regressions in slot-access
hot paths (getCryptoKey* accessors) and in per-call key wrapping
surface on both dimensions.

Signed-off-by: Filip Skokan <panva.ip@gmail.com>
Signed-off-by: Filip Skokan <panva.ip@gmail.com>
Signed-off-by: Filip Skokan <panva.ip@gmail.com>
Signed-off-by: Filip Skokan <panva.ip@gmail.com>
@panva panva force-pushed the native-cryptokey branch from 74cba08 to 1d88d0c Compare April 25, 2026 10:36
Signed-off-by: Filip Skokan <panva.ip@gmail.com>
@panva panva force-pushed the native-cryptokey branch from 1d88d0c to 6843254 Compare April 25, 2026 11:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

commit-queue-squash Add this label to instruct the Commit Queue to squash all the PR commits into the first one. crypto Issues and PRs related to the crypto subsystem. lib / src Issues and PRs related to general changes in the lib or src directory. needs-ci PRs that need a full CI run. request-ci Add this label to start a Jenkins CI on a PR. security Issues and PRs related to security. webcrypto

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants