blob: 0a31fb39f56f7cc9ce54fbeaf5762a3907c68641 [file] [log] [blame]
/*
* 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"
#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)
{
JAMI_WARN("[Contacts] addContact: %s", h.to_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);
c->second.confirmed |= confirmed;
auto hStr = h.toString();
trust_.setCertificateStatus(hStr, tls::TrustStore::PermissionStatus::ALLOWED);
saveContacts();
callbacks_.contactAdded(hStr, c->second.confirmed);
// syncDevices();
return true;
}
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();
callbacks_.contactRemoved(uri, ban);
// syncDevices();
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)
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 (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()
{
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,
std::move(tr.second.payload));
}
bool
ContactList::onTrustRequest(const dht::InfoHash& peer_account,
const dht::InfoHash& peer_device,
time_t received,
bool confirm,
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);
saveContacts();
// syncDevices();
}
}
}
if (not active) {
auto req = trustRequests_.find(peer_account);
if (req == trustRequests_.end()) {
// Add trust request
req = trustRequests_
.emplace(peer_account,
TrustRequest {peer_device, received, std::move(payload)})
.first;
} else {
// Update trust request
if (received < req->second.received) {
req->second.device = peer_device;
req->second.received = received;
req->second.payload = std::move(payload);
} else {
JAMI_DBG("[Contacts] Ignoring outdated trust request from %s",
peer_account.toString().c_str());
}
}
saveTrustRequests();
callbacks_.trustRequest(req->first.toString(), req->second.payload, received);
}
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::PAYLOAD,
std::string(r.second.payload.begin(), r.second.payload.end())}});
}
return ret;
}
bool
ContactList::acceptTrustRequest(const dht::InfoHash& from)
{
// The contact sent us a TR so we are in its contact list
addContact(from, true);
auto i = trustRequests_.find(from);
if (i == trustRequests_.end())
return false;
// Clear trust request
auto treq = std::move(i->second);
trustRequests_.erase(i);
saveTrustRequests();
// Send confirmation
// account_.get().sendTrustRequestConfirm(from);
return true;
}
bool
ContactList::discardTrustRequest(const dht::InfoHash& from)
{
if (trustRequests_.erase(from) > 0) {
saveTrustRequests();
return true;
}
return false;
}
void
ContactList::loadKnownDevices()
{
std::map<dht::InfoHash, std::pair<std::string, uint64_t>> knownDevices;
try {
// read file
auto file = fileutils::loadFile("knownDevicesNames", path_);
// load values
msgpack::object_handle oh = msgpack::unpack((const char*) file.data(), file.size());
oh.get().convert(knownDevices);
} catch (const std::exception& e) {
JAMI_WARN("[Contacts] error loading devices: %s", e.what());
return;
}
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());
}
}
}
void
ContactList::saveKnownDevices() const
{
std::ofstream file(path_ + DIR_SEPARATOR_STR "knownDevicesNames",
std::ios::trunc | std::ios::binary);
std::map<dht::InfoHash, 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::InfoHash& 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;
// match certificate chain
auto verifyResult = accountTrust_.verify(*crt);
if (not verifyResult) {
JAMI_WARN("[Contacts] Found invalid account device: %s: %s",
crt->getId().toString().c_str(),
verifyResult.toString().c_str());
return false;
}
// insert device
auto it = knownDevices_.emplace(crt->getId(), KnownDevice {crt, name, updated});
if (it.second) {
JAMI_DBG("[Contacts] Found account device: %s %s",
name.c_str(),
crt->getId().toString().c_str());
tls::CertificateStore::instance().pinCertificate(crt);
if (crt->ocspResponse){
unsigned int status = crt->ocspResponse->getCertificateStatus();
if (status == GNUTLS_OCSP_CERT_GOOD){
JAMI_DBG("Certificate %s has good OCSP status", crt->getId().to_c_str());
trust_.setCertificateStatus(crt->getId().toString(), tls::TrustStore::PermissionStatus::ALLOWED);
}
else if (status == GNUTLS_OCSP_CERT_REVOKED){
JAMI_ERR("Certificate %s has revoked OCSP status", crt->getId().to_c_str());
trust_.setCertificateStatus(crt->getId().toString(), tls::TrustStore::PermissionStatus::BANNED);
}
else {
JAMI_ERR("Certificate %s has unknown OCSP status", crt->getId().to_c_str());
trust_.setCertificateStatus(crt->getId().toString(), tls::TrustStore::PermissionStatus::UNDEFINED);
}
}
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(),
crt->getId().toString().c_str());
it.first->second.name = name;
saveKnownDevices();
callbacks_.devicesChanged(knownDevices_);
}
}
return true;
}
bool
ContactList::removeAccountDevice(const dht::InfoHash& device)
{
if (knownDevices_.erase(device) > 0) {
saveKnownDevices();
return true;
}
return false;
}
void
ContactList::setAccountDeviceName(const dht::InfoHash& 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::InfoHash& 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 = ringDeviceName_;
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.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.received, {}});
++req;
}
}
for (const auto& dev : knownDevices_) {
sync_data.devices_known.emplace(dev.first, dev.second.name);
}
return sync_data;
}
bool
ContactList::syncDevice(const dht::InfoHash& 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;
}
/*
void
ContactList::onSyncData(DeviceSync&& sync)
{
auto it = knownDevices_.find(sync.from);
if (it == knownDevices_.end()) {
JAMI_WARN("[Contacts] dropping sync data from unknown device");
return;
}
auto sync_date = clock::time_point(clock::duration(sync.date));
if (it->second.last_sync >= sync_date) {
JAMI_DBG("[Contacts] dropping outdated sync data");
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) {
account_.get().findCertificate(d.first, [this,d](const
std::shared_ptr<dht::crypto::Certificate>& crt) { if (not crt) return;
//std::lock_guard<std::mutex> lock(deviceListMutex_);
foundAccountDevice(crt, d.second);
});
}
saveKnownDevices();
// Sync contacts
for (const auto& peer : sync.peers)
updateContact(peer.first, peer.second);
saveContacts();
// Sync trust requests
for (const auto& tr : sync.trust_requests)
onTrustRequest(tr.first, tr.second.device, tr.second.received, false, {});
it->second.last_sync = sync_date;
}
*/
} // namespace jami