From a16b33031c7bcf2eabf1e0c3571000234b7740df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roman=20Smr=C5=BE?= Date: Sun, 24 Jan 2021 22:46:48 +0100 Subject: Attach service --- include/erebos/attach.h | 49 +++++++++++ include/erebos/identity.h | 1 + include/erebos/network.h | 20 ++++- include/erebos/pairing.h | 104 +++++++++++++++++++++++ include/erebos/storage.h | 9 ++ src/CMakeLists.txt | 2 + src/attach.cpp | 124 +++++++++++++++++++++++++++ src/identity.cpp | 5 ++ src/network.cpp | 65 +++++++++++++- src/network.h | 5 +- src/pairing.cpp | 212 ++++++++++++++++++++++++++++++++++++++++++++++ src/pubkey.cpp | 39 +++++++++ src/pubkey.h | 3 + src/storage.cpp | 24 +++--- 14 files changed, 648 insertions(+), 14 deletions(-) create mode 100644 include/erebos/attach.h create mode 100644 include/erebos/pairing.h create mode 100644 src/attach.cpp create mode 100644 src/pairing.cpp diff --git a/include/erebos/attach.h b/include/erebos/attach.h new file mode 100644 index 0000000..14e6af3 --- /dev/null +++ b/include/erebos/attach.h @@ -0,0 +1,49 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include + +namespace erebos { + +using std::mutex; +using std::optional; +using std::promise; +using std::string; +using std::vector; + +struct AttachIdentity; + +class AttachService : public PairingService +{ +public: + AttachService(); + virtual ~AttachService(); + + UUID uuid() const override; + + void attachTo(const Peer &); + +protected: + virtual Stored handlePairingComplete(const Peer &) override; + virtual void handlePairingResult(Context &, Stored) override; + + mutex handlerLock; +}; + +template class Signed; + +struct AttachIdentity +{ + Stored> identity; + vector> keys; + + static AttachIdentity load(const Ref &); + Ref store(const Storage &) const; +}; + +} diff --git a/include/erebos/identity.h b/include/erebos/identity.h index 66a4afb..aeddd08 100644 --- a/include/erebos/identity.h +++ b/include/erebos/identity.h @@ -20,6 +20,7 @@ public: std::optional owner() const; const Identity & finalOwner() const; + Stored keyIdentity() const; Stored keyMessage() const; bool sameAs(const Identity &) const; diff --git a/include/erebos/network.h b/include/erebos/network.h index 8f3debe..5a1fca6 100644 --- a/include/erebos/network.h +++ b/include/erebos/network.h @@ -10,10 +10,15 @@ namespace erebos { class Server { + struct Priv; public: Server(const Head &, std::vector> &&); + Server(const std::shared_ptr &); ~Server(); + const Head & localHead() const; + + const Identity & identity() const; template S & svc(); class PeerList & peerList() const; @@ -22,7 +27,6 @@ public: private: Service & svcHelper(const std::type_info &); - struct Priv; const std::shared_ptr p; }; @@ -39,13 +43,27 @@ public: Peer(const std::shared_ptr & p); ~Peer(); + Server server() const; + + const Storage & tempStorage() const; + const PartialStorage & partialStorage() const; + std::string name() const; std::optional identity() const; bool hasChannel() const; bool send(UUID, const Ref &) const; + bool send(UUID, const Object &) const; + + bool operator==(const Peer & other) const; + bool operator!=(const Peer & other) const; + bool operator<(const Peer & other) const; + bool operator<=(const Peer & other) const; + bool operator>(const Peer & other) const; + bool operator>=(const Peer & other) const; private: + bool send(UUID, const Ref &, const Object &) const; std::shared_ptr p; }; diff --git a/include/erebos/pairing.h b/include/erebos/pairing.h new file mode 100644 index 0000000..f0952d8 --- /dev/null +++ b/include/erebos/pairing.h @@ -0,0 +1,104 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace erebos { + +using std::function; +using std::future; +using std::map; +using std::mutex; +using std::string; +using std::variant; +using std::vector; + +/** + * Template-less base class for the paring functionality that does not depend + * on the result parameter. + */ +class PairingServiceBase : public Service +{ +public: + typedef function RequestInitHook; + void onRequestInit(RequestInitHook); + + typedef function(const Peer &, string)> ConfirmHook; + void onResponse(ConfirmHook); + void onRequest(ConfirmHook); + + typedef function RequestNonceFailedHook; + void onRequestNonceFailed(RequestNonceFailedHook); + +protected: + void requestPairing(UUID serviceId, const Peer & peer); + virtual void handle(Context &) override; + virtual Ref handlePairingCompleteRef(const Peer &) = 0; + virtual void handlePairingResult(Context &) = 0; + +private: + static vector nonceDigest(const Identity & id1, const Identity & id2, + const vector & nonce1, const vector & nonce2); + static string confirmationNumber(const vector &); + void waitForConfirmation(Peer peer, string confirm); + + RequestInitHook requestInitHook; + ConfirmHook responseHook; + ConfirmHook requestHook; + RequestNonceFailedHook requestNonceFailedHook; + + optional result; + + enum class StatePhase { + NoPairing, + OurRequest, + OurRequestConfirm, + OurRequestReady, + PeerRequest, + PeerRequestConfirm, + PairingDone, + PairingFailed + }; + + struct State { + StatePhase phase; + vector nonce; + vector peerCheck; + }; + + map peerStates; + mutex stateLock; +}; + +template +class PairingService : public PairingServiceBase +{ +protected: + virtual Stored handlePairingComplete(const Peer &) = 0; + virtual void handlePairingResult(Context &, Stored) = 0; + + virtual Ref handlePairingCompleteRef(const Peer &) override final; + virtual void handlePairingResult(Context &) override final; +}; + +template +Ref PairingService::handlePairingCompleteRef(const Peer & peer) +{ + return handlePairingComplete(peer).ref(); +} + +template +void PairingService::handlePairingResult(Context & ctx) +{ + handlePairingResult(ctx, Stored::load(ctx.ref())); +} + +} diff --git a/include/erebos/storage.h b/include/erebos/storage.h index 29eaa8f..4f67a4b 100644 --- a/include/erebos/storage.h +++ b/include/erebos/storage.h @@ -123,6 +123,9 @@ public: explicit operator std::string() const; bool isZero() const; + static Digest of(const std::vector & content); + template static Digest of(const ObjectT &); + const std::array & arr() const { return value; } bool operator==(const Digest & other) const { return value == other.value; } @@ -136,6 +139,12 @@ private: std::array value; }; +template +Digest Digest::of(const ObjectT & obj) +{ + return Digest::of(obj.encode()); +} + class PartialRef { public: diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b97293c..d8a076a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -3,10 +3,12 @@ include_directories( ) add_library(erebos + attach channel identity message network + pairing pubkey service state diff --git a/src/attach.cpp b/src/attach.cpp new file mode 100644 index 0000000..2bc2b74 --- /dev/null +++ b/src/attach.cpp @@ -0,0 +1,124 @@ +#include + +#include "identity.h" +#include "pubkey.h" + +#include + +#include + +using namespace erebos; +using std::lock_guard; +using std::nullopt; +using std::runtime_error; + +static const UUID myUUID("4995a5f9-2d4d-48e9-ad3b-0bf1c2a1be7f"); + +AttachService::AttachService() = default; +AttachService::~AttachService() = default; + +UUID AttachService::uuid() const +{ + return myUUID; +} + +void AttachService::attachTo(const Peer & peer) +{ + requestPairing(myUUID, peer); +} + +Stored AttachService::handlePairingComplete(const Peer & peer) +{ + auto owner = peer.server().identity().finalOwner(); + auto id = peer.identity()->ref(); + auto prev = Stored>::load(*peer.identity()->ref()); + + auto idata = peer.tempStorage().store(IdentityData { + .prev = { prev }, + .name = nullopt, + .owner = Stored>::load(*owner.ref()), + .keyIdentity = prev->data->keyIdentity, + .keyMessage = nullopt, + }); + + auto key = SecretKey::load(owner.keyIdentity()); + if (!key) + throw runtime_error("failed to load secret key"); + + auto mkey = SecretKey::load(owner.keyMessage()); + if (!mkey) + throw runtime_error("failed to load secret key"); + + auto sdata = key->sign(idata); + + return peer.tempStorage().store(AttachIdentity { + .identity = sdata, + .keys = { key->getData(), mkey->getData() }, + }); +} + +void AttachService::handlePairingResult(Context & ctx, Stored att) +{ + if (att->identity->data->prev.size() != 1 || + att->identity->data->prev[0].ref().digest() != + ctx.local()->identity()->ref()->digest()) + return; + + if (att->identity->data->keyIdentity.ref().digest() != + ctx.local()->identity()->keyIdentity().ref().digest()) + return; + + auto key = SecretKey::load(ctx.peer().server().identity().keyIdentity()); + if (!key) + throw runtime_error("failed to load secret key"); + + auto id = Identity::load(key->signAdd(att->identity).ref()); + if (!id) + printf("New identity validation failed\n"); + + auto rid = ctx.local().ref().storage().copy(*id->ref()); + id = Identity::load(rid); + + auto owner = id->owner(); + if (!owner) + printf("New identity without owner\n"); + + // Store the keys + for (const auto & k : att->keys) { + SecretKey::fromData(owner->keyIdentity(), k); + SecretKey::fromData(owner->keyMessage(), k); + } + + ctx.local(ctx.local()->identity(*id)); +} + +AttachIdentity AttachIdentity::load(const Ref & ref) +{ + auto rec = ref->asRecord(); + if (!rec) + return AttachIdentity { + .identity = Stored>::load(ref.storage().zref()), + .keys = {}, + }; + + vector> keys; + for (auto s : rec->items("skey")) + if (const auto & b = s.asBinary()) + keys.push_back(*b); + + return AttachIdentity { + .identity = *rec->item("identity").as>(), + .keys = keys, + }; +} + +Ref AttachIdentity::store(const Storage & st) const +{ + vector items; + + items.emplace_back("identity", identity.ref()); + for (const auto & key : keys) + items.emplace_back("skey", key); + + return st.storeObject(Record(std::move(items))); +} diff --git a/src/identity.cpp b/src/identity.cpp index d7dd1f9..fe4cf53 100644 --- a/src/identity.cpp +++ b/src/identity.cpp @@ -60,6 +60,11 @@ const Identity & Identity::finalOwner() const return *this; } +Stored Identity::keyIdentity() const +{ + return p->data[0]->data->keyIdentity; +} + Stored Identity::keyMessage() const { return p->keyMessage; diff --git a/src/network.cpp b/src/network.cpp index c0bcc2a..48ba4b9 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -26,8 +26,23 @@ Server::Server(const Head & head, vector> && svc { } +Server:: Server(const std::shared_ptr & ptr): + p(ptr) +{ +} + Server::~Server() = default; +const Head & Server::localHead() const +{ + return p->localHead; +} + +const Identity & Server::identity() const +{ + return p->self; +} + Service & Server::svcHelper(const std::type_info & tinfo) { for (auto & s : p->services) @@ -45,6 +60,27 @@ PeerList & Server::peerList() const Peer::Peer(const shared_ptr & p): p(p) {} Peer::~Peer() = default; +Server Peer::server() const +{ + if (auto speer = p->speer.lock()) + return Server(speer->server.getptr()); + throw runtime_error("Server no longer running"); +} + +const Storage & Peer::tempStorage() const +{ + if (auto speer = p->speer.lock()) + return speer->tempStorage; + throw runtime_error("Server no longer running"); +} + +const PartialStorage & Peer::partialStorage() const +{ + if (auto speer = p->speer.lock()) + return speer->partStorage; + throw runtime_error("Server no longer running"); +} + string Peer::name() const { if (auto speer = p->speer.lock()) { @@ -87,6 +123,21 @@ bool Peer::hasChannel() const } bool Peer::send(UUID uuid, const Ref & ref) const +{ + return send(uuid, ref, *ref); +} + +bool Peer::send(UUID uuid, const Object & obj) const +{ + if (auto speer = p->speer.lock()) { + auto ref = speer->tempStorage.storeObject(obj); + return send(uuid, ref, obj); + } + + return false; +} + +bool Peer::send(UUID uuid, const Ref & ref, const Object & obj) const { if (hasChannel()) if (auto speer = p->speer.lock()) { @@ -94,13 +145,20 @@ bool Peer::send(UUID uuid, const Ref & ref) const { TransportHeader::Type::ServiceType, uuid }, { TransportHeader::Type::ServiceRef, ref }, }); - speer->send(header, { *ref }); + speer->send(header, { obj }); return true; } return false; } +bool Peer::operator==(const Peer & other) const { return p == other.p; } +bool Peer::operator!=(const Peer & other) const { return p != other.p; } +bool Peer::operator<(const Peer & other) const { return p < other.p; } +bool Peer::operator<=(const Peer & other) const { return p <= other.p; } +bool Peer::operator>(const Peer & other) const { return p > other.p; } +bool Peer::operator>=(const Peer & other) const { return p >= other.p; } + PeerList::PeerList(): p(new Priv) {} PeerList::PeerList(const shared_ptr & p): p(p) {} @@ -200,6 +258,11 @@ Server::Priv::~Priv() close(sock); } +shared_ptr Server::Priv::getptr() +{ + return shared_from_this(); +} + void Server::Priv::doListen() { vector buf, decrypted, *current; diff --git a/src/network.h b/src/network.h index c02dbc3..e22d453 100644 --- a/src/network.h +++ b/src/network.h @@ -134,11 +134,14 @@ struct WaitingRef optional check(ReplyBuilder &); }; -struct Server::Priv +struct Server::Priv : enable_shared_from_this { Priv(const Head & local, const Identity & self, vector> && svcs); ~Priv(); + + shared_ptr getptr(); + void doListen(); void doAnnounce(); diff --git a/src/pairing.cpp b/src/pairing.cpp new file mode 100644 index 0000000..9e161df --- /dev/null +++ b/src/pairing.cpp @@ -0,0 +1,212 @@ +#include + +#include "service.h" + +#include + +#include + +#include +#include +#include +#include + +using namespace erebos; + +using std::lock_guard; +using std::runtime_error; + +void PairingServiceBase::onRequestInit(RequestInitHook hook) +{ + lock_guard lock(stateLock); + requestInitHook = hook; +} + +void PairingServiceBase::onResponse(ConfirmHook hook) +{ + lock_guard lock(stateLock); + responseHook = hook; +} + +void PairingServiceBase::onRequest(ConfirmHook hook) +{ + lock_guard lock(stateLock); + requestHook = hook; +} + +void PairingServiceBase::onRequestNonceFailed(RequestNonceFailedHook hook) +{ + lock_guard lock(stateLock); + requestNonceFailedHook = hook; +} + +void PairingServiceBase::handle(Context & ctx) +{ + auto rec = ctx.ref()->asRecord(); + if (!rec) + return; + + auto pid = ctx.peer().identity(); + if (!pid) + throw runtime_error("Pairing request for peer without known identity"); + + lock_guard lock(stateLock); + auto & state = peerStates.try_emplace(ctx.peer(), State()).first->second; + + if (auto request = rec->item("request").asBinary()) { + if (state.phase != StatePhase::NoPairing) + return; + + if (requestInitHook) + requestInitHook(ctx.peer()); + + state.phase = StatePhase::PeerRequest; + state.peerCheck = *request; + state.nonce.resize(32); + RAND_bytes(state.nonce.data(), state.nonce.size()); + + ctx.peer().send(uuid(), Object(Record({ + { "response", state.nonce }, + }))); + } + + else if (auto response = rec->item("response").asBinary()) { + if (state.phase != StatePhase::OurRequest) { + fprintf(stderr, "Unexpected pairing response.\n"); // TODO + return; + } + + if (responseHook) { + string confirm = confirmationNumber(nonceDigest( + ctx.peer().server().identity(), *pid, + state.nonce, *response)); + std::thread(&PairingServiceBase::waitForConfirmation, + this, ctx.peer(), confirm).detach(); + } + + state.phase = StatePhase::OurRequestConfirm; + + ctx.peer().send(uuid(), Object(Record({ + { "reqnonce", state.nonce }, + }))); + } + + else if (auto reqnonce = rec->item("reqnonce").asBinary()) { + auto check = nonceDigest( + *pid, ctx.peer().server().identity(), + *reqnonce, vector()); + if (check != state.peerCheck) { + if (requestNonceFailedHook) + requestNonceFailedHook(ctx.peer()); + state.phase = StatePhase::PairingFailed; + return; + } + + if (requestHook) { + string confirm = confirmationNumber(nonceDigest( + *pid, ctx.peer().server().identity(), + *reqnonce, state.nonce)); + std::thread(&PairingServiceBase::waitForConfirmation, + this, ctx.peer(), confirm).detach(); + } + + state.phase = StatePhase::PeerRequestConfirm; + } + + else { + if (state.phase == StatePhase::OurRequestReady) { + handlePairingResult(ctx); + state.phase = StatePhase::PairingDone; + } else { + result = ctx.ref(); + } + } +} + +void PairingServiceBase::requestPairing(UUID serviceId, const Peer & peer) +{ + auto pid = peer.identity(); + if (!pid) + throw runtime_error("Pairing request for peer without known identity"); + + lock_guard lock(stateLock); + auto & state = peerStates.try_emplace(peer, State()).first->second; + + state.phase = StatePhase::OurRequest; + state.nonce.resize(32); + RAND_bytes(state.nonce.data(), state.nonce.size()); + + vector items; + items.emplace_back("request", nonceDigest( + peer.server().identity(), *pid, + state.nonce, vector())); + + peer.send(serviceId, Object(Record(std::move(items)))); +} + +vector PairingServiceBase::nonceDigest(const Identity & id1, const Identity & id2, + const vector & nonce1, const vector & nonce2) +{ + vector items; + items.emplace_back("id", id1.ref().value()); + items.emplace_back("id", id2.ref().value()); + items.emplace_back("nonce", nonce1); + items.emplace_back("nonce", nonce2); + + const auto arr = Digest::of(Object(Record(std::move(items)))).arr(); + vector ret(arr.size()); + std::copy_n(arr.begin(), arr.size(), ret.begin()); + return ret; +} + +string PairingServiceBase::confirmationNumber(const vector & digest) +{ + uint32_t confirm; + memcpy(&confirm, digest.data(), sizeof(confirm)); + string ret(7, '\0'); + snprintf(ret.data(), ret.size(), "%06d", ntohl(confirm) % 1000000); + return ret; +} + +void PairingServiceBase::waitForConfirmation(Peer peer, string confirm) +{ + ConfirmHook hook; + { + lock_guard lock(stateLock); + auto & state = peerStates.try_emplace(peer, State()).first->second; + if (state.phase == StatePhase::OurRequestConfirm) + hook = responseHook; + if (state.phase == StatePhase::PeerRequestConfirm) + hook = requestHook; + } + + bool ok = hook(peer, confirm).get(); + + lock_guard lock(stateLock); + auto & state = peerStates.try_emplace(peer, State()).first->second; + + if (ok) { + if (state.phase == StatePhase::OurRequestConfirm) { + if (result) { + peer.server().localHead().update([&] (const Stored & local) { + Service::Context ctx(new Service::Context::Priv { + .ref = *result, + .peer = peer, + .local = local, + }); + + handlePairingResult(ctx); + return ctx.local(); + }); + state.phase = StatePhase::PairingDone; + } else { + state.phase = StatePhase::OurRequestReady; + } + } else if (state.phase == StatePhase::PeerRequestConfirm) { + peer.send(uuid(), handlePairingCompleteRef(peer)); + state.phase = StatePhase::PairingDone; + } + } else { + state.phase = StatePhase::PairingFailed; + } +} diff --git a/src/pubkey.cpp b/src/pubkey.cpp index 6d85d33..6461c10 100644 --- a/src/pubkey.cpp +++ b/src/pubkey.cpp @@ -88,6 +88,45 @@ optional SecretKey::load(const Stored & pub) return SecretKey(key, pub); } +optional SecretKey::fromData(const Stored & pub, const vector & sdata) +{ + shared_ptr pkey( + EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL, + sdata.data(), sdata.size()), + EVP_PKEY_free); + if (!pkey) + return nullopt; + + vector keyData; + size_t keyLen; + + EVP_PKEY_get_raw_public_key(pkey.get(), nullptr, &keyLen); + keyData.resize(keyLen); + EVP_PKEY_get_raw_public_key(pkey.get(), keyData.data(), &keyLen); + + EVP_PKEY_get_raw_public_key(pub->key.get(), nullptr, &keyLen); + keyData.resize(keyLen); + EVP_PKEY_get_raw_public_key(pub->key.get(), keyData.data(), &keyLen); + + if (EVP_PKEY_cmp(pkey.get(), pub->key.get()) != 1) + return nullopt; + + pub.ref().storage().storeKey(pub.ref(), sdata); + return SecretKey(std::move(pkey), pub); +} + +vector SecretKey::getData() const +{ + vector keyData; + size_t keyLen; + + EVP_PKEY_get_raw_private_key(key.get(), nullptr, &keyLen); + keyData.resize(keyLen); + EVP_PKEY_get_raw_private_key(key.get(), keyData.data(), &keyLen); + + return keyData; +} + vector SecretKey::sign(const Digest & dgst) const { unique_ptr diff --git a/src/pubkey.h b/src/pubkey.h index ef7e322..5cb693b 100644 --- a/src/pubkey.h +++ b/src/pubkey.h @@ -34,6 +34,9 @@ public: static SecretKey generate(const Storage & st); static optional load(const Stored & st); + static optional fromData(const Stored &, const vector &); + vector getData() const; + Stored pub() const { return pub_; } template diff --git a/src/storage.cpp b/src/storage.cpp index 51ce9bc..6e2f118 100644 --- a/src/storage.cpp +++ b/src/storage.cpp @@ -613,13 +613,7 @@ Ref Storage::zref() const Digest PartialStorage::Priv::storeBytes(const vector & content) const { - array arr; - int ret = blake2b(arr.data(), content.data(), nullptr, - Digest::size, content.size(), 0); - if (ret != 0) - throw runtime_error("failed to compute digest"); - - Digest digest(arr); + Digest digest = Digest::of(content); backend->storeBytes(digest, content); return digest; } @@ -631,10 +625,7 @@ optional> PartialStorage::Priv::loadBytes(const Digest & digest) return nullopt; auto content = ocontent.value(); - array arr; - int ret = blake2b(arr.data(), content.data(), nullptr, - Digest::size, content.size(), 0); - if (ret != 0 || digest != Digest(arr)) + if (digest != Digest::of(content)) throw runtime_error("digest verification failed"); return content; @@ -820,6 +811,17 @@ bool Digest::isZero() const return true; } +Digest Digest::of(const vector & content) +{ + array arr; + int ret = blake2b(arr.data(), content.data(), nullptr, + size, content.size(), 0); + if (ret != 0) + throw runtime_error("failed to compute digest"); + + return Digest(arr); +} + PartialRef PartialRef::create(PartialStorage st, const Digest & digest) { -- cgit v1.2.3