| /* |
| * 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 |