Files
Kmake/src/crypto/crypto_util.h
2026-05-26 23:36:42 -07:00

598 lines
19 KiB
C++

#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 <algorithm>
#include <climits>
#include <cstdio>
#include <memory>
#include <optional>
#include <string>
#include <vector>
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<v8::Object> 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 <typename T>
void Decode(const v8::FunctionCallbackInfo<v8::Value>& args,
void (*callback)(T*, const v8::FunctionCallbackInfo<v8::Value>&,
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<v8::String>(), enc).IsNothing())
return;
callback(ctx, args, decoder.out(), decoder.size());
} else {
ArrayBufferViewContents<char> 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 <typename... Args>
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>(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 <typename... Args>
void Insert(const NodeCryptoError error, Args&&... args);
v8::MaybeLocal<v8::Value> ToException(
Environment* env,
v8::Local<v8::String> exception_string = v8::Local<v8::String>()) const;
SET_NO_MEMORY_INFO()
SET_MEMORY_INFO_NAME(CryptoErrorStore)
SET_SELF_SIZE(CryptoErrorStore)
private:
std::vector<std::string> errors_;
};
template <typename... Args>
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>(args)...));
}
v8::MaybeLocal<v8::Value> cryptoErrorListToException(
Environment* env, const ncrypto::CryptoErrorList& errors);
template <typename T>
T* MallocOpenSSL(size_t count) {
void* mem = OPENSSL_malloc(MultiplyWithOverflowCheck(count, sizeof(T)));
CHECK_IMPLIES(mem == nullptr, count == 0);
return static_cast<T*>(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 <typename T = void>
inline const T* data() const {
return reinterpret_cast<const T*>(data_);
}
template <typename T = void>
operator ncrypto::Buffer<const T>() const {
return ncrypto::Buffer<const T>{
.data = data<T>(),
.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<unsigned char>(), 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<v8::BackingStore> ReleaseToBackingStore(Environment* env);
v8::Local<v8::ArrayBuffer> ToArrayBuffer(Environment* env);
v8::MaybeLocal<v8::Uint8Array> ToBuffer(Environment* env);
static ByteSource Allocated(void* data, size_t size);
template <typename T>
static ByteSource Allocated(const ncrypto::Buffer<T>& buffer) {
return Allocated(buffer.data, buffer.len);
}
static ByteSource Foreign(const void* data, size_t size);
static ByteSource FromEncodedString(Environment* env,
v8::Local<v8::String> value,
enum encoding enc = BASE64);
static ByteSource FromStringOrBuffer(Environment* env,
v8::Local<v8::Value> value);
static ByteSource FromString(Environment* env,
v8::Local<v8::String> str,
bool ntc = false);
static ByteSource FromBuffer(v8::Local<v8::Value> buffer,
bool ntc = false);
static ByteSource FromBIO(const ncrypto::BIOPointer& bio);
static ByteSource NullTerminatedCopy(Environment* env,
v8::Local<v8::Value> value);
static ByteSource FromSymmetricKeyObjectHandle(v8::Local<v8::Value> handle);
static ByteSource FromSecretKeyBytes(
Environment* env, v8::Local<v8::Value> 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<v8::Value> args);
template <typename CryptoJobTraits>
class CryptoJob : public AsyncWrap, public ThreadPoolWork {
public:
using AdditionalParams = typename CryptoJobTraits::AdditionalParameters;
explicit CryptoJob(Environment* env,
v8::Local<v8::Object> 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<CryptoJob> 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<v8::Value> exception;
v8::Local<v8::Value> 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<void> ToResult(v8::Local<v8::Value>* err,
v8::Local<v8::Value>* result) = 0;
CryptoJobMode mode() const { return mode_; }
CryptoErrorStore* errors() { return &errors_; }
AdditionalParams* params() { return &params_; }
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<v8::Value>& args) {
Environment* env = Environment::GetCurrent(args);
CryptoJob<CryptoJobTraits>* job;
ASSIGN_OR_RETURN_UNWRAP(&job, args.This());
if (job->mode() == kCryptoJobAsync)
return job->ScheduleWork();
v8::Local<v8::Value> 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<v8::Object> target) {
v8::Isolate* isolate = env->isolate();
v8::HandleScope scope(isolate);
v8::Local<v8::Context> context = env->context();
v8::Local<v8::FunctionTemplate> 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 <typename DeriveBitsTraits>
class DeriveBitsJob final : public CryptoJob<DeriveBitsTraits> {
public:
using AdditionalParams = typename DeriveBitsTraits::AdditionalParameters;
static void New(const v8::FunctionCallbackInfo<v8::Value>& args) {
Environment* env = Environment::GetCurrent(args);
CryptoJobMode mode = GetCryptoJobMode(args[0]);
AdditionalParams params;
if (DeriveBitsTraits::AdditionalConfig(mode, args, 1, &params)
.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<v8::Object> target) {
CryptoJob<DeriveBitsTraits>::Initialize(New, env, target);
}
static void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
CryptoJob<DeriveBitsTraits>::RegisterExternalReferences(New, registry);
}
DeriveBitsJob(
Environment* env,
v8::Local<v8::Object> object,
CryptoJobMode mode,
AdditionalParams&& params)
: CryptoJob<DeriveBitsTraits>(
env,
object,
DeriveBitsTraits::Provider,
mode,
std::move(params)) {}
void DoThreadPoolWork() override {
ncrypto::ClearErrorOnReturn clear_error_on_return;
if (!DeriveBitsTraits::DeriveBits(AsyncWrap::env(),
*CryptoJob<DeriveBitsTraits>::params(),
&out_,
this->mode())) {
CryptoErrorStore* errors = CryptoJob<DeriveBitsTraits>::errors();
errors->Capture();
if (errors->Empty())
errors->Insert(NodeCryptoError::DERIVING_BITS_FAILED);
return;
}
success_ = true;
}
v8::Maybe<void> ToResult(v8::Local<v8::Value>* err,
v8::Local<v8::Value>* result) override {
Environment* env = AsyncWrap::env();
CryptoErrorStore* errors = CryptoJob<DeriveBitsTraits>::errors();
if (success_) {
CHECK(errors->Empty());
*err = v8::Undefined(env->isolate());
if (!DeriveBitsTraits::EncodeOutput(
env, *CryptoJob<DeriveBitsTraits>::params(), &out_)
.ToLocal(result)) {
return v8::Nothing<void>();
}
} else {
if (errors->Empty()) errors->Capture();
CHECK(!errors->Empty());
*result = v8::Undefined(env->isolate());
if (!errors->ToException(env).ToLocal(err)) {
return v8::Nothing<void>();
}
}
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<DeriveBitsTraits>::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<v8::Value> arg) {
return arg->IsArrayBufferView() ||
arg->IsArrayBuffer() ||
arg->IsSharedArrayBuffer();
}
template <typename T>
class ArrayBufferOrViewContents final {
public:
ArrayBufferOrViewContents() = default;
ArrayBufferOrViewContents(const ArrayBufferOrViewContents&) = delete;
void operator=(const ArrayBufferOrViewContents&) = delete;
inline explicit ArrayBufferOrViewContents(v8::Local<v8::Value> buf) {
if (buf.IsEmpty()) {
return;
}
CHECK(IsAnyBufferSource(buf));
if (buf->IsArrayBufferView()) {
auto view = buf.As<v8::ArrayBufferView>();
offset_ = view->ByteOffset();
length_ = view->ByteLength();
data_ = view->Buffer()->Data();
} else if (buf->IsArrayBuffer()) {
auto ab = buf.As<v8::ArrayBuffer>();
offset_ = 0;
length_ = ab->ByteLength();
data_ = ab->Data();
} else {
auto sab = buf.As<v8::SharedArrayBuffer>();
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<T*>(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<T*>(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<char*>(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 <typename M>
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<v8::Value> EncodeBignum(Environment* env,
const BIGNUM* bn,
int size);
v8::Maybe<void> SetEncodedValue(Environment* env,
v8::Local<v8::Object> target,
v8::Local<v8::String> name,
const BIGNUM* bn,
int size = 0);
namespace Util {
void Initialize(Environment* env, v8::Local<v8::Object> target);
void RegisterExternalReferences(ExternalReferenceRegistry* registry);
} // namespace Util
} // namespace node::crypto
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
#endif // SRC_CRYPTO_CRYPTO_UTIL_H_