From f0e0e1cd227c08ffd97aa42813a479b8c3ca6d23 Mon Sep 17 00:00:00 2001 From: Linus Jahn Date: Tue, 31 May 2022 12:22:48 +0200 Subject: Split up OMEMO into extra module --- src/CMakeLists.txt | 36 +- src/base/QXmppOmemoData.cpp | 737 ------- src/base/QXmppOmemoDataBase.cpp | 290 +++ src/base/QXmppOmemoDeviceBundle_p.h | 52 - src/base/QXmppOmemoDeviceElement_p.h | 39 - src/base/QXmppOmemoDeviceList_p.h | 30 - src/base/QXmppOmemoElement_p.h | 2 +- src/base/QXmppOmemoEnvelope_p.h | 2 +- src/base/QXmppOmemoIq_p.h | 28 - src/base/QXmppOmemoItems_p.h | 44 - src/client/OmemoCryptoProvider.cpp | 239 -- src/client/OmemoCryptoProvider.h | 17 - src/client/OmemoLibWrappers.h | 100 - src/client/QXmppOmemoManager.cpp | 1282 ----------- src/client/QXmppOmemoManager.h | 157 -- src/client/QXmppOmemoManager_p.cpp | 3714 -------------------------------- src/client/QXmppOmemoManager_p.h | 342 --- src/client/QXmppOmemoMemoryStorage.cpp | 119 - src/client/QXmppOmemoMemoryStorage.h | 43 - src/client/QXmppOmemoStorage.cpp | 97 - src/client/QXmppOmemoStorage.h | 174 -- src/omemo/CMakeLists.txt | 91 + src/omemo/OmemoCryptoProvider.cpp | 239 ++ src/omemo/OmemoCryptoProvider.h | 17 + src/omemo/OmemoLibWrappers.h | 100 + src/omemo/QXmppOmemoConfig.cmake.in | 12 + src/omemo/QXmppOmemoData.cpp | 465 ++++ src/omemo/QXmppOmemoDeviceBundle_p.h | 52 + src/omemo/QXmppOmemoDeviceElement_p.h | 39 + src/omemo/QXmppOmemoDeviceList_p.h | 30 + src/omemo/QXmppOmemoIq_p.h | 28 + src/omemo/QXmppOmemoItems_p.h | 44 + src/omemo/QXmppOmemoManager.cpp | 1282 +++++++++++ src/omemo/QXmppOmemoManager.h | 158 ++ src/omemo/QXmppOmemoManager_p.cpp | 3714 ++++++++++++++++++++++++++++++++ src/omemo/QXmppOmemoManager_p.h | 342 +++ src/omemo/QXmppOmemoMemoryStorage.cpp | 119 + src/omemo/QXmppOmemoMemoryStorage.h | 43 + src/omemo/QXmppOmemoStorage.cpp | 97 + src/omemo/QXmppOmemoStorage.h | 174 ++ 40 files changed, 7344 insertions(+), 7246 deletions(-) delete mode 100644 src/base/QXmppOmemoData.cpp create mode 100644 src/base/QXmppOmemoDataBase.cpp delete mode 100644 src/base/QXmppOmemoDeviceBundle_p.h delete mode 100644 src/base/QXmppOmemoDeviceElement_p.h delete mode 100644 src/base/QXmppOmemoDeviceList_p.h delete mode 100644 src/base/QXmppOmemoIq_p.h delete mode 100644 src/base/QXmppOmemoItems_p.h delete mode 100644 src/client/OmemoCryptoProvider.cpp delete mode 100644 src/client/OmemoCryptoProvider.h delete mode 100644 src/client/OmemoLibWrappers.h delete mode 100644 src/client/QXmppOmemoManager.cpp delete mode 100644 src/client/QXmppOmemoManager.h delete mode 100644 src/client/QXmppOmemoManager_p.cpp delete mode 100644 src/client/QXmppOmemoManager_p.h delete mode 100644 src/client/QXmppOmemoMemoryStorage.cpp delete mode 100644 src/client/QXmppOmemoMemoryStorage.h delete mode 100644 src/client/QXmppOmemoStorage.cpp delete mode 100644 src/client/QXmppOmemoStorage.h create mode 100644 src/omemo/CMakeLists.txt create mode 100644 src/omemo/OmemoCryptoProvider.cpp create mode 100644 src/omemo/OmemoCryptoProvider.h create mode 100644 src/omemo/OmemoLibWrappers.h create mode 100644 src/omemo/QXmppOmemoConfig.cmake.in create mode 100644 src/omemo/QXmppOmemoData.cpp create mode 100644 src/omemo/QXmppOmemoDeviceBundle_p.h create mode 100644 src/omemo/QXmppOmemoDeviceElement_p.h create mode 100644 src/omemo/QXmppOmemoDeviceList_p.h create mode 100644 src/omemo/QXmppOmemoIq_p.h create mode 100644 src/omemo/QXmppOmemoItems_p.h create mode 100644 src/omemo/QXmppOmemoManager.cpp create mode 100644 src/omemo/QXmppOmemoManager.h create mode 100644 src/omemo/QXmppOmemoManager_p.cpp create mode 100644 src/omemo/QXmppOmemoManager_p.h create mode 100644 src/omemo/QXmppOmemoMemoryStorage.cpp create mode 100644 src/omemo/QXmppOmemoMemoryStorage.h create mode 100644 src/omemo/QXmppOmemoStorage.cpp create mode 100644 src/omemo/QXmppOmemoStorage.h (limited to 'src') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4dfb2d16..c02c4015 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -251,28 +251,8 @@ set(SOURCE_FILES ) if(BUILD_OMEMO) - set(INSTALL_HEADER_FILES - ${INSTALL_HEADER_FILES} - - # Client - client/QXmppOmemoManager.h - client/QXmppOmemoMemoryStorage.h - client/QXmppOmemoStorage.h - ) - - set(SOURCE_FILES - ${SOURCE_FILES} - - # Base - base/QXmppOmemoData.cpp - - # Client - client/QXmppOmemoManager.cpp - client/QXmppOmemoManager_p.cpp - client/QXmppOmemoMemoryStorage.cpp - client/QXmppOmemoStorage.cpp - client/OmemoCryptoProvider.cpp - ) + # required to be used in QXmppMessage + set(SOURCE_FILES ${SOURCE_FILES} base/QXmppOmemoDataBase.cpp) endif() if(WITH_GSTREAMER) @@ -326,14 +306,6 @@ target_link_libraries(qxmpp Qt${QT_VERSION_MAJOR}::Xml ) -if(WITH_OMEMO) - target_link_libraries(qxmpp - PRIVATE - PkgConfig::OmemoC - qca-qt${QT_VERSION_MAJOR} - ) -endif() - if(WITH_GSTREAMER) target_link_libraries(qxmpp PRIVATE @@ -366,3 +338,7 @@ install( FILES ${INSTALL_HEADER_FILES} DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/qxmpp" ) + +if(BUILD_OMEMO) + add_subdirectory(omemo) +endif() diff --git a/src/base/QXmppOmemoData.cpp b/src/base/QXmppOmemoData.cpp deleted file mode 100644 index dd99eefd..00000000 --- a/src/base/QXmppOmemoData.cpp +++ /dev/null @@ -1,737 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Germán Márquez Mejía -// SPDX-FileCopyrightText: 2021 Melvin Keskin -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include "QXmppConstants_p.h" -#include "QXmppOmemoDeviceBundle_p.h" -#include "QXmppOmemoDeviceElement_p.h" -#include "QXmppOmemoDeviceList_p.h" -#include "QXmppOmemoElement_p.h" -#include "QXmppOmemoEnvelope_p.h" -#include "QXmppOmemoIq_p.h" -#include "QXmppOmemoItems_p.h" -#include "QXmppUtils.h" - -#include -#include - -/// \cond -/// -/// \class QXmppOmemoDeviceElement -/// -/// \brief The QXmppOmemoDeviceElement class represents an element of the -/// OMEMO device list as defined by \xep{0384, OMEMO Encryption}. -/// - -/// -/// Returns true if the IDs of both elements match. -/// -bool QXmppOmemoDeviceElement::operator==(const QXmppOmemoDeviceElement &other) const -{ - return m_id == other.id(); -} - -/// -/// Returns the ID of this device element. -/// -/// The ID is used to identify a device and fetch its bundle. -/// The ID is 0 if it is unset. -/// -/// \see QXmppOmemoDeviceBundle -/// -/// \return this device element's ID -/// -uint32_t QXmppOmemoDeviceElement::id() const -{ - return m_id; -} - -/// -/// Sets the ID of this device element. -/// -/// The ID must be at least 1 and at most -/// \c std::numeric_limits::max(). -/// -/// \param id this device element's ID -/// -void QXmppOmemoDeviceElement::setId(uint32_t id) -{ - m_id = id; -} - -/// -/// Returns the label of this device element. -/// -/// The label is a human-readable string used to identify the device by users. -/// If no label is set, a default-constructed QString is returned. -/// -/// \return this device element's label -/// -QString QXmppOmemoDeviceElement::label() const -{ - return m_label; -} - -/// -/// Sets the optional label of this device element. -/// -/// The label should not contain more than 53 characters. -/// -/// \param label this device element's label -/// -void QXmppOmemoDeviceElement::setLabel(const QString &label) -{ - m_label = label; -} - -void QXmppOmemoDeviceElement::parse(const QDomElement &element) -{ - m_id = element.attribute(QStringLiteral("id")).toInt(); - m_label = element.attribute(QStringLiteral("label")); -} - -void QXmppOmemoDeviceElement::toXml(QXmlStreamWriter *writer) const -{ - writer->writeStartElement(QStringLiteral("device")); - - writer->writeAttribute(QStringLiteral("id"), QString::number(m_id)); - if (!m_label.isEmpty()) { - writer->writeAttribute(QStringLiteral("label"), m_label); - } - - writer->writeEndElement(); // device -} - -/// -/// Determines whether the given DOM element is an OMEMO device element. -/// -/// \param element DOM element being checked -/// -/// \return true if element is an OMEMO device element, otherwise false -/// -bool QXmppOmemoDeviceElement::isOmemoDeviceElement(const QDomElement &element) -{ - return element.tagName() == QStringLiteral("device") && - element.namespaceURI() == ns_omemo_2; -} - -/// -/// \class QXmppOmemoDeviceList -/// -/// \brief The QXmppOmemoDeviceList class represents an OMEMO device list -/// as defined by \xep{0384, OMEMO Encryption}. -/// - -void QXmppOmemoDeviceList::parse(const QDomElement &element) -{ - for (auto device = element.firstChildElement(QStringLiteral("device")); - !device.isNull(); - device = device.nextSiblingElement(QStringLiteral("device"))) { - QXmppOmemoDeviceElement deviceElement; - deviceElement.parse(device); - append(deviceElement); - } -} - -void QXmppOmemoDeviceList::toXml(QXmlStreamWriter *writer) const -{ - writer->writeStartElement(QStringLiteral("devices")); - writer->writeDefaultNamespace(ns_omemo_2); - - for (const auto &device : *this) { - device.toXml(writer); - } - - writer->writeEndElement(); -} - -/// -/// Determines whether the given DOM element is an OMEMO device list. -/// -/// \param element DOM element being checked -/// -/// \return true if element is an OMEMO device list, otherwise false -/// -bool QXmppOmemoDeviceList::isOmemoDeviceList(const QDomElement &element) -{ - return element.tagName() == QStringLiteral("devices") && - element.namespaceURI() == ns_omemo_2; -} - -/// -/// \class QXmppOmemoDeviceBundle -/// -/// \brief The QXmppOmemoDeviceBundle class represents an OMEMO bundle as -/// defined by \xep{0384, OMEMO Encryption}. -/// -/// It is a collection of publicly accessible data used by the X3DH key exchange. -/// The data is used to build an encrypted session with an OMEMO device. -/// - -/// -/// Returns the public identity key. -/// -/// The public identity key is the public long-term key which never changes. -/// -/// \return the public identity key -/// -QByteArray QXmppOmemoDeviceBundle::publicIdentityKey() const -{ - return m_publicIdentityKey; -} - -/// -/// Sets the public identity key. -/// -/// \param key public identity key -/// -void QXmppOmemoDeviceBundle::setPublicIdentityKey(const QByteArray &key) -{ - m_publicIdentityKey = key; -} - -/// -/// Returns the public pre key that is signed. -/// -/// \return the signed public pre key -/// -QByteArray QXmppOmemoDeviceBundle::signedPublicPreKey() const -{ - return m_signedPublicPreKey; -} - -/// -/// Sets the public pre key that is signed. -/// -/// \param key signed public pre key -/// -void QXmppOmemoDeviceBundle::setSignedPublicPreKey(const QByteArray &key) -{ - m_signedPublicPreKey = key; -} - -/// -/// Returns the ID of the public pre key that is signed. -/// -/// The ID is 0 if it is unset. -/// -/// \return the ID of the signed public pre key -/// -uint32_t QXmppOmemoDeviceBundle::signedPublicPreKeyId() const -{ - return m_signedPublicPreKeyId; -} - -/// -/// Sets the ID of the public pre key that is signed. -/// -/// The ID must be at least 1 and at most -/// \c std::numeric_limits::max(). -/// -/// \param id ID of the signed public pre key -/// -void QXmppOmemoDeviceBundle::setSignedPublicPreKeyId(uint32_t id) -{ - m_signedPublicPreKeyId = id; -} - -/// -/// Returns the signature of the public pre key that is signed. -/// -/// \return the signature of the signed public pre key -/// -QByteArray QXmppOmemoDeviceBundle::signedPublicPreKeySignature() const -{ - return m_signedPublicPreKeySignature; -} - -/// -/// Sets the signature of the public pre key that is signed. -/// -/// \param signature signature of the signed public pre key -/// -void QXmppOmemoDeviceBundle::setSignedPublicPreKeySignature(const QByteArray &signature) -{ - m_signedPublicPreKeySignature = signature; -} - -/// -/// Returns the public pre keys. -/// -/// The key of a key-value pair represents the ID of the corresponding public -/// pre key. -/// The value of a key-value pair represents the public pre key. -/// -/// \return the public pre keys -/// -QHash QXmppOmemoDeviceBundle::publicPreKeys() const -{ - return m_publicPreKeys; -} - -/// -/// Adds a public pre key. -/// -/// The ID must be at least 1 and at most -/// \c std::numeric_limits::max(). -/// -/// \param id ID of the public pre key -/// \param key public pre key -/// -void QXmppOmemoDeviceBundle::addPublicPreKey(uint32_t id, const QByteArray &key) -{ - m_publicPreKeys.insert(id, key); -} - -/// -/// Removes a public pre key. -/// -/// \param id ID of the public pre key -/// \param key public pre key -/// -void QXmppOmemoDeviceBundle::removePublicPreKey(uint32_t id) -{ - m_publicPreKeys.remove(id); -} - -void QXmppOmemoDeviceBundle::parse(const QDomElement &element) -{ - m_publicIdentityKey = QByteArray::fromBase64(element.firstChildElement(QStringLiteral("ik")).text().toLatin1()); - - const auto signedPublicPreKeyElement = element.firstChildElement(QStringLiteral("spk")); - if (!signedPublicPreKeyElement.isNull()) { - m_signedPublicPreKeyId = signedPublicPreKeyElement.attribute(QStringLiteral("id")).toInt(); - m_signedPublicPreKey = QByteArray::fromBase64(signedPublicPreKeyElement.text().toLatin1()); - } - m_signedPublicPreKeySignature = QByteArray::fromBase64(element.firstChildElement(QStringLiteral("spks")).text().toLatin1()); - - const auto publicPreKeysElement = element.firstChildElement(QStringLiteral("prekeys")); - if (!publicPreKeysElement.isNull()) { - for (QDomElement publicPreKeyElement = publicPreKeysElement.firstChildElement(QStringLiteral("pk")); - !publicPreKeyElement.isNull(); - publicPreKeyElement = publicPreKeyElement.nextSiblingElement(QStringLiteral("pk"))) { - m_publicPreKeys.insert(publicPreKeyElement.attribute(QStringLiteral("id")).toInt(), QByteArray::fromBase64(publicPreKeyElement.text().toLatin1())); - } - } -} - -void QXmppOmemoDeviceBundle::toXml(QXmlStreamWriter *writer) const -{ - writer->writeStartElement(QStringLiteral("bundle")); - writer->writeDefaultNamespace(ns_omemo_2); - - writer->writeStartElement(QStringLiteral("ik")); - writer->writeCharacters(publicIdentityKey().toBase64()); - writer->writeEndElement(); - - writer->writeStartElement(QStringLiteral("spk")); - writer->writeAttribute(QStringLiteral("id"), QString::number(signedPublicPreKeyId())); - writer->writeCharacters(signedPublicPreKey().toBase64()); - writer->writeEndElement(); - - writer->writeStartElement(QStringLiteral("spks")); - writer->writeCharacters(signedPublicPreKeySignature().toBase64()); - writer->writeEndElement(); - - writer->writeStartElement(QStringLiteral("prekeys")); - for (auto it = m_publicPreKeys.cbegin(); it != m_publicPreKeys.cend(); it++) { - writer->writeStartElement(QStringLiteral("pk")); - writer->writeAttribute(QStringLiteral("id"), QString::number(it.key())); - writer->writeCharacters(it.value().toBase64()); - writer->writeEndElement(); - } - writer->writeEndElement(); // prekeys - - writer->writeEndElement(); // bundle -} - -/// -/// Determines whether the given DOM element is an OMEMO device bundle. -/// -/// \param element DOM element being checked -/// -/// \return true if element is an OMEMO device bundle, otherwise false -/// -bool QXmppOmemoDeviceBundle::isOmemoDeviceBundle(const QDomElement &element) -{ - return element.tagName() == QStringLiteral("bundle") && - element.namespaceURI() == ns_omemo_2; -} - -/// -/// \class QXmppOmemoEnvelope -/// -/// \brief The QXmppOmemoEnvelope class represents an OMEMO envelope as -/// defined by \xep{0384, OMEMO Encryption}. -/// - -/// -/// Returns the ID of the recipient's device. -/// -/// The ID is 0 if it is unset. -/// -/// \return the recipient's device ID -/// -uint32_t QXmppOmemoEnvelope::recipientDeviceId() const -{ - return m_recipientDeviceId; -} - -/// -/// Sets the ID of the recipient's device. -/// -/// The ID must be at least 1 and at most -/// \c std::numeric_limits::max(). -/// -/// \param id recipient's device ID -/// -void QXmppOmemoEnvelope::setRecipientDeviceId(uint32_t id) -{ - m_recipientDeviceId = id; -} - -/// -/// Returns true if a pre-key was used to prepare this envelope. -/// -/// The default is false. -/// -/// \return true if a pre-key was used to prepare this envelope, otherwise false -/// -bool QXmppOmemoEnvelope::isUsedForKeyExchange() const -{ - return m_isUsedForKeyExchange; -} - -/// -/// Sets whether a pre-key was used to prepare this envelope. -/// -/// \param isUsed whether a pre-key was used to prepare this envelope -/// -void QXmppOmemoEnvelope::setIsUsedForKeyExchange(bool isUsed) -{ - m_isUsedForKeyExchange = isUsed; -} - -/// -/// Returns the BLOB containing the data for the underlying double ratchet library. -/// -/// It should be treated like an obscure BLOB being passed as is to the ratchet -/// library for further processing. -/// -/// \return the binary data for the ratchet library -/// -QByteArray QXmppOmemoEnvelope::data() const -{ - return m_data; -} - -/// -/// Sets the BLOB containing the data from the underlying double ratchet library. -/// -/// It should be treated like an obscure BLOB produced by the ratchet library. -/// -/// \param data binary data from the ratchet library -/// -void QXmppOmemoEnvelope::setData(const QByteArray &data) -{ - m_data = data; -} - -void QXmppOmemoEnvelope::parse(const QDomElement &element) -{ - m_recipientDeviceId = element.attribute(QStringLiteral("rid")).toInt(); - - const auto isUsedForKeyExchange = element.attribute(QStringLiteral("kex")); - if (isUsedForKeyExchange == QStringLiteral("true") || isUsedForKeyExchange == QStringLiteral("1")) { - m_isUsedForKeyExchange = true; - } - - m_data = QByteArray::fromBase64(element.text().toLatin1()); -} - -void QXmppOmemoEnvelope::toXml(QXmlStreamWriter *writer) const -{ - writer->writeStartElement(QStringLiteral("key")); - writer->writeAttribute(QStringLiteral("rid"), QString::number(m_recipientDeviceId)); - - if (m_isUsedForKeyExchange) { - helperToXmlAddAttribute(writer, QStringLiteral("kex"), QStringLiteral("true")); - } - - writer->writeCharacters(m_data.toBase64()); - writer->writeEndElement(); -} - -/// -/// Determines whether the given DOM element is an OMEMO envelope. -/// -/// \param element DOM element being checked -/// -/// \return true if element is an OMEMO envelope, otherwise false -/// -bool QXmppOmemoEnvelope::isOmemoEnvelope(const QDomElement &element) -{ - return element.tagName() == QStringLiteral("key") && - element.namespaceURI() == ns_omemo_2; -} - -/// -/// \class QXmppOmemoElement -/// -/// \brief The QXmppOmemoElement class represents an OMEMO element as -/// defined by \xep{0384, OMEMO Encryption}. -/// - -/// -/// Returns the ID of the sender's device. -/// -/// The ID is 0 if it is unset. -/// -/// \return the sender's device ID -/// -uint32_t QXmppOmemoElement::senderDeviceId() const -{ - return m_senderDeviceId; -} - -/// -/// Sets the ID of the sender's device. -/// -/// The ID must be at least 1 and at most -/// \c std::numeric_limits::max(). -/// -/// \param id sender's device ID -/// -void QXmppOmemoElement::setSenderDeviceId(uint32_t id) -{ - m_senderDeviceId = id; -} - -/// -/// Returns the payload which consists of the encrypted SCE envelope. -/// -/// \return the encrypted payload -/// -QByteArray QXmppOmemoElement::payload() const -{ - return m_payload; -} - -/// -/// Sets the payload which consists of the encrypted SCE envelope. -/// -/// \param payload encrypted payload -/// -void QXmppOmemoElement::setPayload(const QByteArray &payload) -{ - m_payload = payload; -} - -/// -/// Searches for an OMEMO envelope by its recipient JID and device ID. -/// -/// \param recipientJid bare JID of the recipient -/// \param recipientDeviceId ID of the recipient's device -/// -/// \return the found OMEMO envelope -/// -std::optional QXmppOmemoElement::searchEnvelope(const QString &recipientJid, uint32_t recipientDeviceId) const -{ - for (auto itr = m_envelopes.constFind(recipientJid); itr != m_envelopes.constEnd() && itr.key() == recipientJid; ++itr) { - const auto &envelope = itr.value(); - if (envelope.recipientDeviceId() == recipientDeviceId) { - return envelope; - } - } - - return std::nullopt; -} - -/// -/// Adds an OMEMO envelope. -/// -/// If a full JID is passed as recipientJid, it is converted into a bare JID. -/// -/// \see QXmppOmemoEnvelope -/// -/// \param recipientJid bare JID of the recipient -/// \param envelope OMEMO envelope -/// -void QXmppOmemoElement::addEnvelope(const QString &recipientJid, const QXmppOmemoEnvelope &envelope) -{ - m_envelopes.insert(QXmppUtils::jidToBareJid(recipientJid), envelope); -} - -void QXmppOmemoElement::parse(const QDomElement &element) -{ - const auto header = element.firstChildElement(QStringLiteral("header")); - - m_senderDeviceId = header.attribute(QStringLiteral("sid")).toInt(); - - for (auto recipient = header.firstChildElement(QStringLiteral("keys")); - !recipient.isNull(); - recipient = recipient.nextSiblingElement(QStringLiteral("keys"))) { - const auto recipientJid = recipient.attribute(QStringLiteral("jid")); - - for (auto envelope = recipient.firstChildElement(QStringLiteral("key")); - !envelope.isNull(); - envelope = envelope.nextSiblingElement(QStringLiteral("key"))) { - QXmppOmemoEnvelope omemoEnvelope; - omemoEnvelope.parse(envelope); - addEnvelope(recipientJid, omemoEnvelope); - } - } - - m_payload = QByteArray::fromBase64(element.firstChildElement(QStringLiteral("payload")).text().toLatin1()); -} - -void QXmppOmemoElement::toXml(QXmlStreamWriter *writer) const -{ - writer->writeStartElement(QStringLiteral("encrypted")); - writer->writeDefaultNamespace(ns_omemo_2); - - writer->writeStartElement(QStringLiteral("header")); - writer->writeAttribute(QStringLiteral("sid"), QString::number(m_senderDeviceId)); - - const auto recipientJids = m_envelopes.uniqueKeys(); - for (const auto &recipientJid : recipientJids) { - writer->writeStartElement(QStringLiteral("keys")); - writer->writeAttribute(QStringLiteral("jid"), recipientJid); - - for (auto itr = m_envelopes.constFind(recipientJid); itr != m_envelopes.constEnd() && itr.key() == recipientJid; ++itr) { - const auto &envelope = itr.value(); - envelope.toXml(writer); - } - - writer->writeEndElement(); // keys - } - - writer->writeEndElement(); // header - - // The payload element is only included if there is a payload. - // An empty OMEMO message does not contain a payload. - if (!m_payload.isEmpty()) { - writer->writeTextElement(QStringLiteral("payload"), m_payload.toBase64()); - } - - writer->writeEndElement(); // encrypted -} - -/// -/// Determines whether the given DOM element is an OMEMO element. -/// -/// \param element DOM element being checked -/// -/// \return true if element is an OMEMO element, otherwise false -/// -bool QXmppOmemoElement::isOmemoElement(const QDomElement &element) -{ - return element.tagName() == QStringLiteral("encrypted") && - element.namespaceURI() == ns_omemo_2; -} - -/// -/// \class QXmppOmemoIq -/// -/// \brief The QXmppOmemoIq class represents an encrypted IQ stanza as defined -/// by \xep{0384, OMEMO Encryption} and \xep{0420, Stanza Content Encryption} -/// (SCE). -/// -/// \ingroup Stanzas -/// - -/// -/// Returns the OMEMO element which contains the data used by OMEMO. -/// -/// \return the OMEMO element -/// -QXmppOmemoElement QXmppOmemoIq::omemoElement() -{ - return m_omemoElement; -} - -/// -/// Sets the OMEMO element which contains the data used by OMEMO. -/// -/// \param omemoElement OMEMO element -/// -void QXmppOmemoIq::setOmemoElement(const QXmppOmemoElement &omemoElement) -{ - m_omemoElement = omemoElement; -} - -void QXmppOmemoIq::parseElementFromChild(const QDomElement &element) -{ - QDomElement child = element.firstChildElement(); - m_omemoElement.parse(child); -} - -void QXmppOmemoIq::toXmlElementFromChild(QXmlStreamWriter *writer) const -{ - m_omemoElement.toXml(writer); -} - -/// -/// Determines whether the given DOM element is an OMEMO IQ stanza. -/// -/// \param element DOM element being checked -/// -/// \return true if element is an OMEMO IQ stanza, otherwise false -/// -bool QXmppOmemoIq::isOmemoIq(const QDomElement &element) -{ - auto child = element.firstChildElement(); - return !child.isNull() && QXmppOmemoElement::isOmemoElement(child); -} - -QXmppOmemoDeviceBundle QXmppOmemoDeviceBundleItem::deviceBundle() const -{ - return m_deviceBundle; -} - -void QXmppOmemoDeviceBundleItem::setDeviceBundle(const QXmppOmemoDeviceBundle &deviceBundle) -{ - m_deviceBundle = deviceBundle; -} - -bool QXmppOmemoDeviceBundleItem::isItem(const QDomElement &itemElement) -{ - return QXmppPubSubItem::isItem(itemElement, QXmppOmemoDeviceBundle::isOmemoDeviceBundle); -} - -void QXmppOmemoDeviceBundleItem::parsePayload(const QDomElement &payloadElement) -{ - m_deviceBundle.parse(payloadElement); -} - -void QXmppOmemoDeviceBundleItem::serializePayload(QXmlStreamWriter *writer) const -{ - m_deviceBundle.toXml(writer); -} - -QXmppOmemoDeviceList QXmppOmemoDeviceListItem::deviceList() const -{ - return m_deviceList; -} - -void QXmppOmemoDeviceListItem::setDeviceList(const QXmppOmemoDeviceList &deviceList) -{ - m_deviceList = deviceList; -} - -bool QXmppOmemoDeviceListItem::isItem(const QDomElement &itemElement) -{ - return QXmppPubSubItem::isItem(itemElement, QXmppOmemoDeviceList::isOmemoDeviceList); -} - -void QXmppOmemoDeviceListItem::parsePayload(const QDomElement &payloadElement) -{ - m_deviceList.parse(payloadElement); -} - -void QXmppOmemoDeviceListItem::serializePayload(QXmlStreamWriter *writer) const -{ - m_deviceList.toXml(writer); -} -/// \endcond diff --git a/src/base/QXmppOmemoDataBase.cpp b/src/base/QXmppOmemoDataBase.cpp new file mode 100644 index 00000000..a75d54ea --- /dev/null +++ b/src/base/QXmppOmemoDataBase.cpp @@ -0,0 +1,290 @@ +// SPDX-FileCopyrightText: 2021 Germán Márquez Mejía +// SPDX-FileCopyrightText: 2021 Melvin Keskin +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "QXmppConstants_p.h" +#include "QXmppOmemoElement_p.h" +#include "QXmppOmemoEnvelope_p.h" +#include "QXmppUtils.h" + +#include +#include + +/// \cond +/// +/// \class QXmppOmemoEnvelope +/// +/// \brief The QXmppOmemoEnvelope class represents an OMEMO envelope as +/// defined by \xep{0384, OMEMO Encryption}. +/// + +/// +/// Returns the ID of the recipient's device. +/// +/// The ID is 0 if it is unset. +/// +/// \return the recipient's device ID +/// +uint32_t QXmppOmemoEnvelope::recipientDeviceId() const +{ + return m_recipientDeviceId; +} + +/// +/// Sets the ID of the recipient's device. +/// +/// The ID must be at least 1 and at most \c std::numeric_limits::max(). +/// +/// \param id recipient's device ID +/// +void QXmppOmemoEnvelope::setRecipientDeviceId(uint32_t id) +{ + m_recipientDeviceId = id; +} + +/// +/// Returns true if a pre-key was used to prepare this envelope. +/// +/// The default is false. +/// +/// \return true if a pre-key was used to prepare this envelope, otherwise false +/// +bool QXmppOmemoEnvelope::isUsedForKeyExchange() const +{ + return m_isUsedForKeyExchange; +} + +/// +/// Sets whether a pre-key was used to prepare this envelope. +/// +/// \param isUsed whether a pre-key was used to prepare this envelope +/// +void QXmppOmemoEnvelope::setIsUsedForKeyExchange(bool isUsed) +{ + m_isUsedForKeyExchange = isUsed; +} + +/// +/// Returns the BLOB containing the data for the underlying double ratchet library. +/// +/// It should be treated like an obscure BLOB being passed as is to the ratchet +/// library for further processing. +/// +/// \return the binary data for the ratchet library +/// +QByteArray QXmppOmemoEnvelope::data() const +{ + return m_data; +} + +/// +/// Sets the BLOB containing the data from the underlying double ratchet library. +/// +/// It should be treated like an obscure BLOB produced by the ratchet library. +/// +/// \param data binary data from the ratchet library +/// +void QXmppOmemoEnvelope::setData(const QByteArray &data) +{ + m_data = data; +} + +void QXmppOmemoEnvelope::parse(const QDomElement &element) +{ + m_recipientDeviceId = element.attribute(QStringLiteral("rid")).toInt(); + + const auto isUsedForKeyExchange = element.attribute(QStringLiteral("kex")); + if (isUsedForKeyExchange == QStringLiteral("true") || + isUsedForKeyExchange == QStringLiteral("1")) { + m_isUsedForKeyExchange = true; + } + + m_data = QByteArray::fromBase64(element.text().toLatin1()); +} + +void QXmppOmemoEnvelope::toXml(QXmlStreamWriter *writer) const +{ + writer->writeStartElement(QStringLiteral("key")); + writer->writeAttribute(QStringLiteral("rid"), QString::number(m_recipientDeviceId)); + + if (m_isUsedForKeyExchange) { + helperToXmlAddAttribute(writer, QStringLiteral("kex"), QStringLiteral("true")); + } + + writer->writeCharacters(m_data.toBase64()); + writer->writeEndElement(); +} + +/// +/// Determines whether the given DOM element is an OMEMO envelope. +/// +/// \param element DOM element being checked +/// +/// \return true if element is an OMEMO envelope, otherwise false +/// +bool QXmppOmemoEnvelope::isOmemoEnvelope(const QDomElement &element) +{ + return element.tagName() == QStringLiteral("key") && + element.namespaceURI() == ns_omemo_2; +} + +/// +/// \class QXmppOmemoElement +/// +/// \brief The QXmppOmemoElement class represents an OMEMO element as +/// defined by \xep{0384, OMEMO Encryption}. +/// + +/// +/// Returns the ID of the sender's device. +/// +/// The ID is 0 if it is unset. +/// +/// \return the sender's device ID +/// +uint32_t QXmppOmemoElement::senderDeviceId() const +{ + return m_senderDeviceId; +} + +/// +/// Sets the ID of the sender's device. +/// +/// The ID must be at least 1 and at most +/// \c std::numeric_limits::max(). +/// +/// \param id sender's device ID +/// +void QXmppOmemoElement::setSenderDeviceId(uint32_t id) +{ + m_senderDeviceId = id; +} + +/// +/// Returns the payload which consists of the encrypted SCE envelope. +/// +/// \return the encrypted payload +/// +QByteArray QXmppOmemoElement::payload() const +{ + return m_payload; +} + +/// +/// Sets the payload which consists of the encrypted SCE envelope. +/// +/// \param payload encrypted payload +/// +void QXmppOmemoElement::setPayload(const QByteArray &payload) +{ + m_payload = payload; +} + +/// +/// Searches for an OMEMO envelope by its recipient JID and device ID. +/// +/// \param recipientJid bare JID of the recipient +/// \param recipientDeviceId ID of the recipient's device +/// +/// \return the found OMEMO envelope +/// +std::optional QXmppOmemoElement::searchEnvelope(const QString &recipientJid, uint32_t recipientDeviceId) const +{ + for (auto itr = m_envelopes.constFind(recipientJid); + itr != m_envelopes.constEnd() && itr.key() == recipientJid; + ++itr) { + const auto &envelope = itr.value(); + if (envelope.recipientDeviceId() == recipientDeviceId) { + return envelope; + } + } + + return std::nullopt; +} + +/// +/// Adds an OMEMO envelope. +/// +/// If a full JID is passed as recipientJid, it is converted into a bare JID. +/// +/// \see QXmppOmemoEnvelope +/// +/// \param recipientJid bare JID of the recipient +/// \param envelope OMEMO envelope +/// +void QXmppOmemoElement::addEnvelope(const QString &recipientJid, const QXmppOmemoEnvelope &envelope) +{ + m_envelopes.insert(QXmppUtils::jidToBareJid(recipientJid), envelope); +} + +void QXmppOmemoElement::parse(const QDomElement &element) +{ + const auto header = element.firstChildElement(QStringLiteral("header")); + + m_senderDeviceId = header.attribute(QStringLiteral("sid")).toInt(); + + for (auto recipient = header.firstChildElement(QStringLiteral("keys")); + !recipient.isNull(); + recipient = recipient.nextSiblingElement(QStringLiteral("keys"))) { + const auto recipientJid = recipient.attribute(QStringLiteral("jid")); + + for (auto envelope = recipient.firstChildElement(QStringLiteral("key")); + !envelope.isNull(); + envelope = envelope.nextSiblingElement(QStringLiteral("key"))) { + QXmppOmemoEnvelope omemoEnvelope; + omemoEnvelope.parse(envelope); + addEnvelope(recipientJid, omemoEnvelope); + } + } + + m_payload = QByteArray::fromBase64(element.firstChildElement(QStringLiteral("payload")).text().toLatin1()); +} + +void QXmppOmemoElement::toXml(QXmlStreamWriter *writer) const +{ + writer->writeStartElement(QStringLiteral("encrypted")); + writer->writeDefaultNamespace(ns_omemo_2); + + writer->writeStartElement(QStringLiteral("header")); + writer->writeAttribute(QStringLiteral("sid"), QString::number(m_senderDeviceId)); + + const auto recipientJids = m_envelopes.uniqueKeys(); + for (const auto &recipientJid : recipientJids) { + writer->writeStartElement(QStringLiteral("keys")); + writer->writeAttribute(QStringLiteral("jid"), recipientJid); + + for (auto itr = m_envelopes.constFind(recipientJid); + itr != m_envelopes.constEnd() && itr.key() == recipientJid; + ++itr) { + const auto &envelope = itr.value(); + envelope.toXml(writer); + } + + writer->writeEndElement(); // keys + } + + writer->writeEndElement(); // header + + // The payload element is only included if there is a payload. + // An empty OMEMO message does not contain a payload. + if (!m_payload.isEmpty()) { + writer->writeTextElement(QStringLiteral("payload"), m_payload.toBase64()); + } + + writer->writeEndElement(); // encrypted +} + +/// +/// Determines whether the given DOM element is an OMEMO element. +/// +/// \param element DOM element being checked +/// +/// \return true if element is an OMEMO element, otherwise false +/// +bool QXmppOmemoElement::isOmemoElement(const QDomElement &element) +{ + return element.tagName() == QStringLiteral("encrypted") && + element.namespaceURI() == ns_omemo_2; +} +/// \endcond diff --git a/src/base/QXmppOmemoDeviceBundle_p.h b/src/base/QXmppOmemoDeviceBundle_p.h deleted file mode 100644 index 49506e86..00000000 --- a/src/base/QXmppOmemoDeviceBundle_p.h +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Germán Márquez Mejía -// SPDX-FileCopyrightText: 2021 Melvin Keskin -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#ifndef QXMPPOMEMODEVICEBUNDLE_H -#define QXMPPOMEMODEVICEBUNDLE_H - -#include "QXmppGlobal.h" - -#include - -class QDomElement; -class QXmlStreamWriter; - -class QXMPP_AUTOTEST_EXPORT QXmppOmemoDeviceBundle -{ -public: - QByteArray publicIdentityKey() const; - void setPublicIdentityKey(const QByteArray &key); - - QByteArray signedPublicPreKey() const; - void setSignedPublicPreKey(const QByteArray &key); - - uint32_t signedPublicPreKeyId() const; - void setSignedPublicPreKeyId(uint32_t id); - - QByteArray signedPublicPreKeySignature() const; - void setSignedPublicPreKeySignature(const QByteArray &signature); - - QHash publicPreKeys() const; - void addPublicPreKey(uint32_t id, const QByteArray &key); - void removePublicPreKey(uint32_t id); - - /// \cond - void parse(const QDomElement &element); - void toXml(QXmlStreamWriter *writer) const; - /// \endcond - - static bool isOmemoDeviceBundle(const QDomElement &element); - -private: - QByteArray m_publicIdentityKey; - QByteArray m_signedPublicPreKey; - uint32_t m_signedPublicPreKeyId = 0; - QByteArray m_signedPublicPreKeySignature; - QHash m_publicPreKeys; -}; - -Q_DECLARE_TYPEINFO(QXmppOmemoDeviceBundle, Q_MOVABLE_TYPE); - -#endif // QXMPPOMEMODEVICEBUNDLE_H diff --git a/src/base/QXmppOmemoDeviceElement_p.h b/src/base/QXmppOmemoDeviceElement_p.h deleted file mode 100644 index 1c391e30..00000000 --- a/src/base/QXmppOmemoDeviceElement_p.h +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Germán Márquez Mejía -// SPDX-FileCopyrightText: 2021 Melvin Keskin -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#ifndef QXMPPOMEMODEVICEELEMENT_H -#define QXMPPOMEMODEVICEELEMENT_H - -#include "QXmppGlobal.h" - -class QDomElement; -class QXmlStreamWriter; - -class QXMPP_AUTOTEST_EXPORT QXmppOmemoDeviceElement -{ -public: - bool operator==(const QXmppOmemoDeviceElement &other) const; - - uint32_t id() const; - void setId(uint32_t id); - - QString label() const; - void setLabel(const QString &label); - - /// \cond - void parse(const QDomElement &element); - void toXml(QXmlStreamWriter *writer) const; - /// \endcond - - static bool isOmemoDeviceElement(const QDomElement &element); - -private: - uint32_t m_id = 0; - QString m_label; -}; - -Q_DECLARE_TYPEINFO(QXmppOmemoDeviceElement, Q_MOVABLE_TYPE); - -#endif // QXMPPOMEMODEVICEELEMENT_H diff --git a/src/base/QXmppOmemoDeviceList_p.h b/src/base/QXmppOmemoDeviceList_p.h deleted file mode 100644 index 76bff435..00000000 --- a/src/base/QXmppOmemoDeviceList_p.h +++ /dev/null @@ -1,30 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Germán Márquez Mejía -// SPDX-FileCopyrightText: 2021 Melvin Keskin -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#ifndef QXMPPOMEMODEVICELIST_H -#define QXMPPOMEMODEVICELIST_H - -#include "QXmppGlobal.h" - -#include "QList" - -class QDomElement; -class QXmlStreamWriter; -class QXmppOmemoDeviceElement; - -class QXMPP_AUTOTEST_EXPORT QXmppOmemoDeviceList : public QList -{ -public: - /// \cond - void parse(const QDomElement &element); - void toXml(QXmlStreamWriter *writer) const; - /// \endcond - - static bool isOmemoDeviceList(const QDomElement &element); -}; - -Q_DECLARE_TYPEINFO(QXmppOmemoDeviceList, Q_MOVABLE_TYPE); - -#endif // QXMPPOMEMODEVICELIST_H diff --git a/src/base/QXmppOmemoElement_p.h b/src/base/QXmppOmemoElement_p.h index bd113d04..1e160479 100644 --- a/src/base/QXmppOmemoElement_p.h +++ b/src/base/QXmppOmemoElement_p.h @@ -16,7 +16,7 @@ class QDomElement; class QXmppOmemoEnvelope; class QXmlStreamWriter; -class QXMPP_AUTOTEST_EXPORT QXmppOmemoElement +class QXMPP_EXPORT QXmppOmemoElement { public: uint32_t senderDeviceId() const; diff --git a/src/base/QXmppOmemoEnvelope_p.h b/src/base/QXmppOmemoEnvelope_p.h index 376a7ebf..d5eac395 100644 --- a/src/base/QXmppOmemoEnvelope_p.h +++ b/src/base/QXmppOmemoEnvelope_p.h @@ -11,7 +11,7 @@ class QDomElement; class QXmlStreamWriter; -class QXMPP_AUTOTEST_EXPORT QXmppOmemoEnvelope +class QXMPP_EXPORT QXmppOmemoEnvelope { public: uint32_t recipientDeviceId() const; diff --git a/src/base/QXmppOmemoIq_p.h b/src/base/QXmppOmemoIq_p.h deleted file mode 100644 index 15623884..00000000 --- a/src/base/QXmppOmemoIq_p.h +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Melvin Keskin -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#ifndef QXMPPOMEMOIQ_H -#define QXMPPOMEMOIQ_H - -#include "QXmppIq.h" -#include "QXmppOmemoElement_p.h" - -class QXMPP_AUTOTEST_EXPORT QXmppOmemoIq : public QXmppIq -{ -public: - QXmppOmemoElement omemoElement(); - void setOmemoElement(const QXmppOmemoElement &omemoElement); - - /// \cond - void parseElementFromChild(const QDomElement &element) override; - void toXmlElementFromChild(QXmlStreamWriter *writer) const override; - /// \endcond - - static bool isOmemoIq(const QDomElement &element); - -private: - QXmppOmemoElement m_omemoElement; -}; - -#endif // QXMPPOMEMOIQ_H diff --git a/src/base/QXmppOmemoItems_p.h b/src/base/QXmppOmemoItems_p.h deleted file mode 100644 index 9b816ed8..00000000 --- a/src/base/QXmppOmemoItems_p.h +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Melvin Keskin -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#ifndef QXMPPOMEMOITEMS_H -#define QXMPPOMEMOITEMS_H - -#include "QXmppOmemoDeviceBundle_p.h" -#include "QXmppOmemoDeviceList_p.h" -#include "QXmppPubSubItem.h" - -class QXmppOmemoDeviceBundleItem : public QXmppPubSubItem -{ -public: - QXmppOmemoDeviceBundle deviceBundle() const; - void setDeviceBundle(const QXmppOmemoDeviceBundle &deviceBundle); - - static bool isItem(const QDomElement &itemElement); - -protected: - void parsePayload(const QDomElement &payloadElement) override; - void serializePayload(QXmlStreamWriter *writer) const override; - -private: - QXmppOmemoDeviceBundle m_deviceBundle; -}; - -class QXmppOmemoDeviceListItem : public QXmppPubSubItem -{ -public: - QXmppOmemoDeviceList deviceList() const; - void setDeviceList(const QXmppOmemoDeviceList &deviceList); - - static bool isItem(const QDomElement &itemElement); - -protected: - void parsePayload(const QDomElement &payloadElement) override; - void serializePayload(QXmlStreamWriter *writer) const override; - -private: - QXmppOmemoDeviceList m_deviceList; -}; - -#endif // QXMPPOMEMOITEMS_H diff --git a/src/client/OmemoCryptoProvider.cpp b/src/client/OmemoCryptoProvider.cpp deleted file mode 100644 index e39124d4..00000000 --- a/src/client/OmemoCryptoProvider.cpp +++ /dev/null @@ -1,239 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Linus Jahn -// SPDX-FileCopyrightText: 2022 Melvin Keskin -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include "OmemoCryptoProvider.h" - -#include "QXmppOmemoManager_p.h" -#include "QXmppUtils_p.h" - -#include -#include - -using namespace QXmpp::Private; - -inline QXmppOmemoManagerPrivate *managerPrivate(void *ptr) -{ - return reinterpret_cast(ptr); -} - -static int random_func(uint8_t *data, size_t len, void *) -{ - generateRandomBytes(data, len); - return 0; -} - -int hmac_sha256_init_func(void **hmac_context, const uint8_t *key, size_t key_len, void *user_data) -{ - auto *d = managerPrivate(user_data); - - if (!QCA::MessageAuthenticationCode::supportedTypes().contains(PAYLOAD_MESSAGE_AUTHENTICATION_CODE_TYPE)) { - d->warning("Message authentication code type '" % QString(PAYLOAD_MESSAGE_AUTHENTICATION_CODE_TYPE) % "' is not supported by this system"); - return -1; - } - - QCA::SymmetricKey authenticationKey(QByteArray(reinterpret_cast(key), key_len)); - *hmac_context = new QCA::MessageAuthenticationCode(PAYLOAD_MESSAGE_AUTHENTICATION_CODE_TYPE, authenticationKey); - return 0; -} - -int hmac_sha256_update_func(void *hmac_context, const uint8_t *data, size_t data_len, void *) -{ - auto *messageAuthenticationCodeGenerator = reinterpret_cast(hmac_context); - messageAuthenticationCodeGenerator->update(QCA::MemoryRegion(QByteArray(reinterpret_cast(data), data_len))); - return 0; -} - -int hmac_sha256_final_func(void *hmac_context, signal_buffer **output, void *user_data) -{ - auto *d = managerPrivate(user_data); - auto *messageAuthenticationCodeGenerator = reinterpret_cast(hmac_context); - - auto messageAuthenticationCode = messageAuthenticationCodeGenerator->final(); - if (!(*output = signal_buffer_create(reinterpret_cast(messageAuthenticationCode.constData()), messageAuthenticationCode.size()))) { - d->warning("Message authentication code could not be loaded"); - return -1; - } - - return 0; -} - -void hmac_sha256_cleanup_func(void *hmac_context, void *) -{ - auto *messageAuthenticationCodeGenerator = reinterpret_cast(hmac_context); - delete messageAuthenticationCodeGenerator; -} - -int sha512_digest_init_func(void **digest_context, void *) -{ - *digest_context = new QCryptographicHash(QCryptographicHash::Sha512); - return 0; -} - -int sha512_digest_update_func(void *digest_context, const uint8_t *data, size_t data_len, void *) -{ - auto *hashGenerator = reinterpret_cast(digest_context); - hashGenerator->addData(reinterpret_cast(data), data_len); - return 0; -} - -int sha512_digest_final_func(void *digest_context, signal_buffer **output, void *user_data) -{ - auto *d = managerPrivate(user_data); - auto *hashGenerator = reinterpret_cast(digest_context); - - auto hash = hashGenerator->result(); - if (!(*output = signal_buffer_create(reinterpret_cast(hash.constData()), hash.size()))) { - d->warning("Hash could not be loaded"); - return -1; - } - - return 0; -} - -void sha512_digest_cleanup_func(void *digest_context, void *) -{ - auto *hashGenerator = reinterpret_cast(digest_context); - delete hashGenerator; -} - -int encrypt_func(signal_buffer **output, - int cipher, - const uint8_t *key, size_t key_len, - const uint8_t *iv, size_t iv_len, - const uint8_t *plaintext, size_t plaintext_len, - void *user_data) -{ - auto *d = managerPrivate(user_data); - - QString cipherName; - - switch (key_len) { - case 128 / 8: - cipherName = QStringLiteral("aes128"); - break; - case 192 / 8: - cipherName = QStringLiteral("aes192"); - break; - case 256 / 8: - cipherName = QStringLiteral("aes256"); - break; - default: - return -1; - } - - QCA::Cipher::Mode mode; - QCA::Cipher::Padding padding; - - switch (cipher) { - case SG_CIPHER_AES_CTR_NOPADDING: - mode = QCA::Cipher::CTR; - padding = QCA::Cipher::NoPadding; - break; - case SG_CIPHER_AES_CBC_PKCS5: - mode = QCA::Cipher::CBC; - padding = QCA::Cipher::PKCS7; - break; - default: - return -2; - } - - const auto encryptionKey = QCA::SymmetricKey(QByteArray(reinterpret_cast(key), key_len)); - const auto initializationVector = QCA::InitializationVector(QByteArray(reinterpret_cast(iv), iv_len)); - QCA::Cipher encryptionCipher(cipherName, mode, padding, QCA::Encode, encryptionKey, initializationVector); - - auto encryptedData = encryptionCipher.process(QCA::MemoryRegion(QByteArray(reinterpret_cast(plaintext), plaintext_len))); - - if (encryptedData.isEmpty()) { - return -3; - } - - if (!(*output = signal_buffer_create(reinterpret_cast(encryptedData.constData()), encryptedData.size()))) { - d->warning("Encrypted data could not be loaded"); - return -4; - } - - return 0; -} - -int decrypt_func(signal_buffer **output, - int cipher, - const uint8_t *key, size_t key_len, - const uint8_t *iv, size_t iv_len, - const uint8_t *ciphertext, size_t ciphertext_len, - void *user_data) -{ - auto *d = managerPrivate(user_data); - - QString cipherName; - - switch (key_len) { - case 128 / 8: - cipherName = QStringLiteral("aes128"); - break; - case 192 / 8: - cipherName = QStringLiteral("aes192"); - break; - case 256 / 8: - cipherName = QStringLiteral("aes256"); - break; - default: - return -1; - } - - QCA::Cipher::Mode mode; - QCA::Cipher::Padding padding; - - switch (cipher) { - case SG_CIPHER_AES_CTR_NOPADDING: - mode = QCA::Cipher::CTR; - padding = QCA::Cipher::NoPadding; - break; - case SG_CIPHER_AES_CBC_PKCS5: - mode = QCA::Cipher::CBC; - padding = QCA::Cipher::PKCS7; - break; - default: - return -2; - } - - const auto encryptionKey = QCA::SymmetricKey(QByteArray(reinterpret_cast(key), key_len)); - const auto initializationVector = QCA::InitializationVector(QByteArray(reinterpret_cast(iv), iv_len)); - QCA::Cipher decryptionCipher(cipherName, mode, padding, QCA::Decode, encryptionKey, initializationVector); - - auto decryptedData = decryptionCipher.process(QCA::MemoryRegion(QByteArray(reinterpret_cast(ciphertext), ciphertext_len))); - - if (decryptedData.isEmpty()) { - return -3; - } - - if (!(*output = signal_buffer_create(reinterpret_cast(decryptedData.constData()), decryptedData.size()))) { - d->warning("Decrypted data could not be loaded"); - return -4; - } - - return 0; -} - -namespace QXmpp::Omemo::Private { - -signal_crypto_provider createOmemoCryptoProvider(QXmppOmemoManagerPrivate *d) -{ - return { - random_func, - hmac_sha256_init_func, - hmac_sha256_update_func, - hmac_sha256_final_func, - hmac_sha256_cleanup_func, - sha512_digest_init_func, - sha512_digest_update_func, - sha512_digest_final_func, - sha512_digest_cleanup_func, - encrypt_func, - decrypt_func, - d, - }; -} - -} // namespace QXmpp::Omemo::Private diff --git a/src/client/OmemoCryptoProvider.h b/src/client/OmemoCryptoProvider.h deleted file mode 100644 index f4c63cd8..00000000 --- a/src/client/OmemoCryptoProvider.h +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Linus Jahn -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#ifndef OMEMOCRYPTOPROVIDER_H -#define OMEMOCRYPTOPROVIDER_H - -#include - -class QXmppOmemoManagerPrivate; - -namespace QXmpp::Omemo::Private { - -signal_crypto_provider createOmemoCryptoProvider(QXmppOmemoManagerPrivate *d); -} - -#endif // OMEMOCRYPTOPROVIDER_H diff --git a/src/client/OmemoLibWrappers.h b/src/client/OmemoLibWrappers.h deleted file mode 100644 index e157f12c..00000000 --- a/src/client/OmemoLibWrappers.h +++ /dev/null @@ -1,100 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Linus Jahn -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#ifndef OMEMOLIBWRAPPERS_H -#define OMEMOLIBWRAPPERS_H - -#include -#include -#include -#include - -// Wraps various types of the OMEMO library. -template -class OmemoLibPtr -{ - T *m_ptr = nullptr; - -public: - OmemoLibPtr(T *ptr = nullptr) : m_ptr(ptr) { } - OmemoLibPtr(const OmemoLibPtr &) = delete; - ~OmemoLibPtr() - { - if (m_ptr) { - destruct(m_ptr); - } - } - OmemoLibPtr &operator=(const OmemoLibPtr &) = delete; - OmemoLibPtr &operator=(T *ptr) - { - reset(ptr); - return *this; - } - operator bool() const { return m_ptr != nullptr; } - T *operator->() const { return m_ptr; } - T *get() const { return m_ptr; } - T **ptrRef() { return &m_ptr; } - void reset(T *ptr) - { - if (m_ptr) { - destruct(m_ptr); - } - m_ptr = ptr; - } -}; - -template -void omemoLibUnrefHelper(T *ptr) -{ - SIGNAL_UNREF(ptr); -} - -template -using RefCountedPtr = OmemoLibPtr>; - -static QByteArray omemoLibBufferToByteArray(signal_buffer *buffer) -{ - return QByteArray(reinterpret_cast(signal_buffer_data(buffer)), signal_buffer_len(buffer)); -} - -static signal_buffer *omemoLibBufferFromByteArray(const QByteArray &bytes) -{ - return signal_buffer_create(reinterpret_cast(bytes.constData()), bytes.size()); -} - -template -class BufferPtrBase : public OmemoLibPtr -{ -public: - QByteArray toByteArray() const - { - return omemoLibBufferToByteArray(this->get()); - } -}; - -class BufferSecurePtr : public BufferPtrBase -{ -public: - static BufferSecurePtr fromByteArray(const QByteArray &bytes) - { - return { omemoLibBufferFromByteArray(bytes) }; - } -}; - -class BufferPtr : public BufferPtrBase -{ -public: - static BufferPtr fromByteArray(const QByteArray &bytes) - { - return { omemoLibBufferFromByteArray(bytes) }; - } -}; - -using KeyListNodePtr = OmemoLibPtr; -using SessionCipherPtr = OmemoLibPtr; -using SessionBuilderPtr = OmemoLibPtr; -using OmemoContextPtr = OmemoLibPtr; -using StoreContextPtr = OmemoLibPtr; - -#endif // OMEMOLIBWRAPPERS_H diff --git a/src/client/QXmppOmemoManager.cpp b/src/client/QXmppOmemoManager.cpp deleted file mode 100644 index cb66a515..00000000 --- a/src/client/QXmppOmemoManager.cpp +++ /dev/null @@ -1,1282 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Melvin Keskin -// SPDX-FileCopyrightText: 2022 Linus Jahn -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include "QXmppClient.h" -#include "QXmppConstants_p.h" -#include "QXmppOmemoDeviceElement_p.h" -#include "QXmppOmemoDeviceList_p.h" -#include "QXmppOmemoElement_p.h" -#include "QXmppOmemoEnvelope_p.h" -#include "QXmppOmemoIq_p.h" -#include "QXmppOmemoItems_p.h" -#include "QXmppOmemoManager_p.h" -#include "QXmppPubSubEvent.h" -#include "QXmppTrustManager.h" -#include "QXmppUtils.h" - -#include - -using namespace QXmpp; -using namespace QXmpp::Private; -using namespace QXmpp::Omemo::Private; - -using Error = QXmppStanza::Error; -using Manager = QXmppOmemoManager; -using ManagerPrivate = QXmppOmemoManagerPrivate; - -// default label used for the own device -const auto DEVICE_LABEL = QStringLiteral("QXmpp"); - -class QXmppOmemoOwnDevicePrivate : public QSharedData -{ -public: - QString label; - QByteArray keyId; -}; - -/// -/// \class QXmppOmemoOwnDevice -/// -/// \brief The QXmppOmemoOwnDevice class represents the \xep{0384, OMEMO Encryption} device of this -/// client instance. -/// - -/// -/// Constructs an OMEMO device for this client instance. -/// -QXmppOmemoOwnDevice::QXmppOmemoOwnDevice() - : d(new QXmppOmemoOwnDevicePrivate) -{ -} - -/// Copy-constructor. -QXmppOmemoOwnDevice::QXmppOmemoOwnDevice(const QXmppOmemoOwnDevice &other) = default; -/// Move-constructor. -QXmppOmemoOwnDevice::QXmppOmemoOwnDevice(QXmppOmemoOwnDevice &&) noexcept = default; -QXmppOmemoOwnDevice::~QXmppOmemoOwnDevice() = default; -/// Assignment operator. -QXmppOmemoOwnDevice &QXmppOmemoOwnDevice::operator=(const QXmppOmemoOwnDevice &) = default; -/// Move-assignment operator. -QXmppOmemoOwnDevice &QXmppOmemoOwnDevice::operator=(QXmppOmemoOwnDevice &&) = default; - -/// -/// Returns the human-readable string used to identify the device by users. -/// -/// If no label is set, a default-constructed QString is returned. -/// -/// \return the label to identify the device -/// -QString QXmppOmemoOwnDevice::label() const -{ - return d->label; -} - -/// -/// Sets an optional human-readable string used to identify the device by users. -/// -/// The label should not contain more than 53 characters. -/// -/// \param label label to identify the device -/// -void QXmppOmemoOwnDevice::setLabel(const QString &label) -{ - d->label = label; -} - -/// -/// Returns the ID of the public long-term key which never changes. -/// -/// \return public long-term key ID -/// -QByteArray QXmppOmemoOwnDevice::keyId() const -{ - return d->keyId; -} - -/// -/// Sets the ID of the public long-term key which never changes. -/// -/// \param keyId public long-term key ID -/// -void QXmppOmemoOwnDevice::setKeyId(const QByteArray &keyId) -{ - d->keyId = keyId; -} - -class QXmppOmemoDevicePrivate : public QSharedData -{ -public: - QString jid; - TrustLevel trustLevel = TrustLevel::Undecided; - QString label; - QByteArray keyId; -}; - -/// -/// \class QXmppOmemoDevice -/// -/// \brief The QXmppOmemoDevice class represents a \xep{0384, OMEMO Encryption} device. -/// - -/// -/// Constructs an OMEMO device. -/// -QXmppOmemoDevice::QXmppOmemoDevice() - : d(new QXmppOmemoDevicePrivate) -{ -} - -/// Copy-constructor. -QXmppOmemoDevice::QXmppOmemoDevice(const QXmppOmemoDevice &other) = default; -/// Move-constructor. -QXmppOmemoDevice::QXmppOmemoDevice(QXmppOmemoDevice &&) noexcept = default; -QXmppOmemoDevice::~QXmppOmemoDevice() = default; -/// Assignment operator. -QXmppOmemoDevice &QXmppOmemoDevice::operator=(const QXmppOmemoDevice &) = default; -/// Move-assignment operator. -QXmppOmemoDevice &QXmppOmemoDevice::operator=(QXmppOmemoDevice &&) = default; - -/// -/// Returns the device owner's bare JID. -/// -/// \return the bare JID -/// -QString QXmppOmemoDevice::jid() const -{ - return d->jid; -} - -/// -/// Sets the device owner's bare JID. -/// -/// \param jid bare JID of the device owner -/// -void QXmppOmemoDevice::setJid(const QString &jid) -{ - d->jid = jid; -} - -/// -/// Returns the human-readable string used to identify the device by users. -/// -/// If no label is set, a default-constructed QString is returned. -/// -/// \return the label to identify the device -/// -QString QXmppOmemoDevice::label() const -{ - return d->label; -} - -/// -/// Sets an optional human-readable string used to identify the device by users. -/// -/// The label should not contain more than 53 characters. -/// -/// \param label label to identify the device -/// -void QXmppOmemoDevice::setLabel(const QString &label) -{ - d->label = label; -} - -/// -/// Returns the ID of the public long-term key which never changes. -/// -/// \return public long-term key ID -/// -QByteArray QXmppOmemoDevice::keyId() const -{ - return d->keyId; -} - -/// -/// Sets the ID of the public long-term key which never changes. -/// -/// \param keyId public long-term key ID -/// -void QXmppOmemoDevice::setKeyId(const QByteArray &keyId) -{ - d->keyId = keyId; -} - -/// -/// Returns the trust level of the key. -/// -/// \return the key's trust level -/// -TrustLevel QXmppOmemoDevice::trustLevel() const -{ - return d->trustLevel; -} - -/// -/// Sets the trust level of the key. -/// -/// \param trustLevel key's trust level -/// -void QXmppOmemoDevice::setTrustLevel(TrustLevel trustLevel) -{ - d->trustLevel = trustLevel; -} - -/// -/// \class QXmppOmemoManager -/// -/// The QXmppOmemoManager class manages OMEMO encryption as defined in \xep{0384, -/// OMEMO Encryption}. -/// -/// OMEMO uses \xep{0060, Publish-Subscribe} (PubSub) and \xep{0163, Personal Eventing Protocol} -/// (PEP). -/// Thus, they must be supported by the server and the corresponding PubSub manager must be added to -/// the client: -/// \code -/// QXmppPubSubManager *pubSubManager = new QXmppPubSubManager; -/// client->addExtension(pubSubManager); -/// \endcode -/// -/// For interacting with the storage, corresponding implementations of the storage interfaces must -/// be instantiated. -/// Those implementations have to be adapted to your storage such as a database. -/// In case you only need memory and no persistent storage, you can use the existing -/// implementations: -/// \code -/// QXmppOmemoStorage *omemoStorage = new QXmppOmemoMemoryStorage; -/// QXmppTrustStorage *trustStorage = new QXmppTrustMemoryStorage; -/// \endcode -/// -/// A trust manager using its storage must be added to the client: -/// \code -/// QXmppTrustManager *trustManager = new QXmppAtmManager(trustStorage); -/// client->addExtension(trustManager); -/// \endcode -/// -/// Afterwards, the OMEMO manager using its storage must be added to the client: -/// \code -/// QXmppOmemoManager *manager = new QXmppOmemoManager(omemoStorage); -/// client->addExtension(manager); -/// \endcode -/// -/// You can set a security policy used by OMEMO. -/// Is is recommended to apply TOAKAFA for good security and usability when using -/// \xep{0450, Automatic Trust Management (ATM)}: -/// \code -/// manager->setSecurityPolicy(QXmpp::Toakafa); -/// \endcode -/// -/// \xep{0280, Message Carbons} should be used for delivering messages to all endpoints of a user: -/// \code -/// QXmppCarbonManager *carbonManager = new QXmppCarbonManager; -/// client->addExtension(carbonManager); -/// connect(client, &QXmppClient::connected, this, [=]() { -/// carbonManager->setCarbonsEnabled(true); -/// }); -/// connect(carbonManager, &QXmppCarbonManager::messageSent, manager, -/// &QXmppOmemoManager::handleMessage); -/// connect(carbonManager, &QXmppCarbonManager::messageReceived, manager, -/// &QXmppOmemoManager::handleMessage); -/// \endcode -/// -/// The OMEMO data must be loaded before connecting to the server: -/// \code -/// manager->load(); -/// }); -/// \endcode -/// -/// If no OMEMO data could be loaded (i.e., the result of \c load() is "false"), it must be set up -/// first. -/// That can be done as soon as the user is logged in to the server: -/// \code -/// connect(client, &QXmppClient::connected, this, [=]() { -/// auto future = manager->start(); -/// }); -/// \endcode -/// -/// Once the future is finished and the result is "true", the manager is ready for use. -/// Otherwise, check the logging output for details. -/// -/// By default, stanzas are only sent to devices having keys with the following trust levels: -/// \code -/// QXmpp::TrustLevel::AutomaticallyTrusted | QXmpp::TrustLevel::ManuallyTrusted -/// | QXmpp::TrustLevel::Authenticated -/// \endcode -/// That behavior can be changed for each message being sent by specifying the -/// accepted trust levels: -/// \code -/// QXmppSendStanzaParams params; -/// params.setAcceptedTrustLevels(QXmpp::TrustLevel::Authenticated) -/// client->send(stanza, params); -/// \endcode -/// -/// Stanzas can be encrypted for multiple JIDs which is needed in group chats: -/// \code -/// QXmppSendStanzaParams params; -/// params.setEncryptionJids({ "alice@example.org", "bob@example.com" }) -/// client->send(stanza, params); -/// \endcode -/// -/// \warning THIS API IS NOT FINALIZED YET! -/// -/// \ingroup Managers -/// -/// \since QXmpp 1.5 -/// - -/// -/// \typedef QXmppOmemoManager::Result -/// -/// Contains QXmpp::Success for success or an QXmppStanza::Error for an error. -/// - -/// -/// Constructs an OMEMO manager. -/// -/// \param omemoStorage storage used to store all OMEMO data -/// -QXmppOmemoManager::QXmppOmemoManager(QXmppOmemoStorage *omemoStorage) - : d(new ManagerPrivate(this, omemoStorage)) -{ - d->ownDevice.label = DEVICE_LABEL; - d->init(); - d->schedulePeriodicTasks(); -} - -QXmppOmemoManager::~QXmppOmemoManager() = default; - -/// -/// Loads all locally stored OMEMO data. -/// -/// This should be called after starting the client and before the login. -/// It must only be called after \c setUp() has been called once for the user -/// during one of the past login session. -/// It does not need to be called if setUp() has been called during the current -/// login session. -/// -/// \see QXmppOmemoManager::setUp() -/// -/// \return whether everything is loaded successfully -/// -QFuture Manager::load() -{ - QFutureInterface interface(QFutureInterfaceBase::Started); - - auto future = d->omemoStorage->allData(); - await(future, this, [=](QXmppOmemoStorage::OmemoData omemoData) mutable { - const auto &optionalOwnDevice = omemoData.ownDevice; - if (optionalOwnDevice) { - d->ownDevice = *optionalOwnDevice; - } else { - debug("Device could not be loaded because it is not stored"); - reportFinishedResult(interface, false); - return; - } - - const auto &signedPreKeyPairs = omemoData.signedPreKeyPairs; - if (signedPreKeyPairs.isEmpty()) { - warning("Signed Pre keys could not be loaded because none is stored"); - reportFinishedResult(interface, false); - return; - } else { - d->signedPreKeyPairs = signedPreKeyPairs; - d->renewSignedPreKeyPairs(); - } - - const auto &preKeyPairs = omemoData.preKeyPairs; - if (preKeyPairs.isEmpty()) { - warning("Pre keys could not be loaded because none is stored"); - reportFinishedResult(interface, false); - return; - } else { - d->preKeyPairs = preKeyPairs; - } - - d->devices = omemoData.devices; - d->removeDevicesRemovedFromServer(); - - reportFinishedResult(interface, d->isStarted = true); - }); - - return interface.future(); -} - -/// -/// Sets up all OMEMO data locally and on the server. -/// -/// The user must be logged in while calling this. -/// -/// \return whether everything is set up successfully -/// -QFuture Manager::setUp() -{ - QFutureInterface interface(QFutureInterfaceBase::Started); - - auto future = d->setUpDeviceId(); - await(future, this, [=](bool isDeviceIdSetUp) mutable { - if (isDeviceIdSetUp) { - // The identity key pair in its deserialized form is not stored as a - // member variable because it is only needed by - // updateSignedPreKeyPair(). - RefCountedPtr identityKeyPair; - - if (d->setUpIdentityKeyPair(identityKeyPair.ptrRef()) && - d->updateSignedPreKeyPair(identityKeyPair.get()) && - d->updatePreKeyPairs(PRE_KEY_INITIAL_CREATION_COUNT)) { - auto future = d->omemoStorage->setOwnDevice(d->ownDevice); - await(future, this, [=]() mutable { - auto future = d->publishOmemoData(); - await(future, this, [=](bool isPublished) mutable { - reportFinishedResult(interface, d->isStarted = isPublished); - }); - }); - } else { - reportFinishedResult(interface, false); - } - } else { - reportFinishedResult(interface, false); - } - }); - - return interface.future(); -} - -/// -/// Returns the key of this client instance. -/// -/// \return the own key -/// -QFuture Manager::ownKey() -{ - return d->trustManager->ownKey(ns_omemo_2); -} - -/// -/// Returns the JIDs of all key owners mapped to the IDs of their keys with -/// specific trust levels. -/// -/// If no trust levels are passed, all keys are returned. -/// -/// This should be called in order to get all stored keys which can be more than -/// the stored devices because of trust decisions made without a published or -/// received device. -/// -/// \param trustLevels trust levels of the keys -/// -/// \return the key owner JIDs mapped to their keys with specific trust levels -/// -QFuture>> Manager::keys(QXmpp::TrustLevels trustLevels) -{ - return d->trustManager->keys(ns_omemo_2, trustLevels); -} - -/// -/// Returns the IDs of keys mapped to their trust levels for specific key -/// owners. -/// -/// If no trust levels are passed, all keys for jids are returned. -/// -/// This should be called in order to get the stored keys which can be more than -/// the stored devices because of trust decisions made without a published or -/// received device. -/// -/// \param jids key owners' bare JIDs -/// \param trustLevels trust levels of the keys -/// -/// \return the key IDs mapped to their trust levels for specific key owners -/// -QFuture>> Manager::keys(const QList &jids, QXmpp::TrustLevels trustLevels) -{ - return d->trustManager->keys(ns_omemo_2, jids, trustLevels); -} - -/// -/// Changes the label of the own (this client instance's current user's) device. -/// -/// The label is a human-readable string used to identify the device by users. -/// -/// If the OMEMO manager is not started yet, the device label is only changed -/// locally in memory. -/// It is stored persistently in the OMEMO storage and updated on the -/// server if the OMEMO manager is already started or once it is. -/// -/// \param deviceLabel own device's label -/// -/// \return whether the action was successful -/// -QFuture Manager::changeDeviceLabel(const QString &deviceLabel) -{ - return d->changeDeviceLabel(deviceLabel); -} - -/// -/// Returns the maximum count of devices stored per JID. -/// -/// If more devices than that maximum are received for one JID from a server, -/// they will not be stored locally and thus not used for encryption. -/// -/// \return the maximum count of devices stored per JID -/// -int Manager::maximumDevicesPerJid() const -{ - return d->maximumDevicesPerJid; -} - -/// -/// Sets the maximum count of devices stored per JID. -/// -/// If more devices than that maximum are received for one JID from a server, -/// they will not be stored locally and thus not used for encryption. -/// -/// \param maximum maximum count of devices stored per JID -/// -void Manager::setMaximumDevicesPerJid(int maximum) -{ - d->maximumDevicesPerJid = maximum; -} - -/// -/// Returns the maximum count of devices for whom a stanza is encrypted. -/// -/// If more devices than that maximum are stored for all addressed recipients of -/// a stanza, the stanza will only be encrypted for first devices until the -/// maximum is reached. -/// -/// \return the maximum count of devices for whom a stanza is encrypted -/// -int Manager::maximumDevicesPerStanza() const -{ - return d->maximumDevicesPerStanza; -} - -/// Sets the maximum count of devices for whom a stanza is encrypted. -/// -/// If more devices than that maximum are stored for all addressed recipients of -/// a stanza, the stanza will only be encrypted for first devices until the -/// maximum is reached. -/// -/// \param maximum maximum count of devices for whom a stanza is encrypted -/// -void Manager::setMaximumDevicesPerStanza(int maximum) -{ - d->maximumDevicesPerStanza = maximum; -} - -/// -/// Requests device lists from contacts and stores them locally. -/// -/// The user must be logged in while calling this. -/// The JID of the current user must not be passed. -/// -/// \param jids JIDs of the contacts whose device lists are being requested -/// -/// \return the results of the requests for each JID -/// -QFuture Manager::requestDeviceLists(const QList &jids) -{ - if (const auto jidsCount = jids.size()) { - QFutureInterface interface(QFutureInterfaceBase::Started); - auto processedJidsCount = std::make_shared(0); - - for (const auto &jid : jids) { - Q_ASSERT_X(jid != d->ownBareJid(), "Requesting contact's device list", "Own JID passed"); - - auto future = d->requestDeviceList(jid); - await(future, this, [=](auto result) mutable { - DevicesResult devicesResult { - jid, - mapSuccess(std::move(result), [](QXmppOmemoDeviceListItem) { return Success(); }) - }; - interface.reportResult(devicesResult); - - if (++(*processedJidsCount) == jidsCount) { - interface.reportFinished(); - } - }); - } - return interface.future(); - } - return QFutureInterface(QFutureInterfaceBase::Finished).future(); -} - -/// -/// Subscribes the current user's resource to device lists manually. -/// -/// This should be called after each login and only for contacts without -/// presence subscription because their device lists are not automatically -/// subscribed. -/// The user must be logged in while calling this. -/// -/// Call \c QXmppOmemoManager::unsubscribeFromDeviceLists() before logout. -/// -/// \param jids JIDs of the contacts whose device lists are being subscribed -/// -/// \return the results of the subscription for each JID -/// -QFuture Manager::subscribeToDeviceLists(const QList &jids) -{ - QFutureInterface interface(QFutureInterfaceBase::Started); - - if (const auto jidsCount = jids.size()) { - auto processedJidsCount = std::make_shared(0); - - for (const auto &jid : jids) { - auto future = d->subscribeToDeviceList(jid); - await(future, this, [=](QXmppPubSubManager::Result result) mutable { - Manager::DevicesResult devicesResult; - devicesResult.jid = jid; - devicesResult.result = result; - interface.reportResult(devicesResult); - - if (++(*processedJidsCount) == jidsCount) { - interface.reportFinished(); - } - }); - } - } else { - interface.reportFinished(); - } - - return interface.future(); -} - -/// -/// Unsubscribes the current user's resource from all device lists that were -/// manually subscribed by \c QXmppOmemoManager::subscribeToDeviceList(). -/// -/// This should be called before each logout. -/// The user must be logged in while calling this. -/// -/// \return the results of the unsubscription for each JID -/// -QFuture Manager::unsubscribeFromDeviceLists() -{ - return d->unsubscribeFromDeviceLists(d->jidsOfManuallySubscribedDevices); -} - -/// -/// Returns the device of this client instance's current user. -/// -/// \return the own device -/// -QXmppOmemoOwnDevice Manager::ownDevice() -{ - const auto &ownDevice = d->ownDevice; - - QXmppOmemoOwnDevice device; - device.setLabel(ownDevice.label); - device.setKeyId(createKeyId(ownDevice.publicIdentityKey)); - - return device; -} - -/// Returns all locally stored devices except the own device. -/// -/// Only devices that have been received after subscribing the corresponding -/// device lists on the server are stored locally. -/// Thus, only those are returned. -/// Call \c QXmppOmemoManager::subscribeToDeviceLists() for contacts without -/// presence subscription before. -/// -/// /\return all devices except the own device -/// -QFuture> Manager::devices() -{ - return devices(d->devices.keys()); -} - -/// -/// Returns locally stored devices except the own device. -/// -/// Only devices that have been received after subscribing the corresponding -/// device lists on the server are stored locally. -/// Thus, only those are returned. -/// Call \c QXmppOmemoManager::subscribeToDeviceLists() for contacts without -/// presence subscription before. -/// -/// \param jids JIDs whose devices are being retrieved -/// -/// \return all devices of the passed JIDs -/// -QFuture> Manager::devices(const QList &jids) -{ - QFutureInterface> interface(QFutureInterfaceBase::Started); - - auto future = keys(jids); - await(future, this, [=](QHash> keys) mutable { - QVector devices; - - for (const auto &jid : jids) { - const auto &storedDevices = d->devices.value(jid); - const auto &storedKeys = keys.value(jid); - - for (const auto &storedDevice : storedDevices) { - const auto &keyId = storedDevice.keyId; - - QXmppOmemoDevice device; - device.setJid(jid); - device.setLabel(storedDevice.label); - - if (!keyId.isEmpty()) { - device.setKeyId(keyId); - device.setTrustLevel(storedKeys.value(keyId)); - } - - devices.append(device); - } - } - - reportFinishedResult(interface, devices); - }); - - return interface.future(); -} - -/// -/// Removes all devices of a contact and the subscription to the contact's -/// device list. -/// -/// This should be called after removing a contact. -/// The JID of the current user must not be passed. -/// Use \c QXmppOmemoManager::resetAll() in order to remove all devices of the -/// user. -/// -/// \param jid JID of the contact whose devices are being removed -/// -/// \return the result of the contact device removals -/// -QFuture Manager::removeContactDevices(const QString &jid) -{ - QFutureInterface interface(QFutureInterfaceBase::Started); - - Q_ASSERT_X(jid != d->ownBareJid(), "Removing contact device", "Own JID passed"); - - auto future = d->unsubscribeFromDeviceList(jid); - await(future, this, [=](QXmppPubSubManager::Result result) mutable { - if (std::holds_alternative(result)) { - warning("Contact '" % jid % "' could not be removed because the device list subscription could not be removed"); - reportFinishedResult(interface, result); - } else { - d->devices.remove(jid); - - auto future = d->omemoStorage->removeDevices(jid); - await(future, this, [=]() mutable { - auto future = d->trustManager->removeKeys(ns_omemo_2, jid); - await(future, this, [=]() mutable { - reportFinishedResult(interface, result); - emit devicesRemoved(jid); - }); - }); - } - }); - - return interface.future(); -} - -/// -/// Sets the trust levels keys must have in order to build sessions for their -/// devices. -/// -/// \param trustLevels trust levels of the keys used for building sessions -/// -void Manager::setAcceptedSessionBuildingTrustLevels(QXmpp::TrustLevels trustLevels) -{ - d->acceptedSessionBuildingTrustLevels = trustLevels; -} - -/// -/// Returns the trust levels keys must have in order to build sessions for their -/// devices. -/// -/// \return the trust levels of the keys used for building sessions -/// -TrustLevels Manager::acceptedSessionBuildingTrustLevels() -{ - return d->acceptedSessionBuildingTrustLevels; -} - -/// -/// Sets whether sessions are built when new devices are received from the -/// server. -/// -/// This can be used to not call \c QXmppOmemoManager::buildMissingSessions -/// manually. -/// But it should not be used before the initial setup and storing lots of -/// devices locally. -/// Otherwise, it could lead to a massive computation and network load when -/// there are many devices for whom sessions are built. -/// -/// \see QXmppOmemoManager::buildMissingSessions -/// -/// \param isNewDeviceAutoSessionBuildingEnabled whether sessions are built for -/// incoming devices -/// -void Manager::setNewDeviceAutoSessionBuildingEnabled(bool isNewDeviceAutoSessionBuildingEnabled) -{ - d->isNewDeviceAutoSessionBuildingEnabled = isNewDeviceAutoSessionBuildingEnabled; -} - -/// -/// Returns whether sessions are built when new devices are received from the -/// server. -/// -/// \see QXmppOmemoManager::setNewDeviceAutoSessionBuildingEnabled -/// -/// \return whether sessions are built for incoming devices -/// -bool Manager::isNewDeviceAutoSessionBuildingEnabled() -{ - return d->isNewDeviceAutoSessionBuildingEnabled; -} - -/// -/// Builds sessions manually with devices for whom no sessions are available. -/// -/// Usually, sessions are built during sending a first message to a device or -/// after a first message is received from a device. -/// This can be called in order to speed up the sending of a message. -/// If this method is called before sending the first message, all sessions can -/// be built and when the first message is sent, the message has only be -/// encrypted. -/// Especially chats with multiple devices, that can decrease the noticeable -/// time a user has to wait for sending a message. -/// Additionally, the keys are automatically retrieved from the server which is -/// helpful in order to get them when calling \c QXmppOmemoManager::devices(). -/// -/// The user must be logged in while calling this. -/// -/// \param jids JIDs of the device owners for whom the sessions are built -/// -QFuture Manager::buildMissingSessions(const QList &jids) -{ - QFutureInterface interface(QFutureInterfaceBase::Started); - - auto &devices = d->devices; - auto devicesCount = 0; - - for (const auto &jid : jids) { - // Do not exceed the maximum of manageable devices. - if (devicesCount > d->maximumDevicesPerStanza - devicesCount) { - warning("Sessions could not be built for all JIDs because their devices are " - "altogether more than the maximum of manageable devices " % - QString::number(d->maximumDevicesPerStanza) % - u" - Use QXmppOmemoManager::setMaximumDevicesPerStanza() to increase the maximum"); - break; - } else { - devicesCount += devices.value(jid).size(); - } - } - - if (devicesCount) { - auto processedDevicesCount = std::make_shared(0); - - for (const auto &jid : jids) { - auto &processedDevices = devices[jid]; - - for (auto itr = processedDevices.begin(); itr != processedDevices.end(); ++itr) { - const auto &deviceId = itr.key(); - auto &device = itr.value(); - - if (device.session.isEmpty()) { - auto future = d->buildSessionWithDeviceBundle(jid, deviceId, device); - await(future, this, [=](auto) mutable { - if (++(*processedDevicesCount) == devicesCount) { - interface.reportFinished(); - } - }); - } else if (++(*processedDevicesCount) == devicesCount) { - interface.reportFinished(); - } - } - } - } else { - interface.reportFinished(); - } - - return interface.future(); -} - -/// -/// Resets all OMEMO data for this device and the trust data used by OMEMO. -/// -/// ATTENTION: This should only be called when an account is removed locally or -/// if there are unrecoverable problems with the OMEMO setup of this device. -/// -/// The data on the server for other own devices is not removed. -/// Call \c resetAll() for that purpose. -/// -/// The user must be logged in while calling this. -/// -/// Call \c setUp() once this method is finished if you want to set up -/// everything again for this device. -/// Existing sessions are reset, which might lead to undecryptable incoming -/// stanzas until everything is set up again. -/// -QFuture Manager::resetOwnDevice() -{ - return d->resetOwnDevice(); -} - -/// -/// Resets all OMEMO data for all own devices and the trust data used by OMEMO. -/// -/// ATTENTION: This should only be called if there is a certain reason for it -/// since it deletes the data for this device and for other own devices from the -/// server. -/// -/// Call \c resetOwnDevice() if you only want to delete the OMEMO data for this -/// device. -/// -/// The user must be logged in while calling this. -/// -/// Call \c setUp() once this method is finished if you want to set up -/// everything again. -/// Existing sessions are reset, which might lead to undecryptable incoming -/// stanzas until everything is set up again. -/// -QFuture Manager::resetAll() -{ - return d->resetAll(); -} - -/// -/// \fn QXmppOmemoManager::setSecurityPolicy(QXmpp::TrustSecurityPolicy securityPolicy) -/// -/// Sets the security policy used by this E2EE extension. -/// -/// \param securityPolicy security policy being set -/// -QFuture Manager::setSecurityPolicy(QXmpp::TrustSecurityPolicy securityPolicy) -{ - return d->trustManager->setSecurityPolicy(ns_omemo_2, securityPolicy); -} - -/// -/// \fn QXmppOmemoManager::securityPolicy() -/// -/// Returns the security policy used by this E2EE extension. -/// -/// \return the used security policy -/// -QFuture Manager::securityPolicy() -{ - return d->trustManager->securityPolicy(ns_omemo_2); -} - -/// -/// \fn QXmppOmemoManager::setTrustLevel(const QMultiHash &keyIds, QXmpp::TrustLevel trustLevel) -/// -/// Sets the trust level of keys. -/// -/// If a key is not stored, it is added to the storage. -/// -/// \param keyIds key owners' bare JIDs mapped to the IDs of their keys -/// \param trustLevel trust level being set -/// -QFuture Manager::setTrustLevel(const QMultiHash &keyIds, QXmpp::TrustLevel trustLevel) -{ - return d->trustManager->setTrustLevel(ns_omemo_2, keyIds, trustLevel); -} - -/// -/// \fn QXmppOmemoManager::trustLevel(const QString &keyOwnerJid, const QByteArray &keyId) -/// -/// Returns the trust level of a key. -/// -/// If the key is not stored, the trust in that key is undecided. -/// -/// \param keyOwnerJid key owner's bare JID -/// \param keyId ID of the key -/// -/// \return the key's trust level -/// -QFuture Manager::trustLevel(const QString &keyOwnerJid, const QByteArray &keyId) -{ - return d->trustManager->trustLevel(ns_omemo_2, keyOwnerJid, keyId); -} - -/// \cond -QFuture Manager::encryptMessage(QXmppMessage &&message, const std::optional ¶ms) -{ - QVector recipientJids; - std::optional acceptedTrustLevels; - - if (params) { - recipientJids = params->encryptionJids(); - acceptedTrustLevels = params->acceptedTrustLevels(); - } - - if (recipientJids.isEmpty()) { - recipientJids.append(QXmppUtils::jidToBareJid(message.to())); - } - - if (!acceptedTrustLevels) { - acceptedTrustLevels = ACCEPTED_TRUST_LEVELS; - } - - return d->encryptMessageForRecipients(std::move(message), recipientJids, *acceptedTrustLevels); -} - -QFuture Manager::encryptIq(QXmppIq &&iq, const std::optional ¶ms) -{ - QFutureInterface interface(QFutureInterfaceBase::Started); - - if (!d->isStarted) { - QXmpp::SendError error; - error.text = QStringLiteral("OMEMO manager must be started before encrypting"); - error.type = QXmpp::SendError::EncryptionError; - reportFinishedResult(interface, { error }); - } else { - std::optional acceptedTrustLevels; - - if (params) { - acceptedTrustLevels = params->acceptedTrustLevels(); - } - - if (!acceptedTrustLevels) { - acceptedTrustLevels = ACCEPTED_TRUST_LEVELS; - } - - auto future = d->encryptStanza(iq, { QXmppUtils::jidToBareJid(iq.to()) }, *acceptedTrustLevels); - await(future, this, [=, iq = std::move(iq)](std::optional 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 { - QXmppOmemoIq omemoIq; - omemoIq.setId(iq.id()); - omemoIq.setType(iq.type()); - omemoIq.setLang(iq.lang()); - omemoIq.setFrom(iq.from()); - omemoIq.setTo(iq.to()); - omemoIq.setOmemoElement(*omemoElement); - - QByteArray serializedEncryptedIq; - QXmlStreamWriter writer(&serializedEncryptedIq); - omemoIq.toXml(&writer); - - reportFinishedResult(interface, { serializedEncryptedIq }); - } - }); - } - - return interface.future(); -} - -QFuture Manager::decryptIq(const QDomElement &element) -{ - if (!d->isStarted) { - // TODO: Add decryption queue to avoid this error - return makeReadyFuture(SendError { - QStringLiteral("OMEMO manager must be started before decrypting"), - SendError::EncryptionError }); - } - - if (QXmppOmemoIq::isOmemoIq(element)) { - // Tag name and iq type are already checked in QXmppClient. - return chain(d->decryptIq(element), this, [](auto result) -> IqDecryptResult { - if (result) { - return result->iq; - } - return SendError { - QStringLiteral("OMEMO message could not be decrypted"), - SendError::EncryptionError - }; - }); - } - - return makeReadyFuture(NotEncrypted()); -} - -QStringList Manager::discoveryFeatures() const -{ - return { - QString(ns_omemo_2_devices) % "+notify" - }; -} - -bool Manager::handleStanza(const QDomElement &stanza) -{ - if (stanza.tagName() != "iq" || !QXmppOmemoIq::isOmemoIq(stanza)) { - return false; - } - - // TODO: Queue incoming IQs until OMEMO is initialized - if (!d->isStarted) { - warning("Couldn't decrypt incoming IQ because the manager isn't initialized yet."); - return false; - } - - auto type = stanza.attribute("type"); - if (type != "get" && type != "set") { - // ignore incoming result and error IQs (they are handled via Client::sendIq()) - return false; - } - - await(d->decryptIq(stanza), this, [=](auto result) { - if (result) { - injectIq(result->iq, result->e2eeMetadata); - } else { - warning("Could not decrypt incoming OMEMO IQ."); - } - }); - return true; -} - -bool Manager::handleMessage(const QXmppMessage &message) -{ - if (d->isStarted && message.omemoElement()) { - auto future = d->decryptMessage(message); - await(future, this, [=](std::optional optionalDecryptedMessage) mutable { - if (optionalDecryptedMessage) { - injectMessage(std::move(*optionalDecryptedMessage)); - } - }); - - return true; - } - - return false; -} -/// \endcond - -/// -/// \fn QXmppOmemoManager::trustLevelsChanged(const QMultiHash &modifiedKeys) -/// -/// Emitted when the trust levels of keys changed. -/// -/// \param modifiedKeys key owners' bare JIDs mapped to their modified keys -/// - -/// -/// \fn QXmppOmemoManager::deviceAdded(const QString &jid, uint32_t deviceId) -/// -/// Emitted when a device is added. -/// -/// \param jid device owner's bare JID -/// \param deviceId ID of the device -/// - -/// -/// \fn QXmppOmemoManager::deviceChanged(const QString &jid, uint32_t deviceId) -/// -/// Emitted when a device changed. -/// -/// \param jid device owner's bare JID -/// \param deviceId ID of the device -/// - -/// -/// \fn QXmppOmemoManager::deviceRemoved(const QString &jid, uint32_t deviceId) -/// -/// Emitted when a device is removed. -/// -/// \param jid device owner's bare JID -/// \param deviceId ID of the device -/// - -/// -/// \fn QXmppOmemoManager::devicesRemoved(const QString &jid) -/// -/// Emitted when all devices of an owner are removed. -/// -/// \param jid device owner's bare JID -/// - -/// -/// \fn QXmppOmemoManager::allDevicesRemoved() -/// -/// Emitted when all devices are removed. -/// - -/// \cond -void Manager::setClient(QXmppClient *client) -{ - QXmppClientExtension::setClient(client); - client->setEncryptionExtension(this); - - d->trustManager = client->findExtension(); - if (!d->trustManager) { - qFatal("QXmppTrustManager is not available, it must be added to the client before adding QXmppOmemoManager"); - } - - d->pubSubManager = client->findExtension(); - if (!d->pubSubManager) { - qFatal("QXmppPubSubManager is not available, it must be added to the client before adding QXmppOmemoManager"); - } - - connect(d->trustManager, &QXmppTrustManager::trustLevelsChanged, this, [=](const QHash> &modifiedKeys) { - const auto &modifiedOmemoKeys = modifiedKeys.value(ns_omemo_2); - emit trustLevelsChanged(modifiedOmemoKeys); - - for (auto itr = modifiedOmemoKeys.cbegin(); itr != modifiedOmemoKeys.cend(); ++itr) { - const auto &keyOwnerJid = itr.key(); - const auto &keyId = itr.value(); - - // Emit 'deviceChanged()' only if there is a device with the key. - const auto &devices = d->devices.value(keyOwnerJid); - for (auto itr = devices.cbegin(); itr != devices.cend(); ++itr) { - if (itr->keyId == keyId) { - emit deviceChanged(keyOwnerJid, itr.key()); - return; - } - } - } - }); -} - -bool Manager::handlePubSubEvent(const QDomElement &element, const QString &pubSubService, const QString &nodeName) -{ - if (nodeName == ns_omemo_2_devices && QXmppPubSubEvent::isPubSubEvent(element)) { - QXmppPubSubEvent event; - event.parse(element); - - switch (event.eventType()) { - // Items are published or deleted. - case QXmppPubSubEventBase::Items: { - // If there are IDs of deleted items, check for an inconsistency. - // Otherwise, check for published items. - if (const auto retractIds = event.retractIds(); !retractIds.isEmpty()) { - // Specific items are deleted. - const auto &retractedItem = event.retractIds().constFirst(); - if (retractedItem == QXmppPubSubManager::standardItemIdToString(QXmppPubSubManager::Current)) { - d->handleIrregularDeviceListChanges(pubSubService); - } - } else { - const auto items = event.items(); - - // Only process items if the event notification contains one. - // That is necessary because PubSub allows publishing without - // items leading to notification-only events. - if (!items.isEmpty()) { - const auto &deviceListItem = items.constFirst(); - if (deviceListItem.id() == QXmppPubSubManager::standardItemIdToString(QXmppPubSubManager::Current)) { - d->updateDevices(pubSubService, event.items().constFirst()); - } else { - d->handleIrregularDeviceListChanges(pubSubService); - } - } - } - - break; - } - - // All items are deleted. - case QXmppPubSubEventBase::Purge: - // The whole node is deleted. - case QXmppPubSubEventBase::Delete: - d->handleIrregularDeviceListChanges(pubSubService); - break; - case QXmppPubSubEventBase::Configuration: - case QXmppPubSubEventBase::Subscription: - break; - } - - return true; - } - - return false; -} -/// \endcond diff --git a/src/client/QXmppOmemoManager.h b/src/client/QXmppOmemoManager.h deleted file mode 100644 index 91d57799..00000000 --- a/src/client/QXmppOmemoManager.h +++ /dev/null @@ -1,157 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Melvin Keskin -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#ifndef QXMPPOMEMOMANAGER_H -#define QXMPPOMEMOMANAGER_H - -#include "QXmppClientExtension.h" -#include "QXmppE2eeExtension.h" -#include "QXmppMessageHandler.h" -#include "QXmppPubSubEventHandler.h" -#include "QXmppPubSubManager.h" -#include "QXmppTrustSecurityPolicy.h" - -class QXmppOmemoDevicePrivate; -class QXmppOmemoManagerPrivate; -class QXmppOmemoOwnDevicePrivate; -class QXmppOmemoStorage; - -class QXMPP_EXPORT QXmppOmemoOwnDevice -{ -public: - QXmppOmemoOwnDevice(); - QXmppOmemoOwnDevice(const QXmppOmemoOwnDevice &other); - QXmppOmemoOwnDevice(QXmppOmemoOwnDevice &&) noexcept; - ~QXmppOmemoOwnDevice(); - - QXmppOmemoOwnDevice &operator=(const QXmppOmemoOwnDevice &); - QXmppOmemoOwnDevice &operator=(QXmppOmemoOwnDevice &&); - - QString label() const; - void setLabel(const QString &label); - - QByteArray keyId() const; - void setKeyId(const QByteArray &keyId); - -private: - QSharedDataPointer d; -}; - -class QXMPP_EXPORT QXmppOmemoDevice -{ -public: - QXmppOmemoDevice(); - QXmppOmemoDevice(const QXmppOmemoDevice &other); - QXmppOmemoDevice(QXmppOmemoDevice &&) noexcept; - ~QXmppOmemoDevice(); - - QXmppOmemoDevice &operator=(const QXmppOmemoDevice &); - QXmppOmemoDevice &operator=(QXmppOmemoDevice &&); - - QString jid() const; - void setJid(const QString &jid); - - QString label() const; - void setLabel(const QString &label); - - QByteArray keyId() const; - void setKeyId(const QByteArray &keyId); - - QXmpp::TrustLevel trustLevel() const; - void setTrustLevel(QXmpp::TrustLevel trustLevel); - -private: - QSharedDataPointer d; -}; - -class QXMPP_EXPORT QXmppOmemoManager : public QXmppClientExtension, public QXmppE2eeExtension, public QXmppPubSubEventHandler, public QXmppMessageHandler -{ - Q_OBJECT - -public: - using Result = std::variant; - - struct DevicesResult - { - QString jid; - Result result; - }; - - explicit QXmppOmemoManager(QXmppOmemoStorage *omemoStorage); - ~QXmppOmemoManager() override; - - QFuture load(); - QFuture setUp(); - - QFuture ownKey(); - QFuture>> keys(QXmpp::TrustLevels trustLevels = {}); - QFuture>> keys(const QList &jids, QXmpp::TrustLevels trustLevels = {}); - - QFuture changeDeviceLabel(const QString &deviceLabel = {}); - - int maximumDevicesPerJid() const; - void setMaximumDevicesPerJid(int maximum); - - int maximumDevicesPerStanza() const; - void setMaximumDevicesPerStanza(int maximum); - - QFuture requestDeviceLists(const QList &jids); - QFuture subscribeToDeviceLists(const QList &jids); - QFuture unsubscribeFromDeviceLists(); - - QXmppOmemoOwnDevice ownDevice(); - QFuture> devices(); - QFuture> devices(const QList &jids); - QFuture removeContactDevices(const QString &jid); - - void setAcceptedSessionBuildingTrustLevels(QXmpp::TrustLevels trustLevels); - QXmpp::TrustLevels acceptedSessionBuildingTrustLevels(); - - void setNewDeviceAutoSessionBuildingEnabled(bool isNewDeviceAutoSessionBuildingEnabled); - bool isNewDeviceAutoSessionBuildingEnabled(); - - QFuture buildMissingSessions(const QList &jids); - - QFuture resetOwnDevice(); - QFuture resetAll(); - - QFuture setSecurityPolicy(QXmpp::TrustSecurityPolicy securityPolicy); - QFuture securityPolicy(); - - QFuture setTrustLevel(const QMultiHash &keyIds, QXmpp::TrustLevel trustLevel); - QFuture trustLevel(const QString &keyOwnerJid, const QByteArray &keyId); - - /// \cond - QFuture encryptMessage(QXmppMessage &&message, const std::optional ¶ms) override; - - QFuture encryptIq(QXmppIq &&iq, const std::optional ¶ms) override; - QFuture decryptIq(const QDomElement &element) override; - - QStringList discoveryFeatures() const override; - bool handleStanza(const QDomElement &stanza) override; - bool handleMessage(const QXmppMessage &message) override; - /// \endcond - - Q_SIGNAL void trustLevelsChanged(const QMultiHash &modifiedKeys); - - Q_SIGNAL void deviceAdded(const QString &jid, uint32_t deviceId); - Q_SIGNAL void deviceChanged(const QString &jid, uint32_t deviceId); - Q_SIGNAL void deviceRemoved(const QString &jid, uint32_t deviceId); - Q_SIGNAL void devicesRemoved(const QString &jid); - Q_SIGNAL void allDevicesRemoved(); - -protected: - /// \cond - void setClient(QXmppClient *client) override; - bool handlePubSubEvent(const QDomElement &element, const QString &pubSubService, const QString &nodeName) override; - /// \endcond - -private: - std::unique_ptr d; - - friend class QXmppOmemoManagerPrivate; - friend class tst_QXmppOmemoManager; -}; - -#endif // QXMPPOMEMOMANAGER_H diff --git a/src/client/QXmppOmemoManager_p.cpp b/src/client/QXmppOmemoManager_p.cpp deleted file mode 100644 index 2f8ca00d..00000000 --- a/src/client/QXmppOmemoManager_p.cpp +++ /dev/null @@ -1,3714 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Melvin Keskin -// SPDX-FileCopyrightText: 2022 Linus Jahn -// -// 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 - -#include "OmemoCryptoProvider.h" - -#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) -#include -#endif -#include - -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(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 -auto mapToSuccess(std::variant 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(user_data); - auto *d = manager->d.get(); - d->mutex.lock(); - }; - - const auto unlock = [](void *user_data) { - const auto *manager = reinterpret_cast(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(user_data); - const auto *d = manager->d.get(); - - const auto &privateIdentityKey = d->ownDevice.privateIdentityKey; - if (!(*private_data = signal_buffer_create(reinterpret_cast(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(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(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(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(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(user_data); - auto *d = manager->d.get(); - - QXmppOmemoStorage::SignedPreKeyPair signedPreKeyPair; - signedPreKeyPair.creationDate = QDateTime::currentDateTimeUtc(); - signedPreKeyPair.data = QByteArray(reinterpret_cast(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(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(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(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(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(user_data); - auto *d = manager->d.get(); - const auto preKey = QByteArray(reinterpret_cast(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(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(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(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(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(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(user_data); - auto *d = manager->d.get(); - const auto session = QByteArray(reinterpret_cast(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(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(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(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 ManagerPrivate::setUpDeviceId() -{ - QFutureInterface interface(QFutureInterfaceBase::Started); - - auto future = pubSubManager->requestPepItemIds(ns_omemo_2_bundles); - await(future, q, [=](QXmppPubSubManager::ItemIdsResult result) mutable { - if (auto error = std::get_if(&result)) { - warning("Existing / Published device IDs could not be retrieved"); - reportFinishedResult(interface, false); - } else { - const auto &deviceIds = std::get>(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 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 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(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 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 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 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 ManagerPrivate::encryptMessageForRecipients(QXmppMessage &&message, QVector recipientJids, TrustLevels acceptedTrustLevels) -{ - QFutureInterface 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 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 -QFuture> ManagerPrivate::encryptStanza(const T &stanza, const QVector &recipientJids, TrustLevels acceptedTrustLevels) -{ - Q_ASSERT_X(!recipientJids.isEmpty(), "Creating OMEMO envelope", "OMEMO element could not be created because no recipient JIDs are passed"); - - QFutureInterface> 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(); - auto processedDevicesCount = std::make_shared(0); - auto successfullyProcessedDevicesCount = std::make_shared(0); - auto skippedDevicesCount = std::make_shared(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 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 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> ManagerPrivate::encryptStanza(const QXmppIq &, const QVector &, TrustLevels); -template QFuture> ManagerPrivate::encryptStanza(const QXmppMessage &, const QVector &, TrustLevels); - -// -// Encrypts a payload symmetrically. -// -// \param payload payload being symmetrically encrypted -// -// \return the data used for encryption and the result -// -std::optional 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 -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) { - 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 encryptedOmemoEnvelopeData; - if (session_cipher_encrypt(sessionCipher.get(), reinterpret_cast(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(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> ManagerPrivate::decryptMessage(QXmppMessage stanza) -{ - QFutureInterface> 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 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> ManagerPrivate::decryptIq(const QDomElement &iqElement) -{ - using Result = std::optional; - - 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(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(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 -QFuture> ManagerPrivate::decryptStanza(T stanza, const QString &senderJid, uint32_t senderDeviceId, const QXmppOmemoEnvelope &omemoEnvelope, const QByteArray &omemoPayload, bool isMessageStanza) -{ - QFutureInterface> 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(stanza); message.type() == QXmppMessage::GroupChat && (sceEnvelopeReader.to() != recipientJid)) { - warning("Recipient of group chat message does not match SCE affix element ''"); - isSceAffixElementValid = false; - } - } else { - if (sceEnvelopeReader.to() != recipientJid) { - warning("Recipient of IQ does not match SCE affix element ''"); - 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 ManagerPrivate::extractSceEnvelope(const QString &senderJid, uint32_t senderDeviceId, const QXmppOmemoEnvelope &omemoEnvelope, const QByteArray &omemoPayload, bool isMessageStanza) -{ - QFutureInterface 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 ManagerPrivate::extractPayloadDecryptionData(const QString &senderJid, uint32_t senderDeviceId, const QXmppOmemoEnvelope &omemoEnvelope, bool isMessageStanza) -{ - QFutureInterface 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 omemoEnvelopeData; - const auto serializedOmemoEnvelopeData = omemoEnvelope.data(); - - if (pre_key_signal_message_deserialize_omemo(omemoEnvelopeData.ptrRef(), - reinterpret_cast(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 omemoEnvelopeData; - const auto serializedOmemoEnvelopeData = omemoEnvelope.data(); - - if (signal_message_deserialize_omemo(omemoEnvelopeData.ptrRef(), reinterpret_cast(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 ManagerPrivate::publishOmemoData() -{ - QFutureInterface interface(QFutureInterfaceBase::Started); - - auto future = pubSubManager->requestPepFeatures(); - await(future, q, [=](QXmppPubSubManager::FeaturesResult result) mutable { - if (const auto error = std::get_if(&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>(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(&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>(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 -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 -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 -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 -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 -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 -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 -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 -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> ManagerPrivate::requestDeviceBundle(const QString &deviceOwnerJid, uint32_t deviceId) const -{ - QFutureInterface> interface(QFutureInterfaceBase::Started); - - auto future = pubSubManager->requestItem(deviceOwnerJid, ns_omemo_2_bundles, QString::number(deviceId)); - await(future, q, [=](QXmppPubSubManager::ItemResult result) mutable { - if (const auto error = std::get_if(&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(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 -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 -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 -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 -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 -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 -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 -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 -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 -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 -void ManagerPrivate::updateOwnDevicesLocally(bool isDeviceListNodeExistent, Function continuation) -{ - if (isDeviceListNodeExistent && otherOwnDevices().isEmpty()) { - auto future = pubSubManager->requestPepItem(ns_omemo_2_devices, QXmppPubSubManager::Current); - await(future, q, [=](QXmppPubSubManager::ItemResult result) mutable { - if (const auto error = std::get_if(&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(result); - QList 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(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 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 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(&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(&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>(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 -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 -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 -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 -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 -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 -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(&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 -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 -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 -void QXmppOmemoManagerPrivate::runPubSubQueryWithContinuation(QFuture future, const QString &errorMessage, Function continuation) -{ - await(future, q, [this, errorMessage, continuation = std::move(continuation)](auto result) mutable { - if (auto error = std::get_if(&result)) { - warning(errorMessage % u": " % errorToString(*error)); - continuation(false); - } else { - continuation(true); - } - }); -} - -// See QXmppOmemoManager for documentation -QFuture ManagerPrivate::changeDeviceLabel(const QString &deviceLabel) -{ - QFutureInterface 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> ManagerPrivate::requestDeviceList(const QString &jid) -{ - auto future = pubSubManager->requestItem(jid, ns_omemo_2_devices, QXmppPubSubManager::Current); - await(future, q, [this, jid](QXmppPubSubManager::ItemResult result) mutable { - if (const auto error = std::get_if(&result)) { - warning("Device list for JID '" % jid % "' could not be retrieved: " % errorToString(*error)); - } else { - const auto &item = std::get(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 ManagerPrivate::subscribeToDeviceList(const QString &jid) -{ - QFutureInterface 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(&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 ManagerPrivate::unsubscribeFromDeviceLists(const QList &jids) -{ - QFutureInterface interface = (QFutureInterfaceBase::Started); - - const auto jidsCount = jids.size(); - auto processedJidsCount = std::make_shared(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 ManagerPrivate::unsubscribeFromDeviceList(const QString &jid) -{ - QFutureInterface 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(&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 ManagerPrivate::resetOwnDevice() -{ - QFutureInterface 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 ManagerPrivate::resetAll() -{ - QFutureInterface 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 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 ManagerPrivate::buildSessionWithDeviceBundle(const QString &jid, uint32_t deviceId, QXmppOmemoStorage::Device &device) -{ - QFutureInterface interface(QFutureInterfaceBase::Started); - - auto future = requestDeviceBundle(jid, deviceId); - await(future, q, [=, &device](std::optional 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(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 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 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 publicIdentityKey; - RefCountedPtr signedPublicPreKey; - RefCountedPtr signedPublicPreKeySignature; - int signedPublicPreKeySignatureSize; - RefCountedPtr 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 ManagerPrivate::sendEmptyMessage(const QString &recipientJid, uint32_t recipientDeviceId, bool isKeyExchange) const -{ - QFutureInterface 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 ManagerPrivate::storeOwnKey() const -{ - QFutureInterface 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 ManagerPrivate::storeKeyDependingOnSecurityPolicy(const QString &keyOwnerJid, const QByteArray &key) -{ - QFutureInterface interface(QFutureInterfaceBase::Started); - - auto awaitStoreKey = [=](const QFuture &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 ManagerPrivate::storeKey(const QString &keyOwnerJid, const QByteArray &key, TrustLevel trustLevel) const -{ - QFutureInterface 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 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 diff --git a/src/client/QXmppOmemoManager_p.h b/src/client/QXmppOmemoManager_p.h deleted file mode 100644 index 7f697ed2..00000000 --- a/src/client/QXmppOmemoManager_p.h +++ /dev/null @@ -1,342 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Melvin Keskin -// SPDX-FileCopyrightText: 2022 Linus Jahn -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#ifndef QXMPPOMEMOMANAGER_P_H -#define QXMPPOMEMOMANAGER_P_H - -#include "QXmppE2eeMetadata.h" -#include "QXmppOmemoDeviceBundle_p.h" -#include "QXmppOmemoManager.h" -#include "QXmppOmemoStorage.h" - -#include "OmemoLibWrappers.h" -#include -#include -#include - -class QXmppTrustManager; -class QXmppOmemoManager; -class QXmppPubSubManager; -class QXmppPubSubNodeConfig; -class QXmppPubSubPublishOptions; -class QXmppOmemoIq; -class QXmppOmemoEnvelope; -class QXmppOmemoElement; -class QXmppOmemoDeviceListItem; -class QXmppOmemoDeviceBundleItem; - -using namespace QXmpp; -using namespace std::chrono_literals; - -namespace QXmpp::Omemo::Private { - -// default possible trust levels a key must have to be used for encryption -// The class documentation must be adapted if the trust levels are modified. -constexpr auto ACCEPTED_TRUST_LEVELS = TrustLevel::AutomaticallyTrusted | TrustLevel::ManuallyTrusted | TrustLevel::Authenticated; - -// count of unresponded stanzas sent to a device until QXmpp stops encrypting for it -constexpr int UNRESPONDED_STANZAS_UNTIL_ENCRYPTION_IS_STOPPED = 106; - -// count of unresponded stanzas received from a device until a heartbeat message is sent to it -constexpr int UNRESPONDED_STANZAS_UNTIL_HEARTBEAT_MESSAGE_IS_SENT = 53; - -// size of empty OMEMO message's decryption data -constexpr int EMPTY_MESSAGE_DECRYPTION_DATA_SIZE = 32; - -// workaround for PubSub nodes that are not configurable to store 'max' as the value for -// 'pubsub#max_items' -constexpr uint64_t PUBSUB_NODE_MAX_ITEMS_1 = 1000; -constexpr uint64_t PUBSUB_NODE_MAX_ITEMS_2 = 100; -constexpr uint64_t PUBSUB_NODE_MAX_ITEMS_3 = 10; - -constexpr uint32_t PRE_KEY_ID_MIN = 1; -constexpr uint32_t SIGNED_PRE_KEY_ID_MIN = 1; -constexpr uint32_t PRE_KEY_ID_MAX = std::numeric_limits::max(); -constexpr uint32_t SIGNED_PRE_KEY_ID_MAX = std::numeric_limits::max(); -constexpr uint32_t PRE_KEY_INITIAL_CREATION_COUNT = 100; - -// maximum count of devices stored per JID -constexpr int DEVICES_PER_JID_MAX = 200; - -// maximum count of devices for whom a stanza is encrypted -constexpr int DEVICES_PER_STANZA_MAX = 1000; - -// interval to remove old signed pre keys and create new ones -constexpr auto SIGNED_PRE_KEY_RENEWAL_INTERVAL = 24h * 7 * 4; - -// interval to check for old signed pre keys -constexpr auto SIGNED_PRE_KEY_RENEWAL_CHECK_INTERVAL = 24h; - -// interval to remove devices locally after removal from their servers -constexpr auto DEVICE_REMOVAL_INTERVAL = 24h * 7 * 12; - -// interval to check for devices removed from their servers -constexpr auto DEVICE_REMOVAL_CHECK_INTERVAL = 24h; - -constexpr auto PAYLOAD_CIPHER_TYPE = "aes256"; -constexpr QCA::Cipher::Mode PAYLOAD_CIPHER_MODE = QCA::Cipher::CBC; -constexpr QCA::Cipher::Padding PAYLOAD_CIPHER_PADDING = QCA::Cipher::PKCS7; - -constexpr auto HKDF_INFO = "OMEMO Payload"; -constexpr int HKDF_KEY_SIZE = 32; -constexpr int HKDF_SALT_SIZE = 32; -constexpr int HKDF_OUTPUT_SIZE = 60; - -extern const QString PAYLOAD_MESSAGE_AUTHENTICATION_CODE_TYPE; -constexpr uint32_t PAYLOAD_MESSAGE_AUTHENTICATION_CODE_SIZE = 16; - -constexpr int PAYLOAD_KEY_SIZE = 32; -constexpr uint32_t PAYLOAD_INITIALIZATION_VECTOR_SIZE = 16; -constexpr uint32_t PAYLOAD_AUTHENTICATION_KEY_SIZE = 16; - -// boundaries for the count of characters in SCE's element -constexpr uint32_t SCE_RPAD_SIZE_MIN = 0; -constexpr uint32_t SCE_RPAD_SIZE_MAX = 200; - -struct PayloadEncryptionResult -{ - QCA::SecureArray decryptionData; - QByteArray encryptedPayload; -}; - -struct DecryptionResult -{ - QDomElement sceContent; - QXmppE2eeMetadata e2eeMetadata; -}; - -struct IqDecryptionResult -{ - QDomElement iq; - QXmppE2eeMetadata e2eeMetadata; -}; - -QByteArray createKeyId(const QByteArray &key); - -} // namespace QXmpp::Omemo::Private - -using namespace QXmpp::Omemo::Private; - -class QXmppOmemoManagerPrivate -{ -public: - using Result = std::variant; - - QXmppOmemoManager *q; - - bool isStarted = false; - bool isNewDeviceAutoSessionBuildingEnabled = false; - - QXmppOmemoStorage *omemoStorage; - QXmppTrustManager *trustManager = nullptr; - QXmppPubSubManager *pubSubManager = nullptr; - - QCA::Initializer cryptoLibInitializer; - QTimer signedPreKeyPairsRenewalTimer; - QTimer deviceRemovalTimer; - - TrustLevels acceptedSessionBuildingTrustLevels = ACCEPTED_TRUST_LEVELS; - - QXmppOmemoStorage::OwnDevice ownDevice; - QHash preKeyPairs; - QHash signedPreKeyPairs; - QXmppOmemoDeviceBundle deviceBundle; - - int maximumDevicesPerJid = DEVICES_PER_JID_MAX; - int maximumDevicesPerStanza = DEVICES_PER_STANZA_MAX; - - // recipient JID mapped to device ID mapped to device - QHash> devices; - - QList jidsOfManuallySubscribedDevices; - - OmemoContextPtr globalContext; - StoreContextPtr storeContext; - QRecursiveMutex mutex; - signal_crypto_provider cryptoProvider; - - signal_protocol_identity_key_store identityKeyStore; - signal_protocol_pre_key_store preKeyStore; - signal_protocol_signed_pre_key_store signedPreKeyStore; - signal_protocol_session_store sessionStore; - - QXmppOmemoManagerPrivate(QXmppOmemoManager *parent, QXmppOmemoStorage *omemoStorage); - - void init(); - bool initGlobalContext(); - bool initLocking(); - bool initCryptoProvider(); - void initStores(); - - signal_protocol_identity_key_store createIdentityKeyStore() const; - signal_protocol_signed_pre_key_store createSignedPreKeyStore() const; - signal_protocol_pre_key_store createPreKeyStore() const; - signal_protocol_session_store createSessionStore() const; - - QFuture setUpDeviceId(); - bool setUpIdentityKeyPair(ratchet_identity_key_pair **identityKeyPair); - void schedulePeriodicTasks(); - void renewSignedPreKeyPairs(); - bool updateSignedPreKeyPair(ratchet_identity_key_pair *identityKeyPair); - bool renewPreKeyPairs(uint32_t keyPairBeingRenewed); - bool updatePreKeyPairs(uint32_t count = 1); - void removeDevicesRemovedFromServer(); - bool generateIdentityKeyPair(ratchet_identity_key_pair **identityKeyPair) const; - - QFuture encryptMessageForRecipients(QXmppMessage &&message, - QVector recipientJids, - TrustLevels acceptedTrustLevels); - template - QFuture> encryptStanza(const T &stanza, const QVector &recipientJids, TrustLevels acceptedTrustLevels); - std::optional encryptPayload(const QByteArray &payload) const; - template - QByteArray createSceEnvelope(const T &stanza); - QByteArray createOmemoEnvelopeData(const signal_protocol_address &address, const QCA::SecureArray &payloadDecryptionData) const; - - QFuture> decryptMessage(QXmppMessage stanza); - QFuture> decryptIq(const QDomElement &iqElement); - template - QFuture> decryptStanza(T stanza, - const QString &senderJid, - uint32_t senderDeviceId, - const QXmppOmemoEnvelope &omemoEnvelope, - const QByteArray &omemoPayload, - bool isMessageStanza = true); - QFuture extractSceEnvelope(const QString &senderJid, - uint32_t senderDeviceId, - const QXmppOmemoEnvelope &omemoEnvelope, - const QByteArray &omemoPayload, - bool isMessageStanza); - QFuture extractPayloadDecryptionData(const QString &senderJid, - uint32_t senderDeviceId, - const QXmppOmemoEnvelope &omemoEnvelope, - bool isMessageStanza = true); - QByteArray decryptPayload(const QCA::SecureArray &payloadDecryptionData, const QByteArray &payload) const; - - QFuture publishOmemoData(); - - template - void publishDeviceBundle(bool isDeviceBundlesNodeExistent, - bool arePublishOptionsSupported, - bool isAutomaticCreationSupported, - bool isCreationAndConfigurationSupported, - bool isCreationSupported, - bool isConfigurationSupported, - bool isConfigNodeMaxSupported, - Function continuation); - template - void publishDeviceBundleWithoutOptions(bool isDeviceBundlesNodeExistent, - bool isCreationAndConfigurationSupported, - bool isCreationSupported, - bool isConfigurationSupported, - bool isConfigNodeMaxSupported, - Function continuation); - template - void configureNodeAndPublishDeviceBundle(bool isConfigNodeMaxSupported, Function continuation); - template - void createAndConfigureDeviceBundlesNode(bool isConfigNodeMaxSupported, Function continuation); - template - void createDeviceBundlesNode(Function continuation); - template - void configureDeviceBundlesNode(bool isConfigNodeMaxSupported, Function continuation); - template - void publishDeviceBundleItem(Function continuation); - template - void publishDeviceBundleItemWithOptions(Function continuation); - QXmppOmemoDeviceBundleItem deviceBundleItem() const; - QFuture> requestDeviceBundle(const QString &deviceOwnerJid, uint32_t deviceId) const; - template - void deleteDeviceBundle(Function continuation); - - template - void publishDeviceElement(bool isDeviceListNodeExistent, - bool arePublishOptionsSupported, - bool isAutomaticCreationSupported, - bool isCreationAndConfigurationSupported, - bool isCreationSupported, - bool isConfigurationSupported, - Function continuation); - template - void publishDeviceElementWithoutOptions(bool isDeviceListNodeExistent, - bool isCreationAndConfigurationSupported, - bool isCreationSupported, - bool isConfigurationSupported, - Function continuation); - template - void configureNodeAndPublishDeviceElement(Function continuation); - template - void createAndConfigureDeviceListNode(Function continuation); - template - void createDeviceListNode(Function continuation); - template - void configureDeviceListNode(Function continuation); - template - void publishDeviceListItem(bool addOwnDevice, Function continuation); - template - void publishDeviceListItemWithOptions(Function continuation); - QXmppOmemoDeviceListItem deviceListItem(bool addOwnDevice = true); - template - void updateOwnDevicesLocally(bool isDeviceListNodeExistent, Function continuation); - void updateDevices(const QString &deviceOwnerJid, const QXmppOmemoDeviceListItem &deviceListItem); - void handleIrregularDeviceListChanges(const QString &deviceOwnerJid); - template - void deleteDeviceElement(Function continuation); - - template - void createNode(const QString &node, Function continuation); - template - void createNode(const QString &node, const QXmppPubSubNodeConfig &config, Function continuation); - template - void configureNode(const QString &node, const QXmppPubSubNodeConfig &config, Function continuation); - template - void retractItem(const QString &node, uint32_t itemId, Function continuation); - template - void deleteNode(const QString &node, Function continuation); - - template - void publishItem(const QString &node, const T &item, Function continuation); - template - void publishItem(const QString &node, const T &item, const QXmppPubSubPublishOptions &publishOptions, Function continuation); - - template - void runPubSubQueryWithContinuation(QFuture future, const QString &errorMessage, Function continuation); - - QFuture changeDeviceLabel(const QString &deviceLabel); - - QFuture> requestDeviceList(const QString &jid); - void subscribeToNewDeviceLists(const QString &jid, uint32_t deviceId); - QFuture subscribeToDeviceList(const QString &jid); - QFuture unsubscribeFromDeviceLists(const QList &jids); - QFuture unsubscribeFromDeviceList(const QString &jid); - - QFuture resetOwnDevice(); - QFuture resetAll(); - - QFuture buildSessionForNewDevice(const QString &jid, uint32_t deviceId, QXmppOmemoStorage::Device &device); - QFuture buildSessionWithDeviceBundle(const QString &jid, uint32_t deviceId, QXmppOmemoStorage::Device &device); - bool buildSession(signal_protocol_address address, const QXmppOmemoDeviceBundle &deviceBundle); - bool createSessionBundle(session_pre_key_bundle **sessionBundle, - const QByteArray &serializedPublicIdentityKey, - const QByteArray &serializedSignedPublicPreKey, - uint32_t signedPublicPreKeyId, - const QByteArray &serializedSignedPublicPreKeySignature, - const QByteArray &serializedPublicPreKey, - uint32_t publicPreKeyId); - bool deserializePublicIdentityKey(ec_public_key **publicIdentityKey, const QByteArray &serializedPublicIdentityKey) const; - bool deserializeSignedPublicPreKey(ec_public_key **signedPublicPreKey, const QByteArray &serializedSignedPublicPreKey) const; - bool deserializePublicPreKey(ec_public_key **publicPreKey, const QByteArray &serializedPublicPreKey) const; - - QFuture sendEmptyMessage(const QString &recipientJid, uint32_t recipientDeviceId, bool isKeyExchange = false) const; - QFuture storeOwnKey() const; - QFuture storeKeyDependingOnSecurityPolicy(const QString &keyOwnerJid, const QByteArray &key); - QFuture storeKey(const QString &keyOwnerJid, const QByteArray &key, TrustLevel trustLevel = TrustLevel::AutomaticallyDistrusted) const; - QString ownBareJid() const; - QString ownFullJid() const; - QHash otherOwnDevices(); - - void warning(const QString &msg) const; -}; - -#endif // QXMPPOMEMOMANAGER_P_H diff --git a/src/client/QXmppOmemoMemoryStorage.cpp b/src/client/QXmppOmemoMemoryStorage.cpp deleted file mode 100644 index 6ff81002..00000000 --- a/src/client/QXmppOmemoMemoryStorage.cpp +++ /dev/null @@ -1,119 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Melvin Keskin -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include "QXmppOmemoMemoryStorage.h" - -#include "QXmppFutureUtils_p.h" - -using namespace QXmpp::Private; - -/// -/// \class QXmppOmemoMemoryStorage -/// -/// \brief The QXmppOmemoMemoryStorage class stores data used by -/// \xep{0384, OMEMO Encryption} in the memory. -/// -/// \warning THIS API IS NOT FINALIZED YET! -/// -/// \since QXmpp 1.5 -/// - -class QXmppOmemoMemoryStoragePrivate -{ -public: - bool isSetUp = false; - - std::optional ownDevice; - - // IDs of pre key pairs mapped to pre key pairs - QHash preKeyPairs; - - // IDs of signed pre key pairs mapped to signed pre key pairs - QHash signedPreKeyPairs; - - // recipient JID mapped to device ID mapped to device - QHash> devices; -}; - -/// -/// Constructs an OMEMO memory storage. -/// -QXmppOmemoMemoryStorage::QXmppOmemoMemoryStorage() - : d(new QXmppOmemoMemoryStoragePrivate) -{ -} - -QXmppOmemoMemoryStorage::~QXmppOmemoMemoryStorage() = default; - -/// \cond -QFuture QXmppOmemoMemoryStorage::allData() -{ - return makeReadyFuture(std::move(OmemoData { d->ownDevice, - d->signedPreKeyPairs, - d->preKeyPairs, - d->devices })); -} - -QFuture QXmppOmemoMemoryStorage::setOwnDevice(const std::optional &device) -{ - d->ownDevice = device; - return makeReadyFuture(); -} - -QFuture QXmppOmemoMemoryStorage::addSignedPreKeyPair(const uint32_t keyId, const SignedPreKeyPair &keyPair) -{ - d->signedPreKeyPairs.insert(keyId, keyPair); - return makeReadyFuture(); -} - -QFuture QXmppOmemoMemoryStorage::removeSignedPreKeyPair(const uint32_t keyId) -{ - d->signedPreKeyPairs.remove(keyId); - return makeReadyFuture(); -} - -QFuture QXmppOmemoMemoryStorage::addPreKeyPairs(const QHash &keyPairs) -{ - d->preKeyPairs.insert(keyPairs); - return makeReadyFuture(); -} - -QFuture QXmppOmemoMemoryStorage::removePreKeyPair(const uint32_t keyId) -{ - d->preKeyPairs.remove(keyId); - return makeReadyFuture(); -} - -QFuture QXmppOmemoMemoryStorage::addDevice(const QString &jid, const uint32_t deviceId, const QXmppOmemoStorage::Device &device) -{ - d->devices[jid].insert(deviceId, device); - return makeReadyFuture(); -} - -QFuture QXmppOmemoMemoryStorage::removeDevice(const QString &jid, const uint32_t deviceId) -{ - auto &devices = d->devices[jid]; - devices.remove(deviceId); - - // Remove the container for the passed JID if the container stores no - // devices anymore. - if (devices.isEmpty()) { - d->devices.remove(jid); - } - - return makeReadyFuture(); -} - -QFuture QXmppOmemoMemoryStorage::removeDevices(const QString &jid) -{ - d->devices.remove(jid); - return makeReadyFuture(); -} - -QFuture QXmppOmemoMemoryStorage::resetAll() -{ - d.reset(new QXmppOmemoMemoryStoragePrivate()); - return makeReadyFuture(); -} -/// \endcond diff --git a/src/client/QXmppOmemoMemoryStorage.h b/src/client/QXmppOmemoMemoryStorage.h deleted file mode 100644 index dc935de6..00000000 --- a/src/client/QXmppOmemoMemoryStorage.h +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Melvin Keskin -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#ifndef QXMPPOMEMOMEMORYSTORAGE_H -#define QXMPPOMEMOMEMORYSTORAGE_H - -#include "QXmppGlobal.h" -#include "QXmppOmemoStorage.h" - -#include - -class QXmppOmemoMemoryStoragePrivate; - -class QXMPP_EXPORT QXmppOmemoMemoryStorage : public QXmppOmemoStorage -{ -public: - QXmppOmemoMemoryStorage(); - ~QXmppOmemoMemoryStorage() override; - - /// \cond - QFuture allData() override; - - QFuture setOwnDevice(const std::optional &device) override; - - QFuture addSignedPreKeyPair(uint32_t keyId, const SignedPreKeyPair &keyPair) override; - QFuture removeSignedPreKeyPair(uint32_t keyId) override; - - QFuture addPreKeyPairs(const QHash &keyPairs) override; - QFuture removePreKeyPair(uint32_t keyId) override; - - QFuture addDevice(const QString &jid, uint32_t deviceId, const Device &device) override; - QFuture removeDevice(const QString &jid, uint32_t deviceId) override; - QFuture removeDevices(const QString &jid) override; - - QFuture resetAll() override; - /// \endcond - -private: - std::unique_ptr d; -}; - -#endif // QXMPPOMEMOMEMORYSTORAGE_H diff --git a/src/client/QXmppOmemoStorage.cpp b/src/client/QXmppOmemoStorage.cpp deleted file mode 100644 index 97b66dbe..00000000 --- a/src/client/QXmppOmemoStorage.cpp +++ /dev/null @@ -1,97 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Melvin Keskin -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -/// -/// \class QXmppOmemoStorage -/// -/// \brief The QXmppOmemoStorage class stores data used by -/// \xep{0384, OMEMO Encryption}. -/// -/// \warning THIS API IS NOT FINALIZED YET! -/// -/// \since QXmpp 1.5 -/// - -/// -/// \fn QXmppOmemoStorage::allData() -/// -/// Returns all data used by OMEMO. -/// -/// \return the OMEMO data -/// - -/// -/// \fn QXmppOmemoStorage::setOwnDevice(const std::optional &device) -/// -/// Sets the own device (i.e., the device used by this client instance). -/// -/// \param device own device -/// - -/// -/// \fn QXmppOmemoStorage::addSignedPreKeyPair(uint32_t keyId, const SignedPreKeyPair &keyPair) -/// -/// Adds a signed pre key pair. -/// -/// \param keyId ID of the signed pre key pair -/// \param keyPair signed pre key pair -/// - -/// -/// \fn QXmppOmemoStorage::removeSignedPreKeyPair(uint32_t keyId) -/// -/// Removes a signed pre key pair. -/// -/// \param keyId ID of the signed pre key pair -/// - -/// -/// \fn QXmppOmemoStorage::addPreKeyPairs(const QHash &keyPairs) -/// -/// Adds pre key pairs. -/// -/// \param keyPairs key IDs mapped to the pre key pairs -/// - -/// -/// \fn QXmppOmemoStorage::removePreKeyPair(uint32_t keyId) -/// -/// Removes a pre key pair. -/// -/// \param keyId ID of the pre key pair -/// - -/// -/// \fn QXmppOmemoStorage::addDevice(const QString &jid, uint32_t deviceId, const Device &device) -/// -/// Adds other devices (i.e., all devices but the own one). -/// -/// \param jid JID of the device owner -/// \param deviceId ID of the device -/// \param device device being added -/// - -/// -/// \fn QXmppOmemoStorage::removeDevice(const QString &jid, uint32_t deviceId) -/// -/// Removes a device of the other devices (i.e., all devices but the own one). -/// -/// \param jid JID of the device owner -/// \param deviceId ID of the device being removed -/// - -/// -/// \fn QXmppOmemoStorage::removeDevices(const QString &jid) -/// -/// Removes all devices of a passed JID from the other devices (i.e., all -/// devices but the own one). -/// -/// \param jid JID of the device owner -/// - -/// -/// \fn QXmppOmemoStorage::resetAll() -/// -/// Resets all data. -/// diff --git a/src/client/QXmppOmemoStorage.h b/src/client/QXmppOmemoStorage.h deleted file mode 100644 index 6c1388b4..00000000 --- a/src/client/QXmppOmemoStorage.h +++ /dev/null @@ -1,174 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Melvin Keskin -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#ifndef QXMPPOMEMOSTORAGE_H -#define QXMPPOMEMOSTORAGE_H - -#include "QXmppGlobal.h" - -#include - -#include -#include - -class QXMPP_EXPORT QXmppOmemoStorage -{ -public: - /// - /// Contains the data of this client instance's OMEMO device. - /// - struct OwnDevice - { - /// - /// ID used to identify a device and fetch its bundle - /// - /// A valid ID must be at least 1 and at most - /// \c std::numeric_limits::max(). - /// - uint32_t id = 0; - - /// - /// Human-readable string used to identify the device by users - /// - /// The label should not contain more than 53 characters. - /// - QString label; - - /// - /// Private long-term key which never changes - /// - QByteArray privateIdentityKey; - - /// - /// Public long-term key which never changes - /// - QByteArray publicIdentityKey; - - /// - /// ID of the latest pre key pair whose public key is signed - /// - /// A valid ID must be at least 1 and at most - /// \c std::numeric_limits::max(). - /// - uint32_t latestSignedPreKeyId = 1; - - /// - /// ID of the latest pre key pair - /// - /// A valid ID must be at least 1 and at most - /// \c std::numeric_limits::max(). - /// - uint32_t latestPreKeyId = 1; - }; - - /// - /// Contains the data of another OMEMO device. - /// That includes another own device (i.e., not this client instance's one) - /// or a contact's device. - /// - struct Device - { - /// - /// Human-readable string used to identify the device by users - /// - QString label; - - /// - /// ID of the public long-term key which never changes - /// - QByteArray keyId; - - /// - /// Session data which is only used internally by the OMEMO library - /// - QByteArray session; - - /// - /// Count of stanzas sent to the device without receiving a response - /// - /// It can be used to stop encryption in order to maintain a secure - /// communication. - /// - int unrespondedSentStanzasCount = 0; - - /// - /// Count of stanzas received from the device without sending a - /// response - /// - /// It can be used to send an empty response (heartbeat message) in - /// order to maintain a secure communication. - /// - int unrespondedReceivedStanzasCount = 0; - - /// - /// Date when the device was removed from the owner's device list - /// - /// It can be used to stop encrypting when a device is not used anymore. - /// - QDateTime removalFromDeviceListDate; - }; - - /// - /// Contains the data needed to manage an OMEMO signed pre key pair. - /// - struct SignedPreKeyPair - { - /// - /// Date when the signed pre key pair was created - /// - QDateTime creationDate; - - /// - /// Actual signed pre key pair - /// - QByteArray data; - }; - - /// - /// Contains all OMEMO data. - /// - struct OmemoData - { - /// - /// Device of this client instance - /// - std::optional ownDevice; - - /// - /// Key IDs mapped to their signed pre key pairs - /// - QHash signedPreKeyPairs; - - /// - /// Key IDs mapped to their pre key pairs - /// - QHash preKeyPairs; - - /// - /// JIDs of the device owners mapped to device IDs mapped to the other - /// devices (i.e., all devices except the own one) - /// - QHash> devices; - }; - - virtual ~QXmppOmemoStorage() = default; - - virtual QFuture allData() = 0; - - virtual QFuture setOwnDevice(const std::optional &device) = 0; - - virtual QFuture addSignedPreKeyPair(uint32_t keyId, const SignedPreKeyPair &keyPair) = 0; - virtual QFuture removeSignedPreKeyPair(uint32_t keyId) = 0; - - virtual QFuture addPreKeyPairs(const QHash &keyPairs) = 0; - virtual QFuture removePreKeyPair(uint32_t keyId) = 0; - - virtual QFuture addDevice(const QString &jid, uint32_t deviceId, const Device &device) = 0; - virtual QFuture removeDevice(const QString &jid, uint32_t deviceId) = 0; - virtual QFuture removeDevices(const QString &jid) = 0; - - virtual QFuture resetAll() = 0; -}; - -#endif // QXMPPOMEMOSTORAGE_H diff --git a/src/omemo/CMakeLists.txt b/src/omemo/CMakeLists.txt new file mode 100644 index 00000000..3c6e403e --- /dev/null +++ b/src/omemo/CMakeLists.txt @@ -0,0 +1,91 @@ +# SPDX-FileCopyrightText: 2022 Linus Jahn +# +# SPDX-License-Identifier: CC0-1.0 + +include(CMakePackageConfigHelpers) +include(GenerateExportHeader) + +set(OMEMO_CMAKE_DIR "${CMAKE_INSTALL_LIBDIR}/cmake/QXmppOmemo") +set(OMEMO_HEADER_DIR "${CMAKE_INSTALL_FULL_INCLUDEDIR}/qxmpp/omemo") +set(OMEMO_INSTALL_HEADER_FILES + QXmppOmemoManager.h + QXmppOmemoMemoryStorage.h + QXmppOmemoStorage.h +) +set(OMEMO_SOURCE_FILES + OmemoCryptoProvider.cpp + QXmppOmemoData.cpp + QXmppOmemoManager.cpp + QXmppOmemoManager_p.cpp + QXmppOmemoMemoryStorage.cpp + QXmppOmemoStorage.cpp +) + +add_library(QXmppOmemo SHARED ${OMEMO_SOURCE_FILES}) + +target_link_libraries(QXmppOmemo + PUBLIC + qxmpp + Qt${QT_VERSION_MAJOR}::Core + PRIVATE + PkgConfig::OmemoC + qca-qt${QT_VERSION_MAJOR} +) +target_include_directories(QXmppOmemo + PUBLIC + ${OMEMO_HEADER_DIR} + PRIVATE + ${CMAKE_CURRENT_LIST_DIR} + ${CMAKE_CURRENT_BINARY_DIR} +) + +generate_export_header(QXmppOmemo) + +install( + FILES ${OMEMO_INSTALL_HEADER_FILES} ${CMAKE_CURRENT_BINARY_DIR}/qxmppomemo_export.h + DESTINATION ${OMEMO_HEADER_DIR} +) + +set_target_properties(QXmppOmemo PROPERTIES + VERSION ${PROJECT_VERSION} + SOVERSION ${SO_VERSION} + EXPORT_NAME Omemo +) + +install( + TARGETS QXmppOmemo + DESTINATION ${CMAKE_INSTALL_LIBDIR} + EXPORT QXmppOmemoTargets +) + +install( + EXPORT QXmppOmemoTargets + DESTINATION ${OMEMO_CMAKE_DIR} + FILE QXmppOmemo.cmake + NAMESPACE QXmpp:: + COMPONENT Devel +) + +export( + TARGETS QXmppOmemo + FILE QXmppOmemo.cmake +) + +configure_package_config_file( + QXmppOmemoConfig.cmake.in + ${CMAKE_CURRENT_BINARY_DIR}/QXmppOmemoConfig.cmake + INSTALL_DESTINATION ${OMEMO_CMAKE_DIR} +) + +write_basic_package_version_file( + ${CMAKE_CURRENT_BINARY_DIR}/QXmppOmemoConfigVersion.cmake + VERSION ${PROJECT_VERSION} + COMPATIBILITY SameMajorVersion +) + +install( + FILES ${CMAKE_CURRENT_BINARY_DIR}/QXmppOmemoConfig.cmake + ${CMAKE_CURRENT_BINARY_DIR}/QXmppOmemoConfigVersion.cmake + DESTINATION ${OMEMO_CMAKE_DIR} + COMPONENT Devel +) diff --git a/src/omemo/OmemoCryptoProvider.cpp b/src/omemo/OmemoCryptoProvider.cpp new file mode 100644 index 00000000..e39124d4 --- /dev/null +++ b/src/omemo/OmemoCryptoProvider.cpp @@ -0,0 +1,239 @@ +// SPDX-FileCopyrightText: 2021 Linus Jahn +// SPDX-FileCopyrightText: 2022 Melvin Keskin +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "OmemoCryptoProvider.h" + +#include "QXmppOmemoManager_p.h" +#include "QXmppUtils_p.h" + +#include +#include + +using namespace QXmpp::Private; + +inline QXmppOmemoManagerPrivate *managerPrivate(void *ptr) +{ + return reinterpret_cast(ptr); +} + +static int random_func(uint8_t *data, size_t len, void *) +{ + generateRandomBytes(data, len); + return 0; +} + +int hmac_sha256_init_func(void **hmac_context, const uint8_t *key, size_t key_len, void *user_data) +{ + auto *d = managerPrivate(user_data); + + if (!QCA::MessageAuthenticationCode::supportedTypes().contains(PAYLOAD_MESSAGE_AUTHENTICATION_CODE_TYPE)) { + d->warning("Message authentication code type '" % QString(PAYLOAD_MESSAGE_AUTHENTICATION_CODE_TYPE) % "' is not supported by this system"); + return -1; + } + + QCA::SymmetricKey authenticationKey(QByteArray(reinterpret_cast(key), key_len)); + *hmac_context = new QCA::MessageAuthenticationCode(PAYLOAD_MESSAGE_AUTHENTICATION_CODE_TYPE, authenticationKey); + return 0; +} + +int hmac_sha256_update_func(void *hmac_context, const uint8_t *data, size_t data_len, void *) +{ + auto *messageAuthenticationCodeGenerator = reinterpret_cast(hmac_context); + messageAuthenticationCodeGenerator->update(QCA::MemoryRegion(QByteArray(reinterpret_cast(data), data_len))); + return 0; +} + +int hmac_sha256_final_func(void *hmac_context, signal_buffer **output, void *user_data) +{ + auto *d = managerPrivate(user_data); + auto *messageAuthenticationCodeGenerator = reinterpret_cast(hmac_context); + + auto messageAuthenticationCode = messageAuthenticationCodeGenerator->final(); + if (!(*output = signal_buffer_create(reinterpret_cast(messageAuthenticationCode.constData()), messageAuthenticationCode.size()))) { + d->warning("Message authentication code could not be loaded"); + return -1; + } + + return 0; +} + +void hmac_sha256_cleanup_func(void *hmac_context, void *) +{ + auto *messageAuthenticationCodeGenerator = reinterpret_cast(hmac_context); + delete messageAuthenticationCodeGenerator; +} + +int sha512_digest_init_func(void **digest_context, void *) +{ + *digest_context = new QCryptographicHash(QCryptographicHash::Sha512); + return 0; +} + +int sha512_digest_update_func(void *digest_context, const uint8_t *data, size_t data_len, void *) +{ + auto *hashGenerator = reinterpret_cast(digest_context); + hashGenerator->addData(reinterpret_cast(data), data_len); + return 0; +} + +int sha512_digest_final_func(void *digest_context, signal_buffer **output, void *user_data) +{ + auto *d = managerPrivate(user_data); + auto *hashGenerator = reinterpret_cast(digest_context); + + auto hash = hashGenerator->result(); + if (!(*output = signal_buffer_create(reinterpret_cast(hash.constData()), hash.size()))) { + d->warning("Hash could not be loaded"); + return -1; + } + + return 0; +} + +void sha512_digest_cleanup_func(void *digest_context, void *) +{ + auto *hashGenerator = reinterpret_cast(digest_context); + delete hashGenerator; +} + +int encrypt_func(signal_buffer **output, + int cipher, + const uint8_t *key, size_t key_len, + const uint8_t *iv, size_t iv_len, + const uint8_t *plaintext, size_t plaintext_len, + void *user_data) +{ + auto *d = managerPrivate(user_data); + + QString cipherName; + + switch (key_len) { + case 128 / 8: + cipherName = QStringLiteral("aes128"); + break; + case 192 / 8: + cipherName = QStringLiteral("aes192"); + break; + case 256 / 8: + cipherName = QStringLiteral("aes256"); + break; + default: + return -1; + } + + QCA::Cipher::Mode mode; + QCA::Cipher::Padding padding; + + switch (cipher) { + case SG_CIPHER_AES_CTR_NOPADDING: + mode = QCA::Cipher::CTR; + padding = QCA::Cipher::NoPadding; + break; + case SG_CIPHER_AES_CBC_PKCS5: + mode = QCA::Cipher::CBC; + padding = QCA::Cipher::PKCS7; + break; + default: + return -2; + } + + const auto encryptionKey = QCA::SymmetricKey(QByteArray(reinterpret_cast(key), key_len)); + const auto initializationVector = QCA::InitializationVector(QByteArray(reinterpret_cast(iv), iv_len)); + QCA::Cipher encryptionCipher(cipherName, mode, padding, QCA::Encode, encryptionKey, initializationVector); + + auto encryptedData = encryptionCipher.process(QCA::MemoryRegion(QByteArray(reinterpret_cast(plaintext), plaintext_len))); + + if (encryptedData.isEmpty()) { + return -3; + } + + if (!(*output = signal_buffer_create(reinterpret_cast(encryptedData.constData()), encryptedData.size()))) { + d->warning("Encrypted data could not be loaded"); + return -4; + } + + return 0; +} + +int decrypt_func(signal_buffer **output, + int cipher, + const uint8_t *key, size_t key_len, + const uint8_t *iv, size_t iv_len, + const uint8_t *ciphertext, size_t ciphertext_len, + void *user_data) +{ + auto *d = managerPrivate(user_data); + + QString cipherName; + + switch (key_len) { + case 128 / 8: + cipherName = QStringLiteral("aes128"); + break; + case 192 / 8: + cipherName = QStringLiteral("aes192"); + break; + case 256 / 8: + cipherName = QStringLiteral("aes256"); + break; + default: + return -1; + } + + QCA::Cipher::Mode mode; + QCA::Cipher::Padding padding; + + switch (cipher) { + case SG_CIPHER_AES_CTR_NOPADDING: + mode = QCA::Cipher::CTR; + padding = QCA::Cipher::NoPadding; + break; + case SG_CIPHER_AES_CBC_PKCS5: + mode = QCA::Cipher::CBC; + padding = QCA::Cipher::PKCS7; + break; + default: + return -2; + } + + const auto encryptionKey = QCA::SymmetricKey(QByteArray(reinterpret_cast(key), key_len)); + const auto initializationVector = QCA::InitializationVector(QByteArray(reinterpret_cast(iv), iv_len)); + QCA::Cipher decryptionCipher(cipherName, mode, padding, QCA::Decode, encryptionKey, initializationVector); + + auto decryptedData = decryptionCipher.process(QCA::MemoryRegion(QByteArray(reinterpret_cast(ciphertext), ciphertext_len))); + + if (decryptedData.isEmpty()) { + return -3; + } + + if (!(*output = signal_buffer_create(reinterpret_cast(decryptedData.constData()), decryptedData.size()))) { + d->warning("Decrypted data could not be loaded"); + return -4; + } + + return 0; +} + +namespace QXmpp::Omemo::Private { + +signal_crypto_provider createOmemoCryptoProvider(QXmppOmemoManagerPrivate *d) +{ + return { + random_func, + hmac_sha256_init_func, + hmac_sha256_update_func, + hmac_sha256_final_func, + hmac_sha256_cleanup_func, + sha512_digest_init_func, + sha512_digest_update_func, + sha512_digest_final_func, + sha512_digest_cleanup_func, + encrypt_func, + decrypt_func, + d, + }; +} + +} // namespace QXmpp::Omemo::Private diff --git a/src/omemo/OmemoCryptoProvider.h b/src/omemo/OmemoCryptoProvider.h new file mode 100644 index 00000000..f4c63cd8 --- /dev/null +++ b/src/omemo/OmemoCryptoProvider.h @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2022 Linus Jahn +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#ifndef OMEMOCRYPTOPROVIDER_H +#define OMEMOCRYPTOPROVIDER_H + +#include + +class QXmppOmemoManagerPrivate; + +namespace QXmpp::Omemo::Private { + +signal_crypto_provider createOmemoCryptoProvider(QXmppOmemoManagerPrivate *d); +} + +#endif // OMEMOCRYPTOPROVIDER_H diff --git a/src/omemo/OmemoLibWrappers.h b/src/omemo/OmemoLibWrappers.h new file mode 100644 index 00000000..e157f12c --- /dev/null +++ b/src/omemo/OmemoLibWrappers.h @@ -0,0 +1,100 @@ +// SPDX-FileCopyrightText: 2022 Linus Jahn +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#ifndef OMEMOLIBWRAPPERS_H +#define OMEMOLIBWRAPPERS_H + +#include +#include +#include +#include + +// Wraps various types of the OMEMO library. +template +class OmemoLibPtr +{ + T *m_ptr = nullptr; + +public: + OmemoLibPtr(T *ptr = nullptr) : m_ptr(ptr) { } + OmemoLibPtr(const OmemoLibPtr &) = delete; + ~OmemoLibPtr() + { + if (m_ptr) { + destruct(m_ptr); + } + } + OmemoLibPtr &operator=(const OmemoLibPtr &) = delete; + OmemoLibPtr &operator=(T *ptr) + { + reset(ptr); + return *this; + } + operator bool() const { return m_ptr != nullptr; } + T *operator->() const { return m_ptr; } + T *get() const { return m_ptr; } + T **ptrRef() { return &m_ptr; } + void reset(T *ptr) + { + if (m_ptr) { + destruct(m_ptr); + } + m_ptr = ptr; + } +}; + +template +void omemoLibUnrefHelper(T *ptr) +{ + SIGNAL_UNREF(ptr); +} + +template +using RefCountedPtr = OmemoLibPtr>; + +static QByteArray omemoLibBufferToByteArray(signal_buffer *buffer) +{ + return QByteArray(reinterpret_cast(signal_buffer_data(buffer)), signal_buffer_len(buffer)); +} + +static signal_buffer *omemoLibBufferFromByteArray(const QByteArray &bytes) +{ + return signal_buffer_create(reinterpret_cast(bytes.constData()), bytes.size()); +} + +template +class BufferPtrBase : public OmemoLibPtr +{ +public: + QByteArray toByteArray() const + { + return omemoLibBufferToByteArray(this->get()); + } +}; + +class BufferSecurePtr : public BufferPtrBase +{ +public: + static BufferSecurePtr fromByteArray(const QByteArray &bytes) + { + return { omemoLibBufferFromByteArray(bytes) }; + } +}; + +class BufferPtr : public BufferPtrBase +{ +public: + static BufferPtr fromByteArray(const QByteArray &bytes) + { + return { omemoLibBufferFromByteArray(bytes) }; + } +}; + +using KeyListNodePtr = OmemoLibPtr; +using SessionCipherPtr = OmemoLibPtr; +using SessionBuilderPtr = OmemoLibPtr; +using OmemoContextPtr = OmemoLibPtr; +using StoreContextPtr = OmemoLibPtr; + +#endif // OMEMOLIBWRAPPERS_H diff --git a/src/omemo/QXmppOmemoConfig.cmake.in b/src/omemo/QXmppOmemoConfig.cmake.in new file mode 100644 index 00000000..5363c0f5 --- /dev/null +++ b/src/omemo/QXmppOmemoConfig.cmake.in @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: 2017 Niels Ole Salscheider +# +# SPDX-License-Identifier: CC0-1.0 + +@PACKAGE_INIT@ + +include(CMakeFindDependencyMacro) +find_dependency(QXmpp) + +include("${CMAKE_CURRENT_LIST_DIR}/QXmppOmemo.cmake") + +check_required_components(QXmppOmemo) diff --git a/src/omemo/QXmppOmemoData.cpp b/src/omemo/QXmppOmemoData.cpp new file mode 100644 index 00000000..d1dd2a88 --- /dev/null +++ b/src/omemo/QXmppOmemoData.cpp @@ -0,0 +1,465 @@ +// SPDX-FileCopyrightText: 2021 Germán Márquez Mejía +// SPDX-FileCopyrightText: 2021 Melvin Keskin +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "QXmppConstants_p.h" +#include "QXmppOmemoDeviceBundle_p.h" +#include "QXmppOmemoDeviceElement_p.h" +#include "QXmppOmemoDeviceList_p.h" +#include "QXmppOmemoElement_p.h" +#include "QXmppOmemoEnvelope_p.h" +#include "QXmppOmemoIq_p.h" +#include "QXmppOmemoItems_p.h" +#include "QXmppUtils.h" + +#include +#include + +/// \cond +/// +/// \class QXmppOmemoDeviceElement +/// +/// \brief The QXmppOmemoDeviceElement class represents an element of the +/// OMEMO device list as defined by \xep{0384, OMEMO Encryption}. +/// + +/// +/// Returns true if the IDs of both elements match. +/// +bool QXmppOmemoDeviceElement::operator==(const QXmppOmemoDeviceElement &other) const +{ + return m_id == other.id(); +} + +/// +/// Returns the ID of this device element. +/// +/// The ID is used to identify a device and fetch its bundle. +/// The ID is 0 if it is unset. +/// +/// \see QXmppOmemoDeviceBundle +/// +/// \return this device element's ID +/// +uint32_t QXmppOmemoDeviceElement::id() const +{ + return m_id; +} + +/// +/// Sets the ID of this device element. +/// +/// The ID must be at least 1 and at most +/// \c std::numeric_limits::max(). +/// +/// \param id this device element's ID +/// +void QXmppOmemoDeviceElement::setId(uint32_t id) +{ + m_id = id; +} + +/// +/// Returns the label of this device element. +/// +/// The label is a human-readable string used to identify the device by users. +/// If no label is set, a default-constructed QString is returned. +/// +/// \return this device element's label +/// +QString QXmppOmemoDeviceElement::label() const +{ + return m_label; +} + +/// +/// Sets the optional label of this device element. +/// +/// The label should not contain more than 53 characters. +/// +/// \param label this device element's label +/// +void QXmppOmemoDeviceElement::setLabel(const QString &label) +{ + m_label = label; +} + +void QXmppOmemoDeviceElement::parse(const QDomElement &element) +{ + m_id = element.attribute(QStringLiteral("id")).toInt(); + m_label = element.attribute(QStringLiteral("label")); +} + +void QXmppOmemoDeviceElement::toXml(QXmlStreamWriter *writer) const +{ + writer->writeStartElement(QStringLiteral("device")); + + writer->writeAttribute(QStringLiteral("id"), QString::number(m_id)); + if (!m_label.isEmpty()) { + writer->writeAttribute(QStringLiteral("label"), m_label); + } + + writer->writeEndElement(); // device +} + +/// +/// Determines whether the given DOM element is an OMEMO device element. +/// +/// \param element DOM element being checked +/// +/// \return true if element is an OMEMO device element, otherwise false +/// +bool QXmppOmemoDeviceElement::isOmemoDeviceElement(const QDomElement &element) +{ + return element.tagName() == QStringLiteral("device") && + element.namespaceURI() == ns_omemo_2; +} + +/// +/// \class QXmppOmemoDeviceList +/// +/// \brief The QXmppOmemoDeviceList class represents an OMEMO device list +/// as defined by \xep{0384, OMEMO Encryption}. +/// + +void QXmppOmemoDeviceList::parse(const QDomElement &element) +{ + for (auto device = element.firstChildElement(QStringLiteral("device")); + !device.isNull(); + device = device.nextSiblingElement(QStringLiteral("device"))) { + QXmppOmemoDeviceElement deviceElement; + deviceElement.parse(device); + append(deviceElement); + } +} + +void QXmppOmemoDeviceList::toXml(QXmlStreamWriter *writer) const +{ + writer->writeStartElement(QStringLiteral("devices")); + writer->writeDefaultNamespace(ns_omemo_2); + + for (const auto &device : *this) { + device.toXml(writer); + } + + writer->writeEndElement(); +} + +/// +/// Determines whether the given DOM element is an OMEMO device list. +/// +/// \param element DOM element being checked +/// +/// \return true if element is an OMEMO device list, otherwise false +/// +bool QXmppOmemoDeviceList::isOmemoDeviceList(const QDomElement &element) +{ + return element.tagName() == QStringLiteral("devices") && + element.namespaceURI() == ns_omemo_2; +} + +/// +/// \class QXmppOmemoDeviceBundle +/// +/// \brief The QXmppOmemoDeviceBundle class represents an OMEMO bundle as +/// defined by \xep{0384, OMEMO Encryption}. +/// +/// It is a collection of publicly accessible data used by the X3DH key exchange. +/// The data is used to build an encrypted session with an OMEMO device. +/// + +/// +/// Returns the public identity key. +/// +/// The public identity key is the public long-term key which never changes. +/// +/// \return the public identity key +/// +QByteArray QXmppOmemoDeviceBundle::publicIdentityKey() const +{ + return m_publicIdentityKey; +} + +/// +/// Sets the public identity key. +/// +/// \param key public identity key +/// +void QXmppOmemoDeviceBundle::setPublicIdentityKey(const QByteArray &key) +{ + m_publicIdentityKey = key; +} + +/// +/// Returns the public pre key that is signed. +/// +/// \return the signed public pre key +/// +QByteArray QXmppOmemoDeviceBundle::signedPublicPreKey() const +{ + return m_signedPublicPreKey; +} + +/// +/// Sets the public pre key that is signed. +/// +/// \param key signed public pre key +/// +void QXmppOmemoDeviceBundle::setSignedPublicPreKey(const QByteArray &key) +{ + m_signedPublicPreKey = key; +} + +/// +/// Returns the ID of the public pre key that is signed. +/// +/// The ID is 0 if it is unset. +/// +/// \return the ID of the signed public pre key +/// +uint32_t QXmppOmemoDeviceBundle::signedPublicPreKeyId() const +{ + return m_signedPublicPreKeyId; +} + +/// +/// Sets the ID of the public pre key that is signed. +/// +/// The ID must be at least 1 and at most +/// \c std::numeric_limits::max(). +/// +/// \param id ID of the signed public pre key +/// +void QXmppOmemoDeviceBundle::setSignedPublicPreKeyId(uint32_t id) +{ + m_signedPublicPreKeyId = id; +} + +/// +/// Returns the signature of the public pre key that is signed. +/// +/// \return the signature of the signed public pre key +/// +QByteArray QXmppOmemoDeviceBundle::signedPublicPreKeySignature() const +{ + return m_signedPublicPreKeySignature; +} + +/// +/// Sets the signature of the public pre key that is signed. +/// +/// \param signature signature of the signed public pre key +/// +void QXmppOmemoDeviceBundle::setSignedPublicPreKeySignature(const QByteArray &signature) +{ + m_signedPublicPreKeySignature = signature; +} + +/// +/// Returns the public pre keys. +/// +/// The key of a key-value pair represents the ID of the corresponding public +/// pre key. +/// The value of a key-value pair represents the public pre key. +/// +/// \return the public pre keys +/// +QHash QXmppOmemoDeviceBundle::publicPreKeys() const +{ + return m_publicPreKeys; +} + +/// +/// Adds a public pre key. +/// +/// The ID must be at least 1 and at most +/// \c std::numeric_limits::max(). +/// +/// \param id ID of the public pre key +/// \param key public pre key +/// +void QXmppOmemoDeviceBundle::addPublicPreKey(uint32_t id, const QByteArray &key) +{ + m_publicPreKeys.insert(id, key); +} + +/// +/// Removes a public pre key. +/// +/// \param id ID of the public pre key +/// \param key public pre key +/// +void QXmppOmemoDeviceBundle::removePublicPreKey(uint32_t id) +{ + m_publicPreKeys.remove(id); +} + +void QXmppOmemoDeviceBundle::parse(const QDomElement &element) +{ + m_publicIdentityKey = QByteArray::fromBase64(element.firstChildElement(QStringLiteral("ik")).text().toLatin1()); + + const auto signedPublicPreKeyElement = element.firstChildElement(QStringLiteral("spk")); + if (!signedPublicPreKeyElement.isNull()) { + m_signedPublicPreKeyId = signedPublicPreKeyElement.attribute(QStringLiteral("id")).toInt(); + m_signedPublicPreKey = QByteArray::fromBase64(signedPublicPreKeyElement.text().toLatin1()); + } + m_signedPublicPreKeySignature = QByteArray::fromBase64(element.firstChildElement(QStringLiteral("spks")).text().toLatin1()); + + const auto publicPreKeysElement = element.firstChildElement(QStringLiteral("prekeys")); + if (!publicPreKeysElement.isNull()) { + for (QDomElement publicPreKeyElement = publicPreKeysElement.firstChildElement(QStringLiteral("pk")); + !publicPreKeyElement.isNull(); + publicPreKeyElement = publicPreKeyElement.nextSiblingElement(QStringLiteral("pk"))) { + m_publicPreKeys.insert(publicPreKeyElement.attribute(QStringLiteral("id")).toInt(), QByteArray::fromBase64(publicPreKeyElement.text().toLatin1())); + } + } +} + +void QXmppOmemoDeviceBundle::toXml(QXmlStreamWriter *writer) const +{ + writer->writeStartElement(QStringLiteral("bundle")); + writer->writeDefaultNamespace(ns_omemo_2); + + writer->writeStartElement(QStringLiteral("ik")); + writer->writeCharacters(publicIdentityKey().toBase64()); + writer->writeEndElement(); + + writer->writeStartElement(QStringLiteral("spk")); + writer->writeAttribute(QStringLiteral("id"), QString::number(signedPublicPreKeyId())); + writer->writeCharacters(signedPublicPreKey().toBase64()); + writer->writeEndElement(); + + writer->writeStartElement(QStringLiteral("spks")); + writer->writeCharacters(signedPublicPreKeySignature().toBase64()); + writer->writeEndElement(); + + writer->writeStartElement(QStringLiteral("prekeys")); + for (auto it = m_publicPreKeys.cbegin(); it != m_publicPreKeys.cend(); it++) { + writer->writeStartElement(QStringLiteral("pk")); + writer->writeAttribute(QStringLiteral("id"), QString::number(it.key())); + writer->writeCharacters(it.value().toBase64()); + writer->writeEndElement(); + } + writer->writeEndElement(); // prekeys + + writer->writeEndElement(); // bundle +} + +/// +/// Determines whether the given DOM element is an OMEMO device bundle. +/// +/// \param element DOM element being checked +/// +/// \return true if element is an OMEMO device bundle, otherwise false +/// +bool QXmppOmemoDeviceBundle::isOmemoDeviceBundle(const QDomElement &element) +{ + return element.tagName() == QStringLiteral("bundle") && + element.namespaceURI() == ns_omemo_2; +} + +/// +/// \class QXmppOmemoIq +/// +/// \brief The QXmppOmemoIq class represents an encrypted IQ stanza as defined +/// by \xep{0384, OMEMO Encryption} and \xep{0420, Stanza Content Encryption} +/// (SCE). +/// +/// \ingroup Stanzas +/// + +/// +/// Returns the OMEMO element which contains the data used by OMEMO. +/// +/// \return the OMEMO element +/// +QXmppOmemoElement QXmppOmemoIq::omemoElement() +{ + return m_omemoElement; +} + +/// +/// Sets the OMEMO element which contains the data used by OMEMO. +/// +/// \param omemoElement OMEMO element +/// +void QXmppOmemoIq::setOmemoElement(const QXmppOmemoElement &omemoElement) +{ + m_omemoElement = omemoElement; +} + +void QXmppOmemoIq::parseElementFromChild(const QDomElement &element) +{ + QDomElement child = element.firstChildElement(); + m_omemoElement.parse(child); +} + +void QXmppOmemoIq::toXmlElementFromChild(QXmlStreamWriter *writer) const +{ + m_omemoElement.toXml(writer); +} + +/// +/// Determines whether the given DOM element is an OMEMO IQ stanza. +/// +/// \param element DOM element being checked +/// +/// \return true if element is an OMEMO IQ stanza, otherwise false +/// +bool QXmppOmemoIq::isOmemoIq(const QDomElement &element) +{ + auto child = element.firstChildElement(); + return !child.isNull() && QXmppOmemoElement::isOmemoElement(child); +} + +QXmppOmemoDeviceBundle QXmppOmemoDeviceBundleItem::deviceBundle() const +{ + return m_deviceBundle; +} + +void QXmppOmemoDeviceBundleItem::setDeviceBundle(const QXmppOmemoDeviceBundle &deviceBundle) +{ + m_deviceBundle = deviceBundle; +} + +bool QXmppOmemoDeviceBundleItem::isItem(const QDomElement &itemElement) +{ + return QXmppPubSubItem::isItem(itemElement, QXmppOmemoDeviceBundle::isOmemoDeviceBundle); +} + +void QXmppOmemoDeviceBundleItem::parsePayload(const QDomElement &payloadElement) +{ + m_deviceBundle.parse(payloadElement); +} + +void QXmppOmemoDeviceBundleItem::serializePayload(QXmlStreamWriter *writer) const +{ + m_deviceBundle.toXml(writer); +} + +QXmppOmemoDeviceList QXmppOmemoDeviceListItem::deviceList() const +{ + return m_deviceList; +} + +void QXmppOmemoDeviceListItem::setDeviceList(const QXmppOmemoDeviceList &deviceList) +{ + m_deviceList = deviceList; +} + +bool QXmppOmemoDeviceListItem::isItem(const QDomElement &itemElement) +{ + return QXmppPubSubItem::isItem(itemElement, QXmppOmemoDeviceList::isOmemoDeviceList); +} + +void QXmppOmemoDeviceListItem::parsePayload(const QDomElement &payloadElement) +{ + m_deviceList.parse(payloadElement); +} + +void QXmppOmemoDeviceListItem::serializePayload(QXmlStreamWriter *writer) const +{ + m_deviceList.toXml(writer); +} +/// \endcond diff --git a/src/omemo/QXmppOmemoDeviceBundle_p.h b/src/omemo/QXmppOmemoDeviceBundle_p.h new file mode 100644 index 00000000..49506e86 --- /dev/null +++ b/src/omemo/QXmppOmemoDeviceBundle_p.h @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: 2021 Germán Márquez Mejía +// SPDX-FileCopyrightText: 2021 Melvin Keskin +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#ifndef QXMPPOMEMODEVICEBUNDLE_H +#define QXMPPOMEMODEVICEBUNDLE_H + +#include "QXmppGlobal.h" + +#include + +class QDomElement; +class QXmlStreamWriter; + +class QXMPP_AUTOTEST_EXPORT QXmppOmemoDeviceBundle +{ +public: + QByteArray publicIdentityKey() const; + void setPublicIdentityKey(const QByteArray &key); + + QByteArray signedPublicPreKey() const; + void setSignedPublicPreKey(const QByteArray &key); + + uint32_t signedPublicPreKeyId() const; + void setSignedPublicPreKeyId(uint32_t id); + + QByteArray signedPublicPreKeySignature() const; + void setSignedPublicPreKeySignature(const QByteArray &signature); + + QHash publicPreKeys() const; + void addPublicPreKey(uint32_t id, const QByteArray &key); + void removePublicPreKey(uint32_t id); + + /// \cond + void parse(const QDomElement &element); + void toXml(QXmlStreamWriter *writer) const; + /// \endcond + + static bool isOmemoDeviceBundle(const QDomElement &element); + +private: + QByteArray m_publicIdentityKey; + QByteArray m_signedPublicPreKey; + uint32_t m_signedPublicPreKeyId = 0; + QByteArray m_signedPublicPreKeySignature; + QHash m_publicPreKeys; +}; + +Q_DECLARE_TYPEINFO(QXmppOmemoDeviceBundle, Q_MOVABLE_TYPE); + +#endif // QXMPPOMEMODEVICEBUNDLE_H diff --git a/src/omemo/QXmppOmemoDeviceElement_p.h b/src/omemo/QXmppOmemoDeviceElement_p.h new file mode 100644 index 00000000..1c391e30 --- /dev/null +++ b/src/omemo/QXmppOmemoDeviceElement_p.h @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2021 Germán Márquez Mejía +// SPDX-FileCopyrightText: 2021 Melvin Keskin +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#ifndef QXMPPOMEMODEVICEELEMENT_H +#define QXMPPOMEMODEVICEELEMENT_H + +#include "QXmppGlobal.h" + +class QDomElement; +class QXmlStreamWriter; + +class QXMPP_AUTOTEST_EXPORT QXmppOmemoDeviceElement +{ +public: + bool operator==(const QXmppOmemoDeviceElement &other) const; + + uint32_t id() const; + void setId(uint32_t id); + + QString label() const; + void setLabel(const QString &label); + + /// \cond + void parse(const QDomElement &element); + void toXml(QXmlStreamWriter *writer) const; + /// \endcond + + static bool isOmemoDeviceElement(const QDomElement &element); + +private: + uint32_t m_id = 0; + QString m_label; +}; + +Q_DECLARE_TYPEINFO(QXmppOmemoDeviceElement, Q_MOVABLE_TYPE); + +#endif // QXMPPOMEMODEVICEELEMENT_H diff --git a/src/omemo/QXmppOmemoDeviceList_p.h b/src/omemo/QXmppOmemoDeviceList_p.h new file mode 100644 index 00000000..76bff435 --- /dev/null +++ b/src/omemo/QXmppOmemoDeviceList_p.h @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2021 Germán Márquez Mejía +// SPDX-FileCopyrightText: 2021 Melvin Keskin +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#ifndef QXMPPOMEMODEVICELIST_H +#define QXMPPOMEMODEVICELIST_H + +#include "QXmppGlobal.h" + +#include "QList" + +class QDomElement; +class QXmlStreamWriter; +class QXmppOmemoDeviceElement; + +class QXMPP_AUTOTEST_EXPORT QXmppOmemoDeviceList : public QList +{ +public: + /// \cond + void parse(const QDomElement &element); + void toXml(QXmlStreamWriter *writer) const; + /// \endcond + + static bool isOmemoDeviceList(const QDomElement &element); +}; + +Q_DECLARE_TYPEINFO(QXmppOmemoDeviceList, Q_MOVABLE_TYPE); + +#endif // QXMPPOMEMODEVICELIST_H diff --git a/src/omemo/QXmppOmemoIq_p.h b/src/omemo/QXmppOmemoIq_p.h new file mode 100644 index 00000000..15623884 --- /dev/null +++ b/src/omemo/QXmppOmemoIq_p.h @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2022 Melvin Keskin +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#ifndef QXMPPOMEMOIQ_H +#define QXMPPOMEMOIQ_H + +#include "QXmppIq.h" +#include "QXmppOmemoElement_p.h" + +class QXMPP_AUTOTEST_EXPORT QXmppOmemoIq : public QXmppIq +{ +public: + QXmppOmemoElement omemoElement(); + void setOmemoElement(const QXmppOmemoElement &omemoElement); + + /// \cond + void parseElementFromChild(const QDomElement &element) override; + void toXmlElementFromChild(QXmlStreamWriter *writer) const override; + /// \endcond + + static bool isOmemoIq(const QDomElement &element); + +private: + QXmppOmemoElement m_omemoElement; +}; + +#endif // QXMPPOMEMOIQ_H diff --git a/src/omemo/QXmppOmemoItems_p.h b/src/omemo/QXmppOmemoItems_p.h new file mode 100644 index 00000000..9b816ed8 --- /dev/null +++ b/src/omemo/QXmppOmemoItems_p.h @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2022 Melvin Keskin +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#ifndef QXMPPOMEMOITEMS_H +#define QXMPPOMEMOITEMS_H + +#include "QXmppOmemoDeviceBundle_p.h" +#include "QXmppOmemoDeviceList_p.h" +#include "QXmppPubSubItem.h" + +class QXmppOmemoDeviceBundleItem : public QXmppPubSubItem +{ +public: + QXmppOmemoDeviceBundle deviceBundle() const; + void setDeviceBundle(const QXmppOmemoDeviceBundle &deviceBundle); + + static bool isItem(const QDomElement &itemElement); + +protected: + void parsePayload(const QDomElement &payloadElement) override; + void serializePayload(QXmlStreamWriter *writer) const override; + +private: + QXmppOmemoDeviceBundle m_deviceBundle; +}; + +class QXmppOmemoDeviceListItem : public QXmppPubSubItem +{ +public: + QXmppOmemoDeviceList deviceList() const; + void setDeviceList(const QXmppOmemoDeviceList &deviceList); + + static bool isItem(const QDomElement &itemElement); + +protected: + void parsePayload(const QDomElement &payloadElement) override; + void serializePayload(QXmlStreamWriter *writer) const override; + +private: + QXmppOmemoDeviceList m_deviceList; +}; + +#endif // QXMPPOMEMOITEMS_H diff --git a/src/omemo/QXmppOmemoManager.cpp b/src/omemo/QXmppOmemoManager.cpp new file mode 100644 index 00000000..cb66a515 --- /dev/null +++ b/src/omemo/QXmppOmemoManager.cpp @@ -0,0 +1,1282 @@ +// SPDX-FileCopyrightText: 2022 Melvin Keskin +// SPDX-FileCopyrightText: 2022 Linus Jahn +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "QXmppClient.h" +#include "QXmppConstants_p.h" +#include "QXmppOmemoDeviceElement_p.h" +#include "QXmppOmemoDeviceList_p.h" +#include "QXmppOmemoElement_p.h" +#include "QXmppOmemoEnvelope_p.h" +#include "QXmppOmemoIq_p.h" +#include "QXmppOmemoItems_p.h" +#include "QXmppOmemoManager_p.h" +#include "QXmppPubSubEvent.h" +#include "QXmppTrustManager.h" +#include "QXmppUtils.h" + +#include + +using namespace QXmpp; +using namespace QXmpp::Private; +using namespace QXmpp::Omemo::Private; + +using Error = QXmppStanza::Error; +using Manager = QXmppOmemoManager; +using ManagerPrivate = QXmppOmemoManagerPrivate; + +// default label used for the own device +const auto DEVICE_LABEL = QStringLiteral("QXmpp"); + +class QXmppOmemoOwnDevicePrivate : public QSharedData +{ +public: + QString label; + QByteArray keyId; +}; + +/// +/// \class QXmppOmemoOwnDevice +/// +/// \brief The QXmppOmemoOwnDevice class represents the \xep{0384, OMEMO Encryption} device of this +/// client instance. +/// + +/// +/// Constructs an OMEMO device for this client instance. +/// +QXmppOmemoOwnDevice::QXmppOmemoOwnDevice() + : d(new QXmppOmemoOwnDevicePrivate) +{ +} + +/// Copy-constructor. +QXmppOmemoOwnDevice::QXmppOmemoOwnDevice(const QXmppOmemoOwnDevice &other) = default; +/// Move-constructor. +QXmppOmemoOwnDevice::QXmppOmemoOwnDevice(QXmppOmemoOwnDevice &&) noexcept = default; +QXmppOmemoOwnDevice::~QXmppOmemoOwnDevice() = default; +/// Assignment operator. +QXmppOmemoOwnDevice &QXmppOmemoOwnDevice::operator=(const QXmppOmemoOwnDevice &) = default; +/// Move-assignment operator. +QXmppOmemoOwnDevice &QXmppOmemoOwnDevice::operator=(QXmppOmemoOwnDevice &&) = default; + +/// +/// Returns the human-readable string used to identify the device by users. +/// +/// If no label is set, a default-constructed QString is returned. +/// +/// \return the label to identify the device +/// +QString QXmppOmemoOwnDevice::label() const +{ + return d->label; +} + +/// +/// Sets an optional human-readable string used to identify the device by users. +/// +/// The label should not contain more than 53 characters. +/// +/// \param label label to identify the device +/// +void QXmppOmemoOwnDevice::setLabel(const QString &label) +{ + d->label = label; +} + +/// +/// Returns the ID of the public long-term key which never changes. +/// +/// \return public long-term key ID +/// +QByteArray QXmppOmemoOwnDevice::keyId() const +{ + return d->keyId; +} + +/// +/// Sets the ID of the public long-term key which never changes. +/// +/// \param keyId public long-term key ID +/// +void QXmppOmemoOwnDevice::setKeyId(const QByteArray &keyId) +{ + d->keyId = keyId; +} + +class QXmppOmemoDevicePrivate : public QSharedData +{ +public: + QString jid; + TrustLevel trustLevel = TrustLevel::Undecided; + QString label; + QByteArray keyId; +}; + +/// +/// \class QXmppOmemoDevice +/// +/// \brief The QXmppOmemoDevice class represents a \xep{0384, OMEMO Encryption} device. +/// + +/// +/// Constructs an OMEMO device. +/// +QXmppOmemoDevice::QXmppOmemoDevice() + : d(new QXmppOmemoDevicePrivate) +{ +} + +/// Copy-constructor. +QXmppOmemoDevice::QXmppOmemoDevice(const QXmppOmemoDevice &other) = default; +/// Move-constructor. +QXmppOmemoDevice::QXmppOmemoDevice(QXmppOmemoDevice &&) noexcept = default; +QXmppOmemoDevice::~QXmppOmemoDevice() = default; +/// Assignment operator. +QXmppOmemoDevice &QXmppOmemoDevice::operator=(const QXmppOmemoDevice &) = default; +/// Move-assignment operator. +QXmppOmemoDevice &QXmppOmemoDevice::operator=(QXmppOmemoDevice &&) = default; + +/// +/// Returns the device owner's bare JID. +/// +/// \return the bare JID +/// +QString QXmppOmemoDevice::jid() const +{ + return d->jid; +} + +/// +/// Sets the device owner's bare JID. +/// +/// \param jid bare JID of the device owner +/// +void QXmppOmemoDevice::setJid(const QString &jid) +{ + d->jid = jid; +} + +/// +/// Returns the human-readable string used to identify the device by users. +/// +/// If no label is set, a default-constructed QString is returned. +/// +/// \return the label to identify the device +/// +QString QXmppOmemoDevice::label() const +{ + return d->label; +} + +/// +/// Sets an optional human-readable string used to identify the device by users. +/// +/// The label should not contain more than 53 characters. +/// +/// \param label label to identify the device +/// +void QXmppOmemoDevice::setLabel(const QString &label) +{ + d->label = label; +} + +/// +/// Returns the ID of the public long-term key which never changes. +/// +/// \return public long-term key ID +/// +QByteArray QXmppOmemoDevice::keyId() const +{ + return d->keyId; +} + +/// +/// Sets the ID of the public long-term key which never changes. +/// +/// \param keyId public long-term key ID +/// +void QXmppOmemoDevice::setKeyId(const QByteArray &keyId) +{ + d->keyId = keyId; +} + +/// +/// Returns the trust level of the key. +/// +/// \return the key's trust level +/// +TrustLevel QXmppOmemoDevice::trustLevel() const +{ + return d->trustLevel; +} + +/// +/// Sets the trust level of the key. +/// +/// \param trustLevel key's trust level +/// +void QXmppOmemoDevice::setTrustLevel(TrustLevel trustLevel) +{ + d->trustLevel = trustLevel; +} + +/// +/// \class QXmppOmemoManager +/// +/// The QXmppOmemoManager class manages OMEMO encryption as defined in \xep{0384, +/// OMEMO Encryption}. +/// +/// OMEMO uses \xep{0060, Publish-Subscribe} (PubSub) and \xep{0163, Personal Eventing Protocol} +/// (PEP). +/// Thus, they must be supported by the server and the corresponding PubSub manager must be added to +/// the client: +/// \code +/// QXmppPubSubManager *pubSubManager = new QXmppPubSubManager; +/// client->addExtension(pubSubManager); +/// \endcode +/// +/// For interacting with the storage, corresponding implementations of the storage interfaces must +/// be instantiated. +/// Those implementations have to be adapted to your storage such as a database. +/// In case you only need memory and no persistent storage, you can use the existing +/// implementations: +/// \code +/// QXmppOmemoStorage *omemoStorage = new QXmppOmemoMemoryStorage; +/// QXmppTrustStorage *trustStorage = new QXmppTrustMemoryStorage; +/// \endcode +/// +/// A trust manager using its storage must be added to the client: +/// \code +/// QXmppTrustManager *trustManager = new QXmppAtmManager(trustStorage); +/// client->addExtension(trustManager); +/// \endcode +/// +/// Afterwards, the OMEMO manager using its storage must be added to the client: +/// \code +/// QXmppOmemoManager *manager = new QXmppOmemoManager(omemoStorage); +/// client->addExtension(manager); +/// \endcode +/// +/// You can set a security policy used by OMEMO. +/// Is is recommended to apply TOAKAFA for good security and usability when using +/// \xep{0450, Automatic Trust Management (ATM)}: +/// \code +/// manager->setSecurityPolicy(QXmpp::Toakafa); +/// \endcode +/// +/// \xep{0280, Message Carbons} should be used for delivering messages to all endpoints of a user: +/// \code +/// QXmppCarbonManager *carbonManager = new QXmppCarbonManager; +/// client->addExtension(carbonManager); +/// connect(client, &QXmppClient::connected, this, [=]() { +/// carbonManager->setCarbonsEnabled(true); +/// }); +/// connect(carbonManager, &QXmppCarbonManager::messageSent, manager, +/// &QXmppOmemoManager::handleMessage); +/// connect(carbonManager, &QXmppCarbonManager::messageReceived, manager, +/// &QXmppOmemoManager::handleMessage); +/// \endcode +/// +/// The OMEMO data must be loaded before connecting to the server: +/// \code +/// manager->load(); +/// }); +/// \endcode +/// +/// If no OMEMO data could be loaded (i.e., the result of \c load() is "false"), it must be set up +/// first. +/// That can be done as soon as the user is logged in to the server: +/// \code +/// connect(client, &QXmppClient::connected, this, [=]() { +/// auto future = manager->start(); +/// }); +/// \endcode +/// +/// Once the future is finished and the result is "true", the manager is ready for use. +/// Otherwise, check the logging output for details. +/// +/// By default, stanzas are only sent to devices having keys with the following trust levels: +/// \code +/// QXmpp::TrustLevel::AutomaticallyTrusted | QXmpp::TrustLevel::ManuallyTrusted +/// | QXmpp::TrustLevel::Authenticated +/// \endcode +/// That behavior can be changed for each message being sent by specifying the +/// accepted trust levels: +/// \code +/// QXmppSendStanzaParams params; +/// params.setAcceptedTrustLevels(QXmpp::TrustLevel::Authenticated) +/// client->send(stanza, params); +/// \endcode +/// +/// Stanzas can be encrypted for multiple JIDs which is needed in group chats: +/// \code +/// QXmppSendStanzaParams params; +/// params.setEncryptionJids({ "alice@example.org", "bob@example.com" }) +/// client->send(stanza, params); +/// \endcode +/// +/// \warning THIS API IS NOT FINALIZED YET! +/// +/// \ingroup Managers +/// +/// \since QXmpp 1.5 +/// + +/// +/// \typedef QXmppOmemoManager::Result +/// +/// Contains QXmpp::Success for success or an QXmppStanza::Error for an error. +/// + +/// +/// Constructs an OMEMO manager. +/// +/// \param omemoStorage storage used to store all OMEMO data +/// +QXmppOmemoManager::QXmppOmemoManager(QXmppOmemoStorage *omemoStorage) + : d(new ManagerPrivate(this, omemoStorage)) +{ + d->ownDevice.label = DEVICE_LABEL; + d->init(); + d->schedulePeriodicTasks(); +} + +QXmppOmemoManager::~QXmppOmemoManager() = default; + +/// +/// Loads all locally stored OMEMO data. +/// +/// This should be called after starting the client and before the login. +/// It must only be called after \c setUp() has been called once for the user +/// during one of the past login session. +/// It does not need to be called if setUp() has been called during the current +/// login session. +/// +/// \see QXmppOmemoManager::setUp() +/// +/// \return whether everything is loaded successfully +/// +QFuture Manager::load() +{ + QFutureInterface interface(QFutureInterfaceBase::Started); + + auto future = d->omemoStorage->allData(); + await(future, this, [=](QXmppOmemoStorage::OmemoData omemoData) mutable { + const auto &optionalOwnDevice = omemoData.ownDevice; + if (optionalOwnDevice) { + d->ownDevice = *optionalOwnDevice; + } else { + debug("Device could not be loaded because it is not stored"); + reportFinishedResult(interface, false); + return; + } + + const auto &signedPreKeyPairs = omemoData.signedPreKeyPairs; + if (signedPreKeyPairs.isEmpty()) { + warning("Signed Pre keys could not be loaded because none is stored"); + reportFinishedResult(interface, false); + return; + } else { + d->signedPreKeyPairs = signedPreKeyPairs; + d->renewSignedPreKeyPairs(); + } + + const auto &preKeyPairs = omemoData.preKeyPairs; + if (preKeyPairs.isEmpty()) { + warning("Pre keys could not be loaded because none is stored"); + reportFinishedResult(interface, false); + return; + } else { + d->preKeyPairs = preKeyPairs; + } + + d->devices = omemoData.devices; + d->removeDevicesRemovedFromServer(); + + reportFinishedResult(interface, d->isStarted = true); + }); + + return interface.future(); +} + +/// +/// Sets up all OMEMO data locally and on the server. +/// +/// The user must be logged in while calling this. +/// +/// \return whether everything is set up successfully +/// +QFuture Manager::setUp() +{ + QFutureInterface interface(QFutureInterfaceBase::Started); + + auto future = d->setUpDeviceId(); + await(future, this, [=](bool isDeviceIdSetUp) mutable { + if (isDeviceIdSetUp) { + // The identity key pair in its deserialized form is not stored as a + // member variable because it is only needed by + // updateSignedPreKeyPair(). + RefCountedPtr identityKeyPair; + + if (d->setUpIdentityKeyPair(identityKeyPair.ptrRef()) && + d->updateSignedPreKeyPair(identityKeyPair.get()) && + d->updatePreKeyPairs(PRE_KEY_INITIAL_CREATION_COUNT)) { + auto future = d->omemoStorage->setOwnDevice(d->ownDevice); + await(future, this, [=]() mutable { + auto future = d->publishOmemoData(); + await(future, this, [=](bool isPublished) mutable { + reportFinishedResult(interface, d->isStarted = isPublished); + }); + }); + } else { + reportFinishedResult(interface, false); + } + } else { + reportFinishedResult(interface, false); + } + }); + + return interface.future(); +} + +/// +/// Returns the key of this client instance. +/// +/// \return the own key +/// +QFuture Manager::ownKey() +{ + return d->trustManager->ownKey(ns_omemo_2); +} + +/// +/// Returns the JIDs of all key owners mapped to the IDs of their keys with +/// specific trust levels. +/// +/// If no trust levels are passed, all keys are returned. +/// +/// This should be called in order to get all stored keys which can be more than +/// the stored devices because of trust decisions made without a published or +/// received device. +/// +/// \param trustLevels trust levels of the keys +/// +/// \return the key owner JIDs mapped to their keys with specific trust levels +/// +QFuture>> Manager::keys(QXmpp::TrustLevels trustLevels) +{ + return d->trustManager->keys(ns_omemo_2, trustLevels); +} + +/// +/// Returns the IDs of keys mapped to their trust levels for specific key +/// owners. +/// +/// If no trust levels are passed, all keys for jids are returned. +/// +/// This should be called in order to get the stored keys which can be more than +/// the stored devices because of trust decisions made without a published or +/// received device. +/// +/// \param jids key owners' bare JIDs +/// \param trustLevels trust levels of the keys +/// +/// \return the key IDs mapped to their trust levels for specific key owners +/// +QFuture>> Manager::keys(const QList &jids, QXmpp::TrustLevels trustLevels) +{ + return d->trustManager->keys(ns_omemo_2, jids, trustLevels); +} + +/// +/// Changes the label of the own (this client instance's current user's) device. +/// +/// The label is a human-readable string used to identify the device by users. +/// +/// If the OMEMO manager is not started yet, the device label is only changed +/// locally in memory. +/// It is stored persistently in the OMEMO storage and updated on the +/// server if the OMEMO manager is already started or once it is. +/// +/// \param deviceLabel own device's label +/// +/// \return whether the action was successful +/// +QFuture Manager::changeDeviceLabel(const QString &deviceLabel) +{ + return d->changeDeviceLabel(deviceLabel); +} + +/// +/// Returns the maximum count of devices stored per JID. +/// +/// If more devices than that maximum are received for one JID from a server, +/// they will not be stored locally and thus not used for encryption. +/// +/// \return the maximum count of devices stored per JID +/// +int Manager::maximumDevicesPerJid() const +{ + return d->maximumDevicesPerJid; +} + +/// +/// Sets the maximum count of devices stored per JID. +/// +/// If more devices than that maximum are received for one JID from a server, +/// they will not be stored locally and thus not used for encryption. +/// +/// \param maximum maximum count of devices stored per JID +/// +void Manager::setMaximumDevicesPerJid(int maximum) +{ + d->maximumDevicesPerJid = maximum; +} + +/// +/// Returns the maximum count of devices for whom a stanza is encrypted. +/// +/// If more devices than that maximum are stored for all addressed recipients of +/// a stanza, the stanza will only be encrypted for first devices until the +/// maximum is reached. +/// +/// \return the maximum count of devices for whom a stanza is encrypted +/// +int Manager::maximumDevicesPerStanza() const +{ + return d->maximumDevicesPerStanza; +} + +/// Sets the maximum count of devices for whom a stanza is encrypted. +/// +/// If more devices than that maximum are stored for all addressed recipients of +/// a stanza, the stanza will only be encrypted for first devices until the +/// maximum is reached. +/// +/// \param maximum maximum count of devices for whom a stanza is encrypted +/// +void Manager::setMaximumDevicesPerStanza(int maximum) +{ + d->maximumDevicesPerStanza = maximum; +} + +/// +/// Requests device lists from contacts and stores them locally. +/// +/// The user must be logged in while calling this. +/// The JID of the current user must not be passed. +/// +/// \param jids JIDs of the contacts whose device lists are being requested +/// +/// \return the results of the requests for each JID +/// +QFuture Manager::requestDeviceLists(const QList &jids) +{ + if (const auto jidsCount = jids.size()) { + QFutureInterface interface(QFutureInterfaceBase::Started); + auto processedJidsCount = std::make_shared(0); + + for (const auto &jid : jids) { + Q_ASSERT_X(jid != d->ownBareJid(), "Requesting contact's device list", "Own JID passed"); + + auto future = d->requestDeviceList(jid); + await(future, this, [=](auto result) mutable { + DevicesResult devicesResult { + jid, + mapSuccess(std::move(result), [](QXmppOmemoDeviceListItem) { return Success(); }) + }; + interface.reportResult(devicesResult); + + if (++(*processedJidsCount) == jidsCount) { + interface.reportFinished(); + } + }); + } + return interface.future(); + } + return QFutureInterface(QFutureInterfaceBase::Finished).future(); +} + +/// +/// Subscribes the current user's resource to device lists manually. +/// +/// This should be called after each login and only for contacts without +/// presence subscription because their device lists are not automatically +/// subscribed. +/// The user must be logged in while calling this. +/// +/// Call \c QXmppOmemoManager::unsubscribeFromDeviceLists() before logout. +/// +/// \param jids JIDs of the contacts whose device lists are being subscribed +/// +/// \return the results of the subscription for each JID +/// +QFuture Manager::subscribeToDeviceLists(const QList &jids) +{ + QFutureInterface interface(QFutureInterfaceBase::Started); + + if (const auto jidsCount = jids.size()) { + auto processedJidsCount = std::make_shared(0); + + for (const auto &jid : jids) { + auto future = d->subscribeToDeviceList(jid); + await(future, this, [=](QXmppPubSubManager::Result result) mutable { + Manager::DevicesResult devicesResult; + devicesResult.jid = jid; + devicesResult.result = result; + interface.reportResult(devicesResult); + + if (++(*processedJidsCount) == jidsCount) { + interface.reportFinished(); + } + }); + } + } else { + interface.reportFinished(); + } + + return interface.future(); +} + +/// +/// Unsubscribes the current user's resource from all device lists that were +/// manually subscribed by \c QXmppOmemoManager::subscribeToDeviceList(). +/// +/// This should be called before each logout. +/// The user must be logged in while calling this. +/// +/// \return the results of the unsubscription for each JID +/// +QFuture Manager::unsubscribeFromDeviceLists() +{ + return d->unsubscribeFromDeviceLists(d->jidsOfManuallySubscribedDevices); +} + +/// +/// Returns the device of this client instance's current user. +/// +/// \return the own device +/// +QXmppOmemoOwnDevice Manager::ownDevice() +{ + const auto &ownDevice = d->ownDevice; + + QXmppOmemoOwnDevice device; + device.setLabel(ownDevice.label); + device.setKeyId(createKeyId(ownDevice.publicIdentityKey)); + + return device; +} + +/// Returns all locally stored devices except the own device. +/// +/// Only devices that have been received after subscribing the corresponding +/// device lists on the server are stored locally. +/// Thus, only those are returned. +/// Call \c QXmppOmemoManager::subscribeToDeviceLists() for contacts without +/// presence subscription before. +/// +/// /\return all devices except the own device +/// +QFuture> Manager::devices() +{ + return devices(d->devices.keys()); +} + +/// +/// Returns locally stored devices except the own device. +/// +/// Only devices that have been received after subscribing the corresponding +/// device lists on the server are stored locally. +/// Thus, only those are returned. +/// Call \c QXmppOmemoManager::subscribeToDeviceLists() for contacts without +/// presence subscription before. +/// +/// \param jids JIDs whose devices are being retrieved +/// +/// \return all devices of the passed JIDs +/// +QFuture> Manager::devices(const QList &jids) +{ + QFutureInterface> interface(QFutureInterfaceBase::Started); + + auto future = keys(jids); + await(future, this, [=](QHash> keys) mutable { + QVector devices; + + for (const auto &jid : jids) { + const auto &storedDevices = d->devices.value(jid); + const auto &storedKeys = keys.value(jid); + + for (const auto &storedDevice : storedDevices) { + const auto &keyId = storedDevice.keyId; + + QXmppOmemoDevice device; + device.setJid(jid); + device.setLabel(storedDevice.label); + + if (!keyId.isEmpty()) { + device.setKeyId(keyId); + device.setTrustLevel(storedKeys.value(keyId)); + } + + devices.append(device); + } + } + + reportFinishedResult(interface, devices); + }); + + return interface.future(); +} + +/// +/// Removes all devices of a contact and the subscription to the contact's +/// device list. +/// +/// This should be called after removing a contact. +/// The JID of the current user must not be passed. +/// Use \c QXmppOmemoManager::resetAll() in order to remove all devices of the +/// user. +/// +/// \param jid JID of the contact whose devices are being removed +/// +/// \return the result of the contact device removals +/// +QFuture Manager::removeContactDevices(const QString &jid) +{ + QFutureInterface interface(QFutureInterfaceBase::Started); + + Q_ASSERT_X(jid != d->ownBareJid(), "Removing contact device", "Own JID passed"); + + auto future = d->unsubscribeFromDeviceList(jid); + await(future, this, [=](QXmppPubSubManager::Result result) mutable { + if (std::holds_alternative(result)) { + warning("Contact '" % jid % "' could not be removed because the device list subscription could not be removed"); + reportFinishedResult(interface, result); + } else { + d->devices.remove(jid); + + auto future = d->omemoStorage->removeDevices(jid); + await(future, this, [=]() mutable { + auto future = d->trustManager->removeKeys(ns_omemo_2, jid); + await(future, this, [=]() mutable { + reportFinishedResult(interface, result); + emit devicesRemoved(jid); + }); + }); + } + }); + + return interface.future(); +} + +/// +/// Sets the trust levels keys must have in order to build sessions for their +/// devices. +/// +/// \param trustLevels trust levels of the keys used for building sessions +/// +void Manager::setAcceptedSessionBuildingTrustLevels(QXmpp::TrustLevels trustLevels) +{ + d->acceptedSessionBuildingTrustLevels = trustLevels; +} + +/// +/// Returns the trust levels keys must have in order to build sessions for their +/// devices. +/// +/// \return the trust levels of the keys used for building sessions +/// +TrustLevels Manager::acceptedSessionBuildingTrustLevels() +{ + return d->acceptedSessionBuildingTrustLevels; +} + +/// +/// Sets whether sessions are built when new devices are received from the +/// server. +/// +/// This can be used to not call \c QXmppOmemoManager::buildMissingSessions +/// manually. +/// But it should not be used before the initial setup and storing lots of +/// devices locally. +/// Otherwise, it could lead to a massive computation and network load when +/// there are many devices for whom sessions are built. +/// +/// \see QXmppOmemoManager::buildMissingSessions +/// +/// \param isNewDeviceAutoSessionBuildingEnabled whether sessions are built for +/// incoming devices +/// +void Manager::setNewDeviceAutoSessionBuildingEnabled(bool isNewDeviceAutoSessionBuildingEnabled) +{ + d->isNewDeviceAutoSessionBuildingEnabled = isNewDeviceAutoSessionBuildingEnabled; +} + +/// +/// Returns whether sessions are built when new devices are received from the +/// server. +/// +/// \see QXmppOmemoManager::setNewDeviceAutoSessionBuildingEnabled +/// +/// \return whether sessions are built for incoming devices +/// +bool Manager::isNewDeviceAutoSessionBuildingEnabled() +{ + return d->isNewDeviceAutoSessionBuildingEnabled; +} + +/// +/// Builds sessions manually with devices for whom no sessions are available. +/// +/// Usually, sessions are built during sending a first message to a device or +/// after a first message is received from a device. +/// This can be called in order to speed up the sending of a message. +/// If this method is called before sending the first message, all sessions can +/// be built and when the first message is sent, the message has only be +/// encrypted. +/// Especially chats with multiple devices, that can decrease the noticeable +/// time a user has to wait for sending a message. +/// Additionally, the keys are automatically retrieved from the server which is +/// helpful in order to get them when calling \c QXmppOmemoManager::devices(). +/// +/// The user must be logged in while calling this. +/// +/// \param jids JIDs of the device owners for whom the sessions are built +/// +QFuture Manager::buildMissingSessions(const QList &jids) +{ + QFutureInterface interface(QFutureInterfaceBase::Started); + + auto &devices = d->devices; + auto devicesCount = 0; + + for (const auto &jid : jids) { + // Do not exceed the maximum of manageable devices. + if (devicesCount > d->maximumDevicesPerStanza - devicesCount) { + warning("Sessions could not be built for all JIDs because their devices are " + "altogether more than the maximum of manageable devices " % + QString::number(d->maximumDevicesPerStanza) % + u" - Use QXmppOmemoManager::setMaximumDevicesPerStanza() to increase the maximum"); + break; + } else { + devicesCount += devices.value(jid).size(); + } + } + + if (devicesCount) { + auto processedDevicesCount = std::make_shared(0); + + for (const auto &jid : jids) { + auto &processedDevices = devices[jid]; + + for (auto itr = processedDevices.begin(); itr != processedDevices.end(); ++itr) { + const auto &deviceId = itr.key(); + auto &device = itr.value(); + + if (device.session.isEmpty()) { + auto future = d->buildSessionWithDeviceBundle(jid, deviceId, device); + await(future, this, [=](auto) mutable { + if (++(*processedDevicesCount) == devicesCount) { + interface.reportFinished(); + } + }); + } else if (++(*processedDevicesCount) == devicesCount) { + interface.reportFinished(); + } + } + } + } else { + interface.reportFinished(); + } + + return interface.future(); +} + +/// +/// Resets all OMEMO data for this device and the trust data used by OMEMO. +/// +/// ATTENTION: This should only be called when an account is removed locally or +/// if there are unrecoverable problems with the OMEMO setup of this device. +/// +/// The data on the server for other own devices is not removed. +/// Call \c resetAll() for that purpose. +/// +/// The user must be logged in while calling this. +/// +/// Call \c setUp() once this method is finished if you want to set up +/// everything again for this device. +/// Existing sessions are reset, which might lead to undecryptable incoming +/// stanzas until everything is set up again. +/// +QFuture Manager::resetOwnDevice() +{ + return d->resetOwnDevice(); +} + +/// +/// Resets all OMEMO data for all own devices and the trust data used by OMEMO. +/// +/// ATTENTION: This should only be called if there is a certain reason for it +/// since it deletes the data for this device and for other own devices from the +/// server. +/// +/// Call \c resetOwnDevice() if you only want to delete the OMEMO data for this +/// device. +/// +/// The user must be logged in while calling this. +/// +/// Call \c setUp() once this method is finished if you want to set up +/// everything again. +/// Existing sessions are reset, which might lead to undecryptable incoming +/// stanzas until everything is set up again. +/// +QFuture Manager::resetAll() +{ + return d->resetAll(); +} + +/// +/// \fn QXmppOmemoManager::setSecurityPolicy(QXmpp::TrustSecurityPolicy securityPolicy) +/// +/// Sets the security policy used by this E2EE extension. +/// +/// \param securityPolicy security policy being set +/// +QFuture Manager::setSecurityPolicy(QXmpp::TrustSecurityPolicy securityPolicy) +{ + return d->trustManager->setSecurityPolicy(ns_omemo_2, securityPolicy); +} + +/// +/// \fn QXmppOmemoManager::securityPolicy() +/// +/// Returns the security policy used by this E2EE extension. +/// +/// \return the used security policy +/// +QFuture Manager::securityPolicy() +{ + return d->trustManager->securityPolicy(ns_omemo_2); +} + +/// +/// \fn QXmppOmemoManager::setTrustLevel(const QMultiHash &keyIds, QXmpp::TrustLevel trustLevel) +/// +/// Sets the trust level of keys. +/// +/// If a key is not stored, it is added to the storage. +/// +/// \param keyIds key owners' bare JIDs mapped to the IDs of their keys +/// \param trustLevel trust level being set +/// +QFuture Manager::setTrustLevel(const QMultiHash &keyIds, QXmpp::TrustLevel trustLevel) +{ + return d->trustManager->setTrustLevel(ns_omemo_2, keyIds, trustLevel); +} + +/// +/// \fn QXmppOmemoManager::trustLevel(const QString &keyOwnerJid, const QByteArray &keyId) +/// +/// Returns the trust level of a key. +/// +/// If the key is not stored, the trust in that key is undecided. +/// +/// \param keyOwnerJid key owner's bare JID +/// \param keyId ID of the key +/// +/// \return the key's trust level +/// +QFuture Manager::trustLevel(const QString &keyOwnerJid, const QByteArray &keyId) +{ + return d->trustManager->trustLevel(ns_omemo_2, keyOwnerJid, keyId); +} + +/// \cond +QFuture Manager::encryptMessage(QXmppMessage &&message, const std::optional ¶ms) +{ + QVector recipientJids; + std::optional acceptedTrustLevels; + + if (params) { + recipientJids = params->encryptionJids(); + acceptedTrustLevels = params->acceptedTrustLevels(); + } + + if (recipientJids.isEmpty()) { + recipientJids.append(QXmppUtils::jidToBareJid(message.to())); + } + + if (!acceptedTrustLevels) { + acceptedTrustLevels = ACCEPTED_TRUST_LEVELS; + } + + return d->encryptMessageForRecipients(std::move(message), recipientJids, *acceptedTrustLevels); +} + +QFuture Manager::encryptIq(QXmppIq &&iq, const std::optional ¶ms) +{ + QFutureInterface interface(QFutureInterfaceBase::Started); + + if (!d->isStarted) { + QXmpp::SendError error; + error.text = QStringLiteral("OMEMO manager must be started before encrypting"); + error.type = QXmpp::SendError::EncryptionError; + reportFinishedResult(interface, { error }); + } else { + std::optional acceptedTrustLevels; + + if (params) { + acceptedTrustLevels = params->acceptedTrustLevels(); + } + + if (!acceptedTrustLevels) { + acceptedTrustLevels = ACCEPTED_TRUST_LEVELS; + } + + auto future = d->encryptStanza(iq, { QXmppUtils::jidToBareJid(iq.to()) }, *acceptedTrustLevels); + await(future, this, [=, iq = std::move(iq)](std::optional 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 { + QXmppOmemoIq omemoIq; + omemoIq.setId(iq.id()); + omemoIq.setType(iq.type()); + omemoIq.setLang(iq.lang()); + omemoIq.setFrom(iq.from()); + omemoIq.setTo(iq.to()); + omemoIq.setOmemoElement(*omemoElement); + + QByteArray serializedEncryptedIq; + QXmlStreamWriter writer(&serializedEncryptedIq); + omemoIq.toXml(&writer); + + reportFinishedResult(interface, { serializedEncryptedIq }); + } + }); + } + + return interface.future(); +} + +QFuture Manager::decryptIq(const QDomElement &element) +{ + if (!d->isStarted) { + // TODO: Add decryption queue to avoid this error + return makeReadyFuture(SendError { + QStringLiteral("OMEMO manager must be started before decrypting"), + SendError::EncryptionError }); + } + + if (QXmppOmemoIq::isOmemoIq(element)) { + // Tag name and iq type are already checked in QXmppClient. + return chain(d->decryptIq(element), this, [](auto result) -> IqDecryptResult { + if (result) { + return result->iq; + } + return SendError { + QStringLiteral("OMEMO message could not be decrypted"), + SendError::EncryptionError + }; + }); + } + + return makeReadyFuture(NotEncrypted()); +} + +QStringList Manager::discoveryFeatures() const +{ + return { + QString(ns_omemo_2_devices) % "+notify" + }; +} + +bool Manager::handleStanza(const QDomElement &stanza) +{ + if (stanza.tagName() != "iq" || !QXmppOmemoIq::isOmemoIq(stanza)) { + return false; + } + + // TODO: Queue incoming IQs until OMEMO is initialized + if (!d->isStarted) { + warning("Couldn't decrypt incoming IQ because the manager isn't initialized yet."); + return false; + } + + auto type = stanza.attribute("type"); + if (type != "get" && type != "set") { + // ignore incoming result and error IQs (they are handled via Client::sendIq()) + return false; + } + + await(d->decryptIq(stanza), this, [=](auto result) { + if (result) { + injectIq(result->iq, result->e2eeMetadata); + } else { + warning("Could not decrypt incoming OMEMO IQ."); + } + }); + return true; +} + +bool Manager::handleMessage(const QXmppMessage &message) +{ + if (d->isStarted && message.omemoElement()) { + auto future = d->decryptMessage(message); + await(future, this, [=](std::optional optionalDecryptedMessage) mutable { + if (optionalDecryptedMessage) { + injectMessage(std::move(*optionalDecryptedMessage)); + } + }); + + return true; + } + + return false; +} +/// \endcond + +/// +/// \fn QXmppOmemoManager::trustLevelsChanged(const QMultiHash &modifiedKeys) +/// +/// Emitted when the trust levels of keys changed. +/// +/// \param modifiedKeys key owners' bare JIDs mapped to their modified keys +/// + +/// +/// \fn QXmppOmemoManager::deviceAdded(const QString &jid, uint32_t deviceId) +/// +/// Emitted when a device is added. +/// +/// \param jid device owner's bare JID +/// \param deviceId ID of the device +/// + +/// +/// \fn QXmppOmemoManager::deviceChanged(const QString &jid, uint32_t deviceId) +/// +/// Emitted when a device changed. +/// +/// \param jid device owner's bare JID +/// \param deviceId ID of the device +/// + +/// +/// \fn QXmppOmemoManager::deviceRemoved(const QString &jid, uint32_t deviceId) +/// +/// Emitted when a device is removed. +/// +/// \param jid device owner's bare JID +/// \param deviceId ID of the device +/// + +/// +/// \fn QXmppOmemoManager::devicesRemoved(const QString &jid) +/// +/// Emitted when all devices of an owner are removed. +/// +/// \param jid device owner's bare JID +/// + +/// +/// \fn QXmppOmemoManager::allDevicesRemoved() +/// +/// Emitted when all devices are removed. +/// + +/// \cond +void Manager::setClient(QXmppClient *client) +{ + QXmppClientExtension::setClient(client); + client->setEncryptionExtension(this); + + d->trustManager = client->findExtension(); + if (!d->trustManager) { + qFatal("QXmppTrustManager is not available, it must be added to the client before adding QXmppOmemoManager"); + } + + d->pubSubManager = client->findExtension(); + if (!d->pubSubManager) { + qFatal("QXmppPubSubManager is not available, it must be added to the client before adding QXmppOmemoManager"); + } + + connect(d->trustManager, &QXmppTrustManager::trustLevelsChanged, this, [=](const QHash> &modifiedKeys) { + const auto &modifiedOmemoKeys = modifiedKeys.value(ns_omemo_2); + emit trustLevelsChanged(modifiedOmemoKeys); + + for (auto itr = modifiedOmemoKeys.cbegin(); itr != modifiedOmemoKeys.cend(); ++itr) { + const auto &keyOwnerJid = itr.key(); + const auto &keyId = itr.value(); + + // Emit 'deviceChanged()' only if there is a device with the key. + const auto &devices = d->devices.value(keyOwnerJid); + for (auto itr = devices.cbegin(); itr != devices.cend(); ++itr) { + if (itr->keyId == keyId) { + emit deviceChanged(keyOwnerJid, itr.key()); + return; + } + } + } + }); +} + +bool Manager::handlePubSubEvent(const QDomElement &element, const QString &pubSubService, const QString &nodeName) +{ + if (nodeName == ns_omemo_2_devices && QXmppPubSubEvent::isPubSubEvent(element)) { + QXmppPubSubEvent event; + event.parse(element); + + switch (event.eventType()) { + // Items are published or deleted. + case QXmppPubSubEventBase::Items: { + // If there are IDs of deleted items, check for an inconsistency. + // Otherwise, check for published items. + if (const auto retractIds = event.retractIds(); !retractIds.isEmpty()) { + // Specific items are deleted. + const auto &retractedItem = event.retractIds().constFirst(); + if (retractedItem == QXmppPubSubManager::standardItemIdToString(QXmppPubSubManager::Current)) { + d->handleIrregularDeviceListChanges(pubSubService); + } + } else { + const auto items = event.items(); + + // Only process items if the event notification contains one. + // That is necessary because PubSub allows publishing without + // items leading to notification-only events. + if (!items.isEmpty()) { + const auto &deviceListItem = items.constFirst(); + if (deviceListItem.id() == QXmppPubSubManager::standardItemIdToString(QXmppPubSubManager::Current)) { + d->updateDevices(pubSubService, event.items().constFirst()); + } else { + d->handleIrregularDeviceListChanges(pubSubService); + } + } + } + + break; + } + + // All items are deleted. + case QXmppPubSubEventBase::Purge: + // The whole node is deleted. + case QXmppPubSubEventBase::Delete: + d->handleIrregularDeviceListChanges(pubSubService); + break; + case QXmppPubSubEventBase::Configuration: + case QXmppPubSubEventBase::Subscription: + break; + } + + return true; + } + + return false; +} +/// \endcond diff --git a/src/omemo/QXmppOmemoManager.h b/src/omemo/QXmppOmemoManager.h new file mode 100644 index 00000000..cc957e73 --- /dev/null +++ b/src/omemo/QXmppOmemoManager.h @@ -0,0 +1,158 @@ +// SPDX-FileCopyrightText: 2022 Melvin Keskin +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#ifndef QXMPPOMEMOMANAGER_H +#define QXMPPOMEMOMANAGER_H + +#include "QXmppClientExtension.h" +#include "QXmppE2eeExtension.h" +#include "QXmppMessageHandler.h" +#include "QXmppPubSubEventHandler.h" +#include "QXmppPubSubManager.h" +#include "QXmppTrustSecurityPolicy.h" +#include "qxmppomemo_export.h" + +class QXmppOmemoDevicePrivate; +class QXmppOmemoManagerPrivate; +class QXmppOmemoOwnDevicePrivate; +class QXmppOmemoStorage; + +class QXMPPOMEMO_EXPORT QXmppOmemoOwnDevice +{ +public: + QXmppOmemoOwnDevice(); + QXmppOmemoOwnDevice(const QXmppOmemoOwnDevice &other); + QXmppOmemoOwnDevice(QXmppOmemoOwnDevice &&) noexcept; + ~QXmppOmemoOwnDevice(); + + QXmppOmemoOwnDevice &operator=(const QXmppOmemoOwnDevice &); + QXmppOmemoOwnDevice &operator=(QXmppOmemoOwnDevice &&); + + QString label() const; + void setLabel(const QString &label); + + QByteArray keyId() const; + void setKeyId(const QByteArray &keyId); + +private: + QSharedDataPointer d; +}; + +class QXMPPOMEMO_EXPORT QXmppOmemoDevice +{ +public: + QXmppOmemoDevice(); + QXmppOmemoDevice(const QXmppOmemoDevice &other); + QXmppOmemoDevice(QXmppOmemoDevice &&) noexcept; + ~QXmppOmemoDevice(); + + QXmppOmemoDevice &operator=(const QXmppOmemoDevice &); + QXmppOmemoDevice &operator=(QXmppOmemoDevice &&); + + QString jid() const; + void setJid(const QString &jid); + + QString label() const; + void setLabel(const QString &label); + + QByteArray keyId() const; + void setKeyId(const QByteArray &keyId); + + QXmpp::TrustLevel trustLevel() const; + void setTrustLevel(QXmpp::TrustLevel trustLevel); + +private: + QSharedDataPointer d; +}; + +class QXMPPOMEMO_EXPORT QXmppOmemoManager : public QXmppClientExtension, public QXmppE2eeExtension, public QXmppPubSubEventHandler, public QXmppMessageHandler +{ + Q_OBJECT + +public: + using Result = std::variant; + + struct DevicesResult + { + QString jid; + Result result; + }; + + explicit QXmppOmemoManager(QXmppOmemoStorage *omemoStorage); + ~QXmppOmemoManager() override; + + QFuture load(); + QFuture setUp(); + + QFuture ownKey(); + QFuture>> keys(QXmpp::TrustLevels trustLevels = {}); + QFuture>> keys(const QList &jids, QXmpp::TrustLevels trustLevels = {}); + + QFuture changeDeviceLabel(const QString &deviceLabel = {}); + + int maximumDevicesPerJid() const; + void setMaximumDevicesPerJid(int maximum); + + int maximumDevicesPerStanza() const; + void setMaximumDevicesPerStanza(int maximum); + + QFuture requestDeviceLists(const QList &jids); + QFuture subscribeToDeviceLists(const QList &jids); + QFuture unsubscribeFromDeviceLists(); + + QXmppOmemoOwnDevice ownDevice(); + QFuture> devices(); + QFuture> devices(const QList &jids); + QFuture removeContactDevices(const QString &jid); + + void setAcceptedSessionBuildingTrustLevels(QXmpp::TrustLevels trustLevels); + QXmpp::TrustLevels acceptedSessionBuildingTrustLevels(); + + void setNewDeviceAutoSessionBuildingEnabled(bool isNewDeviceAutoSessionBuildingEnabled); + bool isNewDeviceAutoSessionBuildingEnabled(); + + QFuture buildMissingSessions(const QList &jids); + + QFuture resetOwnDevice(); + QFuture resetAll(); + + QFuture setSecurityPolicy(QXmpp::TrustSecurityPolicy securityPolicy); + QFuture securityPolicy(); + + QFuture setTrustLevel(const QMultiHash &keyIds, QXmpp::TrustLevel trustLevel); + QFuture trustLevel(const QString &keyOwnerJid, const QByteArray &keyId); + + /// \cond + QFuture encryptMessage(QXmppMessage &&message, const std::optional ¶ms) override; + + QFuture encryptIq(QXmppIq &&iq, const std::optional ¶ms) override; + QFuture decryptIq(const QDomElement &element) override; + + QStringList discoveryFeatures() const override; + bool handleStanza(const QDomElement &stanza) override; + bool handleMessage(const QXmppMessage &message) override; + /// \endcond + + Q_SIGNAL void trustLevelsChanged(const QMultiHash &modifiedKeys); + + Q_SIGNAL void deviceAdded(const QString &jid, uint32_t deviceId); + Q_SIGNAL void deviceChanged(const QString &jid, uint32_t deviceId); + Q_SIGNAL void deviceRemoved(const QString &jid, uint32_t deviceId); + Q_SIGNAL void devicesRemoved(const QString &jid); + Q_SIGNAL void allDevicesRemoved(); + +protected: + /// \cond + void setClient(QXmppClient *client) override; + bool handlePubSubEvent(const QDomElement &element, const QString &pubSubService, const QString &nodeName) override; + /// \endcond + +private: + std::unique_ptr d; + + friend class QXmppOmemoManagerPrivate; + friend class tst_QXmppOmemoManager; +}; + +#endif // QXMPPOMEMOMANAGER_H diff --git a/src/omemo/QXmppOmemoManager_p.cpp b/src/omemo/QXmppOmemoManager_p.cpp new file mode 100644 index 00000000..2f8ca00d --- /dev/null +++ b/src/omemo/QXmppOmemoManager_p.cpp @@ -0,0 +1,3714 @@ +// SPDX-FileCopyrightText: 2022 Melvin Keskin +// SPDX-FileCopyrightText: 2022 Linus Jahn +// +// 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 + +#include "OmemoCryptoProvider.h" + +#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) +#include +#endif +#include + +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(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 +auto mapToSuccess(std::variant 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(user_data); + auto *d = manager->d.get(); + d->mutex.lock(); + }; + + const auto unlock = [](void *user_data) { + const auto *manager = reinterpret_cast(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(user_data); + const auto *d = manager->d.get(); + + const auto &privateIdentityKey = d->ownDevice.privateIdentityKey; + if (!(*private_data = signal_buffer_create(reinterpret_cast(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(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(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(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(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(user_data); + auto *d = manager->d.get(); + + QXmppOmemoStorage::SignedPreKeyPair signedPreKeyPair; + signedPreKeyPair.creationDate = QDateTime::currentDateTimeUtc(); + signedPreKeyPair.data = QByteArray(reinterpret_cast(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(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(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(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(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(user_data); + auto *d = manager->d.get(); + const auto preKey = QByteArray(reinterpret_cast(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(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(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(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(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(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(user_data); + auto *d = manager->d.get(); + const auto session = QByteArray(reinterpret_cast(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(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(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(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 ManagerPrivate::setUpDeviceId() +{ + QFutureInterface interface(QFutureInterfaceBase::Started); + + auto future = pubSubManager->requestPepItemIds(ns_omemo_2_bundles); + await(future, q, [=](QXmppPubSubManager::ItemIdsResult result) mutable { + if (auto error = std::get_if(&result)) { + warning("Existing / Published device IDs could not be retrieved"); + reportFinishedResult(interface, false); + } else { + const auto &deviceIds = std::get>(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 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 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(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 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 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 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 ManagerPrivate::encryptMessageForRecipients(QXmppMessage &&message, QVector recipientJids, TrustLevels acceptedTrustLevels) +{ + QFutureInterface 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 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 +QFuture> ManagerPrivate::encryptStanza(const T &stanza, const QVector &recipientJids, TrustLevels acceptedTrustLevels) +{ + Q_ASSERT_X(!recipientJids.isEmpty(), "Creating OMEMO envelope", "OMEMO element could not be created because no recipient JIDs are passed"); + + QFutureInterface> 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(); + auto processedDevicesCount = std::make_shared(0); + auto successfullyProcessedDevicesCount = std::make_shared(0); + auto skippedDevicesCount = std::make_shared(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 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 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> ManagerPrivate::encryptStanza(const QXmppIq &, const QVector &, TrustLevels); +template QFuture> ManagerPrivate::encryptStanza(const QXmppMessage &, const QVector &, TrustLevels); + +// +// Encrypts a payload symmetrically. +// +// \param payload payload being symmetrically encrypted +// +// \return the data used for encryption and the result +// +std::optional 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 +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) { + 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 encryptedOmemoEnvelopeData; + if (session_cipher_encrypt(sessionCipher.get(), reinterpret_cast(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(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> ManagerPrivate::decryptMessage(QXmppMessage stanza) +{ + QFutureInterface> 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 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> ManagerPrivate::decryptIq(const QDomElement &iqElement) +{ + using Result = std::optional; + + 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(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(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 +QFuture> ManagerPrivate::decryptStanza(T stanza, const QString &senderJid, uint32_t senderDeviceId, const QXmppOmemoEnvelope &omemoEnvelope, const QByteArray &omemoPayload, bool isMessageStanza) +{ + QFutureInterface> 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(stanza); message.type() == QXmppMessage::GroupChat && (sceEnvelopeReader.to() != recipientJid)) { + warning("Recipient of group chat message does not match SCE affix element ''"); + isSceAffixElementValid = false; + } + } else { + if (sceEnvelopeReader.to() != recipientJid) { + warning("Recipient of IQ does not match SCE affix element ''"); + 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 ManagerPrivate::extractSceEnvelope(const QString &senderJid, uint32_t senderDeviceId, const QXmppOmemoEnvelope &omemoEnvelope, const QByteArray &omemoPayload, bool isMessageStanza) +{ + QFutureInterface 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 ManagerPrivate::extractPayloadDecryptionData(const QString &senderJid, uint32_t senderDeviceId, const QXmppOmemoEnvelope &omemoEnvelope, bool isMessageStanza) +{ + QFutureInterface 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 omemoEnvelopeData; + const auto serializedOmemoEnvelopeData = omemoEnvelope.data(); + + if (pre_key_signal_message_deserialize_omemo(omemoEnvelopeData.ptrRef(), + reinterpret_cast(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 omemoEnvelopeData; + const auto serializedOmemoEnvelopeData = omemoEnvelope.data(); + + if (signal_message_deserialize_omemo(omemoEnvelopeData.ptrRef(), reinterpret_cast(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 ManagerPrivate::publishOmemoData() +{ + QFutureInterface interface(QFutureInterfaceBase::Started); + + auto future = pubSubManager->requestPepFeatures(); + await(future, q, [=](QXmppPubSubManager::FeaturesResult result) mutable { + if (const auto error = std::get_if(&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>(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(&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>(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 +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 +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 +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 +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 +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 +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 +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 +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> ManagerPrivate::requestDeviceBundle(const QString &deviceOwnerJid, uint32_t deviceId) const +{ + QFutureInterface> interface(QFutureInterfaceBase::Started); + + auto future = pubSubManager->requestItem(deviceOwnerJid, ns_omemo_2_bundles, QString::number(deviceId)); + await(future, q, [=](QXmppPubSubManager::ItemResult result) mutable { + if (const auto error = std::get_if(&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(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 +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 +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 +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 +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 +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 +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 +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 +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 +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 +void ManagerPrivate::updateOwnDevicesLocally(bool isDeviceListNodeExistent, Function continuation) +{ + if (isDeviceListNodeExistent && otherOwnDevices().isEmpty()) { + auto future = pubSubManager->requestPepItem(ns_omemo_2_devices, QXmppPubSubManager::Current); + await(future, q, [=](QXmppPubSubManager::ItemResult result) mutable { + if (const auto error = std::get_if(&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(result); + QList 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(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 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 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(&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(&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>(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 +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 +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 +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 +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 +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 +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(&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 +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 +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 +void QXmppOmemoManagerPrivate::runPubSubQueryWithContinuation(QFuture future, const QString &errorMessage, Function continuation) +{ + await(future, q, [this, errorMessage, continuation = std::move(continuation)](auto result) mutable { + if (auto error = std::get_if(&result)) { + warning(errorMessage % u": " % errorToString(*error)); + continuation(false); + } else { + continuation(true); + } + }); +} + +// See QXmppOmemoManager for documentation +QFuture ManagerPrivate::changeDeviceLabel(const QString &deviceLabel) +{ + QFutureInterface 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> ManagerPrivate::requestDeviceList(const QString &jid) +{ + auto future = pubSubManager->requestItem(jid, ns_omemo_2_devices, QXmppPubSubManager::Current); + await(future, q, [this, jid](QXmppPubSubManager::ItemResult result) mutable { + if (const auto error = std::get_if(&result)) { + warning("Device list for JID '" % jid % "' could not be retrieved: " % errorToString(*error)); + } else { + const auto &item = std::get(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 ManagerPrivate::subscribeToDeviceList(const QString &jid) +{ + QFutureInterface 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(&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 ManagerPrivate::unsubscribeFromDeviceLists(const QList &jids) +{ + QFutureInterface interface = (QFutureInterfaceBase::Started); + + const auto jidsCount = jids.size(); + auto processedJidsCount = std::make_shared(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 ManagerPrivate::unsubscribeFromDeviceList(const QString &jid) +{ + QFutureInterface 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(&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 ManagerPrivate::resetOwnDevice() +{ + QFutureInterface 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 ManagerPrivate::resetAll() +{ + QFutureInterface 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 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 ManagerPrivate::buildSessionWithDeviceBundle(const QString &jid, uint32_t deviceId, QXmppOmemoStorage::Device &device) +{ + QFutureInterface interface(QFutureInterfaceBase::Started); + + auto future = requestDeviceBundle(jid, deviceId); + await(future, q, [=, &device](std::optional 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(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 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 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 publicIdentityKey; + RefCountedPtr signedPublicPreKey; + RefCountedPtr signedPublicPreKeySignature; + int signedPublicPreKeySignatureSize; + RefCountedPtr 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 ManagerPrivate::sendEmptyMessage(const QString &recipientJid, uint32_t recipientDeviceId, bool isKeyExchange) const +{ + QFutureInterface 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 ManagerPrivate::storeOwnKey() const +{ + QFutureInterface 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 ManagerPrivate::storeKeyDependingOnSecurityPolicy(const QString &keyOwnerJid, const QByteArray &key) +{ + QFutureInterface interface(QFutureInterfaceBase::Started); + + auto awaitStoreKey = [=](const QFuture &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 ManagerPrivate::storeKey(const QString &keyOwnerJid, const QByteArray &key, TrustLevel trustLevel) const +{ + QFutureInterface 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 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 diff --git a/src/omemo/QXmppOmemoManager_p.h b/src/omemo/QXmppOmemoManager_p.h new file mode 100644 index 00000000..7f697ed2 --- /dev/null +++ b/src/omemo/QXmppOmemoManager_p.h @@ -0,0 +1,342 @@ +// SPDX-FileCopyrightText: 2022 Melvin Keskin +// SPDX-FileCopyrightText: 2022 Linus Jahn +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#ifndef QXMPPOMEMOMANAGER_P_H +#define QXMPPOMEMOMANAGER_P_H + +#include "QXmppE2eeMetadata.h" +#include "QXmppOmemoDeviceBundle_p.h" +#include "QXmppOmemoManager.h" +#include "QXmppOmemoStorage.h" + +#include "OmemoLibWrappers.h" +#include +#include +#include + +class QXmppTrustManager; +class QXmppOmemoManager; +class QXmppPubSubManager; +class QXmppPubSubNodeConfig; +class QXmppPubSubPublishOptions; +class QXmppOmemoIq; +class QXmppOmemoEnvelope; +class QXmppOmemoElement; +class QXmppOmemoDeviceListItem; +class QXmppOmemoDeviceBundleItem; + +using namespace QXmpp; +using namespace std::chrono_literals; + +namespace QXmpp::Omemo::Private { + +// default possible trust levels a key must have to be used for encryption +// The class documentation must be adapted if the trust levels are modified. +constexpr auto ACCEPTED_TRUST_LEVELS = TrustLevel::AutomaticallyTrusted | TrustLevel::ManuallyTrusted | TrustLevel::Authenticated; + +// count of unresponded stanzas sent to a device until QXmpp stops encrypting for it +constexpr int UNRESPONDED_STANZAS_UNTIL_ENCRYPTION_IS_STOPPED = 106; + +// count of unresponded stanzas received from a device until a heartbeat message is sent to it +constexpr int UNRESPONDED_STANZAS_UNTIL_HEARTBEAT_MESSAGE_IS_SENT = 53; + +// size of empty OMEMO message's decryption data +constexpr int EMPTY_MESSAGE_DECRYPTION_DATA_SIZE = 32; + +// workaround for PubSub nodes that are not configurable to store 'max' as the value for +// 'pubsub#max_items' +constexpr uint64_t PUBSUB_NODE_MAX_ITEMS_1 = 1000; +constexpr uint64_t PUBSUB_NODE_MAX_ITEMS_2 = 100; +constexpr uint64_t PUBSUB_NODE_MAX_ITEMS_3 = 10; + +constexpr uint32_t PRE_KEY_ID_MIN = 1; +constexpr uint32_t SIGNED_PRE_KEY_ID_MIN = 1; +constexpr uint32_t PRE_KEY_ID_MAX = std::numeric_limits::max(); +constexpr uint32_t SIGNED_PRE_KEY_ID_MAX = std::numeric_limits::max(); +constexpr uint32_t PRE_KEY_INITIAL_CREATION_COUNT = 100; + +// maximum count of devices stored per JID +constexpr int DEVICES_PER_JID_MAX = 200; + +// maximum count of devices for whom a stanza is encrypted +constexpr int DEVICES_PER_STANZA_MAX = 1000; + +// interval to remove old signed pre keys and create new ones +constexpr auto SIGNED_PRE_KEY_RENEWAL_INTERVAL = 24h * 7 * 4; + +// interval to check for old signed pre keys +constexpr auto SIGNED_PRE_KEY_RENEWAL_CHECK_INTERVAL = 24h; + +// interval to remove devices locally after removal from their servers +constexpr auto DEVICE_REMOVAL_INTERVAL = 24h * 7 * 12; + +// interval to check for devices removed from their servers +constexpr auto DEVICE_REMOVAL_CHECK_INTERVAL = 24h; + +constexpr auto PAYLOAD_CIPHER_TYPE = "aes256"; +constexpr QCA::Cipher::Mode PAYLOAD_CIPHER_MODE = QCA::Cipher::CBC; +constexpr QCA::Cipher::Padding PAYLOAD_CIPHER_PADDING = QCA::Cipher::PKCS7; + +constexpr auto HKDF_INFO = "OMEMO Payload"; +constexpr int HKDF_KEY_SIZE = 32; +constexpr int HKDF_SALT_SIZE = 32; +constexpr int HKDF_OUTPUT_SIZE = 60; + +extern const QString PAYLOAD_MESSAGE_AUTHENTICATION_CODE_TYPE; +constexpr uint32_t PAYLOAD_MESSAGE_AUTHENTICATION_CODE_SIZE = 16; + +constexpr int PAYLOAD_KEY_SIZE = 32; +constexpr uint32_t PAYLOAD_INITIALIZATION_VECTOR_SIZE = 16; +constexpr uint32_t PAYLOAD_AUTHENTICATION_KEY_SIZE = 16; + +// boundaries for the count of characters in SCE's element +constexpr uint32_t SCE_RPAD_SIZE_MIN = 0; +constexpr uint32_t SCE_RPAD_SIZE_MAX = 200; + +struct PayloadEncryptionResult +{ + QCA::SecureArray decryptionData; + QByteArray encryptedPayload; +}; + +struct DecryptionResult +{ + QDomElement sceContent; + QXmppE2eeMetadata e2eeMetadata; +}; + +struct IqDecryptionResult +{ + QDomElement iq; + QXmppE2eeMetadata e2eeMetadata; +}; + +QByteArray createKeyId(const QByteArray &key); + +} // namespace QXmpp::Omemo::Private + +using namespace QXmpp::Omemo::Private; + +class QXmppOmemoManagerPrivate +{ +public: + using Result = std::variant; + + QXmppOmemoManager *q; + + bool isStarted = false; + bool isNewDeviceAutoSessionBuildingEnabled = false; + + QXmppOmemoStorage *omemoStorage; + QXmppTrustManager *trustManager = nullptr; + QXmppPubSubManager *pubSubManager = nullptr; + + QCA::Initializer cryptoLibInitializer; + QTimer signedPreKeyPairsRenewalTimer; + QTimer deviceRemovalTimer; + + TrustLevels acceptedSessionBuildingTrustLevels = ACCEPTED_TRUST_LEVELS; + + QXmppOmemoStorage::OwnDevice ownDevice; + QHash preKeyPairs; + QHash signedPreKeyPairs; + QXmppOmemoDeviceBundle deviceBundle; + + int maximumDevicesPerJid = DEVICES_PER_JID_MAX; + int maximumDevicesPerStanza = DEVICES_PER_STANZA_MAX; + + // recipient JID mapped to device ID mapped to device + QHash> devices; + + QList jidsOfManuallySubscribedDevices; + + OmemoContextPtr globalContext; + StoreContextPtr storeContext; + QRecursiveMutex mutex; + signal_crypto_provider cryptoProvider; + + signal_protocol_identity_key_store identityKeyStore; + signal_protocol_pre_key_store preKeyStore; + signal_protocol_signed_pre_key_store signedPreKeyStore; + signal_protocol_session_store sessionStore; + + QXmppOmemoManagerPrivate(QXmppOmemoManager *parent, QXmppOmemoStorage *omemoStorage); + + void init(); + bool initGlobalContext(); + bool initLocking(); + bool initCryptoProvider(); + void initStores(); + + signal_protocol_identity_key_store createIdentityKeyStore() const; + signal_protocol_signed_pre_key_store createSignedPreKeyStore() const; + signal_protocol_pre_key_store createPreKeyStore() const; + signal_protocol_session_store createSessionStore() const; + + QFuture setUpDeviceId(); + bool setUpIdentityKeyPair(ratchet_identity_key_pair **identityKeyPair); + void schedulePeriodicTasks(); + void renewSignedPreKeyPairs(); + bool updateSignedPreKeyPair(ratchet_identity_key_pair *identityKeyPair); + bool renewPreKeyPairs(uint32_t keyPairBeingRenewed); + bool updatePreKeyPairs(uint32_t count = 1); + void removeDevicesRemovedFromServer(); + bool generateIdentityKeyPair(ratchet_identity_key_pair **identityKeyPair) const; + + QFuture encryptMessageForRecipients(QXmppMessage &&message, + QVector recipientJids, + TrustLevels acceptedTrustLevels); + template + QFuture> encryptStanza(const T &stanza, const QVector &recipientJids, TrustLevels acceptedTrustLevels); + std::optional encryptPayload(const QByteArray &payload) const; + template + QByteArray createSceEnvelope(const T &stanza); + QByteArray createOmemoEnvelopeData(const signal_protocol_address &address, const QCA::SecureArray &payloadDecryptionData) const; + + QFuture> decryptMessage(QXmppMessage stanza); + QFuture> decryptIq(const QDomElement &iqElement); + template + QFuture> decryptStanza(T stanza, + const QString &senderJid, + uint32_t senderDeviceId, + const QXmppOmemoEnvelope &omemoEnvelope, + const QByteArray &omemoPayload, + bool isMessageStanza = true); + QFuture extractSceEnvelope(const QString &senderJid, + uint32_t senderDeviceId, + const QXmppOmemoEnvelope &omemoEnvelope, + const QByteArray &omemoPayload, + bool isMessageStanza); + QFuture extractPayloadDecryptionData(const QString &senderJid, + uint32_t senderDeviceId, + const QXmppOmemoEnvelope &omemoEnvelope, + bool isMessageStanza = true); + QByteArray decryptPayload(const QCA::SecureArray &payloadDecryptionData, const QByteArray &payload) const; + + QFuture publishOmemoData(); + + template + void publishDeviceBundle(bool isDeviceBundlesNodeExistent, + bool arePublishOptionsSupported, + bool isAutomaticCreationSupported, + bool isCreationAndConfigurationSupported, + bool isCreationSupported, + bool isConfigurationSupported, + bool isConfigNodeMaxSupported, + Function continuation); + template + void publishDeviceBundleWithoutOptions(bool isDeviceBundlesNodeExistent, + bool isCreationAndConfigurationSupported, + bool isCreationSupported, + bool isConfigurationSupported, + bool isConfigNodeMaxSupported, + Function continuation); + template + void configureNodeAndPublishDeviceBundle(bool isConfigNodeMaxSupported, Function continuation); + template + void createAndConfigureDeviceBundlesNode(bool isConfigNodeMaxSupported, Function continuation); + template + void createDeviceBundlesNode(Function continuation); + template + void configureDeviceBundlesNode(bool isConfigNodeMaxSupported, Function continuation); + template + void publishDeviceBundleItem(Function continuation); + template + void publishDeviceBundleItemWithOptions(Function continuation); + QXmppOmemoDeviceBundleItem deviceBundleItem() const; + QFuture> requestDeviceBundle(const QString &deviceOwnerJid, uint32_t deviceId) const; + template + void deleteDeviceBundle(Function continuation); + + template + void publishDeviceElement(bool isDeviceListNodeExistent, + bool arePublishOptionsSupported, + bool isAutomaticCreationSupported, + bool isCreationAndConfigurationSupported, + bool isCreationSupported, + bool isConfigurationSupported, + Function continuation); + template + void publishDeviceElementWithoutOptions(bool isDeviceListNodeExistent, + bool isCreationAndConfigurationSupported, + bool isCreationSupported, + bool isConfigurationSupported, + Function continuation); + template + void configureNodeAndPublishDeviceElement(Function continuation); + template + void createAndConfigureDeviceListNode(Function continuation); + template + void createDeviceListNode(Function continuation); + template + void configureDeviceListNode(Function continuation); + template + void publishDeviceListItem(bool addOwnDevice, Function continuation); + template + void publishDeviceListItemWithOptions(Function continuation); + QXmppOmemoDeviceListItem deviceListItem(bool addOwnDevice = true); + template + void updateOwnDevicesLocally(bool isDeviceListNodeExistent, Function continuation); + void updateDevices(const QString &deviceOwnerJid, const QXmppOmemoDeviceListItem &deviceListItem); + void handleIrregularDeviceListChanges(const QString &deviceOwnerJid); + template + void deleteDeviceElement(Function continuation); + + template + void createNode(const QString &node, Function continuation); + template + void createNode(const QString &node, const QXmppPubSubNodeConfig &config, Function continuation); + template + void configureNode(const QString &node, const QXmppPubSubNodeConfig &config, Function continuation); + template + void retractItem(const QString &node, uint32_t itemId, Function continuation); + template + void deleteNode(const QString &node, Function continuation); + + template + void publishItem(const QString &node, const T &item, Function continuation); + template + void publishItem(const QString &node, const T &item, const QXmppPubSubPublishOptions &publishOptions, Function continuation); + + template + void runPubSubQueryWithContinuation(QFuture future, const QString &errorMessage, Function continuation); + + QFuture changeDeviceLabel(const QString &deviceLabel); + + QFuture> requestDeviceList(const QString &jid); + void subscribeToNewDeviceLists(const QString &jid, uint32_t deviceId); + QFuture subscribeToDeviceList(const QString &jid); + QFuture unsubscribeFromDeviceLists(const QList &jids); + QFuture unsubscribeFromDeviceList(const QString &jid); + + QFuture resetOwnDevice(); + QFuture resetAll(); + + QFuture buildSessionForNewDevice(const QString &jid, uint32_t deviceId, QXmppOmemoStorage::Device &device); + QFuture buildSessionWithDeviceBundle(const QString &jid, uint32_t deviceId, QXmppOmemoStorage::Device &device); + bool buildSession(signal_protocol_address address, const QXmppOmemoDeviceBundle &deviceBundle); + bool createSessionBundle(session_pre_key_bundle **sessionBundle, + const QByteArray &serializedPublicIdentityKey, + const QByteArray &serializedSignedPublicPreKey, + uint32_t signedPublicPreKeyId, + const QByteArray &serializedSignedPublicPreKeySignature, + const QByteArray &serializedPublicPreKey, + uint32_t publicPreKeyId); + bool deserializePublicIdentityKey(ec_public_key **publicIdentityKey, const QByteArray &serializedPublicIdentityKey) const; + bool deserializeSignedPublicPreKey(ec_public_key **signedPublicPreKey, const QByteArray &serializedSignedPublicPreKey) const; + bool deserializePublicPreKey(ec_public_key **publicPreKey, const QByteArray &serializedPublicPreKey) const; + + QFuture sendEmptyMessage(const QString &recipientJid, uint32_t recipientDeviceId, bool isKeyExchange = false) const; + QFuture storeOwnKey() const; + QFuture storeKeyDependingOnSecurityPolicy(const QString &keyOwnerJid, const QByteArray &key); + QFuture storeKey(const QString &keyOwnerJid, const QByteArray &key, TrustLevel trustLevel = TrustLevel::AutomaticallyDistrusted) const; + QString ownBareJid() const; + QString ownFullJid() const; + QHash otherOwnDevices(); + + void warning(const QString &msg) const; +}; + +#endif // QXMPPOMEMOMANAGER_P_H diff --git a/src/omemo/QXmppOmemoMemoryStorage.cpp b/src/omemo/QXmppOmemoMemoryStorage.cpp new file mode 100644 index 00000000..6ff81002 --- /dev/null +++ b/src/omemo/QXmppOmemoMemoryStorage.cpp @@ -0,0 +1,119 @@ +// SPDX-FileCopyrightText: 2022 Melvin Keskin +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "QXmppOmemoMemoryStorage.h" + +#include "QXmppFutureUtils_p.h" + +using namespace QXmpp::Private; + +/// +/// \class QXmppOmemoMemoryStorage +/// +/// \brief The QXmppOmemoMemoryStorage class stores data used by +/// \xep{0384, OMEMO Encryption} in the memory. +/// +/// \warning THIS API IS NOT FINALIZED YET! +/// +/// \since QXmpp 1.5 +/// + +class QXmppOmemoMemoryStoragePrivate +{ +public: + bool isSetUp = false; + + std::optional ownDevice; + + // IDs of pre key pairs mapped to pre key pairs + QHash preKeyPairs; + + // IDs of signed pre key pairs mapped to signed pre key pairs + QHash signedPreKeyPairs; + + // recipient JID mapped to device ID mapped to device + QHash> devices; +}; + +/// +/// Constructs an OMEMO memory storage. +/// +QXmppOmemoMemoryStorage::QXmppOmemoMemoryStorage() + : d(new QXmppOmemoMemoryStoragePrivate) +{ +} + +QXmppOmemoMemoryStorage::~QXmppOmemoMemoryStorage() = default; + +/// \cond +QFuture QXmppOmemoMemoryStorage::allData() +{ + return makeReadyFuture(std::move(OmemoData { d->ownDevice, + d->signedPreKeyPairs, + d->preKeyPairs, + d->devices })); +} + +QFuture QXmppOmemoMemoryStorage::setOwnDevice(const std::optional &device) +{ + d->ownDevice = device; + return makeReadyFuture(); +} + +QFuture QXmppOmemoMemoryStorage::addSignedPreKeyPair(const uint32_t keyId, const SignedPreKeyPair &keyPair) +{ + d->signedPreKeyPairs.insert(keyId, keyPair); + return makeReadyFuture(); +} + +QFuture QXmppOmemoMemoryStorage::removeSignedPreKeyPair(const uint32_t keyId) +{ + d->signedPreKeyPairs.remove(keyId); + return makeReadyFuture(); +} + +QFuture QXmppOmemoMemoryStorage::addPreKeyPairs(const QHash &keyPairs) +{ + d->preKeyPairs.insert(keyPairs); + return makeReadyFuture(); +} + +QFuture QXmppOmemoMemoryStorage::removePreKeyPair(const uint32_t keyId) +{ + d->preKeyPairs.remove(keyId); + return makeReadyFuture(); +} + +QFuture QXmppOmemoMemoryStorage::addDevice(const QString &jid, const uint32_t deviceId, const QXmppOmemoStorage::Device &device) +{ + d->devices[jid].insert(deviceId, device); + return makeReadyFuture(); +} + +QFuture QXmppOmemoMemoryStorage::removeDevice(const QString &jid, const uint32_t deviceId) +{ + auto &devices = d->devices[jid]; + devices.remove(deviceId); + + // Remove the container for the passed JID if the container stores no + // devices anymore. + if (devices.isEmpty()) { + d->devices.remove(jid); + } + + return makeReadyFuture(); +} + +QFuture QXmppOmemoMemoryStorage::removeDevices(const QString &jid) +{ + d->devices.remove(jid); + return makeReadyFuture(); +} + +QFuture QXmppOmemoMemoryStorage::resetAll() +{ + d.reset(new QXmppOmemoMemoryStoragePrivate()); + return makeReadyFuture(); +} +/// \endcond diff --git a/src/omemo/QXmppOmemoMemoryStorage.h b/src/omemo/QXmppOmemoMemoryStorage.h new file mode 100644 index 00000000..9e2c89c6 --- /dev/null +++ b/src/omemo/QXmppOmemoMemoryStorage.h @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2022 Melvin Keskin +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#ifndef QXMPPOMEMOMEMORYSTORAGE_H +#define QXMPPOMEMOMEMORYSTORAGE_H + +#include "QXmppOmemoStorage.h" +#include "qxmppomemo_export.h" + +#include + +class QXmppOmemoMemoryStoragePrivate; + +class QXMPPOMEMO_EXPORT QXmppOmemoMemoryStorage : public QXmppOmemoStorage +{ +public: + QXmppOmemoMemoryStorage(); + ~QXmppOmemoMemoryStorage() override; + + /// \cond + QFuture allData() override; + + QFuture setOwnDevice(const std::optional &device) override; + + QFuture addSignedPreKeyPair(uint32_t keyId, const SignedPreKeyPair &keyPair) override; + QFuture removeSignedPreKeyPair(uint32_t keyId) override; + + QFuture addPreKeyPairs(const QHash &keyPairs) override; + QFuture removePreKeyPair(uint32_t keyId) override; + + QFuture addDevice(const QString &jid, uint32_t deviceId, const Device &device) override; + QFuture removeDevice(const QString &jid, uint32_t deviceId) override; + QFuture removeDevices(const QString &jid) override; + + QFuture resetAll() override; + /// \endcond + +private: + std::unique_ptr d; +}; + +#endif // QXMPPOMEMOMEMORYSTORAGE_H diff --git a/src/omemo/QXmppOmemoStorage.cpp b/src/omemo/QXmppOmemoStorage.cpp new file mode 100644 index 00000000..97b66dbe --- /dev/null +++ b/src/omemo/QXmppOmemoStorage.cpp @@ -0,0 +1,97 @@ +// SPDX-FileCopyrightText: 2022 Melvin Keskin +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +/// +/// \class QXmppOmemoStorage +/// +/// \brief The QXmppOmemoStorage class stores data used by +/// \xep{0384, OMEMO Encryption}. +/// +/// \warning THIS API IS NOT FINALIZED YET! +/// +/// \since QXmpp 1.5 +/// + +/// +/// \fn QXmppOmemoStorage::allData() +/// +/// Returns all data used by OMEMO. +/// +/// \return the OMEMO data +/// + +/// +/// \fn QXmppOmemoStorage::setOwnDevice(const std::optional &device) +/// +/// Sets the own device (i.e., the device used by this client instance). +/// +/// \param device own device +/// + +/// +/// \fn QXmppOmemoStorage::addSignedPreKeyPair(uint32_t keyId, const SignedPreKeyPair &keyPair) +/// +/// Adds a signed pre key pair. +/// +/// \param keyId ID of the signed pre key pair +/// \param keyPair signed pre key pair +/// + +/// +/// \fn QXmppOmemoStorage::removeSignedPreKeyPair(uint32_t keyId) +/// +/// Removes a signed pre key pair. +/// +/// \param keyId ID of the signed pre key pair +/// + +/// +/// \fn QXmppOmemoStorage::addPreKeyPairs(const QHash &keyPairs) +/// +/// Adds pre key pairs. +/// +/// \param keyPairs key IDs mapped to the pre key pairs +/// + +/// +/// \fn QXmppOmemoStorage::removePreKeyPair(uint32_t keyId) +/// +/// Removes a pre key pair. +/// +/// \param keyId ID of the pre key pair +/// + +/// +/// \fn QXmppOmemoStorage::addDevice(const QString &jid, uint32_t deviceId, const Device &device) +/// +/// Adds other devices (i.e., all devices but the own one). +/// +/// \param jid JID of the device owner +/// \param deviceId ID of the device +/// \param device device being added +/// + +/// +/// \fn QXmppOmemoStorage::removeDevice(const QString &jid, uint32_t deviceId) +/// +/// Removes a device of the other devices (i.e., all devices but the own one). +/// +/// \param jid JID of the device owner +/// \param deviceId ID of the device being removed +/// + +/// +/// \fn QXmppOmemoStorage::removeDevices(const QString &jid) +/// +/// Removes all devices of a passed JID from the other devices (i.e., all +/// devices but the own one). +/// +/// \param jid JID of the device owner +/// + +/// +/// \fn QXmppOmemoStorage::resetAll() +/// +/// Resets all data. +/// diff --git a/src/omemo/QXmppOmemoStorage.h b/src/omemo/QXmppOmemoStorage.h new file mode 100644 index 00000000..e3139544 --- /dev/null +++ b/src/omemo/QXmppOmemoStorage.h @@ -0,0 +1,174 @@ +// SPDX-FileCopyrightText: 2022 Melvin Keskin +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#ifndef QXMPPOMEMOSTORAGE_H +#define QXMPPOMEMOSTORAGE_H + +#include "qxmppomemo_export.h" + +#include + +#include +#include + +class QXMPPOMEMO_EXPORT QXmppOmemoStorage +{ +public: + /// + /// Contains the data of this client instance's OMEMO device. + /// + struct OwnDevice + { + /// + /// ID used to identify a device and fetch its bundle + /// + /// A valid ID must be at least 1 and at most + /// \c std::numeric_limits::max(). + /// + uint32_t id = 0; + + /// + /// Human-readable string used to identify the device by users + /// + /// The label should not contain more than 53 characters. + /// + QString label; + + /// + /// Private long-term key which never changes + /// + QByteArray privateIdentityKey; + + /// + /// Public long-term key which never changes + /// + QByteArray publicIdentityKey; + + /// + /// ID of the latest pre key pair whose public key is signed + /// + /// A valid ID must be at least 1 and at most + /// \c std::numeric_limits::max(). + /// + uint32_t latestSignedPreKeyId = 1; + + /// + /// ID of the latest pre key pair + /// + /// A valid ID must be at least 1 and at most + /// \c std::numeric_limits::max(). + /// + uint32_t latestPreKeyId = 1; + }; + + /// + /// Contains the data of another OMEMO device. + /// That includes another own device (i.e., not this client instance's one) + /// or a contact's device. + /// + struct Device + { + /// + /// Human-readable string used to identify the device by users + /// + QString label; + + /// + /// ID of the public long-term key which never changes + /// + QByteArray keyId; + + /// + /// Session data which is only used internally by the OMEMO library + /// + QByteArray session; + + /// + /// Count of stanzas sent to the device without receiving a response + /// + /// It can be used to stop encryption in order to maintain a secure + /// communication. + /// + int unrespondedSentStanzasCount = 0; + + /// + /// Count of stanzas received from the device without sending a + /// response + /// + /// It can be used to send an empty response (heartbeat message) in + /// order to maintain a secure communication. + /// + int unrespondedReceivedStanzasCount = 0; + + /// + /// Date when the device was removed from the owner's device list + /// + /// It can be used to stop encrypting when a device is not used anymore. + /// + QDateTime removalFromDeviceListDate; + }; + + /// + /// Contains the data needed to manage an OMEMO signed pre key pair. + /// + struct SignedPreKeyPair + { + /// + /// Date when the signed pre key pair was created + /// + QDateTime creationDate; + + /// + /// Actual signed pre key pair + /// + QByteArray data; + }; + + /// + /// Contains all OMEMO data. + /// + struct OmemoData + { + /// + /// Device of this client instance + /// + std::optional ownDevice; + + /// + /// Key IDs mapped to their signed pre key pairs + /// + QHash signedPreKeyPairs; + + /// + /// Key IDs mapped to their pre key pairs + /// + QHash preKeyPairs; + + /// + /// JIDs of the device owners mapped to device IDs mapped to the other + /// devices (i.e., all devices except the own one) + /// + QHash> devices; + }; + + virtual ~QXmppOmemoStorage() = default; + + virtual QFuture allData() = 0; + + virtual QFuture setOwnDevice(const std::optional &device) = 0; + + virtual QFuture addSignedPreKeyPair(uint32_t keyId, const SignedPreKeyPair &keyPair) = 0; + virtual QFuture removeSignedPreKeyPair(uint32_t keyId) = 0; + + virtual QFuture addPreKeyPairs(const QHash &keyPairs) = 0; + virtual QFuture removePreKeyPair(uint32_t keyId) = 0; + + virtual QFuture addDevice(const QString &jid, uint32_t deviceId, const Device &device) = 0; + virtual QFuture removeDevice(const QString &jid, uint32_t deviceId) = 0; + virtual QFuture removeDevices(const QString &jid) = 0; + + virtual QFuture resetAll() = 0; +}; + +#endif // QXMPPOMEMOSTORAGE_H -- cgit v1.2.3