From d78bb5c490a6ed2526fe2aefb87b2a58e0d85948 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Fri, 24 Apr 2026 13:52:52 +0200 Subject: [PATCH 01/12] src: decouple KeyObject and CryptoKey and move CryptoKey to src Signed-off-by: Filip Skokan From 0a1e601bd0e9880f644aac1d0c613e9f36a8096c Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Fri, 24 Apr 2026 14:09:01 +0200 Subject: [PATCH 02/12] src,crypto: add NativeCryptoKey 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 --- src/crypto/crypto_keys.cc | 307 ++++++++++++++++++++++++++++++++++++++ src/crypto/crypto_keys.h | 90 +++++++++++ src/env_properties.h | 1 + src/node_crypto.cc | 1 + 4 files changed, 399 insertions(+) diff --git a/src/crypto/crypto_keys.cc b/src/crypto/crypto_keys.cc index 29a8b4513d64dc..c0497ffd289d30 100644 --- a/src/crypto/crypto_keys.cc +++ b/src/crypto/crypto_keys.cc @@ -1788,6 +1788,313 @@ std::unique_ptr NativeKeyObject::CloneForMessaging() return std::make_unique(handle_data_); } +void NativeCryptoKey::Initialize(Environment* env, Local target) { + SetMethod(env->context(), + target, + "createCryptoKeyClass", + NativeCryptoKey::CreateCryptoKeyClass); + SetMethod(env->context(), + target, + "getCryptoKeyHandle", + NativeCryptoKey::GetKeyHandle); + SetMethod( + env->context(), target, "getCryptoKeySlots", NativeCryptoKey::GetSlots); +} + +void NativeCryptoKey::RegisterExternalReferences( + ExternalReferenceRegistry* registry) { + registry->Register(NativeCryptoKey::CreateCryptoKeyClass); + registry->Register(NativeCryptoKey::GetKeyHandle); + registry->Register(NativeCryptoKey::GetSlots); + registry->Register(NativeCryptoKey::New); +} + +namespace { +// Brand check: every NativeCryptoKey stores this pointer in its +// kClassTagField slot. Nothing else in the binary can produce the +// same pointer, so HasInstance() can use it to recognize a real +// NativeCryptoKey. +constexpr int kNativeCryptoKeyClassTag = 0; +const void* class_tag() { + return &kNativeCryptoKeyClassTag; +} +} // namespace + +bool NativeCryptoKey::HasInstance(Local value) { + if (!value->IsObject()) return false; + Local obj = value.As(); + if (obj->InternalFieldCount() < NativeCryptoKey::kInternalFieldCount) { + return false; + } + return obj->GetAlignedPointerFromInternalField( + NativeCryptoKey::kClassTagField, EmbedderDataTag::kDefault) == + class_tag(); +} + +void NativeCryptoKey::New(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CHECK_EQ(args.Length(), 4); + // args[0] is a KeyObjectHandle; we keep its KeyObjectData directly. + // args[1] is the algorithm dictionary object. + // args[2] is the usages array of strings. + // args[3] is the extractable boolean. + // + // args[1] is undefined only when called from + // CryptoKeyTransferData::Deserialize for a partially-initialized + // CryptoKey: algorithm/usages/extractable get filled in afterwards + // by FinalizeTransferRead before any JS can see the object. + // + // This constructor is not exposed to user JS - the public CryptoKey + // class throws from its constructor and InternalCryptoKey is kept + // in a module-closure. + CHECK(KeyObjectHandle::HasInstance(env, args[0])); + KeyObjectHandle* handle = Unwrap(args[0].As()); + CHECK_NOT_NULL(handle); + + auto* native = new NativeCryptoKey(env, args.This(), handle->Data()); + + // Brand-check tag for HasInstance(). + args.This()->SetAlignedPointerInInternalField(kClassTagField, + const_cast(class_tag()), + EmbedderDataTag::kDefault); + + if (!args[1]->IsUndefined()) { + CHECK(args[1]->IsObject()); + CHECK(args[2]->IsArray()); + CHECK(args[3]->IsBoolean()); + native->algorithm_.Reset(env->isolate(), args[1].As()); + native->usages_.Reset(env->isolate(), args[2].As()); + native->extractable_ = args[3]->IsTrue(); + } +} + +void NativeCryptoKey::CreateCryptoKeyClass( + const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Isolate* isolate = env->isolate(); + + CHECK_EQ(args.Length(), 1); + Local callback = args[0]; + CHECK(callback->IsFunction()); + + Local t = + NewFunctionTemplate(isolate, NativeCryptoKey::New); + t->InstanceTemplate()->SetInternalFieldCount( + NativeCryptoKey::kInternalFieldCount); + + Local ctor; + if (!t->GetFunction(env->context()).ToLocal(&ctor)) return; + + Local recv = Undefined(env->isolate()); + Local ret_v; + if (!callback.As() + ->Call(env->context(), recv, 1, &ctor) + .ToLocal(&ret_v)) { + return; + } + Local ret = ret_v.As(); + Local internal_ctor_v; + if (!ret->Get(env->context(), 1).ToLocal(&internal_ctor_v)) return; + env->set_crypto_internal_cryptokey_constructor( + internal_ctor_v.As()); + args.GetReturnValue().Set(ret); +} + +void NativeCryptoKey::GetKeyHandle(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CHECK_EQ(args.Length(), 1); + CHECK(HasInstance(args[0])); + NativeCryptoKey* native = Unwrap(args[0].As()); + Local handle; + if (!KeyObjectHandle::Create(env, native->handle_data_).ToLocal(&handle)) { + return; + } + args.GetReturnValue().Set(handle); +} + +// Returns all of the key's internal slot values as a single Array: +// [type, extractable, algorithm, usages, handle]. JS-side helpers +// call this once per key to prime a per-instance cache, so subsequent +// reads don't need to cross into C++ at all. +void NativeCryptoKey::GetSlots(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CHECK_EQ(args.Length(), 1); + if (!HasInstance(args[0])) { + THROW_ERR_INVALID_THIS(env, "Value of \"this\" must be of type CryptoKey"); + return; + } + Isolate* isolate = env->isolate(); + NativeCryptoKey* native = Unwrap(args[0].As()); + + const char* type_str; + switch (native->handle_data_.GetKeyType()) { + case kKeyTypeSecret: + type_str = "secret"; + break; + case kKeyTypePublic: + type_str = "public"; + break; + case kKeyTypePrivate: + type_str = "private"; + break; + default: + UNREACHABLE(); + } + + Local handle; + if (!KeyObjectHandle::Create(env, native->handle_data_).ToLocal(&handle)) { + return; + } + + CHECK(!native->algorithm_.IsEmpty()); + CHECK(!native->usages_.IsEmpty()); + Local slots[] = { + OneByteString(isolate, type_str), + v8::Boolean::New(isolate, native->extractable_), + PersistentToLocal::Strong(native->algorithm_), + PersistentToLocal::Strong(native->usages_), + handle, + }; + args.GetReturnValue().Set(Array::New(isolate, slots, arraysize(slots))); +} + +BaseObject::TransferMode NativeCryptoKey::GetTransferMode() const { + return BaseObject::TransferMode::kCloneable; +} + +std::unique_ptr NativeCryptoKey::CloneForMessaging() + const { + Isolate* isolate = env()->isolate(); + CHECK(!algorithm_.IsEmpty()); + CHECK(!usages_.IsEmpty()); + v8::Global algorithm_copy(isolate, + PersistentToLocal::Strong(algorithm_)); + v8::Global usages_copy(isolate, PersistentToLocal::Strong(usages_)); + return std::make_unique(handle_data_, + std::move(algorithm_copy), + std::move(usages_copy), + extractable_); +} + +Maybe NativeCryptoKey::FinalizeTransferRead( + Local context, v8::ValueDeserializer* deserializer) { + Local bundle_v; + if (!deserializer->ReadValue(context).ToLocal(&bundle_v)) { + return Nothing(); + } + CHECK(bundle_v->IsObject()); + Local bundle = bundle_v.As(); + Isolate* isolate = env()->isolate(); + + Local algorithm_v; + if (!bundle->Get(context, FIXED_ONE_BYTE_STRING(isolate, "algorithm")) + .ToLocal(&algorithm_v)) { + return Nothing(); + } + CHECK(algorithm_v->IsObject()); + algorithm_.Reset(isolate, algorithm_v.As()); + + Local usages_v; + if (!bundle->Get(context, FIXED_ONE_BYTE_STRING(isolate, "usages")) + .ToLocal(&usages_v)) { + return Nothing(); + } + CHECK(usages_v->IsArray()); + usages_.Reset(isolate, usages_v.As()); + + Local extractable_v; + if (!bundle->Get(context, FIXED_ONE_BYTE_STRING(isolate, "extractable")) + .ToLocal(&extractable_v)) { + return Nothing(); + } + CHECK(extractable_v->IsBoolean()); + extractable_ = extractable_v->IsTrue(); + + return v8::JustVoid(); +} + +Maybe NativeCryptoKey::CryptoKeyTransferData::FinalizeTransferWrite( + Local context, v8::ValueSerializer* serializer) { + Isolate* isolate = Isolate::GetCurrent(); + CHECK(!algorithm_.IsEmpty()); + CHECK(!usages_.IsEmpty()); + Local bundle = Object::New(isolate); + Local algorithm_v = PersistentToLocal::Strong(algorithm_); + Local usages_v = PersistentToLocal::Strong(usages_); + if (bundle + ->Set( + context, FIXED_ONE_BYTE_STRING(isolate, "algorithm"), algorithm_v) + .IsNothing() || + bundle->Set(context, FIXED_ONE_BYTE_STRING(isolate, "usages"), usages_v) + .IsNothing() || + bundle + ->Set(context, + FIXED_ONE_BYTE_STRING(isolate, "extractable"), + v8::Boolean::New(isolate, extractable_)) + .IsNothing()) { + return Nothing(); + } + auto ret = serializer->WriteValue(context, bundle); + algorithm_.Reset(); + usages_.Reset(); + return ret; +} + +BaseObjectPtr NativeCryptoKey::CryptoKeyTransferData::Deserialize( + Environment* env, + Local context, + std::unique_ptr self) { + if (context != env->context()) { + THROW_ERR_MESSAGE_TARGET_CONTEXT_UNAVAILABLE(env); + return {}; + } + + // Reconstruct the KeyObjectHandle for the transferred KeyObjectData. + Local handle; + if (!KeyObjectHandle::Create(env, data_).ToLocal(&handle)) return {}; + + // Make sure internal/crypto/keys has been loaded so that the + // CryptoKey constructor is registered with the Environment. + Local arg = + FIXED_ONE_BYTE_STRING(env->isolate(), "internal/crypto/keys"); + if (env->builtin_module_require() + ->Call(context, Null(env->isolate()), 1, &arg) + .IsEmpty()) { + return {}; + } + + // Construct a partially-initialized InternalCryptoKey; algorithm, + // usages and extractable are filled in via FinalizeTransferRead. + Local cryptokey_ctor = env->crypto_internal_cryptokey_constructor(); + CHECK(!cryptokey_ctor.IsEmpty()); + Local ctor_args[] = { + handle, + Undefined(env->isolate()), + Undefined(env->isolate()), + Undefined(env->isolate()), + }; + Local cryptokey; + if (!cryptokey_ctor->NewInstance(context, 4, ctor_args).ToLocal(&cryptokey)) { + return {}; + } + + return BaseObjectPtr( + Unwrap(cryptokey.As())); +} + +void NativeCryptoKey::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("handle_data", handle_data_); + tracker->TrackField("algorithm", algorithm_); + tracker->TrackField("usages", usages_); +} + +void NativeCryptoKey::CryptoKeyTransferData::MemoryInfo( + MemoryTracker* tracker) const { + tracker->TrackField("data", data_); + tracker->TrackField("algorithm", algorithm_); + tracker->TrackField("usages", usages_); +} + namespace Keys { void Initialize(Environment* env, Local target) { target->Set(env->context(), diff --git a/src/crypto/crypto_keys.h b/src/crypto/crypto_keys.h index 237a45ec955beb..1198b9d856c486 100644 --- a/src/crypto/crypto_keys.h +++ b/src/crypto/crypto_keys.h @@ -233,6 +233,96 @@ class NativeKeyObject : public BaseObject { KeyObjectData handle_data_; }; +// NativeCryptoKey is the native base class for the Web Crypto +// `CryptoKey`. It holds the internal slots - `[[type]]`, +// `[[extractable]]`, `[[algorithm]]`, `[[usages]]`, and the +// underlying KeyObjectData. The public `type`, `extractable`, +// `algorithm`, and `usages` accessors on `CryptoKey.prototype` are +// user-configurable per Web IDL, so internal consumers read these +// values directly from the C++ side via a single `GetSlots` call +// which returns all slots at once; JS primes a per-instance cache +// from that result. +class NativeCryptoKey : public BaseObject { + public: + enum InternalFields { + kClassTagField = BaseObject::kInternalFieldCount, + kInternalFieldCount, + }; + + static void Initialize(Environment* env, v8::Local target); + static void RegisterExternalReferences(ExternalReferenceRegistry* registry); + + static void New(const v8::FunctionCallbackInfo& args); + static void CreateCryptoKeyClass( + const v8::FunctionCallbackInfo& args); + + // True if `value` is a real NativeCryptoKey instance. Checks the + // kClassTagField internal field. + // Used by `GetSlots` / `GetKeyHandle` to validate their receiver. + static bool HasInstance(v8::Local value); + + static void GetKeyHandle(const v8::FunctionCallbackInfo& args); + // Returns [type, extractable, algorithm, usages, handle] in one call + // so JS can prime a per-instance cache on first access. + static void GetSlots(const v8::FunctionCallbackInfo& args); + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(NativeCryptoKey) + SET_SELF_SIZE(NativeCryptoKey) + + class CryptoKeyTransferData : public worker::TransferData { + public: + CryptoKeyTransferData(const KeyObjectData& data, + v8::Global&& algorithm, + v8::Global&& usages, + bool extractable) + : data_(data.addRef()), + algorithm_(std::move(algorithm)), + usages_(std::move(usages)), + extractable_(extractable) {} + + BaseObjectPtr Deserialize( + Environment* env, + v8::Local context, + std::unique_ptr self) override; + + v8::Maybe FinalizeTransferWrite( + v8::Local context, + v8::ValueSerializer* serializer) override; + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(CryptoKeyTransferData) + SET_SELF_SIZE(CryptoKeyTransferData) + + private: + KeyObjectData data_; + v8::Global algorithm_; + v8::Global usages_; + bool extractable_; + }; + + BaseObject::TransferMode GetTransferMode() const override; + std::unique_ptr CloneForMessaging() const override; + v8::Maybe FinalizeTransferRead( + v8::Local context, + v8::ValueDeserializer* deserializer) override; + + const KeyObjectData& handle_data() const { return handle_data_; } + + private: + NativeCryptoKey(Environment* env, + v8::Local wrap, + const KeyObjectData& handle_data) + : BaseObject(env, wrap), handle_data_(handle_data.addRef()) { + MakeWeak(); + } + + KeyObjectData handle_data_; + v8::Global algorithm_; + v8::Global usages_; + bool extractable_ = false; +}; + enum WebCryptoKeyFormat { kWebCryptoKeyFormatRaw, kWebCryptoKeyFormatPKCS8, diff --git a/src/env_properties.h b/src/env_properties.h index b95bdfa65e4d39..8713248da8df68 100644 --- a/src/env_properties.h +++ b/src/env_properties.h @@ -480,6 +480,7 @@ V(crypto_key_object_private_constructor, v8::Function) \ V(crypto_key_object_public_constructor, v8::Function) \ V(crypto_key_object_secret_constructor, v8::Function) \ + V(crypto_internal_cryptokey_constructor, v8::Function) \ V(enhance_fatal_stack_after_inspector, v8::Function) \ V(enhance_fatal_stack_before_inspector, v8::Function) \ V(get_source_map_error_source, v8::Function) \ diff --git a/src/node_crypto.cc b/src/node_crypto.cc index 84375f9a737675..c0869f40e0410d 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -48,6 +48,7 @@ namespace crypto { V(Hmac) \ V(Keygen) \ V(Keys) \ + V(NativeCryptoKey) \ V(NativeKeyObject) \ V(PBKDF2Job) \ V(Random) \ From 27230eca5a643d380db5d211723b36f02e3d02a6 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Fri, 24 Apr 2026 14:09:30 +0200 Subject: [PATCH 03/12] lib,crypto: rewire CryptoKey on the native class 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 --- lib/eslint.config_partial.mjs | 4 + lib/internal/crypto/keys.js | 404 +++++++++++++++++-------------- lib/internal/crypto/util.js | 2 - lib/internal/util/comparisons.js | 29 ++- 4 files changed, 238 insertions(+), 201 deletions(-) diff --git a/lib/eslint.config_partial.mjs b/lib/eslint.config_partial.mjs index 48328ed678a0a8..e2701cf3d473ef 100644 --- a/lib/eslint.config_partial.mjs +++ b/lib/eslint.config_partial.mjs @@ -71,6 +71,10 @@ export default [ selector: "CallExpression[callee.type='Identifier'][callee.name='ReflectApply'][arguments.2.type='ArrayExpression']", message: 'Use `FunctionPrototypeCall` to avoid creating an ad-hoc array', }, + { + selector: "VariableDeclarator[init.type='CallExpression'][init.callee.name='internalBinding'][init.arguments.0.value='crypto'] > ObjectPattern > Property[key.name='getCryptoKeyHandle']", + message: "Use `const { getCryptoKeyHandle } = require('internal/crypto/keys');` instead of destructuring it from `internalBinding('crypto')`.", + }, ], 'no-restricted-globals': [ 'error', diff --git a/lib/internal/crypto/keys.js b/lib/internal/crypto/keys.js index a1acb782a5f5db..84f0054b3e9f82 100644 --- a/lib/internal/crypto/keys.js +++ b/lib/internal/crypto/keys.js @@ -15,6 +15,8 @@ const { const { KeyObjectHandle, createNativeKeyObjectClass, + createCryptoKeyClass, + getCryptoKeySlots: nativeGetCryptoKeySlots, kKeyTypeSecret, kKeyTypePublic, kKeyTypePrivate, @@ -56,7 +58,6 @@ const { const { kHandle, - kKeyObject, getArrayBufferOrView, bigIntArrayToUnsignedBigInt, normalizeAlgorithm, @@ -68,12 +69,6 @@ const { isArrayBufferView, } = require('internal/util/types'); -const { - markTransferMode, - kClone, - kDeserialize, -} = require('internal/worker/js_transferable'); - const { customInspectSymbol: kInspect, getDeprecationWarningEmitter, @@ -83,12 +78,12 @@ const { const { inspect } = require('internal/util/inspect'); -const kAlgorithm = Symbol('kAlgorithm'); -const kExtractable = Symbol('kExtractable'); +// Module-local symbol used by KeyObject to store its `type` string +// ("secret"/"public"/"private"). It is also used by `isKeyObject` to +// distinguish KeyObject instances from other types. CryptoKey no longer +// uses any module-local symbol slots - its state lives in C++ internal +// fields on `NativeCryptoKey`. const kKeyType = Symbol('kKeyType'); -const kKeyUsages = Symbol('kKeyUsages'); -const kCachedAlgorithm = Symbol('kCachedAlgorithm'); -const kCachedKeyUsages = Symbol('kCachedKeyUsages'); const emitDEP0203 = getDeprecationWarningEmitter( 'DEP0203', @@ -100,7 +95,7 @@ const maybeEmitDEP0204 = getDeprecationWarningEmitter( 'Passing a non-extractable CryptoKey to KeyObject.from() is deprecated.', undefined, false, - (key) => !key[kExtractable], + (key) => !getCryptoKeyExtractable(key), ); // Key input contexts. @@ -154,7 +149,14 @@ const { if (!isCryptoKey(key)) throw new ERR_INVALID_ARG_TYPE('key', 'CryptoKey', key); maybeEmitDEP0204(key); - return key[kKeyObject]; + const handle = getCryptoKeyHandle(key); + switch (getCryptoKeyType(key)) { + /* eslint-disable no-use-before-define */ + case 'secret': return new SecretKeyObject(handle); + case 'public': return new PublicKeyObject(handle); + case 'private': return new PrivateKeyObject(handle); + /* eslint-enable no-use-before-define */ + } } equals(otherKeyObject) { @@ -251,9 +253,9 @@ const { throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } - if (result[kKeyUsages].length === 0) { + if (getCryptoKeyUsages(result).length === 0) { throw lazyDOMException( - `Usages cannot be empty when importing a ${result.type} key.`, + `Usages cannot be empty when importing a ${getCryptoKeyType(result)} key.`, 'SyntaxError'); } @@ -346,9 +348,10 @@ const { throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } - if (result.type === 'private' && result[kKeyUsages].length === 0) { + const resultType = getCryptoKeyType(result); + if (resultType === 'private' && getCryptoKeyUsages(result).length === 0) { throw lazyDOMException( - `Usages cannot be empty when importing a ${result.type} key.`, + `Usages cannot be empty when importing a ${resultType} key.`, 'SyntaxError'); } @@ -588,7 +591,7 @@ function parsePrivateKeyEncoding(enc, keyType, objName) { return parseKeyEncoding(enc, keyType, false, objName); } -function getKeyObjectHandle(key, ctx) { +function validateAsymmetricKeyType(type, ctx, key) { if (ctx === kCreatePrivate) { throw new ERR_INVALID_ARG_TYPE( 'key', @@ -597,16 +600,14 @@ function getKeyObjectHandle(key, ctx) { ); } - if (key.type !== 'private') { + if (type !== 'private') { if (ctx === kConsumePrivate || ctx === kCreatePublic) - throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(key.type, 'private'); - if (key.type !== 'public') { - throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(key.type, + throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(type, 'private'); + if (type !== 'public') { + throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(type, 'private or public'); } } - - return key[kHandle]; } function getKeyTypes(allowKeyObject, bufferOnly = false) { @@ -631,11 +632,13 @@ function getKeyTypes(allowKeyObject, bufferOnly = false) { function prepareAsymmetricKey(key, ctx, name = 'key') { if (isKeyObject(key)) { // Best case: A key object, as simple as that. - return { data: getKeyObjectHandle(key, ctx) }; + validateAsymmetricKeyType(key.type, ctx, key); + return { data: key[kHandle] }; } if (isCryptoKey(key)) { emitDEP0203(); - return { data: getKeyObjectHandle(key[kKeyObject], ctx) }; + validateAsymmetricKeyType(getCryptoKeyType(key), ctx, key); + return { data: getCryptoKeyHandle(key) }; } if (isStringOrBuffer(key)) { // Expect PEM by default, mostly for backward compatibility. @@ -647,11 +650,13 @@ function prepareAsymmetricKey(key, ctx, name = 'key') { // The 'key' property can be a KeyObject as well to allow specifying // additional options such as padding along with the key. if (isKeyObject(data)) { - return { data: getKeyObjectHandle(data, ctx) }; + validateAsymmetricKeyType(data.type, ctx, data); + return { data: data[kHandle] }; } if (isCryptoKey(data)) { emitDEP0203(); - return { data: getKeyObjectHandle(data[kKeyObject], ctx) }; + validateAsymmetricKeyType(getCryptoKeyType(data), ctx, data); + return { data: getCryptoKeyHandle(data) }; } if (format === 'jwk') { validateObject(data, `${name}.key`); @@ -716,9 +721,10 @@ function prepareSecretKey(key, encoding, bufferOnly = false) { } if (isCryptoKey(key)) { emitDEP0203(); - if (key[kKeyType] !== 'secret') - throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(key[kKeyType], 'secret'); - return key[kKeyObject][kHandle]; + const type = getCryptoKeyType(key); + if (type !== 'secret') + throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(type, 'secret'); + return getCryptoKeyHandle(key); } } if (typeof key !== 'string' && @@ -758,180 +764,200 @@ function createPrivateKey(key) { } function isKeyObject(obj) { - return obj != null && obj[kKeyType] !== undefined && obj[kKeyObject] === undefined; + return obj != null && obj[kKeyType] !== undefined; } -// Our implementation of CryptoKey is a simple wrapper around a KeyObject -// that adapts it to the standard interface. -// TODO(@jasnell): Embedder environments like electron may have issues -// here similar to other things like URL. A chromium provided CryptoKey -// will not be recognized as a Node.js CryptoKey, and vice versa. It -// would be fantastic if we could find a way of making those interop. -class CryptoKey { - constructor() { - throw new ERR_ILLEGAL_CONSTRUCTOR(); - } +// CryptoKey is a plain JS class whose prototype's [[Prototype]] is +// Object.prototype, as Web Crypto requires. Instance storage (type, +// extractable, algorithm, usages, and the KeyObject handle) lives on +// a C++ class, NativeCryptoKey, created by createCryptoKeyClass. +// InternalCryptoKey is the only constructor we expose to internal +// code; it extends NativeCryptoKey to get that storage and then has +// its prototype spliced so the chain visible to user code is: +// instance -> InternalCryptoKey.prototype +// -> CryptoKey.prototype +// -> Object.prototype +// +// All five internal slots are read from C++ in a single call via +// `getCryptoKeySlots`. The resulting array is cached in a private +// class field on `InternalCryptoKey` so that it is invisible to +// reflection (`Object.getOwnPropertySymbols` etc.) and leaves each +// CryptoKey's hidden class pristine. The `getCryptoKey{Type, +// Extractable,Algorithm,Usages,Handle}` helpers index into that +// array; the public `algorithm` / `usages` getters further cache a +// cloned copy (as Web Crypto requires repeat reads to return the +// same object so a consumer's mutation is visible next time). +let getSlots; // Populated by the createCryptoKeyClass callback below. + +const kSlotType = 0; +const kSlotExtractable = 1; +const kSlotAlgorithm = 2; +const kSlotUsages = 3; +const kSlotHandle = 4; +const kSlotClonedAlgorithm = 5; +const kSlotClonedUsages = 6; + +function cloneAlgorithm(raw) { + const cloned = { ...raw }; + if (cloned.hash !== undefined) cloned.hash = { ...cloned.hash }; + if (cloned.publicExponent !== undefined) + cloned.publicExponent = new Uint8Array(cloned.publicExponent); + return cloned; +} - [kInspect](depth, options) { - if (depth < 0) - return this; +const { + 0: CryptoKey, + 1: InternalCryptoKey, +} = createCryptoKeyClass((NativeCryptoKey) => { + class CryptoKey { + constructor() { + throw new ERR_ILLEGAL_CONSTRUCTOR(); + } - const opts = { - ...options, - depth: options.depth == null ? null : options.depth - 1, - }; + [kInspect](depth, options) { + if (depth < 0) + return this; - return `CryptoKey ${inspect({ - type: this[kKeyType], - extractable: this[kExtractable], - algorithm: this[kAlgorithm], - usages: this[kKeyUsages], - }, opts)}`; - } + const opts = { + ...options, + depth: options.depth == null ? null : options.depth - 1, + }; - get [kKeyType]() { - return this[kKeyObject].type; - } + return `CryptoKey ${inspect({ + type: getCryptoKeyType(this), + extractable: getCryptoKeyExtractable(this), + algorithm: getCryptoKeyAlgorithm(this), + usages: getCryptoKeyUsages(this), + }, opts)}`; + } - get type() { - if (!(this instanceof CryptoKey)) - throw new ERR_INVALID_THIS('CryptoKey'); - return this[kKeyType]; - } + get type() { + return getCryptoKeyType(this); + } - get extractable() { - if (!(this instanceof CryptoKey)) - throw new ERR_INVALID_THIS('CryptoKey'); - return this[kExtractable]; - } + get extractable() { + return getCryptoKeyExtractable(this); + } + + get algorithm() { + const slots = getSlots(this); + let cached = slots[kSlotClonedAlgorithm]; + if (cached === undefined) { + cached = cloneAlgorithm(slots[kSlotAlgorithm]); + slots[kSlotClonedAlgorithm] = cached; + } + return cached; + } - get algorithm() { - if (!(this instanceof CryptoKey)) - throw new ERR_INVALID_THIS('CryptoKey'); - if (!this[kCachedAlgorithm]) { - this[kCachedAlgorithm] ??= { ...this[kAlgorithm] }; - this[kCachedAlgorithm].hash &&= { ...this[kCachedAlgorithm].hash }; - this[kCachedAlgorithm].publicExponent &&= new Uint8Array(this[kCachedAlgorithm].publicExponent); + get usages() { + const slots = getSlots(this); + let cached = slots[kSlotClonedUsages]; + if (cached === undefined) { + cached = ArrayFrom(slots[kSlotUsages]); + slots[kSlotClonedUsages] = cached; + } + return cached; } - return this[kCachedAlgorithm]; } - get usages() { - if (!(this instanceof CryptoKey)) - throw new ERR_INVALID_THIS('CryptoKey'); - this[kCachedKeyUsages] ??= ArrayFrom(this[kKeyUsages]); - return this[kCachedKeyUsages]; + class InternalCryptoKey extends NativeCryptoKey { + #slots; + + static getSlots(key) { + if (!key || typeof key !== 'object') + throw new ERR_INVALID_THIS('CryptoKey'); + if (#slots in key) { + const cached = key.#slots; + if (cached !== undefined) return cached; + } + const slots = nativeGetCryptoKeySlots(key); + key.#slots = slots; + return slots; + } } -} + getSlots = InternalCryptoKey.getSlots; + // Hide NativeCryptoKey from user code. + InternalCryptoKey.prototype.constructor = CryptoKey; + ObjectSetPrototypeOf(InternalCryptoKey.prototype, CryptoKey.prototype); + + ObjectDefineProperties(CryptoKey.prototype, { + type: kEnumerableProperty, + extractable: kEnumerableProperty, + algorithm: kEnumerableProperty, + usages: kEnumerableProperty, + [SymbolToStringTag]: { + __proto__: null, + configurable: true, + value: 'CryptoKey', + }, + }); -ObjectDefineProperties(CryptoKey.prototype, { - type: kEnumerableProperty, - extractable: kEnumerableProperty, - algorithm: kEnumerableProperty, - usages: kEnumerableProperty, - [SymbolToStringTag]: { - __proto__: null, - configurable: true, - value: 'CryptoKey', - }, + return [CryptoKey, InternalCryptoKey]; }); +// The helpers below return a CryptoKey's raw internal slot value, +// populating the per-instance cache on first access via a single +// native call. The public `algorithm` / `usages` getters on +// `CryptoKey.prototype` further clone their slot before returning. + /** - * @param {InternalCryptoKey} key - * @param {KeyObject} keyObject - * @param {object} algorithm - * @param {boolean} extractable - * @param {Set} keyUsages + * Returns the value of a CryptoKey's `[[type]]` internal slot. + * @param {CryptoKey} key + * @returns {'secret' | 'public' | 'private'} */ -function defineCryptoKeyProperties( - key, - keyObject, - algorithm, - extractable, - keyUsages, -) { - // Using symbol properties here currently instead of private - // properties because (for now) the performance penalty of - // private fields is still too high. - ObjectDefineProperties(key, { - [kKeyObject]: { - __proto__: null, - value: keyObject, - enumerable: false, - configurable: false, - writable: false, - }, - [kAlgorithm]: { - __proto__: null, - value: algorithm, - enumerable: false, - configurable: false, - writable: false, - }, - [kExtractable]: { - __proto__: null, - value: extractable, - enumerable: false, - configurable: false, - writable: false, - }, - [kKeyUsages]: { - __proto__: null, - value: keyUsages, - enumerable: false, - configurable: false, - writable: false, - }, - }); +function getCryptoKeyType(key) { + return getSlots(key)[kSlotType]; } -// All internal code must use new InternalCryptoKey to create -// CryptoKey instances. The CryptoKey class is exposed to end -// user code but is not permitted to be constructed directly. -// Using markTransferMode also allows the CryptoKey to be -// cloned to Workers. -class InternalCryptoKey { - constructor(keyObject, algorithm, keyUsages, extractable) { - markTransferMode(this, true, false); - // When constructed during transfer the properties get assigned - // in the kDeserialize call. - if (keyObject) { - defineCryptoKeyProperties( - this, - keyObject, - algorithm, - extractable, - keyUsages, - ); - } - } +/** + * Returns the value of a CryptoKey's `[[extractable]]` internal slot. + * @param {CryptoKey} key + * @returns {boolean} + */ +function getCryptoKeyExtractable(key) { + return getSlots(key)[kSlotExtractable]; +} - [kClone]() { - const keyObject = this[kKeyObject]; - const algorithm = this[kAlgorithm]; - const extractable = this[kExtractable]; - const usages = this[kKeyUsages]; +/** + * Returns the CryptoKey's `[[algorithm]]` internal slot, bypassing the + * public `algorithm` getter (which returns a cloned copy). + * @param {CryptoKey} key + * @returns {object} + */ +function getCryptoKeyAlgorithm(key) { + return getSlots(key)[kSlotAlgorithm]; +} - return { - data: { - keyObject, - algorithm, - usages, - extractable, - }, - deserializeInfo: 'internal/crypto/keys:InternalCryptoKey', - }; - } +/** + * Returns the CryptoKey's `[[usages]]` internal slot, bypassing the + * public `usages` getter (which returns a cloned array). + * @param {CryptoKey} key + * @returns {string[]} + */ +function getCryptoKeyUsages(key) { + return getSlots(key)[kSlotUsages]; +} - [kDeserialize]({ keyObject, algorithm, usages, extractable }) { - defineCryptoKeyProperties(this, keyObject, algorithm, extractable, usages); - } +/** + * Returns the KeyObjectHandle wrapping the CryptoKey's underlying + * key material. + * @param {CryptoKey} key + * @returns {KeyObjectHandle} + */ +function getCryptoKeyHandle(key) { + return getSlots(key)[kSlotHandle]; } -InternalCryptoKey.prototype.constructor = CryptoKey; -ObjectSetPrototypeOf(InternalCryptoKey.prototype, CryptoKey.prototype); function isCryptoKey(obj) { - return obj != null && obj[kKeyObject] !== undefined; + if (obj == null || typeof obj !== 'object') + return false; + + try { + getSlots(obj); + return true; + } catch { + return false; + } } function importGenericSecretKey( @@ -952,22 +978,23 @@ function importGenericSecretKey( 'SyntaxError'); } - let keyObject; + let handle; switch (format) { case 'KeyObject': { - keyObject = keyData; + handle = keyData[kHandle]; break; } case 'raw-secret': case 'raw': { - keyObject = createSecretKey(keyData); + handle = new KeyObjectHandle(); + handle.init(kKeyTypeSecret, keyData); break; } default: return undefined; } - return new InternalCryptoKey(keyObject, { name }, keyUsages, false); + return new InternalCryptoKey(handle, { name }, keyUsages, false); } module.exports = { @@ -991,9 +1018,10 @@ module.exports = { PrivateKeyObject, isKeyObject, isCryptoKey, + getCryptoKeyType, + getCryptoKeyExtractable, + getCryptoKeyAlgorithm, + getCryptoKeyUsages, + getCryptoKeyHandle, importGenericSecretKey, - kAlgorithm, - kExtractable, - kKeyType, - kKeyUsages, }; diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index ef0506f3034097..6287d75cfb2e4c 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -92,7 +92,6 @@ const { } = require('internal/util/types'); const kHandle = Symbol('kHandle'); -const kKeyObject = Symbol('kKeyObject'); // This is here because many functions accepted binary strings without // any explicit encoding in older versions of node, and we don't want @@ -824,7 +823,6 @@ module.exports = { getDataViewOrTypedArrayBuffer, getHashes, kHandle, - kKeyObject, setEngine, toBuf, diff --git a/lib/internal/util/comparisons.js b/lib/internal/util/comparisons.js index 694bb570957cb3..f0871cbd1b49ae 100644 --- a/lib/internal/util/comparisons.js +++ b/lib/internal/util/comparisons.js @@ -127,10 +127,11 @@ const { getOwnNonIndexProperties, } = internalBinding('util'); -let kKeyObject; -let kExtractable; -let kAlgorithm; -let kKeyUsages; +let getCryptoKeyHandle; +let getCryptoKeyType; +let getCryptoKeyExtractable; +let getCryptoKeyAlgorithm; +let getCryptoKeyUsages; const kStrict = 2; const kStrictWithoutPrototypes = 3; @@ -413,15 +414,21 @@ function objectComparisonStart(val1, val2, mode, memos) { return false; } } else if (isCryptoKey(val1)) { - if (kKeyObject === undefined) { - kKeyObject = require('internal/crypto/util').kKeyObject; - ({ kExtractable, kAlgorithm, kKeyUsages } = require('internal/crypto/keys')); + if (getCryptoKeyHandle === undefined) { + ({ + getCryptoKeyHandle, + getCryptoKeyType, + getCryptoKeyExtractable, + getCryptoKeyAlgorithm, + getCryptoKeyUsages, + } = require('internal/crypto/keys')); } if (!isCryptoKey(val2) || - val1[kExtractable] !== val2[kExtractable] || - !innerDeepEqual(val1[kAlgorithm], val2[kAlgorithm], mode, memos) || - !innerDeepEqual(val1[kKeyUsages], val2[kKeyUsages], mode, memos) || - !innerDeepEqual(val1[kKeyObject], val2[kKeyObject], mode, memos) + getCryptoKeyType(val1) !== getCryptoKeyType(val2) || + getCryptoKeyExtractable(val1) !== getCryptoKeyExtractable(val2) || + !innerDeepEqual(getCryptoKeyAlgorithm(val1), getCryptoKeyAlgorithm(val2), mode, memos) || + !innerDeepEqual(getCryptoKeyUsages(val1), getCryptoKeyUsages(val2), mode, memos) || + !getCryptoKeyHandle(val1).equals(getCryptoKeyHandle(val2)) ) { return false; } From 02c24566749fe3fc9ed4363289e5869599c9c7bb Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Fri, 24 Apr 2026 14:13:22 +0200 Subject: [PATCH 04/12] lib,crypto: migrate algorithm modules to native CryptoKey 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 --- lib/internal/crypto/aes.js | 85 ++++-------- lib/internal/crypto/argon2.js | 8 +- lib/internal/crypto/cfrg.js | 67 +++++----- lib/internal/crypto/chacha20_poly1305.js | 50 ++----- lib/internal/crypto/diffiehellman.js | 20 +-- lib/internal/crypto/ec.js | 62 ++++----- lib/internal/crypto/hkdf.js | 15 ++- lib/internal/crypto/mac.js | 78 +++-------- lib/internal/crypto/ml_dsa.js | 68 +++++----- lib/internal/crypto/ml_kem.js | 73 ++++++----- lib/internal/crypto/pbkdf2.js | 8 +- lib/internal/crypto/rsa.js | 74 +++++------ lib/internal/crypto/webcrypto.js | 158 ++++++++++++----------- lib/internal/crypto/webcrypto_util.js | 37 ++++-- lib/internal/crypto/webidl.js | 8 +- 15 files changed, 368 insertions(+), 443 deletions(-) diff --git a/lib/internal/crypto/aes.js b/lib/internal/crypto/aes.js index cc443b575da46a..4e59ba49d6bcfa 100644 --- a/lib/internal/crypto/aes.js +++ b/lib/internal/crypto/aes.js @@ -8,10 +8,7 @@ const { const { AESCipherJob, - KeyObjectHandle, kCryptoJobAsync, - kKeyFormatJWK, - kKeyTypeSecret, kKeyVariantAES_CTR_128, kKeyVariantAES_CBC_128, kKeyVariantAES_GCM_128, @@ -27,37 +24,31 @@ const { kKeyVariantAES_GCM_256, kKeyVariantAES_KW_256, kKeyVariantAES_OCB_256, + SecretKeyGenJob, } = internalBinding('crypto'); const { hasAnyNotIn, jobPromise, kHandle, - kKeyObject, } = require('internal/crypto/util'); const { lazyDOMException, - promisify, } = require('internal/util'); const { InternalCryptoKey, - SecretKeyObject, - createSecretKey, - kAlgorithm, + getCryptoKeyAlgorithm, + getCryptoKeyHandle, } = require('internal/crypto/keys'); const { + importJwkSecretKey, + importSecretKey, validateJwk, } = require('internal/crypto/webcrypto_util'); -const { - generateKey: _generateKey, -} = require('internal/crypto/keygen'); - -const generateKey = promisify(_generateKey); - function getAlgorithmName(name, length) { switch (name) { case 'AES-CBC': return `A${length}CBC`; @@ -117,9 +108,9 @@ function asyncAesCtrCipher(mode, key, data, algorithm) { return jobPromise(() => new AESCipherJob( kCryptoJobAsync, mode, - key[kKeyObject][kHandle], + getCryptoKeyHandle(key), data, - getVariant('AES-CTR', key[kAlgorithm].length), + getVariant('AES-CTR', getCryptoKeyAlgorithm(key).length), algorithm.counter, algorithm.length)); } @@ -128,9 +119,9 @@ function asyncAesCbcCipher(mode, key, data, algorithm) { return jobPromise(() => new AESCipherJob( kCryptoJobAsync, mode, - key[kKeyObject][kHandle], + getCryptoKeyHandle(key), data, - getVariant('AES-CBC', key[kAlgorithm].length), + getVariant('AES-CBC', getCryptoKeyAlgorithm(key).length), algorithm.iv)); } @@ -138,9 +129,9 @@ function asyncAesKwCipher(mode, key, data) { return jobPromise(() => new AESCipherJob( kCryptoJobAsync, mode, - key[kKeyObject][kHandle], + getCryptoKeyHandle(key), data, - getVariant('AES-KW', key[kAlgorithm].length))); + getVariant('AES-KW', getCryptoKeyAlgorithm(key).length))); } function asyncAesGcmCipher(mode, key, data, algorithm) { @@ -150,9 +141,9 @@ function asyncAesGcmCipher(mode, key, data, algorithm) { return jobPromise(() => new AESCipherJob( kCryptoJobAsync, mode, - key[kKeyObject][kHandle], + getCryptoKeyHandle(key), data, - getVariant('AES-GCM', key[kAlgorithm].length), + getVariant('AES-GCM', getCryptoKeyAlgorithm(key).length), algorithm.iv, tagByteLength, algorithm.additionalData)); @@ -165,9 +156,9 @@ function asyncAesOcbCipher(mode, key, data, algorithm) { return jobPromise(() => new AESCipherJob( kCryptoJobAsync, mode, - key[kKeyObject][kHandle], + getCryptoKeyHandle(key), data, - getVariant('AES-OCB', key.algorithm.length), + getVariant('AES-OCB', getCryptoKeyAlgorithm(key).length), algorithm.iv, tagByteLength, algorithm.additionalData)); @@ -197,18 +188,10 @@ async function aesGenerateKey(algorithm, extractable, keyUsages) { 'SyntaxError'); } - let key; - try { - key = await generateKey('aes', { length }); - } catch (err) { - throw lazyDOMException( - 'The operation failed for an operation-specific reason' + - `[${err.message}]`, - { name: 'OperationError', cause: err }); - } + const handle = await jobPromise(() => new SecretKeyGenJob(kCryptoJobAsync, length)); return new InternalCryptoKey( - key, + handle, { name, length }, ArrayFrom(usagesSet), extractable); @@ -232,12 +215,13 @@ function aesImportKey( 'SyntaxError'); } - let keyObject; + let handle; let length; switch (format) { case 'KeyObject': { - validateKeyLength(keyData.symmetricKeySize * 8); - keyObject = keyData; + length = keyData.symmetricKeySize * 8; + validateKeyLength(length); + handle = keyData[kHandle]; break; } case 'raw-secret': @@ -245,45 +229,30 @@ function aesImportKey( if (format === 'raw' && name === 'AES-OCB') { return undefined; } - validateKeyLength(keyData.byteLength * 8); - keyObject = createSecretKey(keyData); + length = keyData.byteLength * 8; + validateKeyLength(length); + handle = importSecretKey(keyData); break; } case 'jwk': { validateJwk(keyData, 'oct', extractable, usagesSet, 'enc'); - - const handle = new KeyObjectHandle(); - try { - handle.init(kKeyTypeSecret, keyData, kKeyFormatJWK, null, null); - } catch (err) { - throw lazyDOMException( - 'Invalid keyData', { name: 'DataError', cause: err }); - } - - ({ length } = handle.keyDetail({ })); + handle = importJwkSecretKey(keyData); + length = handle.getSymmetricKeySize() * 8; validateKeyLength(length); - if (keyData.alg !== undefined) { if (keyData.alg !== getAlgorithmName(algorithm.name, length)) throw lazyDOMException( 'JWK "alg" does not match the requested algorithm', 'DataError'); } - - keyObject = new SecretKeyObject(handle); break; } default: return undefined; } - if (length === undefined) { - ({ length } = keyObject[kHandle].keyDetail({ })); - validateKeyLength(length); - } - return new InternalCryptoKey( - keyObject, + handle, { name, length }, keyUsages, extractable); diff --git a/lib/internal/crypto/argon2.js b/lib/internal/crypto/argon2.js index 1d2b8b7cac91e9..6110c55c16dfb8 100644 --- a/lib/internal/crypto/argon2.js +++ b/lib/internal/crypto/argon2.js @@ -24,9 +24,12 @@ const { promisify, } = require('internal/util'); +const { + getCryptoKeyHandle, +} = require('internal/crypto/keys'); + const { getArrayBufferOrView, - kKeyObject, } = require('internal/crypto/util'); const { @@ -216,7 +219,8 @@ async function argon2DeriveBits(algorithm, baseKey, length) { result = await argon2Promise( StringPrototypeToLowerCase(algorithm.name), { - message: baseKey[kKeyObject].export(), + // TODO(panva): call the job directly without needing to re-export the handle + message: getCryptoKeyHandle(baseKey).export(), nonce: algorithm.nonce, parallelism: algorithm.parallelism, tagLength: length / 8, diff --git a/lib/internal/crypto/cfrg.js b/lib/internal/crypto/cfrg.js index 58e8fb02943b78..e650014acc4500 100644 --- a/lib/internal/crypto/cfrg.js +++ b/lib/internal/crypto/cfrg.js @@ -16,6 +16,11 @@ const { kWebCryptoKeyFormatPKCS8, kWebCryptoKeyFormatRaw, kWebCryptoKeyFormatSPKI, + NidKeyPairGenJob, + EVP_PKEY_ED25519, + EVP_PKEY_ED448, + EVP_PKEY_X25519, + EVP_PKEY_X448, } = internalBinding('crypto'); const { @@ -23,21 +28,16 @@ const { hasAnyNotIn, jobPromise, kHandle, - kKeyObject, } = require('internal/crypto/util'); const { lazyDOMException, - promisify, } = require('internal/util'); const { - generateKeyPair: _generateKeyPair, -} = require('internal/crypto/keygen'); - -const { + getCryptoKeyHandle, + getCryptoKeyType, InternalCryptoKey, - kKeyType, } = require('internal/crypto/keys'); const { @@ -47,8 +47,6 @@ const { validateJwk, } = require('internal/crypto/webcrypto_util'); -const generateKeyPair = promisify(_generateKeyPair); - function verifyAcceptableCfrgKeyUse(name, isPublic, usages) { let checkSet; switch (name) { @@ -97,30 +95,23 @@ async function cfrgGenerateKey(algorithm, extractable, keyUsages) { } break; } - let genKeyType; + let nid; switch (name) { case 'Ed25519': - genKeyType = 'ed25519'; + nid = EVP_PKEY_ED25519; break; case 'Ed448': - genKeyType = 'ed448'; + nid = EVP_PKEY_ED448; break; case 'X25519': - genKeyType = 'x25519'; + nid = EVP_PKEY_X25519; break; case 'X448': - genKeyType = 'x448'; + nid = EVP_PKEY_X448; break; } - let keyPair; - try { - keyPair = await generateKeyPair(genKeyType); - } catch (err) { - throw lazyDOMException( - 'The operation failed for an operation-specific reason', - { name: 'OperationError', cause: err }); - } + const handles = await jobPromise(() => new NidKeyPairGenJob(kCryptoJobAsync, nid)); let publicUsages; let privateUsages; @@ -143,14 +134,14 @@ async function cfrgGenerateKey(algorithm, extractable, keyUsages) { const publicKey = new InternalCryptoKey( - keyPair.publicKey, + handles[0], keyAlgorithm, publicUsages, true); const privateKey = new InternalCryptoKey( - keyPair.privateKey, + handles[1], keyAlgorithm, privateUsages, extractable); @@ -162,17 +153,17 @@ function cfrgExportKey(key, format) { try { switch (format) { case kWebCryptoKeyFormatRaw: { - const handle = key[kKeyObject][kHandle]; + const handle = getCryptoKeyHandle(key); return TypedArrayPrototypeGetBuffer( - key[kKeyType] === 'private' ? handle.rawPrivateKey() : handle.rawPublicKey()); + getCryptoKeyType(key) === 'private' ? handle.rawPrivateKey() : handle.rawPublicKey()); } case kWebCryptoKeyFormatSPKI: { return TypedArrayPrototypeGetBuffer( - key[kKeyObject][kHandle].export(kKeyFormatDER, kWebCryptoKeyFormatSPKI)); + getCryptoKeyHandle(key).export(kKeyFormatDER, kWebCryptoKeyFormatSPKI)); } case kWebCryptoKeyFormatPKCS8: { return TypedArrayPrototypeGetBuffer( - key[kKeyObject][kHandle].export(kKeyFormatDER, kWebCryptoKeyFormatPKCS8, null, null)); + getCryptoKeyHandle(key).export(kKeyFormatDER, kWebCryptoKeyFormatPKCS8, null, null)); } default: return undefined; @@ -192,22 +183,22 @@ function cfrgImportKey( keyUsages) { const { name } = algorithm; - let keyObject; + let handle; const usagesSet = new SafeSet(keyUsages); switch (format) { case 'KeyObject': { verifyAcceptableCfrgKeyUse(name, keyData.type === 'public', usagesSet); - keyObject = keyData; + handle = keyData[kHandle]; break; } case 'spki': { verifyAcceptableCfrgKeyUse(name, true, usagesSet); - keyObject = importDerKey(keyData, true); + handle = importDerKey(keyData, true); break; } case 'pkcs8': { verifyAcceptableCfrgKeyUse(name, false, usagesSet); - keyObject = importDerKey(keyData, false); + handle = importDerKey(keyData, false); break; } case 'jwk': { @@ -226,24 +217,24 @@ function cfrgImportKey( const isPublic = keyData.d === undefined; verifyAcceptableCfrgKeyUse(name, isPublic, usagesSet); - keyObject = importJwkKey(isPublic, keyData); + handle = importJwkKey(isPublic, keyData); break; } case 'raw': { verifyAcceptableCfrgKeyUse(name, true, usagesSet); - keyObject = importRawKey(true, keyData, kKeyFormatRawPublic, name); + handle = importRawKey(true, keyData, kKeyFormatRawPublic, name); break; } default: return undefined; } - if (keyObject.asymmetricKeyType !== StringPrototypeToLowerCase(name)) { + if (handle.getAsymmetricKeyType() !== StringPrototypeToLowerCase(name)) { throw lazyDOMException('Invalid key type', 'DataError'); } return new InternalCryptoKey( - keyObject, + handle, { name }, keyUsages, extractable); @@ -253,13 +244,13 @@ async function eddsaSignVerify(key, data, algorithm, signature) { const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify; const type = mode === kSignJobModeSign ? 'private' : 'public'; - if (key[kKeyType] !== type) + if (getCryptoKeyType(key) !== type) throw lazyDOMException(`Key must be a ${type} key`, 'InvalidAccessError'); return await jobPromise(() => new SignJob( kCryptoJobAsync, mode, - key[kKeyObject][kHandle], + getCryptoKeyHandle(key), undefined, undefined, undefined, diff --git a/lib/internal/crypto/chacha20_poly1305.js b/lib/internal/crypto/chacha20_poly1305.js index 2230097c4c5c9f..3c787577688bde 100644 --- a/lib/internal/crypto/chacha20_poly1305.js +++ b/lib/internal/crypto/chacha20_poly1305.js @@ -7,40 +7,31 @@ const { const { ChaCha20Poly1305CipherJob, - KeyObjectHandle, + SecretKeyGenJob, kCryptoJobAsync, - kKeyFormatJWK, - kKeyTypeSecret, } = internalBinding('crypto'); const { hasAnyNotIn, jobPromise, kHandle, - kKeyObject, } = require('internal/crypto/util'); const { lazyDOMException, - promisify, } = require('internal/util'); const { InternalCryptoKey, - SecretKeyObject, - createSecretKey, + getCryptoKeyHandle, } = require('internal/crypto/keys'); const { + importJwkSecretKey, + importSecretKey, validateJwk, } = require('internal/crypto/webcrypto_util'); -const { - randomBytes: _randomBytes, -} = require('internal/crypto/random'); - -const randomBytes = promisify(_randomBytes); - function validateKeyLength(length) { if (length !== 256) throw lazyDOMException('Invalid key length', 'DataError'); @@ -50,7 +41,7 @@ function c20pCipher(mode, key, data, algorithm) { return jobPromise(() => new ChaCha20Poly1305CipherJob( kCryptoJobAsync, mode, - key[kKeyObject][kHandle], + getCryptoKeyHandle(key), data, algorithm.iv, algorithm.additionalData)); @@ -68,18 +59,10 @@ async function c20pGenerateKey(algorithm, extractable, keyUsages) { 'SyntaxError'); } - let keyData; - try { - keyData = await randomBytes(32); - } catch (err) { - throw lazyDOMException( - 'The operation failed for an operation-specific reason' + - `[${err.message}]`, - { name: 'OperationError', cause: err }); - } + const handle = await jobPromise(() => new SecretKeyGenJob(kCryptoJobAsync, 256)); return new InternalCryptoKey( - createSecretKey(keyData), + handle, { name }, ArrayFrom(usagesSet), extractable); @@ -101,26 +84,20 @@ function c20pImportKey( 'SyntaxError'); } - let keyObject; + let handle; switch (format) { case 'KeyObject': { - keyObject = keyData; + handle = keyData[kHandle]; break; } case 'raw-secret': { - keyObject = createSecretKey(keyData); + handle = importSecretKey(keyData); break; } case 'jwk': { validateJwk(keyData, 'oct', extractable, usagesSet, 'enc'); - const handle = new KeyObjectHandle(); - try { - handle.init(kKeyTypeSecret, keyData, kKeyFormatJWK, null, null); - } catch (err) { - throw lazyDOMException( - 'Invalid keyData', { name: 'DataError', cause: err }); - } + handle = importJwkSecretKey(keyData); if (keyData.alg !== undefined && keyData.alg !== 'C20P') { throw lazyDOMException( @@ -128,17 +105,16 @@ function c20pImportKey( 'DataError'); } - keyObject = new SecretKeyObject(handle); break; } default: return undefined; } - validateKeyLength(keyObject.symmetricKeySize * 8); + validateKeyLength(handle.getSymmetricKeySize() * 8); return new InternalCryptoKey( - keyObject, + handle, { name }, keyUsages, extractable); diff --git a/lib/internal/crypto/diffiehellman.js b/lib/internal/crypto/diffiehellman.js index 24f9c5b36e710c..81006c34b34758 100644 --- a/lib/internal/crypto/diffiehellman.js +++ b/lib/internal/crypto/diffiehellman.js @@ -47,8 +47,9 @@ const { } = require('internal/util'); const { - kAlgorithm, - kKeyType, + getCryptoKeyAlgorithm, + getCryptoKeyHandle, + getCryptoKeyType, preparePrivateKey, preparePublicOrPrivateKey, } = require('internal/crypto/keys'); @@ -58,7 +59,6 @@ const { jobPromise, toBuf, kHandle, - kKeyObject, } = require('internal/crypto/util'); const { @@ -329,32 +329,34 @@ let masks; async function ecdhDeriveBits(algorithm, baseKey, length) { const { 'public': key } = algorithm; - if (baseKey[kKeyType] !== 'private') { + if (getCryptoKeyType(baseKey) !== 'private') { throw lazyDOMException( 'baseKey must be a private key', 'InvalidAccessError'); } - if (key[kAlgorithm].name !== baseKey[kAlgorithm].name) { + const keyAlgorithm = getCryptoKeyAlgorithm(key); + const baseKeyAlgorithm = getCryptoKeyAlgorithm(baseKey); + if (keyAlgorithm.name !== baseKeyAlgorithm.name) { throw lazyDOMException( 'The public and private keys must be of the same type', 'InvalidAccessError'); } if ( - key[kAlgorithm].name === 'ECDH' && - key[kAlgorithm].namedCurve !== baseKey[kAlgorithm].namedCurve + keyAlgorithm.name === 'ECDH' && + keyAlgorithm.namedCurve !== baseKeyAlgorithm.namedCurve ) { throw lazyDOMException('Named curve mismatch', 'InvalidAccessError'); } const bits = await jobPromise(() => new DHBitsJob( kCryptoJobAsync, - key[kKeyObject][kHandle], + getCryptoKeyHandle(key), undefined, undefined, undefined, undefined, - baseKey[kKeyObject][kHandle], + getCryptoKeyHandle(baseKey), undefined, undefined, undefined, diff --git a/lib/internal/crypto/ec.js b/lib/internal/crypto/ec.js index 3b0b35e8f0f822..82ce0f13d2557a 100644 --- a/lib/internal/crypto/ec.js +++ b/lib/internal/crypto/ec.js @@ -7,6 +7,7 @@ const { } = primordials; const { + EcKeyPairGenJob, KeyObjectHandle, SignJob, kCryptoJobAsync, @@ -33,23 +34,18 @@ const { jobPromise, normalizeHashName, kHandle, - kKeyObject, kNamedCurveAliases, } = require('internal/crypto/util'); const { lazyDOMException, - promisify, } = require('internal/util'); -const { - generateKeyPair: _generateKeyPair, -} = require('internal/crypto/keygen'); - const { InternalCryptoKey, - kAlgorithm, - kKeyType, + getCryptoKeyAlgorithm, + getCryptoKeyHandle, + getCryptoKeyType, } = require('internal/crypto/keys'); const { @@ -59,8 +55,6 @@ const { validateJwk, } = require('internal/crypto/webcrypto_util'); -const generateKeyPair = promisify(_generateKeyPair); - function verifyAcceptableEcKeyUse(name, isPublic, usages) { let checkSet; switch (name) { @@ -102,14 +96,8 @@ async function ecGenerateKey(algorithm, extractable, keyUsages) { // Fall through } - let keyPair; - try { - keyPair = await generateKeyPair('ec', { namedCurve }); - } catch (err) { - throw lazyDOMException( - 'The operation failed for an operation-specific reason', - { name: 'OperationError', cause: err }); - } + const handles = await jobPromise(() => new EcKeyPairGenJob( + kCryptoJobAsync, namedCurve)); let publicUsages; let privateUsages; @@ -128,14 +116,14 @@ async function ecGenerateKey(algorithm, extractable, keyUsages) { const publicKey = new InternalCryptoKey( - keyPair.publicKey, + handles[0], keyAlgorithm, publicUsages, true); const privateKey = new InternalCryptoKey( - keyPair.privateKey, + handles[1], keyAlgorithm, privateUsages, extractable); @@ -145,7 +133,7 @@ async function ecGenerateKey(algorithm, extractable, keyUsages) { function ecExportKey(key, format) { try { - const handle = key[kKeyObject][kHandle]; + const handle = getCryptoKeyHandle(key); switch (format) { case kWebCryptoKeyFormatRaw: { return TypedArrayPrototypeGetBuffer( @@ -161,13 +149,14 @@ function ecExportKey(key, format) { // P-384: 120 = 23 bytes of SPKI ASN.1 + 97-byte uncompressed point. // P-521: 158 = 25 bytes of SPKI ASN.1 + 133-byte uncompressed point. // Difference in initial SPKI ASN.1 is caused by OIDs and length encoding. + const { namedCurve } = getCryptoKeyAlgorithm(key); if (TypedArrayPrototypeGetByteLength(spki) !== { '__proto__': null, 'P-256': 91, 'P-384': 120, 'P-521': 158, - }[key[kAlgorithm].namedCurve]) { + }[namedCurve]) { const raw = handle.exportECPublicRaw(POINT_CONVERSION_UNCOMPRESSED); const tmp = new KeyObjectHandle(); tmp.init(kKeyTypePublic, raw, kKeyFormatRawPublic, - 'ec', null, key[kAlgorithm].namedCurve); + 'ec', null, namedCurve); spki = tmp.export(kKeyFormatDER, kWebCryptoKeyFormatSPKI); } return TypedArrayPrototypeGetBuffer(spki); @@ -195,22 +184,22 @@ function ecImportKey( ) { const { name, namedCurve } = algorithm; - let keyObject; + let handle; const usagesSet = new SafeSet(keyUsages); switch (format) { case 'KeyObject': { verifyAcceptableEcKeyUse(name, keyData.type === 'public', usagesSet); - keyObject = keyData; + handle = keyData[kHandle]; break; } case 'spki': { verifyAcceptableEcKeyUse(name, true, usagesSet); - keyObject = importDerKey(keyData, true); + handle = importDerKey(keyData, true); break; } case 'pkcs8': { verifyAcceptableEcKeyUse(name, false, usagesSet); - keyObject = importDerKey(keyData, false); + handle = importDerKey(keyData, false); break; } case 'jwk': { @@ -237,12 +226,12 @@ function ecImportKey( const isPublic = keyData.d === undefined; verifyAcceptableEcKeyUse(name, isPublic, usagesSet); - keyObject = importJwkKey(isPublic, keyData); + handle = importJwkKey(isPublic, keyData); break; } case 'raw': { verifyAcceptableEcKeyUse(name, true, usagesSet); - keyObject = importRawKey(true, keyData, kKeyFormatRawPublic, 'ec', namedCurve); + handle = importRawKey(true, keyData, kKeyFormatRawPublic, 'ec', namedCurve); break; } default: @@ -253,23 +242,20 @@ function ecImportKey( case 'ECDSA': // Fall through case 'ECDH': - if (keyObject.asymmetricKeyType !== 'ec') + if (handle.getAsymmetricKeyType() !== 'ec') throw lazyDOMException('Invalid key type', 'DataError'); break; } - if (!keyObject[kHandle].checkEcKeyData()) { + if (!handle.checkEcKeyData()) { throw lazyDOMException('Invalid keyData', 'DataError'); } - const { - namedCurve: checkNamedCurve, - } = keyObject[kHandle].keyDetail({}); - if (kNamedCurveAliases[namedCurve] !== checkNamedCurve) + if (kNamedCurveAliases[namedCurve] !== handle.keyDetail({}).namedCurve) throw lazyDOMException('Named curve mismatch', 'DataError'); return new InternalCryptoKey( - keyObject, + handle, { name, namedCurve }, keyUsages, extractable); @@ -279,7 +265,7 @@ async function ecdsaSignVerify(key, data, { name, hash }, signature) { const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify; const type = mode === kSignJobModeSign ? 'private' : 'public'; - if (key[kKeyType] !== type) + if (getCryptoKeyType(key) !== type) throw lazyDOMException(`Key must be a ${type} key`, 'InvalidAccessError'); const hashname = normalizeHashName(hash.name); @@ -287,7 +273,7 @@ async function ecdsaSignVerify(key, data, { name, hash }, signature) { return await jobPromise(() => new SignJob( kCryptoJobAsync, mode, - key[kKeyObject][kHandle], + getCryptoKeyHandle(key), undefined, undefined, undefined, diff --git a/lib/internal/crypto/hkdf.js b/lib/internal/crypto/hkdf.js index 1a5e9ccd06813e..4203e3ee21c701 100644 --- a/lib/internal/crypto/hkdf.js +++ b/lib/internal/crypto/hkdf.js @@ -20,20 +20,20 @@ const { const { kMaxLength } = require('buffer'); const { + jobPromise, normalizeHashName, toBuf, validateByteSource, - kKeyObject, } = require('internal/crypto/util'); const { createSecretKey, + getCryptoKeyHandle, isKeyObject, } = require('internal/crypto/keys'); const { lazyDOMException, - promisify, } = require('internal/util'); const { @@ -137,7 +137,6 @@ function hkdfSync(hash, key, salt, info, length) { return bits; } -const hkdfPromise = promisify(hkdf); function validateHkdfDeriveBitsLength(length) { if (length === null) throw lazyDOMException('length cannot be null', 'OperationError'); @@ -156,9 +155,13 @@ async function hkdfDeriveBits(algorithm, baseKey, length) { return new ArrayBuffer(0); try { - return await hkdfPromise( - normalizeHashName(hash.name), baseKey[kKeyObject], salt, info, length / 8, - ); + return await jobPromise(() => new HKDFJob( + kCryptoJobAsync, + normalizeHashName(hash.name), + getCryptoKeyHandle(baseKey), + salt, + info, + length / 8)); } catch (err) { throw lazyDOMException( 'The operation failed for an operation-specific reason', diff --git a/lib/internal/crypto/mac.js b/lib/internal/crypto/mac.js index 1689d19d4a0660..5886f6b75f915d 100644 --- a/lib/internal/crypto/mac.js +++ b/lib/internal/crypto/mac.js @@ -8,13 +8,11 @@ const { const { HmacJob, - KeyObjectHandle, KmacJob, kCryptoJobAsync, - kKeyFormatJWK, - kKeyTypeSecret, kSignJobModeSign, kSignJobModeVerify, + SecretKeyGenJob, } = internalBinding('crypto'); const { @@ -23,38 +21,24 @@ const { jobPromise, normalizeHashName, kHandle, - kKeyObject, } = require('internal/crypto/util'); const { lazyDOMException, - promisify, } = require('internal/util'); -const { - generateKey: _generateKey, -} = require('internal/crypto/keygen'); - - -const { - randomBytes: _randomBytes, -} = require('internal/crypto/random'); - -const randomBytes = promisify(_randomBytes); - const { InternalCryptoKey, - SecretKeyObject, - createSecretKey, - kAlgorithm, + getCryptoKeyAlgorithm, + getCryptoKeyHandle, } = require('internal/crypto/keys'); const { + importJwkSecretKey, + importSecretKey, validateJwk, } = require('internal/crypto/webcrypto_util'); -const generateKey = promisify(_generateKey); - async function hmacGenerateKey(algorithm, extractable, keyUsages) { const { hash, @@ -69,17 +53,10 @@ async function hmacGenerateKey(algorithm, extractable, keyUsages) { 'SyntaxError'); } - let key; - try { - key = await generateKey('hmac', { length }); - } catch (err) { - throw lazyDOMException( - 'The operation failed for an operation-specific reason', - { name: 'OperationError', cause: err }); - } + const handle = await jobPromise(() => new SecretKeyGenJob(kCryptoJobAsync, length)); return new InternalCryptoKey( - key, + handle, { name, length, hash }, ArrayFrom(usageSet), extractable); @@ -102,18 +79,10 @@ async function kmacGenerateKey(algorithm, extractable, keyUsages) { 'SyntaxError'); } - let keyData; - try { - keyData = await randomBytes(length / 8); - } catch (err) { - throw lazyDOMException( - 'The operation failed for an operation-specific reason' + - `[${err.message}]`, - { name: 'OperationError', cause: err }); - } + const handle = await jobPromise(() => new SecretKeyGenJob(kCryptoJobAsync, length)); return new InternalCryptoKey( - createSecretKey(keyData), + handle, { name, length }, ArrayFrom(usageSet), extractable); @@ -133,10 +102,12 @@ function macImportKey( `Unsupported key usage for ${algorithm.name} key`, 'SyntaxError'); } - let keyObject; + let handle; + let length; switch (format) { case 'KeyObject': { - keyObject = keyData; + length = keyData.symmetricKeySize * 8; + handle = keyData[kHandle]; break; } case 'raw-secret': @@ -144,7 +115,8 @@ function macImportKey( if (format === 'raw' && !isHmac) { return undefined; } - keyObject = createSecretKey(keyData); + length = keyData.byteLength * 8; + handle = importSecretKey(keyData); break; } case 'jwk': { @@ -160,22 +132,14 @@ function macImportKey( 'DataError'); } - const handle = new KeyObjectHandle(); - try { - handle.init(kKeyTypeSecret, keyData, kKeyFormatJWK, null, null); - } catch (err) { - throw lazyDOMException( - 'Invalid keyData', { name: 'DataError', cause: err }); - } - keyObject = new SecretKeyObject(handle); + handle = importJwkSecretKey(keyData); + length = handle.getSymmetricKeySize() * 8; break; } default: return undefined; } - const { length } = keyObject[kHandle].keyDetail({}); - if (length === 0) throw lazyDOMException('Zero-length key is not supported', 'DataError'); @@ -194,7 +158,7 @@ function macImportKey( } return new InternalCryptoKey( - keyObject, + handle, algorithmObject, keyUsages, extractable); @@ -205,8 +169,8 @@ function hmacSignVerify(key, data, algorithm, signature) { return jobPromise(() => new HmacJob( kCryptoJobAsync, mode, - normalizeHashName(key[kAlgorithm].hash.name), - key[kKeyObject][kHandle], + normalizeHashName(getCryptoKeyAlgorithm(key).hash.name), + getCryptoKeyHandle(key), data, signature)); } @@ -216,7 +180,7 @@ function kmacSignVerify(key, data, algorithm, signature) { return jobPromise(() => new KmacJob( kCryptoJobAsync, mode, - key[kKeyObject][kHandle], + getCryptoKeyHandle(key), algorithm.name, algorithm.customization, algorithm.outputLength / 8, diff --git a/lib/internal/crypto/ml_dsa.js b/lib/internal/crypto/ml_dsa.js index e6c70db034275f..750c4b5bc2ad45 100644 --- a/lib/internal/crypto/ml_dsa.js +++ b/lib/internal/crypto/ml_dsa.js @@ -18,6 +18,10 @@ const { kWebCryptoKeyFormatRaw, kWebCryptoKeyFormatPKCS8, kWebCryptoKeyFormatSPKI, + NidKeyPairGenJob, + EVP_PKEY_ML_DSA_44, + EVP_PKEY_ML_DSA_65, + EVP_PKEY_ML_DSA_87, } = internalBinding('crypto'); const { @@ -25,21 +29,16 @@ const { hasAnyNotIn, jobPromise, kHandle, - kKeyObject, } = require('internal/crypto/util'); const { lazyDOMException, - promisify, } = require('internal/util'); const { - generateKeyPair: _generateKeyPair, -} = require('internal/crypto/keygen'); - -const { + getCryptoKeyHandle, + getCryptoKeyType, InternalCryptoKey, - kKeyType, } = require('internal/crypto/keys'); const { @@ -49,8 +48,6 @@ const { validateJwk, } = require('internal/crypto/webcrypto_util'); -const generateKeyPair = promisify(_generateKeyPair); - function verifyAcceptableMlDsaKeyUse(name, isPublic, usages) { const checkSet = isPublic ? ['verify'] : ['sign']; if (hasAnyNotIn(usages, checkSet)) { @@ -70,15 +67,20 @@ async function mlDsaGenerateKey(algorithm, extractable, keyUsages) { 'SyntaxError'); } - let keyPair; - try { - keyPair = await generateKeyPair(name.toLowerCase()); - } catch (err) { - throw lazyDOMException( - 'The operation failed for an operation-specific reason', - { name: 'OperationError', cause: err }); + let nid; + switch (name) { + case 'ML-DSA-44': + nid = EVP_PKEY_ML_DSA_44; + break; + case 'ML-DSA-65': + nid = EVP_PKEY_ML_DSA_65; + break; + case 'ML-DSA-87': + nid = EVP_PKEY_ML_DSA_87; + break; } + const handles = await jobPromise(() => new NidKeyPairGenJob(kCryptoJobAsync, nid)); const publicUsages = getUsagesUnion(usageSet, 'verify'); const privateUsages = getUsagesUnion(usageSet, 'sign'); @@ -86,14 +88,14 @@ async function mlDsaGenerateKey(algorithm, extractable, keyUsages) { const publicKey = new InternalCryptoKey( - keyPair.publicKey, + handles[0], keyAlgorithm, publicUsages, true); const privateKey = new InternalCryptoKey( - keyPair.privateKey, + handles[1], keyAlgorithm, privateUsages, extractable); @@ -103,17 +105,19 @@ async function mlDsaGenerateKey(algorithm, extractable, keyUsages) { function mlDsaExportKey(key, format) { try { + const handle = getCryptoKeyHandle(key); switch (format) { case kWebCryptoKeyFormatRaw: { - const handle = key[kKeyObject][kHandle]; return TypedArrayPrototypeGetBuffer( - key[kKeyType] === 'private' ? handle.rawSeed() : handle.rawPublicKey()); + getCryptoKeyType(key) === 'private' ? handle.rawSeed() : handle.rawPublicKey()); } case kWebCryptoKeyFormatSPKI: { - return TypedArrayPrototypeGetBuffer(key[kKeyObject][kHandle].export(kKeyFormatDER, kWebCryptoKeyFormatSPKI)); + return TypedArrayPrototypeGetBuffer( + handle.export(kKeyFormatDER, kWebCryptoKeyFormatSPKI)); } case kWebCryptoKeyFormatPKCS8: { - const pkcs8 = key[kKeyObject][kHandle].export(kKeyFormatDER, kWebCryptoKeyFormatPKCS8, null, null); + const pkcs8 = handle.export( + kKeyFormatDER, kWebCryptoKeyFormatPKCS8, null, null); // Edge case only possible when user creates a seedless KeyObject // first and converts it with KeyObject.prototype.toCryptoKey. // 54 = 22 bytes of PKCS#8 ASN.1 + 32-byte seed. @@ -142,17 +146,17 @@ function mlDsaImportKey( keyUsages) { const { name } = algorithm; - let keyObject; + let handle; const usagesSet = new SafeSet(keyUsages); switch (format) { case 'KeyObject': { verifyAcceptableMlDsaKeyUse(name, keyData.type === 'public', usagesSet); - keyObject = keyData; + handle = keyData[kHandle]; break; } case 'spki': { verifyAcceptableMlDsaKeyUse(name, true, usagesSet); - keyObject = importDerKey(keyData, true); + handle = importDerKey(keyData, true); break; } case 'pkcs8': { @@ -170,7 +174,7 @@ function mlDsaImportKey( 'NotSupportedError'); } - keyObject = importDerKey(keyData, false); + handle = importDerKey(keyData, false); break; } case 'jwk': { @@ -182,26 +186,26 @@ function mlDsaImportKey( const isPublic = keyData.priv === undefined; verifyAcceptableMlDsaKeyUse(name, isPublic, usagesSet); - keyObject = importJwkKey(isPublic, keyData); + handle = importJwkKey(isPublic, keyData); break; } case 'raw-public': case 'raw-seed': { const isPublic = format === 'raw-public'; verifyAcceptableMlDsaKeyUse(name, isPublic, usagesSet); - keyObject = importRawKey(isPublic, keyData, isPublic ? kKeyFormatRawPublic : kKeyFormatRawSeed, name); + handle = importRawKey(isPublic, keyData, isPublic ? kKeyFormatRawPublic : kKeyFormatRawSeed, name); break; } default: return undefined; } - if (keyObject.asymmetricKeyType !== StringPrototypeToLowerCase(name)) { + if (handle.getAsymmetricKeyType() !== StringPrototypeToLowerCase(name)) { throw lazyDOMException('Invalid key type', 'DataError'); } return new InternalCryptoKey( - keyObject, + handle, { name }, keyUsages, extractable); @@ -211,13 +215,13 @@ async function mlDsaSignVerify(key, data, algorithm, signature) { const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify; const type = mode === kSignJobModeSign ? 'private' : 'public'; - if (key[kKeyType] !== type) + if (getCryptoKeyType(key) !== type) throw lazyDOMException(`Key must be a ${type} key`, 'InvalidAccessError'); return await jobPromise(() => new SignJob( kCryptoJobAsync, mode, - key[kKeyObject][kHandle], + getCryptoKeyHandle(key), undefined, undefined, undefined, diff --git a/lib/internal/crypto/ml_kem.js b/lib/internal/crypto/ml_kem.js index 6c665c20c0c583..39a6a07d09dc04 100644 --- a/lib/internal/crypto/ml_kem.js +++ b/lib/internal/crypto/ml_kem.js @@ -18,27 +18,27 @@ const { kWebCryptoKeyFormatPKCS8, kWebCryptoKeyFormatRaw, kWebCryptoKeyFormatSPKI, + NidKeyPairGenJob, + EVP_PKEY_ML_KEM_512, + EVP_PKEY_ML_KEM_768, + EVP_PKEY_ML_KEM_1024, } = internalBinding('crypto'); const { getUsagesUnion, hasAnyNotIn, + jobPromise, kHandle, - kKeyObject, } = require('internal/crypto/util'); const { lazyDOMException, - promisify, } = require('internal/util'); const { - generateKeyPair: _generateKeyPair, -} = require('internal/crypto/keygen'); - -const { + getCryptoKeyHandle, + getCryptoKeyType, InternalCryptoKey, - kKeyType, } = require('internal/crypto/keys'); const { @@ -48,8 +48,6 @@ const { validateJwk, } = require('internal/crypto/webcrypto_util'); -const generateKeyPair = promisify(_generateKeyPair); - async function mlKemGenerateKey(algorithm, extractable, keyUsages) { const { name } = algorithm; @@ -60,15 +58,20 @@ async function mlKemGenerateKey(algorithm, extractable, keyUsages) { 'SyntaxError'); } - let keyPair; - try { - keyPair = await generateKeyPair(name.toLowerCase()); - } catch (err) { - throw lazyDOMException( - 'The operation failed for an operation-specific reason', - { name: 'OperationError', cause: err }); + let nid; + switch (name) { + case 'ML-KEM-512': + nid = EVP_PKEY_ML_KEM_512; + break; + case 'ML-KEM-768': + nid = EVP_PKEY_ML_KEM_768; + break; + case 'ML-KEM-1024': + nid = EVP_PKEY_ML_KEM_1024; + break; } + const handles = await jobPromise(() => new NidKeyPairGenJob(kCryptoJobAsync, nid)); const publicUsages = getUsagesUnion(usageSet, 'encapsulateBits', 'encapsulateKey'); const privateUsages = getUsagesUnion(usageSet, 'decapsulateBits', 'decapsulateKey'); @@ -76,14 +79,14 @@ async function mlKemGenerateKey(algorithm, extractable, keyUsages) { const publicKey = new InternalCryptoKey( - keyPair.publicKey, + handles[0], keyAlgorithm, publicUsages, true); const privateKey = new InternalCryptoKey( - keyPair.privateKey, + handles[1], keyAlgorithm, privateUsages, extractable); @@ -93,17 +96,19 @@ async function mlKemGenerateKey(algorithm, extractable, keyUsages) { function mlKemExportKey(key, format) { try { + const handle = getCryptoKeyHandle(key); switch (format) { case kWebCryptoKeyFormatRaw: { - const handle = key[kKeyObject][kHandle]; return TypedArrayPrototypeGetBuffer( - key[kKeyType] === 'private' ? handle.rawSeed() : handle.rawPublicKey()); + getCryptoKeyType(key) === 'private' ? handle.rawSeed() : handle.rawPublicKey()); } case kWebCryptoKeyFormatSPKI: { - return TypedArrayPrototypeGetBuffer(key[kKeyObject][kHandle].export(kKeyFormatDER, kWebCryptoKeyFormatSPKI)); + return TypedArrayPrototypeGetBuffer( + handle.export(kKeyFormatDER, kWebCryptoKeyFormatSPKI)); } case kWebCryptoKeyFormatPKCS8: { - const pkcs8 = key[kKeyObject][kHandle].export(kKeyFormatDER, kWebCryptoKeyFormatPKCS8, null, null); + const pkcs8 = handle.export( + kKeyFormatDER, kWebCryptoKeyFormatPKCS8, null, null); // Edge case only possible when user creates a seedless KeyObject // first and converts it with KeyObject.prototype.toCryptoKey. // 86 = 22 bytes of PKCS#8 ASN.1 + 64-byte seed. @@ -141,17 +146,17 @@ function mlKemImportKey( keyUsages) { const { name } = algorithm; - let keyObject; + let handle; const usagesSet = new SafeSet(keyUsages); switch (format) { case 'KeyObject': { verifyAcceptableMlKemKeyUse(name, keyData.type === 'public', usagesSet); - keyObject = keyData; + handle = keyData[kHandle]; break; } case 'spki': { verifyAcceptableMlKemKeyUse(name, true, usagesSet); - keyObject = importDerKey(keyData, true); + handle = importDerKey(keyData, true); break; } case 'pkcs8': { @@ -169,14 +174,14 @@ function mlKemImportKey( 'NotSupportedError'); } - keyObject = importDerKey(keyData, false); + handle = importDerKey(keyData, false); break; } case 'raw-public': case 'raw-seed': { const isPublic = format === 'raw-public'; verifyAcceptableMlKemKeyUse(name, isPublic, usagesSet); - keyObject = importRawKey(isPublic, keyData, isPublic ? kKeyFormatRawPublic : kKeyFormatRawPrivate, name); + handle = importRawKey(isPublic, keyData, isPublic ? kKeyFormatRawPublic : kKeyFormatRawPrivate, name); break; } case 'jwk': { @@ -188,26 +193,26 @@ function mlKemImportKey( const isPublic = keyData.priv === undefined; verifyAcceptableMlKemKeyUse(name, isPublic, usagesSet); - keyObject = importJwkKey(isPublic, keyData); + handle = importJwkKey(isPublic, keyData); break; } default: return undefined; } - if (keyObject.asymmetricKeyType !== StringPrototypeToLowerCase(name)) { + if (handle.getAsymmetricKeyType() !== StringPrototypeToLowerCase(name)) { throw lazyDOMException('Invalid key type', 'DataError'); } return new InternalCryptoKey( - keyObject, + handle, { name }, keyUsages, extractable); } function mlKemEncapsulate(encapsulationKey) { - if (encapsulationKey[kKeyType] !== 'public') { + if (getCryptoKeyType(encapsulationKey) !== 'public') { throw lazyDOMException(`Key must be a public key`, 'InvalidAccessError'); } @@ -215,7 +220,7 @@ function mlKemEncapsulate(encapsulationKey) { const job = new KEMEncapsulateJob( kCryptoJobAsync, - encapsulationKey[kKeyObject][kHandle], + getCryptoKeyHandle(encapsulationKey), undefined, undefined, undefined, @@ -241,7 +246,7 @@ function mlKemEncapsulate(encapsulationKey) { } function mlKemDecapsulate(decapsulationKey, ciphertext) { - if (decapsulationKey[kKeyType] !== 'private') { + if (getCryptoKeyType(decapsulationKey) !== 'private') { throw lazyDOMException(`Key must be a private key`, 'InvalidAccessError'); } @@ -249,7 +254,7 @@ function mlKemDecapsulate(decapsulationKey, ciphertext) { const job = new KEMDecapsulateJob( kCryptoJobAsync, - decapsulationKey[kKeyObject][kHandle], + getCryptoKeyHandle(decapsulationKey), undefined, undefined, undefined, diff --git a/lib/internal/crypto/pbkdf2.js b/lib/internal/crypto/pbkdf2.js index 2b11535edb3ec7..7f0fa0e1855efe 100644 --- a/lib/internal/crypto/pbkdf2.js +++ b/lib/internal/crypto/pbkdf2.js @@ -23,7 +23,6 @@ const { const { getArrayBufferOrView, normalizeHashName, - kKeyObject, } = require('internal/crypto/util'); const { @@ -31,6 +30,10 @@ const { promisify, } = require('internal/util'); +const { + getCryptoKeyHandle, +} = require('internal/crypto/keys'); + function pbkdf2(password, salt, iterations, keylen, digest, callback) { if (typeof digest === 'function') { callback = digest; @@ -113,8 +116,9 @@ async function pbkdf2DeriveBits(algorithm, baseKey, length) { let result; try { + // TODO(panva): call the job directly without needing to re-export the handle result = await pbkdf2Promise( - baseKey[kKeyObject].export(), salt, iterations, length / 8, normalizeHashName(hash.name), + getCryptoKeyHandle(baseKey).export(), salt, iterations, length / 8, normalizeHashName(hash.name), ); } catch (err) { throw lazyDOMException( diff --git a/lib/internal/crypto/rsa.js b/lib/internal/crypto/rsa.js index b87ef49dbd5aa4..486611b3e73d59 100644 --- a/lib/internal/crypto/rsa.js +++ b/lib/internal/crypto/rsa.js @@ -15,9 +15,11 @@ const { kSignJobModeSign, kSignJobModeVerify, kKeyVariantRSA_OAEP, + kKeyVariantRSA_SSA_PKCS1_v1_5, kWebCryptoCipherEncrypt, kWebCryptoKeyFormatPKCS8, kWebCryptoKeyFormatSPKI, + RsaKeyPairGenJob, RSA_PKCS1_PSS_PADDING, } = internalBinding('crypto'); @@ -34,18 +36,17 @@ const { normalizeHashName, validateMaxBufferLength, kHandle, - kKeyObject, } = require('internal/crypto/util'); const { lazyDOMException, - promisify, } = require('internal/util'); const { InternalCryptoKey, - kAlgorithm, - kKeyType, + getCryptoKeyAlgorithm, + getCryptoKeyHandle, + getCryptoKeyType, } = require('internal/crypto/keys'); const { @@ -54,12 +55,6 @@ const { validateJwk, } = require('internal/crypto/webcrypto_util'); -const { - generateKeyPair: _generateKeyPair, -} = require('internal/crypto/keygen'); - -const generateKeyPair = promisify(_generateKeyPair); - function verifyAcceptableRsaKeyUse(name, isPublic, usages) { let checkSet; switch (name) { @@ -92,7 +87,7 @@ async function rsaOaepCipher(mode, key, data, algorithm) { validateRsaOaepAlgorithm(algorithm); const type = mode === kWebCryptoCipherEncrypt ? 'public' : 'private'; - if (key[kKeyType] !== type) { + if (getCryptoKeyType(key) !== type) { throw lazyDOMException( 'The requested operation is not valid for the provided key', 'InvalidAccessError'); @@ -101,10 +96,10 @@ async function rsaOaepCipher(mode, key, data, algorithm) { return await jobPromise(() => new RSACipherJob( kCryptoJobAsync, mode, - key[kKeyObject][kHandle], + getCryptoKeyHandle(key), data, kKeyVariantRSA_OAEP, - normalizeHashName(key[kAlgorithm].hash.name), + normalizeHashName(getCryptoKeyAlgorithm(key).hash.name), algorithm.label)); } @@ -145,17 +140,11 @@ async function rsaKeyGenerate( } } - let keyPair; - try { - keyPair = await generateKeyPair('rsa', { - modulusLength, - publicExponent: publicExponentConverted, - }); - } catch (err) { - throw lazyDOMException( - 'The operation failed for an operation-specific reason', - { name: 'OperationError', cause: err }); - } + const handles = await jobPromise(() => new RsaKeyPairGenJob( + kCryptoJobAsync, + kKeyVariantRSA_SSA_PKCS1_v1_5, + modulusLength, + publicExponentConverted)); const keyAlgorithm = { name, @@ -181,14 +170,14 @@ async function rsaKeyGenerate( const publicKey = new InternalCryptoKey( - keyPair.publicKey, + handles[0], keyAlgorithm, publicUsages, true); const privateKey = new InternalCryptoKey( - keyPair.privateKey, + handles[1], keyAlgorithm, privateUsages, extractable); @@ -201,11 +190,11 @@ function rsaExportKey(key, format) { switch (format) { case kWebCryptoKeyFormatSPKI: { return TypedArrayPrototypeGetBuffer( - key[kKeyObject][kHandle].export(kKeyFormatDER, kWebCryptoKeyFormatSPKI)); + getCryptoKeyHandle(key).export(kKeyFormatDER, kWebCryptoKeyFormatSPKI)); } case kWebCryptoKeyFormatPKCS8: { return TypedArrayPrototypeGetBuffer( - key[kKeyObject][kHandle].export(kKeyFormatDER, kWebCryptoKeyFormatPKCS8, null, null)); + getCryptoKeyHandle(key).export(kKeyFormatDER, kWebCryptoKeyFormatPKCS8, null, null)); } default: return undefined; @@ -224,21 +213,21 @@ function rsaImportKey( extractable, keyUsages) { const usagesSet = new SafeSet(keyUsages); - let keyObject; + let handle; switch (format) { case 'KeyObject': { verifyAcceptableRsaKeyUse(algorithm.name, keyData.type === 'public', usagesSet); - keyObject = keyData; + handle = keyData[kHandle]; break; } case 'spki': { verifyAcceptableRsaKeyUse(algorithm.name, true, usagesSet); - keyObject = importDerKey(keyData, true); + handle = importDerKey(keyData, true); break; } case 'pkcs8': { verifyAcceptableRsaKeyUse(algorithm.name, false, usagesSet); - keyObject = importDerKey(keyData, false); + handle = importDerKey(keyData, false); break; } case 'jwk': { @@ -260,23 +249,23 @@ function rsaImportKey( const isPublic = keyData.d === undefined; verifyAcceptableRsaKeyUse(algorithm.name, isPublic, usagesSet); - keyObject = importJwkKey(isPublic, keyData); + handle = importJwkKey(isPublic, keyData); break; } default: return undefined; } - if (keyObject.asymmetricKeyType !== 'rsa') { + if (handle.getAsymmetricKeyType() !== 'rsa') { throw lazyDOMException('Invalid key type', 'DataError'); } const { modulusLength, publicExponent, - } = keyObject[kHandle].keyDetail({}); + } = handle.keyDetail({}); - return new InternalCryptoKey(keyObject, { + return new InternalCryptoKey(handle, { name: algorithm.name, modulusLength, publicExponent: new Uint8Array(publicExponent), @@ -288,30 +277,31 @@ async function rsaSignVerify(key, data, { saltLength }, signature) { const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify; const type = mode === kSignJobModeSign ? 'private' : 'public'; - if (key[kKeyType] !== type) + if (getCryptoKeyType(key) !== type) throw lazyDOMException(`Key must be a ${type} key`, 'InvalidAccessError'); return await jobPromise(() => { - if (key[kAlgorithm].name === 'RSA-PSS') { + const algorithm = getCryptoKeyAlgorithm(key); + if (algorithm.name === 'RSA-PSS') { validateInt32( saltLength, 'algorithm.saltLength', 0, - MathCeil((key[kAlgorithm].modulusLength - 1) / 8) - getDigestSizeInBytes(key[kAlgorithm].hash.name) - 2); + MathCeil((algorithm.modulusLength - 1) / 8) - getDigestSizeInBytes(algorithm.hash.name) - 2); } return new SignJob( kCryptoJobAsync, signature === undefined ? kSignJobModeSign : kSignJobModeVerify, - key[kKeyObject][kHandle], + getCryptoKeyHandle(key), undefined, undefined, undefined, undefined, data, - normalizeHashName(key[kAlgorithm].hash.name), + normalizeHashName(algorithm.hash.name), saltLength, - key[kAlgorithm].name === 'RSA-PSS' ? RSA_PKCS1_PSS_PADDING : undefined, + algorithm.name === 'RSA-PSS' ? RSA_PKCS1_PSS_PADDING : undefined, undefined, undefined, signature); diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js index b0e143ce46ea60..2e5c85c91b73cf 100644 --- a/lib/internal/crypto/webcrypto.js +++ b/lib/internal/crypto/webcrypto.js @@ -35,11 +35,13 @@ const { const { createPublicKey, CryptoKey, + getCryptoKeyAlgorithm, + getCryptoKeyExtractable, + getCryptoKeyHandle, + getCryptoKeyType, + getCryptoKeyUsages, importGenericSecretKey, - kAlgorithm, - kKeyUsages, - kExtractable, - kKeyType, + PrivateKeyObject, } = require('internal/crypto/keys'); const { @@ -51,8 +53,6 @@ const { normalizeAlgorithm, normalizeHashName, validateMaxBufferLength, - kHandle, - kKeyObject, } = require('internal/crypto/util'); const { @@ -198,12 +198,15 @@ async function generateKey( throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } - if ( - (resultType === 'CryptoKey' && - (result[kKeyType] === 'secret' || result[kKeyType] === 'private') && - result[kKeyUsages].length === 0) || - (resultType === 'CryptoKeyPair' && result.privateKey[kKeyUsages].length === 0) - ) { + if (resultType === 'CryptoKey') { + const type = getCryptoKeyType(result); + if ((type === 'secret' || type === 'private') && + getCryptoKeyUsages(result).length === 0) { + throw lazyDOMException( + 'Usages cannot be empty when creating a key.', + 'SyntaxError'); + } + } else if (getCryptoKeyUsages(result.privateKey).length === 0) { throw lazyDOMException( 'Usages cannot be empty when creating a key.', 'SyntaxError'); @@ -234,12 +237,12 @@ async function deriveBits(algorithm, baseKey, length = null) { } algorithm = normalizeAlgorithm(algorithm, 'deriveBits'); - if (!ArrayPrototypeIncludes(baseKey[kKeyUsages], 'deriveBits')) { + if (!ArrayPrototypeIncludes(getCryptoKeyUsages(baseKey), 'deriveBits')) { throw lazyDOMException( 'baseKey does not have deriveBits usage', 'InvalidAccessError'); } - if (baseKey[kAlgorithm].name !== algorithm.name) + if (getCryptoKeyAlgorithm(baseKey).name !== algorithm.name) throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError'); switch (algorithm.name) { case 'X25519': @@ -339,12 +342,12 @@ async function deriveKey( algorithm = normalizeAlgorithm(algorithm, 'deriveBits'); derivedKeyAlgorithm = normalizeAlgorithm(derivedKeyAlgorithm, 'importKey'); - if (!ArrayPrototypeIncludes(baseKey[kKeyUsages], 'deriveKey')) { + if (!ArrayPrototypeIncludes(getCryptoKeyUsages(baseKey), 'deriveKey')) { throw lazyDOMException( 'baseKey does not have deriveKey usage', 'InvalidAccessError'); } - if (baseKey[kAlgorithm].name !== algorithm.name) + if (getCryptoKeyAlgorithm(baseKey).name !== algorithm.name) throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError'); const length = getKeyLength(normalizeAlgorithm(arguments[2], 'get key length')); @@ -386,7 +389,7 @@ async function deriveKey( } async function exportKeySpki(key) { - switch (key[kAlgorithm].name) { + switch (getCryptoKeyAlgorithm(key).name) { case 'RSASSA-PKCS1-v1_5': // Fall through case 'RSA-PSS': @@ -428,7 +431,7 @@ async function exportKeySpki(key) { } async function exportKeyPkcs8(key) { - switch (key[kAlgorithm].name) { + switch (getCryptoKeyAlgorithm(key).name) { case 'RSASSA-PKCS1-v1_5': // Fall through case 'RSA-PSS': @@ -470,7 +473,7 @@ async function exportKeyPkcs8(key) { } async function exportKeyRawPublic(key, format) { - switch (key[kAlgorithm].name) { + switch (getCryptoKeyAlgorithm(key).name) { case 'ECDSA': // Fall through case 'ECDH': @@ -515,7 +518,7 @@ async function exportKeyRawPublic(key, format) { } async function exportKeyRawSeed(key) { - switch (key[kAlgorithm].name) { + switch (getCryptoKeyAlgorithm(key).name) { case 'ML-DSA-44': // Fall through case 'ML-DSA-65': @@ -536,7 +539,7 @@ async function exportKeyRawSeed(key) { } async function exportKeyRawSecret(key, format) { - switch (key[kAlgorithm].name) { + switch (getCryptoKeyAlgorithm(key).name) { case 'AES-CTR': // Fall through case 'AES-CBC': @@ -546,7 +549,7 @@ async function exportKeyRawSecret(key, format) { case 'AES-KW': // Fall through case 'HMAC': - return TypedArrayPrototypeGetBuffer(key[kKeyObject][kHandle].export()); + return TypedArrayPrototypeGetBuffer(getCryptoKeyHandle(key).export()); case 'AES-OCB': // Fall through case 'KMAC128': @@ -555,7 +558,7 @@ async function exportKeyRawSecret(key, format) { // Fall through case 'ChaCha20-Poly1305': if (format === 'raw-secret') { - return TypedArrayPrototypeGetBuffer(key[kKeyObject][kHandle].export()); + return TypedArrayPrototypeGetBuffer(getCryptoKeyHandle(key).export()); } return undefined; default: @@ -564,28 +567,29 @@ async function exportKeyRawSecret(key, format) { } async function exportKeyJWK(key) { + const algorithm = getCryptoKeyAlgorithm(key); const parameters = { - key_ops: key[kKeyUsages], - ext: key[kExtractable], + key_ops: getCryptoKeyUsages(key), + ext: getCryptoKeyExtractable(key), }; - switch (key[kAlgorithm].name) { + switch (algorithm.name) { case 'RSASSA-PKCS1-v1_5': { const alg = normalizeHashName( - key[kAlgorithm].hash.name, + algorithm.hash.name, normalizeHashName.kContextJwkRsa); if (alg) parameters.alg = alg; break; } case 'RSA-PSS': { const alg = normalizeHashName( - key[kAlgorithm].hash.name, + algorithm.hash.name, normalizeHashName.kContextJwkRsaPss); if (alg) parameters.alg = alg; break; } case 'RSA-OAEP': { const alg = normalizeHashName( - key[kAlgorithm].hash.name, + algorithm.hash.name, normalizeHashName.kContextJwkRsaOaep); if (alg) parameters.alg = alg; break; @@ -613,7 +617,7 @@ async function exportKeyJWK(key) { case 'Ed25519': // Fall through case 'Ed448': - parameters.alg = key[kAlgorithm].name; + parameters.alg = algorithm.name; break; case 'AES-CTR': // Fall through @@ -625,14 +629,14 @@ async function exportKeyJWK(key) { // Fall through case 'AES-KW': parameters.alg = require('internal/crypto/aes') - .getAlgorithmName(key[kAlgorithm].name, key[kAlgorithm].length); + .getAlgorithmName(algorithm.name, algorithm.length); break; case 'ChaCha20-Poly1305': parameters.alg = 'C20P'; break; case 'HMAC': { const alg = normalizeHashName( - key[kAlgorithm].hash.name, + algorithm.hash.name, normalizeHashName.kContextJwkHmac); if (alg) parameters.alg = alg; break; @@ -648,7 +652,7 @@ async function exportKeyJWK(key) { return undefined; } - return key[kKeyObject][kHandle].exportJwk(parameters, true); + return getCryptoKeyHandle(key).exportJwk(parameters, true); } async function exportKey(format, key) { @@ -666,26 +670,28 @@ async function exportKey(format, key) { context: '2nd argument', }); + const algorithm = getCryptoKeyAlgorithm(key); try { - normalizeAlgorithm(key[kAlgorithm], 'exportKey'); + normalizeAlgorithm(algorithm, 'exportKey'); } catch { throw lazyDOMException( - `${key[kAlgorithm].name} key export is not supported`, 'NotSupportedError'); + `${algorithm.name} key export is not supported`, 'NotSupportedError'); } - if (!key[kExtractable]) + if (!getCryptoKeyExtractable(key)) throw lazyDOMException('key is not extractable', 'InvalidAccessError'); + const type = getCryptoKeyType(key); let result; switch (format) { case 'spki': { - if (key[kKeyType] === 'public') { + if (type === 'public') { result = await exportKeySpki(key); } break; } case 'pkcs8': { - if (key[kKeyType] === 'private') { + if (type === 'private') { result = await exportKeyPkcs8(key); } break; @@ -695,27 +701,27 @@ async function exportKey(format, key) { break; } case 'raw-secret': { - if (key[kKeyType] === 'secret') { + if (type === 'secret') { result = await exportKeyRawSecret(key, format); } break; } case 'raw-public': { - if (key[kKeyType] === 'public') { + if (type === 'public') { result = await exportKeyRawPublic(key, format); } break; } case 'raw-seed': { - if (key[kKeyType] === 'private') { + if (type === 'private') { result = await exportKeyRawSeed(key); } break; } case 'raw': { - if (key[kKeyType] === 'secret') { + if (type === 'secret') { result = await exportKeyRawSecret(key, format); - } else if (key[kKeyType] === 'public') { + } else if (type === 'public') { result = await exportKeyRawPublic(key, format); } break; @@ -724,7 +730,7 @@ async function exportKey(format, key) { if (!result) { throw lazyDOMException( - `Unable to export ${key[kAlgorithm].name} ${key[kKeyType]} key using ${format} format`, + `Unable to export ${algorithm.name} ${type} key using ${format} format`, 'NotSupportedError'); } @@ -844,9 +850,10 @@ function importKeySync(format, keyData, algorithm, extractable, keyUsages) { 'NotSupportedError'); } - if ((result.type === 'secret' || result.type === 'private') && result[kKeyUsages].length === 0) { + const type = getCryptoKeyType(result); + if ((type === 'secret' || type === 'private') && getCryptoKeyUsages(result).length === 0) { throw lazyDOMException( - `Usages cannot be empty when importing a ${result.type} key.`, + `Usages cannot be empty when importing a ${type} key.`, 'SyntaxError'); } @@ -926,10 +933,10 @@ async function wrapKey(format, key, wrappingKey, algorithm) { algorithm = normalizeAlgorithm(algorithm, 'encrypt'); } - if (algorithm.name !== wrappingKey[kAlgorithm].name) + if (algorithm.name !== getCryptoKeyAlgorithm(wrappingKey).name) throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError'); - if (!ArrayPrototypeIncludes(wrappingKey[kKeyUsages], 'wrapKey')) + if (!ArrayPrototypeIncludes(getCryptoKeyUsages(wrappingKey), 'wrapKey')) throw lazyDOMException( 'Unable to use this key to wrapKey', 'InvalidAccessError'); @@ -1011,10 +1018,10 @@ async function unwrapKey( unwrappedKeyAlgo = normalizeAlgorithm(unwrappedKeyAlgo, 'importKey'); - if (unwrapAlgo.name !== unwrappingKey[kAlgorithm].name) + if (unwrapAlgo.name !== getCryptoKeyAlgorithm(unwrappingKey).name) throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError'); - if (!ArrayPrototypeIncludes(unwrappingKey[kKeyUsages], 'unwrapKey')) + if (!ArrayPrototypeIncludes(getCryptoKeyUsages(unwrappingKey), 'unwrapKey')) throw lazyDOMException( 'Unable to use this key to unwrapKey', 'InvalidAccessError'); @@ -1051,10 +1058,10 @@ async function signVerify(algorithm, key, data, signature) { } algorithm = normalizeAlgorithm(algorithm, usage); - if (algorithm.name !== key[kAlgorithm].name) + if (algorithm.name !== getCryptoKeyAlgorithm(key).name) throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError'); - if (!ArrayPrototypeIncludes(key[kKeyUsages], usage)) + if (!ArrayPrototypeIncludes(getCryptoKeyUsages(key), usage)) throw lazyDOMException( `Unable to use this key to ${usage}`, 'InvalidAccessError'); @@ -1192,10 +1199,10 @@ async function encrypt(algorithm, key, data) { algorithm = normalizeAlgorithm(algorithm, 'encrypt'); - if (algorithm.name !== key[kAlgorithm].name) + if (algorithm.name !== getCryptoKeyAlgorithm(key).name) throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError'); - if (!ArrayPrototypeIncludes(key[kKeyUsages], 'encrypt')) + if (!ArrayPrototypeIncludes(getCryptoKeyUsages(key), 'encrypt')) throw lazyDOMException( 'Unable to use this key to encrypt', 'InvalidAccessError'); @@ -1229,10 +1236,10 @@ async function decrypt(algorithm, key, data) { algorithm = normalizeAlgorithm(algorithm, 'decrypt'); - if (algorithm.name !== key[kAlgorithm].name) + if (algorithm.name !== getCryptoKeyAlgorithm(key).name) throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError'); - if (!ArrayPrototypeIncludes(key[kKeyUsages], 'decrypt')) + if (!ArrayPrototypeIncludes(getCryptoKeyUsages(key), 'decrypt')) throw lazyDOMException( 'Unable to use this key to decrypt', 'InvalidAccessError'); @@ -1262,13 +1269,16 @@ async function getPublicKey(key, keyUsages) { context: '2nd argument', }); - if (key[kKeyType] !== 'private') + const type = getCryptoKeyType(key); + if (type !== 'private') throw lazyDOMException('key must be a private key', - key[kKeyType] === 'secret' ? 'NotSupportedError' : 'InvalidAccessError'); + type === 'secret' ? 'NotSupportedError' : 'InvalidAccessError'); - const keyObject = createPublicKey(key[kKeyObject]); - return keyObject.toCryptoKey(key[kAlgorithm], true, keyUsages); + // TODO(panva): this is by no means a hot path, but let's still follow up to get + // rid of this awkwardness + const keyObject = createPublicKey(new PrivateKeyObject(getCryptoKeyHandle(key))); + return keyObject.toCryptoKey(getCryptoKeyAlgorithm(key), true, keyUsages); } async function encapsulateBits(encapsulationAlgorithm, encapsulationKey) { @@ -1288,20 +1298,21 @@ async function encapsulateBits(encapsulationAlgorithm, encapsulationKey) { }); const normalizedEncapsulationAlgorithm = normalizeAlgorithm(encapsulationAlgorithm, 'encapsulate'); + const keyAlgorithm = getCryptoKeyAlgorithm(encapsulationKey); - if (normalizedEncapsulationAlgorithm.name !== encapsulationKey[kAlgorithm].name) { + if (normalizedEncapsulationAlgorithm.name !== keyAlgorithm.name) { throw lazyDOMException( 'key algorithm mismatch', 'InvalidAccessError'); } - if (!ArrayPrototypeIncludes(encapsulationKey[kKeyUsages], 'encapsulateBits')) { + if (!ArrayPrototypeIncludes(getCryptoKeyUsages(encapsulationKey), 'encapsulateBits')) { throw lazyDOMException( 'encapsulationKey does not have encapsulateBits usage', 'InvalidAccessError'); } - switch (encapsulationKey[kAlgorithm].name) { + switch (keyAlgorithm.name) { case 'ML-KEM-512': case 'ML-KEM-768': case 'ML-KEM-1024': @@ -1342,21 +1353,22 @@ async function encapsulateKey(encapsulationAlgorithm, encapsulationKey, sharedKe const normalizedEncapsulationAlgorithm = normalizeAlgorithm(encapsulationAlgorithm, 'encapsulate'); const normalizedSharedKeyAlgorithm = normalizeAlgorithm(sharedKeyAlgorithm, 'importKey'); + const keyAlgorithm = getCryptoKeyAlgorithm(encapsulationKey); - if (normalizedEncapsulationAlgorithm.name !== encapsulationKey[kAlgorithm].name) { + if (normalizedEncapsulationAlgorithm.name !== keyAlgorithm.name) { throw lazyDOMException( 'key algorithm mismatch', 'InvalidAccessError'); } - if (!ArrayPrototypeIncludes(encapsulationKey[kKeyUsages], 'encapsulateKey')) { + if (!ArrayPrototypeIncludes(getCryptoKeyUsages(encapsulationKey), 'encapsulateKey')) { throw lazyDOMException( 'encapsulationKey does not have encapsulateKey usage', 'InvalidAccessError'); } let encapsulateBits; - switch (encapsulationKey[kAlgorithm].name) { + switch (keyAlgorithm.name) { case 'ML-KEM-512': case 'ML-KEM-768': case 'ML-KEM-1024': @@ -1402,20 +1414,21 @@ async function decapsulateBits(decapsulationAlgorithm, decapsulationKey, ciphert }); const normalizedDecapsulationAlgorithm = normalizeAlgorithm(decapsulationAlgorithm, 'decapsulate'); + const keyAlgorithm = getCryptoKeyAlgorithm(decapsulationKey); - if (normalizedDecapsulationAlgorithm.name !== decapsulationKey[kAlgorithm].name) { + if (normalizedDecapsulationAlgorithm.name !== keyAlgorithm.name) { throw lazyDOMException( 'key algorithm mismatch', 'InvalidAccessError'); } - if (!ArrayPrototypeIncludes(decapsulationKey[kKeyUsages], 'decapsulateBits')) { + if (!ArrayPrototypeIncludes(getCryptoKeyUsages(decapsulationKey), 'decapsulateBits')) { throw lazyDOMException( 'decapsulationKey does not have decapsulateBits usage', 'InvalidAccessError'); } - switch (decapsulationKey[kAlgorithm].name) { + switch (keyAlgorithm.name) { case 'ML-KEM-512': case 'ML-KEM-768': case 'ML-KEM-1024': @@ -1462,21 +1475,22 @@ async function decapsulateKey( const normalizedDecapsulationAlgorithm = normalizeAlgorithm(decapsulationAlgorithm, 'decapsulate'); const normalizedSharedKeyAlgorithm = normalizeAlgorithm(sharedKeyAlgorithm, 'importKey'); + const keyAlgorithm = getCryptoKeyAlgorithm(decapsulationKey); - if (normalizedDecapsulationAlgorithm.name !== decapsulationKey[kAlgorithm].name) { + if (normalizedDecapsulationAlgorithm.name !== keyAlgorithm.name) { throw lazyDOMException( 'key algorithm mismatch', 'InvalidAccessError'); } - if (!ArrayPrototypeIncludes(decapsulationKey[kKeyUsages], 'decapsulateKey')) { + if (!ArrayPrototypeIncludes(getCryptoKeyUsages(decapsulationKey), 'decapsulateKey')) { throw lazyDOMException( 'decapsulationKey does not have decapsulateKey usage', 'InvalidAccessError'); } let decapsulatedBits; - switch (decapsulationKey[kAlgorithm].name) { + switch (keyAlgorithm.name) { case 'ML-KEM-512': case 'ML-KEM-768': case 'ML-KEM-1024': diff --git a/lib/internal/crypto/webcrypto_util.js b/lib/internal/crypto/webcrypto_util.js index db340fa620c35a..f7795833edfc1d 100644 --- a/lib/internal/crypto/webcrypto_util.js +++ b/lib/internal/crypto/webcrypto_util.js @@ -8,6 +8,7 @@ const { kKeyEncodingSPKI, kKeyTypePublic, kKeyTypePrivate, + kKeyTypeSecret, } = internalBinding('crypto'); const { @@ -18,11 +19,6 @@ const { lazyDOMException, } = require('internal/util'); -const { - PrivateKeyObject, - PublicKeyObject, -} = require('internal/crypto/keys'); - function importDerKey(keyData, isPublic) { const handle = new KeyObjectHandle(); const keyType = isPublic ? kKeyTypePublic : kKeyTypePrivate; @@ -33,9 +29,7 @@ function importDerKey(keyData, isPublic) { throw lazyDOMException( 'Invalid keyData', { name: 'DataError', cause: err }); } - return isPublic ? - new PublicKeyObject(handle) : - new PrivateKeyObject(handle); + return handle; } function validateJwk(keyData, kty, extractable, usagesSet, expectedUse) { @@ -66,9 +60,7 @@ function importJwkKey(isPublic, keyData) { throw lazyDOMException( 'Invalid keyData', { name: 'DataError', cause: err }); } - return isPublic ? - new PublicKeyObject(handle) : - new PrivateKeyObject(handle); + return handle; } function importRawKey(isPublic, keyData, format, name, namedCurve) { @@ -80,14 +72,31 @@ function importRawKey(isPublic, keyData, format, name, namedCurve) { throw lazyDOMException( 'Invalid keyData', { name: 'DataError', cause: err }); } - return isPublic ? - new PublicKeyObject(handle) : - new PrivateKeyObject(handle); + return handle; +} + +function importSecretKey(keyData) { + const handle = new KeyObjectHandle(); + handle.init(kKeyTypeSecret, keyData); + return handle; +} + +function importJwkSecretKey(keyData) { + const handle = new KeyObjectHandle(); + try { + handle.init(kKeyTypeSecret, keyData, kKeyFormatJWK, null, null); + } catch (err) { + throw lazyDOMException( + 'Invalid keyData', { name: 'DataError', cause: err }); + } + return handle; } module.exports = { importDerKey, importJwkKey, + importJwkSecretKey, importRawKey, + importSecretKey, validateJwk, }; diff --git a/lib/internal/crypto/webidl.js b/lib/internal/crypto/webidl.js index 0c14bd22d6a9ad..bcb182f75d6198 100644 --- a/lib/internal/crypto/webidl.js +++ b/lib/internal/crypto/webidl.js @@ -43,6 +43,10 @@ const { setOwnProperty, } = require('internal/util'); const { CryptoKey } = require('internal/crypto/webcrypto'); +const { + getCryptoKeyAlgorithm, + getCryptoKeyType, +} = require('internal/crypto/keys'); const { validateMaxBufferLength, kNamedCurveAliases, @@ -738,11 +742,11 @@ converters.EcdhKeyDeriveParams = createDictionaryConverter( key: 'public', converter: converters.CryptoKey, validator: (V, dict) => { - if (V.type !== 'public') + if (getCryptoKeyType(V) !== 'public') throw lazyDOMException( 'algorithm.public must be a public key', 'InvalidAccessError'); - if (StringPrototypeToLowerCase(V.algorithm.name) !== StringPrototypeToLowerCase(dict.name)) + if (StringPrototypeToLowerCase(getCryptoKeyAlgorithm(V).name) !== StringPrototypeToLowerCase(dict.name)) throw lazyDOMException( 'key algorithm mismatch', 'InvalidAccessError'); From 0cb66d094fe44849fb4aee053a52f10c0156722f Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Fri, 24 Apr 2026 14:13:46 +0200 Subject: [PATCH 05/12] src,crypto: relax RSA/EC keygen arg checks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/crypto/crypto_ec.cc | 17 +++++++++++------ src/crypto/crypto_rsa.cc | 4 ++-- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/crypto/crypto_ec.cc b/src/crypto/crypto_ec.cc index 9cd50a421f8715..9355b7f7a6ca64 100644 --- a/src/crypto/crypto_ec.cc +++ b/src/crypto/crypto_ec.cc @@ -436,7 +436,6 @@ Maybe EcKeyGenTraits::AdditionalConfig( EcKeyPairGenConfig* params) { Environment* env = Environment::GetCurrent(args); CHECK(args[*offset]->IsString()); // curve name - CHECK(args[*offset + 1]->IsInt32()); // param encoding Utf8Value curve_name(env->isolate(), args[*offset]); params->params.curve_nid = Ec::GetCurveIdFromName(*curve_name); @@ -445,11 +444,17 @@ Maybe EcKeyGenTraits::AdditionalConfig( return Nothing(); } - params->params.param_encoding = args[*offset + 1].As()->Value(); - if (params->params.param_encoding != OPENSSL_EC_NAMED_CURVE && - params->params.param_encoding != OPENSSL_EC_EXPLICIT_CURVE) { - THROW_ERR_OUT_OF_RANGE(env, "Invalid param_encoding specified"); - return Nothing(); + // param encoding + if (args[*offset + 1]->IsNullOrUndefined()) { + params->params.param_encoding = OPENSSL_EC_NAMED_CURVE; + } else { + CHECK(args[*offset + 1]->IsInt32()); + params->params.param_encoding = args[*offset + 1].As()->Value(); + if (params->params.param_encoding != OPENSSL_EC_NAMED_CURVE && + params->params.param_encoding != OPENSSL_EC_EXPLICIT_CURVE) { + THROW_ERR_OUT_OF_RANGE(env, "Invalid param_encoding specified"); + return Nothing(); + } } *offset += 2; diff --git a/src/crypto/crypto_rsa.cc b/src/crypto/crypto_rsa.cc index e39a7fe72de651..900f0a9acc9b34 100644 --- a/src/crypto/crypto_rsa.cc +++ b/src/crypto/crypto_rsa.cc @@ -128,9 +128,9 @@ Maybe RsaKeyGenTraits::AdditionalConfig( static_cast(args[*offset].As()->Value()); CHECK_IMPLIES(params->params.variant != kKeyVariantRSA_PSS, - args.Length() == 10); + static_cast(args.Length()) >= *offset + 3); CHECK_IMPLIES(params->params.variant == kKeyVariantRSA_PSS, - args.Length() == 13); + static_cast(args.Length()) >= *offset + 6); params->params.modulus_bits = args[*offset + 1].As()->Value(); params->params.exponent = args[*offset + 2].As()->Value(); From fc5b670bb524ed8f8fb6d9470ccb10acfb80db52 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Fri, 24 Apr 2026 14:14:00 +0200 Subject: [PATCH 06/12] lib,crypto: validate HkdfParams info length early 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 --- lib/internal/crypto/util.js | 6 +-- lib/internal/crypto/webidl.js | 1 + .../test-webcrypto-derivebits-hkdf.js | 37 ++++++++++++------- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index 6287d75cfb2e4c..16d6b87edf14df 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -526,10 +526,10 @@ const simpleAlgorithmDictionaries = { TurboShakeParams: {}, }; -function validateMaxBufferLength(data, name) { - if (data.byteLength > kMaxBufferLength) { +function validateMaxBufferLength(data, name, max = kMaxBufferLength) { + if (data.byteLength > max) { throw lazyDOMException( - `${name} must be less than ${kMaxBufferLength + 1} bits`, + `${name} must be at most ${max} bytes`, 'OperationError'); } } diff --git a/lib/internal/crypto/webidl.js b/lib/internal/crypto/webidl.js index bcb182f75d6198..43f0906a3af902 100644 --- a/lib/internal/crypto/webidl.js +++ b/lib/internal/crypto/webidl.js @@ -567,6 +567,7 @@ converters.HkdfParams = createDictionaryConverter( { key: 'info', converter: converters.BufferSource, + validator: (V, dict) => validateMaxBufferLength(V, 'algorithm.info', 1024), required: true, }, ]); diff --git a/test/parallel/test-webcrypto-derivebits-hkdf.js b/test/parallel/test-webcrypto-derivebits-hkdf.js index 2759223e76a060..689eaeb38fd66f 100644 --- a/test/parallel/test-webcrypto-derivebits-hkdf.js +++ b/test/parallel/test-webcrypto-derivebits-hkdf.js @@ -628,16 +628,27 @@ async function testWrongKeyType( })().then(common.mustCall()); // https://github.com/w3c/webcrypto/pull/380 -{ - crypto.subtle.importKey('raw', new Uint8Array(0), 'HKDF', false, ['deriveBits']).then((key) => { - return crypto.subtle.deriveBits({ - name: 'HKDF', - hash: { name: 'SHA-256' }, - info: new Uint8Array(0), - salt: new Uint8Array(0), - }, key, 0); - }).then((bits) => { - assert.deepStrictEqual(bits, new ArrayBuffer(0)); - }) - .then(common.mustCall()); -} +(async function() { + const key = await crypto.subtle.importKey('raw', new Uint8Array(0), 'HKDF', false, ['deriveBits']); + const bits = await crypto.subtle.deriveBits({ + name: 'HKDF', + hash: { name: 'SHA-256' }, + info: new Uint8Array(0), + salt: new Uint8Array(0), + }, key, 0); + assert.deepStrictEqual(bits, new ArrayBuffer(0)); +})().then(common.mustCall()); + +// OpenSSL limits info to 1024 bytes +(async function() { + const key = await crypto.subtle.importKey('raw', new Uint8Array(0), 'HKDF', false, ['deriveBits']); + await assert.rejects(crypto.subtle.deriveBits({ + name: 'HKDF', + hash: { name: 'SHA-256' }, + info: new Uint8Array(1025), + salt: new Uint8Array(0), + }, key, 0), { + name: 'OperationError', + message: 'algorithm.info must be at most 1024 bytes', + }); +})().then(common.mustCall()); From 51cdf20289746613b1f13230de3c2ebe10469398 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Fri, 24 Apr 2026 14:14:16 +0200 Subject: [PATCH 07/12] lib,crypto: add early structural JWK validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- lib/internal/crypto/webcrypto_util.js | 45 ++++++++++++++++++- ...rypto-encrypt-decrypt-chacha20-poly1305.js | 1 + .../test-webcrypto-export-import-cfrg.js | 4 +- .../test-webcrypto-export-import-ec.js | 4 +- .../test-webcrypto-export-import-ml-dsa.js | 4 +- .../test-webcrypto-export-import-ml-kem.js | 4 +- 6 files changed, 53 insertions(+), 9 deletions(-) diff --git a/lib/internal/crypto/webcrypto_util.js b/lib/internal/crypto/webcrypto_util.js index f7795833edfc1d..320bf0436de553 100644 --- a/lib/internal/crypto/webcrypto_util.js +++ b/lib/internal/crypto/webcrypto_util.js @@ -33,10 +33,53 @@ function importDerKey(keyData, isPublic) { } function validateJwk(keyData, kty, extractable, usagesSet, expectedUse) { - if (!keyData.kty) + if (typeof keyData.kty !== 'string') throw lazyDOMException('Invalid keyData', 'DataError'); if (keyData.kty !== kty) throw lazyDOMException('Invalid JWK "kty" Parameter', 'DataError'); + switch (kty) { + case 'RSA': + if (typeof keyData.n !== 'string' || + typeof keyData.e !== 'string' || + (keyData.d !== undefined && typeof keyData.d !== 'string')) + throw lazyDOMException('Invalid keyData', 'DataError'); + if (typeof keyData.d === 'string' && + (typeof keyData.p !== 'string' || + typeof keyData.q !== 'string' || + typeof keyData.dp !== 'string' || + typeof keyData.dq !== 'string' || + typeof keyData.qi !== 'string')) + throw lazyDOMException('Invalid keyData', 'DataError'); + break; + case 'EC': + if (typeof keyData.crv !== 'string' || + typeof keyData.x !== 'string' || + typeof keyData.y !== 'string' || + (keyData.d !== undefined && typeof keyData.d !== 'string')) + throw lazyDOMException('Invalid keyData', 'DataError'); + break; + case 'OKP': + if (typeof keyData.crv !== 'string' || + typeof keyData.x !== 'string' || + (keyData.d !== undefined && typeof keyData.d !== 'string')) + throw lazyDOMException('Invalid keyData', 'DataError'); + break; + case 'oct': + if (typeof keyData.k !== 'string') + throw lazyDOMException('Invalid keyData', 'DataError'); + break; + case 'AKP': + if (typeof keyData.alg !== 'string' || + typeof keyData.pub !== 'string' || + (keyData.priv !== undefined && typeof keyData.priv !== 'string')) + throw lazyDOMException('Invalid keyData', 'DataError'); + break; + default: { + // It is not possible to get here because all possible cases are handled above. + const assert = require('internal/assert'); + assert.fail('Unreachable code'); + } + } if (usagesSet.size > 0 && keyData.use !== undefined) { if (keyData.use !== expectedUse) throw lazyDOMException('Invalid JWK "use" Parameter', 'DataError'); diff --git a/test/parallel/test-webcrypto-encrypt-decrypt-chacha20-poly1305.js b/test/parallel/test-webcrypto-encrypt-decrypt-chacha20-poly1305.js index 0825027a7c3c02..0f930a356712ed 100644 --- a/test/parallel/test-webcrypto-encrypt-decrypt-chacha20-poly1305.js +++ b/test/parallel/test-webcrypto-encrypt-decrypt-chacha20-poly1305.js @@ -220,6 +220,7 @@ async function testDecrypt({ keyBuffer, algorithm, result }) { // JWK error conditions const jwkTests = [ + [{ kty: 'oct' }, /Invalid keyData/], [{ k: baseJwk.k }, /Invalid keyData/], [{ ...baseJwk, kty: 'RSA' }, /Invalid JWK "kty" Parameter/], [{ ...baseJwk, use: 'sig' }, /Invalid JWK "use" Parameter/], diff --git a/test/parallel/test-webcrypto-export-import-cfrg.js b/test/parallel/test-webcrypto-export-import-cfrg.js index c6e3509a4362bc..e85836a8feadda 100644 --- a/test/parallel/test-webcrypto-export-import-cfrg.js +++ b/test/parallel/test-webcrypto-export-import-cfrg.js @@ -355,7 +355,7 @@ async function testImportJwk({ name, publicUsages, privateUsages }, extractable) { name }, extractable, publicUsages), - { message: 'JWK "crv" Parameter and algorithm name mismatch' }); + { message: crv ? 'JWK "crv" Parameter and algorithm name mismatch' : 'Invalid keyData' }); await assert.rejects( subtle.importKey( @@ -364,7 +364,7 @@ async function testImportJwk({ name, publicUsages, privateUsages }, extractable) { name }, extractable, privateUsages), - { message: 'JWK "crv" Parameter and algorithm name mismatch' }); + { message: crv ? 'JWK "crv" Parameter and algorithm name mismatch' : 'Invalid keyData' }); } await assert.rejects( diff --git a/test/parallel/test-webcrypto-export-import-ec.js b/test/parallel/test-webcrypto-export-import-ec.js index 98d0d7d3342ccb..f6157bff2b69d6 100644 --- a/test/parallel/test-webcrypto-export-import-ec.js +++ b/test/parallel/test-webcrypto-export-import-ec.js @@ -322,7 +322,7 @@ async function testImportJwk( { name, namedCurve }, extractable, publicUsages), - { message: 'JWK "crv" does not match the requested algorithm' }); + { message: crv ? 'JWK "crv" does not match the requested algorithm' : 'Invalid keyData' }); await assert.rejects( subtle.importKey( @@ -331,7 +331,7 @@ async function testImportJwk( { name, namedCurve }, extractable, privateUsages), - { message: 'JWK "crv" does not match the requested algorithm' }); + { message: crv ? 'JWK "crv" does not match the requested algorithm' : 'Invalid keyData' }); } await assert.rejects( diff --git a/test/parallel/test-webcrypto-export-import-ml-dsa.js b/test/parallel/test-webcrypto-export-import-ml-dsa.js index 7030c452003c63..63766a7b377c77 100644 --- a/test/parallel/test-webcrypto-export-import-ml-dsa.js +++ b/test/parallel/test-webcrypto-export-import-ml-dsa.js @@ -347,7 +347,7 @@ async function testImportJwk({ name, publicUsages, privateUsages }, extractable) { name }, extractable, publicUsages), - { message: 'JWK "alg" Parameter and algorithm name mismatch' }); + { message: alg ? 'JWK "alg" Parameter and algorithm name mismatch' : 'Invalid keyData' }); await assert.rejects( subtle.importKey( @@ -356,7 +356,7 @@ async function testImportJwk({ name, publicUsages, privateUsages }, extractable) { name }, extractable, privateUsages), - { message: 'JWK "alg" Parameter and algorithm name mismatch' }); + { message: alg ? 'JWK "alg" Parameter and algorithm name mismatch' : 'Invalid keyData' }); } await assert.rejects( diff --git a/test/parallel/test-webcrypto-export-import-ml-kem.js b/test/parallel/test-webcrypto-export-import-ml-kem.js index 9b75dee64511d9..e25a59b5151578 100644 --- a/test/parallel/test-webcrypto-export-import-ml-kem.js +++ b/test/parallel/test-webcrypto-export-import-ml-kem.js @@ -423,7 +423,7 @@ async function testImportJwk({ name, publicUsages, privateUsages }, extractable) { name }, extractable, publicUsages), - { message: 'JWK "alg" Parameter and algorithm name mismatch' }); + { message: alg ? 'JWK "alg" Parameter and algorithm name mismatch' : 'Invalid keyData' }); await assert.rejects( subtle.importKey( @@ -432,7 +432,7 @@ async function testImportJwk({ name, publicUsages, privateUsages }, extractable) { name }, extractable, privateUsages), - { message: 'JWK "alg" Parameter and algorithm name mismatch' }); + { message: alg ? 'JWK "alg" Parameter and algorithm name mismatch' : 'Invalid keyData' }); } await assert.rejects( From 81a53a579d952fc19832d13f73664e2f614f0758 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Fri, 24 Apr 2026 14:14:32 +0200 Subject: [PATCH 08/12] test: add CryptoKey class regression tests 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 --- .../test-webcrypto-cryptokey-brand-check.js | 120 +++++++++ ...test-webcrypto-cryptokey-clone-transfer.js | 251 ++++++++++++++++++ .../test-webcrypto-cryptokey-hidden-slots.js | 125 +++++++++ ...test-webcrypto-cryptokey-no-own-symbols.js | 52 ++++ 4 files changed, 548 insertions(+) create mode 100644 test/parallel/test-webcrypto-cryptokey-brand-check.js create mode 100644 test/parallel/test-webcrypto-cryptokey-clone-transfer.js create mode 100644 test/parallel/test-webcrypto-cryptokey-hidden-slots.js create mode 100644 test/parallel/test-webcrypto-cryptokey-no-own-symbols.js diff --git a/test/parallel/test-webcrypto-cryptokey-brand-check.js b/test/parallel/test-webcrypto-cryptokey-brand-check.js new file mode 100644 index 00000000000000..1cbf37340bef67 --- /dev/null +++ b/test/parallel/test-webcrypto-cryptokey-brand-check.js @@ -0,0 +1,120 @@ +'use strict'; + +// The four CryptoKey prototype getters (`type`, `extractable`, +// `algorithm`, `usages`) are user-configurable per Web IDL, so they +// can be invoked with an arbitrary `this`. The native callbacks that +// implement them must brand-check their receiver and throw cleanly +// (ERR_INVALID_THIS) rather than crashing the process or returning +// garbage. This test exercises four progressively more hostile +// receiver shapes, including subverting `instanceof` via +// `Symbol.hasInstance`, to make sure the C++ brand check holds. +// +// It also verifies that `util.types.isCryptoKey()` cannot be fooled +// by prototype spoofing. + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('node:assert'); +const { types: { isCryptoKey } } = require('node:util'); +const { subtle } = globalThis.crypto; + +(async () => { + const key = await subtle.generateKey( + { name: 'HMAC', hash: 'SHA-256' }, + true, + ['sign'], + ); + + const CryptoKey = key.constructor; + + // Capture the underlying prototype getters once, so that subsequent + // tampering with `CryptoKey.prototype` cannot affect what we call. + const getters = { + type: Object.getOwnPropertyDescriptor(CryptoKey.prototype, 'type').get, + extractable: + Object.getOwnPropertyDescriptor(CryptoKey.prototype, 'extractable').get, + algorithm: + Object.getOwnPropertyDescriptor(CryptoKey.prototype, 'algorithm').get, + usages: + Object.getOwnPropertyDescriptor(CryptoKey.prototype, 'usages').get, + }; + + // Sanity: each getter works on a real CryptoKey. + Object.entries(getters).forEach(([name, getter]) => { + assert.notStrictEqual(getter.call(key), undefined, `baseline ${name}`); + }); + assert.strictEqual(isCryptoKey(key), true); + + const invalidThis = { code: 'ERR_INVALID_THIS', name: 'TypeError' }; + + // Plain object receiver. + Object.entries(getters).forEach(([, getter]) => { + assert.throws(() => getter.call({}), invalidThis); + }); + + // Null-prototype object receiver. + Object.entries(getters).forEach(([, getter]) => { + assert.throws(() => getter.call({ __proto__: null }), invalidThis); + }); + + // Primitive receiver. + Object.entries(getters).forEach(([, getter]) => { + assert.throws(() => getter.call(1), invalidThis); + }); + + // Null. + Object.entries(getters).forEach(([, getter]) => { + // eslint-disable-next-line no-useless-call + assert.throws(() => getter.call(null), invalidThis); + }); + + // Undefined. + Object.entries(getters).forEach(([, getter]) => { + assert.throws(() => getter.call(), invalidThis); + }); + + // Function + Object.entries(getters).forEach(([, getter]) => { + assert.throws(() => getter.call(function() {}), invalidThis); + }); + + // Prototype spoofing with InternalCryptoKey.prototype must not pass + // util.types.isCryptoKey(). + const spoofed = {}; + Object.setPrototypeOf(spoofed, Object.getPrototypeOf(key)); + assert.strictEqual(spoofed instanceof CryptoKey, true); + assert.strictEqual(isCryptoKey(spoofed), false); + + // Subvert `instanceof CryptoKey` via Symbol.hasInstance, then + // invoke the native getters on a forged object. The C++ tag + // check must reject the receiver even though `instanceof` + // reports true. + Object.defineProperty(CryptoKey, Symbol.hasInstance, { + configurable: true, + value: () => true, + }); + const fake = { foo: 'bar' }; + assert.strictEqual(fake instanceof CryptoKey, true); + assert.strictEqual(isCryptoKey(fake), false); + Object.entries(getters).forEach(([, getter]) => { + assert.throws(() => getter.call(fake), invalidThis); + }); + + // Subverted `instanceof` plus a real BaseObject of a different + // kind (a Buffer) as the receiver. Without the C++ tag check + // this would type-confuse `Unwrap`. + const buf = Buffer.alloc(16); + assert.strictEqual(buf instanceof CryptoKey, true); + assert.strictEqual(isCryptoKey(buf), false); + Object.entries(getters).forEach(([, getter]) => { + assert.throws(() => getter.call(buf), invalidThis); + }); + + // The real CryptoKey continues to work after all of the above. + assert.strictEqual(getters.type.call(key), 'secret'); + assert.strictEqual(getters.extractable.call(key), true); + assert.strictEqual(getters.algorithm.call(key).name, 'HMAC'); + assert.deepStrictEqual(getters.usages.call(key), ['sign']); +})().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-cryptokey-clone-transfer.js b/test/parallel/test-webcrypto-cryptokey-clone-transfer.js new file mode 100644 index 00000000000000..65d1841b0cd702 --- /dev/null +++ b/test/parallel/test-webcrypto-cryptokey-clone-transfer.js @@ -0,0 +1,251 @@ +'use strict'; + +// Tests that CryptoKey instances can be structured-cloned (same-realm +// via `structuredClone`, cross-realm via `MessagePort.postMessage` and +// `Worker.postMessage`) and that the clones: +// 1. preserve all of [[type]], [[extractable]], [[algorithm]], +// [[usages]] internal slots (as observed through both the public +// accessors and the custom util.inspect output), +// 2. are usable in cryptographic operations (sign/verify/encrypt/ +// decrypt/exportKey) and produce the same output as the original, +// 3. can themselves be cloned again (round-trip), and +// 4. work for secret, public, and private keys and for both +// extractable and non-extractable keys. + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('node:assert'); +const { inspect } = require('node:util'); +const { once } = require('node:events'); +const { Worker, MessageChannel } = require('node:worker_threads'); +const { subtle } = globalThis.crypto; + +function assertSameCryptoKey(a, b) { + assert.notStrictEqual(a, b); + assert.strictEqual(a.type, b.type); + assert.strictEqual(a.extractable, b.extractable); + assert.deepStrictEqual(a.algorithm, b.algorithm); + assert.deepStrictEqual([...a.usages].sort(), [...b.usages].sort()); + // util.inspect reads native internal slots directly, so a clone's + // rendered form must match the original's. + assert.strictEqual(inspect(a, { depth: 4 }), inspect(b, { depth: 4 })); + // assert.deepStrictEqual on CryptoKey objects goes through the + // dedicated isCryptoKey branch in comparisons.js; a clone must be + // deep-equal to its source. + assert.deepStrictEqual(a, b); +} + +async function roundTripViaMessageChannel(key) { + const { port1, port2 } = new MessageChannel(); + port1.postMessage(key); + const [received] = await once(port2, 'message'); + port1.close(); + port2.close(); + return received; +} + +async function checkHmacKey(original) { + const data = Buffer.from('some data to sign'); + + const cloned = structuredClone(original); + assertSameCryptoKey(original, cloned); + + const viaPort = await roundTripViaMessageChannel(original); + assertSameCryptoKey(original, viaPort); + + // Round-trip: clone a clone. + const clonedAgain = structuredClone(viaPort); + assertSameCryptoKey(original, clonedAgain); + const viaPortAgain = await roundTripViaMessageChannel(cloned); + assertSameCryptoKey(original, viaPortAgain); + + // Signatures produced by every copy must match. + const sigs = await Promise.all( + [original, cloned, viaPort, clonedAgain, viaPortAgain].map( + (k) => subtle.sign('HMAC', k, data), + ), + ); + for (let i = 1; i < sigs.length; i++) { + assert.deepStrictEqual(Buffer.from(sigs[0]), Buffer.from(sigs[i])); + } + + // Each copy must verify a signature produced by any other copy. + for (const verifier of [original, cloned, viaPort, clonedAgain]) { + for (const sig of sigs) { + assert.strictEqual( + await subtle.verify('HMAC', verifier, sig, data), true); + } + } + + // Exported JWK must match byte-for-byte when extractable. + if (original.extractable) { + const jwks = await Promise.all( + [original, cloned, viaPort, clonedAgain].map( + (k) => subtle.exportKey('jwk', k), + ), + ); + for (let i = 1; i < jwks.length; i++) { + assert.deepStrictEqual(jwks[0], jwks[i]); + } + } else { + // Non-extractable keys must refuse export on every copy. + for (const k of [cloned, viaPort, clonedAgain]) { + await assert.rejects(subtle.exportKey('jwk', k), + { name: 'InvalidAccessError' }); + } + } +} + +async function checkAsymmetricKeyPair({ publicKey, privateKey }) { + const data = Buffer.from('payload'); + + for (const original of [publicKey, privateKey]) { + const cloned = structuredClone(original); + assertSameCryptoKey(original, cloned); + const viaPort = await roundTripViaMessageChannel(original); + assertSameCryptoKey(original, viaPort); + const clonedAgain = structuredClone(viaPort); + assertSameCryptoKey(original, clonedAgain); + } + + // Sign with the original private key, verify with every cloned public key. + const signature = await subtle.sign( + { name: 'ECDSA', hash: 'SHA-256' }, privateKey, data); + const publicClones = [ + publicKey, + structuredClone(publicKey), + await roundTripViaMessageChannel(publicKey), + structuredClone(await roundTripViaMessageChannel(publicKey)), + ]; + for (const pub of publicClones) { + assert.strictEqual( + await subtle.verify({ name: 'ECDSA', hash: 'SHA-256' }, + pub, signature, data), + true); + } + + // Sign with every cloned private key, verify with the original public key. + const privateClones = [ + structuredClone(privateKey), + await roundTripViaMessageChannel(privateKey), + structuredClone(await roundTripViaMessageChannel(privateKey)), + ]; + for (const priv of privateClones) { + const sig = await subtle.sign( + { name: 'ECDSA', hash: 'SHA-256' }, priv, data); + assert.strictEqual( + await subtle.verify({ name: 'ECDSA', hash: 'SHA-256' }, + publicKey, sig, data), + true); + } +} + +async function checkTransferToWorker(key) { + // A one-shot worker that receives a key, asserts its properties, + // signs with it, and echoes the key back together with the signature. + const worker = new Worker(` + 'use strict'; + const { parentPort } = require('node:worker_threads'); + const { subtle } = globalThis.crypto; + parentPort.once('message', async ({ key, expected }) => { + try { + if (key.type !== expected.type || + key.extractable !== expected.extractable || + key.algorithm.name !== expected.algorithm.name || + key.algorithm.hash?.name !== expected.algorithm.hash?.name) { + throw new Error('slot mismatch in worker'); + } + const sig = await subtle.sign('HMAC', key, Buffer.from('wdata')); + // Echo the key back so the parent can verify round-trip. + parentPort.postMessage({ key, sig: Buffer.from(sig) }); + } catch (err) { + parentPort.postMessage({ error: err.message }); + } + }); + `, { eval: true }); + + worker.postMessage({ + key, + expected: { + type: key.type, + extractable: key.extractable, + algorithm: { + name: key.algorithm.name, + hash: key.algorithm.hash ? { name: key.algorithm.hash.name } : undefined, + }, + }, + }); + const [msg] = await once(worker, 'message'); + await worker.terminate(); + + assert.strictEqual(msg.error, undefined, msg.error); + // The key echoed back from the worker must itself be a fully-formed + // CryptoKey with all slots preserved. + assertSameCryptoKey(key, msg.key); + // The signature produced inside the worker must verify against the + // parent-side key. + assert.strictEqual( + await subtle.verify('HMAC', key, msg.sig, Buffer.from('wdata')), + true); +} + +(async () => { + // Extractable HMAC (secret) + const hmacExtractable = await subtle.importKey( + 'raw', + Buffer.from( + '000102030405060708090a0b0c0d0e0f' + + '101112131415161718191a1b1c1d1e1f', 'hex'), + { name: 'HMAC', hash: 'SHA-256' }, + true, + ['sign', 'verify']); + await checkHmacKey(hmacExtractable); + await checkTransferToWorker(hmacExtractable); + + // Non-extractable HMAC (secret) + const hmacNonExtractable = await subtle.generateKey( + { name: 'HMAC', hash: 'SHA-384' }, + false, + ['sign', 'verify']); + await checkHmacKey(hmacNonExtractable); + await checkTransferToWorker(hmacNonExtractable); + + // AES-GCM secret key + { + const key = await subtle.generateKey( + { name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']); + const cloned = structuredClone(key); + assertSameCryptoKey(key, cloned); + const viaPort = await roundTripViaMessageChannel(key); + assertSameCryptoKey(key, viaPort); + const clonedAgain = structuredClone(viaPort); + assertSameCryptoKey(key, clonedAgain); + + const iv = globalThis.crypto.getRandomValues(new Uint8Array(12)); + const plaintext = Buffer.from('secret payload'); + const ciphertext = await subtle.encrypt( + { name: 'AES-GCM', iv }, key, plaintext); + // Decrypt with every clone. + for (const k of [cloned, viaPort, clonedAgain]) { + const decrypted = await subtle.decrypt( + { name: 'AES-GCM', iv }, k, ciphertext); + assert.deepStrictEqual(Buffer.from(decrypted), plaintext); + } + } + + // ECDSA keypair (public extractable, private non-extractable) + const ecKeypair = await subtle.generateKey( + { name: 'ECDSA', namedCurve: 'P-256' }, + false, + ['sign', 'verify']); + await checkAsymmetricKeyPair(ecKeypair); + + // ECDSA with extractable private key (covers the extractable-private path) + const ecKeypairExtractable = await subtle.generateKey( + { name: 'ECDSA', namedCurve: 'P-384' }, + true, + ['sign', 'verify']); + await checkAsymmetricKeyPair(ecKeypairExtractable); +})().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-cryptokey-hidden-slots.js b/test/parallel/test-webcrypto-cryptokey-hidden-slots.js new file mode 100644 index 00000000000000..2225490480d9fa --- /dev/null +++ b/test/parallel/test-webcrypto-cryptokey-hidden-slots.js @@ -0,0 +1,125 @@ +'use strict'; + +// CryptoKey prototype getters (`type`, `extractable`, +// `algorithm`, `usages`) are configurable in the Web Crypto IDL and +// can therefore be replaced by user code. Internal consumers of those +// attributes, and the custom inspect output, must NOT go through the +// public prototype getters, they must read the underlying native +// internal slots directly. This test mutates the prototype getters +// (and mutates the per-instance `algorithm`/`usages` caches returned +// by those getters) and asserts that: +// +// 1. util.inspect() shows the real internal values, unaffected by +// the replacement getters. +// 2. Internal operations (export) that receive the mutated CryptoKey +// still succeed and observe the real internal state, not the +// replaced one. + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('node:assert'); +const { inspect } = require('node:util'); +const { subtle } = globalThis.crypto; + +(async () => { + const key = await subtle.generateKey( + { name: 'HMAC', hash: 'SHA-256' }, + true, + ['sign', 'verify'], + ); + + // Snapshot the real values BEFORE tampering. + const realType = key.type; + const realExtractable = key.extractable; + const realAlgorithm = { ...key.algorithm, hash: { ...key.algorithm.hash } }; + const realUsages = [...key.usages]; + + // 1) Replace all four prototype getters. + let proto = Object.getPrototypeOf(key); + while (proto && !Object.getOwnPropertyDescriptor(proto, 'type')) { + proto = Object.getPrototypeOf(proto); + } + assert.ok(proto, 'could not find CryptoKey.prototype'); + const forgedDescriptors = { + type: { + configurable: true, + enumerable: true, + get() { return 'FORGED-TYPE'; }, + }, + extractable: { + configurable: true, + enumerable: true, + get() { return 'FORGED-EXTRACTABLE'; }, + }, + algorithm: { + configurable: true, + enumerable: true, + get() { return { name: 'FORGED-ALGORITHM', hash: { name: 'FORGED-HASH' } }; }, + }, + usages: { + configurable: true, + enumerable: true, + get() { return ['forged-usage']; }, + }, + }; + const originalDescriptors = {}; + for (const [name, descriptor] of Object.entries(forgedDescriptors)) { + originalDescriptors[name] = Object.getOwnPropertyDescriptor(proto, name); + Object.defineProperty(proto, name, descriptor); + } + + try { + // Confirm the forgeries are in effect from user-code perspective. + assert.strictEqual(key.type, 'FORGED-TYPE'); + assert.strictEqual(key.extractable, 'FORGED-EXTRACTABLE'); + assert.strictEqual(key.algorithm.name, 'FORGED-ALGORITHM'); + assert.deepStrictEqual(key.usages, ['forged-usage']); + + // 2) util.inspect() must not be influenced by the forged getters, + // it must read the real internal slots directly. + const rendered = inspect(key, { depth: 4 }); + assert.match(rendered, /type: 'secret'/); + assert.match(rendered, /extractable: true/); + assert.match(rendered, /name: 'HMAC'/); + assert.match(rendered, /name: 'SHA-256'/); + assert.match(rendered, /'sign'/); + assert.match(rendered, /'verify'/); + assert.doesNotMatch(rendered, /FORGED/); + + // 3) Internal consumers that receive this CryptoKey must see the + // real internal slots. exportKey('jwk') reads [[type]], + // [[extractable]], [[algorithm]], and [[usages]]; if any + // went through the user-visible getter the call would either + // throw or produce forged output. + const jwk = await subtle.exportKey('jwk', key); + assert.strictEqual(jwk.kty, 'oct'); + assert.strictEqual(jwk.alg, 'HS256'); + assert.strictEqual(jwk.ext, true); + assert.deepStrictEqual(jwk.key_ops.sort(), ['sign', 'verify']); + + // 4) Importing back from the exported JWK must yield an equivalent + // key, i.e. the real algorithm and usages round-trip. + const reimported = await subtle.importKey('jwk', jwk, + { name: 'HMAC', hash: 'SHA-256' }, + true, ['sign', 'verify']); + // Reimported's prototype is the same mutated prototype, so we must + // also inspect() the reimported key to check real slots. + const rerendered = inspect(reimported, { depth: 4 }); + assert.match(rerendered, /type: 'secret'/); + assert.match(rerendered, /name: 'HMAC'/); + assert.doesNotMatch(rerendered, /FORGED/); + } finally { + // Restore the original getters so subsequent tests are unaffected. + for (const [name, descriptor] of Object.entries(originalDescriptors)) { + Object.defineProperty(proto, name, descriptor); + } + } + + // After restoration, the real values come back through the public API. + assert.strictEqual(key.type, realType); + assert.strictEqual(key.extractable, realExtractable); + assert.deepStrictEqual(key.algorithm, realAlgorithm); + assert.deepStrictEqual(key.usages, realUsages); +})().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-cryptokey-no-own-symbols.js b/test/parallel/test-webcrypto-cryptokey-no-own-symbols.js new file mode 100644 index 00000000000000..ee328e0d10c65b --- /dev/null +++ b/test/parallel/test-webcrypto-cryptokey-no-own-symbols.js @@ -0,0 +1,52 @@ +'use strict'; + +// CryptoKey instances must not expose any own Symbol-keyed properties +// to user code. The internal slots backing the public getters are +// kept off-instance (in a module-local WeakMap) so that reflection +// APIs such as Object.getOwnPropertySymbols() and Reflect.ownKeys() +// cannot enumerate them, even after the public getters have been +// invoked and their per-instance caches populated. + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('node:assert'); +const { subtle } = globalThis.crypto; + +(async () => { + const keys = [ + await subtle.generateKey( + { name: 'HMAC', hash: 'SHA-256' }, true, ['sign', 'verify']), + (await subtle.generateKey( + { name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify'])).privateKey, + (await subtle.generateKey( + { name: 'RSA-PSS', modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256' }, + true, ['sign', 'verify'])).publicKey, + await subtle.generateKey( + { name: 'AES-GCM', length: 128 }, true, ['encrypt', 'decrypt']), + ]; + + for (const key of keys) { + // Touch every public getter so any lazy per-instance caching would + // materialise now. + /* eslint-disable no-unused-expressions */ + key.type; + key.extractable; + key.algorithm; + key.usages; + // Read the getters a second time to exercise the cache-hit path. + key.algorithm; + key.usages; + /* eslint-enable no-unused-expressions */ + + assert.deepStrictEqual( + Object.getOwnPropertySymbols(key), [], + `CryptoKey has own Symbol properties: ${ + Object.getOwnPropertySymbols(key).map(String).join(', ')}`, + ); + assert.deepStrictEqual(Object.getOwnPropertyNames(key), []); + assert.deepStrictEqual(Reflect.ownKeys(key), []); + } +})().then(common.mustCall()); From 4a0dbab816c6a86978a663323a804efefe7d2477 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Fri, 24 Apr 2026 14:14:44 +0200 Subject: [PATCH 09/12] benchmark: add Web Crypto sign/verify benchmarks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- benchmark/crypto/webcrypto-sign.js | 97 ++++++++++++++++++++++++++ benchmark/crypto/webcrypto-verify.js | 100 +++++++++++++++++++++++++++ 2 files changed, 197 insertions(+) create mode 100644 benchmark/crypto/webcrypto-sign.js create mode 100644 benchmark/crypto/webcrypto-verify.js diff --git a/benchmark/crypto/webcrypto-sign.js b/benchmark/crypto/webcrypto-sign.js new file mode 100644 index 00000000000000..d298ec69eef437 --- /dev/null +++ b/benchmark/crypto/webcrypto-sign.js @@ -0,0 +1,97 @@ +'use strict'; + +const common = require('../common.js'); +const { hasOpenSSL } = require('../../test/common/crypto.js'); +const { subtle } = globalThis.crypto; + +const kAlgorithms = { + 'ec': { name: 'ECDSA', namedCurve: 'P-256' }, + 'rsassa-pkcs1-v1_5': { + name: 'RSASSA-PKCS1-v1_5', + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256', + }, + 'rsa-pss': { + name: 'RSA-PSS', + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256', + }, + 'ed25519': { name: 'Ed25519' }, +}; + +if (hasOpenSSL(3, 5)) { + kAlgorithms['ml-dsa-44'] = { name: 'ML-DSA-44' }; +} + +const kSignParams = { + 'ec': { name: 'ECDSA', hash: 'SHA-256' }, + 'rsassa-pkcs1-v1_5': { name: 'RSASSA-PKCS1-v1_5' }, + 'rsa-pss': { name: 'RSA-PSS', saltLength: 32 }, + 'ed25519': { name: 'Ed25519' }, + 'ml-dsa-44': { name: 'ML-DSA-44' }, +}; + +const data = globalThis.crypto.getRandomValues(new Uint8Array(256)); + +let keys; + +const bench = common.createBenchmark(main, { + keyType: Object.keys(kAlgorithms), + mode: ['serial', 'parallel'], + keyReuse: ['shared', 'unique'], + n: [1e3], +}, { + combinationFilter(p) { + // Unique only differs from shared when operations overlap (parallel); + // sequential calls have no contention so unique+serial adds no value. + if (p.keyReuse === 'unique') return p.mode === 'parallel'; + return true; + }, +}); + +async function measureSerial(n, signParams, sharedKey) { + bench.start(); + for (let i = 0; i < n; ++i) { + await subtle.sign(signParams, sharedKey || keys[i], data); + } + bench.end(n); +} + +async function measureParallel(n, signParams, sharedKey) { + const promises = new Array(n); + bench.start(); + for (let i = 0; i < n; ++i) { + promises[i] = subtle.sign(signParams, sharedKey || keys[i], data); + } + await Promise.all(promises); + bench.end(n); +} + +async function main({ n, mode, keyReuse, keyType }) { + const algorithm = kAlgorithms[keyType]; + const signParams = kSignParams[keyType]; + + if (!keys || keys.length !== n || keys[0].algorithm.name !== signParams.name) { + keys = new Array(n); + // Generate one key pair, then import its pkcs8 bytes n times to get + // distinct CryptoKey instances. + const kp = await subtle.generateKey(algorithm, true, ['sign', 'verify']); + const pkcs8 = await subtle.exportKey('pkcs8', kp.privateKey); + for (let i = 0; i < n; ++i) { + keys[i] = await subtle.importKey('pkcs8', pkcs8, algorithm, false, ['sign']); + } + } + + const sharedKey = keyReuse === 'shared' ? keys[0] : undefined; + + switch (mode) { + case 'serial': + await measureSerial(n, signParams, sharedKey); + break; + case 'parallel': + await measureParallel(n, signParams, sharedKey); + break; + } +} diff --git a/benchmark/crypto/webcrypto-verify.js b/benchmark/crypto/webcrypto-verify.js new file mode 100644 index 00000000000000..6b0d52cddc30d8 --- /dev/null +++ b/benchmark/crypto/webcrypto-verify.js @@ -0,0 +1,100 @@ +'use strict'; + +const common = require('../common.js'); +const { hasOpenSSL } = require('../../test/common/crypto.js'); +const { subtle } = globalThis.crypto; + +const kAlgorithms = { + 'ec': { name: 'ECDSA', namedCurve: 'P-256' }, + 'rsassa-pkcs1-v1_5': { + name: 'RSASSA-PKCS1-v1_5', + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256', + }, + 'rsa-pss': { + name: 'RSA-PSS', + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256', + }, + 'ed25519': { name: 'Ed25519' }, +}; + +if (hasOpenSSL(3, 5)) { + kAlgorithms['ml-dsa-44'] = { name: 'ML-DSA-44' }; +} + +const kSignParams = { + 'ec': { name: 'ECDSA', hash: 'SHA-256' }, + 'rsassa-pkcs1-v1_5': { name: 'RSASSA-PKCS1-v1_5' }, + 'rsa-pss': { name: 'RSA-PSS', saltLength: 32 }, + 'ed25519': { name: 'Ed25519' }, + 'ml-dsa-44': { name: 'ML-DSA-44' }, +}; + +const data = globalThis.crypto.getRandomValues(new Uint8Array(256)); + +let publicKeys; +let signature; + +const bench = common.createBenchmark(main, { + keyType: Object.keys(kAlgorithms), + mode: ['serial', 'parallel'], + keyReuse: ['shared', 'unique'], + n: [1e3], +}, { + combinationFilter(p) { + // Unique only differs from shared when operations overlap (parallel); + // sequential calls have no contention so unique+serial adds no value. + if (p.keyReuse === 'unique') return p.mode === 'parallel'; + return true; + }, +}); + +async function measureSerial(n, verifyParams, sharedKey) { + bench.start(); + for (let i = 0; i < n; ++i) { + await subtle.verify(verifyParams, sharedKey || publicKeys[i], signature, data); + } + bench.end(n); +} + +async function measureParallel(n, verifyParams, sharedKey) { + const promises = new Array(n); + bench.start(); + for (let i = 0; i < n; ++i) { + promises[i] = subtle.verify(verifyParams, sharedKey || publicKeys[i], signature, data); + } + await Promise.all(promises); + bench.end(n); +} + +async function main({ n, mode, keyReuse, keyType }) { + const algorithm = kAlgorithms[keyType]; + const verifyParams = kSignParams[keyType]; + + if (!publicKeys || publicKeys.length !== n || + publicKeys[0].algorithm.name !== verifyParams.name) { + publicKeys = new Array(n); + // Generate one key pair, then import its spki bytes n times to get + // distinct CryptoKey instances. + const kp = await subtle.generateKey(algorithm, true, ['sign', 'verify']); + const spki = await subtle.exportKey('spki', kp.publicKey); + for (let i = 0; i < n; ++i) { + publicKeys[i] = await subtle.importKey('spki', spki, algorithm, false, ['verify']); + } + signature = await subtle.sign(verifyParams, kp.privateKey, data); + } + + const sharedKey = keyReuse === 'shared' ? publicKeys[0] : undefined; + + switch (mode) { + case 'serial': + await measureSerial(n, verifyParams, sharedKey); + break; + case 'parallel': + await measureParallel(n, verifyParams, sharedKey); + break; + } +} From 1cb4aaddaf719e8d0dedc4feed139e837dc6650b Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Fri, 24 Apr 2026 16:31:07 +0200 Subject: [PATCH 10/12] fixup! src,crypto: add NativeCryptoKey Signed-off-by: Filip Skokan --- src/crypto/crypto_keys.cc | 17 ----------------- src/crypto/crypto_keys.h | 3 +-- 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/src/crypto/crypto_keys.cc b/src/crypto/crypto_keys.cc index c0497ffd289d30..ae9c8b9b105799 100644 --- a/src/crypto/crypto_keys.cc +++ b/src/crypto/crypto_keys.cc @@ -1793,10 +1793,6 @@ void NativeCryptoKey::Initialize(Environment* env, Local target) { target, "createCryptoKeyClass", NativeCryptoKey::CreateCryptoKeyClass); - SetMethod(env->context(), - target, - "getCryptoKeyHandle", - NativeCryptoKey::GetKeyHandle); SetMethod( env->context(), target, "getCryptoKeySlots", NativeCryptoKey::GetSlots); } @@ -1804,7 +1800,6 @@ void NativeCryptoKey::Initialize(Environment* env, Local target) { void NativeCryptoKey::RegisterExternalReferences( ExternalReferenceRegistry* registry) { registry->Register(NativeCryptoKey::CreateCryptoKeyClass); - registry->Register(NativeCryptoKey::GetKeyHandle); registry->Register(NativeCryptoKey::GetSlots); registry->Register(NativeCryptoKey::New); } @@ -1900,18 +1895,6 @@ void NativeCryptoKey::CreateCryptoKeyClass( args.GetReturnValue().Set(ret); } -void NativeCryptoKey::GetKeyHandle(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - CHECK_EQ(args.Length(), 1); - CHECK(HasInstance(args[0])); - NativeCryptoKey* native = Unwrap(args[0].As()); - Local handle; - if (!KeyObjectHandle::Create(env, native->handle_data_).ToLocal(&handle)) { - return; - } - args.GetReturnValue().Set(handle); -} - // Returns all of the key's internal slot values as a single Array: // [type, extractable, algorithm, usages, handle]. JS-side helpers // call this once per key to prime a per-instance cache, so subsequent diff --git a/src/crypto/crypto_keys.h b/src/crypto/crypto_keys.h index 1198b9d856c486..e71083e93b3a8a 100644 --- a/src/crypto/crypto_keys.h +++ b/src/crypto/crypto_keys.h @@ -258,10 +258,9 @@ class NativeCryptoKey : public BaseObject { // True if `value` is a real NativeCryptoKey instance. Checks the // kClassTagField internal field. - // Used by `GetSlots` / `GetKeyHandle` to validate their receiver. + // Used by `GetSlots` to validate its receiver. static bool HasInstance(v8::Local value); - static void GetKeyHandle(const v8::FunctionCallbackInfo& args); // Returns [type, extractable, algorithm, usages, handle] in one call // so JS can prime a per-instance cache on first access. static void GetSlots(const v8::FunctionCallbackInfo& args); From e11737be9587b263b331c16661cc8a2371ec09f5 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Fri, 24 Apr 2026 16:36:36 +0200 Subject: [PATCH 11/12] fixup! lib,crypto: rewire CryptoKey on the native class Signed-off-by: Filip Skokan --- lib/eslint.config_partial.mjs | 4 ++-- lib/internal/crypto/keys.js | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/eslint.config_partial.mjs b/lib/eslint.config_partial.mjs index e2701cf3d473ef..61632a7ee447e6 100644 --- a/lib/eslint.config_partial.mjs +++ b/lib/eslint.config_partial.mjs @@ -72,8 +72,8 @@ export default [ message: 'Use `FunctionPrototypeCall` to avoid creating an ad-hoc array', }, { - selector: "VariableDeclarator[init.type='CallExpression'][init.callee.name='internalBinding'][init.arguments.0.value='crypto'] > ObjectPattern > Property[key.name='getCryptoKeyHandle']", - message: "Use `const { getCryptoKeyHandle } = require('internal/crypto/keys');` instead of destructuring it from `internalBinding('crypto')`.", + selector: "VariableDeclarator[init.type='CallExpression'][init.callee.name='internalBinding'][init.arguments.0.value='crypto'] > ObjectPattern > Property[key.name='getCryptoKeySlots']", + message: "Use `const { getCryptoKeySlots } = require('internal/crypto/keys');` instead of destructuring it from `internalBinding('crypto')`.", }, ], 'no-restricted-globals': [ diff --git a/lib/internal/crypto/keys.js b/lib/internal/crypto/keys.js index 84f0054b3e9f82..36d523fbf9841b 100644 --- a/lib/internal/crypto/keys.js +++ b/lib/internal/crypto/keys.js @@ -16,6 +16,7 @@ const { KeyObjectHandle, createNativeKeyObjectClass, createCryptoKeyClass, + // eslint-disable-next-line no-restricted-syntax -- intended here getCryptoKeySlots: nativeGetCryptoKeySlots, kKeyTypeSecret, kKeyTypePublic, From 74cba08bc70c437d0153e657226ef4e758d08092 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Fri, 24 Apr 2026 19:14:57 +0200 Subject: [PATCH 12/12] fixup! src,crypto: add NativeCryptoKey Signed-off-by: Filip Skokan --- src/crypto/crypto_keys.cc | 75 +++++++++++++++++++-------------------- src/crypto/crypto_keys.h | 11 +++--- src/env_properties.h | 1 + 3 files changed, 43 insertions(+), 44 deletions(-) diff --git a/src/crypto/crypto_keys.cc b/src/crypto/crypto_keys.cc index ae9c8b9b105799..8c5cc660faf30b 100644 --- a/src/crypto/crypto_keys.cc +++ b/src/crypto/crypto_keys.cc @@ -1805,25 +1805,16 @@ void NativeCryptoKey::RegisterExternalReferences( } namespace { -// Brand check: every NativeCryptoKey stores this pointer in its -// kClassTagField slot. Nothing else in the binary can produce the -// same pointer, so HasInstance() can use it to recognize a real -// NativeCryptoKey. -constexpr int kNativeCryptoKeyClassTag = 0; -const void* class_tag() { - return &kNativeCryptoKeyClassTag; +// Verifies that `value` is a `NativeCryptoKey` by checking whether it +// was constructed from the Environment's `NativeCryptoKey` template. +bool IsNativeCryptoKey(Environment* env, Local value) { + auto t = env->crypto_cryptokey_constructor_template(); + return !t.IsEmpty() && t->HasInstance(value); } } // namespace -bool NativeCryptoKey::HasInstance(Local value) { - if (!value->IsObject()) return false; - Local obj = value.As(); - if (obj->InternalFieldCount() < NativeCryptoKey::kInternalFieldCount) { - return false; - } - return obj->GetAlignedPointerFromInternalField( - NativeCryptoKey::kClassTagField, EmbedderDataTag::kDefault) == - class_tag(); +bool NativeCryptoKey::HasInstance(Environment* env, Local value) { + return IsNativeCryptoKey(env, value); } void NativeCryptoKey::New(const FunctionCallbackInfo& args) { @@ -1848,17 +1839,12 @@ void NativeCryptoKey::New(const FunctionCallbackInfo& args) { auto* native = new NativeCryptoKey(env, args.This(), handle->Data()); - // Brand-check tag for HasInstance(). - args.This()->SetAlignedPointerInInternalField(kClassTagField, - const_cast(class_tag()), - EmbedderDataTag::kDefault); - if (!args[1]->IsUndefined()) { CHECK(args[1]->IsObject()); CHECK(args[2]->IsArray()); CHECK(args[3]->IsBoolean()); - native->algorithm_.Reset(env->isolate(), args[1].As()); - native->usages_.Reset(env->isolate(), args[2].As()); + args.This()->SetInternalField(kAlgorithmField, args[1]); + args.This()->SetInternalField(kUsagesField, args[2]); native->extractable_ = args[3]->IsTrue(); } } @@ -1876,6 +1862,8 @@ void NativeCryptoKey::CreateCryptoKeyClass( NewFunctionTemplate(isolate, NativeCryptoKey::New); t->InstanceTemplate()->SetInternalFieldCount( NativeCryptoKey::kInternalFieldCount); + CHECK(env->crypto_cryptokey_constructor_template().IsEmpty()); + env->set_crypto_cryptokey_constructor_template(t); Local ctor; if (!t->GetFunction(env->context()).ToLocal(&ctor)) return; @@ -1890,6 +1878,7 @@ void NativeCryptoKey::CreateCryptoKeyClass( Local ret = ret_v.As(); Local internal_ctor_v; if (!ret->Get(env->context(), 1).ToLocal(&internal_ctor_v)) return; + CHECK(env->crypto_internal_cryptokey_constructor().IsEmpty()); env->set_crypto_internal_cryptokey_constructor( internal_ctor_v.As()); args.GetReturnValue().Set(ret); @@ -1902,12 +1891,13 @@ void NativeCryptoKey::CreateCryptoKeyClass( void NativeCryptoKey::GetSlots(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); CHECK_EQ(args.Length(), 1); - if (!HasInstance(args[0])) { + if (!HasInstance(env, args[0])) { THROW_ERR_INVALID_THIS(env, "Value of \"this\" must be of type CryptoKey"); return; } Isolate* isolate = env->isolate(); - NativeCryptoKey* native = Unwrap(args[0].As()); + Local obj = args[0].As(); + NativeCryptoKey* native = Unwrap(obj); const char* type_str; switch (native->handle_data_.GetKeyType()) { @@ -1929,13 +1919,15 @@ void NativeCryptoKey::GetSlots(const FunctionCallbackInfo& args) { return; } - CHECK(!native->algorithm_.IsEmpty()); - CHECK(!native->usages_.IsEmpty()); + Local algorithm = obj->GetInternalField(kAlgorithmField).As(); + Local usages = obj->GetInternalField(kUsagesField).As(); + CHECK(algorithm->IsObject()); + CHECK(usages->IsArray()); Local slots[] = { OneByteString(isolate, type_str), v8::Boolean::New(isolate, native->extractable_), - PersistentToLocal::Strong(native->algorithm_), - PersistentToLocal::Strong(native->usages_), + algorithm, + usages, handle, }; args.GetReturnValue().Set(Array::New(isolate, slots, arraysize(slots))); @@ -1948,11 +1940,13 @@ BaseObject::TransferMode NativeCryptoKey::GetTransferMode() const { std::unique_ptr NativeCryptoKey::CloneForMessaging() const { Isolate* isolate = env()->isolate(); - CHECK(!algorithm_.IsEmpty()); - CHECK(!usages_.IsEmpty()); - v8::Global algorithm_copy(isolate, - PersistentToLocal::Strong(algorithm_)); - v8::Global usages_copy(isolate, PersistentToLocal::Strong(usages_)); + Local obj = object(); + Local algorithm_v = obj->GetInternalField(kAlgorithmField).As(); + Local usages_v = obj->GetInternalField(kUsagesField).As(); + CHECK(algorithm_v->IsObject()); + CHECK(usages_v->IsArray()); + v8::Global algorithm_copy(isolate, algorithm_v.As()); + v8::Global usages_copy(isolate, usages_v.As()); return std::make_unique(handle_data_, std::move(algorithm_copy), std::move(usages_copy), @@ -1968,6 +1962,13 @@ Maybe NativeCryptoKey::FinalizeTransferRead( CHECK(bundle_v->IsObject()); Local bundle = bundle_v.As(); Isolate* isolate = env()->isolate(); + Local obj = object(); + + // The partially-initialized object produced by + // CryptoKeyTransferData::Deserialize should not have algorithm/usages + // set yet. + CHECK(obj->GetInternalField(kAlgorithmField).As()->IsUndefined()); + CHECK(obj->GetInternalField(kUsagesField).As()->IsUndefined()); Local algorithm_v; if (!bundle->Get(context, FIXED_ONE_BYTE_STRING(isolate, "algorithm")) @@ -1975,7 +1976,7 @@ Maybe NativeCryptoKey::FinalizeTransferRead( return Nothing(); } CHECK(algorithm_v->IsObject()); - algorithm_.Reset(isolate, algorithm_v.As()); + obj->SetInternalField(kAlgorithmField, algorithm_v); Local usages_v; if (!bundle->Get(context, FIXED_ONE_BYTE_STRING(isolate, "usages")) @@ -1983,7 +1984,7 @@ Maybe NativeCryptoKey::FinalizeTransferRead( return Nothing(); } CHECK(usages_v->IsArray()); - usages_.Reset(isolate, usages_v.As()); + obj->SetInternalField(kUsagesField, usages_v); Local extractable_v; if (!bundle->Get(context, FIXED_ONE_BYTE_STRING(isolate, "extractable")) @@ -2067,8 +2068,6 @@ BaseObjectPtr NativeCryptoKey::CryptoKeyTransferData::Deserialize( void NativeCryptoKey::MemoryInfo(MemoryTracker* tracker) const { tracker->TrackField("handle_data", handle_data_); - tracker->TrackField("algorithm", algorithm_); - tracker->TrackField("usages", usages_); } void NativeCryptoKey::CryptoKeyTransferData::MemoryInfo( diff --git a/src/crypto/crypto_keys.h b/src/crypto/crypto_keys.h index e71083e93b3a8a..3c2c840f4e1bbf 100644 --- a/src/crypto/crypto_keys.h +++ b/src/crypto/crypto_keys.h @@ -245,7 +245,8 @@ class NativeKeyObject : public BaseObject { class NativeCryptoKey : public BaseObject { public: enum InternalFields { - kClassTagField = BaseObject::kInternalFieldCount, + kAlgorithmField = BaseObject::kInternalFieldCount, + kUsagesField, kInternalFieldCount, }; @@ -256,10 +257,10 @@ class NativeCryptoKey : public BaseObject { static void CreateCryptoKeyClass( const v8::FunctionCallbackInfo& args); - // True if `value` is a real NativeCryptoKey instance. Checks the - // kClassTagField internal field. + // True if `value` is a real NativeCryptoKey instance. Uses the + // FunctionTemplate stored on the Environment as a brand check. // Used by `GetSlots` to validate its receiver. - static bool HasInstance(v8::Local value); + static bool HasInstance(Environment* env, v8::Local value); // Returns [type, extractable, algorithm, usages, handle] in one call // so JS can prime a per-instance cache on first access. @@ -317,8 +318,6 @@ class NativeCryptoKey : public BaseObject { } KeyObjectData handle_data_; - v8::Global algorithm_; - v8::Global usages_; bool extractable_ = false; }; diff --git a/src/env_properties.h b/src/env_properties.h index 8713248da8df68..92bb3b63de8003 100644 --- a/src/env_properties.h +++ b/src/env_properties.h @@ -403,6 +403,7 @@ V(contextify_global_template, v8::ObjectTemplate) \ V(contextify_wrapper_template, v8::ObjectTemplate) \ V(cpu_usage_template, v8::DictionaryTemplate) \ + V(crypto_cryptokey_constructor_template, v8::FunctionTemplate) \ V(crypto_key_object_handle_constructor, v8::FunctionTemplate) \ V(env_proxy_template, v8::ObjectTemplate) \ V(env_proxy_ctor_template, v8::FunctionTemplate) \