diff options
Diffstat (limited to 'src/client/QXmppOmemoManager_p.cpp')
| -rw-r--r-- | src/client/QXmppOmemoManager_p.cpp | 3714 |
1 files changed, 3714 insertions, 0 deletions
diff --git a/src/client/QXmppOmemoManager_p.cpp b/src/client/QXmppOmemoManager_p.cpp new file mode 100644 index 00000000..2f8ca00d --- /dev/null +++ b/src/client/QXmppOmemoManager_p.cpp @@ -0,0 +1,3714 @@ +// SPDX-FileCopyrightText: 2022 Melvin Keskin <melvo@olomono.de> +// SPDX-FileCopyrightText: 2022 Linus Jahn <lnj@kaidan.im> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +/// \cond + +#include "QXmppOmemoManager_p.h" + +#include "QXmppConstants_p.h" +#include "QXmppOmemoDeviceElement_p.h" +#include "QXmppOmemoElement_p.h" +#include "QXmppOmemoEnvelope_p.h" +#include "QXmppOmemoIq_p.h" +#include "QXmppOmemoItems_p.h" +#include "QXmppPubSubItem.h" +#include "QXmppSceEnvelope_p.h" +#include "QXmppTrustManager.h" +#include "QXmppUtils.h" +#include "QXmppUtils_p.h" + +#include <protocol.h> + +#include "OmemoCryptoProvider.h" + +#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) +#include <QRandomGenerator> +#endif +#include <QStringBuilder> + +using namespace QXmpp; +using namespace QXmpp::Private; +using namespace QXmpp::Omemo::Private; + +using Error = QXmppStanza::Error; +using Manager = QXmppOmemoManager; +using ManagerPrivate = QXmppOmemoManagerPrivate; + +namespace QXmpp::Omemo::Private { + +const QString PAYLOAD_MESSAGE_AUTHENTICATION_CODE_TYPE = QStringLiteral("hmac(sha256)"); + +// +// Creates a key ID. +// +// The first byte representing a version string used by the OMEMO library but +// not needed for trust management is removed. +// It corresponds to the fingerprint shown to users which also does not contain +// the first byte. +// +// \param key key for whom its ID is created +// +// \return the key ID +// +QByteArray createKeyId(const QByteArray &key) +{ + return QByteArray(key).remove(0, 1); +} + +} // namespace QXmpp::Omemo::Private + +// +// Contains address data for an OMEMO device and a method to get the corresponding OMEMO library +// data structure. +// +class Address +{ +public: + // + // Creates an OMEMO device address. + // + // \param jid bare JID of the device owner + // \param deviceId ID of the device + // + Address(const QString &jid, uint32_t deviceId) + : m_jid(jid.toUtf8()), m_deviceId(int32_t(deviceId)) + { + } + // + // Returns the representation of the OMEMO device address used by the OMEMO library. + // + // \return the OMEMO library device address + // + signal_protocol_address data() const + { + return { m_jid.data(), size_t(m_jid.size()), m_deviceId }; + } + +private: + QByteArray m_jid; + int32_t m_deviceId; +}; + +// +// Creates a PEP node configuration for the device list. +// +// \return the device list node configuration +// +static QXmppPubSubNodeConfig deviceListNodeConfig() +{ + QXmppPubSubNodeConfig config; + config.setAccessModel(QXmppPubSubNodeConfig::Open); + + return config; +} + +// +// Creates publish options for publishing the device list to a corresponding PEP node. +// +// \return the device list node publish options +// +static QXmppPubSubPublishOptions deviceListNodePublishOptions() +{ + QXmppPubSubPublishOptions publishOptions; + publishOptions.setAccessModel(QXmppPubSubPublishOptions::Open); + + return publishOptions; +} + +// +// Creates a PEP node configuration for device bundles. +// +// \return the device bundles node configuration +// +static QXmppPubSubNodeConfig deviceBundlesNodeConfig(QXmppPubSubNodeConfig::ItemLimit itemLimit = QXmppPubSubNodeConfig::Max()) +{ + QXmppPubSubNodeConfig config; + config.setAccessModel(QXmppPubSubNodeConfig::Open); + config.setMaxItems(itemLimit); + + return config; +} + +// +// Creates publish options for publishing device bundles to a corresponding PEP node. +// +// \return the device bundles node publish options +// +static QXmppPubSubPublishOptions deviceBundlesNodePublishOptions(QXmppPubSubNodeConfig::ItemLimit itemLimit = QXmppPubSubNodeConfig::Max()) +{ + QXmppPubSubPublishOptions publishOptions; + publishOptions.setAccessModel(QXmppPubSubPublishOptions::Open); + publishOptions.setMaxItems(itemLimit); + + return publishOptions; +} + +// +// Deserializes the signature of a signed public pre key. +// +// \param signedPublicPreKeySignature signed public pre key signature location +// \param serializedSignedPublicPreKeySignature serialized signature of the +// signed public pre key +// +// \return whether it succeeded +// +static int deserializeSignedPublicPreKeySignature(const uint8_t **signedPublicPreKeySignature, const QByteArray &serializedSignedPublicPreKeySignature) +{ + *signedPublicPreKeySignature = reinterpret_cast<const uint8_t *>(serializedSignedPublicPreKeySignature.constData()); + return serializedSignedPublicPreKeySignature.size(); +} + +// +// Extracts the JID from an address used by the OMEMO library. +// +// \param address address containing the JID data +// +// \return the extracted JID +// +static QString extractJid(signal_protocol_address address) +{ + return QString::fromUtf8(address.name, address.name_len); +} + +static QString errorToString(const QXmppStanza::Error &err) +{ + return u"Error('" % err.text() % u"', type=" % QString::number(err.type()) % u", condition=" % + QString::number(err.condition()) % u")"; +} + +static void replaceChildElements(QDomElement &oldElement, const QDomElement &newElement) +{ + // remove old child elements + while (true) { + if (auto childElement = oldElement.firstChildElement(); !childElement.isNull()) { + oldElement.removeChild(childElement); + } else { + break; + } + } + // append new child elements + for (auto childElement = newElement.firstChildElement(); + !childElement.isNull(); + childElement = childElement.nextSiblingElement()) { + oldElement.appendChild(childElement); + } +} + +template<typename T, typename Err> +auto mapToSuccess(std::variant<T, Err> var) +{ + return mapSuccess(std::move(var), [](T) { return Success(); }); +} + +QXmppOmemoManagerPrivate::QXmppOmemoManagerPrivate(Manager *parent, QXmppOmemoStorage *omemoStorage) + : q(parent), + omemoStorage(omemoStorage), + signedPreKeyPairsRenewalTimer(parent), + deviceRemovalTimer(parent) +{ +} + +// +// Initializes the OMEMO library. +// +void ManagerPrivate::init() +{ + if (initGlobalContext() && + initLocking() && + initCryptoProvider()) { + initStores(); + } else { + warning(QStringLiteral("OMEMO library could not be initialized")); + } +} + +// +// Initializes the OMEMO library's global context. +// +// \return whether the initialization succeeded +// +bool ManagerPrivate::initGlobalContext() +{ + // "q" is passed as the parameter "user_data" to functions called by + // the OMEMO library when no explicit "user_data" is set for those + // functions (e.g., to the lock and unlock functions). + if (signal_context_create(globalContext.ptrRef(), q) < 0) { + warning("Signal context could not be be created"); + return false; + } + + return true; +} + +// +// Initializes the OMEMO library's locking functions. +// +// \return whether the initialization succeeded +// +bool ManagerPrivate::initLocking() +{ + const auto lock = [](void *user_data) { + const auto *manager = reinterpret_cast<Manager *>(user_data); + auto *d = manager->d.get(); + d->mutex.lock(); + }; + + const auto unlock = [](void *user_data) { + const auto *manager = reinterpret_cast<Manager *>(user_data); + auto *d = manager->d.get(); + d->mutex.unlock(); + }; + + if (signal_context_set_locking_functions(globalContext.get(), lock, unlock) < 0) { + warning("Locking functions could not be set"); + return false; + } + + return true; +} + +// +// Initializes the OMEMO library's crypto provider. +// +// \return whether the initialization succeeded +// +bool ManagerPrivate::initCryptoProvider() +{ + cryptoProvider = createOmemoCryptoProvider(this); + + if (signal_context_set_crypto_provider(globalContext.get(), &cryptoProvider) < 0) { + warning("Crypto provider could not be set"); + return false; + } + + return true; +} + +// +// Initializes the OMEMO library's stores. +// +// \return whether the initialization succeeded +// +void ManagerPrivate::initStores() +{ + identityKeyStore = createIdentityKeyStore(); + preKeyStore = createPreKeyStore(); + signedPreKeyStore = createSignedPreKeyStore(); + sessionStore = createSessionStore(); + + signal_protocol_store_context_create(storeContext.ptrRef(), globalContext.get()); + signal_protocol_store_context_set_identity_key_store(storeContext.get(), &identityKeyStore); + signal_protocol_store_context_set_pre_key_store(storeContext.get(), &preKeyStore); + signal_protocol_store_context_set_signed_pre_key_store(storeContext.get(), &signedPreKeyStore); + signal_protocol_store_context_set_session_store(storeContext.get(), &sessionStore); +} + +// +// Creates the OMEMO library's identity key store. +// +// The identity key is the long-term key. +// +// \return the identity key store +// +signal_protocol_identity_key_store ManagerPrivate::createIdentityKeyStore() const +{ + signal_protocol_identity_key_store store; + + store.get_identity_key_pair = [](signal_buffer **public_data, signal_buffer **private_data, void *user_data) { + auto *manager = reinterpret_cast<Manager *>(user_data); + const auto *d = manager->d.get(); + + const auto &privateIdentityKey = d->ownDevice.privateIdentityKey; + if (!(*private_data = signal_buffer_create(reinterpret_cast<const uint8_t *>(privateIdentityKey.constData()), privateIdentityKey.size()))) { + manager->warning("Private identity key could not be loaded"); + return -1; + } + + const auto &publicIdentityKey = d->ownDevice.publicIdentityKey; + if (!(*public_data = signal_buffer_create(reinterpret_cast<const uint8_t *>(publicIdentityKey.constData()), publicIdentityKey.size()))) { + manager->warning("Public identity key could not be loaded"); + return -1; + } + + return 0; + }; + + store.get_local_registration_id = [](void *user_data, uint32_t *registration_id) { + const auto *manager = reinterpret_cast<Manager *>(user_data); + const auto *d = manager->d.get(); + *registration_id = d->ownDevice.id; + return 0; + }; + + store.save_identity = [](const signal_protocol_address *, uint8_t *, size_t, void *) { + // Do not use the OMEMO library's trust management. + return 0; + }; + + store.is_trusted_identity = [](const signal_protocol_address *, uint8_t *, size_t, void *) { + // Do not use the OMEMO library's trust management. + // All keys are trusted at this level / by the OMEMO library. + return 1; + }; + + store.destroy_func = [](void *) { + }; + + store.user_data = q; + + return store; +} + +// +// Creates the OMEMO library's signed pre key store. +// +// A signed pre key is used for building a session. +// +// \return the signed pre key store +// +signal_protocol_signed_pre_key_store ManagerPrivate::createSignedPreKeyStore() const +{ + signal_protocol_signed_pre_key_store store; + + store.load_signed_pre_key = [](signal_buffer **record, uint32_t signed_pre_key_id, void *user_data) { + auto *manager = reinterpret_cast<Manager *>(user_data); + const auto *d = manager->d.get(); + const auto &signedPreKeyPair = d->signedPreKeyPairs.value(signed_pre_key_id).data; + + if (signedPreKeyPair.isEmpty()) { + return SG_ERR_INVALID_KEY_ID; + } + + if (!(*record = signal_buffer_create(reinterpret_cast<const uint8_t *>(signedPreKeyPair.constData()), signedPreKeyPair.size()))) { + manager->warning("Signed pre key pair could not be loaded"); + return SG_ERR_INVALID_KEY_ID; + } + + return SG_SUCCESS; + }; + + store.store_signed_pre_key = [](uint32_t signed_pre_key_id, uint8_t *record, size_t record_len, void *user_data) { + auto *manager = reinterpret_cast<Manager *>(user_data); + auto *d = manager->d.get(); + + QXmppOmemoStorage::SignedPreKeyPair signedPreKeyPair; + signedPreKeyPair.creationDate = QDateTime::currentDateTimeUtc(); + signedPreKeyPair.data = QByteArray(reinterpret_cast<const char *>(record), record_len); + + d->signedPreKeyPairs.insert(signed_pre_key_id, signedPreKeyPair); + d->omemoStorage->addSignedPreKeyPair(signed_pre_key_id, signedPreKeyPair); + + return 0; + }; + + store.contains_signed_pre_key = [](uint32_t signed_pre_key_id, void *user_data) { + const auto *manager = reinterpret_cast<Manager *>(user_data); + const auto *d = manager->d.get(); + return d->signedPreKeyPairs.contains(signed_pre_key_id) ? 1 : 0; + }; + + store.remove_signed_pre_key = [](uint32_t signed_pre_key_id, void *user_data) { + const auto *manager = reinterpret_cast<Manager *>(user_data); + auto *d = manager->d.get(); + d->signedPreKeyPairs.remove(signed_pre_key_id); + d->omemoStorage->removeSignedPreKeyPair(signed_pre_key_id); + return 0; + }; + + store.destroy_func = [](void *) { + }; + + store.user_data = q; + + return store; +} + +// +// Creates the OMEMO library's pre key store. +// +// A pre key is used for building a session. +// +// \return the pre key store +// +signal_protocol_pre_key_store ManagerPrivate::createPreKeyStore() const +{ + signal_protocol_pre_key_store store; + + store.load_pre_key = [](signal_buffer **record, uint32_t pre_key_id, void *user_data) { + auto *manager = reinterpret_cast<Manager *>(user_data); + const auto *d = manager->d.get(); + const auto &preKey = d->preKeyPairs.value(pre_key_id); + + if (preKey.isEmpty()) { + return SG_ERR_INVALID_KEY_ID; + } + + if (!(*record = signal_buffer_create(reinterpret_cast<const uint8_t *>(preKey.constData()), preKey.size()))) { + manager->warning("Pre key could not be loaded"); + return SG_ERR_INVALID_KEY_ID; + } + + return SG_SUCCESS; + }; + + store.store_pre_key = [](uint32_t pre_key_id, uint8_t *record, size_t record_len, void *user_data) { + const auto *manager = reinterpret_cast<Manager *>(user_data); + auto *d = manager->d.get(); + const auto preKey = QByteArray(reinterpret_cast<const char *>(record), record_len); + d->preKeyPairs.insert(pre_key_id, preKey); + d->omemoStorage->addPreKeyPairs({ { pre_key_id, preKey } }); + return 0; + }; + + store.contains_pre_key = [](uint32_t pre_key_id, void *user_data) { + const auto *manager = reinterpret_cast<Manager *>(user_data); + const auto *d = manager->d.get(); + return d->preKeyPairs.contains(pre_key_id) ? 1 : 0; + }; + + store.remove_pre_key = [](uint32_t pre_key_id, void *user_data) { + auto *manager = reinterpret_cast<Manager *>(user_data); + auto *d = manager->d.get(); + + if (!d->renewPreKeyPairs(pre_key_id)) { + return -1; + } + + return 0; + }; + + store.destroy_func = [](void *) { + }; + + store.user_data = q; + + return store; +} + +// +// Creates the OMEMO library's session store. +// +// A session contains all data needed for encryption and decryption. +// +// \return the session store +// +signal_protocol_session_store ManagerPrivate::createSessionStore() const +{ + signal_protocol_session_store store; + + store.load_session_func = [](signal_buffer **record, signal_buffer **, const signal_protocol_address *address, void *user_data) { + auto *manager = reinterpret_cast<Manager *>(user_data); + const auto *d = manager->d.get(); + const auto jid = extractJid(*address); + + const auto &session = d->devices.value(jid).value(uint32_t(address->device_id)).session; + + if (session.isEmpty()) { + return 0; + } + + if (!(*record = signal_buffer_create(reinterpret_cast<const uint8_t *>(session.constData()), size_t(session.size())))) { + manager->warning("Session could not be loaded"); + return -1; + } + + return 1; + }; + + store.get_sub_device_sessions_func = [](signal_int_list **sessions, const char *name, size_t name_len, void *user_data) { + auto *manager = reinterpret_cast<Manager *>(user_data); + const auto *d = manager->d.get(); + const auto jid = QString::fromUtf8(name, name_len); + auto userDevices = d->devices.value(jid); + + // Remove all devices not having an active session. + for (auto itr = userDevices.begin(); itr != userDevices.end();) { + const auto &device = itr.value(); + if (device.session.isEmpty() || device.unrespondedSentStanzasCount == UNRESPONDED_STANZAS_UNTIL_ENCRYPTION_IS_STOPPED) { + itr = userDevices.erase(itr); + } else { + ++itr; + } + } + + signal_int_list *deviceIds = signal_int_list_alloc(); + for (auto itr = userDevices.cbegin(); itr != userDevices.cend(); ++itr) { + const auto deviceId = itr.key(); + if (signal_int_list_push_back(deviceIds, int(deviceId)) < 0) { + manager->warning("Device ID could not be added to list"); + return -1; + } + } + + *sessions = deviceIds; + return int(signal_int_list_size(*sessions)); + }; + + store.store_session_func = [](const signal_protocol_address *address, uint8_t *record, size_t record_len, uint8_t *, size_t, void *user_data) { + const auto *manager = reinterpret_cast<Manager *>(user_data); + auto *d = manager->d.get(); + const auto session = QByteArray(reinterpret_cast<const char *>(record), record_len); + const auto jid = extractJid(*address); + const auto deviceId = int(address->device_id); + + auto &device = d->devices[jid][deviceId]; + device.session = session; + d->omemoStorage->addDevice(jid, deviceId, device); + return 0; + }; + + store.contains_session_func = [](const signal_protocol_address *address, void *user_data) { + const auto *manager = reinterpret_cast<Manager *>(user_data); + const auto *d = manager->d.get(); + const auto jid = extractJid(*address); + return d->devices.value(jid).value(int(address->device_id)).session.isEmpty() ? 0 : 1; + }; + + store.delete_session_func = [](const signal_protocol_address *address, void *user_data) { + const auto *manager = reinterpret_cast<Manager *>(user_data); + auto *d = manager->d.get(); + const auto jid = extractJid(*address); + const auto deviceId = int(address->device_id); + auto &device = d->devices[jid][deviceId]; + if (!device.session.isEmpty()) { + device.session.clear(); + d->omemoStorage->addDevice(jid, deviceId, device); + } + return 1; + }; + + store.delete_all_sessions_func = [](const char *name, size_t name_len, void *user_data) { + const auto *manager = reinterpret_cast<Manager *>(user_data); + auto *d = manager->d.get(); + const auto jid = QString::fromUtf8(name, name_len); + auto deletedSessionsCount = 0; + auto &userDevices = d->devices[jid]; + for (auto itr = userDevices.begin(); itr != userDevices.end(); ++itr) { + const auto &deviceId = itr.key(); + auto &device = itr.value(); + if (!device.session.isEmpty()) { + device.session.clear(); + d->omemoStorage->addDevice(jid, deviceId, device); + ++deletedSessionsCount; + } + } + return deletedSessionsCount; + }; + + store.destroy_func = [](void *) { + }; + + store.user_data = q; + + return store; +} + +// +// Sets up the device ID. +// +// The more devices a user has, the higher the possibility of duplicate device IDs is. +// Especially for IoT scenarios with millions of devices, that can be an issue. +// Therefore, a new device ID is generated in case of a duplicate. +// +// \return whether it succeeded +// +QFuture<bool> ManagerPrivate::setUpDeviceId() +{ + QFutureInterface<bool> interface(QFutureInterfaceBase::Started); + + auto future = pubSubManager->requestPepItemIds(ns_omemo_2_bundles); + await(future, q, [=](QXmppPubSubManager::ItemIdsResult result) mutable { + if (auto error = std::get_if<Error>(&result)) { + warning("Existing / Published device IDs could not be retrieved"); + reportFinishedResult(interface, false); + } else { + const auto &deviceIds = std::get<QVector<QString>>(result); + + while (true) { + uint32_t deviceId = 0; + if (signal_protocol_key_helper_generate_registration_id(&deviceId, 0, globalContext.get()) < 0) { + warning("Device ID could not be generated"); + reportFinishedResult(interface, false); + break; + } + + if (!deviceIds.contains(QString::number(deviceId))) { + ownDevice.id = deviceId; + reportFinishedResult(interface, true); + break; + } + } + } + }); + + return interface.future(); +} + +// +// Sets up an identity key pair. +// +// The identity key pair consists of a private and a public long-term key. +// +// \return whether it succeeded +// +bool ManagerPrivate::setUpIdentityKeyPair(ratchet_identity_key_pair **identityKeyPair) +{ + if (signal_protocol_key_helper_generate_identity_key_pair(identityKeyPair, globalContext.get()) < 0) { + warning("Identity key pair could not be generated"); + return false; + } + + BufferSecurePtr privateIdentityKeyBuffer; + + if (ec_private_key_serialize(privateIdentityKeyBuffer.ptrRef(), ratchet_identity_key_pair_get_private(*identityKeyPair)) < 0) { + warning("Private identity key could not be serialized"); + return false; + } + + const auto privateIdentityKey = privateIdentityKeyBuffer.toByteArray(); + ownDevice.privateIdentityKey = privateIdentityKey; + + BufferPtr publicIdentityKeyBuffer; + + if (ec_public_key_serialize(publicIdentityKeyBuffer.ptrRef(), ratchet_identity_key_pair_get_public(*identityKeyPair)) < 0) { + warning("Public identity key could not be serialized"); + return false; + } + + const auto publicIdentityKey = publicIdentityKeyBuffer.toByteArray(); + deviceBundle.setPublicIdentityKey(publicIdentityKey); + ownDevice.publicIdentityKey = publicIdentityKey; + storeOwnKey(); + + return true; +} + +// +// Schedules periodic (time-based) tasks that cannot be done on a specific event. +// +void ManagerPrivate::schedulePeriodicTasks() +{ + QObject::connect(&signedPreKeyPairsRenewalTimer, &QTimer::timeout, q, [=]() mutable { + renewSignedPreKeyPairs(); + }); + + QObject::connect(&deviceRemovalTimer, &QTimer::timeout, q, [=]() mutable { + removeDevicesRemovedFromServer(); + }); + + signedPreKeyPairsRenewalTimer.start(SIGNED_PRE_KEY_RENEWAL_CHECK_INTERVAL); + deviceRemovalTimer.start(DEVICE_REMOVAL_CHECK_INTERVAL); +} + +// +// Removes old signed pre key pairs and creates a new one. +// +void ManagerPrivate::renewSignedPreKeyPairs() +{ + const auto currentDate = QDateTime::currentDateTimeUtc().toSecsSinceEpoch() * 1s; + auto isSignedPreKeyPairRemoved = false; + + for (auto itr = signedPreKeyPairs.begin(); itr != signedPreKeyPairs.end();) { + const auto creationDate = itr.value().creationDate.toSecsSinceEpoch() * 1s; + + // Remove signed pre key pairs older than + // SIGNED_PRE_KEY_RENEWAL_INTERVAL. + if (currentDate - creationDate > SIGNED_PRE_KEY_RENEWAL_INTERVAL) { + itr = signedPreKeyPairs.erase(itr); + omemoStorage->removeSignedPreKeyPair(itr.key()); + isSignedPreKeyPairRemoved = true; + } else { + ++itr; + } + } + + if (isSignedPreKeyPairRemoved) { + RefCountedPtr<ratchet_identity_key_pair> identityKeyPair; + generateIdentityKeyPair(identityKeyPair.ptrRef()); + updateSignedPreKeyPair(identityKeyPair.get()); + + // Store the own device containing the new signed pre key ID. + omemoStorage->setOwnDevice(ownDevice); + + publishDeviceBundleItem([=](bool isPublished) { + if (!isPublished) { + warning("Own device bundle item could not be published during renewal of signed pre key pairs"); + } + }); + } +} + +// +// Updates the signed pre key pairs. +// +// Make sure that +// \code +// d->omemoStorage->setOwnDevice(d->ownDevice); +// \endcode +// is called afterwards to store the change of +// \code +// d->ownDevice.latestSignedPreKeyId() +// \endcode +// . +// +// \return whether it succeeded +// +bool ManagerPrivate::updateSignedPreKeyPair(ratchet_identity_key_pair *identityKeyPair) +{ + RefCountedPtr<session_signed_pre_key> signedPreKeyPair; + auto latestSignedPreKeyId = ownDevice.latestSignedPreKeyId; + + // Ensure that no signed pre key ID exceeds SIGNED_PRE_KEY_ID_MAX + // Do not increment during setup. + if (latestSignedPreKeyId + 1 > SIGNED_PRE_KEY_ID_MAX) { + latestSignedPreKeyId = SIGNED_PRE_KEY_ID_MIN; + } else if (latestSignedPreKeyId != SIGNED_PRE_KEY_ID_MIN) { + ++latestSignedPreKeyId; + } + + if (signal_protocol_key_helper_generate_signed_pre_key( + signedPreKeyPair.ptrRef(), + identityKeyPair, + latestSignedPreKeyId, + uint64_t(QDateTime::currentMSecsSinceEpoch()), + globalContext.get()) < 0) { + warning("Signed pre key pair could not be generated"); + return false; + } + + BufferSecurePtr signedPreKeyPairBuffer; + + if (session_signed_pre_key_serialize(signedPreKeyPairBuffer.ptrRef(), signedPreKeyPair.get()) < 0) { + warning("Signed pre key pair could not be serialized"); + return false; + } + + QXmppOmemoStorage::SignedPreKeyPair signedPreKeyPairForStorage; + signedPreKeyPairForStorage.creationDate = QDateTime::currentDateTimeUtc(); + signedPreKeyPairForStorage.data = signedPreKeyPairBuffer.toByteArray(); + + signedPreKeyPairs.insert(latestSignedPreKeyId, signedPreKeyPairForStorage); + omemoStorage->addSignedPreKeyPair(latestSignedPreKeyId, signedPreKeyPairForStorage); + + BufferPtr signedPublicPreKeyBuffer; + + if (ec_public_key_serialize(signedPublicPreKeyBuffer.ptrRef(), ec_key_pair_get_public(session_signed_pre_key_get_key_pair(signedPreKeyPair.get()))) < 0) { + warning("Signed public pre key could not be serialized"); + return false; + } + + const auto signedPublicPreKeyByteArray = signedPublicPreKeyBuffer.toByteArray(); + + deviceBundle.setSignedPublicPreKeyId(latestSignedPreKeyId); + deviceBundle.setSignedPublicPreKey(signedPublicPreKeyByteArray); + deviceBundle.setSignedPublicPreKeySignature(QByteArray(reinterpret_cast<const char *>(session_signed_pre_key_get_signature(signedPreKeyPair.get())), session_signed_pre_key_get_signature_len(signedPreKeyPair.get()))); + + ownDevice.latestSignedPreKeyId = latestSignedPreKeyId; + + return true; +} + +// +// Deletes a pre key pair and creates a new one. +// +// \param keyPairBeingRenewed key pair being replaced by a new one +// +// \return whether it succeeded +// +bool ManagerPrivate::renewPreKeyPairs(uint32_t keyPairBeingRenewed) +{ + preKeyPairs.remove(keyPairBeingRenewed); + omemoStorage->removePreKeyPair(keyPairBeingRenewed); + deviceBundle.removePublicPreKey(keyPairBeingRenewed); + + if (!updatePreKeyPairs()) { + return false; + } + + // Store the own device containing the new pre key ID. + omemoStorage->setOwnDevice(ownDevice); + + publishDeviceBundleItem([=](bool isPublished) { + if (!isPublished) { + warning("Own device bundle item could not be published during renewal of pre key pairs"); + } + }); + + return true; +} + +// +// Updates the pre key pairs locally. +// +// Make sure that +// \code +// d->omemoStorage->setOwnDevice(d->ownDevice) +// \endcode +// is called +// afterwards to store the change of +// \code +// d->ownDevice.latestPreKeyId() +// \endcode +// . +// +// \param count number of pre key pairs to update +// +// \return whether it succeeded +// +bool ManagerPrivate::updatePreKeyPairs(uint32_t count) +{ + KeyListNodePtr newPreKeyPairs; + auto latestPreKeyId = ownDevice.latestPreKeyId; + + // Ensure that no pre key ID exceeds PRE_KEY_ID_MAX. + // Do not increment during setup. + if (latestPreKeyId + count > PRE_KEY_ID_MAX) { + latestPreKeyId = PRE_KEY_ID_MIN; + } else if (latestPreKeyId != PRE_KEY_ID_MIN) { + ++latestPreKeyId; + } + + if (signal_protocol_key_helper_generate_pre_keys(newPreKeyPairs.ptrRef(), latestPreKeyId, count, globalContext.get()) < 0) { + warning("Pre key pairs could not be generated"); + return false; + } + + QHash<uint32_t, QByteArray> serializedPreKeyPairs; + + for (auto *node = newPreKeyPairs.get(); + node != nullptr; + node = signal_protocol_key_helper_key_list_next(node)) { + BufferSecurePtr preKeyPairBuffer; + BufferPtr publicPreKeyBuffer; + + auto preKeyPair = signal_protocol_key_helper_key_list_element(node); + + if (session_pre_key_serialize(preKeyPairBuffer.ptrRef(), preKeyPair) < 0) { + warning("Pre key pair could not be serialized"); + return false; + } + + const auto preKeyId = session_pre_key_get_id(preKeyPair); + + serializedPreKeyPairs.insert(preKeyId, preKeyPairBuffer.toByteArray()); + + if (ec_public_key_serialize(publicPreKeyBuffer.ptrRef(), ec_key_pair_get_public(session_pre_key_get_key_pair(preKeyPair))) < 0) { + warning("Public pre key could not be serialized"); + return false; + } + + const auto serializedPublicPreKey = publicPreKeyBuffer.toByteArray(); + deviceBundle.addPublicPreKey(preKeyId, serializedPublicPreKey); + } + + this->preKeyPairs.insert(serializedPreKeyPairs); + omemoStorage->addPreKeyPairs(serializedPreKeyPairs); + ownDevice.latestPreKeyId = latestPreKeyId - 1 + count; + + return true; +} + +// +// Removes locally stored devices after a specific time if they are removed from their owners' +// device lists on their servers. +// +void ManagerPrivate::removeDevicesRemovedFromServer() +{ + const auto currentDate = QDateTime::currentDateTimeUtc().toSecsSinceEpoch() * 1s; + + for (auto itr = devices.begin(); itr != devices.end(); ++itr) { + const auto &jid = itr.key(); + auto &userDevices = itr.value(); + + for (auto devicesItr = userDevices.begin(); devicesItr != userDevices.end();) { + const auto &deviceId = devicesItr.key(); + const auto &device = devicesItr.value(); + + // Remove data for devices removed from their servers after + // DEVICE_REMOVAL_INTERVAL. + const auto &removalDate = device.removalFromDeviceListDate; + if (!removalDate.isNull() && + currentDate - removalDate.toSecsSinceEpoch() * 1s > DEVICE_REMOVAL_INTERVAL) { + devicesItr = userDevices.erase(devicesItr); + omemoStorage->removeDevice(jid, deviceId); + trustManager->removeKeys(ns_omemo_2, QList { device.keyId }); + emit q->deviceRemoved(jid, deviceId); + } else { + ++devicesItr; + } + } + } +} + +// +// Generates an identity key pair. +// +// The identity key pair is the pair of private and a public long-term key. +// +// \param identityKeyPair identity key pair location +// +// \return whether it succeeded +// +bool ManagerPrivate::generateIdentityKeyPair(ratchet_identity_key_pair **identityKeyPair) const +{ + BufferSecurePtr privateIdentityKeyBuffer = BufferSecurePtr::fromByteArray(ownDevice.privateIdentityKey); + + if (!privateIdentityKeyBuffer) { + warning("Buffer for serialized private identity key could not be created"); + return false; + } + + RefCountedPtr<ec_private_key> privateIdentityKey; + + if (curve_decode_private_point(privateIdentityKey.ptrRef(), signal_buffer_data(privateIdentityKeyBuffer.get()), signal_buffer_len(privateIdentityKeyBuffer.get()), globalContext.get()) < 0) { + warning("Private identity key could not be deserialized"); + return false; + } + + const auto &serializedPublicIdentityKey = ownDevice.publicIdentityKey; + BufferPtr publicIdentityKeyBuffer = BufferPtr::fromByteArray(serializedPublicIdentityKey); + + if (!publicIdentityKeyBuffer) { + warning("Buffer for serialized public identity key could not be created"); + return false; + } + + RefCountedPtr<ec_public_key> publicIdentityKey; + + if (curve_decode_point(publicIdentityKey.ptrRef(), signal_buffer_data(publicIdentityKeyBuffer.get()), signal_buffer_len(publicIdentityKeyBuffer.get()), globalContext.get()) < 0) { + warning("Public identity key could not be deserialized"); + return false; + } + + if (ratchet_identity_key_pair_create(identityKeyPair, publicIdentityKey.get(), privateIdentityKey.get()) < 0) { + warning("Identity key pair could not be deserialized"); + return false; + } + + return true; +} + +// +// Encrypts a message for specific recipients. +// +// \param message message to be encrypted +// \param recipientJids JIDs for whom the message is encrypted +// \param acceptedTrustLevels trust levels the keys of the recipients' devices must have to +// encrypt for them +// +// \return the result of the encryption +// +QFuture<QXmppE2eeExtension::MessageEncryptResult> ManagerPrivate::encryptMessageForRecipients(QXmppMessage &&message, QVector<QString> recipientJids, TrustLevels acceptedTrustLevels) +{ + QFutureInterface<QXmppE2eeExtension::MessageEncryptResult> interface(QFutureInterfaceBase::Started); + + if (!isStarted) { + QXmpp::SendError error = { QStringLiteral("OMEMO manager must be started before encrypting"), QXmpp::SendError::EncryptionError }; + reportFinishedResult(interface, { error }); + } else { + recipientJids.append(ownBareJid()); + + auto future = encryptStanza(message, recipientJids, acceptedTrustLevels); + await(future, q, [=, message = std::move(message)](std::optional<QXmppOmemoElement> omemoElement) mutable { + if (!omemoElement) { + QXmpp::SendError error; + error.text = QStringLiteral("OMEMO element could not be created"); + error.type = QXmpp::SendError::EncryptionError; + reportFinishedResult(interface, { error }); + } else { + const auto areDeliveryReceiptsUsed = message.isReceiptRequested() || !message.receiptId().isEmpty(); + + // The following cases are covered: + // 1. Message with body (possibly including a chat state or used + // for delivery receipts) => usage of EME and fallback body + // 2. Message without body + // 2.1. Message with chat state or used for delivery receipts + // => neither usage of EME nor fallback body, but hint for + // server-side storage in case of delivery receipts usage + // 2.2. Other message (e.g., trust message) => usage of EME and + // fallback body to look like a normal message + if (!message.body().isEmpty() || (message.state() == QXmppMessage::None && !areDeliveryReceiptsUsed)) { + message.setEncryptionMethod(QXmpp::Omemo2); + + // A message processing hint for instructing the server to + // store the message is not needed because of the public + // fallback body. + message.setE2eeFallbackBody(QStringLiteral("This message is encrypted with %1 but could not be decrypted").arg(message.encryptionName())); + message.setIsFallback(true); + } else if (areDeliveryReceiptsUsed) { + // A message processing hint for instructing the server to + // store the message is needed because of the missing public + // fallback body. + message.addHint(QXmppMessage::Store); + } + + message.setOmemoElement(omemoElement); + + QByteArray serializedEncryptedMessage; + QXmlStreamWriter writer(&serializedEncryptedMessage); + message.toXml(&writer, QXmpp::ScePublic); + + reportFinishedResult(interface, { serializedEncryptedMessage }); + } + }); + } + + return interface.future(); +} + +// +// Encrypts a message or IQ stanza. +// +// \param stanza stanza to be encrypted +// \param recipientJids JIDs of the devices for whom the stanza is encrypted +// \param acceptedTrustLevels trust levels the keys of the recipients' devices must have to +// encrypt for them +// +// \return the OMEMO element containing the stanza's encrypted content if the encryption is +// successful, otherwise none +// +template<typename T> +QFuture<std::optional<QXmppOmemoElement>> ManagerPrivate::encryptStanza(const T &stanza, const QVector<QString> &recipientJids, TrustLevels acceptedTrustLevels) +{ + Q_ASSERT_X(!recipientJids.isEmpty(), "Creating OMEMO envelope", "OMEMO element could not be created because no recipient JIDs are passed"); + + QFutureInterface<std::optional<QXmppOmemoElement>> interface(QFutureInterfaceBase::Started); + + if (const auto optionalPayloadEncryptionResult = encryptPayload(createSceEnvelope(stanza))) { + const auto &payloadEncryptionResult = *optionalPayloadEncryptionResult; + + auto devicesCount = std::accumulate(recipientJids.cbegin(), recipientJids.cend(), 0, [=](const auto sum, const auto &jid) { + return sum + devices.value(jid).size(); + }); + + // Do not exceed the maximum of manageable devices. + if (devicesCount > maximumDevicesPerStanza) { + warning(u"OMEMO payload could not be encrypted for all recipients because their " + "devices are altogether more than the maximum of manageable devices " % + QString::number(maximumDevicesPerStanza) % + u" - Use QXmppOmemoManager::setMaximumDevicesPerStanza() to increase the maximum"); + devicesCount = maximumDevicesPerStanza; + } + + if (devicesCount) { + auto omemoElement = std::make_shared<QXmppOmemoElement>(); + auto processedDevicesCount = std::make_shared<int>(0); + auto successfullyProcessedDevicesCount = std::make_shared<int>(0); + auto skippedDevicesCount = std::make_shared<int>(0); + + // Add envelopes for all devices of the recipients. + for (const auto &jid : recipientJids) { + auto recipientDevices = devices.value(jid); + + for (auto itr = recipientDevices.begin(); itr != recipientDevices.end(); ++itr) { + const auto &deviceId = itr.key(); + const auto &device = itr.value(); + + // Skip encrypting for a device if it does not respond for a while. + if (const auto unrespondedSentStanzasCount = device.unrespondedSentStanzasCount; unrespondedSentStanzasCount == UNRESPONDED_STANZAS_UNTIL_ENCRYPTION_IS_STOPPED) { + if (++(*skippedDevicesCount) == devicesCount) { + warning("OMEMO element could not be created because no recipient device responded to " % + QString::number(unrespondedSentStanzasCount) % " sent stanzas"); + reportFinishedResult(interface, {}); + } + + continue; + } + + auto controlDeviceProcessing = [=](bool isSuccessful = true) mutable { + if (isSuccessful) { + ++(*successfullyProcessedDevicesCount); + } + + if (++(*processedDevicesCount) == devicesCount) { + if (*successfullyProcessedDevicesCount == 0) { + warning("OMEMO element could not be created because no recipient " + "devices with keys having accepted trust levels could be found"); + reportFinishedResult(interface, {}); + } else { + omemoElement->setSenderDeviceId(ownDevice.id); + omemoElement->setPayload(payloadEncryptionResult.encryptedPayload); + reportFinishedResult(interface, { *omemoElement }); + } + } + }; + + const auto address = Address(jid, deviceId); + + auto addOmemoEnvelope = [=](bool isKeyExchange = false) mutable { + // Create and add an OMEMO envelope only if its data could be created + // and the corresponding device has not been removed by another method + // in the meantime. + if (const auto data = createOmemoEnvelopeData(address.data(), payloadEncryptionResult.decryptionData); data.isEmpty()) { + warning("OMEMO envelope for recipient JID '" % jid % + "' and device ID '" % QString::number(deviceId) % + "' could not be created because its data could not be encrypted"); + controlDeviceProcessing(false); + } else if (devices.value(jid).contains(deviceId)) { + auto &deviceBeingModified = devices[jid][deviceId]; + deviceBeingModified.unrespondedReceivedStanzasCount = 0; + ++deviceBeingModified.unrespondedSentStanzasCount; + omemoStorage->addDevice(jid, deviceId, deviceBeingModified); + + QXmppOmemoEnvelope omemoEnvelope; + omemoEnvelope.setRecipientDeviceId(deviceId); + if (isKeyExchange) { + omemoEnvelope.setIsUsedForKeyExchange(true); + } + omemoEnvelope.setData(data); + omemoElement->addEnvelope(jid, omemoEnvelope); + controlDeviceProcessing(); + } + }; + + auto buildSessionDependingOnTrustLevel = [=](const QXmppOmemoDeviceBundle &deviceBundle, TrustLevel trustLevel) mutable { + // Build a session if the device's key has a specific trust level. + if (!acceptedTrustLevels.testFlag(trustLevel)) { + q->debug("Session could not be created for JID '" % jid % + "' with device ID '" % QString::number(deviceId) % + "' because its key's trust level '" % + QString::number(int(trustLevel)) % "' is not accepted"); + controlDeviceProcessing(false); + } else if (!buildSession(address.data(), deviceBundle)) { + warning("Session could not be created for JID '" % jid % "' and device ID '" % QString::number(deviceId) % "'"); + controlDeviceProcessing(false); + } else { + addOmemoEnvelope(true); + } + }; + + // If the key ID is not stored (empty), the device bundle must be retrieved + // first. + // Afterwards, the bundle can be used to determine the key's trust level and + // to build the session. + // If the key ID is stored (not empty), the trust level can be directly + // determined and the session built. + if (device.keyId.isEmpty()) { + auto future = requestDeviceBundle(jid, deviceId); + await(future, q, [=](std::optional<QXmppOmemoDeviceBundle> optionalDeviceBundle) mutable { + // Process the device bundle only if one could be fetched and the + // corresponding device has not been removed by another method in + // the meantime. + if (optionalDeviceBundle && devices.value(jid).contains(deviceId)) { + auto &deviceBeingModified = devices[jid][deviceId]; + const auto &deviceBundle = *optionalDeviceBundle; + const auto key = deviceBundle.publicIdentityKey(); + deviceBeingModified.keyId = createKeyId(key); + + auto future = q->trustLevel(jid, deviceBeingModified.keyId); + await(future, q, [=](TrustLevel trustLevel) mutable { + // Store the retrieved key's trust level if it is not stored + // yet. + if (trustLevel == TrustLevel::Undecided) { + auto future = storeKeyDependingOnSecurityPolicy(jid, key); + await(future, q, [=](TrustLevel trustLevel) mutable { + omemoStorage->addDevice(jid, deviceId, deviceBeingModified); + emit q->deviceChanged(jid, deviceId); + buildSessionDependingOnTrustLevel(deviceBundle, trustLevel); + }); + } else { + omemoStorage->addDevice(jid, deviceId, deviceBeingModified); + emit q->deviceChanged(jid, deviceId); + buildSessionDependingOnTrustLevel(deviceBundle, trustLevel); + } + }); + } else { + warning("OMEMO envelope could not be created because no device bundle could be fetched"); + controlDeviceProcessing(false); + } + }); + } else { + auto future = q->trustLevel(jid, device.keyId); + await(future, q, [=](TrustLevel trustLevel) mutable { + // Create only OMEMO envelopes for devices that have keys with + // specific trust levels. + if (acceptedTrustLevels.testFlag(trustLevel)) { + // Build a new session if none is stored. + // Otherwise, use the existing session. + if (device.session.isEmpty()) { + auto future = requestDeviceBundle(jid, deviceId); + await(future, q, [=](std::optional<QXmppOmemoDeviceBundle> optionalDeviceBundle) mutable { + if (optionalDeviceBundle) { + const auto &deviceBundle = *optionalDeviceBundle; + buildSessionDependingOnTrustLevel(deviceBundle, trustLevel); + } else { + warning("OMEMO envelope could not be created because no device bundle could be fetched"); + controlDeviceProcessing(false); + } + }); + } else { + addOmemoEnvelope(); + } + } else { + q->debug("OMEMO envelope could not be created for JID '" % jid % + "' and device ID '" % QString::number(deviceId) % + "' because the device's key has an unaccepted trust level '" % + QString::number(int(trustLevel)) % "'"); + controlDeviceProcessing(false); + } + }); + } + } + } + } else { + warning("OMEMO element could not be created because no recipient devices could be found"); + reportFinishedResult(interface, {}); + } + } else { + warning("OMEMO payload could not be encrypted"); + reportFinishedResult(interface, {}); + } + + return interface.future(); +} + +template QFuture<std::optional<QXmppOmemoElement>> ManagerPrivate::encryptStanza<QXmppIq>(const QXmppIq &, const QVector<QString> &, TrustLevels); +template QFuture<std::optional<QXmppOmemoElement>> ManagerPrivate::encryptStanza<QXmppMessage>(const QXmppMessage &, const QVector<QString> &, TrustLevels); + +// +// Encrypts a payload symmetrically. +// +// \param payload payload being symmetrically encrypted +// +// \return the data used for encryption and the result +// +std::optional<PayloadEncryptionResult> ManagerPrivate::encryptPayload(const QByteArray &payload) const +{ + auto hkdfKey = QCA::SecureArray(QCA::Random::randomArray(HKDF_KEY_SIZE)); + const auto hkdfSalt = QCA::InitializationVector(QCA::SecureArray(HKDF_SALT_SIZE)); + const auto hkdfInfo = QCA::InitializationVector(QCA::SecureArray(HKDF_INFO)); + auto hkdfOutput = QCA::HKDF().makeKey(hkdfKey, hkdfSalt, hkdfInfo, HKDF_OUTPUT_SIZE); + + // first part of hkdfKey + auto encryptionKey = QCA::SymmetricKey(hkdfOutput); + encryptionKey.resize(PAYLOAD_KEY_SIZE); + + // middle part of hkdfKey + auto authenticationKey = QCA::SymmetricKey(PAYLOAD_AUTHENTICATION_KEY_SIZE); + const auto authenticationKeyOffset = hkdfOutput.data() + PAYLOAD_KEY_SIZE; + std::copy(authenticationKeyOffset, authenticationKeyOffset + PAYLOAD_AUTHENTICATION_KEY_SIZE, authenticationKey.data()); + + // last part of hkdfKey + auto initializationVector = QCA::InitializationVector(PAYLOAD_INITIALIZATION_VECTOR_SIZE); + const auto initializationVectorOffset = hkdfOutput.data() + PAYLOAD_KEY_SIZE + PAYLOAD_AUTHENTICATION_KEY_SIZE; + std::copy(initializationVectorOffset, initializationVectorOffset + PAYLOAD_INITIALIZATION_VECTOR_SIZE, initializationVector.data()); + + QCA::Cipher cipher(PAYLOAD_CIPHER_TYPE, PAYLOAD_CIPHER_MODE, PAYLOAD_CIPHER_PADDING, QCA::Encode, encryptionKey, initializationVector); + auto encryptedPayload = cipher.process(QCA::MemoryRegion(payload)); + + if (encryptedPayload.isEmpty()) { + warning("Following payload could not be encrypted: " % QString::fromUtf8(payload)); + return {}; + } + + if (!QCA::MessageAuthenticationCode::supportedTypes().contains(PAYLOAD_MESSAGE_AUTHENTICATION_CODE_TYPE)) { + warning("Message authentication code type '" % QString(PAYLOAD_MESSAGE_AUTHENTICATION_CODE_TYPE) % "' is not supported by this system"); + return {}; + } + + auto messageAuthenticationCodeGenerator = QCA::MessageAuthenticationCode(PAYLOAD_MESSAGE_AUTHENTICATION_CODE_TYPE, authenticationKey); + auto messageAuthenticationCode = QCA::SecureArray(messageAuthenticationCodeGenerator.process(encryptedPayload)); + messageAuthenticationCode.resize(PAYLOAD_MESSAGE_AUTHENTICATION_CODE_SIZE); + + PayloadEncryptionResult payloadEncryptionData; + payloadEncryptionData.decryptionData = hkdfKey.append(messageAuthenticationCode); + payloadEncryptionData.encryptedPayload = encryptedPayload.toByteArray(); + + return payloadEncryptionData; +} + +// +// Creates the SCE envelope as defined in \xep{0420, Stanza Content Encryption} for a message +// or IQ stanza. +// +// The stanza's content that should be encrypted is put into the SCE content and that is added +// to the SCE envelope. +// Additionally, the standard SCE affix elements are added to the SCE envelope. +// +// \param stanza stanza for whom the SCE envelope is created +// +// \return the serialized SCE envelope +// +template<typename T> +QByteArray ManagerPrivate::createSceEnvelope(const T &stanza) +{ + QByteArray serializedSceEnvelope; + QXmlStreamWriter writer(&serializedSceEnvelope); + QXmppSceEnvelopeWriter sceEnvelopeWriter(writer); + sceEnvelopeWriter.start(); + sceEnvelopeWriter.writeTimestamp(QDateTime::currentDateTimeUtc()); + sceEnvelopeWriter.writeTo(QXmppUtils::jidToBareJid(stanza.to())); + sceEnvelopeWriter.writeFrom(q->client()->configuration().jidBare()); + sceEnvelopeWriter.writeRpad(generateRandomBytes(SCE_RPAD_SIZE_MIN, SCE_RPAD_SIZE_MAX).toBase64()); + sceEnvelopeWriter.writeContent([&writer, &stanza] { + if constexpr (std::is_same_v<T, QXmppMessage>) { + stanza.serializeExtensions(&writer, SceSensitive, ns_client); + } else { + // If the IQ stanza contains an error (i.e., it is an error response), that error is + // serialized instead of actual content. + const auto error = stanza.error(); + if (error.typeOpt()) { + error.toXml(&writer); + } else { + stanza.toXmlElementFromChild(&writer); + } + } + }); + sceEnvelopeWriter.end(); + + return serializedSceEnvelope; +} + +// +// Creates the data of an OMEMO envelope. +// +// Encrypts the data used for a symmetric encryption of a payload asymmetrically with the +// recipient device's key. +// +// \param address address of a recipient device +// \param payloadDecryptionData data used for symmetric encryption being asymmetrically +// encrypted +// +// \return the encrypted and serialized OMEMO envelope data or a default-constructed byte array +// on failure +// +QByteArray ManagerPrivate::createOmemoEnvelopeData(const signal_protocol_address &address, const QCA::SecureArray &payloadDecryptionData) const +{ + SessionCipherPtr sessionCipher; + + if (session_cipher_create(sessionCipher.ptrRef(), storeContext.get(), &address, globalContext.get()) < 0) { + warning("Session cipher could not be created"); + return {}; + } + + session_cipher_set_version(sessionCipher.get(), CIPHERTEXT_OMEMO_VERSION); + + RefCountedPtr<ciphertext_message> encryptedOmemoEnvelopeData; + if (session_cipher_encrypt(sessionCipher.get(), reinterpret_cast<const uint8_t *>(payloadDecryptionData.constData()), payloadDecryptionData.size(), encryptedOmemoEnvelopeData.ptrRef()) != SG_SUCCESS) { + warning("Payload decryption data could not be encrypted"); + return {}; + } + + signal_buffer *serializedEncryptedOmemoEnvelopeData = ciphertext_message_get_serialized(encryptedOmemoEnvelopeData.get()); + + return { + reinterpret_cast<const char *>(signal_buffer_data(serializedEncryptedOmemoEnvelopeData)), + int(signal_buffer_len(serializedEncryptedOmemoEnvelopeData)) + }; +} + +// +// Decrypts a message stanza. +// +// In case of an empty (i.e., without payload) OMEMO message for session initiation, only the +// dummy payload's decryption data is decrypted but no payload. +// In case of a normal OMEMO message (i.e., with payload), the payload is decrypted and set as +// the content (i.e., first child element) of the returned stanza. +// +// \param stanza message stanza to be decrypted +// +// \return the decrypted stanza if it could be decrypted +// +QFuture<std::optional<QXmppMessage>> ManagerPrivate::decryptMessage(QXmppMessage stanza) +{ + QFutureInterface<std::optional<QXmppMessage>> interface(QFutureInterfaceBase::Started); + + // At this point, the stanza has always an OMEMO element. + const auto omemoElement = *stanza.omemoElement(); + + if (auto optionalOmemoEnvelope = omemoElement.searchEnvelope(ownBareJid(), ownDevice.id)) { + const auto senderJid = QXmppUtils::jidToBareJid(stanza.from()); + const auto senderDeviceId = omemoElement.senderDeviceId(); + const auto omemoEnvelope = *optionalOmemoEnvelope; + const auto omemoPayload = omemoElement.payload(); + + subscribeToNewDeviceLists(senderJid, senderDeviceId); + + // Process empty OMEMO messages sent by a receiver of this device's first OMEMO message + // for it after building the initial session or sent by devices to build a new session + // with this device. + if (omemoPayload.isEmpty()) { + auto future = extractPayloadDecryptionData(senderJid, senderDeviceId, omemoEnvelope); + await(future, q, [=](QCA::SecureArray payloadDecryptionData) mutable { + if (payloadDecryptionData.isEmpty()) { + warning("Empty OMEMO message could not be successfully processed"); + } else { + q->debug("Successfully processed empty OMEMO message"); + } + + reportFinishedResult(interface, {}); + }); + } else { + auto future = decryptStanza(stanza, senderJid, senderDeviceId, omemoEnvelope, omemoPayload); + await(future, q, [=](std::optional<DecryptionResult> optionalDecryptionResult) mutable { + if (optionalDecryptionResult) { + const auto decryptionResult = std::move(*optionalDecryptionResult); + stanza.parseExtensions(decryptionResult.sceContent, SceSensitive); + + // Remove the OMEMO element from the message because it is not needed + // anymore after decryption. + stanza.setOmemoElement({}); + + stanza.setE2eeMetadata(decryptionResult.e2eeMetadata); + + reportFinishedResult(interface, { stanza }); + } else { + reportFinishedResult(interface, {}); + } + }); + } + } + + return interface.future(); +} + +// +// Decrypts an IQ stanza. +// +// The payload is decrypted and set as the content (i.e., first child element) of the returned +// stanza. +// +// \param iqElement DOM element of the IQ stanza to be decrypted. It MUST be an QXmppOmemoIq. +// +// \return the serialized decrypted stanza if it could be decrypted +// +QFuture<std::optional<IqDecryptionResult>> ManagerPrivate::decryptIq(const QDomElement &iqElement) +{ + using Result = std::optional<IqDecryptionResult>; + + QXmppOmemoIq iq; + iq.parse(iqElement); + auto omemoElement = iq.omemoElement(); + + if (const auto envelope = omemoElement.searchEnvelope(ownBareJid(), ownDevice.id)) { + const auto senderJid = QXmppUtils::jidToBareJid(iq.from()); + const auto senderDeviceId = omemoElement.senderDeviceId(); + + subscribeToNewDeviceLists(senderJid, senderDeviceId); + + auto future = decryptStanza(iq, senderJid, senderDeviceId, *envelope, omemoElement.payload(), false); + return chain<Result>(future, q, [iqElement](auto result) -> Result { + if (result) { + auto decryptedElement = iqElement.cloneNode(true).toElement(); + replaceChildElements(decryptedElement, result->sceContent); + + return IqDecryptionResult { decryptedElement, result->e2eeMetadata }; + } + return {}; + }); + } + return makeReadyFuture<Result>(std::nullopt); +} + +// +// Decrypts a message or IQ stanza. +// +// In case of an empty (i.e., without payload) OMEMO message for session initiation, only the +// dummy payload decryption data is decrypted but no payload. +// In case of a normal OMEMO stanza (i.e., with payload), the payload is decrypted and set as +// the content (i.e., first child element) of the returned stanza. +// +// \param stanza message or IQ stanza being decrypted +// \param senderJid JID of the stanza's sender +// \param senderDeviceId device ID of the stanza's sender +// \param omemoEnvelope OMEMO envelope within the OMEMO element +// \param omemoPayload OMEMO payload within the OMEMO element +// \param isMessageStanza whether the received stanza is a message stanza +// +// \return the result of the decryption if it succeeded +// +template<typename T> +QFuture<std::optional<DecryptionResult>> ManagerPrivate::decryptStanza(T stanza, const QString &senderJid, uint32_t senderDeviceId, const QXmppOmemoEnvelope &omemoEnvelope, const QByteArray &omemoPayload, bool isMessageStanza) +{ + QFutureInterface<std::optional<DecryptionResult>> interface(QFutureInterfaceBase::Started); + + auto future = extractSceEnvelope(senderJid, senderDeviceId, omemoEnvelope, omemoPayload, isMessageStanza); + await(future, q, [=](QByteArray serializedSceEnvelope) mutable { + if (serializedSceEnvelope.isEmpty()) { + warning("SCE envelope could not be extracted"); + reportFinishedResult(interface, {}); + } else { + QDomDocument document; + document.setContent(serializedSceEnvelope, true); + QXmppSceEnvelopeReader sceEnvelopeReader(document.documentElement()); + + if (sceEnvelopeReader.from() != senderJid) { + warning("Sender '" % senderJid % "' of stanza does not match SCE 'from' affix element '" % sceEnvelopeReader.from() % "'"); + reportFinishedResult(interface, {}); + } else { + const auto recipientJid = QXmppUtils::jidToBareJid(stanza.to()); + auto isSceAffixElementValid = true; + + if (isMessageStanza) { + if (const auto &message = dynamic_cast<const QXmppMessage &>(stanza); message.type() == QXmppMessage::GroupChat && (sceEnvelopeReader.to() != recipientJid)) { + warning("Recipient of group chat message does not match SCE affix element '<to/>'"); + isSceAffixElementValid = false; + } + } else { + if (sceEnvelopeReader.to() != recipientJid) { + warning("Recipient of IQ does not match SCE affix element '<to/>'"); + isSceAffixElementValid = false; + } + } + + if (!isSceAffixElementValid) { + reportFinishedResult(interface, {}); + } else { + auto &device = devices[senderJid][senderDeviceId]; + device.unrespondedSentStanzasCount = 0; + + // Send a heartbeat message to the sender if too many stanzas were + // received responding to none. + if (device.unrespondedReceivedStanzasCount == UNRESPONDED_STANZAS_UNTIL_HEARTBEAT_MESSAGE_IS_SENT) { + sendEmptyMessage(senderJid, senderDeviceId); + device.unrespondedReceivedStanzasCount = 0; + } else { + ++device.unrespondedReceivedStanzasCount; + } + + QXmppE2eeMetadata e2eeMetadata; + e2eeMetadata.setSceTimestamp(sceEnvelopeReader.timestamp()); + e2eeMetadata.setEncryption(QXmpp::Omemo2); + const auto &senderDevice = devices.value(senderJid).value(senderDeviceId); + e2eeMetadata.setSenderKey(senderDevice.keyId); + + reportFinishedResult(interface, { { sceEnvelopeReader.contentElement(), e2eeMetadata } }); + } + } + } + }); + + return interface.future(); +} + +// +// Extracts the SCE envelope from an OMEMO payload. +// +// The data used to encrypt the payload is decrypted and then used to decrypt the payload which +// contains the SCE envelope. +// +// \param senderJid bare JID of the stanza's sender +// \param senderDeviceId device ID of the stanza's sender +// \param omemoEnvelope OMEMO envelope containing the payload decryption data +// \param omemoPayload OMEMO payload containing the SCE envelope +// \param isMessageStanza whether the received stanza is a message stanza +// +// \return the serialized SCE envelope if it could be extracted, otherwise a +// default-constructed byte array +// +QFuture<QByteArray> ManagerPrivate::extractSceEnvelope(const QString &senderJid, uint32_t senderDeviceId, const QXmppOmemoEnvelope &omemoEnvelope, const QByteArray &omemoPayload, bool isMessageStanza) +{ + QFutureInterface<QByteArray> interface(QFutureInterfaceBase::Started); + + auto future = extractPayloadDecryptionData(senderJid, senderDeviceId, omemoEnvelope, isMessageStanza); + await(future, q, [=](QCA::SecureArray payloadDecryptionData) mutable { + if (payloadDecryptionData.isEmpty()) { + warning("Data for decrypting OMEMO payload could not be extracted"); + reportFinishedResult(interface, {}); + } else { + reportFinishedResult(interface, decryptPayload(payloadDecryptionData, omemoPayload)); + } + }); + + return interface.future(); +} + +// +// Extracts the data used to decrypt the OMEMO payload. +// +// Decrypts the the payload decryption data and handles the OMEMO sessions. +// +// \param senderJid bare JID of the stanza's sender +// \param senderDeviceId device ID of the stanza's sender +// \param omemoEnvelope OMEMO envelope containing the payload decryption data +// \param isMessageStanza whether the received stanza is a message stanza +// +// \return the serialized payload decryption data if it could be extracted, otherwise a +// default-constructed secure array +// +QFuture<QCA::SecureArray> ManagerPrivate::extractPayloadDecryptionData(const QString &senderJid, uint32_t senderDeviceId, const QXmppOmemoEnvelope &omemoEnvelope, bool isMessageStanza) +{ + QFutureInterface<QCA::SecureArray> interface(QFutureInterfaceBase::Started); + + SessionCipherPtr sessionCipher; + const auto address = Address(senderJid, senderDeviceId); + const auto addressData = address.data(); + + if (session_cipher_create(sessionCipher.ptrRef(), storeContext.get(), &addressData, globalContext.get()) < 0) { + warning("Session cipher could not be created"); + return {}; + } + + session_cipher_set_version(sessionCipher.get(), CIPHERTEXT_OMEMO_VERSION); + + BufferSecurePtr payloadDecryptionDataBuffer; + + auto reportResult = [=](const BufferSecurePtr &buffer) mutable { + // The buffer is copied into the SecureArray to avoid a QByteArray which is not secure. + // However, it would be simpler if SecureArray had an appropriate constructor for that. + const auto payloadDecryptionDataPointer = signal_buffer_data(buffer.get()); + const auto payloadDecryptionDataBufferSize = signal_buffer_len(buffer.get()); + auto payloadDecryptionData = QCA::SecureArray(payloadDecryptionDataBufferSize); + std::copy_n(payloadDecryptionDataPointer, payloadDecryptionDataBufferSize, payloadDecryptionData.data()); + + reportFinishedResult(interface, payloadDecryptionData); + }; + + // There are three cases: + // 1. If the stanza contains key exchange data, a new session is automatically built by the + // OMEMO library during decryption. + // 2. If the stanza does not contain key exchange data and there is no existing session, the + // stanza cannot be decrypted but a new session is built for future communication. + // 3. If the stanza does not contain key exchange data and there is an existing session, + // that session is used to decrypt the stanza. + if (omemoEnvelope.isUsedForKeyExchange()) { + RefCountedPtr<pre_key_signal_message> omemoEnvelopeData; + const auto serializedOmemoEnvelopeData = omemoEnvelope.data(); + + if (pre_key_signal_message_deserialize_omemo(omemoEnvelopeData.ptrRef(), + reinterpret_cast<const uint8_t *>(serializedOmemoEnvelopeData.data()), + serializedOmemoEnvelopeData.size(), + senderDeviceId, + globalContext.get()) < 0) { + warning("OMEMO envelope data could not be deserialized"); + reportFinishedResult(interface, {}); + } else { + BufferPtr publicIdentityKeyBuffer; + + if (ec_public_key_serialize(publicIdentityKeyBuffer.ptrRef(), pre_key_signal_message_get_identity_key(omemoEnvelopeData.get())) < 0) { + warning("Public Identity key could not be retrieved"); + reportFinishedResult(interface, {}); + } else { + const auto key = publicIdentityKeyBuffer.toByteArray(); + auto &device = devices[senderJid][senderDeviceId]; + auto &storedKeyId = device.keyId; + const auto createdKeyId = createKeyId(key); + + // Store the key if its ID has changed. + if (storedKeyId != createdKeyId) { + storedKeyId = createdKeyId; + omemoStorage->addDevice(senderJid, senderDeviceId, device); + emit q->deviceChanged(senderJid, senderDeviceId); + } + + // Decrypt the OMEMO envelope data and build a session. + switch (session_cipher_decrypt_pre_key_signal_message(sessionCipher.get(), omemoEnvelopeData.get(), nullptr, payloadDecryptionDataBuffer.ptrRef())) { + case SG_ERR_INVALID_MESSAGE: + warning("OMEMO envelope data for key exchange is not valid"); + reportFinishedResult(interface, {}); + break; + case SG_ERR_DUPLICATE_MESSAGE: + warning("OMEMO envelope data for key exchange is already received"); + reportFinishedResult(interface, {}); + break; + case SG_ERR_LEGACY_MESSAGE: + warning("OMEMO envelope data for key exchange format is deprecated"); + reportFinishedResult(interface, {}); + break; + case SG_ERR_INVALID_KEY_ID: { + const auto preKeyId = QString::number(pre_key_signal_message_get_pre_key_id(omemoEnvelopeData.get())); + warning("Pre key with ID '" % preKeyId % + "' of OMEMO envelope data for key exchange could not be found locally"); + reportFinishedResult(interface, {}); + break; + } + case SG_ERR_INVALID_KEY: + warning("OMEMO envelope data for key exchange is incorrectly formatted"); + reportFinishedResult(interface, {}); + break; + case SG_ERR_UNTRUSTED_IDENTITY: + warning("Identity key of OMEMO envelope data for key exchange is not trusted by OMEMO library"); + reportFinishedResult(interface, {}); + break; + case SG_SUCCESS: + reportResult(payloadDecryptionDataBuffer); + + // Send an empty message back to the sender in order to notify the sender's + // device that the session initiation is completed. + // Do not send an empty message if the received stanza is an IQ stanza + // because a response is already directly sent. + if (isMessageStanza) { + sendEmptyMessage(senderJid, senderDeviceId); + } + + // Store the key's trust level if it is not stored yet. + auto future = q->trustLevel(senderJid, storedKeyId); + await(future, q, [=](TrustLevel trustLevel) mutable { + if (trustLevel == TrustLevel::Undecided) { + auto future = storeKeyDependingOnSecurityPolicy(senderJid, key); + await(future, q, [=](auto) mutable { + interface.reportFinished(); + }); + } else { + interface.reportFinished(); + } + }); + } + } + } + } else if (auto &device = devices[senderJid][senderDeviceId]; device.session.isEmpty()) { + warning("Received OMEMO stanza cannot be decrypted because there is no session with " + "sending device, new session is being built"); + + auto future = buildSessionWithDeviceBundle(senderJid, senderDeviceId, device); + await(future, q, [=](auto) mutable { + reportFinishedResult(interface, {}); + }); + } else { + RefCountedPtr<signal_message> omemoEnvelopeData; + const auto serializedOmemoEnvelopeData = omemoEnvelope.data(); + + if (signal_message_deserialize_omemo(omemoEnvelopeData.ptrRef(), reinterpret_cast<const uint8_t *>(serializedOmemoEnvelopeData.data()), serializedOmemoEnvelopeData.size(), globalContext.get()) < 0) { + warning("OMEMO envelope data could not be deserialized"); + reportFinishedResult(interface, {}); + } else { + // Decrypt the OMEMO envelope data. + switch (session_cipher_decrypt_signal_message(sessionCipher.get(), omemoEnvelopeData.get(), nullptr, payloadDecryptionDataBuffer.ptrRef())) { + case SG_ERR_INVALID_MESSAGE: + warning("OMEMO envelope data is not valid"); + reportFinishedResult(interface, {}); + break; + case SG_ERR_DUPLICATE_MESSAGE: + warning("OMEMO envelope data is already received"); + reportFinishedResult(interface, {}); + break; + case SG_ERR_LEGACY_MESSAGE: + warning("OMEMO envelope data format is deprecated"); + reportFinishedResult(interface, {}); + break; + case SG_ERR_NO_SESSION: + warning("Session for OMEMO envelope data could not be found"); + reportFinishedResult(interface, {}); + case SG_SUCCESS: + reportResult(payloadDecryptionDataBuffer); + } + } + } + + return interface.future(); +} + +// +// Decrypts the OMEMO payload. +// +// \param payloadDecryptionData data needed to decrypt the payload +// \param payload payload to be decrypted +// +// \return the decrypted payload or a default-constructed byte array on failure +// +QByteArray ManagerPrivate::decryptPayload(const QCA::SecureArray &payloadDecryptionData, const QByteArray &payload) const +{ + auto hkdfKey = QCA::SecureArray(payloadDecryptionData); + hkdfKey.resize(HKDF_KEY_SIZE); + const auto hkdfSalt = QCA::InitializationVector(QCA::SecureArray(HKDF_SALT_SIZE)); + const auto hkdfInfo = QCA::InitializationVector(QCA::SecureArray(HKDF_INFO)); + auto hkdfOutput = QCA::HKDF().makeKey(hkdfKey, hkdfSalt, hkdfInfo, HKDF_OUTPUT_SIZE); + + // first part of hkdfKey + auto encryptionKey = QCA::SymmetricKey(hkdfOutput); + encryptionKey.resize(PAYLOAD_KEY_SIZE); + + // middle part of hkdfKey + auto authenticationKey = QCA::SymmetricKey(PAYLOAD_AUTHENTICATION_KEY_SIZE); + const auto authenticationKeyOffset = hkdfOutput.data() + PAYLOAD_KEY_SIZE; + std::copy(authenticationKeyOffset, authenticationKeyOffset + PAYLOAD_AUTHENTICATION_KEY_SIZE, authenticationKey.data()); + + // last part of hkdfKey + auto initializationVector = QCA::InitializationVector(PAYLOAD_INITIALIZATION_VECTOR_SIZE); + const auto initializationVectorOffset = hkdfOutput.data() + PAYLOAD_KEY_SIZE + PAYLOAD_AUTHENTICATION_KEY_SIZE; + std::copy(initializationVectorOffset, initializationVectorOffset + PAYLOAD_INITIALIZATION_VECTOR_SIZE, initializationVector.data()); + + if (!QCA::MessageAuthenticationCode::supportedTypes().contains(PAYLOAD_MESSAGE_AUTHENTICATION_CODE_TYPE)) { + warning("Message authentication code type '" % QString(PAYLOAD_MESSAGE_AUTHENTICATION_CODE_TYPE) % "' is not supported by this system"); + return {}; + } + + auto messageAuthenticationCodeGenerator = QCA::MessageAuthenticationCode(PAYLOAD_MESSAGE_AUTHENTICATION_CODE_TYPE, authenticationKey); + auto messageAuthenticationCode = QCA::SecureArray(messageAuthenticationCodeGenerator.process(payload)); + messageAuthenticationCode.resize(PAYLOAD_MESSAGE_AUTHENTICATION_CODE_SIZE); + + auto expectedMessageAuthenticationCode = QCA::SecureArray(payloadDecryptionData.toByteArray().right(PAYLOAD_MESSAGE_AUTHENTICATION_CODE_SIZE)); + + if (messageAuthenticationCode != expectedMessageAuthenticationCode) { + warning("Message authentication code does not match expected one"); + return {}; + } + + QCA::Cipher cipher(PAYLOAD_CIPHER_TYPE, PAYLOAD_CIPHER_MODE, PAYLOAD_CIPHER_PADDING, QCA::Decode, encryptionKey, initializationVector); + auto decryptedPayload = cipher.process(QCA::MemoryRegion(payload)); + + if (decryptedPayload.isEmpty()) { + warning("Following payload could not be decrypted: " % QString(payload)); + return {}; + } + + return decryptedPayload.toByteArray(); +} + +// +// Publishes the OMEMO data for this device. +// +// \return whether it succeeded +// +QFuture<bool> ManagerPrivate::publishOmemoData() +{ + QFutureInterface<bool> interface(QFutureInterfaceBase::Started); + + auto future = pubSubManager->requestPepFeatures(); + await(future, q, [=](QXmppPubSubManager::FeaturesResult result) mutable { + if (const auto error = std::get_if<Error>(&result)) { + warning("Features of PEP service '" % ownBareJid() % "' could not be retrieved" % errorToString(*error)); + warning("Device bundle and device list could not be published"); + reportFinishedResult(interface, false); + } else { + const auto &pepServiceFeatures = std::get<QVector<QString>>(result); + + // Check if the PEP service supports publishing items at all and also publishing + // multiple items. + // The support for publishing multiple items is needed to publish multiple device + // bundles to the corresponding node. + // It is checked here because if that is not possible, the publication of the device + // element must not be published. + // TODO: Uncomment the following line and remove the other one once ejabberd released version > 21.12 + // if (pepServiceFeatures.contains(ns_pubsub_publish) && pepServiceFeatures.contains(ns_pubsub_multi_items)) { + if (pepServiceFeatures.contains(ns_pubsub_publish)) { + auto future = pubSubManager->fetchPepNodes(); + await(future, q, [=](QXmppPubSubManager::NodesResult result) mutable { + if (const auto error = std::get_if<Error>(&result)) { + warning("Nodes of JID '" % ownBareJid() % "' could not be fetched to check if nodes '" % + QString(ns_omemo_2_bundles) % "' and '" % QString(ns_omemo_2_devices) % + "' exist" % errorToString(*error)); + warning("Device bundle and device list could not be published"); + reportFinishedResult(interface, false); + } else { + const auto &nodes = std::get<QVector<QString>>(result); + + const auto deviceListNodeExists = nodes.contains(ns_omemo_2_devices); + const auto arePublishOptionsSupported = pepServiceFeatures.contains(ns_pubsub_publish_options); + const auto isAutomaticCreationSupported = pepServiceFeatures.contains(ns_pubsub_auto_create); + const auto isCreationAndConfigurationSupported = pepServiceFeatures.contains(ns_pubsub_create_and_configure); + const auto isCreationSupported = pepServiceFeatures.contains(ns_pubsub_create_nodes); + const auto isConfigurationSupported = pepServiceFeatures.contains(ns_pubsub_config_node); + + // The device bundle is published before the device data is published. + // That way, it ensures that other devices are notified about this new + // device only after the corresponding device bundle is published. + auto handleResult = [=, this](bool isPublished) mutable { + if (isPublished) { + publishDeviceElement(deviceListNodeExists, + arePublishOptionsSupported, + isAutomaticCreationSupported, + isCreationAndConfigurationSupported, + isCreationSupported, + isConfigurationSupported, + [=](bool isPublished) mutable { + if (!isPublished) { + warning("Device element could not be published"); + } + reportFinishedResult(interface, isPublished); + }); + } else { + warning("Device bundle could not be published"); + reportFinishedResult(interface, false); + } + }; + publishDeviceBundle(nodes.contains(ns_omemo_2_bundles), + arePublishOptionsSupported, + isAutomaticCreationSupported, + isCreationAndConfigurationSupported, + isCreationSupported, + isConfigurationSupported, + pepServiceFeatures.contains(ns_pubsub_config_node_max), + handleResult); + } + }); + } else { + warning("Publishing (multiple) items to PEP node '" % ownBareJid() % "' is not supported"); + warning("Device bundle and device list could not be published"); + reportFinishedResult(interface, false); + } + } + }); + + return interface.future(); +} + +// +// Publishes this device's bundle. +// +// If no node for device bundles exists, a new one is created. +// +// \param isDeviceBundlesNodeExistent whether the PEP node for device bundles exists +// \param arePublishOptionsSupported whether publish options are supported by the PEP service +// \param isAutomaticCreationSupported whether the PEP service supports the automatic creation +// of nodes when new items are published +// \param isCreationAndConfigurationSupported whether the PEP service supports the +// configuration of nodes during their creation +// \param isCreationSupported whether the PEP service supports creating nodes +// \param isConfigurationSupported whether the PEP service supports configuring existing +// nodes +// \param isConfigNodeMaxSupported whether the PEP service supports to set the maximum number +// of allowed items per node to the maximum it supports +// \param continuation function to be called with the bool value whether it succeeded +// +template<typename Function> +void ManagerPrivate::publishDeviceBundle(bool isDeviceBundlesNodeExistent, + bool arePublishOptionsSupported, + bool isAutomaticCreationSupported, + bool isCreationAndConfigurationSupported, + bool isCreationSupported, + bool isConfigurationSupported, + bool isConfigNodeMaxSupported, + Function continuation) +{ + // Check if the PEP service supports configuration of nodes during publication of items. + if (arePublishOptionsSupported) { + if (isAutomaticCreationSupported || isDeviceBundlesNodeExistent) { + // The supported publish options cannot be determined because they are not announced + // via Service Discovery. + // Especially, there is no feature like ns_pubsub_multi_items and no error case + // specified for the usage of + // QXmppPubSubNodeConfig::ItemLimit as a publish option. + // Thus, it simply tries to publish the item with that publish option. + // If that fails, it tries to manually create and configure the node and publish the + // item. + publishDeviceBundleItemWithOptions([=](bool isPublished) mutable { + if (isPublished) { + continuation(true); + } else { + auto handleResult = [this, continuation = std::move(continuation)](bool isPublished) mutable { + if (!isPublished) { + q->debug("PEP service '" % ownBareJid() % + "' does not support feature '" % + QString(ns_pubsub_publish_options) % + "' for all publish options, also not '" % + QString(ns_pubsub_create_and_configure) % + "', '" % QString(ns_pubsub_create_nodes) % "', '" % + QString(ns_pubsub_config_node) % "' and the node does not exist"); + } + continuation(isPublished); + }; + publishDeviceBundleWithoutOptions(isDeviceBundlesNodeExistent, + isCreationAndConfigurationSupported, + isCreationSupported, + // TODO: Uncomment the following line and remove the other one once ejabberd released version > 21.12 + // isConfigurationSupported, + true, + isConfigNodeMaxSupported, + handleResult); + } + }); + } else if (isCreationSupported) { + // Create a node manually if the PEP service does not support creation of nodes + // during publication of items and no node already + // exists. + createDeviceBundlesNode([=](bool isCreated) mutable { + if (isCreated) { + // The supported publish options cannot be determined because they are not + // announced via Service Discovery. + // Especially, there is no feature like ns_pubsub_multi_items and no error + // case specified for the usage of QXmppPubSubNodeConfig::ItemLimit as a + // publish option. + // Thus, it simply tries to publish the item with that publish option. + // If that fails, it tries to manually configure the node and publish the + // item. + publishDeviceBundleItemWithOptions([=](bool isPublished) mutable { + if (isPublished) { + continuation(true); + } else if (isConfigurationSupported) { + configureNodeAndPublishDeviceBundle(isConfigNodeMaxSupported, continuation); + } else { + q->debug("PEP service '" % ownBareJid() % + "' does not support feature '" % + QString(ns_pubsub_publish_options) % + "' for all publish options and also not '" % + QString(ns_pubsub_config_node) % "'"); + continuation(false); + } + }); + } else { + continuation(false); + } + }); + } else { + q->debug("PEP service '" % ownBareJid() % "' does not support features '" % + QString(ns_pubsub_auto_create) % "', '" % QString(ns_pubsub_create_nodes) % + "' and the node does not exist"); + continuation(false); + } + } else { + auto handleResult = [this, continuation = std::move(continuation)](bool isPublished) mutable { + if (!isPublished) { + q->debug("PEP service '" % ownBareJid() % "' does not support features '" % + QString(ns_pubsub_publish_options) % "', '" % + QString(ns_pubsub_create_and_configure) % "', '" % + QString(ns_pubsub_create_nodes) % "', '" % + QString(ns_pubsub_config_node) % "' and the node does not exist"); + } + continuation(isPublished); + }; + publishDeviceBundleWithoutOptions(isDeviceBundlesNodeExistent, + isCreationAndConfigurationSupported, + isCreationSupported, + // TODO: Uncomment the following line and remove the other one once ejabberd released version > 21.12 + // isConfigurationSupported, + true, + isConfigNodeMaxSupported, + handleResult); + } +} + +// +// Publish this device's bundle without publish options. +// +// If no node for device bundles exists, a new one is created. +// +// \param isDeviceBundlesNodeExistent whether the PEP node for device bundles exists +// \param isCreationAndConfigurationSupported whether the PEP service supports the +// configuration of nodes during their creation +// \param isCreationSupported whether the PEP service supports creating nodes +// \param isConfigurationSupported whether the PEP service supports configuring existing +// nodes +// \param isConfigNodeMaxSupported whether the PEP service supports to set the maximum number +// of allowed items per node to the maximum it supports +// \param continuation function to be called with the bool value whether it succeeded +// +template<typename Function> +void ManagerPrivate::publishDeviceBundleWithoutOptions(bool isDeviceBundlesNodeExistent, + bool isCreationAndConfigurationSupported, + bool isCreationSupported, + bool isConfigurationSupported, + bool isConfigNodeMaxSupported, + Function continuation) +{ + if (isDeviceBundlesNodeExistent && isConfigurationSupported) { + configureNodeAndPublishDeviceBundle(isConfigNodeMaxSupported, continuation); + } else if (isCreationAndConfigurationSupported) { + createAndConfigureDeviceBundlesNode(isConfigNodeMaxSupported, [=](bool isCreatedAndConfigured) mutable { + if (isCreatedAndConfigured) { + publishDeviceBundleItem(continuation); + } else { + continuation(false); + } + }); + } else if (isCreationSupported && isConfigurationSupported) { + createDeviceBundlesNode([=](bool isCreated) mutable { + if (isCreated) { + configureNodeAndPublishDeviceBundle(isConfigNodeMaxSupported, continuation); + } else { + continuation(false); + } + }); + } else { + continuation(false); + } +} + +// +// Configures the existing PEP node for device bundles and publishes this device's bundle on it. +// +// \param isConfigNodeMaxSupported whether the PEP service supports to set the maximum number +// of allowed items per node to the maximum it supports +// \param continuation function to be called with the bool value whether it succeeded +// +template<typename Function> +void ManagerPrivate::configureNodeAndPublishDeviceBundle(bool isConfigNodeMaxSupported, Function continuation) +{ + configureDeviceBundlesNode(isConfigNodeMaxSupported, [=](bool isConfigured) mutable { + if (isConfigured) { + publishDeviceBundleItem(continuation); + } else { + continuation(false); + } + }); +} + +// +// Creates a PEP node for device bundles and configures it accordingly. +// +// \param isConfigNodeMaxSupported whether the PEP service supports to set the maximum number +// of allowed items per node to the maximum it supports +// \param continuation function to be called with the bool value whether it succeeded +// +template<typename Function> +void ManagerPrivate::createAndConfigureDeviceBundlesNode(bool isConfigNodeMaxSupported, Function continuation) +{ + if (isConfigNodeMaxSupported) { + createNode(ns_omemo_2_bundles, deviceBundlesNodeConfig(), continuation); + } else { + createNode(ns_omemo_2_bundles, deviceBundlesNodeConfig(PUBSUB_NODE_MAX_ITEMS_1), [=](bool isCreated) mutable { + if (isCreated) { + continuation(true); + } else { + createNode(ns_omemo_2_bundles, deviceBundlesNodeConfig(PUBSUB_NODE_MAX_ITEMS_2), [=](bool isCreated) mutable { + if (isCreated) { + continuation(true); + } else { + createNode(ns_omemo_2_bundles, deviceBundlesNodeConfig(PUBSUB_NODE_MAX_ITEMS_3), continuation); + } + }); + } + }); + } +} + +// +// Creates a PEP node for device bundles. +// +// \param continuation function to be called with the bool value whether it succeeded +// +template<typename Function> +void ManagerPrivate::createDeviceBundlesNode(Function continuation) +{ + createNode(ns_omemo_2_bundles, continuation); +} + +// +// Configures an existing PEP node for device bundles. +// +// There is no feature (like ns_pubsub_config_node_max as a config option) and no error case +// specified for the usage of \c QXmppPubSubNodeConfig::Max() as the value for the config +// option \c QXmppPubSubNodeConfig::ItemLimit. +// Thus, it tries to configure the node with that config option's value and if it fails, it +// tries again with pre-defined values. +// Each pre-defined value can exceed the maximum supported by the PEP service. +// Therefore, multiple values are tried. +// +// \param isConfigNodeMaxSupported whether the PEP service supports to set the +// maximum number of allowed items per node to the maximum it supports +// \param continuation function to be called with the bool value whether it succeeded +// +template<typename Function> +void ManagerPrivate::configureDeviceBundlesNode(bool isConfigNodeMaxSupported, Function continuation) +{ + if (isConfigNodeMaxSupported) { + configureNode(ns_omemo_2_bundles, deviceBundlesNodeConfig(), continuation); + } else { + configureNode(ns_omemo_2_bundles, deviceBundlesNodeConfig(PUBSUB_NODE_MAX_ITEMS_1), [=](bool isConfigured) mutable { + if (isConfigured) { + continuation(true); + } else { + configureNode(ns_omemo_2_bundles, deviceBundlesNodeConfig(PUBSUB_NODE_MAX_ITEMS_2), [=](bool isConfigured) mutable { + if (isConfigured) { + continuation(true); + } else { + configureNode(ns_omemo_2_bundles, deviceBundlesNodeConfig(PUBSUB_NODE_MAX_ITEMS_3), continuation); + } + }); + } + }); + } +} + +// +// Publishes this device bundle's item on the corresponding existing PEP node. +// +// \param continuation function to be called with the bool value whether it succeeded +// +template<typename Function> +void ManagerPrivate::publishDeviceBundleItem(Function continuation) +{ + publishItem(ns_omemo_2_bundles, deviceBundleItem(), continuation); +} + +// +// Publishes this device bundle's item with publish options. +// +// If no node for device bundles exists, a new one is created. +// +// There is no feature (like ns_pubsub_config_node_max as a config option) and no error case +// specified for the usage of \c QXmppPubSubNodeConfig::Max() as the value for the publish +// option \c QXmppPubSubNodeConfig::ItemLimit. +// Thus, it tries to publish the item with that publish option's value and if it fails, it +// tries again with pre-defined values. +// Each pre-defined value can exceed the maximum supported by the PEP service. +// Therefore, multiple values are tried. +// +// \param continuation function to be called with the bool value whether it succeeded +// +template<typename Function> +void ManagerPrivate::publishDeviceBundleItemWithOptions(Function continuation) +{ + publishItem(ns_omemo_2_bundles, deviceBundleItem(), deviceBundlesNodePublishOptions(), [=](bool isPublished) mutable { + if (isPublished) { + continuation(true); + } else { + publishItem(ns_omemo_2_bundles, deviceBundleItem(), deviceBundlesNodePublishOptions(PUBSUB_NODE_MAX_ITEMS_1), [=](bool isPublished) mutable { + if (isPublished) { + continuation(true); + } else { + publishItem(ns_omemo_2_bundles, deviceBundleItem(), deviceBundlesNodePublishOptions(PUBSUB_NODE_MAX_ITEMS_2), [=](bool isPublished) mutable { + if (isPublished) { + continuation(true); + } else { + publishItem(ns_omemo_2_bundles, deviceBundleItem(), deviceBundlesNodePublishOptions(PUBSUB_NODE_MAX_ITEMS_3), continuation); + } + }); + } + }); + } + }); +} + +// +// Creates a PEP item for this device's bundle. +// +// \return this device bundle's item +// +QXmppOmemoDeviceBundleItem ManagerPrivate::deviceBundleItem() const +{ + QXmppOmemoDeviceBundleItem item; + item.setId(QString::number(ownDevice.id)); + item.setDeviceBundle(deviceBundle); + + return item; +} + +// +// Requests a device bundle from a PEP service. +// +// \param deviceOwnerJid bare JID of the device's owner +// \param deviceId ID of the device whose bundle is requested +// +// \return the device bundle on success, otherwise a nullptr +// +QFuture<std::optional<QXmppOmemoDeviceBundle>> ManagerPrivate::requestDeviceBundle(const QString &deviceOwnerJid, uint32_t deviceId) const +{ + QFutureInterface<std::optional<QXmppOmemoDeviceBundle>> interface(QFutureInterfaceBase::Started); + + auto future = pubSubManager->requestItem<QXmppOmemoDeviceBundleItem>(deviceOwnerJid, ns_omemo_2_bundles, QString::number(deviceId)); + await(future, q, [=](QXmppPubSubManager::ItemResult<QXmppOmemoDeviceBundleItem> result) mutable { + if (const auto error = std::get_if<Error>(&result)) { + warning("Device bundle for JID '" % deviceOwnerJid % "' and device ID '" % + QString::number(deviceId) % "' could not be retrieved" % errorToString(*error)); + reportFinishedResult(interface, {}); + } else { + const auto &item = std::get<QXmppOmemoDeviceBundleItem>(result); + reportFinishedResult(interface, { item.deviceBundle() }); + } + }); + + return interface.future(); +} + +// +// Removes the device bundle for this device or deletes the whole node if it would be empty +// after the retraction. +// +// \param continuation function to be called with the bool value whether it succeeded +// +template<typename Function> +void ManagerPrivate::deleteDeviceBundle(Function continuation) +{ + if (otherOwnDevices().isEmpty()) { + deleteNode(ns_omemo_2_bundles, continuation); + } else { + retractItem(ns_omemo_2_bundles, ownDevice.id, continuation); + } +} + +// +// Publishes this device's element within the device list. +// +// If no node for the device list exists, a new one is created. +// +// \param isDeviceListNodeExistent whether the PEP node for the device list exists +// \param arePublishOptionsSupported whether publish options are supported by the PEP service +// \param isAutomaticCreationSupported whether the PEP service supports the automatic creation +// of nodes when new items are published +// \param isCreationAndConfigurationSupported whether the PEP service supports the +// configuration of nodes during their creation +// \param isCreationSupported whether the PEP service supports creating nodes +// \param isConfigurationSupported whether the PEP service supports configuring existing +// nodes +// \param continuation function to be called with the bool value whether it succeeded +// +template<typename Function> +void ManagerPrivate::publishDeviceElement(bool isDeviceListNodeExistent, + bool arePublishOptionsSupported, + bool isAutomaticCreationSupported, + bool isCreationAndConfigurationSupported, + bool isCreationSupported, + bool isConfigurationSupported, + Function continuation) +{ + updateOwnDevicesLocally(isDeviceListNodeExistent, [=](bool isUpdated) mutable { + if (isUpdated) { + // Check if the PEP service supports configuration of nodes during + // publication of items. + if (arePublishOptionsSupported) { + if (isAutomaticCreationSupported || isDeviceListNodeExistent) { + // The supported publish options cannot be determined because they + // are not announced via Service Discovery. + // Thus, it simply tries to publish the item with the specified + // publish options. + // If that fails, it tries to manually create and configure the node + // and publish the item. + publishDeviceListItemWithOptions([=](bool isPublished) mutable { + if (isPublished) { + continuation(true); + } else { + auto handleResult = [this, continuation = std::move(continuation)](bool isPublished) mutable { + if (!isPublished) { + q->debug("PEP service '" % ownBareJid() % "' does not support feature '" % QString(ns_pubsub_publish_options) % "' for all publish options, also not '" % QString(ns_pubsub_create_and_configure) % "', '" % QString(ns_pubsub_create_nodes) % "', '" % QString(ns_pubsub_config_node) % "' and the node does not exist"); + } + continuation(isPublished); + }; + publishDeviceElementWithoutOptions(isDeviceListNodeExistent, + isCreationAndConfigurationSupported, + isCreationSupported, + // TODO: Uncomment the following line and remove the other one once ejabberd released version > 21.12 + // isConfigurationSupported); + true, + handleResult); + } + }); + } else if (isCreationSupported) { + // Create a node manually if the PEP service does not support creation of + // nodes during publication of items and no node already exists. + createDeviceListNode([=](bool isCreated) mutable { + if (isCreated) { + // The supported publish options cannot be determined because they + // are not announced via Service Discovery. + // Thus, it simply tries to publish the item with the specified + // publish options. + // If that fails, it tries to manually configure the node and + // publish the item. + publishDeviceListItemWithOptions([=, continuation = std::move(continuation)](bool isPublished) mutable { + if (isPublished) { + continuation(true); + } else if (isConfigurationSupported) { + configureNodeAndPublishDeviceElement(continuation); + } else { + q->debug("PEP service '" % ownBareJid() % + "' does not support feature '" % + QString(ns_pubsub_publish_options) % + "' for all publish options and also not '" % + QString(ns_pubsub_config_node) % "'"); + continuation(false); + } + }); + } else { + continuation(false); + } + }); + } else { + q->debug("PEP service '" % ownBareJid() % "' does not support features '" % + QString(ns_pubsub_auto_create) % "', '" % + QString(ns_pubsub_create_nodes) % "' and the node does not exist"); + continuation(false); + } + } else { + auto handleResult = [=](bool isPublished) mutable { + if (!isPublished) { + q->debug("PEP service '" % ownBareJid() % "' does not support features '" % QString(ns_pubsub_publish_options) % "', '" % QString(ns_pubsub_create_and_configure) % "', '" % QString(ns_pubsub_create_nodes) % "', '" % QString(ns_pubsub_config_node) % "' and the node does not exist"); + } + continuation(isPublished); + }; + publishDeviceElementWithoutOptions(isDeviceListNodeExistent, + isCreationAndConfigurationSupported, + isCreationSupported, + // TODO: Uncomment the following line and remove the other one once ejabberd released version > 21.12 + // isConfigurationSupported); + true, + handleResult); + } + } else { + continuation(false); + } + }); +} + +// +// Publish this device's element without publish options. +// +// If no node for the device list exists, a new one is created. +// +// \param isDeviceListNodeExistent whether the PEP node for the device list exists +// \param isCreationAndConfigurationSupported whether the PEP service supports the +// configuration of nodes during their creation +// \param isCreationSupported whether the PEP service supports creating nodes +// \param isConfigurationSupported whether the PEP service supports configuring existing +// nodes +// \param continuation function to be called with the bool value whether it succeeded +// +template<typename Function> +void ManagerPrivate::publishDeviceElementWithoutOptions(bool isDeviceListNodeExistent, bool isCreationAndConfigurationSupported, bool isCreationSupported, bool isConfigurationSupported, Function continuation) +{ + if (isDeviceListNodeExistent && isConfigurationSupported) { + configureNodeAndPublishDeviceElement(continuation); + } else if (isCreationAndConfigurationSupported) { + createAndConfigureDeviceListNode([=](bool isCreatedAndConfigured) mutable { + if (isCreatedAndConfigured) { + publishDeviceListItem(true, continuation); + } else { + continuation(false); + } + }); + } else if (isCreationSupported && isConfigurationSupported) { + createDeviceListNode([=](bool isCreated) mutable { + if (isCreated) { + configureNodeAndPublishDeviceElement(continuation); + } else { + continuation(false); + } + }); + } else { + continuation(false); + } +} + +// +// Configures the existing PEP node for the device list and publishes this device's element on +// it. +// +// \param continuation function to be called with the bool value whether it succeeded +// +template<typename Function> +void ManagerPrivate::configureNodeAndPublishDeviceElement(Function continuation) +{ + configureDeviceListNode([=](bool isConfigured) mutable { + if (isConfigured) { + publishDeviceListItem(true, continuation); + } else { + continuation(false); + } + }); +} + +// +// Creates a PEP node for the device list and configures it accordingly. +// +// \param continuation function to be called with the bool value whether it succeeded +// +template<typename Function> +void ManagerPrivate::createAndConfigureDeviceListNode(Function continuation) +{ + createNode(ns_omemo_2_devices, deviceListNodeConfig(), continuation); +} + +// +// Creates a PEP node for the device list. +// +// \param continuation function to be called with the bool value whether it succeeded +// +template<typename Function> +void ManagerPrivate::createDeviceListNode(Function continuation) +{ + createNode(ns_omemo_2_devices, continuation); +} + +// +// Configures an existing PEP node for the device list. +// +// \param continuation function to be called with the bool value whether it succeeded +// +template<typename Function> +void ManagerPrivate::configureDeviceListNode(Function continuation) +{ + configureNode(ns_omemo_2_devices, deviceListNodeConfig(), std::move(continuation)); +} + +// +// Publishes the device list item containing this device's element on the corresponding existing +// PEP node. +// +// \param continuation function to be called with the bool value whether it succeeded +// +template<typename Function> +void ManagerPrivate::publishDeviceListItem(bool addOwnDevice, Function continuation) +{ + publishItem(ns_omemo_2_devices, deviceListItem(addOwnDevice), continuation); +} + +// +// Publishes the device list item containing this device's element with publish options. +// +// If no node for the device list exists, a new one is created. +// +// \param continuation function to be called with the bool value whether it succeeded +// +template<typename Function> +void ManagerPrivate::publishDeviceListItemWithOptions(Function continuation) +{ + publishItem(ns_omemo_2_devices, deviceListItem(), deviceListNodePublishOptions(), continuation); +} + +// +// Creates a PEP item for the device list containing this device's element. +// +// \return the device list item +// +QXmppOmemoDeviceListItem ManagerPrivate::deviceListItem(bool addOwnDevice) +{ + QXmppOmemoDeviceList deviceList; + + // Add this device to the device list. + if (addOwnDevice) { + QXmppOmemoDeviceElement deviceElement; + deviceElement.setId(ownDevice.id); + deviceElement.setLabel(ownDevice.label); + deviceList.append(deviceElement); + } + + // Add all remaining own devices to the device list. + const auto ownDevices = otherOwnDevices(); + for (auto itr = ownDevices.cbegin(); itr != ownDevices.cend(); ++itr) { + const auto &deviceId = itr.key(); + const auto &device = itr.value(); + + QXmppOmemoDeviceElement deviceElement; + deviceElement.setId(deviceId); + deviceElement.setLabel(device.label); + deviceList.append(deviceElement); + } + + QXmppOmemoDeviceListItem item; + item.setId(QXmppPubSubManager::standardItemIdToString(QXmppPubSubManager::Current)); + item.setDeviceList(deviceList); + + return item; +} + +// +// Updates the own locally stored devices by requesting the current device list from the own +// PEP service. +// +// \param isDeviceListNodeExistent whether the node for the device list exists +// \param continuation function to be called with the bool value whether it succeeded +// +template<typename Function> +void ManagerPrivate::updateOwnDevicesLocally(bool isDeviceListNodeExistent, Function continuation) +{ + if (isDeviceListNodeExistent && otherOwnDevices().isEmpty()) { + auto future = pubSubManager->requestPepItem<QXmppOmemoDeviceListItem>(ns_omemo_2_devices, QXmppPubSubManager::Current); + await(future, q, [=](QXmppPubSubManager::ItemResult<QXmppOmemoDeviceListItem> result) mutable { + if (const auto error = std::get_if<Error>(&result)) { + warning("Device list for JID '" % ownBareJid() % + "' could not be retrieved and thus not updated" % + errorToString(*error)); + continuation(false); + } else { + const auto &deviceListItem = std::get<QXmppOmemoDeviceListItem>(result); + QList<QXmppOmemoDeviceElement> deviceList = deviceListItem.deviceList(); + + if (auto devicesCount = deviceList.size()) { + // Do not exceed the maximum of manageable devices. + if (devicesCount > maximumDevicesPerJid) { + warning(u"Received own OMEMO device list could not be stored locally " + "completely because the devices are more than the maximum of " + "manageable devices " % + QString::number(maximumDevicesPerJid) % + u" - Use 'QXmppOmemoManager::setMaximumDevicesPerJid()' to " + "increase the maximum"); + deviceList = deviceList.mid(0, maximumDevicesPerJid); + devicesCount = maximumDevicesPerJid; + } + + auto processedDevicesCount = std::make_shared<int>(0); + + // Store all device elements retrieved from the device list locally as + // devices. + // The own device (i.e., a device element in the device list with the same + // ID as of this device) is skipped. + for (const auto &deviceElement : std::as_const(deviceList)) { + if (const auto deviceId = deviceElement.id(); deviceId != ownDevice.id) { + const auto jid = ownBareJid(); + auto &device = devices[jid][deviceId]; + device.label = deviceElement.label(); + + auto future = omemoStorage->addDevice(jid, deviceId, device); + await(future, q, [=, &device]() mutable { + auto future = buildSessionForNewDevice(jid, deviceId, device); + await(future, q, [=](auto) mutable { + emit q->deviceAdded(jid, deviceId); + + if (++(*processedDevicesCount) == devicesCount) { + continuation(true); + } + }); + }); + } + } + } else { + continuation(true); + } + } + }); + } else { + continuation(true); + } +} + +// +// Updates all locally stored devices by a passed device list item. +// +// \param deviceOwnerJid bare JID of the devices' owner +// \param deviceListItem PEP item containing the device list +// +void ManagerPrivate::updateDevices(const QString &deviceOwnerJid, const QXmppOmemoDeviceListItem &deviceListItem) +{ + const auto isOwnDeviceListNode = ownBareJid() == deviceOwnerJid; + QList<QXmppOmemoDeviceElement> deviceList = deviceListItem.deviceList(); + auto isOwnDeviceListIncorrect = false; + + // Do not exceed the maximum of manageable devices. + if (deviceList.size() > maximumDevicesPerJid) { + warning(u"Received OMEMO device list of JID '" % deviceOwnerJid % + "' could not be stored locally completely because the devices are more than the " + "maximum of manageable devices " % + QString::number(maximumDevicesPerJid) % + u" - Use 'QXmppOmemoManager::setMaximumDevicesPerJid()' to increase the maximum"); + deviceList = deviceList.mid(0, maximumDevicesPerJid); + } + + if (isOwnDeviceListNode) { + QList<uint32_t> deviceIds; + + // Search for inconsistencies in the device list to keep it + // correct. + // The following problems are corrected: + // * Multiple device elements have the same IDs. + // * There is no device element for this device. + // * There are device elements with the same ID as this device + // but different labels. + for (auto itr = deviceList.begin(); itr != deviceList.end();) { + const auto deviceElementId = itr->id(); + + if (deviceIds.contains(deviceElementId)) { + isOwnDeviceListIncorrect = true; + itr = deviceList.erase(itr); + } else { + deviceIds.append(deviceElementId); + + if (itr->id() == ownDevice.id) { + if (itr->label() != ownDevice.label) { + isOwnDeviceListIncorrect = true; + } + + itr = deviceList.erase(itr); + } else { + ++itr; + } + } + } + } + + // Set a timestamp for locally stored devices that are removed later if + // they are not included in the device list (i.e., they were removed + // by their owner). + auto &ownerDevices = devices[deviceOwnerJid]; + for (auto itr = ownerDevices.begin(); itr != ownerDevices.end(); ++itr) { + const auto &deviceId = itr.key(); + auto &device = itr.value(); + auto isDeviceFound = false; + + for (const auto &deviceElement : std::as_const(deviceList)) { + if (deviceId == deviceElement.id()) { + isDeviceFound = true; + break; + } + } + + if (!isDeviceFound) { + device.removalFromDeviceListDate = QDateTime::currentDateTimeUtc(); + omemoStorage->addDevice(deviceOwnerJid, deviceId, device); + } + } + + // Update locally stored devices if they are modified in the device + // list or store devices locally if they are new in the device list. + for (const auto &deviceElement : std::as_const(deviceList)) { + auto isDeviceFound = false; + + for (auto itr = ownerDevices.begin(); itr != ownerDevices.end(); ++itr) { + const auto &deviceId = itr.key(); + auto &device = itr.value(); + + if (deviceId == deviceElement.id()) { + auto isDeviceModified = false; + auto isDeviceLabelModified = false; + + // Reset the date of removal from server, if it has been + // removed before. + if (!device.removalFromDeviceListDate.isNull()) { + device.removalFromDeviceListDate = {}; + isDeviceModified = true; + } + + // Update the stored label if it differs from the new + // one. + if (device.label != deviceElement.label()) { + device.label = deviceElement.label(); + isDeviceModified = true; + isDeviceLabelModified = true; + } + + // Store the modifications. + if (isDeviceModified) { + omemoStorage->addDevice(deviceOwnerJid, deviceId, device); + + if (isDeviceLabelModified) { + emit q->deviceChanged(deviceOwnerJid, deviceId); + } + } + + isDeviceFound = true; + break; + } + } + + // Create a new entry and store it if there is no such entry + // yet. + if (!isDeviceFound) { + const auto deviceId = deviceElement.id(); + auto &device = ownerDevices[deviceId]; + device.label = deviceElement.label(); + omemoStorage->addDevice(deviceOwnerJid, deviceId, device); + + auto future = buildSessionForNewDevice(deviceOwnerJid, deviceId, device); + await(future, q, [=](auto) { + emit q->deviceAdded(deviceOwnerJid, deviceId); + }); + } + } + + // Publish an own correct device list if the PEP service's one is incorrect + // and the devices are already set up locally. + if (isOwnDeviceListIncorrect) { + if (!this->devices.isEmpty()) { + publishDeviceListItem(true, [=](bool isPublished) { + if (!isPublished) { + warning("Own device list item could not be published in order to correct the PEP service's one"); + } + }); + } + } +} + +// +// Corrects the own device list on the PEP service by the locally stored +// devices or set a contact device to be removed locally in the future. +// +// \param deviceOwnerJid bare JID of the devices' owner +// +void ManagerPrivate::handleIrregularDeviceListChanges(const QString &deviceOwnerJid) +{ + const auto isOwnDeviceListNode = ownBareJid() == deviceOwnerJid; + + if (isOwnDeviceListNode) { + // Publish a new device list for the own devices if their device list + // item is removed, if their device list node is removed or if all + // the node's items are removed. + auto future = pubSubManager->deletePepNode(ns_omemo_2_devices); + await(future, q, [=](QXmppPubSubManager::Result result) { + if (const auto error = std::get_if<Error>(&result)) { + warning("Node '" % QString(ns_omemo_2_devices) % "' of JID '" % deviceOwnerJid % + "' could not be deleted in order to recover from an inconsistent node" % + errorToString(*error)); + } else { + auto future = pubSubManager->requestPepFeatures(); + await(future, q, [=](QXmppPubSubManager::FeaturesResult result) { + if (const auto error = std::get_if<Error>(&result)) { + warning("Features of PEP service '" % deviceOwnerJid % + "' could not be retrieved" % errorToString(*error)); + warning("Device list could not be published"); + } else { + const auto &pepServiceFeatures = std::get<QVector<QString>>(result); + + const auto arePublishOptionsSupported = pepServiceFeatures.contains(ns_pubsub_publish_options); + const auto isAutomaticCreationSupported = pepServiceFeatures.contains(ns_pubsub_auto_create); + const auto isCreationAndConfigurationSupported = pepServiceFeatures.contains(ns_pubsub_create_and_configure); + const auto isCreationSupported = pepServiceFeatures.contains(ns_pubsub_create_nodes); + const auto isConfigurationSupported = pepServiceFeatures.contains(ns_pubsub_config_node); + + publishDeviceElement(false, + arePublishOptionsSupported, + isAutomaticCreationSupported, + isCreationAndConfigurationSupported, + isCreationSupported, + isConfigurationSupported, + [=](bool isPublished) { + if (!isPublished) { + warning("Device element could not be published"); + } + }); + } + }); + } + }); + } else { + auto &ownerDevices = this->devices[deviceOwnerJid]; + + // Set a timestamp for locally stored contact devices being removed + // later if their device list item is removed, if their device list node + // is removed or if all the node's items are removed. + for (auto itr = ownerDevices.begin(); itr != ownerDevices.end(); ++itr) { + const auto &deviceId = itr.key(); + auto &device = itr.value(); + + device.removalFromDeviceListDate = QDateTime::currentDateTimeUtc(); + + // Store the modification. + omemoStorage->addDevice(deviceOwnerJid, deviceId, device); + } + } +} + +// +// Removes the device element for this device or deletes the whole PEP node if +// it would be empty after the retraction. +// +// \param continuation function to be called with the bool value whether it succeeded +// +template<typename Function> +void ManagerPrivate::deleteDeviceElement(Function continuation) +{ + if (otherOwnDevices().isEmpty()) { + deleteNode(ns_omemo_2_devices, std::move(continuation)); + } else { + publishDeviceListItem(false, std::move(continuation)); + } +} + +// +// Creates a PEP node. +// +// \param node node to be created +// \param continuation function to be called with the bool value whether it succeeded +// +template<typename Function> +void ManagerPrivate::createNode(const QString &node, Function continuation) +{ + runPubSubQueryWithContinuation(pubSubManager->createPepNode(node), + "Node '" % node % "' of JID '" % ownBareJid() % "' could not be created", + std::move(continuation)); +} + +// +// Creates a PEP node with a configuration. +// +// \param node node to be created +// \param config configuration to be applied +// \param continuation function to be called with the bool value whether it succeeded +// +template<typename Function> +void ManagerPrivate::createNode(const QString &node, const QXmppPubSubNodeConfig &config, Function continuation) +{ + runPubSubQueryWithContinuation(pubSubManager->createPepNode(node, config), + "Node '" % node % "' of JID '" % ownBareJid() % "' could not be created", + std::move(continuation)); +} + +// +// Configures an existing PEP node. +// +// \param node node to be configured +// \param config configuration to be applied +// \param continuation function to be called with the bool value whether it succeeded +// +template<typename Function> +void ManagerPrivate::configureNode(const QString &node, const QXmppPubSubNodeConfig &config, Function continuation) +{ + runPubSubQueryWithContinuation(pubSubManager->configurePepNode(node, config), + "Node '" % node % "' of JID '" % ownBareJid() % "' could not be configured", + std::move(continuation)); +} + +// +// Retracts an item from a PEP node. +// +// \param node node containing the item +// \param itemId ID of the item +// \param continuation function to be called with the bool value whether it succeeded +// +template<typename Function> +void ManagerPrivate::retractItem(const QString &node, uint32_t itemId, Function continuation) +{ + const auto itemIdString = QString::number(itemId); + runPubSubQueryWithContinuation(pubSubManager->retractPepItem(node, itemIdString), + "Item '" % itemIdString % "' of node '" % node % "' and JID '" % ownBareJid() % "' could not be retracted", + std::move(continuation)); +} + +// +// Deletes a PEP node. +// +// \param node node to be deleted +// \param continuation function to be called with the bool value whether it succeeded +// +template<typename Function> +void ManagerPrivate::deleteNode(const QString &node, Function continuation) +{ + auto future = pubSubManager->deletePepNode(node); + await(future, q, [=, continuation = std::move(continuation)](QXmppPubSubManager::Result result) mutable { + const auto error = std::get_if<Error>(&result); + if (error) { + const auto errorType = error->type(); + const auto errorCondition = error->condition(); + + // Skip the error handling if the node is already deleted. + if (!(errorType == Error::Cancel && errorCondition == Error::ItemNotFound)) { + warning("Node '" % node % "' of JID '" % ownBareJid() % "' could not be deleted" % + errorToString(*error)); + continuation(false); + } else { + continuation(true); + } + } else { + continuation(true); + } + }); +} + +// +// Publishes a PEP item. +// +// \param node node containing the item +// \param item item to be published +// \param continuation function to be called with the bool value whether it succeeded +// +template<typename T, typename Function> +void ManagerPrivate::publishItem(const QString &node, const T &item, Function continuation) +{ + runPubSubQueryWithContinuation(pubSubManager->publishPepItem(node, item), + "Item with ID '" % item.id() % + "' could not be published to node '" % node % "' of JID '" % + ownBareJid() % "'", + std::move(continuation)); +} + +// +// Publishes a PEP item with publish options. +// +// \param node node containing the item +// \param item item to be published +// \param publishOptions publish options to be applied +// \param continuation function to be called with the bool value whether it succeeded +// +template<typename T, typename Function> +void ManagerPrivate::publishItem(const QString &node, const T &item, const QXmppPubSubPublishOptions &publishOptions, Function continuation) +{ + runPubSubQueryWithContinuation(pubSubManager->publishPepItem(node, item, publishOptions), + "Item with ID '" % item.id() % "' could not be published to node '" % node % "' of JID '" % ownBareJid() % "'", + std::move(continuation)); +} + +// +// Runs a PubSub query and processes a continuation function. +// +// \param future PubSub query to be run +// \param errorMessage message to be logged in case of an error +// \param continuation function to be called after the PubSub query +// +template<typename T, typename Function> +void QXmppOmemoManagerPrivate::runPubSubQueryWithContinuation(QFuture<T> future, const QString &errorMessage, Function continuation) +{ + await(future, q, [this, errorMessage, continuation = std::move(continuation)](auto result) mutable { + if (auto error = std::get_if<Error>(&result)) { + warning(errorMessage % u": " % errorToString(*error)); + continuation(false); + } else { + continuation(true); + } + }); +} + +// See QXmppOmemoManager for documentation +QFuture<bool> ManagerPrivate::changeDeviceLabel(const QString &deviceLabel) +{ + QFutureInterface<bool> interface(QFutureInterfaceBase::Started); + + ownDevice.label = deviceLabel; + + if (isStarted) { + auto future = omemoStorage->setOwnDevice(ownDevice); + await(future, q, [=]() mutable { + publishDeviceListItem(true, [=](bool isPublished) mutable { + reportFinishedResult(interface, isPublished); + }); + }); + } else { + reportFinishedResult(interface, true); + } + + return interface.future(); +} + +// +// Requests the device list of a contact manually and stores it locally. +// +// This should be called for offline contacts whose servers do not distribute +// the last published PubSub item if that contact is offline (e.g., with at +// least ejabberd version <= 21.12) +// +// \param jid JID of the contact whose device list is being requested +// +// \return the result of the request +// +QFuture<QXmppPubSubManager::ItemResult<QXmppOmemoDeviceListItem>> ManagerPrivate::requestDeviceList(const QString &jid) +{ + auto future = pubSubManager->requestItem<QXmppOmemoDeviceListItem>(jid, ns_omemo_2_devices, QXmppPubSubManager::Current); + await(future, q, [this, jid](QXmppPubSubManager::ItemResult<QXmppOmemoDeviceListItem> result) mutable { + if (const auto error = std::get_if<Error>(&result)) { + warning("Device list for JID '" % jid % "' could not be retrieved: " % errorToString(*error)); + } else { + const auto &item = std::get<QXmppOmemoDeviceListItem>(result); + updateDevices(jid, item); + } + }); + return future; +} + +// +// Subscribes to the device list of a contact if the contact's device is not stored yet. +// +// \param jid JID of the contact whose device list is being subscribed +// \param deviceId ID of the device that is checked +// +void ManagerPrivate::subscribeToNewDeviceLists(const QString &jid, uint32_t deviceId) +{ + if (!devices.value(jid).contains(deviceId)) { + subscribeToDeviceList(jid); + } +} + +// +// Subscribes the current user's resource to a device list manually. +// +// A server may not send the last published item automatically. +// To ensure that the subscribed device list can be stored locally in any case, +// the current PubSub item containing the device list is requested manually. +// +// \param jid JID of the contact whose device list is being subscribed +// +// \return the result of the subscription and manual request +// +QFuture<QXmppPubSubManager::Result> ManagerPrivate::subscribeToDeviceList(const QString &jid) +{ + QFutureInterface<QXmppPubSubManager::Result> interface(QFutureInterfaceBase::Started); + + auto future = pubSubManager->subscribeToNode(jid, ns_omemo_2_devices, ownFullJid()); + await(future, q, [=](QXmppPubSubManager::Result result) mutable { + if (const auto error = std::get_if<Error>(&result)) { + warning("Device list for JID '" % jid % "' could not be subscribed: " % errorToString(*error)); + reportFinishedResult(interface, { *error }); + } else { + jidsOfManuallySubscribedDevices.append(jid); + + auto future = requestDeviceList(jid); + await(future, q, [=](auto result) mutable { + reportFinishedResult(interface, mapToSuccess(std::move(result))); + }); + } + }); + + return interface.future(); +} + +// +// Unsubscribes the current user's resource from device lists that were +// manually subscribed by +// \c QXmppOmemoManagerPrivate::subscribeToDeviceList(). +// +// \param jids JIDs of the contacts whose device lists are being +// unsubscribed +// +// \return the results of each unsubscribe request +// +QFuture<Manager::DevicesResult> ManagerPrivate::unsubscribeFromDeviceLists(const QList<QString> &jids) +{ + QFutureInterface<Manager::DevicesResult> interface = (QFutureInterfaceBase::Started); + + const auto jidsCount = jids.size(); + auto processedJidsCount = std::make_shared<int>(0); + + if (jidsCount == 0) { + interface.reportFinished(); + } + + for (const auto &jid : jids) { + auto future = unsubscribeFromDeviceList(jid); + await(future, q, [=](QXmppPubSubManager::Result result) mutable { + Manager::DevicesResult devicesResult; + devicesResult.jid = jid; + devicesResult.result = result; + interface.reportResult(devicesResult); + + if (++(*processedJidsCount) == jidsCount) { + interface.reportFinished(); + } + }); + } + + return interface.future(); +} + +// +// Unsubscribes the current user's resource from a device list that were +// manually subscribed by +// \c QXmppOmemoManagerPrivate::subscribeToDeviceList(). +// +// \param jid JID of the contact whose device list is being unsubscribed +// +// \return the result of the unsubscription +// +QFuture<QXmppPubSubManager::Result> ManagerPrivate::unsubscribeFromDeviceList(const QString &jid) +{ + QFutureInterface<QXmppPubSubManager::Result> interface(QFutureInterfaceBase::Started); + + auto future = pubSubManager->unsubscribeFromNode(jid, ns_omemo_2_devices, ownFullJid()); + await(future, q, [=](QXmppPubSubManager::Result result) mutable { + if (const auto error = std::get_if<Error>(&result)) { + warning("Device list for JID '" % jid % "' could not be unsubscribed: " % errorToString(*error)); + } else { + jidsOfManuallySubscribedDevices.removeAll(jid); + } + + reportFinishedResult(interface, result); + }); + + return interface.future(); +} + +// See QXmppOmemoManager for documentation +QFuture<bool> ManagerPrivate::resetOwnDevice() +{ + QFutureInterface<bool> interface(QFutureInterfaceBase::Started); + + isStarted = false; + + auto future = trustManager->resetAll(ns_omemo_2); + await(future, q, [=]() mutable { + auto future = omemoStorage->resetAll(); + await(future, q, [=]() mutable { + deleteDeviceElement([=](bool isDeviceElementDeleted) mutable { + if (isDeviceElementDeleted) { + deleteDeviceBundle([=](bool isDeviceBundleDeleted) mutable { + if (isDeviceBundleDeleted) { + ownDevice = {}; + preKeyPairs.clear(); + signedPreKeyPairs.clear(); + deviceBundle = {}; + devices.clear(); + + emit q->allDevicesRemoved(); + } + + reportFinishedResult(interface, isDeviceBundleDeleted); + }); + } else { + reportFinishedResult(interface, false); + } + }); + }); + }); + + return interface.future(); +} + +// See QXmppOmemoManager for documentation +QFuture<bool> ManagerPrivate::resetAll() +{ + QFutureInterface<bool> interface(QFutureInterfaceBase::Started); + + isStarted = false; + + auto future = trustManager->resetAll(ns_omemo_2); + await(future, q, [this, interface]() mutable { + auto future = omemoStorage->resetAll(); + await(future, q, [this, interface]() mutable { + deleteNode(ns_omemo_2_devices, [this, interface](bool isDevicesNodeDeleted) mutable { + if (isDevicesNodeDeleted) { + deleteNode(ns_omemo_2_bundles, [this, interface](bool isBundlesNodeDeleted) mutable { + if (isBundlesNodeDeleted) { + ownDevice = {}; + preKeyPairs.clear(); + signedPreKeyPairs.clear(); + deviceBundle = {}; + devices.clear(); + + emit q->allDevicesRemoved(); + } + + reportFinishedResult(interface, isBundlesNodeDeleted); + }); + } else { + reportFinishedResult(interface, false); + } + }); + }); + }); + + return interface.future(); +} + +// +// Builds a new session for a new received device if that is enabled. +// +// \see QXmppOmemoManager::setNewDeviceAutoSessionBuildingEnabled() +// +// \param jid JID of the device's owner +// \param deviceId ID of the device +// \param device locally stored device which will be modified +// +// \return true if a session could be built or it is not enabled, otherwise +// false +// +QFuture<bool> ManagerPrivate::buildSessionForNewDevice(const QString &jid, uint32_t deviceId, QXmppOmemoStorage::Device &device) +{ + if (isNewDeviceAutoSessionBuildingEnabled) { + return buildSessionWithDeviceBundle(jid, deviceId, device); + } else { + return makeReadyFuture(true); + } +} + +// +// Requests a device bundle and builds a new session with it. +// +// \param jid JID of the device's owner +// \param deviceId ID of the device +// \param device locally stored device which will be modified +// +// \return whether a session could be built +// +QFuture<bool> ManagerPrivate::buildSessionWithDeviceBundle(const QString &jid, uint32_t deviceId, QXmppOmemoStorage::Device &device) +{ + QFutureInterface<bool> interface(QFutureInterfaceBase::Started); + + auto future = requestDeviceBundle(jid, deviceId); + await(future, q, [=, &device](std::optional<QXmppOmemoDeviceBundle> optionalDeviceBundle) mutable { + if (optionalDeviceBundle) { + const auto &deviceBundle = *optionalDeviceBundle; + const auto key = deviceBundle.publicIdentityKey(); + device.keyId = createKeyId(key); + + auto future = q->trustLevel(jid, device.keyId); + await(future, q, [=](TrustLevel trustLevel) mutable { + auto buildSessionDependingOnTrustLevel = [=](TrustLevel trustLevel) mutable { + // Build a session if the device's key has a specific trust + // level and send an empty OMEMO (key exchange) message to + // make the receiving device build a new session too. + if (!acceptedSessionBuildingTrustLevels.testFlag(trustLevel)) { + warning("Session could not be created for JID '" % jid % "' with device ID '" % + QString::number(deviceId) % "' because its key's trust level '" % + QString::number(int(trustLevel)) % "' is not accepted"); + reportFinishedResult(interface, false); + } else if (const auto address = Address(jid, deviceId); !buildSession(address.data(), deviceBundle)) { + warning("Session could not be created for JID '" % jid % "' and device ID '" % + QString::number(deviceId) % "'"); + reportFinishedResult(interface, false); + } else { + auto future = sendEmptyMessage(jid, deviceId, true); + await(future, q, [=](QXmpp::SendResult result) mutable { + if (std::holds_alternative<QXmpp::SendError>(result)) { + warning("Session could be created but empty message could not be sent to JID '" % + jid % "' and device ID '" % QString::number(deviceId) % "'"); + reportFinishedResult(interface, false); + } else { + reportFinishedResult(interface, true); + } + }); + } + }; + + if (trustLevel == TrustLevel::Undecided) { + // Store the key's trust level if it is not stored yet. + auto future = storeKeyDependingOnSecurityPolicy(jid, key); + await(future, q, [=](TrustLevel trustLevel) mutable { + buildSessionDependingOnTrustLevel(trustLevel); + }); + } else { + buildSessionDependingOnTrustLevel(trustLevel); + } + }); + } else { + warning("Session could not be created because no device bundle could be fetched for " + "JID '" % + jid % "' and device ID '" % QString::number(deviceId) % "'"); + reportFinishedResult(interface, false); + } + }); + + return interface.future(); +} + +// +// Builds an OMEMO session. +// +// A session is used for encryption and decryption. +// +// \param address address of the device for whom the session is built +// \param deviceBundle device bundle containing all data to build the session +// +// \return whether it succeeded +// +bool ManagerPrivate::buildSession(signal_protocol_address address, const QXmppOmemoDeviceBundle &deviceBundle) +{ + QFutureInterface<bool> interface(QFutureInterfaceBase::Started); + + // Choose a pre key randomly. + const auto publicPreKeys = deviceBundle.publicPreKeys(); + if (publicPreKeys.isEmpty()) { + warning("No public pre key could be found in device bundle"); + } + const auto publicPreKeyIds = publicPreKeys.keys(); +#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) + const auto publicPreKeyIndex = QRandomGenerator::system()->bounded(publicPreKeyIds.size()); +#else + const auto publicPreKeyIndex = qrand() % publicPreKeyIds.size(); +#endif + const auto publicPreKeyId = publicPreKeyIds.at(publicPreKeyIndex); + const auto publicPreKey = publicPreKeys.value(publicPreKeyId); + + SessionBuilderPtr sessionBuilder; + if (session_builder_create(sessionBuilder.ptrRef(), storeContext.get(), &address, globalContext.get()) < 0) { + warning("Session builder could not be created"); + return false; + } + session_builder_set_version(sessionBuilder.get(), CIPHERTEXT_OMEMO_VERSION); + + RefCountedPtr<session_pre_key_bundle> sessionBundle; + + if (!createSessionBundle(sessionBundle.ptrRef(), + deviceBundle.publicIdentityKey(), + deviceBundle.signedPublicPreKey(), + deviceBundle.signedPublicPreKeyId(), + deviceBundle.signedPublicPreKeySignature(), + publicPreKey, + publicPreKeyId)) { + warning("Session bundle could not be created"); + return false; + } + + if (session_builder_process_pre_key_bundle(sessionBuilder.get(), sessionBundle.get()) != SG_SUCCESS) { + warning("Session bundle could not be processed"); + return false; + } + + return true; +} + +// +// Creates a session bundle. +// +// \param sessionBundle session bundle location +// \param serializedPublicIdentityKey serialized public identity key +// \param serializedSignedPublicPreKey serialized signed public pre key +// \param signedPublicPreKeyId ID of the signed public pre key +// \param serializedSignedPublicPreKeySignature serialized signature of the +// signed public pre key +// \param serializedPublicPreKey serialized public pre key +// \param publicPreKeyId ID of the public pre key +// +// \return whether it succeeded +// +bool ManagerPrivate::createSessionBundle(session_pre_key_bundle **sessionBundle, + const QByteArray &serializedPublicIdentityKey, + const QByteArray &serializedSignedPublicPreKey, + uint32_t signedPublicPreKeyId, + const QByteArray &serializedSignedPublicPreKeySignature, + const QByteArray &serializedPublicPreKey, + uint32_t publicPreKeyId) +{ + RefCountedPtr<ec_public_key> publicIdentityKey; + RefCountedPtr<ec_public_key> signedPublicPreKey; + RefCountedPtr<const uint8_t> signedPublicPreKeySignature; + int signedPublicPreKeySignatureSize; + RefCountedPtr<ec_public_key> publicPreKey; + + if (deserializePublicIdentityKey(publicIdentityKey.ptrRef(), serializedPublicIdentityKey) && + deserializeSignedPublicPreKey(signedPublicPreKey.ptrRef(), serializedSignedPublicPreKey) && + (signedPublicPreKeySignatureSize = deserializeSignedPublicPreKeySignature(signedPublicPreKeySignature.ptrRef(), serializedSignedPublicPreKeySignature)) && + deserializePublicPreKey(publicPreKey.ptrRef(), serializedPublicPreKey)) { + + // "0" is passed as "device_id" to the OMEMO library because it is not + // used by OMEMO. + // Only the device ID is of interest which is used as "registration_id" + // within the OMEMO library. + if (session_pre_key_bundle_create(sessionBundle, + ownDevice.id, + 0, + publicPreKeyId, + publicPreKey.get(), + signedPublicPreKeyId, + signedPublicPreKey.get(), + signedPublicPreKeySignature.get(), + signedPublicPreKeySignatureSize, + publicIdentityKey.get()) < 0) { + return false; + } + + return true; + } else { + warning("Session bundle data could not be deserialized"); + return false; + } +} + +// +// Deserializes a public identity key. +// +// \param publicIdentityKey public identity key location +// \param serializedPublicIdentityKey serialized public identity key +// +// \return whether it succeeded +// +bool ManagerPrivate::deserializePublicIdentityKey(ec_public_key **publicIdentityKey, const QByteArray &serializedPublicIdentityKey) const +{ + BufferPtr publicIdentityKeyBuffer = BufferPtr::fromByteArray(serializedPublicIdentityKey); + + if (!publicIdentityKeyBuffer) { + warning("Buffer for serialized public identity key could not be created"); + return false; + } + + if (curve_decode_point(publicIdentityKey, signal_buffer_data(publicIdentityKeyBuffer.get()), signal_buffer_len(publicIdentityKeyBuffer.get()), globalContext.get()) < 0) { + warning("Public identity key could not be deserialized"); + return false; + } + + return true; +} + +// +// Deserializes a signed public pre key. +// +// \param signedPublicPreKey signed public pre key location +// \param serializedSignedPublicPreKey serialized signed public pre key +// +// \return whether it succeeded +// +bool ManagerPrivate::deserializeSignedPublicPreKey(ec_public_key **signedPublicPreKey, const QByteArray &serializedSignedPublicPreKey) const +{ + BufferPtr signedPublicPreKeyBuffer = BufferPtr::fromByteArray(serializedSignedPublicPreKey); + + if (!signedPublicPreKeyBuffer) { + warning("Buffer for serialized signed public pre key could not be created"); + return false; + } + + if (curve_decode_point(signedPublicPreKey, signal_buffer_data(signedPublicPreKeyBuffer.get()), signal_buffer_len(signedPublicPreKeyBuffer.get()), globalContext.get()) < 0) { + warning("Signed public pre key could not be deserialized"); + return false; + } + + return true; +} + +// +// Deserializes a public pre key. +// +// \param publicPreKey public pre key location +// \param serializedPublicPreKey serialized public pre key +// +// \return whether it succeeded +// +bool ManagerPrivate::deserializePublicPreKey(ec_public_key **publicPreKey, const QByteArray &serializedPublicPreKey) const +{ + auto publicPreKeyBuffer = BufferPtr::fromByteArray(serializedPublicPreKey); + + if (!publicPreKeyBuffer) { + warning("Buffer for serialized public pre key could not be created"); + return false; + } + + if (curve_decode_point(publicPreKey, signal_buffer_data(publicPreKeyBuffer.get()), signal_buffer_len(publicPreKeyBuffer.get()), globalContext.get()) < 0) { + warning("Public pre key could not be deserialized"); + return false; + } + + return true; +} + +// +// Sends an empty OMEMO message. +// +// An empty OMEMO message is a message without an OMEMO payload. +// It is used to trigger the completion, rebuilding or refreshing of OMEMO +// sessions. +// +// \param recipientJid JID of the message's recipient +// \param recipientDeviceId ID of the recipient's device +// \param isKeyExchange whether the message is used to build a new session +// +// \return the result of the sending +// +QFuture<QXmpp::SendResult> ManagerPrivate::sendEmptyMessage(const QString &recipientJid, uint32_t recipientDeviceId, bool isKeyExchange) const +{ + QFutureInterface<QXmpp::SendResult> interface(QFutureInterfaceBase::Started); + + const auto address = Address(recipientJid, recipientDeviceId); + const auto decryptionData = QCA::SecureArray(EMPTY_MESSAGE_DECRYPTION_DATA_SIZE); + + if (const auto data = createOmemoEnvelopeData(address.data(), decryptionData); data.isEmpty()) { + warning("OMEMO envelope for recipient JID '" % recipientJid % "' and device ID '" % + QString::number(recipientDeviceId) % + "' could not be created because its data could not be encrypted"); + SendError error = { QStringLiteral("OMEMO envelope could not be created"), SendError::EncryptionError }; + reportFinishedResult(interface, { error }); + } else { + QXmppOmemoEnvelope omemoEnvelope; + omemoEnvelope.setRecipientDeviceId(recipientDeviceId); + if (isKeyExchange) { + omemoEnvelope.setIsUsedForKeyExchange(true); + } + omemoEnvelope.setData(data); + + QXmppOmemoElement omemoElement; + omemoElement.addEnvelope(recipientJid, omemoEnvelope); + omemoElement.setSenderDeviceId(ownDevice.id); + + QXmppMessage message; + message.setTo(recipientJid); + message.addHint(QXmppMessage::Store); + message.setOmemoElement(omemoElement); + + auto future = q->client()->sendUnencrypted(std::move(message)); + await(future, q, [=](QXmpp::SendResult result) mutable { + reportFinishedResult(interface, result); + }); + } + + return interface.future(); +} + +// +// Sets the key of this client instance's device. +// +// The first byte representing a version string used by the OMEMO library but +// not needed for trust management is removed before storing it. +// It corresponds to the fingerprint shown to users which also does not contain +// the first byte. +// +QFuture<void> ManagerPrivate::storeOwnKey() const +{ + QFutureInterface<void> interface(QFutureInterfaceBase::Started); + + auto future = trustManager->setOwnKey(ns_omemo_2, createKeyId(ownDevice.publicIdentityKey)); + await(future, q, [=]() mutable { + interface.reportFinished(); + }); + + return interface.future(); +} + +// +// Stores a key while its trust level is determined by the used security +// policy. +// +// \param keyOwnerJid bare JID of the key owner +// \param key key to store +// +// \return the trust level of the stored key +// +QFuture<TrustLevel> ManagerPrivate::storeKeyDependingOnSecurityPolicy(const QString &keyOwnerJid, const QByteArray &key) +{ + QFutureInterface<TrustLevel> interface(QFutureInterfaceBase::Started); + + auto awaitStoreKey = [=](const QFuture<TrustLevel> &future) mutable { + await(future, q, [=](TrustLevel trustLevel) mutable { + reportFinishedResult(interface, trustLevel); + }); + }; + + auto future = q->securityPolicy(); + await(future, q, [=](TrustSecurityPolicy securityPolicy) mutable { + switch (securityPolicy) { + case NoSecurityPolicy: { + auto future = storeKey(keyOwnerJid, key); + awaitStoreKey(future); + break; + } + case Toakafa: { + auto future = trustManager->hasKey(ns_omemo_2, keyOwnerJid, TrustLevel::Authenticated); + await(future, q, [=](bool hasAuthenticatedKey) mutable { + if (hasAuthenticatedKey) { + // If there is at least one authenticated key, add the + // new key as an automatically distrusted one. + auto future = storeKey(keyOwnerJid, key); + awaitStoreKey(future); + } else { + // If no key is authenticated yet, add the new key as an + // automatically trusted one. + auto future = storeKey(keyOwnerJid, key, TrustLevel::AutomaticallyTrusted); + awaitStoreKey(future); + } + }); + break; + } + default: + Q_UNREACHABLE(); + } + }); + + return interface.future(); +} + +// +// Stores a key. +// +// \param keyOwnerJid bare JID of the key owner +// \param key key to store +// \param trustLevel trust level of the key +// +// \return the trust level of the stored key +// +QFuture<TrustLevel> ManagerPrivate::storeKey(const QString &keyOwnerJid, const QByteArray &key, TrustLevel trustLevel) const +{ + QFutureInterface<TrustLevel> interface(QFutureInterfaceBase::Started); + + auto future = trustManager->addKeys(ns_omemo_2, keyOwnerJid, { createKeyId(key) }, trustLevel); + await(future, q, [=]() mutable { + emit q->trustLevelsChanged({ { keyOwnerJid, key } }); + reportFinishedResult(interface, trustLevel); + }); + + return interface.future(); +} + +// +// Returns the own bare JID set in the client's configuration. +// +// \return the own bare JID +// +QString ManagerPrivate::ownBareJid() const +{ + return q->client()->configuration().jidBare(); +} + +// +// Returns the own full JID set in the client's configuration. +// +// \return the own full JID +// +QString ManagerPrivate::ownFullJid() const +{ + return q->client()->configuration().jid(); +} + +// +// Returns the devices with the own JID except the device of this client +// instance. +// +// \return the other own devices +// +QHash<uint32_t, QXmppOmemoStorage::Device> ManagerPrivate::otherOwnDevices() +{ + return devices.value(ownBareJid()); +} + +// +// Calls the logger warning method. +// +// \param msg warning message +// +void ManagerPrivate::warning(const QString &msg) const +{ + q->warning(msg); +} + +/// \endcond |
