diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/client/websocketpp_client.cpp | 191 | ||||
-rw-r--r-- | src/data/event.cpp (renamed from src/event.cpp) | 29 | ||||
-rw-r--r-- | src/data/filters.cpp (renamed from src/filters.cpp) | 21 | ||||
-rw-r--r-- | src/service/nostr_service_base.cpp (renamed from src/nostr_service.cpp) | 300 | ||||
-rw-r--r-- | src/signer/noscrypt_signer.cpp | 697 |
5 files changed, 963 insertions, 275 deletions
diff --git a/src/client/websocketpp_client.cpp b/src/client/websocketpp_client.cpp index c7504e1..b8458e6 100644 --- a/src/client/websocketpp_client.cpp +++ b/src/client/websocketpp_client.cpp @@ -1,129 +1,108 @@ -#include "web_socket_client.hpp" - -using std::error_code; -using std::function; -using std::lock_guard; -using std::make_tuple; -using std::mutex; -using std::string; -using std::tuple; -using std::unordered_map; - -namespace client -{ -/** - * @brief An implementation of the `IWebSocketClient` interface that uses the WebSocket++ library. - */ -class WebsocketppClient : public IWebSocketClient -{ -public: - void start() override - { - this->_client.init_asio(); - this->_client.start_perpetual(); - }; +#include <mutex> - void stop() override - { - this->_client.stop_perpetual(); - this->_client.stop(); - }; +#include "client/websocketpp_client.hpp" - void openConnection(string uri) override - { - error_code error; - websocketpp_client::connection_ptr connection = this->_client.get_connection(uri, error); +using namespace nostr::client; +using namespace std; - if (error.value() == -1) - { - // PLOG_ERROR << "Error connecting to relay " << relay << ": " << error.message(); - } - - // Configure the connection here via the connection pointer. - connection->set_fail_handler([this, uri](auto handle) { - // PLOG_ERROR << "Error connecting to relay " << relay << ": Handshake failed."; - lock_guard<mutex> lock(this->_propertyMutex); - if (this->isConnected(uri)) - { - this->_connectionHandles.erase(uri); - } - }); +void WebsocketppClient::start() +{ + this->_client.init_asio(); + this->_client.start_perpetual(); +}; - lock_guard<mutex> lock(this->_propertyMutex); - this->_connectionHandles[uri] = connection->get_handle(); - this->_client.connect(connection); - }; +void WebsocketppClient::stop() +{ + this->_client.stop_perpetual(); + this->_client.stop(); +}; - bool isConnected(string uri) override - { - lock_guard<mutex> lock(this->_propertyMutex); - return this->_connectionHandles.find(uri) != this->_connectionHandles.end(); - }; +void WebsocketppClient::openConnection(string uri) +{ + error_code error; + websocketpp_client::connection_ptr connection = this->_client.get_connection(uri, error); - tuple<string, bool> send(string message, string uri) override + if (error.value() == -1) { - error_code error; + // PLOG_ERROR << "Error connecting to relay " << relay << ": " << error.message(); + } - // Make sure the connection isn't closed from under us. + // Configure the connection here via the connection pointer. + connection->set_fail_handler([this, uri](auto handle) { + // PLOG_ERROR << "Error connecting to relay " << relay << ": Handshake failed."; lock_guard<mutex> lock(this->_propertyMutex); - this->_client.send( - this->_connectionHandles[uri], - message, - websocketpp::frame::opcode::text, - error); - - if (error.value() == -1) + if (this->isConnected(uri)) { - return make_tuple(uri, false); + this->_connectionHandles.erase(uri); } + }); - return make_tuple(uri, true); - }; + lock_guard<mutex> lock(this->_propertyMutex); + this->_connectionHandles[uri] = connection->get_handle(); + this->_client.connect(connection); +}; - tuple<string, bool> send(string message, string uri, function<void(const string&)> messageHandler) override - { - auto successes = this->send(message, uri); - this->receive(uri, messageHandler); - return successes; - }; +bool WebsocketppClient::isConnected(string uri) +{ + lock_guard<mutex> lock(this->_propertyMutex); + return this->_connectionHandles.find(uri) != this->_connectionHandles.end(); +}; - void receive(string uri, function<void(const string&)> messageHandler) override - { - lock_guard<mutex> lock(this->_propertyMutex); - auto connectionHandle = this->_connectionHandles[uri]; - auto connection = this->_client.get_con_from_hdl(connectionHandle); +tuple<string, bool> WebsocketppClient::send(string message, string uri) +{ + error_code error; - connection->set_message_handler([messageHandler]( - websocketpp::connection_hdl connectionHandle, - websocketpp_client::message_ptr message) - { - messageHandler(message->get_payload()); - }); - }; + // Make sure the connection isn't closed from under us. + lock_guard<mutex> lock(this->_propertyMutex); + this->_client.send( + this->_connectionHandles[uri], + message, + websocketpp::frame::opcode::text, + error); - void closeConnection(string uri) override + if (error.value() == -1) { - lock_guard<mutex> lock(this->_propertyMutex); + return make_tuple(uri, false); + } - websocketpp::connection_hdl handle = this->_connectionHandles[uri]; - this->_client.close( - handle, - websocketpp::close::status::going_away, - "_client requested close."); - - this->_connectionHandles.erase(uri); - }; + return make_tuple(uri, true); +}; -private: - typedef websocketpp::client<websocketpp::config::asio_client> websocketpp_client; - typedef unordered_map<string, websocketpp::connection_hdl>::iterator connection_hdl_iterator; +tuple<string, bool> WebsocketppClient::send( + string message, + string uri, + function<void(const string&)> messageHandler) +{ + auto successes = this->send(message, uri); + this->receive(uri, messageHandler); + return successes; +}; - websocketpp_client _client; - unordered_map<string, websocketpp::connection_hdl> _connectionHandles; - mutex _propertyMutex; +void WebsocketppClient::receive( + string uri, + function<void(const string&)> messageHandler) +{ + lock_guard<mutex> lock(this->_propertyMutex); + auto connectionHandle = this->_connectionHandles[uri]; + auto connection = this->_client.get_con_from_hdl(connectionHandle); - void onMessage(websocketpp::connection_hdl handle, websocketpp_client::message_ptr message) + connection->set_message_handler([messageHandler]( + websocketpp::connection_hdl connectionHandle, + websocketpp_client::message_ptr message) { - }; + messageHandler(message->get_payload()); + }); +}; + +void WebsocketppClient::closeConnection(string uri) +{ + lock_guard<mutex> lock(this->_propertyMutex); + + websocketpp::connection_hdl handle = this->_connectionHandles[uri]; + this->_client.close( + handle, + websocketpp::close::status::going_away, + "_client requested close."); + + this->_connectionHandles.erase(uri); }; -} // namespace client diff --git a/src/event.cpp b/src/data/event.cpp index 703efae..a1a96f1 100644 --- a/src/event.cpp +++ b/src/data/event.cpp @@ -1,12 +1,13 @@ -#include <ctime> +#include <sstream> +#include <stdexcept> -#include "nostr.hpp" +#include "data/data.hpp" using namespace nlohmann; +using namespace nostr::data; using namespace std; -namespace nostr -{ +// TODO: Verify event signature using noscrypt. string Event::serialize() { try @@ -19,12 +20,13 @@ string Event::serialize() } json j = { - {"pubkey", this->pubkey}, - {"created_at", this->createdAt}, - {"kind", this->kind}, - {"tags", this->tags}, - {"content", this->content}, - {"sig", this->sig}}; + { "pubkey", this->pubkey }, + { "created_at", this->createdAt }, + { "kind", this->kind }, + { "tags", this->tags }, + { "content", this->content }, + { "sig", this->sig } + }; j["id"] = this->generateId(j.dump()); @@ -88,12 +90,6 @@ void Event::validate() { throw std::invalid_argument("Event::validate: A valid event kind is required."); } - - bool hasSignature = this->sig.length() > 0; - if (!hasSignature) - { - throw std::invalid_argument("Event::validate: The event must be signed."); - } }; string Event::generateId(string serializedData) const @@ -123,4 +119,3 @@ bool Event::operator==(const Event& other) const return this->id == other.id; }; -} // namespace nostr diff --git a/src/filters.cpp b/src/data/filters.cpp index 6f62e0b..b725002 100644 --- a/src/filters.cpp +++ b/src/data/filters.cpp @@ -1,10 +1,11 @@ -#include "nostr.hpp" +#include <stdexcept> + +#include "data/data.hpp" using namespace nlohmann; +using namespace nostr::data; using namespace std; -namespace nostr -{ string Filters::serialize(string& subscriptionId) { try @@ -17,12 +18,13 @@ string Filters::serialize(string& subscriptionId) } json j = { - {"ids", this->ids}, - {"authors", this->authors}, - {"kinds", this->kinds}, - {"since", this->since}, - {"until", this->until}, - {"limit", this->limit}}; + { "ids", this->ids }, + { "authors", this->authors }, + { "kinds", this->kinds }, + { "since", this->since }, + { "until", this->until }, + { "limit", this->limit } + }; for (auto& tag : this->tags) { @@ -64,4 +66,3 @@ void Filters::validate() throw invalid_argument("Filters::validate: At least one filter must be set."); } }; -} // namespace nostr diff --git a/src/nostr_service.cpp b/src/service/nostr_service_base.cpp index 5adca3b..26748e0 100644 --- a/src/nostr_service.cpp +++ b/src/service/nostr_service_base.cpp @@ -1,54 +1,60 @@ -#include "nostr.hpp" -#include "client/web_socket_client.hpp" +#include <exception> +#include <future> +#include <stdexcept> +#include <thread> + +#include <uuid_v4.h> + +#include "service/nostr_service_base.hpp" using namespace nlohmann; +using namespace nostr::service; using namespace std; -namespace nostr -{ -NostrService::NostrService( +NostrServiceBase::NostrServiceBase( shared_ptr<plog::IAppender> appender, - shared_ptr<client::IWebSocketClient> client, - shared_ptr<ISigner> signer) -: NostrService(appender, client, signer, {}) { }; + shared_ptr<client::IWebSocketClient> client) +: NostrServiceBase(appender, client, {}) { }; -NostrService::NostrService( +NostrServiceBase::NostrServiceBase( shared_ptr<plog::IAppender> appender, shared_ptr<client::IWebSocketClient> client, - shared_ptr<ISigner> signer, vector<string> relays) -: _defaultRelays(relays), _client(client), _signer(signer) +: _defaultRelays(relays), _client(client) { plog::init(plog::debug, appender.get()); client->start(); }; -NostrService::~NostrService() +NostrServiceBase::~NostrServiceBase() { this->_client->stop(); }; -vector<string> NostrService::defaultRelays() const { return this->_defaultRelays; }; +vector<string> NostrServiceBase::defaultRelays() const +{ return this->_defaultRelays; }; -vector<string> NostrService::activeRelays() const { return this->_activeRelays; }; +vector<string> NostrServiceBase::activeRelays() const +{ return this->_activeRelays; }; -unordered_map<string, vector<string>> NostrService::subscriptions() const { return this->_subscriptions; }; +unordered_map<string, vector<string>> NostrServiceBase::subscriptions() const +{ return this->_subscriptions; }; -vector<string> NostrService::openRelayConnections() +vector<string> NostrServiceBase::openRelayConnections() { return this->openRelayConnections(this->_defaultRelays); }; -vector<string> NostrService::openRelayConnections(vector<string> relays) +vector<string> NostrServiceBase::openRelayConnections(vector<string> relays) { PLOG_INFO << "Attempting to connect to Nostr relays."; - vector<string> unconnectedRelays = this->getUnconnectedRelays(relays); + vector<string> unconnectedRelays = this->_getUnconnectedRelays(relays); vector<thread> connectionThreads; for (string relay : unconnectedRelays) { thread connectionThread([this, relay]() { - this->connect(relay); + this->_connect(relay); }); connectionThreads.push_back(move(connectionThread)); } @@ -58,15 +64,15 @@ vector<string> NostrService::openRelayConnections(vector<string> relays) connectionThread.join(); } - size_t targetCount = relays.size(); - size_t activeCount = this->_activeRelays.size(); + std::size_t targetCount = relays.size(); + std::size_t activeCount = this->_activeRelays.size(); PLOG_INFO << "Connected to " << activeCount << "/" << targetCount << " target relays."; // This property should only contain successful relays at this point. return this->_activeRelays; }; -void NostrService::closeRelayConnections() +void NostrServiceBase::closeRelayConnections() { if (this->_activeRelays.size() == 0) { @@ -77,16 +83,16 @@ void NostrService::closeRelayConnections() this->closeRelayConnections(this->_activeRelays); }; -void NostrService::closeRelayConnections(vector<string> relays) +void NostrServiceBase::closeRelayConnections(vector<string> relays) { PLOG_INFO << "Disconnecting from Nostr relays."; - vector<string> connectedRelays = getConnectedRelays(relays); + vector<string> connectedRelays = this->_getConnectedRelays(relays); vector<thread> disconnectionThreads; for (string relay : connectedRelays) { thread disconnectionThread([this, relay]() { - this->disconnect(relay); + this->_disconnect(relay); }); disconnectionThreads.push_back(move(disconnectionThread)); @@ -102,7 +108,8 @@ void NostrService::closeRelayConnections(vector<string> relays) }; // TODO: Make this method return a promise. -tuple<vector<string>, vector<string>> NostrService::publishEvent(shared_ptr<Event> event) +tuple<vector<string>, vector<string>> NostrServiceBase::publishEvent( + shared_ptr<nostr::data::Event> event) { vector<string> successfulRelays; vector<string> failedRelays; @@ -112,7 +119,6 @@ tuple<vector<string>, vector<string>> NostrService::publishEvent(shared_ptr<Even json message; try { - this->_signer->sign(event); message = json::array({ "EVENT", event->serialize() }); } catch (const std::invalid_argument& e) @@ -139,7 +145,7 @@ tuple<vector<string>, vector<string>> NostrService::publishEvent(shared_ptr<Even relay, [this, &relay, &event, &publishPromise](string response) { - this->onAcceptance(response, [this, &relay, &event, &publishPromise](bool isAccepted) + this->_onAcceptance(response, [this, &relay, &event, &publishPromise](bool isAccepted) { if (isAccepted) { @@ -174,119 +180,128 @@ tuple<vector<string>, vector<string>> NostrService::publishEvent(shared_ptr<Even } } - size_t targetCount = targetRelays.size(); - size_t successfulCount = successfulRelays.size(); + std::size_t targetCount = targetRelays.size(); + std::size_t successfulCount = successfulRelays.size(); PLOG_INFO << "Published event to " << successfulCount << "/" << targetCount << " target relays."; return make_tuple(successfulRelays, failedRelays); }; -// TODO: Make this method return a promise. // TODO: Add a timeout to this method to prevent hanging while waiting for the relay. -vector<shared_ptr<Event>> NostrService::queryRelays(shared_ptr<Filters> filters) +future<vector<shared_ptr<nostr::data::Event>>> NostrServiceBase::queryRelays( + shared_ptr<nostr::data::Filters> filters) { - if (filters->limit > 64 || filters->limit < 1) + return async(launch::async, [this, filters]() -> vector<shared_ptr<Event>> { - PLOG_WARNING << "Filters limit must be between 1 and 64, inclusive. Setting limit to 16."; - filters->limit = 16; - } - - vector<shared_ptr<Event>> events; - - string subscriptionId = this->generateSubscriptionId(); - string request; - - try - { - request = filters->serialize(subscriptionId); - } - catch (const invalid_argument& e) - { - PLOG_ERROR << "Failed to serialize filters - invalid object: " << e.what(); - throw e; - } - catch (const json::exception& je) - { - PLOG_ERROR << "Failed to serialize filters - JSON exception: " << je.what(); - throw je; - } + if (filters->limit > 64 || filters->limit < 1) + { + PLOG_WARNING << "Filters limit must be between 1 and 64, inclusive. Setting limit to 16."; + filters->limit = 16; + } - vector<future<tuple<string, bool>>> requestFutures; + vector<shared_ptr<Event>> events; - // Send the same query to each relay. As events trickle in from each relay, they will be added - // to the events vector. Multiple copies of an event may be received if the same event is - // stored on multiple relays. The function will block until all of the relays send an EOSE or - // CLOSE message. - for (const string relay : this->_activeRelays) - { - promise<tuple<string, bool>> eosePromise; - requestFutures.push_back(move(eosePromise.get_future())); + string subscriptionId = this->generateSubscriptionId(); + string request; - auto [uri, success] = this->_client->send( - request, - relay, - [this, &relay, &events, &eosePromise](string payload) - { - this->onSubscriptionMessage( - payload, - [&events](const string&, shared_ptr<Event> event) - { - events.push_back(event); - }, - [relay, &eosePromise](const string&) - { - eosePromise.set_value(make_tuple(relay, true)); - }, - [relay, &eosePromise](const string&, const string&) - { - eosePromise.set_value(make_tuple(relay, false)); - }); - }); - - if (success) + try { - PLOG_INFO << "Sent query to relay " << relay; - lock_guard<mutex> lock(this->_propertyMutex); - this->_subscriptions[subscriptionId].push_back(relay); + request = filters->serialize(subscriptionId); } - else + catch (const invalid_argument& e) { - PLOG_WARNING << "Failed to send query to relay " << relay; - eosePromise.set_value(make_tuple(uri, false)); + PLOG_ERROR << "Failed to serialize filters - invalid object: " << e.what(); + throw e; } - } - - // Close open subscriptions and disconnect from relays after events are received. - for (auto& publishFuture : requestFutures) - { - auto [relay, isEose] = publishFuture.get(); - if (isEose) + catch (const json::exception& je) { - PLOG_INFO << "Received EOSE message from relay " << relay; + PLOG_ERROR << "Failed to serialize filters - JSON exception: " << je.what(); + throw je; } - else + + vector<future<tuple<string, bool>>> requestFutures; + + unordered_set<string> uniqueEventIds; + + // Send the same query to each relay. As events trickle in from each relay, they will be added + // to the events vector. Duplicate copies of the same event will be ignored, as events are + // stored on multiple relays. The function will block until all of the relays send an EOSE or + // CLOSE message. + for (const string relay : this->_activeRelays) { - PLOG_WARNING << "Received CLOSE message from relay " << relay; - this->closeRelayConnections({ relay }); + promise<tuple<string, bool>> eosePromise; + requestFutures.push_back(move(eosePromise.get_future())); + + auto [uri, success] = this->_client->send( + request, + relay, + [this, &relay, &events, &eosePromise, &uniqueEventIds](string payload) + { + this->onSubscriptionMessage( + payload, + [&events, &uniqueEventIds](const string&, shared_ptr<Event> event) + { + // Check if the event is unique before adding. + if (uniqueEventIds.insert(event->id).second) + { + events.push_back(event); + } + }, + [relay, &eosePromise](const string&) + { + eosePromise.set_value(make_tuple(relay, true)); + }, + [relay, &eosePromise](const string&, const string&) + { + eosePromise.set_value(make_tuple(relay, false)); + }); + }); + + if (success) + { + PLOG_INFO << "Sent query to relay " << relay; + lock_guard<mutex> lock(this->_propertyMutex); + this->_subscriptions[subscriptionId].push_back(relay); + } + else + { + PLOG_WARNING << "Failed to send query to relay " << relay; + eosePromise.set_value(make_tuple(uri, false)); + } } - } - this->closeSubscription(subscriptionId); - // TODO: De-duplicate events in the vector before returning. - return events; + // Close open subscriptions and disconnect from relays after events are received. + + for (auto& publishFuture : requestFutures) + { + auto [relay, isEose] = publishFuture.get(); + if (isEose) + { + PLOG_INFO << "Received EOSE message from relay " << relay; + } + else + { + PLOG_WARNING << "Received CLOSE message from relay " << relay; + this->closeRelayConnections({ relay }); + } + } + this->closeSubscription(subscriptionId); + + return events; + }); }; -string NostrService::queryRelays( - shared_ptr<Filters> filters, - function<void(const string&, shared_ptr<Event>)> eventHandler, +string NostrServiceBase::queryRelays( + shared_ptr<nostr::data::Filters> filters, + function<void(const string&, shared_ptr<nostr::data::Event>)> eventHandler, function<void(const string&)> eoseHandler, function<void(const string&, const string&)> closeHandler) { vector<string> successfulRelays; vector<string> failedRelays; - string subscriptionId = this->generateSubscriptionId(); + string subscriptionId = this->_generateSubscriptionId(); string request = filters->serialize(subscriptionId); vector<future<tuple<string, bool>>> requestFutures; for (const string relay : this->_activeRelays) @@ -303,7 +318,7 @@ string NostrService::queryRelays( relay, [this, &eventHandler, &eoseHandler, &closeHandler](string payload) { - this->onSubscriptionMessage(payload, eventHandler, eoseHandler, closeHandler); + this->_onSubscriptionMessage(payload, eventHandler, eoseHandler, closeHandler); }); }); requestFutures.push_back(move(requestFuture)); @@ -322,20 +337,20 @@ string NostrService::queryRelays( } } - size_t targetCount = this->_activeRelays.size(); - size_t successfulCount = successfulRelays.size(); + std::size_t targetCount = this->_activeRelays.size(); + std::size_t successfulCount = successfulRelays.size(); PLOG_INFO << "Sent query to " << successfulCount << "/" << targetCount << " open relay connections."; return subscriptionId; }; -tuple<vector<string>, vector<string>> NostrService::closeSubscription(string subscriptionId) +tuple<vector<string>, vector<string>> NostrServiceBase::closeSubscription(string subscriptionId) { vector<string> successfulRelays; vector<string> failedRelays; vector<string> subscriptionRelays; - size_t subscriptionRelayCount; + std::size_t subscriptionRelayCount; vector<future<tuple<string, bool>>> closeFutures; try @@ -375,7 +390,7 @@ tuple<vector<string>, vector<string>> NostrService::closeSubscription(string sub } } - size_t successfulCount = successfulRelays.size(); + std::size_t successfulCount = successfulRelays.size(); PLOG_INFO << "Sent CLOSE request for subscription " << subscriptionId << " to " << successfulCount << "/" << subscriptionRelayCount << " open relay connections."; // If there were no failures, and the subscription has been closed on all of its relays, forget @@ -389,21 +404,21 @@ tuple<vector<string>, vector<string>> NostrService::closeSubscription(string sub return make_tuple(successfulRelays, failedRelays); }; -bool NostrService::closeSubscription(string subscriptionId, string relay) +bool NostrServiceBase::closeSubscription(string subscriptionId, string relay) { - if (!this->hasSubscription(subscriptionId, relay)) + if (!this->_hasSubscription(subscriptionId, relay)) { PLOG_WARNING << "Subscription " << subscriptionId << " not found on relay " << relay; return false; } - if (!this->isConnected(relay)) + if (!this->_isConnected(relay)) { PLOG_WARNING << "Relay " << relay << " is not connected."; return false; } - string request = this->generateCloseRequest(subscriptionId); + string request = this->_generateCloseRequest(subscriptionId); auto [uri, success] = this->_client->send(request, relay); if (success) @@ -429,7 +444,7 @@ bool NostrService::closeSubscription(string subscriptionId, string relay) return success; }; -vector<string> NostrService::closeSubscriptions() +vector<string> NostrServiceBase::closeSubscriptions() { unique_lock<mutex> lock(this->_propertyMutex); vector<string> subscriptionIds; @@ -452,7 +467,7 @@ vector<string> NostrService::closeSubscriptions() return remainingSubscriptions; }; -vector<string> NostrService::getConnectedRelays(vector<string> relays) +vector<string> NostrServiceBase::_getConnectedRelays(vector<string> relays) { PLOG_VERBOSE << "Identifying connected relays."; vector<string> connectedRelays; @@ -469,7 +484,7 @@ vector<string> NostrService::getConnectedRelays(vector<string> relays) } else if (isActive && !isConnected) { - this->eraseActiveRelay(relay); + this->_eraseActiveRelay(relay); } else if (!isActive && isConnected) { @@ -480,7 +495,7 @@ vector<string> NostrService::getConnectedRelays(vector<string> relays) return connectedRelays; }; -vector<string> NostrService::getUnconnectedRelays(vector<string> relays) +vector<string> NostrServiceBase::_getUnconnectedRelays(vector<string> relays) { PLOG_VERBOSE << "Identifying unconnected relays."; vector<string> unconnectedRelays; @@ -499,7 +514,7 @@ vector<string> NostrService::getUnconnectedRelays(vector<string> relays) else if (isActive && !isConnected) { PLOG_VERBOSE << "Relay " << relay << " is active but not connected. Removing from active relays list."; - this->eraseActiveRelay(relay); + this->_eraseActiveRelay(relay); unconnectedRelays.push_back(relay); } else if (!isActive && isConnected) @@ -511,7 +526,7 @@ vector<string> NostrService::getUnconnectedRelays(vector<string> relays) return unconnectedRelays; }; -bool NostrService::isConnected(string relay) +bool NostrServiceBase::_isConnected(string relay) { auto it = find(this->_activeRelays.begin(), this->_activeRelays.end(), relay); if (it != this->_activeRelays.end()) // If the relay is in this->_activeRelays @@ -521,7 +536,7 @@ bool NostrService::isConnected(string relay) return false; }; -void NostrService::eraseActiveRelay(string relay) +void NostrServiceBase::_eraseActiveRelay(string relay) { auto it = find(this->_activeRelays.begin(), this->_activeRelays.end(), relay); if (it != this->_activeRelays.end()) // If the relay is in this->_activeRelays @@ -530,7 +545,7 @@ void NostrService::eraseActiveRelay(string relay) } }; -void NostrService::connect(string relay) +void NostrServiceBase::_connect(string relay) { PLOG_VERBOSE << "Connecting to relay " << relay; this->_client->openConnection(relay); @@ -549,28 +564,28 @@ void NostrService::connect(string relay) } }; -void NostrService::disconnect(string relay) +void NostrServiceBase::_disconnect(string relay) { this->_client->closeConnection(relay); lock_guard<mutex> lock(this->_propertyMutex); - this->eraseActiveRelay(relay); + this->_eraseActiveRelay(relay); }; -string NostrService::generateSubscriptionId() +string NostrServiceBase::_generateSubscriptionId() { UUIDv4::UUIDGenerator<std::mt19937_64> uuidGenerator; UUIDv4::UUID uuid = uuidGenerator.getUUID(); return uuid.str(); }; -string NostrService::generateCloseRequest(string subscriptionId) +string NostrServiceBase::_generateCloseRequest(string subscriptionId) { json jarr = json::array({ "CLOSE", subscriptionId }); return jarr.dump(); }; -bool NostrService::hasSubscription(string subscriptionId) +bool NostrServiceBase::_hasSubscription(string subscriptionId) { lock_guard<mutex> lock(this->_propertyMutex); auto it = this->_subscriptions.find(subscriptionId); @@ -578,7 +593,7 @@ bool NostrService::hasSubscription(string subscriptionId) return it != this->_subscriptions.end(); }; -bool NostrService::hasSubscription(string subscriptionId, string relay) +bool NostrServiceBase::_hasSubscription(string subscriptionId, string relay) { lock_guard<mutex> lock(this->_propertyMutex); auto subscriptionIt = this->_subscriptions.find(subscriptionId); @@ -594,9 +609,9 @@ bool NostrService::hasSubscription(string subscriptionId, string relay) return relayIt != relays.end(); }; -void NostrService::onSubscriptionMessage( +void NostrServiceBase::_onSubscriptionMessage( string message, - function<void(const string&, shared_ptr<Event>)> eventHandler, + function<void(const string&, shared_ptr<nostr::data::Event>)> eventHandler, function<void(const string&)> eoseHandler, function<void(const string&, const string&)> closeHandler) { @@ -607,8 +622,8 @@ void NostrService::onSubscriptionMessage( if (messageType == "EVENT") { string subscriptionId = jMessage.at(1); - Event event = Event::fromString(jMessage.at(2)); - eventHandler(subscriptionId, make_shared<Event>(event)); + nostr::data::Event event = nostr::data::Event::fromString(jMessage.at(2)); + eventHandler(subscriptionId, make_shared<nostr::data::Event>(event)); } else if (messageType == "EOSE") { @@ -639,7 +654,9 @@ void NostrService::onSubscriptionMessage( } }; -void NostrService::onAcceptance(string message, function<void(const bool)> acceptanceHandler) +void NostrServiceBase::_onAcceptance( + string message, + function<void(const bool)> acceptanceHandler) { try { @@ -657,4 +674,3 @@ void NostrService::onAcceptance(string message, function<void(const bool)> accep throw je; } }; -} // namespace nostr diff --git a/src/signer/noscrypt_signer.cpp b/src/signer/noscrypt_signer.cpp new file mode 100644 index 0000000..68cdf6a --- /dev/null +++ b/src/signer/noscrypt_signer.cpp @@ -0,0 +1,697 @@ +#include <algorithm> +#include <chrono> +#include <cstring> +#include <memory> +#include <random> +#include <sstream> +#include <tuple> + +#include <nlohmann/json.hpp> +#include <openssl/err.h> +#include <openssl/rand.h> +#include <uuid_v4.h> + +#include "signer/noscrypt_signer.hpp" + +using namespace nostr::data; +using namespace nostr::service; +using namespace nostr::signer; +using namespace std; + +#pragma region Constructors and Destructors + +NoscryptSigner::NoscryptSigner( + shared_ptr<plog::IAppender> appender, + shared_ptr<INostrServiceBase> nostrService) +{ + plog::init(plog::debug, appender.get()); + + this->_reseedRandomNumberGenerator(); + this->_initNoscryptContext(); + this->_createLocalKeypair(); + + this->_nostrService = nostrService; +}; + +NoscryptSigner::~NoscryptSigner() +{ + NCDestroyContext(this->_noscryptContext.get()); +}; + +#pragma endregion + +#pragma region Public Interface + +void NoscryptSigner::receiveConnection(string connectionToken) +{ + if (connectionToken.empty()) + { + PLOG_ERROR << "No connection token was provided - unable to connect to a remote signer."; + return; + } + + int queryStart = this->_parseRemotePublicKey(connectionToken); + if (queryStart == -1) + { + return; + } + + string remainingToken = connectionToken.substr(queryStart); + int splitIndex = remainingToken.find('&'); + do + { + string param = remainingToken.substr(0, splitIndex); + this->_handleConnectionTokenParam(param); + + remainingToken = remainingToken.substr(splitIndex + 1); + splitIndex = remainingToken.find('&'); + } while (splitIndex != string::npos); + + // TODO: Handle any messaging with the remote signer. +}; + +string NoscryptSigner::initiateConnection( + vector<string> relays, + string name, + string url, + string description) +{ + // Return an empty string if the local keypair is invalid. + if (this->_getLocalPrivateKey().empty() || this->_getLocalPublicKey().empty()) + { + PLOG_ERROR << "A valid local keypair is required to connect to a remote signer."; + return string(); + } + + // Return an empty string if no relays are provided. + if (relays.empty()) + { + PLOG_ERROR << "At least one relay must be provided to connect to a remote signer."; + return string(); + } + + // Store the provided relay list to be used for sending and receving connection events. + this->_relays = relays; + + // Generate the connection token. + stringstream ss; + ss << "nostrconnect://" << this->_localPublicKey; + for (int i = 0; i < relays.size(); i++) + { + ss << (i == 0 ? "?" : "&"); + ss << "relay=" << relays[i]; + } + ss << "&metadata={"; + ss << "\"name\":\"" << name << "\","; + ss << "\"url\":\"" << url << "\","; + ss << "\"description\":\"" << description << "\""; + ss << "}"; + + return ss.str(); + + // TODO: Handle any messaging with the remote signer. +}; + +shared_ptr<promise<bool>> NoscryptSigner::sign(shared_ptr<Event> event) +{ + auto signingPromise = make_shared<promise<bool>>(); + + auto signerAvailable = this->_pingSigner().get_future(); + if (signerAvailable.get() == false) + { + PLOG_ERROR << "Ping to the remote signer failed - the remote signer may be unavailable."; + signingPromise->set_value(false); + return signingPromise; + } + + // Create the JSON-RPC-like message content. + auto params = nlohmann::json::array(); + params.push_back(event->serialize()); + + auto requestId = this->_generateSignerRequestId(); + + // Create a filter set to find events from the remote signer. + auto remoteSignerFilters = this->_buildSignerMessageFilters(); + + // Generate the signing request event. + nlohmann::json jrpc = { + { "id", requestId }, + { "method", "sign_event" }, + { "params", params } + }; + auto signingRequest = this->_wrapSignerMessage(jrpc); + + // Send the signing request. + this->_nostrService->publishEvent(signingRequest); + + // Wait for the remote signer's response. + this->_nostrService->queryRelays( + remoteSignerFilters, + [this, &event, &signingPromise](const string&, shared_ptr<Event> signerEvent) + { + // Assign the response event to the `event` parameter, accomplishing the intended + // function result via side effect. + string signerResponse = this->_unwrapSignerMessage(signerEvent); + event = make_shared<Event>(Event::fromString(signerResponse)); + signingPromise->set_value(true); + }, + [&signingPromise](const string&) + { + signingPromise->set_value(false); + }, + [&signingPromise](const string&, const string&) + { + signingPromise->set_value(false); + }); + + return signingPromise; +}; + +#pragma endregion + +#pragma region Private Accessors + +inline string NoscryptSigner::_getLocalPrivateKey() const +{ + stringstream privkeyStream; + for (int i = 0; i < sizeof(NCSecretKey); i++) + { + privkeyStream << hex << setw(2) << setfill('0') << static_cast<int>(this->_localPrivateKey->key[i]); + } + + return privkeyStream.str(); +}; + +inline void NoscryptSigner::_setLocalPrivateKey(const string value) +{ + auto seckey = make_unique<NCSecretKey>(); + + for (auto i = 0; i < sizeof(NCSecretKey); i++) + { + stringstream ss; + ss << hex << value.substr(i * 2, 2); + uint8_t byte; + ss >> byte; + seckey->key[i] = byte; + } + + this->_localPrivateKey = move(seckey); +}; + +inline string NoscryptSigner::_getLocalPublicKey() const +{ + stringstream pubkeyStream; + for (int i = 0; i < sizeof(NCPublicKey); i++) + { + pubkeyStream << hex << setw(2) << setfill('0') << static_cast<int>(this->_localPublicKey->key[i]); + } + + return pubkeyStream.str(); +}; + +inline void NoscryptSigner::_setLocalPublicKey(const string value) +{ + auto pubkey = make_unique<NCPublicKey>(); + + for (auto i = 0; i < sizeof(NCPublicKey); i++) + { + stringstream ss; + ss << hex << value.substr(i * 2, 2); + uint8_t byte; + ss >> byte; + pubkey->key[i] = byte; + } + + this->_localPublicKey = move(pubkey); +}; + +inline string NoscryptSigner::_getRemotePublicKey() const +{ + stringstream pubkeyStream; + for (int i = 0; i < sizeof(NCPublicKey); i++) + { + pubkeyStream << hex << setw(2) << setfill('0') << static_cast<int>(this->_remotePublicKey->key[i]); + } + + return pubkeyStream.str(); +}; + +inline void NoscryptSigner::_setRemotePublicKey(const string value) +{ + auto pubkey = make_unique<NCPublicKey>(); + + for (auto i = 0; i < sizeof(NCPublicKey); i++) + { + stringstream ss; + ss << hex << value.substr(i * 2, 2); + uint8_t byte; + ss >> byte; + pubkey->key[i] = byte; + } + + this->_remotePublicKey = move(pubkey); +}; + +#pragma endregion + +#pragma region Setup + +void NoscryptSigner::_initNoscryptContext() +{ + shared_ptr<NCContext> context; + auto contextStructSize = NCGetContextStructSize(); + auto randomEntropy = make_unique<uint8_t>(contextStructSize); + + random_device rd; + mt19937 gen(rd()); + uniform_int_distribution<> dist(0, contextStructSize); + generate_n(randomEntropy.get(), contextStructSize, [&]() { return dist(gen); }); + + NCResult initResult = NCInitContext(context.get(), randomEntropy.get()); + this->_logNoscryptInitResult(initResult); + + this->_noscryptContext = move(context); +}; + +/** + * @brief Generates a private/public key pair for local use. + * @returns The generated keypair of the form `[privateKey, publicKey]`, or a pair of empty + * strings if the function failed. + * @remarks This keypair is intended for temporary use, and should not be saved or used outside + * of this class. + */ +void NoscryptSigner::_createLocalKeypair() +{ + string privateKey; + string publicKey; + + // To generate a private key, all we need is a random 32-bit buffer. + auto secret = make_unique<NCSecretKey>(); + + // Loop attempts to generate a secret key until a valid key is produced. + // Limit the number of attempts to prevent resource exhaustion in the event of a failure. + NCResult secretValidationResult; + int loopCount = 0; + do + { + int rc = RAND_bytes(secret->key, sizeof(NCSecretKey)); + if (rc != 1) + { + unsigned long err = ERR_get_error(); + PLOG_ERROR << "OpenSSL error " << err << " occurred while generating a secret key."; + } + + secretValidationResult = NCValidateSecretKey(this->_noscryptContext.get(), secret.get()); + } while (secretValidationResult != NC_SUCCESS && ++loopCount < 64); + + this->_logNoscryptSecretValidationResult(secretValidationResult); + this->_localPrivateKey = move(secret); + + // Use noscrypt to derive the public key from its private counterpart. + auto pubkey = make_unique<NCPublicKey>(); + NCResult pubkeyGenerationResult = NCGetPublicKey( + this->_noscryptContext.get(), + secret.get(), + pubkey.get()); + this->_logNoscryptPubkeyGenerationResult(pubkeyGenerationResult); + this->_localPublicKey = move(pubkey); +}; + +int NoscryptSigner::_parseRemotePublicKey(string connectionToken) +{ + int queryStart = connectionToken.find('?'); + if (queryStart == string::npos) + { + PLOG_ERROR << "The connection token is invalid - no query string was found."; + return -1; + } + + const int pubkeyStart = 9; + string prefix = connectionToken.substr(0, pubkeyStart); + if (prefix != "bunker://") + { + PLOG_ERROR << "The connection token is invalid - the token must begin with 'bunker://'."; + return -1; + } + + string remotePubkey = connectionToken.substr(pubkeyStart, queryStart); + this->_setRemotePublicKey(remotePubkey); + + return queryStart + 1; +}; + +void NoscryptSigner::_handleConnectionTokenParam(string param) +{ + // Parse the query param into a key-value pair. + int splitIndex = param.find('='); + if (splitIndex == string::npos) + { + PLOG_ERROR << "The connection token query param is invalid - it is not of the form 'key=value'."; + return; + } + + string key = param.substr(0, splitIndex); + string value = param.substr(splitIndex + 1); + + // Handle the key-value pair. + if (key == "relay") + { + this->_relays.push_back(value); + } + else if (key == "secret") + { + this->_bunkerSecret = value; + } +}; + +#pragma endregion + +#pragma region Signer Helpers + +inline string NoscryptSigner::_generateSignerRequestId() const +{ + UUIDv4::UUIDGenerator<std::mt19937_64> uuidGenerator; + UUIDv4::UUID uuid = uuidGenerator.getUUID(); + return uuid.str(); +}; + +shared_ptr<Event> NoscryptSigner::_wrapSignerMessage(nlohmann::json jrpc) +{ + // Encrypt the message payload. + string encryptedContent; + switch (this->_nostrConnectEncryption) + { + case Encryption::NIP44: + encryptedContent = this->_encryptNip44(jrpc.dump()); + if (!encryptedContent.empty()) + { + break; + } + + // Use NIP-04 encryption as a fallback. + case Encryption::NIP04: + encryptedContent = this->_encryptNip04(jrpc.dump()); + break; + } + + // Wrap the event to be signed in a signing request event. + auto wrapperEvent = make_shared<Event>(); + wrapperEvent->pubkey = this->_getLocalPublicKey(); + wrapperEvent->kind = this->_nostrConnectKind; + wrapperEvent->tags.push_back({ "p", this->_getRemotePublicKey() }); + wrapperEvent->content = encryptedContent; + + // Generate a random seed for the signer. + auto random32 = make_shared<uint8_t>(32); + int code = RAND_bytes(random32.get(), 32); + if (code <= 0) + { + PLOG_ERROR << "Failed to generate a random 32-byte seed buffer for the signer."; + return nullptr; + } + + // Sign the wrapper message with the local secret key. + string serializedEvent = wrapperEvent->serialize(); + uint32_t dataSize = serializedEvent.length(); + auto signature = make_unique<uint8_t>(64); + NCResult signatureResult = NCSignData( + this->_noscryptContext.get(), + this->_localPrivateKey.get(), + random32.get(), + reinterpret_cast<uint8_t*>(serializedEvent.data()), + dataSize, + signature.get()); + + // TODO: Handle result codes. + if (signatureResult != NC_SUCCESS) + { + return nullptr; + } + + // Add the signature to the event. + wrapperEvent->sig = string((char*)signature.get(), 64); + + return wrapperEvent; +}; + +string NoscryptSigner::_unwrapSignerMessage(shared_ptr<Event> event) +{ + // TODO: Verify the incoming event. + + // Extract and decrypt the event payload. + string encryptedContent = event->content; + string decryptedContent; + + // NIP-04 encrypted strings include `?iv=` near the end (source: hodlbod). + if (encryptedContent.find("?iv=") != string::npos) + { + decryptedContent = this->_decryptNip04(encryptedContent); + } + else + { + decryptedContent = this->_decryptNip44(encryptedContent); + } + + // Parse the decrypted string into a JSON object. + return decryptedContent; +}; + +inline shared_ptr<Filters> NoscryptSigner::_buildSignerMessageFilters() const +{ + auto filters = make_shared<Filters>(); + filters->authors.push_back(this->_getRemotePublicKey()); + filters->kinds.push_back(this->_nostrConnectKind); + filters->tags["p"] = { this->_getLocalPublicKey() }; + filters->since = time(nullptr); + + return filters; +}; + +promise<bool> NoscryptSigner::_pingSigner() +{ + promise<bool> pingPromise; + + // Generate a ping message and wrap it for the signer. + nlohmann::json jrpc = + { + { "id", this->_generateSignerRequestId() }, + { "method", "ping" }, + { "params", nlohmann::json::array() } + }; + auto messageEvent = this->_wrapSignerMessage(jrpc); + + // Generate a filter to receive the response. + auto pingFilter = this->_buildSignerMessageFilters(); + + this->_nostrService->publishEvent(messageEvent); + + // TODO: Handle the relay response. + this->_nostrService->queryRelays( + pingFilter, + [this, &pingPromise](const string&, shared_ptr<Event> pongEvent) + { + // + string pongMessage = this->_unwrapSignerMessage(pongEvent); + pingPromise.set_value(pongMessage == "pong"); + }, + [&pingPromise](const string&) + { + pingPromise.set_value(false); + }, + [&pingPromise](const string&, const string&) + { + pingPromise.set_value(false); + }); + + return pingPromise; +}; + +#pragma endregion + +#pragma region Cryptography + +void NoscryptSigner::_reseedRandomNumberGenerator(uint32_t bufferSize) +{ + int rc = RAND_load_file("/dev/random", bufferSize); + if (rc != bufferSize) + { + PLOG_WARNING << "Failed to reseed the RNG with /dev/random, falling back to /dev/urandom."; + RAND_poll(); + } +}; + +string NoscryptSigner::_encryptNip04(std::string input) +{ + throw runtime_error("NIP-04 encryption is not yet implemented."); +}; + +string NoscryptSigner::_decryptNip04(string input) +{ + throw runtime_error("NIP-04 decryption is not yet implemented."); +}; + +string NoscryptSigner::_encryptNip44(string input) +{ + uint32_t nip44Version = 0x02; + + auto nonce = make_shared<uint8_t>(32); + auto hmacKey = make_shared<uint8_t>(32); + + uint32_t bufferSize = input.length(); + auto output = make_shared<uint8_t>(bufferSize); + + // Generate a nonce to use for the encryption. + int code = RAND_bytes(nonce.get(), 32); + if (code <= 0) + { + PLOG_ERROR << "Failed to generate a nonce for NIP-44 encryption."; + return string(); + } + + // Setup the encryption context. + auto encryptionArgs = make_unique<NCEncryptionArgs>(NCEncryptionArgs + { + nonce.get(), + hmacKey.get(), + reinterpret_cast<uint8_t*>(input.data()), + output.get(), + bufferSize, + nip44Version + }); + + // Perform the encryption. + NCResult encryptionResult = NCEncrypt( + this->_noscryptContext.get(), + this->_localPrivateKey.get(), + this->_remotePublicKey.get(), + encryptionArgs.get()); + + // TODO: Handle various codes. + if (encryptionResult != NC_SUCCESS) + { + return string(); + } + + return string((char*)output.get(), bufferSize); +}; + +string NoscryptSigner::_decryptNip44(string input) +{ + uint32_t nip44Version = 0x02; + + auto nonce = make_shared<uint8_t>(32); + auto hmacKey = make_shared<uint8_t>(32); + + uint32_t bufferSize = input.length(); + auto output = make_shared<uint8_t>(bufferSize); + + // Generate a nonce to use for the decryption. + int code = RAND_bytes(nonce.get(), 32); + if (code <= 0) + { + PLOG_ERROR << "Failed to generate a nonce for NIP-44 decryption."; + return string(); + } + + // Set up the decryption context. + auto decryptionArgs = make_unique<NCEncryptionArgs>(NCEncryptionArgs + { + nonce.get(), + hmacKey.get(), + reinterpret_cast<uint8_t*>(input.data()), + output.get(), + bufferSize, + nip44Version + }); + + // Perform the decryption. + NCResult decryptionResult = NCDecrypt( + this->_noscryptContext.get(), + this->_localPrivateKey.get(), + this->_remotePublicKey.get(), + decryptionArgs.get()); + + // TODO: Handle various codes. + if (decryptionResult != NC_SUCCESS) + { + return string(); + } + + return string((char*)output.get(), bufferSize); +}; + +#pragma endregion + +#pragma region Logging + +inline void NoscryptSigner::_logNoscryptInitResult(NCResult initResult) const +{ + switch (initResult) { + case NC_SUCCESS: + PLOG_INFO << "noscrypt - success"; + break; + + case E_NULL_PTR: + PLOG_ERROR << "noscrypt - error: A null pointer was passed to the initializer."; + break; + + case E_INVALID_ARG: + PLOG_ERROR << "noscrypt - error: An invalid argument was passed to the initializer."; + break; + + case E_INVALID_CONTEXT: + PLOG_ERROR << "noscrypt - error: The NCContext struct is in an invalid state."; + break; + + case E_ARGUMENT_OUT_OF_RANGE: + PLOG_ERROR << "noscrypt - error: An initializer argument was outside the range of acceptable values."; + break; + + case E_OPERATION_FAILED: + PLOG_ERROR << "noscrypt - error"; + break; + } +}; + +inline void NoscryptSigner::_logNoscryptSecretValidationResult(NCResult secretValidationResult) const +{ + if (secretValidationResult == NC_SUCCESS) + { + PLOG_INFO << "noscrypt_signer - success: Generated a valid secret key."; + } + else + { + PLOG_ERROR << "noscrypt_signer - error: Failed to generate a valid secret key."; + } +}; + +inline void NoscryptSigner::_logNoscryptPubkeyGenerationResult(NCResult pubkeyGenerationResult) const +{ + switch (pubkeyGenerationResult) { + case NC_SUCCESS: + PLOG_INFO << "noscrypt - success: Generated a valid public key."; + break; + + case E_NULL_PTR: + PLOG_ERROR << "noscrypt - error: A null pointer was passed to the public key generation function."; + break; + + case E_INVALID_ARG: + PLOG_ERROR << "noscrypt - error: An invalid argument was passed to the public key generation function."; + break; + + case E_INVALID_CONTEXT: + PLOG_ERROR << "noscrypt - error: The NCContext struct is in an invalid state."; + break; + + case E_ARGUMENT_OUT_OF_RANGE: + PLOG_ERROR << "noscrypt - error: An argument was outside the range of acceptable values."; + break; + + case E_OPERATION_FAILED: + PLOG_ERROR << "noscrypt - error: Failed to generate the public key from the secret key."; + break; + } +}; + +#pragma endregion |