#ifndef SRC_CRYPTO_CRYPTO_UTIL_H_ #define SRC_CRYPTO_CRYPTO_UTIL_H_ #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS #include "async_wrap.h" #include "env.h" #include "node_errors.h" #include "node_external_reference.h" #include "node_internals.h" #include "string_bytes.h" #include "util.h" #include "v8.h" #include "ncrypto.h" #include #include #include #include #include #include #include namespace node::crypto { // Currently known sizes of commonly used OpenSSL struct sizes. // OpenSSL considers it's various structs to be opaque and the // sizes may change from one version of OpenSSL to another, so // these values should not be trusted to remain static. These // are provided to allow for some close to reasonable memory // tracking. constexpr size_t kSizeOf_DH = 144; constexpr size_t kSizeOf_EC_KEY = 80; constexpr size_t kSizeOf_EVP_CIPHER_CTX = 168; constexpr size_t kSizeOf_EVP_MD_CTX = 48; constexpr size_t kSizeOf_EVP_PKEY = 72; constexpr size_t kSizeOf_EVP_PKEY_CTX = 80; constexpr size_t kSizeOf_HMAC_CTX = 32; bool ProcessFipsOptions(); bool InitCryptoOnce(v8::Isolate* isolate); void InitCryptoOnce(); void InitCrypto(v8::Local target); extern void UseExtraCaCerts(std::string_view file); void CleanupCachedRootCertificates(); int PasswordCallback(char* buf, int size, int rwflag, void* u); int NoPasswordCallback(char* buf, int size, int rwflag, void* u); // Decode is used by the various stream-based crypto utilities to decode // string input. template void Decode(const v8::FunctionCallbackInfo& args, void (*callback)(T*, const v8::FunctionCallbackInfo&, const char*, size_t)) { T* ctx; ASSIGN_OR_RETURN_UNWRAP(&ctx, args.This()); if (args[0]->IsString()) { StringBytes::InlineDecoder decoder; Environment* env = Environment::GetCurrent(args); enum encoding enc = ParseEncoding(env->isolate(), args[1], UTF8); if (decoder.Decode(env, args[0].As(), enc).IsNothing()) return; callback(ctx, args, decoder.out(), decoder.size()); } else { ArrayBufferViewContents buf(args[0]); callback(ctx, args, buf.data(), buf.length()); } } #define NODE_CRYPTO_ERROR_CODES_MAP(V) \ V(CIPHER_JOB_FAILED, "Cipher job failed") \ V(DERIVING_BITS_FAILED, "Deriving bits failed") \ V(ENGINE_NOT_FOUND, "Engine \"%s\" was not found") \ V(INVALID_KEY_TYPE, "Invalid key type") \ V(KEY_GENERATION_JOB_FAILED, "Key generation job failed") \ V(OK, "Ok") \ enum class NodeCryptoError { #define V(CODE, DESCRIPTION) CODE, NODE_CRYPTO_ERROR_CODES_MAP(V) #undef V }; template std::string getNodeCryptoErrorString(const NodeCryptoError error, Args&&... args) { const char* error_string = nullptr; switch (error) { #define V(CODE, DESCRIPTION) \ case NodeCryptoError::CODE: \ error_string = DESCRIPTION; \ break; NODE_CRYPTO_ERROR_CODES_MAP(V) #undef V } return SPrintF(error_string, std::forward(args)...); } // Utility struct used to harvest error information from openssl's error stack struct CryptoErrorStore final : public MemoryRetainer { public: void Capture(); bool Empty() const; template void Insert(const NodeCryptoError error, Args&&... args); v8::MaybeLocal ToException( Environment* env, v8::Local exception_string = v8::Local()) const; SET_NO_MEMORY_INFO() SET_MEMORY_INFO_NAME(CryptoErrorStore) SET_SELF_SIZE(CryptoErrorStore) private: std::vector errors_; }; template void CryptoErrorStore::Insert(const NodeCryptoError error, Args&&... args) { const char* error_string = nullptr; switch (error) { #define V(CODE, DESCRIPTION) \ case NodeCryptoError::CODE: error_string = DESCRIPTION; break; NODE_CRYPTO_ERROR_CODES_MAP(V) #undef V } errors_.emplace_back(SPrintF(error_string, std::forward(args)...)); } v8::MaybeLocal cryptoErrorListToException( Environment* env, const ncrypto::CryptoErrorList& errors); template T* MallocOpenSSL(size_t count) { void* mem = OPENSSL_malloc(MultiplyWithOverflowCheck(count, sizeof(T))); CHECK_IMPLIES(mem == nullptr, count == 0); return static_cast(mem); } // A helper class representing a read-only byte array. When deallocated, its // contents are zeroed. class ByteSource final { public: ByteSource() = default; ByteSource(ByteSource&& other) noexcept; ~ByteSource(); ByteSource& operator=(ByteSource&& other) noexcept; ByteSource(const ByteSource&) = delete; ByteSource& operator=(const ByteSource&) = delete; template inline const T* data() const { return reinterpret_cast(data_); } template operator ncrypto::Buffer() const { return ncrypto::Buffer{ .data = data(), .len = size(), }; } inline size_t size() const { return size_; } inline bool empty() const { return size_ == 0; } inline operator bool() const { return data_ != nullptr; } inline ncrypto::BignumPointer ToBN() const { return ncrypto::BignumPointer(data(), size()); } // Creates a v8::BackingStore that takes over responsibility for // any allocated data. The ByteSource will be reset with size = 0 // after being called. std::unique_ptr ReleaseToBackingStore(Environment* env); v8::Local ToArrayBuffer(Environment* env); v8::MaybeLocal ToBuffer(Environment* env); static ByteSource Allocated(void* data, size_t size); template static ByteSource Allocated(const ncrypto::Buffer& buffer) { return Allocated(buffer.data, buffer.len); } static ByteSource Foreign(const void* data, size_t size); static ByteSource FromEncodedString(Environment* env, v8::Local value, enum encoding enc = BASE64); static ByteSource FromStringOrBuffer(Environment* env, v8::Local value); static ByteSource FromString(Environment* env, v8::Local str, bool ntc = false); static ByteSource FromBuffer(v8::Local buffer, bool ntc = false); static ByteSource FromBIO(const ncrypto::BIOPointer& bio); static ByteSource NullTerminatedCopy(Environment* env, v8::Local value); static ByteSource FromSymmetricKeyObjectHandle(v8::Local handle); static ByteSource FromSecretKeyBytes( Environment* env, v8::Local value); private: const void* data_ = nullptr; void* allocated_data_ = nullptr; size_t size_ = 0; ByteSource(const void* data, void* allocated_data, size_t size) : data_(data), allocated_data_(allocated_data), size_(size) {} }; enum CryptoJobMode { kCryptoJobAsync, kCryptoJobSync }; CryptoJobMode GetCryptoJobMode(v8::Local args); template class CryptoJob : public AsyncWrap, public ThreadPoolWork { public: using AdditionalParams = typename CryptoJobTraits::AdditionalParameters; explicit CryptoJob(Environment* env, v8::Local object, AsyncWrap::ProviderType type, CryptoJobMode mode, AdditionalParams&& params) : AsyncWrap(env, object, type), ThreadPoolWork(env, "crypto"), mode_(mode), params_(std::move(params)) { // If the CryptoJob is async, then the instance will be // cleaned up when AfterThreadPoolWork is called. if (mode == kCryptoJobSync) MakeWeak(); } bool IsNotIndicativeOfMemoryLeakAtExit() const override { // CryptoJobs run a work in the libuv thread pool and may still // exist when the event loop empties and starts to exit. return true; } void AfterThreadPoolWork(int status) override { Environment* env = AsyncWrap::env(); CHECK_EQ(mode_, kCryptoJobAsync); CHECK(status == 0 || status == UV_ECANCELED); std::unique_ptr ptr(this); // If the job was canceled do not execute the callback. // TODO(@jasnell): We should likely revisit skipping the // callback on cancel as that could leave the JS in a pending // state (e.g. unresolved promises...) if (status == UV_ECANCELED) return; v8::HandleScope handle_scope(env->isolate()); v8::Context::Scope context_scope(env->context()); v8::Local exception; v8::Local args[2]; { node::errors::TryCatchScope try_catch(env); // If ToResult returns Nothing, then an exception should have been // thrown and we should have caught it. Otherwise, args[0] and args[1] // both should have been set to a value, even if the value is undefined. if (ptr->ToResult(&args[0], &args[1]).IsNothing()) { CHECK(try_catch.HasCaught()); CHECK(try_catch.CanContinue()); exception = try_catch.Exception(); } } if (!exception.IsEmpty()) { ptr->MakeCallback(env->ondone_string(), 1, &exception); } else { CHECK(!args[0].IsEmpty()); CHECK(!args[1].IsEmpty()); ptr->MakeCallback(env->ondone_string(), arraysize(args), args); } } virtual v8::Maybe ToResult(v8::Local* err, v8::Local* result) = 0; CryptoJobMode mode() const { return mode_; } CryptoErrorStore* errors() { return &errors_; } AdditionalParams* params() { return ¶ms_; } const char* MemoryInfoName() const override { return CryptoJobTraits::JobName; } void MemoryInfo(MemoryTracker* tracker) const override { tracker->TrackField("params", params_); tracker->TrackField("errors", errors_); } static void Run(const v8::FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); CryptoJob* job; ASSIGN_OR_RETURN_UNWRAP(&job, args.This()); if (job->mode() == kCryptoJobAsync) return job->ScheduleWork(); v8::Local ret[2]; env->PrintSyncTrace(); job->DoThreadPoolWork(); if (job->ToResult(&ret[0], &ret[1]).IsJust()) { CHECK(!ret[0].IsEmpty()); CHECK(!ret[1].IsEmpty()); args.GetReturnValue().Set( v8::Array::New(env->isolate(), ret, arraysize(ret))); } } static void Initialize( v8::FunctionCallback new_fn, Environment* env, v8::Local target) { v8::Isolate* isolate = env->isolate(); v8::HandleScope scope(isolate); v8::Local context = env->context(); v8::Local job = NewFunctionTemplate(isolate, new_fn); job->Inherit(AsyncWrap::GetConstructorTemplate(env)); job->InstanceTemplate()->SetInternalFieldCount( AsyncWrap::kInternalFieldCount); SetProtoMethod(isolate, job, "run", Run); SetConstructorFunction(context, target, CryptoJobTraits::JobName, job); } static void RegisterExternalReferences(v8::FunctionCallback new_fn, ExternalReferenceRegistry* registry) { registry->Register(new_fn); registry->Register(Run); } private: const CryptoJobMode mode_; CryptoErrorStore errors_; AdditionalParams params_; }; template class DeriveBitsJob final : public CryptoJob { public: using AdditionalParams = typename DeriveBitsTraits::AdditionalParameters; static void New(const v8::FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); CryptoJobMode mode = GetCryptoJobMode(args[0]); AdditionalParams params; if (DeriveBitsTraits::AdditionalConfig(mode, args, 1, ¶ms) .IsNothing()) { // The DeriveBitsTraits::AdditionalConfig is responsible for // calling an appropriate THROW_CRYPTO_* variant reporting // whatever error caused initialization to fail. return; } new DeriveBitsJob(env, args.This(), mode, std::move(params)); } static void Initialize( Environment* env, v8::Local target) { CryptoJob::Initialize(New, env, target); } static void RegisterExternalReferences(ExternalReferenceRegistry* registry) { CryptoJob::RegisterExternalReferences(New, registry); } DeriveBitsJob( Environment* env, v8::Local object, CryptoJobMode mode, AdditionalParams&& params) : CryptoJob( env, object, DeriveBitsTraits::Provider, mode, std::move(params)) {} void DoThreadPoolWork() override { ncrypto::ClearErrorOnReturn clear_error_on_return; if (!DeriveBitsTraits::DeriveBits(AsyncWrap::env(), *CryptoJob::params(), &out_, this->mode())) { CryptoErrorStore* errors = CryptoJob::errors(); errors->Capture(); if (errors->Empty()) errors->Insert(NodeCryptoError::DERIVING_BITS_FAILED); return; } success_ = true; } v8::Maybe ToResult(v8::Local* err, v8::Local* result) override { Environment* env = AsyncWrap::env(); CryptoErrorStore* errors = CryptoJob::errors(); if (success_) { CHECK(errors->Empty()); *err = v8::Undefined(env->isolate()); if (!DeriveBitsTraits::EncodeOutput( env, *CryptoJob::params(), &out_) .ToLocal(result)) { return v8::Nothing(); } } else { if (errors->Empty()) errors->Capture(); CHECK(!errors->Empty()); *result = v8::Undefined(env->isolate()); if (!errors->ToException(env).ToLocal(err)) { return v8::Nothing(); } } CHECK(!result->IsEmpty()); CHECK(!err->IsEmpty()); return v8::JustVoid(); } SET_SELF_SIZE(DeriveBitsJob) void MemoryInfo(MemoryTracker* tracker) const override { tracker->TrackFieldWithSize("out", out_.size()); CryptoJob::MemoryInfo(tracker); } private: ByteSource out_; bool success_ = false; }; void ThrowCryptoError(Environment* env, unsigned long err, // NOLINT(runtime/int) const char* message = nullptr); // WebIDL AllowSharedBufferSource. inline bool IsAnyBufferSource(v8::Local arg) { return arg->IsArrayBufferView() || arg->IsArrayBuffer() || arg->IsSharedArrayBuffer(); } template class ArrayBufferOrViewContents final { public: ArrayBufferOrViewContents() = default; ArrayBufferOrViewContents(const ArrayBufferOrViewContents&) = delete; void operator=(const ArrayBufferOrViewContents&) = delete; inline explicit ArrayBufferOrViewContents(v8::Local buf) { if (buf.IsEmpty()) { return; } CHECK(IsAnyBufferSource(buf)); if (buf->IsArrayBufferView()) { auto view = buf.As(); offset_ = view->ByteOffset(); length_ = view->ByteLength(); data_ = view->Buffer()->Data(); } else if (buf->IsArrayBuffer()) { auto ab = buf.As(); offset_ = 0; length_ = ab->ByteLength(); data_ = ab->Data(); } else { auto sab = buf.As(); offset_ = 0; length_ = sab->ByteLength(); data_ = sab->Data(); } } inline const T* data() const { // Ideally, these would return nullptr if IsEmpty() or length_ is zero, // but some of the openssl API react badly if given a nullptr even when // length is zero, so we have to return something. if (empty()) return &buf; return reinterpret_cast(data_) + offset_; } inline T* data() { // Ideally, these would return nullptr if IsEmpty() or length_ is zero, // but some of the openssl API react badly if given a nullptr even when // length is zero, so we have to return something. if (empty()) return &buf; return reinterpret_cast(data_) + offset_; } inline size_t size() const { return length_; } inline bool empty() const { return length_ == 0; } // In most cases, input buffer sizes passed in to openssl need to // be limited to <= INT_MAX. This utility method helps us check. inline bool CheckSizeInt32() { return size() <= INT_MAX; } inline ByteSource ToByteSource() const { return ByteSource::Foreign(data(), size()); } inline ByteSource ToCopy() const { if (empty()) return {}; auto buf = ncrypto::DataPointer::Alloc(size()); memcpy(buf.get(), data(), size()); return ByteSource::Allocated(buf.release()); } inline ByteSource ToNullTerminatedCopy() const { if (empty()) return {}; auto buf = ncrypto::DataPointer::Alloc(size() + 1); memcpy(buf.get(), data(), size()); static_cast(buf.get())[size()] = 0; return ByteSource::Allocated(buf.release()); } inline ncrypto::DataPointer ToDataPointer() const { if (empty()) return {}; if (auto dp = ncrypto::DataPointer::Alloc(size())) { memcpy(dp.get(), data(), size()); return dp; } return {}; } template void CopyTo(M* dest, size_t len) const { static_assert(sizeof(M) == 1, "sizeof(M) must equal 1"); len = std::min(len, size()); if (len > 0 && data() != nullptr) { memcpy(dest, data(), len); } } private: T buf = 0; size_t offset_ = 0; size_t length_ = 0; void* data_ = nullptr; // Declaring operator new and delete as deleted is not spec compliant. // Therefore declare them private instead to disable dynamic alloc void* operator new(size_t); void* operator new[](size_t); void operator delete(void*); void operator delete[](void*); }; v8::MaybeLocal EncodeBignum(Environment* env, const BIGNUM* bn, int size); v8::Maybe SetEncodedValue(Environment* env, v8::Local target, v8::Local name, const BIGNUM* bn, int size = 0); namespace Util { void Initialize(Environment* env, v8::Local target); void RegisterExternalReferences(ExternalReferenceRegistry* registry); } // namespace Util } // namespace node::crypto #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS #endif // SRC_CRYPTO_CRYPTO_UTIL_H_