summaryrefslogtreecommitdiff
path: root/src/pairing.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/pairing.cpp')
-rw-r--r--src/pairing.cpp306
1 files changed, 306 insertions, 0 deletions
diff --git a/src/pairing.cpp b/src/pairing.cpp
new file mode 100644
index 0000000..dca5b03
--- /dev/null
+++ b/src/pairing.cpp
@@ -0,0 +1,306 @@
+#include <erebos/pairing.h>
+
+#include "service.h"
+
+#include <future>
+#include <openssl/rand.h>
+
+#include <arpa/inet.h>
+
+#include <algorithm>
+#include <stdexcept>
+#include <thread>
+#include <vector>
+
+using namespace erebos;
+
+using std::lock_guard;
+using std::make_shared;
+using std::runtime_error;
+using std::scoped_lock;
+using std::thread;
+using std::unique_lock;
+
+PairingServiceBase::PairingServiceBase(Config && c):
+ config(move(c))
+{
+}
+
+PairingServiceBase::~PairingServiceBase()
+{
+ // There may be some threads in waitForConfirmation waiting on client
+ // promise, so make sure they do not touch the service state anymore:
+ for (auto & [peer, state] : peerStates) {
+ scoped_lock lock(state->lock);
+ if (state->phase != StatePhase::PairingDone &&
+ state->phase != StatePhase::PairingFailed) {
+ state->outcome.set_value(Outcome::Stale);
+ state->phase = StatePhase::PairingFailed;
+ }
+ }
+}
+
+PairingServiceBase::Config & PairingServiceBase::Config::onRequestInit(RequestInitHook hook)
+{
+ requestInitHook = hook;
+ return *this;
+}
+
+PairingServiceBase::Config & PairingServiceBase::Config::onResponse(ConfirmHook hook)
+{
+ responseHook = hook;
+ return *this;
+}
+
+PairingServiceBase::Config & PairingServiceBase::Config::onRequest(ConfirmHook hook)
+{
+ requestHook = hook;
+ return *this;
+}
+
+PairingServiceBase::Config & PairingServiceBase::Config::onRequestNonceFailed(RequestNonceFailedHook hook)
+{
+ requestNonceFailedHook = hook;
+ return *this;
+}
+
+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(), new State()).first->second;
+ unique_lock lock_state(state->lock);
+
+ if (auto request = rec->item("request").asBinary()) {
+ auto idReqRef = rec->item("id-req").asRef();
+ if (!idReqRef)
+ return;
+ auto idReq = Identity::load(*idReqRef);
+ if (!idReq)
+ return;
+ if (!idReq->sameAs(*pid))
+ return;
+
+ auto idRspRef = rec->item("id-rsp").asRef();
+ if (!idRspRef)
+ return;
+ auto idRsp = Identity::load(*idRspRef);
+ if (!idRsp)
+ return;
+ if (!idRsp->sameAs(ctx.peer().server().identity()))
+ return;
+
+ if (state->phase >= StatePhase::PairingDone) {
+ auto nstate = make_shared<State>();
+ lock_state = unique_lock(nstate->lock);
+ state = move(nstate);
+ } else if (state->phase != StatePhase::NoPairing)
+ return;
+
+ if (config.requestInitHook)
+ config.requestInitHook(ctx.peer());
+
+ state->phase = StatePhase::PeerRequest;
+ state->idReq = idReq;
+ state->idRsp = idRsp;
+ 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 (config.responseHook) {
+ string confirm = confirmationNumber(nonceDigest(
+ *state->idReq, *state->idRsp,
+ state->nonce, *response));
+ std::thread(&PairingServiceBase::waitForConfirmation,
+ this, ctx.peer(), state, confirm, config.responseHook).detach();
+ }
+
+ state->phase = StatePhase::OurRequestConfirm;
+
+ ctx.peer().send(uuid(), Object(Record({
+ { "reqnonce", state->nonce },
+ })));
+ }
+
+ else if (auto reqnonce = rec->item("reqnonce").asBinary()) {
+ if (state->phase != StatePhase::PeerRequest)
+ return;
+
+ auto check = nonceDigest(
+ *state->idReq, *state->idRsp,
+ *reqnonce, vector<uint8_t>());
+ if (check != state->peerCheck) {
+ if (config.requestNonceFailedHook)
+ config.requestNonceFailedHook(ctx.peer());
+ if (state->phase < StatePhase::PairingDone) {
+ state->phase = StatePhase::PairingFailed;
+ ctx.afterCommit([&]() {
+ state->outcome.set_value(Outcome::NonceMismatch);
+ });
+ }
+ return;
+ }
+
+ if (config.requestHook) {
+ string confirm = confirmationNumber(nonceDigest(
+ *state->idReq, *state->idRsp,
+ *reqnonce, state->nonce));
+ std::thread(&PairingServiceBase::waitForConfirmation,
+ this, ctx.peer(), state, confirm, config.requestHook).detach();
+ }
+
+ state->phase = StatePhase::PeerRequestConfirm;
+ }
+
+ else if (rec->item("reject")) {
+ if (state->phase < StatePhase::PairingDone) {
+ state->phase = StatePhase::PairingFailed;
+ ctx.afterCommit([&]() {
+ state->outcome.set_value(Outcome::PeerRejected);
+ });
+ }
+ }
+
+ else {
+ if (state->phase == StatePhase::OurRequestReady) {
+ handlePairingResult(ctx);
+ state->phase = StatePhase::PairingDone;
+ ctx.afterCommit([&]() {
+ state->outcome.set_value(Outcome::Success);
+ });
+ } 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");
+
+ unique_lock lock(stateLock);
+ auto & state = peerStates.try_emplace(peer, new State()).first->second;
+
+ if (state->phase != StatePhase::NoPairing) {
+ auto nstate = make_shared<State>();
+ lock = unique_lock(nstate->lock);
+ state = move(nstate);
+ }
+
+ state->phase = StatePhase::OurRequest;
+ state->idReq = peer.server().identity();
+ state->idRsp = pid;
+ state->nonce.resize(32);
+ RAND_bytes(state->nonce.data(), state->nonce.size());
+
+ vector<Record::Item> items;
+ items.emplace_back("id-req", state->idReq->ref().value());
+ items.emplace_back("id-rsp", state->idRsp->ref().value());
+ items.emplace_back("request", nonceDigest(
+ *state->idReq, *state->idRsp,
+ state->nonce, vector<uint8_t>()));
+
+ peer.send(serviceId, Object(Record(std::move(items))));
+}
+
+vector<uint8_t> PairingServiceBase::nonceDigest(const Identity & idReq, const Identity & idRsp,
+ const vector<uint8_t> & nonceReq, const vector<uint8_t> & nonceRsp)
+{
+ vector<Record::Item> items;
+ items.emplace_back("id-req", idReq.ref().value());
+ items.emplace_back("id-rsp", idRsp.ref().value());
+ items.emplace_back("nonce-req", nonceReq);
+ items.emplace_back("nonce-rsp", nonceRsp);
+
+ const auto arr = Digest::of(Object(Record(std::move(items)))).arr();
+ vector<uint8_t> ret(arr.size());
+ std::copy_n(arr.begin(), arr.size(), ret.begin());
+ return ret;
+}
+
+string PairingServiceBase::confirmationNumber(const vector<uint8_t> & digest)
+{
+ uint32_t confirm;
+ memcpy(&confirm, digest.data(), sizeof(confirm));
+ string ret(6, '\0');
+ snprintf(ret.data(), ret.size() + 1, "%06d", ntohl(confirm) % 1000000);
+ return ret;
+}
+
+void PairingServiceBase::waitForConfirmation(Peer peer, weak_ptr<State> wstate, string confirm, ConfirmHook hook)
+{
+ future<Outcome> outcome;
+ if (auto state = wstate.lock()) {
+ outcome = state->outcome.get_future();
+ } else {
+ return;
+ }
+
+ bool ok;
+ try {
+ ok = hook(peer, confirm, std::move(outcome)).get();
+ }
+ catch (const std::future_error & e) {
+ if (e.code() == std::future_errc::broken_promise)
+ ok = false;
+ else
+ throw;
+ }
+
+ auto state = wstate.lock();
+ if (!state)
+ return; // Server was closed
+
+ scoped_lock lock(state->lock);
+
+ if (ok) {
+ if (state->phase == StatePhase::OurRequestConfirm) {
+ if (result) {
+ peer.server().localHead().update([&] (const Stored<LocalState> & local) {
+ Service::Context ctx(new Service::Context::Priv {
+ .ref = *result,
+ .peer = peer,
+ .local = local,
+ });
+
+ handlePairingResult(ctx);
+ return ctx.local();
+ });
+ state->phase = StatePhase::PairingDone;
+ state->outcome.set_value(Outcome::Success);
+ } else {
+ state->phase = StatePhase::OurRequestReady;
+ }
+ } else if (state->phase == StatePhase::PeerRequestConfirm) {
+ peer.send(uuid(), handlePairingCompleteRef(peer));
+ state->phase = StatePhase::PairingDone;
+ state->outcome.set_value(Outcome::Success);
+ }
+ } else {
+ if (state->phase != StatePhase::PairingFailed) {
+ peer.send(uuid(), Object(Record({{ "reject", Record::Item::Empty {} }})));
+ state->phase = StatePhase::PairingFailed;
+ state->outcome.set_value(Outcome::UserRejected);
+ }
+ }
+}