Select Git revision
account_manager.cpp
-
Sébastien Blin authored
This fix ut_compability and a regression from 95280b19 Change-Id: I4a41e3c383fd8c8d31e844d2cd58fa82ef04fd00 GitLab: #579
Sébastien Blin authoredThis fix ut_compability and a regression from 95280b19 Change-Id: I4a41e3c383fd8c8d31e844d2cd58fa82ef04fd00 GitLab: #579
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
account_manager.cpp 26.18 KiB
/*
* Copyright (C) 2004-2021 Savoir-faire Linux Inc.
* Author : Adrien Béraud <adrien.beraud@savoirfairelinux.com>
*
* 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 <https://www.gnu.org/licenses/>.
*/
#include "account_manager.h"
#include "accountarchive.h"
#include "jamiaccount.h"
#include "base64.h"
#include "jami/account_const.h"
#include "account_schema.h"
#include "archiver.h"
#include "libdevcrypto/Common.h"
#include <opendht/thread_pool.h>
#include <opendht/crypto.h>
#include <exception>
#include <future>
#include <fstream>
#include <gnutls/ocsp.h>
namespace jami {
AccountManager::CertRequest
AccountManager::buildRequest(PrivateKey fDeviceKey)
{
return dht::ThreadPool::computation().get<std::unique_ptr<dht::crypto::CertificateRequest>>(
[fDeviceKey] {
auto request = std::make_unique<dht::crypto::CertificateRequest>();
request->setName("Jami device");
auto deviceKey = fDeviceKey.get();
request->setUID(deviceKey->getPublicKey().getId().toString());
request->sign(*deviceKey);
return request;
});
}
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<dht::crypto::PrivateKey>(std::move(dht_key)),
std::make_shared<dht::crypto::Certificate>(std::move(dht_cert))};
} catch (const std::exception& e) {
JAMI_ERR("Error loading identity: %s", e.what());
}
return {};
}
std::shared_ptr<dht::Value>
AccountManager::parseAnnounce(const std::string& announceBase64,
const std::string& accountId,
const std::string& deviceId)
{
auto announce_val = std::make_shared<dht::Value>();
try {
auto announce = base64::decode(announceBase64);
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 {};
}
DeviceAnnouncement da;
da.unpackValue(*announce_val);
if (da.from.toString() != accountId or da.dev.toString() != deviceId) {
JAMI_ERR("[Auth] device ID mismatch in announce");
return {};
}
} catch (const std::exception& e) {
JAMI_ERR("[Auth] can't read announce: %s", e.what());
return {};
}
return announce_val;
}
const AccountInfo*
AccountManager::useIdentity(const dht::crypto::Identity& identity,
const std::string& receipt,
const std::vector<uint8_t>& receiptSignature,
const std::string& username,
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<ContactList>(accountCertificate, path_, onChange);
auto result = contactList->isValidAccountDevice(*identity.second);
if (not result) {
JAMI_ERR("[Auth] can't use identity: device certificate chain can't be verified: %s",
result.toString().c_str());
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<Json::CharReader>(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;
}
auto announce = parseAnnounce(root["announce"].asString(), id, dev_id);
if (not announce) {
return nullptr;
}
onChange_ = std::move(onChange);
auto info = std::make_unique<AccountInfo>();
info->identity = identity;
info->contacts = std::move(contactList);
info->contacts->load();
info->accountId = id;
info->devicePk = identity.first->getSharedPublicKey();
info->deviceId = info->devicePk->getLongId().toString();
info->announce = std::move(announce);
info->ethAccount = root["eth"].asString();
info->username = username;
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();
}
void
AccountManager::startSync(const OnNewDeviceCb& cb, const OnDeviceAnnouncedCb& dcb)
{
// Put device announcement
if (info_->announce) {
auto h = dht::InfoHash(info_->accountId);
JAMI_DBG("announcing device at %s", h.toString().c_str());
dht_->put(
h,
info_->announce,
[dcb = std::move(dcb), h](bool ok) {
if (ok)
JAMI_DBG("device announced at %s", h.toString().c_str());
// We do not care about the status, it's a permanent put, if this fail,
// this means the DHT is disconnected but the put will be retried when connected.
if (dcb)
dcb();
},
{},
true);
for (const auto& crl : info_->identity.second->issuer->getRevocationLists())
dht_->put(h, crl, dht::DoneCallback {}, {}, true);
dht_->listen<DeviceAnnouncement>(h, [this, cb = std::move(cb)](DeviceAnnouncement&& dev) {
// dev.from
findCertificate(dev.dev,
[this, cb](const std::shared_ptr<dht::crypto::Certificate>& crt) {
foundAccountDevice(crt);
if (cb)
cb(crt);
});
return true;
});
dht_->listen<dht::crypto::RevocationList>(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<dht::crypto::RevocationList>(
std::move(crl)));
}
return true;
});
syncDevices();
} else {
JAMI_WARN("can't announce device: no announcement...");
}
auto inboxKey = dht::InfoHash::get("inbox:" + info_->devicePk->getId().toString());
dht_->listen<dht::TrustRequest>(inboxKey, [this](dht::TrustRequest&& v) {
if (v.service != DHT_TYPE_NS)
return true;
// allowPublic always true for trust requests (only forbidden if banned)
onPeerMessage(*v.owner,
true,
[this, v](const std::shared_ptr<dht::crypto::Certificate>&,
dht::InfoHash peer_account) mutable {
JAMI_WARN("Got trust request from: %s / %s. ConversationId: %s",
peer_account.toString().c_str(),
v.from.toString().c_str(),
v.conversationId.c_str());
if (info_)
if (info_->contacts->onTrustRequest(peer_account,
v.owner,
time(nullptr),
v.confirm,
v.conversationId,
std::move(v.payload))) {
sendTrustRequestConfirm(peer_account, v.conversationId);
info_->contacts->saveTrustRequests();
}
});
return true;
});
}
const std::map<dht::PkId, KnownDevice>&
AccountManager::getKnownDevices() const
{
return info_->contacts->getKnownDevices();
}
bool
AccountManager::foundAccountDevice(const std::shared_ptr<dht::crypto::Certificate>& 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(DeviceId(info_->deviceId), name);
}
std::string
AccountManager::getAccountDeviceName() const
{
if (info_)
return info_->contacts->getAccountDeviceName(DeviceId(info_->deviceId));
return {};
}
bool
AccountManager::foundPeerDevice(const std::shared_ptr<dht::crypto::Certificate>& 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;
}
// Check cached OCSP response
if (crt->ocspResponse and crt->ocspResponse->getCertificateStatus() != GNUTLS_OCSP_CERT_GOOD) {
JAMI_ERR("Certificate %s is disabled by cached OCSP response", crt->getId().to_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::onPeerMessage(const dht::crypto::PublicKey& peer_device,
bool allowPublic,
std::function<void(const std::shared_ptr<dht::crypto::Certificate>& crt,
const dht::InfoHash& peer_account)>&& cb)
{
// quick check in case we already explicilty banned this device
auto trustStatus = getCertificateStatus(peer_device.toString());
if (trustStatus == tls::TrustStore::PermissionStatus::BANNED) {
JAMI_WARN("[Auth] Discarding message from banned device %s", peer_device.toString().c_str());
return;
}
findCertificate(peer_device.getId(),
[this, cb = std::move(cb), allowPublic](
const std::shared_ptr<dht::crypto::Certificate>& cert) {
dht::InfoHash peer_account_id;
if (onPeerCertificate(cert, allowPublic, peer_account_id)) {
cb(cert, peer_account_id);
}
});
}
bool
AccountManager::onPeerCertificate(const std::shared_ptr<dht::crypto::Certificate>& cert,
bool allowPublic,
dht::InfoHash& account_id)
{
dht::InfoHash peer_account_id;
if (not foundPeerDevice(cert, peer_account_id)) {
JAMI_WARN("[Auth] Discarding message from invalid peer certificate");
return false;
}
if (not isAllowed(*cert, allowPublic)) {
JAMI_WARN("[Auth] Discarding message from unauthorized peer %s.",
peer_account_id.toString().c_str());
return false;
}
account_id = peer_account_id;
return true;
}
bool
AccountManager::addContact(const std::string& uri, bool confirmed, const std::string& conversationId)
{
JAMI_WARN("AccountManager::addContact %d", confirmed);
dht::InfoHash h(uri);
if (not h) {
JAMI_ERR("addContact: invalid contact URI");
return false;
}
if (not info_) {
JAMI_ERR("addContact(): account not loaded");
return false;
}
if (info_->contacts->addContact(h, confirmed, conversationId)) {
syncDevices();
return true;
}
return false;
}
void
AccountManager::removeContact(const std::string& uri, bool banned)
{
dht::InfoHash h(uri);
if (not h) {
JAMI_ERR("removeContact: invalid contact URI");
return;
}
if (not info_) {
JAMI_ERR("addContact(): account not loaded");
return;
}
if (info_->contacts->removeContact(h, banned)) {
syncDevices();
}
}
void
AccountManager::removeContactConversation(const std::string& uri)
{
dht::InfoHash h(uri);
if (not h) {
JAMI_ERR("removeContact: invalid contact URI");
return;
}
if (not info_) {
JAMI_ERR("addContact(): account not loaded");
return;
}
if (info_->contacts->removeContactConversation(h)) {
syncDevices();
}
}
std::vector<std::map<std::string, std::string>>
AccountManager::getContacts() const
{
if (not info_) {
JAMI_ERR("getContacts(): account not loaded");
return {};
}
const auto& contacts = info_->contacts->getContacts();
std::vector<std::map<std::string, std::string>> 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;
}
void
AccountManager::setConversations(const std::map<std::string, ConvInfo>& newConv)
{
if (info_) {
info_->conversations = newConv;
saveConvInfos();
}
}
void
AccountManager::setConversationMembers(const std::string& convId,
const std::vector<std::string>& members)
{
if (info_) {
auto convIt = info_->conversations.find(convId);
if (convIt != info_->conversations.end()) {
convIt->second.members = members;
saveConvInfos();
}
}
}
void
AccountManager::saveConvInfos() const
{
if (!info_)
return;
std::ofstream file(info_->contacts->path() + DIR_SEPARATOR_STR "convInfo",
std::ios::trunc | std::ios::binary);
msgpack::pack(file, info_->conversations);
}
void
AccountManager::addConversation(const ConvInfo& info)
{
if (info_) {
info_->conversations[info.id] = info;
saveConvInfos();
}
}
void
AccountManager::setConversationsRequests(const std::map<std::string, ConversationRequest>& newConvReq)
{
if (info_) {
std::lock_guard<std::mutex> lk(conversationsRequestsMtx);
info_->conversationsRequests = newConvReq;
saveConvRequests();
}
}
void
AccountManager::saveConvRequests() const
{
if (!info_)
return;
std::ofstream file(info_->contacts->path() + DIR_SEPARATOR_STR "convRequests",
std::ios::trunc | std::ios::binary);
msgpack::pack(file, info_->conversationsRequests);
}
std::optional<ConversationRequest>
AccountManager::getRequest(const std::string& id) const
{
if (info_) {
std::lock_guard<std::mutex> lk(conversationsRequestsMtx);
auto it = info_->conversationsRequests.find(id);
if (it != info_->conversationsRequests.end())
return it->second;
}
return std::nullopt;
}
void
AccountManager::addConversationRequest(const std::string& id, const ConversationRequest& req)
{
if (info_) {
std::lock_guard<std::mutex> lk(conversationsRequestsMtx);
info_->conversationsRequests[id] = req;
saveConvRequests();
}
}
void
AccountManager::rmConversationRequest(const std::string& id)
{
if (info_) {
std::lock_guard<std::mutex> lk(conversationsRequestsMtx);
info_->conversationsRequests.erase(id);
saveConvRequests();
}
}
/** Obtain details about one account contact in serializable form. */
std::map<std::string, std::string>
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
AccountManager::findCertificate(
const dht::InfoHash& h,
std::function<void(const std::shared_ptr<dht::crypto::Certificate>&)>&& cb)
{
if (auto cert = tls::CertificateStore::instance().getCertificate(h.toString())) {
if (cb)
cb(cert);
} else {
dht_->findCertificate(h, [cb](const std::shared_ptr<dht::crypto::Certificate>& crt) {
if (crt)
tls::CertificateStore::instance().pinCertificate(crt);
if (cb)
cb(crt);
});
}
return true;
}
bool
AccountManager::findCertificate(
const dht::PkId& id, std::function<void(const std::shared_ptr<dht::crypto::Certificate>&)>&& cb)
{
if (auto cert = tls::CertificateStore::instance().getCertificate(id.toString())) {
if (cb)
cb(cert);
} else {
/*dht_->findCertificate(id, [cb](const std::shared_ptr<dht::crypto::Certificate>& crt) {
if (crt)
tls::CertificateStore::instance().pinCertificate(crt);
if (cb)
cb(crt);
});*/
if (cb)
cb(nullptr);
}
return true;
}
bool
AccountManager::setCertificateStatus(const std::string& cert_id,
tls::TrustStore::PermissionStatus status)
{
return info_ and info_->contacts->setCertificateStatus(cert_id, status);
}
std::vector<std::string>
AccountManager::getCertificatesByStatus(tls::TrustStore::PermissionStatus status)
{
return info_ ? info_->contacts->getCertificatesByStatus(status) : std::vector<std::string> {};
}
tls::TrustStore::PermissionStatus
AccountManager::getCertificateStatus(const std::string& cert_id) const
{
return info_ ? info_->contacts->getCertificateStatus(cert_id)
: tls::TrustStore::PermissionStatus::UNDEFINED;
}
bool
AccountManager::isAllowed(const crypto::Certificate& crt, bool allowPublic)
{
return info_ and info_->contacts->isAllowed(crt, allowPublic);
}
std::vector<std::map<std::string, std::string>>
AccountManager::getTrustRequests() const
{
if (not info_) {
JAMI_ERR("getTrustRequests(): account not loaded");
return {};
}
return info_->contacts->getTrustRequests();
}
bool
AccountManager::acceptTrustRequest(const std::string& from, bool includeConversation)
{
dht::InfoHash f(from);
if (info_) {
auto req = info_->contacts->getTrustRequest(dht::InfoHash(from));
if (info_->contacts->acceptTrustRequest(f)) {
sendTrustRequestConfirm(f,
includeConversation
? req[DRing::Account::TrustRequest::CONVERSATIONID]
: "");
syncDevices();
return true;
}
return false;
}
return false;
}
bool
AccountManager::discardTrustRequest(const std::string& from)
{
dht::InfoHash f(from);
return info_ and info_->contacts->discardTrustRequest(f);
}
void
AccountManager::sendTrustRequest(const std::string& to,
const std::string& convId,
const std::vector<uint8_t>& 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 (not info_) {
JAMI_ERR("sendTrustRequest(): account not loaded");
return;
}
if (info_->contacts->addContact(toH, false, convId)) {
syncDevices();
}
forEachDevice(toH,
[this, toH, convId, payload](const std::shared_ptr<dht::crypto::PublicKey>& dev) {
JAMI_WARN("sending trust request to: %s / %s",
toH.toString().c_str(),
dev->getLongId().toString().c_str());
dht_->putEncrypted(dht::InfoHash::get("inbox:" + dev->getId().toString()),
dev,
dht::TrustRequest(DHT_TYPE_NS, convId, payload));
});
}
void
AccountManager::sendTrustRequestConfirm(const dht::InfoHash& toH, const std::string& convId)
{
JAMI_WARN("AccountManager::sendTrustRequestConfirm");
dht::TrustRequest answer {DHT_TYPE_NS, ""};
answer.confirm = true;
answer.conversationId = convId;
if (!convId.empty() && info_)
info_->contacts->acceptConversation(convId);
forEachDevice(toH, [this, toH, answer](const std::shared_ptr<dht::crypto::PublicKey>& dev) {
JAMI_WARN("sending trust request reply: %s / %s",
toH.toString().c_str(),
dev->getLongId().toString().c_str());
dht_->putEncrypted(dht::InfoHash::get("inbox:" + dev->getId().toString()),
dev,
answer);
});
}
void
AccountManager::forEachDevice(
const dht::InfoHash& to,
std::function<void(const std::shared_ptr<dht::crypto::PublicKey>&)>&& op,
std::function<void(bool)>&& end)
{
if (not dht_) {
JAMI_ERR("forEachDevice: no dht");
if (end)
end(false);
return;
}
dht_->get<dht::crypto::RevocationList>(to, [to](dht::crypto::RevocationList&& crl) {
tls::CertificateStore::instance().pinRevocationList(to.toString(), std::move(crl));
return true;
});
struct State
{
unsigned remaining {0};
std::set<dht::PkId> treatedDevices {};
std::function<void(const std::shared_ptr<dht::crypto::PublicKey>&)> onDevice;
std::function<void(bool)> onEnd;
void found(std::shared_ptr<dht::crypto::PublicKey> pk)
{
remaining--;
if (pk && *pk) {
auto longId = pk->getLongId();
if (treatedDevices.emplace(longId).second) {
onDevice(pk);
}
}
ended();
}
void ended()
{
if (remaining == 0 && onEnd) {
JAMI_DBG("Found %lu devices", treatedDevices.size());
onEnd(not treatedDevices.empty());
onDevice = {};
onEnd = {};
}
}
};
auto state = std::make_shared<State>();
state->onDevice = std::move(op);
state->onEnd = std::move(end);
dht_->get<DeviceAnnouncement>(
to,
[this, to, state](DeviceAnnouncement&& dev) {
if (dev.from != to)
return true;
if (dev.pk) {
state->found(std::move(dev.pk));
} else {
state->remaining++;
findCertificate(dev.dev,
[state](const std::shared_ptr<dht::crypto::Certificate>& cert) {
state->found(cert ? std::make_shared<dht::crypto::PublicKey>(
cert->getPublicKey())
: std::shared_ptr<dht::crypto::PublicKey> {});
});
}
return true;
},
[state](bool /*ok*/) { state->ended(); });
}
void
AccountManager::lookupUri(const std::string& name,
const std::string& defaultServer,
LookupCallback cb)
{
nameDir_.get().lookupUri(name, defaultServer, std::move(cb));
}
void
AccountManager::lookupAddress(const std::string& addr, LookupCallback cb)
{
nameDir_.get().lookupAddress(addr, cb);
}
} // namespace jami