Skip to content
Snippets Groups Projects
Select Git revision
  • master default protected
  • release/202005
  • release/202001
  • release/201912
  • release/201911
  • release/releaseWindowsTestOne
  • release/windowsReleaseTest
  • release/releaseTest
  • release/releaseWindowsTest
  • release/201910
  • release/qt/201910
  • release/windows-test/201910
  • release/201908
  • release/201906
  • release/201905
  • release/201904
  • release/201903
  • release/201902
  • release/201901
  • release/201812
  • 4.0.0
  • 2.2.0
  • 2.1.0
  • 2.0.1
  • 2.0.0
  • 1.4.1
  • 1.4.0
  • 1.3.0
  • 1.2.0
  • 1.1.0
30 results

account_manager.cpp

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