Skip to content
Snippets Groups Projects
Select Git revision
  • a022a5f6f1e52d30e5a6e2f84e8e0e30a0ff1e1b
  • master default protected
2 results

chatview-i18n-(design-draft).md

Blame
  • 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