From f50b94bff318c4d9df5ff193c44b65cd3f7ab512 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Sun, 9 Jun 2024 18:40:07 -0500 Subject: Add NIP-44 encryption for signer events --- include/signer/noscrypt_signer.hpp | 35 ++++++-- include/signer/signer.hpp | 5 +- src/data/event.cpp | 13 +-- src/data/filters.cpp | 13 +-- src/signer/noscrypt_signer.cpp | 160 ++++++++++++++++++++++++++++++++++--- 5 files changed, 199 insertions(+), 27 deletions(-) diff --git a/include/signer/noscrypt_signer.hpp b/include/signer/noscrypt_signer.hpp index 606438e..7201c12 100644 --- a/include/signer/noscrypt_signer.hpp +++ b/include/signer/noscrypt_signer.hpp @@ -2,11 +2,7 @@ #include #include - -extern "C" -{ #include -} #include "service/nostr_service_base.hpp" #include "signer/signer.hpp" @@ -32,10 +28,14 @@ public: std::string url, std::string description) override; - void sign(std::shared_ptr event) override; + std::shared_ptr> sign(std::shared_ptr event) override; private: std::shared_ptr _noscryptContext; + std::shared_ptr _nostrService; + + std::shared_ptr _remotePubkey; // TODO: Set this when available. + std::shared_ptr _localSecret; std::string _localPrivateKey; std::string _localPublicKey; @@ -79,6 +79,31 @@ private: */ void _handleConnectionTokenParam(std::string param); + /** + * @brief Generates a unique ID for a signer request. + * @returns A GUID string. + */ + std::string _generateSignerRequestId(); + + #pragma region Cryptography + + /** + * @brief Reseeds OpenSSL's pseudo-random number generator, using `/dev/random` as the seed, if + * possible. + */ + void _reseedRandomNumberGenerator(uint32_t bufferSize = 32); + + std::string _encryptNip04(); + + /** + * @brief Encrypts a string according to the standard specified in NIP-44. + * @param input The string to be encrypted. + * @return The encrypted input. + */ + std::string _encryptNip44(const std::string input); // TODO: Return or set HMAC? + + #pragma endregion + #pragma region Logging void _logNoscryptInitResult(NCResult initResult); diff --git a/include/signer/signer.hpp b/include/signer/signer.hpp index c774d1d..0faaff8 100644 --- a/include/signer/signer.hpp +++ b/include/signer/signer.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -21,9 +22,11 @@ public: /** * @brief Signs the given Nostr event. * @param event The event to sign. + * @returns A promise that will be fulfilled when the event has been signed. It will be + * fulfilled with `true` if the signing succeeded, and `false` if it failed. * @remark The event's `sig` field will be updated in-place with the signature. */ - virtual void sign(std::shared_ptr event) = 0; + virtual std::shared_ptr> sign(std::shared_ptr event) = 0; }; class INostrConnectSigner : public ISigner diff --git a/src/data/event.cpp b/src/data/event.cpp index e275f31..d101531 100644 --- a/src/data/event.cpp +++ b/src/data/event.cpp @@ -19,12 +19,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()); diff --git a/src/data/filters.cpp b/src/data/filters.cpp index 11c099b..a8e3343 100644 --- a/src/data/filters.cpp +++ b/src/data/filters.cpp @@ -18,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) { diff --git a/src/signer/noscrypt_signer.cpp b/src/signer/noscrypt_signer.cpp index d02aa93..1ac3b88 100644 --- a/src/signer/noscrypt_signer.cpp +++ b/src/signer/noscrypt_signer.cpp @@ -1,19 +1,29 @@ #include +#include #include #include #include +#include +#include +#include +#include + #include "signer/noscrypt_signer.hpp" +using namespace nostr::data; +using namespace nostr::service; using namespace nostr::signer; using namespace std; NoscryptSigner::NoscryptSigner( shared_ptr appender, - shared_ptr nostrService) + shared_ptr nostrService) { plog::init(plog::debug, appender.get()); + this->_reseedRandomNumberGenerator(); + this->_noscryptContext = this->_initNoscryptContext(); if (this->_noscryptContext == nullptr) { @@ -23,6 +33,8 @@ NoscryptSigner::NoscryptSigner( const auto [privateKey, publicKey] = this->_createLocalKeypair(); this->_localPrivateKey = privateKey; this->_localPublicKey = publicKey; + + this->_nostrService = nostrService; }; NoscryptSigner::~NoscryptSigner() @@ -100,9 +112,63 @@ string NoscryptSigner::initiateConnection( // TODO: Handle any messaging with the remote signer. }; -void NoscryptSigner::sign(shared_ptr event) +shared_ptr> NoscryptSigner::sign(shared_ptr event) { - // Sign the event here. + const int nostrConnectKind = 24133; // Kind 24133 is reserved for NIP-46 events. + + auto signingPromise = make_shared>(); + + // Create the JSON-RPC-like message content. + auto params = nlohmann::json::array(); + params.push_back(event->serialize()); + + auto requestId = this->_generateSignerRequestId(); + + nlohmann::json jrpc = { + { "id", requestId }, + { "method", "sign_event" }, + { "params", params } + }; + + // TODO: Encrypt the message content with NIP-04 (or NIP-44). + NCEncryptionArgs encryptionArgs; + + string encryptedContent; + + // Wrap the event to be signed in a signing request event. + auto signingRequest = make_shared(); + signingRequest->pubkey = this->_localPublicKey; + signingRequest->kind = nostrConnectKind; + signingRequest->tags.push_back({ "p", this->_remotePublicKey }); + signingRequest->content = encryptedContent; + // TODO: Sign the wrapper event with the local private key. + + // Create a filter set to find events from the remote signer. + auto remoteSignerFilters = make_shared(); + remoteSignerFilters->kinds.push_back(nostrConnectKind); + remoteSignerFilters->since = time(nullptr); // Filter for new signer events. + remoteSignerFilters->tags["p"] = { this->_localPublicKey }; // Signer events tag the local npub. + remoteSignerFilters->limit = 1; // We only need the immediate response to the signing request. + + // TODO: Ping the remote signer before sending the request. + + // Send the signing request. + this->_nostrService->publishEvent(signingRequest); + + // Wait for the remote signer's response. + this->_nostrService->queryRelays( + remoteSignerFilters, + [this, &signingPromise](const string&, shared_ptr event) + { + // TODO: Handle the response from the remote signer. + + // Eventually: + signingPromise->set_value(true); + }, + nullptr, + nullptr); + + return signingPromise; }; /** @@ -146,19 +212,22 @@ tuple NoscryptSigner::_createLocalKeypair() // To generate a private key, all we need is a random 32-bit buffer. unique_ptr secretKey(new NCSecretKey); - random_device rd; - mt19937 gen(rd()); - uniform_int_distribution<> dist(0, sizeof(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 { - generate_n(secretKey.get()->key, sizeof(NCSecretKey), [&]() { return dist(gen); }); + int rc = RAND_bytes(secretKey->key, sizeof(NCSecretKey)); + if (rc != 1) + { + unsigned long err = ERR_get_error(); + PLOG_ERROR << "OpenSSL error " << err << " occurred while generating a secret key."; + return make_tuple(string(), string()); + } + secretValidationResult = NCValidateSecretKey(this->_noscryptContext.get(), secretKey.get()); - } while (secretValidationResult != NC_SUCCESS && ++loopCount < 1024); + } while (secretValidationResult != NC_SUCCESS && ++loopCount < 64); this->_logNoscryptSecretValidationResult(secretValidationResult); if (secretValidationResult != NC_SUCCESS) @@ -167,6 +236,8 @@ tuple NoscryptSigner::_createLocalKeypair() return make_tuple(string(), string()); } + this->_localSecret = move(secretKey); + // Convert the buffer into a hex string for a more human-friendly representation. stringstream secretKeyStream; for (int i = 0; i < sizeof(NCSecretKey); i++) @@ -248,6 +319,77 @@ void NoscryptSigner::_handleConnectionTokenParam(string param) } }; +string NoscryptSigner::_generateSignerRequestId() +{ + UUIDv4::UUIDGenerator uuidGenerator; + UUIDv4::UUID uuid = uuidGenerator.getUUID(); + return uuid.str(); +}; + +#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() +{ + throw runtime_error("NIP-04 encryption is not yet implemented."); +}; + +string NoscryptSigner::_encryptNip44(const string input) +{ + uint32_t nip44Version = 0x02; + + shared_ptr nonce(new uint8_t[32]); + shared_ptr hmacKey(new uint8_t[32]); + + uint32_t bufferSize = input.length(); + shared_ptr output(new 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. + NCEncryptionArgs encryptionArgs = + { + nonce.get(), + hmacKey.get(), + (uint8_t*)input.c_str(), + output.get(), + bufferSize, + nip44Version + }; + + // Perform the encryption. + NCResult encryptionResult = NCEncrypt( + this->_noscryptContext.get(), + this->_localSecret.get(), + this->_remotePubkey.get(), + &encryptionArgs); + + // TODO: Handle various codes. + if (encryptionResult != NC_SUCCESS) + { + return string(); + } + + return string((char*)output.get(), bufferSize); +}; + +#pragma endregion + #pragma region Logging void NoscryptSigner::_logNoscryptInitResult(NCResult initResult) -- cgit