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

archive_account_manager.cpp

Blame
    • Sébastien Blin's avatar
      d6dcc987
      misc: avoid crash if no certificate added for device · d6dcc987
      Sébastien Blin authored
      Using foundAccountDevice with a dht::PkId, can add known devices
      without certificates. Iterating over knownDevices_ without checking
      any certificate will lead to a segfault.
      This will not happen under normal circumstances, because foundAccountDevice
      only uses certificates, except for testing purpose
      
      Change-Id: I9bf6682d7d6539c0f905e80995333db0d948c5af
      d6dcc987
      History
      misc: avoid crash if no certificate added for device
      Sébastien Blin authored
      Using foundAccountDevice with a dht::PkId, can add known devices
      without certificates. Iterating over knownDevices_ without checking
      any certificate will lead to a segfault.
      This will not happen under normal circumstances, because foundAccountDevice
      only uses certificates, except for testing purpose
      
      Change-Id: I9bf6682d7d6539c0f905e80995333db0d948c5af
    Code owners
    Assign users and groups as approvers for specific file changes. Learn more.
    archive_account_manager.cpp 31.58 KiB
    /*
     *  Copyright (C) 2004-2023 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 "archive_account_manager.h"
    #include "accountarchive.h"
    #include "fileutils.h"
    #include "libdevcrypto/Common.h"
    #include "archiver.h"
    #include "base64.h"
    #include "jami/account_const.h"
    #include "account_schema.h"
    #include "jamidht/conversation_module.h"
    #include "manager.h"
    
    #include <opendht/dhtrunner.h>
    #include <opendht/thread_pool.h>
    
    #include <memory>
    #include <fstream>
    
    namespace jami {
    
    const constexpr auto EXPORT_KEY_RENEWAL_TIME = std::chrono::minutes(20);
    
    void
    ArchiveAccountManager::initAuthentication(const std::string& accountId,
                                              PrivateKey key,
                                              std::string deviceName,
                                              std::unique_ptr<AccountCredentials> credentials,
                                              AuthSuccessCallback onSuccess,
                                              AuthFailureCallback onFailure,
                                              const OnChangeCallback& onChange)
    {
        auto ctx = std::make_shared<AuthContext>();
        ctx->accountId = accountId;
        ctx->key = key;
        ctx->request = buildRequest(key);
        ctx->deviceName = std::move(deviceName);
        ctx->credentials = dynamic_unique_cast<ArchiveAccountCredentials>(std::move(credentials));
        ctx->onSuccess = std::move(onSuccess);
        ctx->onFailure = std::move(onFailure);
    
        if (not ctx->credentials) {
            ctx->onFailure(AuthError::INVALID_ARGUMENTS, "invalid credentials");
            return;
        }
    
        onChange_ = std::move(onChange);
    
        if (ctx->credentials->scheme == "dht") {
            loadFromDHT(ctx);
            return;
        }
    
        dht::ThreadPool::computation().run([ctx = std::move(ctx), w = weak_from_this()] {
            auto this_ = std::static_pointer_cast<ArchiveAccountManager>(w.lock());
            if (not this_) return;
            try {
                if (ctx->credentials->scheme == "file") {
                    // Import from external archive
                    this_->loadFromFile(*ctx);
                } else {
                    // Create/migrate local account
                    bool hasArchive = not ctx->credentials->uri.empty()
                                        and std::filesystem::is_regular_file(ctx->credentials->uri);
                    if (hasArchive) {
                        // Create/migrate from local archive
                        if (ctx->credentials->updateIdentity.first
                            and ctx->credentials->updateIdentity.second
                            and needsMigration(ctx->credentials->updateIdentity)) {
                            this_->migrateAccount(*ctx);
                        } else {
                            this_->loadFromFile(*ctx);
                        }
                    } else if (ctx->credentials->updateIdentity.first
                                and ctx->credentials->updateIdentity.second) {
                        auto future_keypair = dht::ThreadPool::computation().get<dev::KeyPair>(
                            &dev::KeyPair::create);
                        AccountArchive a;
                        JAMI_WARN("[Auth] converting certificate from old account %s",
                                    ctx->credentials->updateIdentity.first->getPublicKey()
                                        .getId()
                                        .toString()
                                        .c_str());
                        a.id = std::move(ctx->credentials->updateIdentity);
                        try {
                            a.ca_key = std::make_shared<dht::crypto::PrivateKey>(
                                fileutils::loadFile("ca.key", this_->path_));
                        } catch (...) {
                        }
                        this_->updateCertificates(a, ctx->credentials->updateIdentity);
                        a.eth_key = future_keypair.get().secret().makeInsecure().asBytes();
                        this_->onArchiveLoaded(*ctx, std::move(a));
                    } else {
                        this_->createAccount(*ctx);
                    }
                }
            } catch (const std::exception& e) {
                ctx->onFailure(AuthError::UNKNOWN, e.what());
            }
        });
    }
    
    bool
    ArchiveAccountManager::updateCertificates(AccountArchive& archive, dht::crypto::Identity& device)
    {
        JAMI_WARN("Updating certificates");
        using Certificate = dht::crypto::Certificate;
    
        // We need the CA key to resign certificates
        if (not archive.id.first or not *archive.id.first or not archive.id.second or not archive.ca_key
            or not *archive.ca_key)
            return false;
    
        // Currently set the CA flag and update expiration dates
        bool updated = false;
    
        auto& cert = archive.id.second;
        auto ca = cert->issuer;
        // Update CA if possible and relevant
        if (not ca or (not ca->issuer and (not ca->isCA() or ca->getExpiration() < clock::now()))) {
            ca = std::make_shared<Certificate>(
                Certificate::generate(*archive.ca_key, "Jami CA", {}, true));
            updated = true;
            JAMI_DBG("CA CRT re-generated");
        }
    
        // Update certificate
        if (updated or not cert->isCA() or cert->getExpiration() < clock::now()) {
            cert = std::make_shared<Certificate>(
                Certificate::generate(*archive.id.first,
                                      "Jami",
                                      dht::crypto::Identity {archive.ca_key, ca},
                                      true));
            updated = true;
            JAMI_DBG("Jami CRT re-generated");
        }
    
        if (updated and device.first and *device.first) {
            // update device certificate
            device.second = std::make_shared<Certificate>(
                Certificate::generate(*device.first, "Jami device", archive.id));
            JAMI_DBG("device CRT re-generated");
        }
    
        return updated;
    }
    
    bool
    ArchiveAccountManager::setValidity(const std::string& password,
                                       dht::crypto::Identity& device,
                                       const dht::InfoHash& id,
                                       int64_t validity)
    {
        auto archive = readArchive(password);
        // We need the CA key to resign certificates
        if (not archive.id.first or not *archive.id.first or not archive.id.second or not archive.ca_key
            or not *archive.ca_key)
            return false;
    
        auto updated = false;
    
        if (id)
            JAMI_WARN("Updating validity for certificate with id: %s", id.to_c_str());
        else
            JAMI_WARN("Updating validity for certificates");
    
        auto& cert = archive.id.second;
        auto ca = cert->issuer;
        if (not ca)
            return false;
    
        // using Certificate = dht::crypto::Certificate;
        //  Update CA if possible and relevant
        if (not id or ca->getId() == id) {
            ca->setValidity(*archive.ca_key, validity);
            updated = true;
            JAMI_DBG("CA CRT re-generated");
        }
    
        // Update certificate
        if (updated or not id or cert->getId() == id) {
            cert->setValidity(dht::crypto::Identity {archive.ca_key, ca}, validity);
            device.second->issuer = cert;
            updated = true;
            JAMI_DBG("Jami CRT re-generated");
        }
    
        if (updated) {
            archive.save(fileutils::getFullPath(path_, archivePath_), password);
        }
    
        if (updated or not id or device.second->getId() == id) {
            // update device certificate
            device.second->setValidity(archive.id, validity);
            updated = true;
        }
    
        return updated;
    }
    
    void
    ArchiveAccountManager::createAccount(AuthContext& ctx)
    {
        AccountArchive a;
        auto future_keypair = dht::ThreadPool::computation().get<dev::KeyPair>(&dev::KeyPair::create);
        auto ca = dht::crypto::generateIdentity("Jami CA");
        if (!ca.first || !ca.second) {
            throw std::runtime_error("Can't generate CA for this account.");
        }
        a.id = dht::crypto::generateIdentity("Jami", ca, 4096, true);
        if (!a.id.first || !a.id.second) {
            throw std::runtime_error("Can't generate identity for this account.");
        }
        JAMI_WARN("[Auth] new account: CA: %s, RingID: %s",
                  ca.second->getId().toString().c_str(),
                  a.id.second->getId().toString().c_str());
        a.ca_key = ca.first;
        auto keypair = future_keypair.get();
        a.eth_key = keypair.secret().makeInsecure().asBytes();
        onArchiveLoaded(ctx, std::move(a));
    }
    
    void
    ArchiveAccountManager::loadFromFile(AuthContext& ctx)
    {
        JAMI_WARN("[Auth] loading archive from: %s", ctx.credentials->uri.c_str());
        AccountArchive archive;
        try {
            archive = AccountArchive(ctx.credentials->uri, ctx.credentials->password);
        } catch (const std::exception& ex) {
            JAMI_WARN("[Auth] can't read file: %s", ex.what());
            ctx.onFailure(AuthError::INVALID_ARGUMENTS, ex.what());
            return;
        }
        onArchiveLoaded(ctx, std::move(archive));
    }
    
    struct ArchiveAccountManager::DhtLoadContext
    {
        dht::DhtRunner dht;
        std::pair<bool, bool> stateOld {false, true};
        std::pair<bool, bool> stateNew {false, true};
        bool found {false};
    };
    
    void
    ArchiveAccountManager::loadFromDHT(const std::shared_ptr<AuthContext>& ctx)
    {
        ctx->dhtContext = std::make_unique<DhtLoadContext>();
        ctx->dhtContext->dht.run(ctx->credentials->dhtPort, {}, true);
        for (const auto& bootstrap : ctx->credentials->dhtBootstrap)
            ctx->dhtContext->dht.bootstrap(bootstrap);
        auto searchEnded = [ctx]() {
            if (not ctx->dhtContext or ctx->dhtContext->found) {
                return;
            }
            auto& s = *ctx->dhtContext;
            if (s.stateOld.first && s.stateNew.first) {
                dht::ThreadPool::computation().run(
                    [ctx, network_error = !s.stateOld.second && !s.stateNew.second] {
                        ctx->dhtContext.reset();
                        JAMI_WARN("[Auth] failure looking for archive on DHT: %s",
                                  /**/ network_error ? "network error" : "not found");
                        ctx->onFailure(network_error ? AuthError::NETWORK : AuthError::UNKNOWN, "");
                    });
            }
        };
    
        auto search = [ctx, searchEnded, w=weak_from_this()](bool previous) {
            std::vector<uint8_t> key;
            dht::InfoHash loc;
            auto& s = previous ? ctx->dhtContext->stateOld : ctx->dhtContext->stateNew;
    
            // compute archive location and decryption keys
            try {
                std::tie(key, loc) = computeKeys(ctx->credentials->password,
                                                 ctx->credentials->uri,
                                                 previous);
                JAMI_DBG("[Auth] trying to load account from DHT with %s at %s",
                         /**/ ctx->credentials->uri.c_str(),
                         loc.toString().c_str());
                if (not ctx->dhtContext or ctx->dhtContext->found) {
                    return;
                }
                ctx->dhtContext->dht.get(
                    loc,
                    [ctx, key = std::move(key), w](const std::shared_ptr<dht::Value>& val) {
                        std::vector<uint8_t> decrypted;
                        try {
                            decrypted = archiver::decompress(dht::crypto::aesDecrypt(val->data, key));
                        } catch (const std::exception& ex) {
                            return true;
                        }
                        JAMI_DBG("[Auth] found archive on the DHT");
                        ctx->dhtContext->found = true;
                        dht::ThreadPool::computation().run([ctx,
                                                            decrypted = std::move(decrypted), w] {
                            try {
                                auto archive = AccountArchive(decrypted);
                                if (auto sthis = std::static_pointer_cast<ArchiveAccountManager>(w.lock())) {
                                    if (ctx->dhtContext) {
                                        ctx->dhtContext->dht.join();
                                        ctx->dhtContext.reset();
                                    }
                                    sthis->onArchiveLoaded(*ctx,
                                                          std::move(archive) /*, std::move(contacts)*/);
                                }
                            } catch (const std::exception& e) {
                                ctx->onFailure(AuthError::UNKNOWN, "");
                            }
                        });
                        return not ctx->dhtContext->found;
                    },
                    [=, &s](bool ok) {
                        JAMI_DBG("[Auth] DHT archive search ended at %s", /**/ loc.toString().c_str());
                        s.first = true;
                        s.second = ok;
                        searchEnded();
                    });
            } catch (const std::exception& e) {
                // JAMI_ERR("Error computing kedht::ThreadPool::computation().run(ys: %s", e.what());
                s.first = true;
                s.second = true;
                searchEnded();
                return;
            }
        };
        dht::ThreadPool::computation().run(std::bind(search, true));
        dht::ThreadPool::computation().run(std::bind(search, false));
    }
    
    void
    ArchiveAccountManager::migrateAccount(AuthContext& ctx)
    {
        JAMI_WARN("[Auth] account migration needed");
        AccountArchive archive;
        try {
            archive = readArchive(ctx.credentials->password);
        } catch (...) {
            JAMI_DBG("[Auth] Can't load archive");
            ctx.onFailure(AuthError::INVALID_ARGUMENTS, "");
            return;
        }
    
        updateArchive(archive);
    
        if (updateCertificates(archive, ctx.credentials->updateIdentity)) {
            // because updateCertificates already regenerate a device, we do not need to
            // regenerate one in onArchiveLoaded
            onArchiveLoaded(ctx, std::move(archive));
        } else
            ctx.onFailure(AuthError::UNKNOWN, "");
    }
    
    void
    ArchiveAccountManager::onArchiveLoaded(AuthContext& ctx,
                                           AccountArchive&& a)
    {
        auto ethAccount = dev::KeyPair(dev::Secret(a.eth_key)).address().hex();
        dhtnet::fileutils::check_dir(path_, 0700);
    
        a.save(fileutils::getFullPath(path_, archivePath_), ctx.credentials ? ctx.credentials->password : "");
    
        if (not a.id.second->isCA()) {
            JAMI_ERR("[Auth] trying to sign a certificate with a non-CA.");
        }
    
        std::shared_ptr<dht::crypto::Certificate> deviceCertificate;
        std::unique_ptr<ContactList> contacts;
        auto usePreviousIdentity = false;
        // If updateIdentity got a valid certificate, there is no need for a new cert
        if (auto oldId = ctx.credentials->updateIdentity.second) {
            contacts = std::make_unique<ContactList>(ctx.accountId, oldId, path_, onChange_);
            if (contacts->isValidAccountDevice(*oldId) && ctx.credentials->updateIdentity.first) {
                deviceCertificate = oldId;
                usePreviousIdentity = true;
                JAMI_WARN("[Auth] Using previously generated certificate %s",
                                              deviceCertificate->getLongId().toString().c_str());
            } else {
                contacts.reset();
            }
        }
    
        // Generate a new device if needed
        if (!deviceCertificate) {
            JAMI_WARN("[Auth] creating new device certificate");
            auto request = ctx.request.get();
            if (not request->verify()) {
                JAMI_ERR("[Auth] Invalid certificate request.");
                ctx.onFailure(AuthError::INVALID_ARGUMENTS, "");
                return;
            }
            deviceCertificate = std::make_shared<dht::crypto::Certificate>(
                dht::crypto::Certificate::generate(*request, a.id));
            JAMI_WARNING("[Auth] created new device: {}",
                      deviceCertificate->getLongId());
        }
    
        auto receipt = makeReceipt(a.id, *deviceCertificate, ethAccount);
        auto receiptSignature = a.id.first->sign({receipt.first.begin(), receipt.first.end()});
    
        auto info = std::make_unique<AccountInfo>();
        auto pk = usePreviousIdentity ? ctx.credentials->updateIdentity.first : ctx.key.get();
        auto sharedPk = pk->getSharedPublicKey();
        info->identity.first = pk;
        info->identity.second = deviceCertificate;
        info->accountId = a.id.second->getId().toString();
        info->devicePk = sharedPk;
        info->deviceId = info->devicePk->getLongId().toString();
        if (ctx.deviceName.empty())
            ctx.deviceName = info->deviceId.substr(8);
    
        if (!contacts)
            contacts = std::make_unique<ContactList>(ctx.accountId, a.id.second, path_, onChange_);
        info->contacts = std::move(contacts);
        info->contacts->setContacts(a.contacts);
        info->contacts->foundAccountDevice(deviceCertificate, ctx.deviceName, clock::now());
        info->ethAccount = ethAccount;
        info->announce = std::move(receipt.second);
        ConversationModule::saveConvInfosToPath(path_, a.conversations);
        ConversationModule::saveConvRequestsToPath(path_, a.conversationsRequests);
        info_ = std::move(info);
    
        ctx.onSuccess(*info_,
                      std::move(a.config),
                      std::move(receipt.first),
                      std::move(receiptSignature));
    }
    
    std::pair<std::vector<uint8_t>, dht::InfoHash>
    ArchiveAccountManager::computeKeys(const std::string& password,
                                       const std::string& pin,
                                       bool previous)
    {
        // Compute time seed
        auto now = std::chrono::duration_cast<std::chrono::seconds>(clock::now().time_since_epoch());
        auto tseed = now.count() / std::chrono::seconds(EXPORT_KEY_RENEWAL_TIME).count();
        if (previous)
            tseed--;
        std::ostringstream ss;
        ss << std::hex << tseed;
        auto tseed_str = ss.str();
    
        // Generate key for archive encryption, using PIN as the salt
        std::vector<uint8_t> salt_key;
        salt_key.reserve(pin.size() + tseed_str.size());
        salt_key.insert(salt_key.end(), pin.begin(), pin.end());
        salt_key.insert(salt_key.end(), tseed_str.begin(), tseed_str.end());
        auto key = dht::crypto::stretchKey(password, salt_key, 256 / 8);
    
        // Generate public storage location as SHA1(key).
        auto loc = dht::InfoHash::get(key);
    
        return {key, loc};
    }
    
    std::pair<std::string, std::shared_ptr<dht::Value>>
    ArchiveAccountManager::makeReceipt(const dht::crypto::Identity& id,
                                       const dht::crypto::Certificate& device,
                                       const std::string& ethAccount)
    {
        JAMI_DBG("[Auth] signing device receipt");
        auto devId = device.getId();
        DeviceAnnouncement announcement;
        announcement.dev = devId;
        announcement.pk = device.getSharedPublicKey();
        dht::Value ann_val {announcement};
        ann_val.sign(*id.first);
    
        auto packedAnnoucement = ann_val.getPacked();
        JAMI_DBG("[Auth] device announcement size: %zu", packedAnnoucement.size());
    
        std::ostringstream is;
        is << "{\"id\":\"" << id.second->getId() << "\",\"dev\":\"" << devId << "\",\"eth\":\""
           << ethAccount << "\",\"announce\":\"" << base64::encode(packedAnnoucement) << "\"}";
    
        // auto announce_ = ;
        return {is.str(), std::make_shared<dht::Value>(std::move(ann_val))};
    }
    
    bool
    ArchiveAccountManager::needsMigration(const dht::crypto::Identity& id)
    {
        if (not id.second)
            return false;
        auto cert = id.second->issuer;
        while (cert) {
            if (not cert->isCA()) {
                JAMI_WARN("certificate %s is not a CA, needs update.", cert->getId().toString().c_str());
                return true;
            }
            if (cert->getExpiration() < clock::now()) {
                JAMI_WARN("certificate %s is expired, needs update.", cert->getId().toString().c_str());
                return true;
            }
            cert = cert->issuer;
        }
        return false;
    }
    
    void
    ArchiveAccountManager::syncDevices()
    {
        if (not dht_ or not dht_->isRunning()) {
            JAMI_WARN("Not syncing devices: DHT is not running");
            return;
        }
        JAMI_DBG("Building device sync from %s", info_->deviceId.c_str());
        auto sync_data = info_->contacts->getSyncData();
    
        for (const auto& dev : getKnownDevices()) {
            // don't send sync data to ourself
            if (dev.first.toString() == info_->deviceId)
                continue;
            if (!dev.second.certificate) {
                JAMI_WARNING("Cannot find certificate for {}", dev.first);
                continue;
            }
            auto pk = dev.second.certificate->getSharedPublicKey();
            JAMI_DBG("sending device sync to %s %s",
                     dev.second.name.c_str(),
                     dev.first.toString().c_str());
            auto syncDeviceKey = dht::InfoHash::get("inbox:" + pk->getId().toString());
            dht_->putEncrypted(syncDeviceKey, pk, sync_data);
        }
    }
    
    void
    ArchiveAccountManager::startSync(const OnNewDeviceCb& cb, const OnDeviceAnnouncedCb& dcb, bool publishPresence)
    {
        AccountManager::startSync(std::move(cb), std::move(dcb), publishPresence);
    
        dht_->listen<DeviceSync>(
            dht::InfoHash::get("inbox:" + info_->devicePk->getId().toString()),
            [this](DeviceSync&& sync) {
                // Received device sync data.
                // check device certificate
                findCertificate(sync.from,
                                [this,
                                 sync](const std::shared_ptr<dht::crypto::Certificate>& cert) mutable {
                                    if (!cert or cert->getId() != sync.from) {
                                        JAMI_WARN("Can't find certificate for device %s",
                                                  sync.from.toString().c_str());
                                        return;
                                    }
                                    if (not foundAccountDevice(cert))
                                        return;
                                    onSyncData(std::move(sync));
                                });
    
                return true;
            });
    }
    
    AccountArchive
    ArchiveAccountManager::readArchive(const std::string& pwd) const
    {
        JAMI_DBG("[Auth] reading account archive");
        return AccountArchive(fileutils::getFullPath(path_, archivePath_), pwd);
    }
    
    void
    ArchiveAccountManager::updateArchive(AccountArchive& archive) const
    {
        using namespace libjami::Account::ConfProperties;
    
        // Keys not exported to archive
        static const auto filtered_keys = {Ringtone::PATH,
                                           ARCHIVE_PATH,
                                           DEVICE_ID,
                                           DEVICE_NAME,
                                           Conf::CONFIG_DHT_PORT,
                                           DHT_PROXY_LIST_URL,
                                           AUTOANSWER,
                                           PROXY_ENABLED,
                                           PROXY_SERVER,
                                           PROXY_PUSH_TOKEN};
    
        // Keys with meaning of file path where the contents has to be exported in base64
        static const auto encoded_keys = {TLS::CA_LIST_FILE,
                                          TLS::CERTIFICATE_FILE,
                                          TLS::PRIVATE_KEY_FILE};
    
        JAMI_DBG("[Auth] building account archive");
        for (const auto& it : onExportConfig_()) {
            // filter-out?
            if (std::any_of(std::begin(filtered_keys), std::end(filtered_keys), [&](const auto& key) {
                    return key == it.first;
                }))
                continue;
    
            // file contents?
            if (std::any_of(std::begin(encoded_keys), std::end(encoded_keys), [&](const auto& key) {
                    return key == it.first;
                })) {
                try {
                    archive.config.emplace(it.first, base64::encode(fileutils::loadFile(it.second)));
                } catch (...) {
                }
            } else
                archive.config[it.first] = it.second;
        }
        if (info_) {
            // If migrating from same archive, info_ will be null
            archive.contacts = info_->contacts->getContacts();
            // Note we do not know accountID_ here, use path
            archive.conversations = ConversationModule::convInfosFromPath(path_);
            archive.conversationsRequests = ConversationModule::convRequestsFromPath(path_);
        }
    }
    
    void
    ArchiveAccountManager::saveArchive(AccountArchive& archive, const std::string& pwd)
    {
        try {
            updateArchive(archive);
            if (archivePath_.empty())
                archivePath_ = "export.gz";
            archive.save(fileutils::getFullPath(path_, archivePath_), pwd);
        } catch (const std::runtime_error& ex) {
            JAMI_ERR("[Auth] Can't export archive: %s", ex.what());
            return;
        }
    }
    
    bool
    ArchiveAccountManager::changePassword(const std::string& password_old,
                                          const std::string& password_new)
    {
        try {
            auto path = fileutils::getFullPath(path_, archivePath_);
            AccountArchive(path, password_old).save(path, password_new);
            return true;
        } catch (const std::exception&) {
            return false;
        }
    }
    
    std::string
    generatePIN(size_t length = 16, size_t split = 8)
    {
        static constexpr const char alphabet[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
        dht::crypto::random_device rd;
        std::uniform_int_distribution<size_t> dis(0, sizeof(alphabet) - 2);
        std::string ret;
        ret.reserve(length);
        for (size_t i = 0; i < length; i++) {
            ret.push_back(alphabet[dis(rd)]);
            if (i % split == split - 1 and i != length - 1)
                ret.push_back('-');
        }
        return ret;
    }
    
    void
    ArchiveAccountManager::addDevice(const std::string& password, AddDeviceCallback cb)
    {
        dht::ThreadPool::computation().run([password, cb = std::move(cb), w=weak_from_this()] {
            auto this_ = std::static_pointer_cast<ArchiveAccountManager>(w.lock());
            if (not this_) return;
    
            std::vector<uint8_t> key;
            dht::InfoHash loc;
            std::string pin_str;
            AccountArchive a;
            try {
                JAMI_DBG("[Auth] exporting account");
    
                a = this_->readArchive(password);
    
                // Generate random PIN
                pin_str = generatePIN();
    
                std::tie(key, loc) = computeKeys(password, pin_str);
            } catch (const std::exception& e) {
                JAMI_ERR("[Auth] can't export account: %s", e.what());
                cb(AddDeviceResult::ERROR_CREDENTIALS, {});
                return;
            }
            // now that key and loc are computed, display to user in lowercase
            std::transform(pin_str.begin(), pin_str.end(), pin_str.begin(), ::tolower);
            try {
                this_->updateArchive(a);
                auto encrypted = dht::crypto::aesEncrypt(archiver::compress(a.serialize()), key);
                if (not this_->dht_ or not this_->dht_->isRunning())
                    throw std::runtime_error("DHT is not running..");
                JAMI_WARN("[Auth] exporting account with PIN: %s at %s (size %zu)",
                            pin_str.c_str(),
                            loc.toString().c_str(),
                            encrypted.size());
                this_->dht_->put(loc, encrypted, [cb, pin = std::move(pin_str)](bool ok) {
                    JAMI_DBG("[Auth] account archive published: %s", ok ? "success" : "failure");
                    if (ok)
                        cb(AddDeviceResult::SUCCESS_SHOW_PIN, pin);
                    else
                        cb(AddDeviceResult::ERROR_NETWORK, {});
                });
            } catch (const std::exception& e) {
                JAMI_ERR("[Auth] can't export account: %s", e.what());
                cb(AddDeviceResult::ERROR_NETWORK, {});
                return;
            }
        });
    }
    
    bool
    ArchiveAccountManager::revokeDevice(
                                        const std::string& password,
                                        const std::string& device,
                                        RevokeDeviceCallback cb)
    {
        auto fa = dht::ThreadPool::computation().getShared<AccountArchive>(
            [this, password] { return readArchive(password); });
        findCertificate(DeviceId(device),
            [fa = std::move(fa), password, device, cb, w=weak_from_this()](
                const std::shared_ptr<dht::crypto::Certificate>& crt) mutable {
                    if (not crt) {
                        cb(RevokeDeviceResult::ERROR_NETWORK);
                        return;
                    }
                    auto this_ = std::static_pointer_cast<ArchiveAccountManager>(w.lock());
                    if (not this_) return;
                    this_->info_->contacts->foundAccountDevice(crt);
                    AccountArchive a;
                    try {
                        a = fa.get();
                    } catch (...) {
                        cb(RevokeDeviceResult::ERROR_CREDENTIALS);
                        return;
                    }
                    // Add revoked device to the revocation list and resign it
                    if (not a.revoked)
                        a.revoked = std::make_shared<decltype(a.revoked)::element_type>();
                    a.revoked->revoke(*crt);
                    a.revoked->sign(a.id);
                    // add to CRL cache
                    this_->certStore().pinRevocationList(a.id.second->getId().toString(), a.revoked);
                    this_->certStore().loadRevocations(*a.id.second);
    
                    // Announce CRL immediately
                    auto h = a.id.second->getId();
                    this_->dht_->put(h, a.revoked, dht::DoneCallback {}, {}, true);
    
                    this_->saveArchive(a, password);
                    this_->info_->contacts->removeAccountDevice(crt->getLongId());
                    cb(RevokeDeviceResult::SUCCESS);
                    this_->syncDevices();
                });
        return false;
    }
    
    bool
    ArchiveAccountManager::exportArchive(const std::string& destinationPath, const std::string& password)
    {
        try {
            // Save contacts if possible before exporting
            AccountArchive archive = readArchive(password);
            updateArchive(archive);
            archive.save(fileutils::getFullPath(path_, archivePath_), password);
    
            // Export the file
            auto sourcePath = fileutils::getFullPath(path_, archivePath_);
            std::ifstream src(sourcePath, std::ios::in | std::ios::binary);
            if (!src)
                return false;
            std::ofstream dst(destinationPath, std::ios::out | std::ios::binary);
            dst << src.rdbuf();
            return true;
        } catch (const std::runtime_error& ex) {
            JAMI_ERR("[Auth] Can't export archive: %s", ex.what());
            return false;
        } catch (...) {
            JAMI_ERR("[Auth] Can't export archive: can't read archive");
            return false;
        }
    }
    
    bool
    ArchiveAccountManager::isPasswordValid(const std::string& password)
    {
        try {
            readArchive(password);
            return true;
        } catch (...) {
            return false;
        }
    }
    
    #if HAVE_RINGNS
    void
    ArchiveAccountManager::registerName(const std::string& password,
                                        const std::string& name,
                                        RegistrationCallback cb)
    {
        std::string signedName;
        auto nameLowercase {name};
        std::transform(nameLowercase.begin(), nameLowercase.end(), nameLowercase.begin(), ::tolower);
        std::string publickey;
        std::string accountId;
        std::string ethAccount;
    
        try {
            auto archive = readArchive(password);
            auto privateKey = archive.id.first;
            const auto& pk = privateKey->getPublicKey();
            publickey = pk.toString();
            accountId = pk.getId().toString();
            signedName = base64::encode(
                privateKey->sign(std::vector<uint8_t>(nameLowercase.begin(), nameLowercase.end())));
            ethAccount = dev::KeyPair(dev::Secret(archive.eth_key)).address().hex();
        } catch (const std::exception& e) {
            // JAMI_ERR("[Auth] can't export account: %s", e.what());
            cb(NameDirectory::RegistrationResponse::invalidCredentials);
            return;
        }
    
        nameDir_.get().registerName(accountId, nameLowercase, ethAccount, cb, signedName, publickey);
    }
    #endif
    
    } // namespace jami