diff --git a/MSVC/ring-daemon.vcxproj b/MSVC/ring-daemon.vcxproj
index a5d3abc5f..10d1263f3 100644
--- a/MSVC/ring-daemon.vcxproj
+++ b/MSVC/ring-daemon.vcxproj
@@ -718,6 +718,8 @@
+
+
@@ -876,6 +878,9 @@
+
+
+
diff --git a/MSVC/ring-daemon.vcxproj.filters b/MSVC/ring-daemon.vcxproj.filters
index 2324c271d..6efcfd311 100644
--- a/MSVC/ring-daemon.vcxproj.filters
+++ b/MSVC/ring-daemon.vcxproj.filters
@@ -421,6 +421,12 @@
Source Files
+
+ Source Files\jamidht
+
+
+ Source Files\jamidht
+
@@ -867,6 +873,15 @@
Source Files
+
+ Source Files\jamidht
+
+
+ Source Files\jamidht
+
+
+ Source Files\jamidht
+
diff --git a/bin/Makefile.am b/bin/Makefile.am
index 7ba240018..63f86319d 100644
--- a/bin/Makefile.am
+++ b/bin/Makefile.am
@@ -34,6 +34,6 @@ dring_CXXFLAGS= -I$(top_srcdir)/src ${DBUSCPP_CFLAGS} \
dring_LDADD = dbus/libclient_dbus.la ${DBUSCPP_LIBS} $(top_builddir)/src/libring.la -ldl
endif
-if RING_NODEJS
+if ENABLE_NODEJS
SUBDIRS+=nodejs
endif
diff --git a/configure.ac b/configure.ac
index 6cb5ae83b..208a8b854 100644
--- a/configure.ac
+++ b/configure.ac
@@ -421,16 +421,16 @@ AC_DEFINE_UNQUOTED([HAVE_RINGNS], `if test "x$enable_ringns" != "xno"; then echo
dnl nodejs module
AC_ARG_WITH([nodejs], AS_HELP_STRING([--with-nodejs], [Enable NodeJS module]))
-AM_CONDITIONAL([RING_NODEJS], test "x$enable_nodejs" != "xno", [Define if you use the NodeJS module])
+AM_CONDITIONAL([ENABLE_NODEJS], test "x$enable_nodejs" != "xno", [Define if you use the NodeJS module])
AC_DEFINE_UNQUOTED([HAVE_NODEJS], `if test "x$enable_ringns" != "xno"; then echo 1; else echo 0; fi`, [Define if you use the NodeJS module])
AS_IF([test "x$with_nodejs" = "xyes"], [
AC_PATH_PROG(SWIG, swig, "")
AS_AC_EXPAND(SBINDIR, $sbindir)
AC_SUBST(SBINDIR)
AC_CONFIG_FILES([bin/nodejs/Makefile])
- AM_CONDITIONAL(RING_NODEJS, true)
+ AM_CONDITIONAL(ENABLE_NODEJS, true)
],
- AM_CONDITIONAL(RING_NODEJS, false)
+ AM_CONDITIONAL(ENABLE_NODEJS, false)
);
AS_IF([test "x$enable_ringns" != "xno"], [
diff --git a/src/im/message_engine.cpp b/src/im/message_engine.cpp
index 033fdec71..956710919 100644
--- a/src/im/message_engine.cpp
+++ b/src/im/message_engine.cpp
@@ -211,7 +211,7 @@ MessageEngine::load()
}
JAMI_DBG("[Account %s] loaded %lu messages from %s", account_.getAccountID().c_str(), loaded, savePath_.c_str());
} catch (const std::exception& e) {
- JAMI_ERR("[Account %s] couldn't load messages from %s: %s", account_.getAccountID().c_str(), savePath_.c_str(), e.what());
+ JAMI_DBG("[Account %s] couldn't load messages from %s: %s", account_.getAccountID().c_str(), savePath_.c_str(), e.what());
}
}
diff --git a/src/jamidht/Makefile.am b/src/jamidht/Makefile.am
index 36014ec78..21f0493f7 100644
--- a/src/jamidht/Makefile.am
+++ b/src/jamidht/Makefile.am
@@ -19,10 +19,16 @@ libringacc_la_SOURCES = \
accountarchive.cpp \
accountarchive.h \
p2p.cpp \
- p2p.h
+ p2p.h \
+ contact_list.h \
+ contact_list.cpp \
+ account_manager.h \
+ account_manager.cpp
if RINGNS
libringacc_la_SOURCES += \
namedirectory.cpp \
namedirectory.h
endif
+
+nobase_include_HEADERS= jami_contact.h
diff --git a/src/jamidht/account_manager.cpp b/src/jamidht/account_manager.cpp
new file mode 100644
index 000000000..ff69f0ddd
--- /dev/null
+++ b/src/jamidht/account_manager.cpp
@@ -0,0 +1,1095 @@
+/*
+ * Copyright (C) 2014-2019 Savoir-faire Linux Inc.
+ * Author : Adrien Béraud
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+#include "account_manager.h"
+#include "accountarchive.h"
+#include "jamiaccount.h"
+#include "base64.h"
+#include "dring/account_const.h"
+#include "account_schema.h"
+#include "archiver.h"
+
+#include "libdevcrypto/Common.h"
+
+#include
+
+#include
+#include
+#include
+
+namespace jami {
+
+const constexpr auto EXPORT_KEY_RENEWAL_TIME = std::chrono::minutes(20);
+constexpr static const char* const DHT_TYPE_NS = "cx.ring";
+
+template
+std::unique_ptr dynamic_unique_cast(std::unique_ptr&& p) {
+ if (auto cast = dynamic_cast(p.get())) {
+ std::unique_ptr result(cast);
+ p.release();
+ return result;
+ }
+ return {};
+}
+
+void
+ArchiveAccountManager::initAuthentication(
+ CertRequest request,
+ std::unique_ptr credentials,
+ AuthSuccessCallback onSuccess,
+ AuthFailureCallback onFailure,
+ OnChangeCallback onChange)
+{
+ auto ctx = std::make_shared();
+ ctx->request = std::move(request);
+ ctx->credentials = dynamic_unique_cast(std::move(credentials));
+ ctx->onSuccess = std::move(onSuccess);
+ ctx->onFailure = std::move(onFailure);
+
+ if (not ctx->credentials) {
+ onFailure(AuthError::INVALID_ARGUMENTS, "invalid credentials");
+ return;
+ }
+
+ onChange_ = std::move(onChange);
+
+ if (ctx->credentials->scheme == "dht") {
+ loadFromDHT(ctx);
+ return;
+ }
+
+ dht::ThreadPool::computation().run([
+ ctx = std::move(ctx),
+ onAsync = onAsync_
+ ]() mutable {
+ onAsync([ctx = std::move(ctx)](AccountManager& accountManager) mutable {
+ auto& this_ = *static_cast(&accountManager);
+ try {
+ if (ctx->credentials->scheme == "file") {
+ this_.loadFromFile(ctx);
+ return;
+ } else {
+ if (ctx->credentials->updateIdentity.first and ctx->credentials->updateIdentity.second) {
+ auto future_keypair = dht::ThreadPool::computation().get(&dev::KeyPair::create);
+ AccountArchive a;
+ JAMI_WARN("[Auth] converting certificate from old account %s", ctx->credentials->updateIdentity.first->getPublicKey().getId().toString().c_str());
+ a.id = std::move(ctx->credentials->updateIdentity);
+ try {
+ a.ca_key = std::make_shared(fileutils::loadFile("ca.key", this_.path_));
+ } catch (...) {}
+ this_.updateCertificates(a, ctx->credentials->updateIdentity);
+ auto keypair = future_keypair.get();
+ a.eth_key = keypair.secret().makeInsecure().asBytes();
+ this_.onArchiveLoaded(*ctx, std::move(a));
+ } else {
+ this_.createAccount(ctx);
+ }
+ }
+ } catch (const std::exception& e) {
+ ctx->onFailure(AuthError::UNKNOWN, e.what());
+ }
+ });
+ });
+}
+
+bool
+ArchiveAccountManager::updateCertificates(AccountArchive& archive, dht::crypto::Identity& device)
+{
+ JAMI_WARN("Updating certificates");
+ using Certificate = dht::crypto::Certificate;
+
+ // We need the CA key to resign certificates
+ if (not archive.id.first or
+ not *archive.id.first or
+ not archive.id.second or
+ not archive.ca_key or
+ not *archive.ca_key)
+ return false;
+
+ // Currently set the CA flag and update expiration dates
+ bool updated = false;
+
+ auto& cert = archive.id.second;
+ auto ca = cert->issuer;
+ // Update CA if possible and relevant
+ if (not ca or (not ca->issuer and (not ca->isCA() or ca->getExpiration() < clock::now()))) {
+ ca = std::make_shared(Certificate::generate(*archive.ca_key, "Jami CA", {}, true));
+ updated = true;
+ JAMI_DBG("CA CRT re-generated");
+ }
+
+ // Update certificate
+ if (updated or not cert->isCA() or cert->getExpiration() < clock::now()) {
+ cert = std::make_shared(Certificate::generate(*archive.id.first, "Jami", dht::crypto::Identity{archive.ca_key, ca}, true));
+ updated = true;
+ JAMI_DBG("Jami CRT re-generated");
+ }
+
+ if (updated and device.first and *device.first) {
+ // update device certificate
+ device.second = std::make_shared(Certificate::generate(*device.first, "Jami device", archive.id));
+ JAMI_DBG("device CRT re-generated");
+ }
+
+ return updated;
+}
+
+void
+ArchiveAccountManager::createAccount(const std::shared_ptr& ctx)
+{
+ AccountArchive a;
+ auto future_keypair = dht::ThreadPool::computation().get(&dev::KeyPair::create);
+ auto ca = dht::crypto::generateIdentity("Jami CA");
+ if (!ca.first || !ca.second) {
+ throw std::runtime_error("Can't generate CA for this account.");
+ }
+ a.id = dht::crypto::generateIdentity("Jami", ca, 4096, true);
+ if (!a.id.first || !a.id.second) {
+ throw std::runtime_error("Can't generate identity for this account.");
+ }
+ JAMI_WARN("[Auth] new account: CA: %s, RingID: %s",
+ ca.second->getId().toString().c_str(),
+ a.id.second->getId().toString().c_str());
+ a.ca_key = ca.first;
+ auto keypair = future_keypair.get();
+ a.eth_key = keypair.secret().makeInsecure().asBytes();
+ onArchiveLoaded(*ctx, std::move(a));
+}
+
+void
+ArchiveAccountManager::loadFromFile(const std::shared_ptr& ctx)
+{
+ AccountArchive archive;
+ try {
+ archive = AccountArchive(ctx->credentials->uri, ctx->credentials->password);
+ } catch (const std::exception& ex) {
+ JAMI_WARN("[Auth] can't read file: %s", ex.what());
+ ctx->onFailure(AuthError::UNKNOWN, ex.what());
+ return;
+ }
+ onArchiveLoaded(*ctx, std::move(archive));
+}
+
+struct ArchiveAccountManager::DhtLoadContext {
+ dht::DhtRunner dht;
+ std::pair stateOld {false, true};
+ std::pair stateNew {false, true};
+ bool found {false};
+};
+
+void
+ArchiveAccountManager::loadFromDHT(const std::shared_ptr& ctx)
+{
+ ctx->dhtContext = std::make_unique();
+ ctx->dhtContext->dht.run(ctx->credentials->dhtPort, {}, true);
+ for (const auto& bootstrap : ctx->credentials->dhtBootstrap)
+ ctx->dhtContext->dht.bootstrap(bootstrap);
+ auto searchEnded = [ctx]() {
+ if (not ctx->dhtContext or ctx->dhtContext->found) {
+ return;
+ }
+ auto& s = *ctx->dhtContext;
+ if (s.stateOld.first && s.stateNew.first) {
+ dht::ThreadPool::computation().run([ctx, network_error = !s.stateOld.second && !s.stateNew.second]{
+ ctx->dhtContext.reset();
+ JAMI_WARN("[Auth] failure looking for archive on DHT: %s", /**/network_error ? "network error" : "not found");
+ ctx->onFailure(network_error ? AuthError::NETWORK : AuthError::UNKNOWN, "");
+ });
+ }
+ };
+
+ auto search = [ctx, searchEnded, onAsync = onAsync_](bool previous) {
+ std::vector key;
+ dht::InfoHash loc;
+ auto& s = previous ? ctx->dhtContext->stateOld : ctx->dhtContext->stateNew;
+
+ // compute archive location and decryption keys
+ try {
+ std::tie(key, loc) = computeKeys(ctx->credentials->password, ctx->credentials->uri, previous);
+ JAMI_DBG("[Auth] trying to load account from DHT with %s at %s", /**/ctx->credentials->uri.c_str(), loc.toString().c_str());
+ ctx->dhtContext->dht.get(loc, [ctx, key=std::move(key), onAsync](const std::shared_ptr& val) {
+ std::vector decrypted;
+ try {
+ decrypted = archiver::decompress(dht::crypto::aesDecrypt(val->data, key));
+ } catch (const std::exception& ex) {
+ return true;
+ }
+ JAMI_DBG("[Auth] found archive on the DHT");
+ ctx->dhtContext->found = true;
+ dht::ThreadPool::computation().run([
+ ctx,
+ decrypted = std::move(decrypted),
+ onAsync
+ ]{
+ try {
+ auto archive = AccountArchive(decrypted);
+ onAsync([&](AccountManager& accountManager) {
+ auto& this_ = *static_cast(&accountManager);
+ if (ctx->dhtContext) {
+ ctx->dhtContext->dht.join();
+ ctx->dhtContext.reset();
+ }
+ this_.onArchiveLoaded(*ctx, std::move(archive)/*, std::move(contacts)*/);
+ });
+ } catch (const std::exception& e) {
+ ctx->onFailure(AuthError::UNKNOWN, "");
+ }
+ });
+ return not ctx->dhtContext->found;
+ }, [=, &s](bool ok) {
+ JAMI_DBG("[Auth] DHT archive search ended at %s", /**/loc.toString().c_str());
+ s.first = true;
+ s.second = ok;
+ searchEnded();
+ });
+ } catch (const std::exception& e) {
+ // JAMI_ERR("Error computing kedht::ThreadPool::computation().run(ys: %s", e.what());
+ s.first = true;
+ s.second = true;
+ searchEnded();
+ return;
+ }
+ };
+ dht::ThreadPool::computation().run(std::bind(search, true));
+ dht::ThreadPool::computation().run(std::bind(search, false));
+}
+
+void
+ArchiveAccountManager::onArchiveLoaded(
+ AuthContext& ctx,
+ AccountArchive&& a)
+{
+ auto ethAccount = dev::KeyPair(dev::Secret(a.eth_key)).address().hex();
+ fileutils::check_dir(path_.c_str(), 0700);
+
+ auto path = fileutils::getFullPath(path_, archivePath_);
+ a.save(path, ctx.credentials ? ctx.credentials->password : "");
+
+ if (not a.id.second->isCA()) {
+ JAMI_ERR("[Auth] trying to sign a certificate with a non-CA.");
+ }
+ JAMI_WARN("generating device certificate");
+
+ auto request = ctx.request.get();
+ if (not request->verify()) {
+ JAMI_ERR("[Auth] Invalid certificate request.");
+ ctx.onFailure(AuthError::INVALID_ARGUMENTS, "");
+ return;
+ }
+
+ auto deviceCertificate = std::make_shared(dht::crypto::Certificate::generate(*request, a.id));
+ auto receipt = makeReceipt(a.id, *deviceCertificate, ethAccount);
+ auto receiptSignature = a.id.first->sign({receipt.first.begin(), receipt.first.end()});
+
+ auto info = std::make_unique();
+ info->identity.second = deviceCertificate;
+ info->contacts = std::make_unique(a.id.second, path_, onChange_);
+ info->contacts->setContacts(a.contacts);
+ info->accountId = a.id.second->getId().toString();
+ info->deviceId = deviceCertificate->getPublicKey().getId().toString();
+ info->ethAccount = ethAccount;
+ info->announce = std::move(receipt.second);
+ info_ = std::move(info);
+
+ JAMI_WARN("[Auth] created new device: %s", info_->deviceId.c_str());
+ ctx.onSuccess(*info_, {}, std::move(receipt.first), std::move(receiptSignature));
+}
+
+std::pair, dht::InfoHash>
+ArchiveAccountManager::computeKeys(const std::string& password, const std::string& pin, bool previous)
+{
+ // Compute time seed
+ auto now = std::chrono::duration_cast(clock::now().time_since_epoch());
+ auto tseed = now.count() / std::chrono::seconds(EXPORT_KEY_RENEWAL_TIME).count();
+ if (previous)
+ tseed--;
+ std::stringstream ss;
+ ss << std::hex << tseed;
+ auto tseed_str = ss.str();
+
+ // Generate key for archive encryption, using PIN as the salt
+ std::vector salt_key;
+ salt_key.reserve(pin.size() + tseed_str.size());
+ salt_key.insert(salt_key.end(), pin.begin(), pin.end());
+ salt_key.insert(salt_key.end(), tseed_str.begin(), tseed_str.end());
+ auto key = dht::crypto::stretchKey(password, salt_key, 256/8);
+
+ // Generate public storage location as SHA1(key).
+ auto loc = dht::InfoHash::get(key);
+
+ return {key, loc};
+}
+
+std::pair>
+ArchiveAccountManager::makeReceipt(const dht::crypto::Identity& id, const dht::crypto::Certificate& device, const std::string& ethAccount)
+{
+ JAMI_DBG("[Auth] signing device receipt");
+ auto devId = device.getId();
+ DeviceAnnouncement announcement;
+ announcement.dev = devId;
+ dht::Value ann_val {announcement};
+ ann_val.sign(*id.first);
+
+ std::ostringstream is;
+ is << "{\"id\":\"" << id.second->getId()
+ << "\",\"dev\":\"" << devId
+ << "\",\"eth\":\"" << ethAccount
+ << "\",\"announce\":\"" << base64::encode(ann_val.getPacked()) << "\"}";
+
+ //auto announce_ = ;
+ return {is.str(), std::make_shared(std::move(ann_val))};
+}
+
+bool
+ArchiveAccountManager::needsMigration(const dht::crypto::Identity& id)
+{
+ if (not id.second)
+ return false;
+ auto cert = id.second->issuer;
+ while (cert) {
+ if (not cert->isCA()){
+ JAMI_WARN("certificate %s is not a CA, needs update.", cert->getId().toString().c_str());
+ return true;
+ }
+ if (cert->getExpiration() < clock::now()) {
+ JAMI_WARN("certificate %s is expired, needs update.", cert->getId().toString().c_str());
+ return true;
+ }
+ cert = cert->issuer;
+ }
+ return false;
+}
+
+dht::crypto::Identity
+AccountManager::loadIdentity(const std::string& crt_path, const std::string& key_path, const std::string& key_pwd) const
+{
+ JAMI_DBG("Loading certificate from '%s' and key from '%s' at %s", crt_path.c_str(), key_path.c_str(), path_.c_str());
+ try {
+ dht::crypto::Certificate dht_cert(fileutils::loadFile(crt_path, path_));
+ dht::crypto::PrivateKey dht_key(fileutils::loadFile(key_path, path_), key_pwd);
+ auto crt_id = dht_cert.getLongId();
+ if (!crt_id or crt_id != dht_key.getPublicKey().getLongId()) {
+ JAMI_ERR("Device certificate not matching public key!");
+ return {};
+ }
+ if (not dht_cert.issuer) {
+ JAMI_ERR("Device certificate %s has no issuer", dht_cert.getId().to_c_str());
+ return {};
+ }
+ // load revocation lists for device authority (account certificate).
+ tls::CertificateStore::instance().loadRevocations(*dht_cert.issuer);
+
+ return {
+ std::make_shared(std::move(dht_key)),
+ std::make_shared(std::move(dht_cert))
+ };
+ }
+ catch (const std::exception& e) {
+ JAMI_ERR("Error loading identity: %s", e.what());
+ }
+ return {};
+}
+
+const AccountInfo*
+AccountManager::useIdentity(
+ const dht::crypto::Identity& identity,
+ const std::string& receipt,
+ const std::vector& receiptSignature,
+ OnChangeCallback&& onChange)
+{
+ if (receipt.empty() or receiptSignature.empty())
+ return nullptr;
+
+ if (not identity.first or not identity.second) {
+ JAMI_ERR("[Auth] no identity provided");
+ return nullptr;
+ }
+
+ auto accountCertificate = identity.second->issuer;
+ if (not accountCertificate) {
+ JAMI_ERR("[Auth] device certificate must be issued by the account certificate");
+ return nullptr;
+ }
+
+ // match certificate chain
+ auto contactList = std::make_unique(accountCertificate, path_, onChange);
+ if (not contactList->isValidAccountDevice(*identity.second)) {
+ JAMI_ERR("[Auth] can't use identity: device certificate chain can't be verified");
+ return nullptr;
+ }
+
+ auto pk = accountCertificate->getPublicKey();
+ JAMI_DBG("[Auth] checking device receipt for %s", pk.getId().toString().c_str());
+ if (!pk.checkSignature({receipt.begin(), receipt.end()}, receiptSignature)) {
+ JAMI_ERR("[Auth] device receipt signature check failed");
+ return nullptr;
+ }
+
+ Json::Value root;
+ Json::CharReaderBuilder rbuilder;
+ auto reader = std::unique_ptr(rbuilder.newCharReader());
+ if (!reader->parse(&receipt[0], &receipt[receipt.size()], &root, nullptr)) {
+ JAMI_ERR() << this << " device receipt parsing error";
+ return nullptr;
+ }
+
+ auto dev_id = root["dev"].asString();
+ if (dev_id != identity.second->getId().toString()) {
+ JAMI_ERR("[Auth] device ID mismatch between receipt and certificate");
+ return nullptr;
+ }
+ auto id = root["id"].asString();
+ if (id != pk.getId().toString()) {
+ JAMI_ERR("[Auth] account ID mismatch between receipt and certificate");
+ return nullptr;
+ }
+
+ dht::Value announce_val;
+ try {
+ auto announce = base64::decode(root["announce"].asString());
+ msgpack::object_handle announce_msg = msgpack::unpack((const char*)announce.data(), announce.size());
+ announce_val.msgpack_unpack(announce_msg.get());
+ if (not announce_val.checkSignature()) {
+ JAMI_ERR("[Auth] announce signature check failed");
+ return nullptr;
+ }
+ DeviceAnnouncement da;
+ da.unpackValue(announce_val);
+ if (da.from.toString() != id or da.dev.toString() != dev_id) {
+ JAMI_ERR("[Auth] device ID mismatch in announce");
+ return nullptr;
+ }
+ } catch (const std::exception& e) {
+ JAMI_ERR("[Auth] can't read announce: %s", e.what());
+ return nullptr;
+ }
+
+ onChange_ = std::move(onChange);
+
+ auto info = std::make_unique();
+ info->identity = identity;
+ info->contacts = std::move(contactList);
+ info->contacts->load();
+ info->accountId = id;
+ info->deviceId = identity.first->getPublicKey().getId().toString();
+ info->announce = std::make_shared(std::move(announce_val));
+ info->ethAccount = root["eth"].asString();
+ info_ = std::move(info);
+
+ JAMI_DBG("[Auth] Device %s receipt checked successfully for account %s", info_->deviceId.c_str(), id.c_str());
+ return info_.get();
+}
+
+AccountArchive
+ArchiveAccountManager::readArchive(const std::string& pwd) const
+{
+ JAMI_DBG("[Auth] reading account archive");
+ return AccountArchive(fileutils::getFullPath(path_, archivePath_), pwd);
+}
+
+void
+ArchiveAccountManager::updateArchive(AccountArchive& archive) const
+{
+ using namespace DRing::Account::ConfProperties;
+
+ // Keys not exported to archive
+ static const auto filtered_keys = { Ringtone::PATH,
+ ARCHIVE_PATH,
+ RING_DEVICE_ID,
+ RING_DEVICE_NAME,
+ Conf::CONFIG_DHT_PORT };
+
+ // Keys with meaning of file path where the contents has to be exported in base64
+ static const auto encoded_keys = { TLS::CA_LIST_FILE,
+ TLS::CERTIFICATE_FILE,
+ TLS::PRIVATE_KEY_FILE };
+
+ JAMI_DBG("[Auth] building account archive");
+ for (const auto& it : onExportConfig_()) {
+ // filter-out?
+ if (std::any_of(std::begin(filtered_keys), std::end(filtered_keys),
+ [&](const auto& key){ return key == it.first; }))
+ continue;
+
+ // file contents?
+ if (std::any_of(std::begin(encoded_keys), std::end(encoded_keys),
+ [&](const auto& key){ return key == it.first; })) {
+ try {
+ archive.config.emplace(it.first, base64::encode(fileutils::loadFile(it.second)));
+ } catch (...) {}
+ } else
+ archive.config.insert(it);
+ }
+ archive.contacts = info_->contacts->getContacts();
+}
+
+void
+ArchiveAccountManager::saveArchive(AccountArchive& archive, const std::string& pwd)
+{
+ try {
+ updateArchive(archive);
+ if (archivePath_.empty())
+ archivePath_ = "export.gz";
+ archive.save(fileutils::getFullPath(path_, archivePath_), pwd);
+ //archiveHasPassword_ = not pwd.empty();
+ } catch (const std::runtime_error& ex) {
+ JAMI_ERR("[Auth] Can't export archive: %s", ex.what());
+ return;
+ }
+}
+
+bool
+ArchiveAccountManager::changePassword(const std::string& password_old, const std::string& password_new)
+{
+ try {
+ auto path = fileutils::getFullPath(path_, archivePath_);
+ AccountArchive(path, password_old).save(path, password_new);
+ return true;
+ } catch (const std::exception&) {
+ return false;
+ }
+}
+
+std::string
+generatePIN(size_t length = 8)
+{
+ static constexpr const char alphabet[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+ dht::crypto::random_device rd;
+ std::uniform_int_distribution dis(0, sizeof(alphabet)-2);
+ std::string ret;
+ ret.reserve(length);
+ for (size_t i=0; i(&accountManager);
+
+ std::vector key;
+ dht::InfoHash loc;
+ std::string pin_str;
+ AccountArchive a;
+ try {
+ JAMI_DBG("[Auth] exporting account");
+
+ a = this_.readArchive(password);
+
+ // Generate random PIN
+ pin_str = generatePIN();
+
+ std::tie(key, loc) = computeKeys(password, pin_str);
+ } catch (const std::exception& e) {
+ JAMI_ERR("[Auth] can't export account: %s", e.what());
+ cb(AddDeviceResult::ERROR_CREDENTIALS, {});
+ return;
+ }
+ // now that key and loc are computed, display to user in lowercase
+ std::transform(pin_str.begin(), pin_str.end(), pin_str.begin(), ::tolower);
+ try {
+ this_.updateArchive(a);
+ auto encrypted = dht::crypto::aesEncrypt(archiver::compress(a.serialize()), key);
+ if (not this_.dht_->isRunning())
+ throw std::runtime_error("DHT is not running..");
+ JAMI_WARN("[Auth] exporting account with PIN: %s at %s (size %zu)", pin_str.c_str(), loc.toString().c_str(), encrypted.size());
+ this_.dht_->put(loc, encrypted, [cb, pin = std::move(pin_str)](bool ok) {
+ JAMI_DBG("[Auth] account archive published: %s", ok ? "success" : "failure");
+ if (ok)
+ cb(AddDeviceResult::SUCCESS_SHOW_PIN, pin);
+ else
+ cb(AddDeviceResult::ERROR_NETWORK, {});
+ });
+ } catch (const std::exception& e) {
+ JAMI_ERR("[Auth] can't export account: %s", e.what());
+ cb(AddDeviceResult::ERROR_NETWORK, {});
+ return;
+ }
+ });
+ });
+}
+
+bool
+ArchiveAccountManager::revokeDevice(const std::string& password, const std::string& device, RevokeDeviceCallback cb)
+{
+ auto fa = dht::ThreadPool::computation().getShared([this, password] { return readArchive(password); });
+ findCertificate(dht::InfoHash(device),
+ [fa=std::move(fa),password,device,cb,onAsync=onAsync_](const std::shared_ptr& crt) mutable
+ {
+ if (not crt) {
+ cb(RevokeDeviceResult::ERROR_NETWORK);
+ return;
+ }
+ onAsync([cb, crt=std::move(crt), fa=std::move(fa), password=std::move(password)](AccountManager& accountManager) mutable {
+ auto& this_ = *static_cast(&accountManager);
+ this_.info_->contacts->foundAccountDevice(crt);
+ AccountArchive a;
+ try {
+ a = fa.get();
+ } catch (...) {
+ cb(RevokeDeviceResult::ERROR_CREDENTIALS);
+ return;
+ }
+ // Add revoked device to the revocation list and resign it
+ if (not a.revoked)
+ a.revoked = std::make_shared();
+ a.revoked->revoke(*crt);
+ a.revoked->sign(a.id);
+ // add to CRL cache
+ tls::CertificateStore::instance().pinRevocationList(a.id.second->getId().toString(), a.revoked);
+ tls::CertificateStore::instance().loadRevocations(*a.id.second);
+
+ this_.saveArchive(a, password);
+ this_.info_->contacts->removeAccountDevice(crt->getId());
+ cb(RevokeDeviceResult::SUCCESS);
+ this_.syncDevices();
+ });
+ });
+ return false;
+}
+
+bool
+ArchiveAccountManager::exportArchive(const std::string& destinationPath, const std::string& password)
+{
+ try {
+ // Save contacts if possible before exporting
+ AccountArchive archive = readArchive(password);
+ updateArchive(archive);
+ archive.save(fileutils::getFullPath(path_, archivePath_), password);
+
+ // Export the file
+ auto sourcePath = fileutils::getFullPath(path_, archivePath_);
+ std::ifstream src(sourcePath, std::ios::in | std::ios::binary);
+ if (!src) return false;
+ std::ofstream dst(destinationPath, std::ios::out | std::ios::binary);
+ dst << src.rdbuf();
+ return true;
+ } catch (const std::runtime_error& ex) {
+ JAMI_ERR("[Auth] Can't export archive: %s", ex.what());
+ return false;
+ } catch (...) {
+ JAMI_ERR("[Auth] Can't export archive: can't read archive");
+ return false;
+ }
+}
+
+const std::map&
+AccountManager::getKnownDevices() const {
+ return info_->contacts->getKnownDevices();
+}
+
+bool
+AccountManager::foundAccountDevice(const std::shared_ptr& crt, const std::string& name, const time_point& last_sync)
+{
+ return info_->contacts->foundAccountDevice(crt, name, last_sync);
+}
+
+void
+AccountManager::setAccountDeviceName(const std::string& name)
+{
+ if (info_)
+ info_->contacts->setAccountDeviceName(dht::InfoHash(info_->deviceId), name);
+}
+
+
+bool
+AccountManager::foundPeerDevice(const std::shared_ptr& crt, dht::InfoHash& account_id)
+{
+ if (not crt)
+ return false;
+
+ auto top_issuer = crt;
+ while (top_issuer->issuer)
+ top_issuer = top_issuer->issuer;
+
+ // Device certificate can't be self-signed
+ if (top_issuer == crt) {
+ JAMI_WARN("Found invalid peer device: %s", crt->getId().toString().c_str());
+ return false;
+ }
+
+ // Check peer certificate chain
+ // Trust store with top issuer as the only CA
+ dht::crypto::TrustList peer_trust;
+ peer_trust.add(*top_issuer);
+ if (not peer_trust.verify(*crt)) {
+ JAMI_WARN("Found invalid peer device: %s", crt->getId().toString().c_str());
+ return false;
+ }
+
+ account_id = crt->issuer->getId();
+ JAMI_WARN("Found peer device: %s account:%s CA:%s", crt->getId().toString().c_str(), account_id.toString().c_str(), top_issuer->getId().toString().c_str());
+ return true;
+}
+
+
+void
+AccountManager::addContact(const std::string& uri, bool confirmed)
+{
+ JAMI_WARN("AccountManager::addContact %d", confirmed);
+ dht::InfoHash h (uri);
+ if (not h) {
+ JAMI_ERR("addContact: invalid contact URI");
+ return;
+ }
+ if (info_->contacts->addContact(h, confirmed)) {
+ syncDevices();
+ }
+}
+
+void
+AccountManager::removeContact(const std::string& uri, bool banned)
+{
+ dht::InfoHash h (uri);
+ if (not h) {
+ JAMI_ERR("removeContact: invalid contact URI");
+ return;
+ }
+ if (info_->contacts->removeContact(h, banned)) {
+ syncDevices();
+ }
+}
+
+std::vector>
+AccountManager::getContacts() const
+{
+ if (not info_) {
+ JAMI_ERR("getContacts(): account not loaded");
+ return {};
+ }
+ const auto& contacts = info_->contacts->getContacts();
+ std::vector> ret;
+ ret.reserve(contacts.size());
+
+ for (const auto& c : contacts) {
+ auto details = c.second.toMap();
+ if (not details.empty()) {
+ details["id"] = c.first.toString();
+ ret.emplace_back(std::move(details));
+ }
+ }
+ return ret;
+}
+
+/** Obtain details about one account contact in serializable form. */
+std::map
+AccountManager::getContactDetails(const std::string& uri) const
+{
+ dht::InfoHash h (uri);
+ if (not h) {
+ JAMI_ERR("getContactDetails: invalid contact URI");
+ return {};
+ }
+ return info_->contacts->getContactDetails(h);
+}
+
+bool
+ArchiveAccountManager::findCertificate(const dht::InfoHash& h, std::function&)>&& cb)
+{
+ if (auto cert = tls::CertificateStore::instance().getCertificate(h.toString())) {
+ if (cb)
+ cb(cert);
+ } else {
+ dht_->findCertificate(h, [cb](const std::shared_ptr& crt) {
+ if (crt)
+ tls::CertificateStore::instance().pinCertificate(crt);
+ if (cb)
+ cb(crt);
+ });
+ }
+ return true;
+}
+
+bool
+AccountManager::setCertificateStatus(const std::string& cert_id, tls::TrustStore::PermissionStatus status)
+{
+ return info_->contacts->setCertificateStatus(cert_id, status);
+}
+
+std::vector
+AccountManager::getCertificatesByStatus(tls::TrustStore::PermissionStatus status)
+{
+ return info_->contacts->getCertificatesByStatus(status);
+}
+
+tls::TrustStore::PermissionStatus
+AccountManager::getCertificateStatus(const std::string& cert_id) const
+{
+ return info_->contacts->getCertificateStatus(cert_id);
+}
+
+bool
+AccountManager::isAllowed(const crypto::Certificate& crt, bool allowPublic)
+{
+ return info_->contacts->isAllowed(crt, allowPublic);
+}
+
+std::vector>
+AccountManager::getTrustRequests() const
+{
+ if (not info_) {
+ JAMI_ERR("getTrustRequests(): account not loaded");
+ return {};
+ }
+ return info_->contacts->getTrustRequests();
+}
+
+bool
+AccountManager::acceptTrustRequest(const std::string& from)
+{
+ dht::InfoHash f(from);
+ if (info_->contacts->acceptTrustRequest(f)) {
+ sendTrustRequestConfirm(f);
+ syncDevices();
+ return true;
+ }
+ return false;
+}
+
+bool
+AccountManager::discardTrustRequest(const std::string& from)
+{
+ dht::InfoHash f(from);
+ return info_->contacts->discardTrustRequest(f);
+}
+
+void
+AccountManager::sendTrustRequest(const std::string& to, const std::vector& payload)
+{
+ JAMI_WARN("AccountManager::sendTrustRequest");
+ auto toH = dht::InfoHash(to);
+ if (not toH) {
+ JAMI_ERR("can't send trust request to invalid hash: %s", to.c_str());
+ return;
+ }
+ if (info_->contacts->addContact(toH)) {
+ syncDevices();
+ }
+ forEachDevice(toH, [this,toH,payload](const dht::InfoHash& dev)
+ {
+ JAMI_WARN("sending trust request to: %s / %s", toH.toString().c_str(), dev.toString().c_str());
+ dht_->putEncrypted(dht::InfoHash::get("inbox:"+dev.toString()),
+ dev,
+ dht::TrustRequest(DHT_TYPE_NS, payload));
+ });
+}
+
+void
+AccountManager::sendTrustRequestConfirm(const dht::InfoHash& toH)
+{
+ JAMI_WARN("AccountManager::sendTrustRequestConfirm");
+ dht::TrustRequest answer {DHT_TYPE_NS};
+ answer.confirm = true;
+ forEachDevice(toH, [this,toH,answer](const dht::InfoHash& dev) {
+ JAMI_WARN("sending trust request reply: %s / %s", toH.toString().c_str(), dev.toString().c_str());
+ dht_->putEncrypted(dht::InfoHash::get("inbox:"+dev.toString()), dev, answer);
+ });
+}
+
+
+void
+AccountManager::forEachDevice(const dht::InfoHash& to,
+ std::function&& op,
+ std::function&& end)
+{
+ auto treatedDevices = std::make_shared>();
+ dht_->get(to, [to](dht::crypto::RevocationList&& crl){
+ tls::CertificateStore::instance().pinRevocationList(to.toString(), std::move(crl));
+ return true;
+ });
+ dht_->get(to, [this,to,treatedDevices,op=std::move(op)](DeviceAnnouncement&& dev) {
+ if (dev.from != to)
+ return true;
+ if (treatedDevices->emplace(dev.dev).second) {
+ op(dev.dev);
+ }
+ return true;
+ }, [=, end=std::move(end)](bool /*ok*/){
+ JAMI_DBG("Found %lu devices for %s", treatedDevices->size(), to.to_c_str());
+ if (end)
+ end(not treatedDevices->empty());
+ });
+}
+
+
+void
+ArchiveAccountManager::syncDevices()
+{
+ JAMI_DBG("Building device sync from %s", info_->deviceId.c_str());
+ auto sync_data = info_->contacts->getSyncData();
+
+ for (const auto& dev : getKnownDevices()) {
+ // don't send sync data to ourself
+ if (dev.first.toString() == info_->deviceId)
+ continue;
+ JAMI_DBG("sending device sync to %s %s", dev.second.name.c_str(), dev.first.toString().c_str());
+ auto syncDeviceKey = dht::InfoHash::get("inbox:"+dev.first.toString());
+ dht_->putEncrypted(syncDeviceKey, dev.first, sync_data);
+ }
+}
+
+
+void
+ArchiveAccountManager::startSync()
+{
+ // Put device annoucement
+ if (info_->announce) {
+ auto h = dht::InfoHash(info_->accountId);
+ JAMI_DBG("announcing device at %s", h.toString().c_str());
+ dht_->put(h, info_->announce, dht::DoneCallback{}, {}, true);
+ for (const auto& crl : info_->identity.second->issuer->getRevocationLists())
+ dht_->put(h, crl, dht::DoneCallback{}, {}, true);
+ dht_->listen(h, [this](DeviceAnnouncement&& dev) {
+ findCertificate(dev.dev, [this](const std::shared_ptr& crt) {
+ foundAccountDevice(crt);
+ });
+ return true;
+ });
+ dht_->listen(h, [this](dht::crypto::RevocationList&& crl) {
+ if (crl.isSignedBy(*info_->identity.second->issuer)) {
+ JAMI_DBG("found CRL for account.");
+ tls::CertificateStore::instance().pinRevocationList(
+ info_->accountId,
+ std::make_shared(std::move(crl)));
+ }
+ return true;
+ });
+ syncDevices();
+ } else {
+ JAMI_WARN("can't announce device: no annoucement...");
+ }
+
+ auto inboxKey = dht::InfoHash::get("inbox:"+info_->deviceId);
+ dht_->listen(
+ inboxKey,
+ [this](dht::TrustRequest&& v) {
+ if (v.service != DHT_TYPE_NS)
+ return true;
+
+ findCertificate(v.from, [this, v](const std::shared_ptr& cert) mutable {
+ // check peer certificate
+ dht::InfoHash peer_account;
+ if (not foundPeerDevice(cert, peer_account)) {
+ return;
+ }
+
+ JAMI_WARN("Got trust request from: %s / %s", peer_account.toString().c_str(), v.from.toString().c_str());
+ if (info_)
+ if (info_->contacts->onTrustRequest(peer_account, v.from, time(nullptr), v.confirm, std::move(v.payload))) {
+ sendTrustRequestConfirm(peer_account);
+ }
+ });
+ return true;
+ }
+ );
+
+ dht_->listen(
+ inboxKey,
+ [this](DeviceSync&& sync) {
+ // Received device sync data.
+ // check device certificate
+ findCertificate(sync.from, [this,sync](const std::shared_ptr& cert) mutable {
+ if (!cert or cert->getId() != sync.from) {
+ JAMI_WARN("Can't find certificate for device %s", sync.from.toString().c_str());
+ return;
+ }
+ if (not foundAccountDevice(cert))
+ return;
+ onSyncData(std::move(sync));
+ });
+
+ return true;
+ }
+ );
+}
+
+void
+ArchiveAccountManager::onSyncData(DeviceSync&& sync)
+{
+ auto sync_date = clock::time_point(clock::duration(sync.date));
+ if (not info_->contacts->syncDevice(sync.from, sync_date)) {
+ return;
+ }
+
+ // Sync known devices
+ JAMI_DBG("[Contacts] received device sync data (%lu devices, %lu contacts)", sync.devices_known.size(), sync.peers.size());
+ for (const auto& d : sync.devices_known) {
+ findCertificate(d.first, [this,d](const std::shared_ptr& crt) {
+ if (not crt)
+ return;
+ //std::lock_guard lock(deviceListMutex_);
+ foundAccountDevice(crt, d.second);
+ });
+ }
+ //saveKnownDevices();
+
+ // Sync contacts
+ for (const auto& peer : sync.peers)
+ info_->contacts->updateContact(peer.first, peer.second);
+ //saveContacts();
+
+ // Sync trust requests
+ for (const auto& tr : sync.trust_requests)
+ info_->contacts->onTrustRequest(tr.first, tr.second.device, tr.second.received, false, {});
+}
+
+
+#if HAVE_RINGNS
+void
+ArchiveAccountManager::lookupName(const std::string& name, LookupCallback cb)
+{
+ nameDir_.get().lookupName(name, cb);
+}
+
+void
+ArchiveAccountManager::lookupAddress(const std::string& addr, LookupCallback cb)
+{
+ nameDir_.get().lookupAddress(addr, cb);
+}
+
+void
+ArchiveAccountManager::registerName(const std::string& password, const std::string& name, RegistrationCallback cb)
+{
+ std::string signedName;
+ auto nameLowercase {name};
+ std::transform(nameLowercase.begin(), nameLowercase.end(), nameLowercase.begin(), ::tolower);
+ std::string publickey;
+ std::string accountId;
+ std::string ethAccount;
+
+ try {
+ auto archive = readArchive(password);
+ auto privateKey = archive.id.first;
+ auto pk = privateKey->getPublicKey();
+ publickey = pk.toString();
+ accountId = pk.getId().toString();
+ signedName = base64::encode(privateKey->sign(std::vector(nameLowercase.begin(), nameLowercase.end())));
+ ethAccount = dev::KeyPair(dev::Secret(archive.eth_key)).address().hex();
+ } catch (const std::exception& e) {
+ //JAMI_ERR("[Auth] can't export account: %s", e.what());
+ cb(NameDirectory::RegistrationResponse::invalidCredentials);
+ return;
+ }
+
+ nameDir_.get().registerName(accountId, nameLowercase, ethAccount, cb, signedName, publickey);
+}
+#endif
+
+}
diff --git a/src/jamidht/account_manager.h b/src/jamidht/account_manager.h
new file mode 100644
index 000000000..c15d538b6
--- /dev/null
+++ b/src/jamidht/account_manager.h
@@ -0,0 +1,283 @@
+/*
+ * Copyright (C) 2014-2019 Savoir-faire Linux Inc.
+ * Author : Adrien Béraud
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+#pragma once
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "contact_list.h"
+#include "logger.h"
+#if HAVE_RINGNS
+#include "namedirectory.h"
+#endif
+
+#include
+
+#include
+#include
+#include