Select Git revision
configure.ac
-
Adrien Béraud authoredAdrien Béraud authored
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
contact_list.cpp 20.23 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 "contact_list.h"
#include "logger.h"
#include "jamiaccount.h"
#include "fileutils.h"
#ifdef ENABLE_PLUGIN
#include "manager.h"
#include "plugin/jamipluginmanager.h"
#endif
#include "account_const.h"
#include <fstream>
#include <gnutls/ocsp.h>
namespace jami {
ContactList::ContactList(const std::shared_ptr<crypto::Certificate>& cert,
const std::string& path,
OnChangeCallback cb)
: path_(path)
, callbacks_(std::move(cb))
{
if (cert)
accountTrust_.add(*cert);
}
ContactList::~ContactList() {}
void
ContactList::load()
{
loadContacts();
loadTrustRequests();
loadKnownDevices();
}
void
ContactList::save()
{
saveContacts();
saveTrustRequests();
saveKnownDevices();
}
bool
ContactList::setCertificateStatus(const std::string& cert_id,
const tls::TrustStore::PermissionStatus status)
{
if (contacts_.find(dht::InfoHash(cert_id)) != contacts_.end()) {
JAMI_DBG("Can't set certificate status for existing contacts %s", cert_id.c_str());
return false;
}
return trust_.setCertificateStatus(cert_id, status);
}
bool
ContactList::addContact(const dht::InfoHash& h, bool confirmed, const std::string& conversationId)
{
JAMI_WARN("[Contacts] addContact: %s, conversation: %s", h.to_c_str(), conversationId.c_str());
auto c = contacts_.find(h);
if (c == contacts_.end())
c = contacts_.emplace(h, Contact {}).first;
else if (c->second.isActive() and c->second.confirmed == confirmed)
return false;
c->second.added = std::time(nullptr);
// NOTE: because we can re-add a contact after removing it
// we should reset removed (as not removed anymore). This fix isActive()
// if addContact is called just after removeContact during the same second
c->second.removed = 0;
c->second.conversationId = conversationId;
c->second.confirmed |= confirmed;
auto hStr = h.toString();
trust_.setCertificateStatus(hStr, tls::TrustStore::PermissionStatus::ALLOWED);
saveContacts();
callbacks_.contactAdded(hStr, c->second.confirmed);
return true;
}
void
ContactList::updateConversation(const dht::InfoHash& h, const std::string& conversationId)
{
auto c = contacts_.find(h);
if (c != contacts_.end()) {
c->second.conversationId = conversationId;
saveContacts();
}
}
bool
ContactList::removeContact(const dht::InfoHash& h, bool ban)
{
JAMI_WARN("[Contacts] removeContact: %s", h.to_c_str());
auto c = contacts_.find(h);
if (c == contacts_.end())
c = contacts_.emplace(h, Contact {}).first;
else if (not c->second.isActive() and c->second.banned == ban)
return false;
c->second.removed = std::time(nullptr);
c->second.banned = ban;
auto uri = h.toString();
trust_.setCertificateStatus(uri,
ban ? tls::TrustStore::PermissionStatus::BANNED
: tls::TrustStore::PermissionStatus::UNDEFINED);
if (ban and trustRequests_.erase(h) > 0)
saveTrustRequests();
saveContacts();
#ifdef ENABLE_PLUGIN
std::size_t found = path_.find_last_of(DIR_SEPARATOR_CH);
if (found != std::string::npos) {
auto filename = path_.substr(found + 1);
jami::Manager::instance()
.getJamiPluginManager()
.getChatServicesManager()
.cleanChatSubjects(filename, uri);
}
#endif
callbacks_.contactRemoved(uri, ban);
return true;
}
bool
ContactList::removeContactConversation(const dht::InfoHash& h)
{
auto c = contacts_.find(h);
if (c == contacts_.end())
return false;
c->second.conversationId = "";
saveContacts();
return true;
}
std::map<std::string, std::string>
ContactList::getContactDetails(const dht::InfoHash& h) const
{
const auto c = contacts_.find(h);
if (c == std::end(contacts_)) {
JAMI_WARN("[Contacts] contact '%s' not found", h.to_c_str());
return {};
}
auto details = c->second.toMap();
if (not details.empty())
details["id"] = c->first.toString();
return details;
}
const std::map<dht::InfoHash, Contact>&
ContactList::getContacts() const
{
return contacts_;
}
void
ContactList::setContacts(const std::map<dht::InfoHash, Contact>& contacts)
{
contacts_ = contacts;
saveContacts();
// Set contacts is used when creating a new device, so just announce new contacts
for (auto& peer : contacts)
if (peer.second.isActive())
callbacks_.contactAdded(peer.first.toString(), peer.second.confirmed);
}
void
ContactList::updateContact(const dht::InfoHash& id, const Contact& contact)
{
if (not id) {
JAMI_ERR("[Contacts] updateContact: invalid contact ID");
return;
}
bool stateChanged {false};
auto c = contacts_.find(id);
if (c == contacts_.end()) {
// JAMI_DBG("[Contacts] new contact: %s", id.toString().c_str());
c = contacts_.emplace(id, contact).first;
stateChanged = c->second.isActive() or c->second.isBanned();
} else {
JAMI_DBG("[Contacts] updated contact: %s", id.toString().c_str());
stateChanged = c->second.update(contact);
}
if (stateChanged) {
if (trustRequests_.erase(id) > 0)
saveTrustRequests();
if (c->second.isActive()) {
trust_.setCertificateStatus(id.toString(), tls::TrustStore::PermissionStatus::ALLOWED);
callbacks_.contactAdded(id.toString(), c->second.confirmed);
} else {
if (c->second.banned)
trust_.setCertificateStatus(id.toString(),
tls::TrustStore::PermissionStatus::BANNED);
callbacks_.contactRemoved(id.toString(), c->second.banned);
}
}
}
void
ContactList::loadContacts()
{
decltype(contacts_) contacts;
try {
// read file
auto file = fileutils::loadFile("contacts", path_);
// load values
msgpack::object_handle oh = msgpack::unpack((const char*) file.data(), file.size());
oh.get().convert(contacts);
} catch (const std::exception& e) {
JAMI_WARN("[Contacts] error loading contacts: %s", e.what());
return;
}
for (auto& peer : contacts)
updateContact(peer.first, peer.second);
}
void
ContactList::saveContacts() const
{
std::ofstream file(path_ + DIR_SEPARATOR_STR "contacts", std::ios::trunc | std::ios::binary);
msgpack::pack(file, contacts_);
}
void
ContactList::saveTrustRequests() const
{
std::ofstream file(path_ + DIR_SEPARATOR_STR "incomingTrustRequests",
std::ios::trunc | std::ios::binary);
msgpack::pack(file, trustRequests_);
}
void
ContactList::loadTrustRequests()
{
if (!fileutils::isFile(fileutils::getFullPath(path_, "incomingTrustRequests")))
return;
std::map<dht::InfoHash, TrustRequest> requests;
try {
// read file
auto file = fileutils::loadFile("incomingTrustRequests", path_);
// load values
msgpack::object_handle oh = msgpack::unpack((const char*) file.data(), file.size());
oh.get().convert(requests);
} catch (const std::exception& e) {
JAMI_WARN("[Contacts] error loading trust requests: %s", e.what());
return;
}
for (auto& tr : requests)
onTrustRequest(tr.first,
tr.second.device,
tr.second.received,
false,
tr.second.conversationId,
std::move(tr.second.payload));
}
bool
ContactList::onTrustRequest(const dht::InfoHash& peer_account,
const std::shared_ptr<dht::crypto::PublicKey>& peer_device,
time_t received,
bool confirm,
const std::string& conversationId,
std::vector<uint8_t>&& payload)
{
bool accept = false;
// Check existing contact
auto contact = contacts_.find(peer_account);
bool active = false;
if (contact != contacts_.end()) {
// Banned contact: discard request
if (contact->second.isBanned())
return false;
if (contact->second.isActive()) {
active = true;
// Send confirmation
if (not confirm)
accept = true;
if (not contact->second.confirmed) {
contact->second.confirmed = true;
callbacks_.contactAdded(peer_account.toString(), true);
}
}
}
if (not active) {
auto req = trustRequests_.find(peer_account);
if (req == trustRequests_.end()) {
// Add trust request
req = trustRequests_
.emplace(peer_account,
TrustRequest {peer_device, conversationId, received, payload})
.first;
} else {
// Update trust request
if (received < req->second.received) {
req->second.device = peer_device;
req->second.received = received;
req->second.payload = payload;
} else {
JAMI_DBG("[Contacts] Ignoring outdated trust request from %s",
peer_account.toString().c_str());
}
}
saveTrustRequests();
}
// Note: call JamiAccount's callback to build ConversationRequest anyway
if (!confirm)
callbacks_.trustRequest(peer_account.toString(),
conversationId,
std::move(payload),
received);
else
callbacks_.onConfirmation(peer_account.toString(), conversationId);
return accept;
}
/* trust requests */
std::vector<std::map<std::string, std::string>>
ContactList::getTrustRequests() const
{
using Map = std::map<std::string, std::string>;
std::vector<Map> ret;
ret.reserve(trustRequests_.size());
for (const auto& r : trustRequests_) {
ret.emplace_back(
Map {{DRing::Account::TrustRequest::FROM, r.first.toString()},
{DRing::Account::TrustRequest::RECEIVED, std::to_string(r.second.received)},
{DRing::Account::TrustRequest::CONVERSATIONID, r.second.conversationId},
{DRing::Account::TrustRequest::PAYLOAD,
std::string(r.second.payload.begin(), r.second.payload.end())}});
}
return ret;
}
std::map<std::string, std::string>
ContactList::getTrustRequest(const dht::InfoHash& from) const
{
using Map = std::map<std::string, std::string>;
auto r = trustRequests_.find(from);
if (r == trustRequests_.end())
return {};
return Map {{DRing::Account::TrustRequest::FROM, r->first.toString()},
{DRing::Account::TrustRequest::RECEIVED, std::to_string(r->second.received)},
{DRing::Account::TrustRequest::CONVERSATIONID, r->second.conversationId},
{DRing::Account::TrustRequest::PAYLOAD,
std::string(r->second.payload.begin(), r->second.payload.end())}};
}
bool
ContactList::acceptTrustRequest(const dht::InfoHash& from)
{
// The contact sent us a TR so we are in its contact list
auto i = trustRequests_.find(from);
if (i == trustRequests_.end())
return false;
addContact(from, true, i->second.conversationId);
// Clear trust request
trustRequests_.erase(i);
saveTrustRequests();
return true;
}
void
ContactList::acceptConversation(const std::string& convId)
{
if (callbacks_.acceptConversation)
callbacks_.acceptConversation(convId);
}
bool
ContactList::discardTrustRequest(const dht::InfoHash& from)
{
if (trustRequests_.erase(from) > 0) {
saveTrustRequests();
return true;
}
return false;
}
void
ContactList::loadKnownDevices()
{
try {
// read file
auto file = fileutils::loadFile("knownDevices", path_);
// load values
msgpack::object_handle oh = msgpack::unpack((const char*) file.data(), file.size());
std::map<dht::PkId, std::pair<std::string, uint64_t>> knownDevices;
oh.get().convert(knownDevices);
for (const auto& d : knownDevices) {
/*JAMI_DBG("[Contacts] loading known account device %s %s",
d.second.first.c_str(),
d.first.toString().c_str());*/
if (auto crt = tls::CertificateStore::instance().getCertificate(d.first.toString())) {
if (not foundAccountDevice(crt, d.second.first, clock::from_time_t(d.second.second)))
JAMI_WARN("[Contacts] can't add device %s", d.first.toString().c_str());
} else {
JAMI_WARN("[Contacts] can't find certificate for device %s",
d.first.toString().c_str());
}
}
} catch (const std::exception& e) {
// Legacy fallback
try {
auto file = fileutils::loadFile("knownDevicesNames", path_);
msgpack::object_handle oh = msgpack::unpack((const char*) file.data(), file.size());
std::map<dht::InfoHash, std::pair<std::string, uint64_t>> knownDevices;
oh.get().convert(knownDevices);
for (const auto& d : knownDevices) {
if (auto crt = tls::CertificateStore::instance().getCertificate(
d.first.toString())) {
if (not foundAccountDevice(crt,
d.second.first,
clock::from_time_t(d.second.second)))
JAMI_WARN("[Contacts] can't add device %s", d.first.toString().c_str());
}
}
} catch (const std::exception& e) {
JAMI_WARN("[Contacts] error loading devices: %s", e.what());
}
return;
}
}
void
ContactList::saveKnownDevices() const
{
std::ofstream file(path_ + DIR_SEPARATOR_STR "knownDevices", std::ios::trunc | std::ios::binary);
std::map<dht::PkId, std::pair<std::string, uint64_t>> devices;
for (const auto& id : knownDevices_)
devices.emplace(id.first,
std::make_pair(id.second.name, clock::to_time_t(id.second.last_sync)));
msgpack::pack(file, devices);
}
void
ContactList::foundAccountDevice(const dht::PkId& device,
const std::string& name,
const time_point& updated)
{
// insert device
auto it = knownDevices_.emplace(device, KnownDevice {{}, name, updated});
if (it.second) {
JAMI_DBG("[Contacts] Found account device: %s %s", name.c_str(), device.toString().c_str());
saveKnownDevices();
callbacks_.devicesChanged(knownDevices_);
} else {
// update device name
if (not name.empty() and it.first->second.name != name) {
JAMI_DBG("[Contacts] updating device name: %s %s",
name.c_str(),
device.toString().c_str());
it.first->second.name = name;
saveKnownDevices();
callbacks_.devicesChanged(knownDevices_);
}
}
}
bool
ContactList::foundAccountDevice(const std::shared_ptr<dht::crypto::Certificate>& crt,
const std::string& name,
const time_point& updated)
{
if (not crt)
return false;
auto id = crt->getLongId();
// match certificate chain
auto verifyResult = accountTrust_.verify(*crt);
if (not verifyResult) {
JAMI_WARN("[Contacts] Found invalid account device: %s: %s",
id.toString().c_str(),
verifyResult.toString().c_str());
return false;
}
// insert device
auto it = knownDevices_.emplace(id, KnownDevice {crt, name, updated});
if (it.second) {
JAMI_DBG("[Contacts] Found account device: %s %s", name.c_str(), id.toString().c_str());
tls::CertificateStore::instance().pinCertificate(crt);
if (crt->ocspResponse) {
unsigned int status = crt->ocspResponse->getCertificateStatus();
if (status == GNUTLS_OCSP_CERT_REVOKED) {
JAMI_ERR("Certificate %s has revoked OCSP status", id.to_c_str());
trust_.setCertificateStatus(crt, tls::TrustStore::PermissionStatus::BANNED, false);
}
}
saveKnownDevices();
callbacks_.devicesChanged(knownDevices_);
} else {
// update device name
if (not name.empty() and it.first->second.name != name) {
JAMI_DBG("[Contacts] updating device name: %s %s", name.c_str(), id.to_c_str());
it.first->second.name = name;
saveKnownDevices();
callbacks_.devicesChanged(knownDevices_);
}
}
return true;
}
bool
ContactList::removeAccountDevice(const dht::PkId& device)
{
if (knownDevices_.erase(device) > 0) {
saveKnownDevices();
return true;
}
return false;
}
void
ContactList::setAccountDeviceName(const dht::PkId& device, const std::string& name)
{
auto dev = knownDevices_.find(device);
if (dev != knownDevices_.end()) {
if (dev->second.name != name) {
dev->second.name = name;
saveKnownDevices();
}
}
}
std::string
ContactList::getAccountDeviceName(const dht::PkId& device) const
{
auto dev = knownDevices_.find(device);
if (dev != knownDevices_.end()) {
return dev->second.name;
}
return {};
}
DeviceSync
ContactList::getSyncData() const
{
DeviceSync sync_data;
sync_data.date = clock::now().time_since_epoch().count();
// sync_data.device_name = deviceName_;
sync_data.peers = getContacts();
static constexpr size_t MAX_TRUST_REQUESTS = 20;
if (trustRequests_.size() <= MAX_TRUST_REQUESTS)
for (const auto& req : trustRequests_)
sync_data.trust_requests.emplace(req.first,
TrustRequest {req.second.device,
req.second.conversationId,
req.second.received,
{}});
else {
size_t inserted = 0;
auto req = trustRequests_.lower_bound(dht::InfoHash::getRandom());
while (inserted++ < MAX_TRUST_REQUESTS) {
if (req == trustRequests_.end())
req = trustRequests_.begin();
sync_data.trust_requests.emplace(req->first,
TrustRequest {req->second.device,
req->second.conversationId,
req->second.received,
{}});
++req;
}
}
for (const auto& dev : knownDevices_) {
sync_data.devices.emplace(dev.second.certificate->getLongId(),
KnownDeviceSync {dev.second.name,
dev.second.certificate->getId()});
}
return sync_data;
}
bool
ContactList::syncDevice(const dht::PkId& device, const time_point& syncDate)
{
auto it = knownDevices_.find(device);
if (it == knownDevices_.end()) {
JAMI_WARN("[Contacts] dropping sync data from unknown device");
return false;
}
if (it->second.last_sync >= syncDate) {
JAMI_DBG("[Contacts] dropping outdated sync data");
return false;
}
it->second.last_sync = syncDate;
return true;
}
} // namespace jami