#include "crypto.h" #include #include #include namespace opendht_node { namespace { using PublicKey = std::shared_ptr; using PrivateKey = std::shared_ptr; using Certificate = std::shared_ptr; using Identity = dht::crypto::Identity; using Handle = std::variant; constexpr napi_type_tag cryptoTag {0x37cdf3e4648e4cb5ULL, 0x964fb031062c245aULL}; Handle& handle(Napi::Value value) { bool matches = false; if (!value.IsExternal() || napi_check_object_type_tag(value.Env(), value, &cryptoTag, &matches) != napi_ok || !matches) throw Napi::TypeError::New(value.Env(), "Invalid OpenDHT crypto handle"); return *value.As>().Data(); } template Type read(Napi::Value value) { if (auto result = std::get_if(&handle(value))) return *result; throw Napi::TypeError::New(value.Env(), "Wrong OpenDHT crypto handle type"); } Napi::Value wrap(Napi::Env env, Handle value) { auto owner = std::make_unique(std::move(value)); auto result = Napi::External::New(env, owner.get(), [](Napi::Env, Handle* data) { delete data; }); owner.release(); if (napi_type_tag_object(env, result, &cryptoTag) != napi_ok) throw Napi::Error::New(env, "Could not tag OpenDHT crypto handle"); return result; } dht::Blob bytes(Napi::Value value) { if (!value.IsTypedArray() || value.As().TypedArrayType() != napi_uint8_array) throw Napi::TypeError::New(value.Env(), "Expected a Uint8Array"); auto array = value.As(); dht::Blob result(array.ElementLength()); if (!result.empty()) std::copy_n(array.Data(), result.size(), result.data()); return result; } std::string string(Napi::Value value) { if (!value.IsString()) throw Napi::TypeError::New(value.Env(), "Expected a string"); auto result = value.As().Utf8Value(); if (result.find('\0') != std::string::npos) throw Napi::TypeError::New(value.Env(), "Crypto strings must not contain NUL bytes"); return result; } Napi::Buffer buffer(Napi::Env env, const dht::Blob& value) { return Napi::Buffer::Copy(env, value.data(), value.size()); } PublicKey publicKey(const Handle& value) { if (auto key = std::get_if(&value)) return *key; if (auto key = std::get_if(&value)) return (*key)->getSharedPublicKey(); if (auto certificate = std::get_if(&value)) return (*certificate)->getSharedPublicKey(); return std::get(value).second->getSharedPublicKey(); } class GenerateWorker : public Napi::AsyncWorker { public: GenerateWorker(Napi::Env env, bool identity, std::string name, bool rsa, unsigned bits) : Napi::AsyncWorker(env, "OpenDHT crypto generation") , deferred_(Napi::Promise::Deferred::New(env)) , identity_(identity) , name_(std::move(name)) , rsa_(rsa) , bits_(bits) {} Napi::Promise promise() const { return deferred_.Promise(); } void Execute() override { try { if (identity_) result_ = rsa_ ? dht::crypto::generateIdentity(name_, {}, bits_) : dht::crypto::generateEcIdentity(name_); else result_ = std::make_shared(rsa_ ? dht::crypto::PrivateKey::generate(bits_) : dht::crypto::PrivateKey::generateEC()); } catch (const std::exception& error) { SetError(error.what()); } } void OnOK() override { deferred_.Resolve(wrap(Env(), std::move(result_))); } void OnError(const Napi::Error& error) override { deferred_.Reject(error.Value()); } private: Napi::Promise::Deferred deferred_; bool identity_; std::string name_; bool rsa_; unsigned bits_; Handle result_; }; Napi::Value generate(const Napi::CallbackInfo& info) { return guarded(info, [&]() -> Napi::Value { const auto kind = string(info[0]); const auto name = string(info[1]); const auto algorithm = string(info[2]); if (algorithm != "rsa" && algorithm != "ec") throw Napi::TypeError::New(info.Env(), "algorithm must be rsa or ec"); if (!info[3].IsNumber()) throw Napi::TypeError::New(info.Env(), "bits must be a number"); const auto bits = info[3].As().DoubleValue(); if (!std::isfinite(bits) || std::floor(bits) != bits || bits < 2048 || bits > 8192) throw Napi::RangeError::New(info.Env(), "RSA bits must be an integer between 2048 and 8192"); if (kind != "private" && kind != "identity") throw Napi::TypeError::New(info.Env(), "Invalid crypto generation type"); auto worker = std::make_unique(info.Env(), kind == "identity", name, algorithm == "rsa", bits); auto promise = worker->promise(); worker->Queue(); worker.release(); return promise; }); } Napi::Value import(const Napi::CallbackInfo& info) { return guarded(info, [&]() -> Napi::Value { const auto kind = string(info[0]); auto data = bytes(info[1]); if (data.empty()) throw Napi::TypeError::New(info.Env(), "Crypto imports must not be empty"); if (kind == "private") return wrap(info.Env(), std::make_shared(data, string(info[2]))); if (kind == "public") return wrap(info.Env(), std::make_shared(data)); if (kind == "certificate") return wrap(info.Env(), std::make_shared(data)); throw Napi::TypeError::New(info.Env(), "Invalid crypto import type"); }); } Napi::Value serialize(const Napi::CallbackInfo& info) { return guarded(info, [&]() -> Napi::Value { auto& value = handle(info[0]); if (auto key = std::get_if(&value)) return buffer(info.Env(), (*key)->serialize(string(info[1]))); if (auto certificate = std::get_if(&value)) return buffer(info.Env(), (*certificate)->getPacked()); dht::Blob result; readPublicKey(info[0])->pack(result); return buffer(info.Env(), result); }); } Napi::Value getPublicKey(const Napi::CallbackInfo& info) { return guarded(info, [&] { return wrap(info.Env(), publicKey(handle(info[0]))); }); } Napi::Value getId(const Napi::CallbackInfo& info) { return guarded(info, [&]() -> Napi::Value { const auto key = publicKey(handle(info[0])); if (!info[1].IsBoolean()) throw Napi::TypeError::New(info.Env(), "Expected a boolean"); return Napi::String::New(info.Env(), info[1].As().Value() ? key->getLongId().toString() : key->getId().toString()); }); } Napi::Value sign(const Napi::CallbackInfo& info) { return guarded(info, [&]() -> Napi::Value { return buffer(info.Env(), read(info[0])->sign(bytes(info[1]))); }); } Napi::Value verify(const Napi::CallbackInfo& info) { return guarded(info, [&]() -> Napi::Value { return Napi::Boolean::New(info.Env(), readPublicKey(info[0])->checkSignature(bytes(info[1]), bytes(info[2]))); }); } Napi::Value encrypt(const Napi::CallbackInfo& info) { return guarded(info, [&]() -> Napi::Value { const auto key = readEncryptionKey(info[0]); return buffer(info.Env(), key->encrypt(bytes(info[1]))); }); } Napi::Value decrypt(const Napi::CallbackInfo& info) { return guarded(info, [&]() -> Napi::Value { return buffer(info.Env(), read(info[0])->decrypt(bytes(info[1]))); }); } Napi::Value identity(const Napi::CallbackInfo& info) { return guarded(info, [&]() -> Napi::Value { auto key = read(info[0]); auto certificate = read(info[1]); if (key->getPublicKey().getLongId() != certificate->getLongId()) throw Napi::TypeError::New(info.Env(), "Identity key and certificate do not match"); return wrap(info.Env(), Identity {std::move(key), std::move(certificate)}); }); } Napi::Value identityPart(const Napi::CallbackInfo& info) { return guarded(info, [&]() -> Napi::Value { const auto value = readIdentity(info[0]); if (!info[1].IsBoolean()) throw Napi::TypeError::New(info.Env(), "Expected a boolean"); return info[1].As().Value() ? wrap(info.Env(), value.first) : wrap(info.Env(), value.second); }); } } // namespace dht::crypto::Identity readIdentity(Napi::Value value) { return read(value); } std::shared_ptr readPublicKey(Napi::Value value) { return read(value); } std::shared_ptr readEncryptionKey(Napi::Value value) { const auto key = readPublicKey(value); const auto algorithm = gnutls_pubkey_get_pk_algorithm(key->pk, nullptr); if (algorithm != GNUTLS_PK_RSA #if GNUTLS_VERSION_NUMBER >= 0x030804 && algorithm != GNUTLS_PK_RSA_OAEP #endif ) throw Napi::TypeError::New(value.Env(), "Encryption requires an RSA public key"); return key; } void initializeCrypto(Napi::Env env, Napi::Object exports) { exports.Set("cryptoGenerate", Napi::Function::New(env, generate)); exports.Set("cryptoImport", Napi::Function::New(env, import)); exports.Set("cryptoSerialize", Napi::Function::New(env, serialize)); exports.Set("cryptoPublicKey", Napi::Function::New(env, getPublicKey)); exports.Set("cryptoId", Napi::Function::New(env, getId)); exports.Set("cryptoSign", Napi::Function::New(env, sign)); exports.Set("cryptoVerify", Napi::Function::New(env, verify)); exports.Set("cryptoEncrypt", Napi::Function::New(env, encrypt)); exports.Set("cryptoDecrypt", Napi::Function::New(env, decrypt)); exports.Set("cryptoIdentity", Napi::Function::New(env, identity)); exports.Set("cryptoIdentityPart", Napi::Function::New(env, identityPart)); } } // namespace opendht_node