From 7b86953353055deacbaf38b56659946678585259 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Fri, 17 Jul 2026 10:46:05 +0200 Subject: [PATCH] src: avoid redundant KEM encapsulation copies KEM encapsulation produces separate ciphertext and shared-secret allocations. The existing DeriveBitsJob path packs both values into an intermediate buffer, then copies them again into separate buffers. Instead, this uses a dedicated KEMEncapsulateJob to retain both outputs across the worker boundary and convert each directly through ByteSource. This removes the intermediate allocation and at least one complete round of copies. Signed-off-by: Filip Skokan --- src/crypto/crypto_kem.cc | 183 +++++++++++++++++++-------------------- src/crypto/crypto_kem.h | 31 +++++-- 2 files changed, 110 insertions(+), 104 deletions(-) diff --git a/src/crypto/crypto_kem.cc b/src/crypto/crypto_kem.cc index 09fbf0844f48f2..2cbeed3660d131 100644 --- a/src/crypto/crypto_kem.cc +++ b/src/crypto/crypto_kem.cc @@ -8,7 +8,6 @@ #include "crypto/crypto_util.h" #include "env-inl.h" #include "memory_tracker-inl.h" -#include "node_buffer.h" #include "threadpoolwork-inl.h" #include "v8.h" @@ -16,13 +15,13 @@ namespace node { using ncrypto::EVPKeyPointer; using v8::Array; -using v8::ArrayBufferView; using v8::FunctionCallbackInfo; using v8::Local; using v8::Maybe; using v8::MaybeLocal; using v8::Nothing; using v8::Object; +using v8::Uint8Array; using v8::Value; namespace crypto { @@ -49,49 +48,6 @@ void KEMConfiguration::MemoryInfo(MemoryTracker* tracker) const { namespace { -bool DoKEMEncapsulate(Environment* env, - const EVPKeyPointer& public_key, - ByteSource* out, - CryptoJobMode mode, - CryptoErrorStore* errors) { - auto result = ncrypto::KEM::Encapsulate(public_key); - if (!result) { - errors->Insert(NodeCryptoError::ENCAPSULATION_FAILED); - errors->SetNodeErrorCode("ERR_CRYPTO_OPERATION_FAILED"); - return false; - } - - // Pack the result: [ciphertext_len][shared_key_len][ciphertext][shared_key] - size_t ciphertext_len = result->ciphertext.size(); - size_t shared_key_len = result->shared_key.size(); - size_t total_len = - sizeof(uint32_t) + sizeof(uint32_t) + ciphertext_len + shared_key_len; - - auto data = ncrypto::DataPointer::Alloc(total_len); - if (!data) { - errors->Insert(NodeCryptoError::ALLOCATION_FAILED); - errors->SetNodeErrorCode("ERR_CRYPTO_OPERATION_FAILED"); - return false; - } - - unsigned char* ptr = static_cast(data.get()); - - // Write size headers - *reinterpret_cast(ptr) = static_cast(ciphertext_len); - *reinterpret_cast(ptr + sizeof(uint32_t)) = - static_cast(shared_key_len); - - // Write ciphertext and shared key data - unsigned char* ciphertext_ptr = ptr + 2 * sizeof(uint32_t); - unsigned char* shared_key_ptr = ciphertext_ptr + ciphertext_len; - - std::memcpy(ciphertext_ptr, result->ciphertext.get(), ciphertext_len); - std::memcpy(shared_key_ptr, result->shared_key.get(), shared_key_len); - - *out = ByteSource::Allocated(data.release()); - return true; -} - bool DoKEMDecapsulate(Environment* env, const EVPKeyPointer& private_key, const ByteSource& ciphertext, @@ -133,72 +89,109 @@ Maybe KEMEncapsulateTraits::AdditionalConfig( return v8::JustVoid(); } -bool KEMEncapsulateTraits::DeriveBits(Environment* env, - const KEMConfiguration& params, - ByteSource* out, - CryptoJobMode mode, - CryptoErrorStore* errors) { - Mutex::ScopedLock lock(params.key.mutex()); - const auto& public_key = params.key.GetAsymmetricKey(); +void KEMEncapsulateJob::New(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CHECK(args.IsConstructCall()); + + CryptoJobMode mode = GetCryptoJobMode(args[0]); + AdditionalParams params; + if (KEMEncapsulateTraits::AdditionalConfig(mode, args, 1, ¶ms) + .IsNothing()) { + return; + } - return DoKEMEncapsulate(env, public_key, out, mode, errors); + new KEMEncapsulateJob(env, args.This(), mode, std::move(params)); } -MaybeLocal KEMEncapsulateTraits::EncodeOutput( - Environment* env, const KEMConfiguration& params, ByteSource* out) { - // The output contains: - // [ciphertext_len][shared_key_len][ciphertext][shared_key] - const unsigned char* data = out->data(); - - uint32_t ciphertext_len = *reinterpret_cast(data); - uint32_t shared_key_len = - *reinterpret_cast(data + sizeof(uint32_t)); - - const unsigned char* ciphertext_ptr = data + 2 * sizeof(uint32_t); - const unsigned char* shared_key_ptr = ciphertext_ptr + ciphertext_len; - - MaybeLocal ciphertext_buf = - node::Buffer::Copy(env->isolate(), - reinterpret_cast(ciphertext_ptr), - ciphertext_len); - - MaybeLocal shared_key_buf = - node::Buffer::Copy(env->isolate(), - reinterpret_cast(shared_key_ptr), - shared_key_len); - - Local ciphertext_obj; - Local shared_key_obj; - if (!ciphertext_buf.ToLocal(&ciphertext_obj) || - !shared_key_buf.ToLocal(&shared_key_obj)) { - return MaybeLocal(); +void KEMEncapsulateJob::Initialize(Environment* env, Local target) { + CryptoJob::Initialize(New, env, target); +} + +void KEMEncapsulateJob::RegisterExternalReferences( + ExternalReferenceRegistry* registry) { + CryptoJob::RegisterExternalReferences(New, registry); +} + +KEMEncapsulateJob::KEMEncapsulateJob(Environment* env, + Local object, + CryptoJobMode mode, + AdditionalParams&& params) + : CryptoJob(env, + object, + KEMEncapsulateTraits::Provider, + mode, + std::move(params)) {} + +void KEMEncapsulateJob::DoThreadPoolWork() { + ncrypto::ClearErrorOnReturn clear_error_on_return; + AdditionalParams* params = CryptoJob::params(); + Mutex::ScopedLock lock(params->key.mutex()); + out_ = ncrypto::KEM::Encapsulate(params->key.GetAsymmetricKey()); + if (!out_) { + CryptoErrorStore* errors = CryptoJob::errors(); + errors->Insert(NodeCryptoError::ENCAPSULATION_FAILED); + errors->SetNodeErrorCode("ERR_CRYPTO_OPERATION_FAILED"); } +} - if (params.job_mode == kCryptoJobWebCrypto) { - Local result = Object::New(env->isolate()); - if (!result +Maybe KEMEncapsulateJob::ToResult(Local* err, + Local* result) { + Environment* env = AsyncWrap::env(); + CryptoErrorStore* errors = CryptoJob::errors(); + if (!out_) { + if (errors->Empty()) errors->Capture(); + CHECK(!errors->Empty()); + *result = v8::Undefined(env->isolate()); + if (!errors->ToException(env).ToLocal(err)) return Nothing(); + return v8::JustVoid(); + } + + CHECK(errors->Empty()); + *err = v8::Undefined(env->isolate()); + + ByteSource ciphertext = ByteSource::Allocated(out_->ciphertext.release()); + ByteSource shared_key = ByteSource::Allocated(out_->shared_key.release()); + + if (mode() == kCryptoJobWebCrypto) { + Local output = Object::New(env->isolate()); + if (!output ->DefineOwnProperty(env->context(), OneByteString(env->isolate(), "sharedKey"), - shared_key_obj.As()->Buffer()) + shared_key.ToArrayBuffer(env)) .FromMaybe(false) || - !result + !output ->DefineOwnProperty(env->context(), OneByteString(env->isolate(), "ciphertext"), - ciphertext_obj.As()->Buffer()) + ciphertext.ToArrayBuffer(env)) .FromMaybe(false)) { - return MaybeLocal(); + return Nothing(); } - return result; + *result = output; + return v8::JustVoid(); } - // Return an array [sharedKey, ciphertext]. - Local result = Array::New(env->isolate(), 2); - if (result->Set(env->context(), 0, shared_key_obj).IsNothing() || - result->Set(env->context(), 1, ciphertext_obj).IsNothing()) { - return MaybeLocal(); + Local shared_key_buf; + Local ciphertext_buf; + if (!shared_key.ToBuffer(env).ToLocal(&shared_key_buf) || + !ciphertext.ToBuffer(env).ToLocal(&ciphertext_buf)) { + return Nothing(); } - return result; + Local output = Array::New(env->isolate(), 2); + if (output->Set(env->context(), 0, shared_key_buf).IsNothing() || + output->Set(env->context(), 1, ciphertext_buf).IsNothing()) { + return Nothing(); + } + *result = output; + return v8::JustVoid(); +} + +void KEMEncapsulateJob::MemoryInfo(MemoryTracker* tracker) const { + if (out_) { + tracker->TrackFieldWithSize("ciphertext", out_->ciphertext.size()); + tracker->TrackFieldWithSize("shared_key", out_->shared_key.size()); + } + CryptoJob::MemoryInfo(tracker); } // KEMDecapsulateTraits implementation diff --git a/src/crypto/crypto_kem.h b/src/crypto/crypto_kem.h index e00aa04baa897e..70eb96d57c6387 100644 --- a/src/crypto/crypto_kem.h +++ b/src/crypto/crypto_kem.h @@ -44,16 +44,30 @@ struct KEMEncapsulateTraits final { const v8::FunctionCallbackInfo& args, unsigned int offset, KEMConfiguration* params); +}; - static bool DeriveBits(Environment* env, - const KEMConfiguration& params, - ByteSource* out, - CryptoJobMode mode, - CryptoErrorStore* errors); +class KEMEncapsulateJob final : public CryptoJob { + public: + using AdditionalParams = KEMEncapsulateTraits::AdditionalParameters; - static v8::MaybeLocal EncodeOutput(Environment* env, - const KEMConfiguration& params, - ByteSource* out); + static void New(const v8::FunctionCallbackInfo& args); + static void Initialize(Environment* env, v8::Local target); + static void RegisterExternalReferences(ExternalReferenceRegistry* registry); + + KEMEncapsulateJob(Environment* env, + v8::Local object, + CryptoJobMode mode, + AdditionalParams&& params); + + void DoThreadPoolWork() override; + v8::Maybe ToResult(v8::Local* err, + v8::Local* result) override; + + SET_SELF_SIZE(KEMEncapsulateJob) + void MemoryInfo(MemoryTracker* tracker) const override; + + private: + std::optional out_; }; struct KEMDecapsulateTraits final { @@ -80,7 +94,6 @@ struct KEMDecapsulateTraits final { ByteSource* out); }; -using KEMEncapsulateJob = DeriveBitsJob; using KEMDecapsulateJob = DeriveBitsJob; void InitializeKEM(Environment* env, v8::Local target);