aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMelvin Keskin <melvo@olomono.de>2022-05-09 21:45:49 +0200
committerLinus Jahn <lnj@kaidan.im>2022-08-13 15:55:03 +0200
commitceb62dd9d0d86bc8327ace116930962cf7fad1e9 (patch)
tree35bdcccf3e1bebddadcdab6205383975ca66e4c4
parentec0669845b9072ea6cdc0fefb66f1d07511386a4 (diff)
Implement XEP-0384: OMEMO Encryption v0.8
This implements XEP-0384 in version v0.8 with a manager and storage classes to be user-implemented for persistant storage. The license of the code is LGPL-2.1-or-later as usual. However since libomemo-c (libsignal-protocol-c) is GPL-3.0, the built binary is always licensed under GPL-3.0. Having our code LGPL licensed will make it avoids relicensing in the future in case we port it to an LGPL compatible omemo library. Closes #133. Co-authored-by: Linus Jahn <lnj@kaidan.im>
-rw-r--r--CMakeLists.txt16
-rw-r--r--README.md1
-rw-r--r--doc/xep.doc2
-rw-r--r--src/CMakeLists.txt34
-rw-r--r--src/base/QXmppConstants.cpp2
-rw-r--r--src/base/QXmppConstants_p.h2
-rw-r--r--src/base/QXmppMessage.cpp14
-rw-r--r--src/base/QXmppMessage.h12
-rw-r--r--src/base/QXmppOmemoData.cpp2
-rw-r--r--src/client/OmemoCryptoProvider.cpp239
-rw-r--r--src/client/OmemoCryptoProvider.h17
-rw-r--r--src/client/OmemoLibWrappers.h100
-rw-r--r--src/client/QXmppAtmTrustMemoryStorage.cpp2
-rw-r--r--src/client/QXmppOmemoManager.cpp1282
-rw-r--r--src/client/QXmppOmemoManager.h157
-rw-r--r--src/client/QXmppOmemoManager_p.cpp3714
-rw-r--r--src/client/QXmppOmemoManager_p.h342
-rw-r--r--src/client/QXmppOmemoMemoryStorage.cpp119
-rw-r--r--src/client/QXmppOmemoMemoryStorage.h43
-rw-r--r--src/client/QXmppOmemoStorage.cpp97
-rw-r--r--src/client/QXmppOmemoStorage.h174
-rw-r--r--src/client/QXmppTrustMemoryStorage.cpp2
-rw-r--r--tests/CMakeLists.txt11
-rw-r--r--tests/qxmppomemodata/tst_qxmppomemodata.cpp2
-rw-r--r--tests/qxmppomemomanager/tst_qxmppomemomanager.cpp513
-rw-r--r--tests/qxmppomemomemorystorage/tst_qxmppomemomemorystorage.cpp310
26 files changed, 7197 insertions, 12 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 16569284..5f0a3f97 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -26,7 +26,7 @@ option(BUILD_TESTS "Build tests." ON)
option(BUILD_INTERNAL_TESTS "Build internal tests." OFF)
option(BUILD_DOCUMENTATION "Build API documentation." OFF)
option(BUILD_EXAMPLES "Build examples." ON)
-
+option(BUILD_OMEMO "Build the OMEMO module" OFF)
option(WITH_GSTREAMER "Build with GStreamer support for Jingle" OFF)
add_definitions(
@@ -36,6 +36,20 @@ add_definitions(
-DQT_NO_FOREACH
)
+if(BUILD_OMEMO)
+ add_definitions(-DBUILD_OMEMO)
+
+ # libomemo-c
+ find_package(PkgConfig REQUIRED)
+ pkg_check_modules(OmemoC REQUIRED IMPORTED_TARGET libomemo-c)
+
+ # QCA
+ find_package(Qca-qt${QT_VERSION_MAJOR} REQUIRED)
+ if(${QT_VERSION_MAJOR} EQUAL 6)
+ find_package(Qt6Core5Compat REQUIRED)
+ endif()
+endif()
+
add_subdirectory(src)
if(BUILD_TESTS)
diff --git a/README.md b/README.md
index 2329ff6f..4936e52d 100644
--- a/README.md
+++ b/README.md
@@ -44,6 +44,7 @@ You can pass the following arguments to CMake:
BUILD_EXAMPLES to build the examples (default: true)
BUILD_TESTS to build the unit tests (default: true)
BUILD_INTERNAL_TESTS to build the unit tests testing private parts of the API (default: false)
+ BUILD_OMEMO to build the OMEMO module (default: false)
WITH_GSTREAMER to enable audio/video over jingle (default: false)
Installing QXmpp
diff --git a/doc/xep.doc b/doc/xep.doc
index a124db75..46a9701a 100644
--- a/doc/xep.doc
+++ b/doc/xep.doc
@@ -62,6 +62,7 @@ Complete:
- \xep{0367, Message Attaching} (v0.3)
- \xep{0380, Explicit Message Encryption} (v0.4)
- \xep{0382, Spoiler messages} (v0.2)
+- \xep{0384, OMEMO Encryption} (v0.8)
- \xep{0420, Stanza Content Encryption} (v0.4)
- \xep{0428, Fallback Indication} (v0.1)
- \xep{0434, Trust Messages} (v0.6)
@@ -70,7 +71,6 @@ Complete:
Ongoing:
- \xep{0009, Jabber-RPC} (API is not finalized yet)
- \xep{0369, Mediated Information eXchange (MIX)} (Only IQ queries implemented) (v0.14)
-- \xep{0384, OMEMO Encryption} (Only wire protocol data classes implemented) (v0.8)
- \xep{0405, Mediated Information eXchange (MIX): Participant Server Requirements} (Only IQ queries implemented) (v0.4)
- \xep{0407, Mediated Information eXchange (MIX): Miscellaneous Capabilities} (QXmppMixInvitation) (v0.1)
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 3a73a6bf..4dfb2d16 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -169,7 +169,6 @@ set(SOURCE_FILES
base/QXmppMucIq.cpp
base/QXmppNonza.cpp
base/QXmppNonSASLAuth.cpp
- base/QXmppOmemoData.cpp
base/QXmppPacket.cpp
base/QXmppPingIq.cpp
base/QXmppPresence.cpp
@@ -251,6 +250,31 @@ set(SOURCE_FILES
server/QXmppServerPlugin.cpp
)
+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
+ )
+endif()
+
if(WITH_GSTREAMER)
find_package(GStreamer REQUIRED)
find_package(GLIB2 REQUIRED)
@@ -302,6 +326,14 @@ 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
diff --git a/src/base/QXmppConstants.cpp b/src/base/QXmppConstants.cpp
index 29b3862c..7864d229 100644
--- a/src/base/QXmppConstants.cpp
+++ b/src/base/QXmppConstants.cpp
@@ -170,6 +170,8 @@ const char *ns_spoiler = "urn:xmpp:spoiler:0";
const char *ns_omemo = "eu.siacs.conversations.axolotl";
const char *ns_omemo_1 = "urn:xmpp:omemo:1";
const char *ns_omemo_2 = "urn:xmpp:omemo:2";
+const char *ns_omemo_2_bundles = "urn:xmpp:omemo:2:bundles";
+const char *ns_omemo_2_devices = "urn:xmpp:omemo:2:devices";
// XEP-0405: Mediated Information eXchange (MIX): Participant Server Requirements
const char *ns_mix_pam = "urn:xmpp:mix:pam:1";
const char *ns_mix_roster = "urn:xmpp:mix:roster:0";
diff --git a/src/base/QXmppConstants_p.h b/src/base/QXmppConstants_p.h
index 6583cfdb..3bfff544 100644
--- a/src/base/QXmppConstants_p.h
+++ b/src/base/QXmppConstants_p.h
@@ -182,6 +182,8 @@ extern const char *ns_spoiler;
extern const char *ns_omemo;
extern const char *ns_omemo_1;
extern const char *ns_omemo_2;
+extern const char *ns_omemo_2_bundles;
+extern const char *ns_omemo_2_devices;
// XEP-0405: Mediated Information eXchange (MIX): Participant Server Requirements
extern const char *ns_mix_pam;
extern const char *ns_mix_roster;
diff --git a/src/base/QXmppMessage.cpp b/src/base/QXmppMessage.cpp
index d583fd7e..2f0fb5fe 100644
--- a/src/base/QXmppMessage.cpp
+++ b/src/base/QXmppMessage.cpp
@@ -11,8 +11,10 @@
#include "QXmppConstants_p.h"
#include "QXmppGlobal_p.h"
#include "QXmppMixInvitation.h"
+#ifdef BUILD_OMEMO
#include "QXmppOmemoElement_p.h"
#include "QXmppOmemoEnvelope_p.h"
+#endif
#include "QXmppTrustMessageElement.h"
#include "QXmppUtils.h"
@@ -136,10 +138,10 @@ public:
// XEP-0382: Spoiler messages
bool isSpoiler;
QString spoilerHint;
-
+#ifdef BUILD_OMEMO
// XEP-0384: OMEMO Encryption
std::optional<QXmppOmemoElement> omemoElement;
-
+#endif
// XEP-0407: Mediated Information eXchange (MIX): Miscellaneous Capabilities
std::optional<QXmppMixInvitation> mixInvitation;
@@ -1078,6 +1080,8 @@ void QXmppMessage::setSpoilerHint(const QString &spoilerHint)
d->isSpoiler = true;
}
+#ifdef BUILD_OMEMO
+/// \cond
///
/// Returns an included OMEMO element as defined by \xep{0384, OMEMO Encryption}.
///
@@ -1097,6 +1101,8 @@ void QXmppMessage::setOmemoElement(const std::optional<QXmppOmemoElement> &omemo
{
d->omemoElement = omemoElement;
}
+/// \endcond
+#endif
///
/// Returns an included \xep{0369}: Mediated Information eXchange (MIX)
@@ -1297,6 +1303,7 @@ bool QXmppMessage::parseExtension(const QDomElement &element, QXmpp::SceMode sce
d->encryptionName = element.attribute(QStringLiteral("name"));
return true;
}
+#ifdef BUILD_OMEMO
// XEP-0384: OMEMO Encryption
if (QXmppOmemoElement::isOmemoElement(element)) {
QXmppOmemoElement omemoElement;
@@ -1304,6 +1311,7 @@ bool QXmppMessage::parseExtension(const QDomElement &element, QXmpp::SceMode sce
d->omemoElement = omemoElement;
return true;
}
+#endif
// XEP-0428: Fallback Indication
if (checkElement(element, QStringLiteral("fallback"), ns_fallback_indication)) {
d->isFallback = true;
@@ -1523,10 +1531,12 @@ void QXmppMessage::serializeExtensions(QXmlStreamWriter *writer, QXmpp::SceMode
writer->writeEndElement();
}
+#ifdef BUILD_OMEMO
// XEP-0384: OMEMO Encryption
if (d->omemoElement) {
d->omemoElement->toXml(writer);
}
+#endif
// XEP-0428: Fallback Indication
if (d->isFallback) {
diff --git a/src/base/QXmppMessage.h b/src/base/QXmppMessage.h
index e9d442c1..ba6917c4 100644
--- a/src/base/QXmppMessage.h
+++ b/src/base/QXmppMessage.h
@@ -18,7 +18,9 @@
class QXmppMessagePrivate;
class QXmppBitsOfBinaryDataList;
class QXmppMixInvitation;
+#ifdef BUILD_OMEMO
class QXmppOmemoElement;
+#endif
class QXmppTrustMessageElement;
///
@@ -232,10 +234,6 @@ public:
QString spoilerHint() const;
void setSpoilerHint(const QString &);
- // XEP-0384: OMEMO Encryption
- std::optional<QXmppOmemoElement> omemoElement() const;
- void setOmemoElement(const std::optional<QXmppOmemoElement> &omemoElement);
-
// XEP-0407: Mediated Information eXchange (MIX): Miscellaneous Capabilities
std::optional<QXmppMixInvitation> mixInvitation() const;
void setMixInvitation(const std::optional<QXmppMixInvitation> &mixInvitation);
@@ -249,6 +247,12 @@ public:
void setTrustMessageElement(const std::optional<QXmppTrustMessageElement> &trustMessageElement);
/// \cond
+#ifdef BUILD_OMEMO
+ // XEP-0384: OMEMO Encryption
+ std::optional<QXmppOmemoElement> omemoElement() const;
+ void setOmemoElement(const std::optional<QXmppOmemoElement> &omemoElement);
+#endif
+
void parse(const QDomElement &element) override final;
virtual void parse(const QDomElement &element, QXmpp::SceMode);
void toXml(QXmlStreamWriter *writer) const override final;
diff --git a/src/base/QXmppOmemoData.cpp b/src/base/QXmppOmemoData.cpp
index 33215d67..dd99eefd 100644
--- a/src/base/QXmppOmemoData.cpp
+++ b/src/base/QXmppOmemoData.cpp
@@ -247,7 +247,7 @@ QByteArray QXmppOmemoDeviceBundle::signedPublicPreKeySignature() const
}
///
-/// Returns the signature of the public pre key that is signed.
+/// Sets the signature of the public pre key that is signed.
///
/// \param signature signature of the signed public pre key
///
diff --git a/src/client/OmemoCryptoProvider.cpp b/src/client/OmemoCryptoProvider.cpp
new file mode 100644
index 00000000..e39124d4
--- /dev/null
+++ b/src/client/OmemoCryptoProvider.cpp
@@ -0,0 +1,239 @@
+// SPDX-FileCopyrightText: 2021 Linus Jahn <lnj@kaidan.im>
+// SPDX-FileCopyrightText: 2022 Melvin Keskin <melvo@olomono.de>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "OmemoCryptoProvider.h"
+
+#include "QXmppOmemoManager_p.h"
+#include "QXmppUtils_p.h"
+
+#include <QStringBuilder>
+#include <QtCrypto>
+
+using namespace QXmpp::Private;
+
+inline QXmppOmemoManagerPrivate *managerPrivate(void *ptr)
+{
+ return reinterpret_cast<QXmppOmemoManagerPrivate *>(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<const char *>(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<QCA::MessageAuthenticationCode *>(hmac_context);
+ messageAuthenticationCodeGenerator->update(QCA::MemoryRegion(QByteArray(reinterpret_cast<const char *>(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<QCA::MessageAuthenticationCode *>(hmac_context);
+
+ auto messageAuthenticationCode = messageAuthenticationCodeGenerator->final();
+ if (!(*output = signal_buffer_create(reinterpret_cast<const uint8_t *>(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<QCA::MessageAuthenticationCode *>(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<QCryptographicHash *>(digest_context);
+ hashGenerator->addData(reinterpret_cast<const char *>(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<QCryptographicHash *>(digest_context);
+
+ auto hash = hashGenerator->result();
+ if (!(*output = signal_buffer_create(reinterpret_cast<const uint8_t *>(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<QCryptographicHash *>(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<const char *>(key), key_len));
+ const auto initializationVector = QCA::InitializationVector(QByteArray(reinterpret_cast<const char *>(iv), iv_len));
+ QCA::Cipher encryptionCipher(cipherName, mode, padding, QCA::Encode, encryptionKey, initializationVector);
+
+ auto encryptedData = encryptionCipher.process(QCA::MemoryRegion(QByteArray(reinterpret_cast<const char *>(plaintext), plaintext_len)));
+
+ if (encryptedData.isEmpty()) {
+ return -3;
+ }
+
+ if (!(*output = signal_buffer_create(reinterpret_cast<const uint8_t *>(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<const char *>(key), key_len));
+ const auto initializationVector = QCA::InitializationVector(QByteArray(reinterpret_cast<const char *>(iv), iv_len));
+ QCA::Cipher decryptionCipher(cipherName, mode, padding, QCA::Decode, encryptionKey, initializationVector);
+
+ auto decryptedData = decryptionCipher.process(QCA::MemoryRegion(QByteArray(reinterpret_cast<const char *>(ciphertext), ciphertext_len)));
+
+ if (decryptedData.isEmpty()) {
+ return -3;
+ }
+
+ if (!(*output = signal_buffer_create(reinterpret_cast<const uint8_t *>(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
new file mode 100644
index 00000000..f4c63cd8
--- /dev/null
+++ b/src/client/OmemoCryptoProvider.h
@@ -0,0 +1,17 @@
+// SPDX-FileCopyrightText: 2022 Linus Jahn <lnj@kaidan.im>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#ifndef OMEMOCRYPTOPROVIDER_H
+#define OMEMOCRYPTOPROVIDER_H
+
+#include <signal_protocol.h>
+
+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
new file mode 100644
index 00000000..e157f12c
--- /dev/null
+++ b/src/client/OmemoLibWrappers.h
@@ -0,0 +1,100 @@
+// SPDX-FileCopyrightText: 2022 Linus Jahn <lnj@kaidan.im>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#ifndef OMEMOLIBWRAPPERS_H
+#define OMEMOLIBWRAPPERS_H
+
+#include <key_helper.h>
+#include <session_builder.h>
+#include <session_cipher.h>
+#include <signal_protocol.h>
+
+// Wraps various types of the OMEMO library.
+template<typename T, void(destruct)(T *)>
+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<T, destruct> &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<typename T>
+void omemoLibUnrefHelper(T *ptr)
+{
+ SIGNAL_UNREF(ptr);
+}
+
+template<typename T>
+using RefCountedPtr = OmemoLibPtr<T, omemoLibUnrefHelper<T>>;
+
+static QByteArray omemoLibBufferToByteArray(signal_buffer *buffer)
+{
+ return QByteArray(reinterpret_cast<const char *>(signal_buffer_data(buffer)), signal_buffer_len(buffer));
+}
+
+static signal_buffer *omemoLibBufferFromByteArray(const QByteArray &bytes)
+{
+ return signal_buffer_create(reinterpret_cast<const unsigned char *>(bytes.constData()), bytes.size());
+}
+
+template<void(destruct)(signal_buffer *)>
+class BufferPtrBase : public OmemoLibPtr<signal_buffer, destruct>
+{
+public:
+ QByteArray toByteArray() const
+ {
+ return omemoLibBufferToByteArray(this->get());
+ }
+};
+
+class BufferSecurePtr : public BufferPtrBase<signal_buffer_bzero_free>
+{
+public:
+ static BufferSecurePtr fromByteArray(const QByteArray &bytes)
+ {
+ return { omemoLibBufferFromByteArray(bytes) };
+ }
+};
+
+class BufferPtr : public BufferPtrBase<signal_buffer_free>
+{
+public:
+ static BufferPtr fromByteArray(const QByteArray &bytes)
+ {
+ return { omemoLibBufferFromByteArray(bytes) };
+ }
+};
+
+using KeyListNodePtr = OmemoLibPtr<signal_protocol_key_helper_pre_key_list_node, signal_protocol_key_helper_key_list_free>;
+using SessionCipherPtr = OmemoLibPtr<session_cipher, session_cipher_free>;
+using SessionBuilderPtr = OmemoLibPtr<session_builder, session_builder_free>;
+using OmemoContextPtr = OmemoLibPtr<signal_context, signal_context_destroy>;
+using StoreContextPtr = OmemoLibPtr<signal_protocol_store_context, signal_protocol_store_context_destroy>;
+
+#endif // OMEMOLIBWRAPPERS_H
diff --git a/src/client/QXmppAtmTrustMemoryStorage.cpp b/src/client/QXmppAtmTrustMemoryStorage.cpp
index 404c5564..eb8fe4df 100644
--- a/src/client/QXmppAtmTrustMemoryStorage.cpp
+++ b/src/client/QXmppAtmTrustMemoryStorage.cpp
@@ -7,6 +7,8 @@
#include "QXmppFutureUtils_p.h"
#include "QXmppTrustMessageKeyOwner.h"
+#include <QMultiHash>
+
using namespace QXmpp::Private;
///
diff --git a/src/client/QXmppOmemoManager.cpp b/src/client/QXmppOmemoManager.cpp
new file mode 100644
index 00000000..cb66a515
--- /dev/null
+++ b/src/client/QXmppOmemoManager.cpp
@@ -0,0 +1,1282 @@
+// SPDX-FileCopyrightText: 2022 Melvin Keskin <melvo@olomono.de>
+// SPDX-FileCopyrightText: 2022 Linus Jahn <lnj@kaidan.im>
+//
+// 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 <QStringBuilder>
+
+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<bool> Manager::load()
+{
+ QFutureInterface<bool> 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<bool> Manager::setUp()
+{
+ QFutureInterface<bool> 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<ratchet_identity_key_pair> 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<QByteArray> 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<QHash<QXmpp::TrustLevel, QMultiHash<QString, QByteArray>>> 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<QHash<QString, QHash<QByteArray, QXmpp::TrustLevel>>> Manager::keys(const QList<QString> &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<bool> 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::DevicesResult> Manager::requestDeviceLists(const QList<QString> &jids)
+{
+ if (const auto jidsCount = jids.size()) {
+ QFutureInterface<Manager::DevicesResult> interface(QFutureInterfaceBase::Started);
+ auto processedJidsCount = std::make_shared<int>(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<DevicesResult>(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::DevicesResult> Manager::subscribeToDeviceLists(const QList<QString> &jids)
+{
+ QFutureInterface<Manager::DevicesResult> interface(QFutureInterfaceBase::Started);
+
+ if (const auto jidsCount = jids.size()) {
+ auto processedJidsCount = std::make_shared<int>(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::DevicesResult> 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<QVector<QXmppOmemoDevice>> 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<QVector<QXmppOmemoDevice>> Manager::devices(const QList<QString> &jids)
+{
+ QFutureInterface<QVector<QXmppOmemoDevice>> interface(QFutureInterfaceBase::Started);
+
+ auto future = keys(jids);
+ await(future, this, [=](QHash<QString, QHash<QByteArray, TrustLevel>> keys) mutable {
+ QVector<QXmppOmemoDevice> 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<QXmppPubSubManager::Result> Manager::removeContactDevices(const QString &jid)
+{
+ QFutureInterface<QXmppPubSubManager::Result> 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<QXmppStanza::Error>(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<void> Manager::buildMissingSessions(const QList<QString> &jids)
+{
+ QFutureInterface<void> 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<int>(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<bool> 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<bool> 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<void> 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<QXmpp::TrustSecurityPolicy> Manager::securityPolicy()
+{
+ return d->trustManager->securityPolicy(ns_omemo_2);
+}
+
+///
+/// \fn QXmppOmemoManager::setTrustLevel(const QMultiHash<QString, QByteArray> &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<void> Manager::setTrustLevel(const QMultiHash<QString, QByteArray> &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<QXmpp::TrustLevel> Manager::trustLevel(const QString &keyOwnerJid, const QByteArray &keyId)
+{
+ return d->trustManager->trustLevel(ns_omemo_2, keyOwnerJid, keyId);
+}
+
+/// \cond
+QFuture<QXmppE2eeExtension::MessageEncryptResult> Manager::encryptMessage(QXmppMessage &&message, const std::optional<QXmppSendStanzaParams> &params)
+{
+ QVector<QString> recipientJids;
+ std::optional<TrustLevels> 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<QXmppE2eeExtension::IqEncryptResult> Manager::encryptIq(QXmppIq &&iq, const std::optional<QXmppSendStanzaParams> &params)
+{
+ QFutureInterface<QXmppE2eeExtension::IqEncryptResult> 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<TrustLevels> 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<QXmppOmemoElement> omemoElement) mutable {
+ if (!omemoElement) {
+ QXmpp::SendError error;
+ error.text = QStringLiteral("OMEMO element could not be created");
+ error.type = QXmpp::SendError::EncryptionError;
+ reportFinishedResult(interface, { error });
+ } else {
+ 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<QXmppE2eeExtension::IqDecryptResult> Manager::decryptIq(const QDomElement &element)
+{
+ if (!d->isStarted) {
+ // TODO: Add decryption queue to avoid this error
+ return makeReadyFuture<IqDecryptResult>(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<IqDecryptResult>(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<IqDecryptResult>(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<QXmppMessage> optionalDecryptedMessage) mutable {
+ if (optionalDecryptedMessage) {
+ injectMessage(std::move(*optionalDecryptedMessage));
+ }
+ });
+
+ return true;
+ }
+
+ return false;
+}
+/// \endcond
+
+///
+/// \fn QXmppOmemoManager::trustLevelsChanged(const QMultiHash<QString, QByteArray> &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<QXmppTrustManager>();
+ if (!d->trustManager) {
+ qFatal("QXmppTrustManager is not available, it must be added to the client before adding QXmppOmemoManager");
+ }
+
+ d->pubSubManager = client->findExtension<QXmppPubSubManager>();
+ 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<QString, QMultiHash<QString, QByteArray>> &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<QXmppOmemoDeviceListItem>::isPubSubEvent(element)) {
+ QXmppPubSubEvent<QXmppOmemoDeviceListItem> 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
new file mode 100644
index 00000000..91d57799
--- /dev/null
+++ b/src/client/QXmppOmemoManager.h
@@ -0,0 +1,157 @@
+// SPDX-FileCopyrightText: 2022 Melvin Keskin <melvo@olomono.de>
+//
+// 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<QXmppOmemoOwnDevicePrivate> 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<QXmppOmemoDevicePrivate> d;
+};
+
+class QXMPP_EXPORT QXmppOmemoManager : public QXmppClientExtension, public QXmppE2eeExtension, public QXmppPubSubEventHandler, public QXmppMessageHandler
+{
+ Q_OBJECT
+
+public:
+ using Result = std::variant<QXmpp::Success, QXmppStanza::Error>;
+
+ struct DevicesResult
+ {
+ QString jid;
+ Result result;
+ };
+
+ explicit QXmppOmemoManager(QXmppOmemoStorage *omemoStorage);
+ ~QXmppOmemoManager() override;
+
+ QFuture<bool> load();
+ QFuture<bool> setUp();
+
+ QFuture<QByteArray> ownKey();
+ QFuture<QHash<QXmpp::TrustLevel, QMultiHash<QString, QByteArray>>> keys(QXmpp::TrustLevels trustLevels = {});
+ QFuture<QHash<QString, QHash<QByteArray, QXmpp::TrustLevel>>> keys(const QList<QString> &jids, QXmpp::TrustLevels trustLevels = {});
+
+ QFuture<bool> changeDeviceLabel(const QString &deviceLabel = {});
+
+ int maximumDevicesPerJid() const;
+ void setMaximumDevicesPerJid(int maximum);
+
+ int maximumDevicesPerStanza() const;
+ void setMaximumDevicesPerStanza(int maximum);
+
+ QFuture<DevicesResult> requestDeviceLists(const QList<QString> &jids);
+ QFuture<DevicesResult> subscribeToDeviceLists(const QList<QString> &jids);
+ QFuture<DevicesResult> unsubscribeFromDeviceLists();
+
+ QXmppOmemoOwnDevice ownDevice();
+ QFuture<QVector<QXmppOmemoDevice>> devices();
+ QFuture<QVector<QXmppOmemoDevice>> devices(const QList<QString> &jids);
+ QFuture<QXmppPubSubManager::Result> removeContactDevices(const QString &jid);
+
+ void setAcceptedSessionBuildingTrustLevels(QXmpp::TrustLevels trustLevels);
+ QXmpp::TrustLevels acceptedSessionBuildingTrustLevels();
+
+ void setNewDeviceAutoSessionBuildingEnabled(bool isNewDeviceAutoSessionBuildingEnabled);
+ bool isNewDeviceAutoSessionBuildingEnabled();
+
+ QFuture<void> buildMissingSessions(const QList<QString> &jids);
+
+ QFuture<bool> resetOwnDevice();
+ QFuture<bool> resetAll();
+
+ QFuture<void> setSecurityPolicy(QXmpp::TrustSecurityPolicy securityPolicy);
+ QFuture<QXmpp::TrustSecurityPolicy> securityPolicy();
+
+ QFuture<void> setTrustLevel(const QMultiHash<QString, QByteArray> &keyIds, QXmpp::TrustLevel trustLevel);
+ QFuture<QXmpp::TrustLevel> trustLevel(const QString &keyOwnerJid, const QByteArray &keyId);
+
+ /// \cond
+ QFuture<MessageEncryptResult> encryptMessage(QXmppMessage &&message, const std::optional<QXmppSendStanzaParams> &params) override;
+
+ QFuture<IqEncryptResult> encryptIq(QXmppIq &&iq, const std::optional<QXmppSendStanzaParams> &params) override;
+ QFuture<IqDecryptResult> 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<QString, QByteArray> &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<QXmppOmemoManagerPrivate> 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
new file mode 100644
index 00000000..2f8ca00d
--- /dev/null
+++ b/src/client/QXmppOmemoManager_p.cpp
@@ -0,0 +1,3714 @@
+// SPDX-FileCopyrightText: 2022 Melvin Keskin <melvo@olomono.de>
+// SPDX-FileCopyrightText: 2022 Linus Jahn <lnj@kaidan.im>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+/// \cond
+
+#include "QXmppOmemoManager_p.h"
+
+#include "QXmppConstants_p.h"
+#include "QXmppOmemoDeviceElement_p.h"
+#include "QXmppOmemoElement_p.h"
+#include "QXmppOmemoEnvelope_p.h"
+#include "QXmppOmemoIq_p.h"
+#include "QXmppOmemoItems_p.h"
+#include "QXmppPubSubItem.h"
+#include "QXmppSceEnvelope_p.h"
+#include "QXmppTrustManager.h"
+#include "QXmppUtils.h"
+#include "QXmppUtils_p.h"
+
+#include <protocol.h>
+
+#include "OmemoCryptoProvider.h"
+
+#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
+#include <QRandomGenerator>
+#endif
+#include <QStringBuilder>
+
+using namespace QXmpp;
+using namespace QXmpp::Private;
+using namespace QXmpp::Omemo::Private;
+
+using Error = QXmppStanza::Error;
+using Manager = QXmppOmemoManager;
+using ManagerPrivate = QXmppOmemoManagerPrivate;
+
+namespace QXmpp::Omemo::Private {
+
+const QString PAYLOAD_MESSAGE_AUTHENTICATION_CODE_TYPE = QStringLiteral("hmac(sha256)");
+
+//
+// Creates a key ID.
+//
+// The first byte representing a version string used by the OMEMO library but
+// not needed for trust management is removed.
+// It corresponds to the fingerprint shown to users which also does not contain
+// the first byte.
+//
+// \param key key for whom its ID is created
+//
+// \return the key ID
+//
+QByteArray createKeyId(const QByteArray &key)
+{
+ return QByteArray(key).remove(0, 1);
+}
+
+} // namespace QXmpp::Omemo::Private
+
+//
+// Contains address data for an OMEMO device and a method to get the corresponding OMEMO library
+// data structure.
+//
+class Address
+{
+public:
+ //
+ // Creates an OMEMO device address.
+ //
+ // \param jid bare JID of the device owner
+ // \param deviceId ID of the device
+ //
+ Address(const QString &jid, uint32_t deviceId)
+ : m_jid(jid.toUtf8()), m_deviceId(int32_t(deviceId))
+ {
+ }
+ //
+ // Returns the representation of the OMEMO device address used by the OMEMO library.
+ //
+ // \return the OMEMO library device address
+ //
+ signal_protocol_address data() const
+ {
+ return { m_jid.data(), size_t(m_jid.size()), m_deviceId };
+ }
+
+private:
+ QByteArray m_jid;
+ int32_t m_deviceId;
+};
+
+//
+// Creates a PEP node configuration for the device list.
+//
+// \return the device list node configuration
+//
+static QXmppPubSubNodeConfig deviceListNodeConfig()
+{
+ QXmppPubSubNodeConfig config;
+ config.setAccessModel(QXmppPubSubNodeConfig::Open);
+
+ return config;
+}
+
+//
+// Creates publish options for publishing the device list to a corresponding PEP node.
+//
+// \return the device list node publish options
+//
+static QXmppPubSubPublishOptions deviceListNodePublishOptions()
+{
+ QXmppPubSubPublishOptions publishOptions;
+ publishOptions.setAccessModel(QXmppPubSubPublishOptions::Open);
+
+ return publishOptions;
+}
+
+//
+// Creates a PEP node configuration for device bundles.
+//
+// \return the device bundles node configuration
+//
+static QXmppPubSubNodeConfig deviceBundlesNodeConfig(QXmppPubSubNodeConfig::ItemLimit itemLimit = QXmppPubSubNodeConfig::Max())
+{
+ QXmppPubSubNodeConfig config;
+ config.setAccessModel(QXmppPubSubNodeConfig::Open);
+ config.setMaxItems(itemLimit);
+
+ return config;
+}
+
+//
+// Creates publish options for publishing device bundles to a corresponding PEP node.
+//
+// \return the device bundles node publish options
+//
+static QXmppPubSubPublishOptions deviceBundlesNodePublishOptions(QXmppPubSubNodeConfig::ItemLimit itemLimit = QXmppPubSubNodeConfig::Max())
+{
+ QXmppPubSubPublishOptions publishOptions;
+ publishOptions.setAccessModel(QXmppPubSubPublishOptions::Open);
+ publishOptions.setMaxItems(itemLimit);
+
+ return publishOptions;
+}
+
+//
+// Deserializes the signature of a signed public pre key.
+//
+// \param signedPublicPreKeySignature signed public pre key signature location
+// \param serializedSignedPublicPreKeySignature serialized signature of the
+// signed public pre key
+//
+// \return whether it succeeded
+//
+static int deserializeSignedPublicPreKeySignature(const uint8_t **signedPublicPreKeySignature, const QByteArray &serializedSignedPublicPreKeySignature)
+{
+ *signedPublicPreKeySignature = reinterpret_cast<const uint8_t *>(serializedSignedPublicPreKeySignature.constData());
+ return serializedSignedPublicPreKeySignature.size();
+}
+
+//
+// Extracts the JID from an address used by the OMEMO library.
+//
+// \param address address containing the JID data
+//
+// \return the extracted JID
+//
+static QString extractJid(signal_protocol_address address)
+{
+ return QString::fromUtf8(address.name, address.name_len);
+}
+
+static QString errorToString(const QXmppStanza::Error &err)
+{
+ return u"Error('" % err.text() % u"', type=" % QString::number(err.type()) % u", condition=" %
+ QString::number(err.condition()) % u")";
+}
+
+static void replaceChildElements(QDomElement &oldElement, const QDomElement &newElement)
+{
+ // remove old child elements
+ while (true) {
+ if (auto childElement = oldElement.firstChildElement(); !childElement.isNull()) {
+ oldElement.removeChild(childElement);
+ } else {
+ break;
+ }
+ }
+ // append new child elements
+ for (auto childElement = newElement.firstChildElement();
+ !childElement.isNull();
+ childElement = childElement.nextSiblingElement()) {
+ oldElement.appendChild(childElement);
+ }
+}
+
+template<typename T, typename Err>
+auto mapToSuccess(std::variant<T, Err> var)
+{
+ return mapSuccess(std::move(var), [](T) { return Success(); });
+}
+
+QXmppOmemoManagerPrivate::QXmppOmemoManagerPrivate(Manager *parent, QXmppOmemoStorage *omemoStorage)
+ : q(parent),
+ omemoStorage(omemoStorage),
+ signedPreKeyPairsRenewalTimer(parent),
+ deviceRemovalTimer(parent)
+{
+}
+
+//
+// Initializes the OMEMO library.
+//
+void ManagerPrivate::init()
+{
+ if (initGlobalContext() &&
+ initLocking() &&
+ initCryptoProvider()) {
+ initStores();
+ } else {
+ warning(QStringLiteral("OMEMO library could not be initialized"));
+ }
+}
+
+//
+// Initializes the OMEMO library's global context.
+//
+// \return whether the initialization succeeded
+//
+bool ManagerPrivate::initGlobalContext()
+{
+ // "q" is passed as the parameter "user_data" to functions called by
+ // the OMEMO library when no explicit "user_data" is set for those
+ // functions (e.g., to the lock and unlock functions).
+ if (signal_context_create(globalContext.ptrRef(), q) < 0) {
+ warning("Signal context could not be be created");
+ return false;
+ }
+
+ return true;
+}
+
+//
+// Initializes the OMEMO library's locking functions.
+//
+// \return whether the initialization succeeded
+//
+bool ManagerPrivate::initLocking()
+{
+ const auto lock = [](void *user_data) {
+ const auto *manager = reinterpret_cast<Manager *>(user_data);
+ auto *d = manager->d.get();
+ d->mutex.lock();
+ };
+
+ const auto unlock = [](void *user_data) {
+ const auto *manager = reinterpret_cast<Manager *>(user_data);
+ auto *d = manager->d.get();
+ d->mutex.unlock();
+ };
+
+ if (signal_context_set_locking_functions(globalContext.get(), lock, unlock) < 0) {
+ warning("Locking functions could not be set");
+ return false;
+ }
+
+ return true;
+}
+
+//
+// Initializes the OMEMO library's crypto provider.
+//
+// \return whether the initialization succeeded
+//
+bool ManagerPrivate::initCryptoProvider()
+{
+ cryptoProvider = createOmemoCryptoProvider(this);
+
+ if (signal_context_set_crypto_provider(globalContext.get(), &cryptoProvider) < 0) {
+ warning("Crypto provider could not be set");
+ return false;
+ }
+
+ return true;
+}
+
+//
+// Initializes the OMEMO library's stores.
+//
+// \return whether the initialization succeeded
+//
+void ManagerPrivate::initStores()
+{
+ identityKeyStore = createIdentityKeyStore();
+ preKeyStore = createPreKeyStore();
+ signedPreKeyStore = createSignedPreKeyStore();
+ sessionStore = createSessionStore();
+
+ signal_protocol_store_context_create(storeContext.ptrRef(), globalContext.get());
+ signal_protocol_store_context_set_identity_key_store(storeContext.get(), &identityKeyStore);
+ signal_protocol_store_context_set_pre_key_store(storeContext.get(), &preKeyStore);
+ signal_protocol_store_context_set_signed_pre_key_store(storeContext.get(), &signedPreKeyStore);
+ signal_protocol_store_context_set_session_store(storeContext.get(), &sessionStore);
+}
+
+//
+// Creates the OMEMO library's identity key store.
+//
+// The identity key is the long-term key.
+//
+// \return the identity key store
+//
+signal_protocol_identity_key_store ManagerPrivate::createIdentityKeyStore() const
+{
+ signal_protocol_identity_key_store store;
+
+ store.get_identity_key_pair = [](signal_buffer **public_data, signal_buffer **private_data, void *user_data) {
+ auto *manager = reinterpret_cast<Manager *>(user_data);
+ const auto *d = manager->d.get();
+
+ const auto &privateIdentityKey = d->ownDevice.privateIdentityKey;
+ if (!(*private_data = signal_buffer_create(reinterpret_cast<const uint8_t *>(privateIdentityKey.constData()), privateIdentityKey.size()))) {
+ manager->warning("Private identity key could not be loaded");
+ return -1;
+ }
+
+ const auto &publicIdentityKey = d->ownDevice.publicIdentityKey;
+ if (!(*public_data = signal_buffer_create(reinterpret_cast<const uint8_t *>(publicIdentityKey.constData()), publicIdentityKey.size()))) {
+ manager->warning("Public identity key could not be loaded");
+ return -1;
+ }
+
+ return 0;
+ };
+
+ store.get_local_registration_id = [](void *user_data, uint32_t *registration_id) {
+ const auto *manager = reinterpret_cast<Manager *>(user_data);
+ const auto *d = manager->d.get();
+ *registration_id = d->ownDevice.id;
+ return 0;
+ };
+
+ store.save_identity = [](const signal_protocol_address *, uint8_t *, size_t, void *) {
+ // Do not use the OMEMO library's trust management.
+ return 0;
+ };
+
+ store.is_trusted_identity = [](const signal_protocol_address *, uint8_t *, size_t, void *) {
+ // Do not use the OMEMO library's trust management.
+ // All keys are trusted at this level / by the OMEMO library.
+ return 1;
+ };
+
+ store.destroy_func = [](void *) {
+ };
+
+ store.user_data = q;
+
+ return store;
+}
+
+//
+// Creates the OMEMO library's signed pre key store.
+//
+// A signed pre key is used for building a session.
+//
+// \return the signed pre key store
+//
+signal_protocol_signed_pre_key_store ManagerPrivate::createSignedPreKeyStore() const
+{
+ signal_protocol_signed_pre_key_store store;
+
+ store.load_signed_pre_key = [](signal_buffer **record, uint32_t signed_pre_key_id, void *user_data) {
+ auto *manager = reinterpret_cast<Manager *>(user_data);
+ const auto *d = manager->d.get();
+ const auto &signedPreKeyPair = d->signedPreKeyPairs.value(signed_pre_key_id).data;
+
+ if (signedPreKeyPair.isEmpty()) {
+ return SG_ERR_INVALID_KEY_ID;
+ }
+
+ if (!(*record = signal_buffer_create(reinterpret_cast<const uint8_t *>(signedPreKeyPair.constData()), signedPreKeyPair.size()))) {
+ manager->warning("Signed pre key pair could not be loaded");
+ return SG_ERR_INVALID_KEY_ID;
+ }
+
+ return SG_SUCCESS;
+ };
+
+ store.store_signed_pre_key = [](uint32_t signed_pre_key_id, uint8_t *record, size_t record_len, void *user_data) {
+ auto *manager = reinterpret_cast<Manager *>(user_data);
+ auto *d = manager->d.get();
+
+ QXmppOmemoStorage::SignedPreKeyPair signedPreKeyPair;
+ signedPreKeyPair.creationDate = QDateTime::currentDateTimeUtc();
+ signedPreKeyPair.data = QByteArray(reinterpret_cast<const char *>(record), record_len);
+
+ d->signedPreKeyPairs.insert(signed_pre_key_id, signedPreKeyPair);
+ d->omemoStorage->addSignedPreKeyPair(signed_pre_key_id, signedPreKeyPair);
+
+ return 0;
+ };
+
+ store.contains_signed_pre_key = [](uint32_t signed_pre_key_id, void *user_data) {
+ const auto *manager = reinterpret_cast<Manager *>(user_data);
+ const auto *d = manager->d.get();
+ return d->signedPreKeyPairs.contains(signed_pre_key_id) ? 1 : 0;
+ };
+
+ store.remove_signed_pre_key = [](uint32_t signed_pre_key_id, void *user_data) {
+ const auto *manager = reinterpret_cast<Manager *>(user_data);
+ auto *d = manager->d.get();
+ d->signedPreKeyPairs.remove(signed_pre_key_id);
+ d->omemoStorage->removeSignedPreKeyPair(signed_pre_key_id);
+ return 0;
+ };
+
+ store.destroy_func = [](void *) {
+ };
+
+ store.user_data = q;
+
+ return store;
+}
+
+//
+// Creates the OMEMO library's pre key store.
+//
+// A pre key is used for building a session.
+//
+// \return the pre key store
+//
+signal_protocol_pre_key_store ManagerPrivate::createPreKeyStore() const
+{
+ signal_protocol_pre_key_store store;
+
+ store.load_pre_key = [](signal_buffer **record, uint32_t pre_key_id, void *user_data) {
+ auto *manager = reinterpret_cast<Manager *>(user_data);
+ const auto *d = manager->d.get();
+ const auto &preKey = d->preKeyPairs.value(pre_key_id);
+
+ if (preKey.isEmpty()) {
+ return SG_ERR_INVALID_KEY_ID;
+ }
+
+ if (!(*record = signal_buffer_create(reinterpret_cast<const uint8_t *>(preKey.constData()), preKey.size()))) {
+ manager->warning("Pre key could not be loaded");
+ return SG_ERR_INVALID_KEY_ID;
+ }
+
+ return SG_SUCCESS;
+ };
+
+ store.store_pre_key = [](uint32_t pre_key_id, uint8_t *record, size_t record_len, void *user_data) {
+ const auto *manager = reinterpret_cast<Manager *>(user_data);
+ auto *d = manager->d.get();
+ const auto preKey = QByteArray(reinterpret_cast<const char *>(record), record_len);
+ d->preKeyPairs.insert(pre_key_id, preKey);
+ d->omemoStorage->addPreKeyPairs({ { pre_key_id, preKey } });
+ return 0;
+ };
+
+ store.contains_pre_key = [](uint32_t pre_key_id, void *user_data) {
+ const auto *manager = reinterpret_cast<Manager *>(user_data);
+ const auto *d = manager->d.get();
+ return d->preKeyPairs.contains(pre_key_id) ? 1 : 0;
+ };
+
+ store.remove_pre_key = [](uint32_t pre_key_id, void *user_data) {
+ auto *manager = reinterpret_cast<Manager *>(user_data);
+ auto *d = manager->d.get();
+
+ if (!d->renewPreKeyPairs(pre_key_id)) {
+ return -1;
+ }
+
+ return 0;
+ };
+
+ store.destroy_func = [](void *) {
+ };
+
+ store.user_data = q;
+
+ return store;
+}
+
+//
+// Creates the OMEMO library's session store.
+//
+// A session contains all data needed for encryption and decryption.
+//
+// \return the session store
+//
+signal_protocol_session_store ManagerPrivate::createSessionStore() const
+{
+ signal_protocol_session_store store;
+
+ store.load_session_func = [](signal_buffer **record, signal_buffer **, const signal_protocol_address *address, void *user_data) {
+ auto *manager = reinterpret_cast<Manager *>(user_data);
+ const auto *d = manager->d.get();
+ const auto jid = extractJid(*address);
+
+ const auto &session = d->devices.value(jid).value(uint32_t(address->device_id)).session;
+
+ if (session.isEmpty()) {
+ return 0;
+ }
+
+ if (!(*record = signal_buffer_create(reinterpret_cast<const uint8_t *>(session.constData()), size_t(session.size())))) {
+ manager->warning("Session could not be loaded");
+ return -1;
+ }
+
+ return 1;
+ };
+
+ store.get_sub_device_sessions_func = [](signal_int_list **sessions, const char *name, size_t name_len, void *user_data) {
+ auto *manager = reinterpret_cast<Manager *>(user_data);
+ const auto *d = manager->d.get();
+ const auto jid = QString::fromUtf8(name, name_len);
+ auto userDevices = d->devices.value(jid);
+
+ // Remove all devices not having an active session.
+ for (auto itr = userDevices.begin(); itr != userDevices.end();) {
+ const auto &device = itr.value();
+ if (device.session.isEmpty() || device.unrespondedSentStanzasCount == UNRESPONDED_STANZAS_UNTIL_ENCRYPTION_IS_STOPPED) {
+ itr = userDevices.erase(itr);
+ } else {
+ ++itr;
+ }
+ }
+
+ signal_int_list *deviceIds = signal_int_list_alloc();
+ for (auto itr = userDevices.cbegin(); itr != userDevices.cend(); ++itr) {
+ const auto deviceId = itr.key();
+ if (signal_int_list_push_back(deviceIds, int(deviceId)) < 0) {
+ manager->warning("Device ID could not be added to list");
+ return -1;
+ }
+ }
+
+ *sessions = deviceIds;
+ return int(signal_int_list_size(*sessions));
+ };
+
+ store.store_session_func = [](const signal_protocol_address *address, uint8_t *record, size_t record_len, uint8_t *, size_t, void *user_data) {
+ const auto *manager = reinterpret_cast<Manager *>(user_data);
+ auto *d = manager->d.get();
+ const auto session = QByteArray(reinterpret_cast<const char *>(record), record_len);
+ const auto jid = extractJid(*address);
+ const auto deviceId = int(address->device_id);
+
+ auto &device = d->devices[jid][deviceId];
+ device.session = session;
+ d->omemoStorage->addDevice(jid, deviceId, device);
+ return 0;
+ };
+
+ store.contains_session_func = [](const signal_protocol_address *address, void *user_data) {
+ const auto *manager = reinterpret_cast<Manager *>(user_data);
+ const auto *d = manager->d.get();
+ const auto jid = extractJid(*address);
+ return d->devices.value(jid).value(int(address->device_id)).session.isEmpty() ? 0 : 1;
+ };
+
+ store.delete_session_func = [](const signal_protocol_address *address, void *user_data) {
+ const auto *manager = reinterpret_cast<Manager *>(user_data);
+ auto *d = manager->d.get();
+ const auto jid = extractJid(*address);
+ const auto deviceId = int(address->device_id);
+ auto &device = d->devices[jid][deviceId];
+ if (!device.session.isEmpty()) {
+ device.session.clear();
+ d->omemoStorage->addDevice(jid, deviceId, device);
+ }
+ return 1;
+ };
+
+ store.delete_all_sessions_func = [](const char *name, size_t name_len, void *user_data) {
+ const auto *manager = reinterpret_cast<Manager *>(user_data);
+ auto *d = manager->d.get();
+ const auto jid = QString::fromUtf8(name, name_len);
+ auto deletedSessionsCount = 0;
+ auto &userDevices = d->devices[jid];
+ for (auto itr = userDevices.begin(); itr != userDevices.end(); ++itr) {
+ const auto &deviceId = itr.key();
+ auto &device = itr.value();
+ if (!device.session.isEmpty()) {
+ device.session.clear();
+ d->omemoStorage->addDevice(jid, deviceId, device);
+ ++deletedSessionsCount;
+ }
+ }
+ return deletedSessionsCount;
+ };
+
+ store.destroy_func = [](void *) {
+ };
+
+ store.user_data = q;
+
+ return store;
+}
+
+//
+// Sets up the device ID.
+//
+// The more devices a user has, the higher the possibility of duplicate device IDs is.
+// Especially for IoT scenarios with millions of devices, that can be an issue.
+// Therefore, a new device ID is generated in case of a duplicate.
+//
+// \return whether it succeeded
+//
+QFuture<bool> ManagerPrivate::setUpDeviceId()
+{
+ QFutureInterface<bool> interface(QFutureInterfaceBase::Started);
+
+ auto future = pubSubManager->requestPepItemIds(ns_omemo_2_bundles);
+ await(future, q, [=](QXmppPubSubManager::ItemIdsResult result) mutable {
+ if (auto error = std::get_if<Error>(&result)) {
+ warning("Existing / Published device IDs could not be retrieved");
+ reportFinishedResult(interface, false);
+ } else {
+ const auto &deviceIds = std::get<QVector<QString>>(result);
+
+ while (true) {
+ uint32_t deviceId = 0;
+ if (signal_protocol_key_helper_generate_registration_id(&deviceId, 0, globalContext.get()) < 0) {
+ warning("Device ID could not be generated");
+ reportFinishedResult(interface, false);
+ break;
+ }
+
+ if (!deviceIds.contains(QString::number(deviceId))) {
+ ownDevice.id = deviceId;
+ reportFinishedResult(interface, true);
+ break;
+ }
+ }
+ }
+ });
+
+ return interface.future();
+}
+
+//
+// Sets up an identity key pair.
+//
+// The identity key pair consists of a private and a public long-term key.
+//
+// \return whether it succeeded
+//
+bool ManagerPrivate::setUpIdentityKeyPair(ratchet_identity_key_pair **identityKeyPair)
+{
+ if (signal_protocol_key_helper_generate_identity_key_pair(identityKeyPair, globalContext.get()) < 0) {
+ warning("Identity key pair could not be generated");
+ return false;
+ }
+
+ BufferSecurePtr privateIdentityKeyBuffer;
+
+ if (ec_private_key_serialize(privateIdentityKeyBuffer.ptrRef(), ratchet_identity_key_pair_get_private(*identityKeyPair)) < 0) {
+ warning("Private identity key could not be serialized");
+ return false;
+ }
+
+ const auto privateIdentityKey = privateIdentityKeyBuffer.toByteArray();
+ ownDevice.privateIdentityKey = privateIdentityKey;
+
+ BufferPtr publicIdentityKeyBuffer;
+
+ if (ec_public_key_serialize(publicIdentityKeyBuffer.ptrRef(), ratchet_identity_key_pair_get_public(*identityKeyPair)) < 0) {
+ warning("Public identity key could not be serialized");
+ return false;
+ }
+
+ const auto publicIdentityKey = publicIdentityKeyBuffer.toByteArray();
+ deviceBundle.setPublicIdentityKey(publicIdentityKey);
+ ownDevice.publicIdentityKey = publicIdentityKey;
+ storeOwnKey();
+
+ return true;
+}
+
+//
+// Schedules periodic (time-based) tasks that cannot be done on a specific event.
+//
+void ManagerPrivate::schedulePeriodicTasks()
+{
+ QObject::connect(&signedPreKeyPairsRenewalTimer, &QTimer::timeout, q, [=]() mutable {
+ renewSignedPreKeyPairs();
+ });
+
+ QObject::connect(&deviceRemovalTimer, &QTimer::timeout, q, [=]() mutable {
+ removeDevicesRemovedFromServer();
+ });
+
+ signedPreKeyPairsRenewalTimer.start(SIGNED_PRE_KEY_RENEWAL_CHECK_INTERVAL);
+ deviceRemovalTimer.start(DEVICE_REMOVAL_CHECK_INTERVAL);
+}
+
+//
+// Removes old signed pre key pairs and creates a new one.
+//
+void ManagerPrivate::renewSignedPreKeyPairs()
+{
+ const auto currentDate = QDateTime::currentDateTimeUtc().toSecsSinceEpoch() * 1s;
+ auto isSignedPreKeyPairRemoved = false;
+
+ for (auto itr = signedPreKeyPairs.begin(); itr != signedPreKeyPairs.end();) {
+ const auto creationDate = itr.value().creationDate.toSecsSinceEpoch() * 1s;
+
+ // Remove signed pre key pairs older than
+ // SIGNED_PRE_KEY_RENEWAL_INTERVAL.
+ if (currentDate - creationDate > SIGNED_PRE_KEY_RENEWAL_INTERVAL) {
+ itr = signedPreKeyPairs.erase(itr);
+ omemoStorage->removeSignedPreKeyPair(itr.key());
+ isSignedPreKeyPairRemoved = true;
+ } else {
+ ++itr;
+ }
+ }
+
+ if (isSignedPreKeyPairRemoved) {
+ RefCountedPtr<ratchet_identity_key_pair> identityKeyPair;
+ generateIdentityKeyPair(identityKeyPair.ptrRef());
+ updateSignedPreKeyPair(identityKeyPair.get());
+
+ // Store the own device containing the new signed pre key ID.
+ omemoStorage->setOwnDevice(ownDevice);
+
+ publishDeviceBundleItem([=](bool isPublished) {
+ if (!isPublished) {
+ warning("Own device bundle item could not be published during renewal of signed pre key pairs");
+ }
+ });
+ }
+}
+
+//
+// Updates the signed pre key pairs.
+//
+// Make sure that
+// \code
+// d->omemoStorage->setOwnDevice(d->ownDevice);
+// \endcode
+// is called afterwards to store the change of
+// \code
+// d->ownDevice.latestSignedPreKeyId()
+// \endcode
+// .
+//
+// \return whether it succeeded
+//
+bool ManagerPrivate::updateSignedPreKeyPair(ratchet_identity_key_pair *identityKeyPair)
+{
+ RefCountedPtr<session_signed_pre_key> signedPreKeyPair;
+ auto latestSignedPreKeyId = ownDevice.latestSignedPreKeyId;
+
+ // Ensure that no signed pre key ID exceeds SIGNED_PRE_KEY_ID_MAX
+ // Do not increment during setup.
+ if (latestSignedPreKeyId + 1 > SIGNED_PRE_KEY_ID_MAX) {
+ latestSignedPreKeyId = SIGNED_PRE_KEY_ID_MIN;
+ } else if (latestSignedPreKeyId != SIGNED_PRE_KEY_ID_MIN) {
+ ++latestSignedPreKeyId;
+ }
+
+ if (signal_protocol_key_helper_generate_signed_pre_key(
+ signedPreKeyPair.ptrRef(),
+ identityKeyPair,
+ latestSignedPreKeyId,
+ uint64_t(QDateTime::currentMSecsSinceEpoch()),
+ globalContext.get()) < 0) {
+ warning("Signed pre key pair could not be generated");
+ return false;
+ }
+
+ BufferSecurePtr signedPreKeyPairBuffer;
+
+ if (session_signed_pre_key_serialize(signedPreKeyPairBuffer.ptrRef(), signedPreKeyPair.get()) < 0) {
+ warning("Signed pre key pair could not be serialized");
+ return false;
+ }
+
+ QXmppOmemoStorage::SignedPreKeyPair signedPreKeyPairForStorage;
+ signedPreKeyPairForStorage.creationDate = QDateTime::currentDateTimeUtc();
+ signedPreKeyPairForStorage.data = signedPreKeyPairBuffer.toByteArray();
+
+ signedPreKeyPairs.insert(latestSignedPreKeyId, signedPreKeyPairForStorage);
+ omemoStorage->addSignedPreKeyPair(latestSignedPreKeyId, signedPreKeyPairForStorage);
+
+ BufferPtr signedPublicPreKeyBuffer;
+
+ if (ec_public_key_serialize(signedPublicPreKeyBuffer.ptrRef(), ec_key_pair_get_public(session_signed_pre_key_get_key_pair(signedPreKeyPair.get()))) < 0) {
+ warning("Signed public pre key could not be serialized");
+ return false;
+ }
+
+ const auto signedPublicPreKeyByteArray = signedPublicPreKeyBuffer.toByteArray();
+
+ deviceBundle.setSignedPublicPreKeyId(latestSignedPreKeyId);
+ deviceBundle.setSignedPublicPreKey(signedPublicPreKeyByteArray);
+ deviceBundle.setSignedPublicPreKeySignature(QByteArray(reinterpret_cast<const char *>(session_signed_pre_key_get_signature(signedPreKeyPair.get())), session_signed_pre_key_get_signature_len(signedPreKeyPair.get())));
+
+ ownDevice.latestSignedPreKeyId = latestSignedPreKeyId;
+
+ return true;
+}
+
+//
+// Deletes a pre key pair and creates a new one.
+//
+// \param keyPairBeingRenewed key pair being replaced by a new one
+//
+// \return whether it succeeded
+//
+bool ManagerPrivate::renewPreKeyPairs(uint32_t keyPairBeingRenewed)
+{
+ preKeyPairs.remove(keyPairBeingRenewed);
+ omemoStorage->removePreKeyPair(keyPairBeingRenewed);
+ deviceBundle.removePublicPreKey(keyPairBeingRenewed);
+
+ if (!updatePreKeyPairs()) {
+ return false;
+ }
+
+ // Store the own device containing the new pre key ID.
+ omemoStorage->setOwnDevice(ownDevice);
+
+ publishDeviceBundleItem([=](bool isPublished) {
+ if (!isPublished) {
+ warning("Own device bundle item could not be published during renewal of pre key pairs");
+ }
+ });
+
+ return true;
+}
+
+//
+// Updates the pre key pairs locally.
+//
+// Make sure that
+// \code
+// d->omemoStorage->setOwnDevice(d->ownDevice)
+// \endcode
+// is called
+// afterwards to store the change of
+// \code
+// d->ownDevice.latestPreKeyId()
+// \endcode
+// .
+//
+// \param count number of pre key pairs to update
+//
+// \return whether it succeeded
+//
+bool ManagerPrivate::updatePreKeyPairs(uint32_t count)
+{
+ KeyListNodePtr newPreKeyPairs;
+ auto latestPreKeyId = ownDevice.latestPreKeyId;
+
+ // Ensure that no pre key ID exceeds PRE_KEY_ID_MAX.
+ // Do not increment during setup.
+ if (latestPreKeyId + count > PRE_KEY_ID_MAX) {
+ latestPreKeyId = PRE_KEY_ID_MIN;
+ } else if (latestPreKeyId != PRE_KEY_ID_MIN) {
+ ++latestPreKeyId;
+ }
+
+ if (signal_protocol_key_helper_generate_pre_keys(newPreKeyPairs.ptrRef(), latestPreKeyId, count, globalContext.get()) < 0) {
+ warning("Pre key pairs could not be generated");
+ return false;
+ }
+
+ QHash<uint32_t, QByteArray> serializedPreKeyPairs;
+
+ for (auto *node = newPreKeyPairs.get();
+ node != nullptr;
+ node = signal_protocol_key_helper_key_list_next(node)) {
+ BufferSecurePtr preKeyPairBuffer;
+ BufferPtr publicPreKeyBuffer;
+
+ auto preKeyPair = signal_protocol_key_helper_key_list_element(node);
+
+ if (session_pre_key_serialize(preKeyPairBuffer.ptrRef(), preKeyPair) < 0) {
+ warning("Pre key pair could not be serialized");
+ return false;
+ }
+
+ const auto preKeyId = session_pre_key_get_id(preKeyPair);
+
+ serializedPreKeyPairs.insert(preKeyId, preKeyPairBuffer.toByteArray());
+
+ if (ec_public_key_serialize(publicPreKeyBuffer.ptrRef(), ec_key_pair_get_public(session_pre_key_get_key_pair(preKeyPair))) < 0) {
+ warning("Public pre key could not be serialized");
+ return false;
+ }
+
+ const auto serializedPublicPreKey = publicPreKeyBuffer.toByteArray();
+ deviceBundle.addPublicPreKey(preKeyId, serializedPublicPreKey);
+ }
+
+ this->preKeyPairs.insert(serializedPreKeyPairs);
+ omemoStorage->addPreKeyPairs(serializedPreKeyPairs);
+ ownDevice.latestPreKeyId = latestPreKeyId - 1 + count;
+
+ return true;
+}
+
+//
+// Removes locally stored devices after a specific time if they are removed from their owners'
+// device lists on their servers.
+//
+void ManagerPrivate::removeDevicesRemovedFromServer()
+{
+ const auto currentDate = QDateTime::currentDateTimeUtc().toSecsSinceEpoch() * 1s;
+
+ for (auto itr = devices.begin(); itr != devices.end(); ++itr) {
+ const auto &jid = itr.key();
+ auto &userDevices = itr.value();
+
+ for (auto devicesItr = userDevices.begin(); devicesItr != userDevices.end();) {
+ const auto &deviceId = devicesItr.key();
+ const auto &device = devicesItr.value();
+
+ // Remove data for devices removed from their servers after
+ // DEVICE_REMOVAL_INTERVAL.
+ const auto &removalDate = device.removalFromDeviceListDate;
+ if (!removalDate.isNull() &&
+ currentDate - removalDate.toSecsSinceEpoch() * 1s > DEVICE_REMOVAL_INTERVAL) {
+ devicesItr = userDevices.erase(devicesItr);
+ omemoStorage->removeDevice(jid, deviceId);
+ trustManager->removeKeys(ns_omemo_2, QList { device.keyId });
+ emit q->deviceRemoved(jid, deviceId);
+ } else {
+ ++devicesItr;
+ }
+ }
+ }
+}
+
+//
+// Generates an identity key pair.
+//
+// The identity key pair is the pair of private and a public long-term key.
+//
+// \param identityKeyPair identity key pair location
+//
+// \return whether it succeeded
+//
+bool ManagerPrivate::generateIdentityKeyPair(ratchet_identity_key_pair **identityKeyPair) const
+{
+ BufferSecurePtr privateIdentityKeyBuffer = BufferSecurePtr::fromByteArray(ownDevice.privateIdentityKey);
+
+ if (!privateIdentityKeyBuffer) {
+ warning("Buffer for serialized private identity key could not be created");
+ return false;
+ }
+
+ RefCountedPtr<ec_private_key> privateIdentityKey;
+
+ if (curve_decode_private_point(privateIdentityKey.ptrRef(), signal_buffer_data(privateIdentityKeyBuffer.get()), signal_buffer_len(privateIdentityKeyBuffer.get()), globalContext.get()) < 0) {
+ warning("Private identity key could not be deserialized");
+ return false;
+ }
+
+ const auto &serializedPublicIdentityKey = ownDevice.publicIdentityKey;
+ BufferPtr publicIdentityKeyBuffer = BufferPtr::fromByteArray(serializedPublicIdentityKey);
+
+ if (!publicIdentityKeyBuffer) {
+ warning("Buffer for serialized public identity key could not be created");
+ return false;
+ }
+
+ RefCountedPtr<ec_public_key> publicIdentityKey;
+
+ if (curve_decode_point(publicIdentityKey.ptrRef(), signal_buffer_data(publicIdentityKeyBuffer.get()), signal_buffer_len(publicIdentityKeyBuffer.get()), globalContext.get()) < 0) {
+ warning("Public identity key could not be deserialized");
+ return false;
+ }
+
+ if (ratchet_identity_key_pair_create(identityKeyPair, publicIdentityKey.get(), privateIdentityKey.get()) < 0) {
+ warning("Identity key pair could not be deserialized");
+ return false;
+ }
+
+ return true;
+}
+
+//
+// Encrypts a message for specific recipients.
+//
+// \param message message to be encrypted
+// \param recipientJids JIDs for whom the message is encrypted
+// \param acceptedTrustLevels trust levels the keys of the recipients' devices must have to
+// encrypt for them
+//
+// \return the result of the encryption
+//
+QFuture<QXmppE2eeExtension::MessageEncryptResult> ManagerPrivate::encryptMessageForRecipients(QXmppMessage &&message, QVector<QString> recipientJids, TrustLevels acceptedTrustLevels)
+{
+ QFutureInterface<QXmppE2eeExtension::MessageEncryptResult> interface(QFutureInterfaceBase::Started);
+
+ if (!isStarted) {
+ QXmpp::SendError error = { QStringLiteral("OMEMO manager must be started before encrypting"), QXmpp::SendError::EncryptionError };
+ reportFinishedResult(interface, { error });
+ } else {
+ recipientJids.append(ownBareJid());
+
+ auto future = encryptStanza(message, recipientJids, acceptedTrustLevels);
+ await(future, q, [=, message = std::move(message)](std::optional<QXmppOmemoElement> omemoElement) mutable {
+ if (!omemoElement) {
+ QXmpp::SendError error;
+ error.text = QStringLiteral("OMEMO element could not be created");
+ error.type = QXmpp::SendError::EncryptionError;
+ reportFinishedResult(interface, { error });
+ } else {
+ const auto areDeliveryReceiptsUsed = message.isReceiptRequested() || !message.receiptId().isEmpty();
+
+ // The following cases are covered:
+ // 1. Message with body (possibly including a chat state or used
+ // for delivery receipts) => usage of EME and fallback body
+ // 2. Message without body
+ // 2.1. Message with chat state or used for delivery receipts
+ // => neither usage of EME nor fallback body, but hint for
+ // server-side storage in case of delivery receipts usage
+ // 2.2. Other message (e.g., trust message) => usage of EME and
+ // fallback body to look like a normal message
+ if (!message.body().isEmpty() || (message.state() == QXmppMessage::None && !areDeliveryReceiptsUsed)) {
+ message.setEncryptionMethod(QXmpp::Omemo2);
+
+ // A message processing hint for instructing the server to
+ // store the message is not needed because of the public
+ // fallback body.
+ message.setE2eeFallbackBody(QStringLiteral("This message is encrypted with %1 but could not be decrypted").arg(message.encryptionName()));
+ message.setIsFallback(true);
+ } else if (areDeliveryReceiptsUsed) {
+ // A message processing hint for instructing the server to
+ // store the message is needed because of the missing public
+ // fallback body.
+ message.addHint(QXmppMessage::Store);
+ }
+
+ message.setOmemoElement(omemoElement);
+
+ QByteArray serializedEncryptedMessage;
+ QXmlStreamWriter writer(&serializedEncryptedMessage);
+ message.toXml(&writer, QXmpp::ScePublic);
+
+ reportFinishedResult(interface, { serializedEncryptedMessage });
+ }
+ });
+ }
+
+ return interface.future();
+}
+
+//
+// Encrypts a message or IQ stanza.
+//
+// \param stanza stanza to be encrypted
+// \param recipientJids JIDs of the devices for whom the stanza is encrypted
+// \param acceptedTrustLevels trust levels the keys of the recipients' devices must have to
+// encrypt for them
+//
+// \return the OMEMO element containing the stanza's encrypted content if the encryption is
+// successful, otherwise none
+//
+template<typename T>
+QFuture<std::optional<QXmppOmemoElement>> ManagerPrivate::encryptStanza(const T &stanza, const QVector<QString> &recipientJids, TrustLevels acceptedTrustLevels)
+{
+ Q_ASSERT_X(!recipientJids.isEmpty(), "Creating OMEMO envelope", "OMEMO element could not be created because no recipient JIDs are passed");
+
+ QFutureInterface<std::optional<QXmppOmemoElement>> interface(QFutureInterfaceBase::Started);
+
+ if (const auto optionalPayloadEncryptionResult = encryptPayload(createSceEnvelope(stanza))) {
+ const auto &payloadEncryptionResult = *optionalPayloadEncryptionResult;
+
+ auto devicesCount = std::accumulate(recipientJids.cbegin(), recipientJids.cend(), 0, [=](const auto sum, const auto &jid) {
+ return sum + devices.value(jid).size();
+ });
+
+ // Do not exceed the maximum of manageable devices.
+ if (devicesCount > maximumDevicesPerStanza) {
+ warning(u"OMEMO payload could not be encrypted for all recipients because their "
+ "devices are altogether more than the maximum of manageable devices " %
+ QString::number(maximumDevicesPerStanza) %
+ u" - Use QXmppOmemoManager::setMaximumDevicesPerStanza() to increase the maximum");
+ devicesCount = maximumDevicesPerStanza;
+ }
+
+ if (devicesCount) {
+ auto omemoElement = std::make_shared<QXmppOmemoElement>();
+ auto processedDevicesCount = std::make_shared<int>(0);
+ auto successfullyProcessedDevicesCount = std::make_shared<int>(0);
+ auto skippedDevicesCount = std::make_shared<int>(0);
+
+ // Add envelopes for all devices of the recipients.
+ for (const auto &jid : recipientJids) {
+ auto recipientDevices = devices.value(jid);
+
+ for (auto itr = recipientDevices.begin(); itr != recipientDevices.end(); ++itr) {
+ const auto &deviceId = itr.key();
+ const auto &device = itr.value();
+
+ // Skip encrypting for a device if it does not respond for a while.
+ if (const auto unrespondedSentStanzasCount = device.unrespondedSentStanzasCount; unrespondedSentStanzasCount == UNRESPONDED_STANZAS_UNTIL_ENCRYPTION_IS_STOPPED) {
+ if (++(*skippedDevicesCount) == devicesCount) {
+ warning("OMEMO element could not be created because no recipient device responded to " %
+ QString::number(unrespondedSentStanzasCount) % " sent stanzas");
+ reportFinishedResult(interface, {});
+ }
+
+ continue;
+ }
+
+ auto controlDeviceProcessing = [=](bool isSuccessful = true) mutable {
+ if (isSuccessful) {
+ ++(*successfullyProcessedDevicesCount);
+ }
+
+ if (++(*processedDevicesCount) == devicesCount) {
+ if (*successfullyProcessedDevicesCount == 0) {
+ warning("OMEMO element could not be created because no recipient "
+ "devices with keys having accepted trust levels could be found");
+ reportFinishedResult(interface, {});
+ } else {
+ omemoElement->setSenderDeviceId(ownDevice.id);
+ omemoElement->setPayload(payloadEncryptionResult.encryptedPayload);
+ reportFinishedResult(interface, { *omemoElement });
+ }
+ }
+ };
+
+ const auto address = Address(jid, deviceId);
+
+ auto addOmemoEnvelope = [=](bool isKeyExchange = false) mutable {
+ // Create and add an OMEMO envelope only if its data could be created
+ // and the corresponding device has not been removed by another method
+ // in the meantime.
+ if (const auto data = createOmemoEnvelopeData(address.data(), payloadEncryptionResult.decryptionData); data.isEmpty()) {
+ warning("OMEMO envelope for recipient JID '" % jid %
+ "' and device ID '" % QString::number(deviceId) %
+ "' could not be created because its data could not be encrypted");
+ controlDeviceProcessing(false);
+ } else if (devices.value(jid).contains(deviceId)) {
+ auto &deviceBeingModified = devices[jid][deviceId];
+ deviceBeingModified.unrespondedReceivedStanzasCount = 0;
+ ++deviceBeingModified.unrespondedSentStanzasCount;
+ omemoStorage->addDevice(jid, deviceId, deviceBeingModified);
+
+ QXmppOmemoEnvelope omemoEnvelope;
+ omemoEnvelope.setRecipientDeviceId(deviceId);
+ if (isKeyExchange) {
+ omemoEnvelope.setIsUsedForKeyExchange(true);
+ }
+ omemoEnvelope.setData(data);
+ omemoElement->addEnvelope(jid, omemoEnvelope);
+ controlDeviceProcessing();
+ }
+ };
+
+ auto buildSessionDependingOnTrustLevel = [=](const QXmppOmemoDeviceBundle &deviceBundle, TrustLevel trustLevel) mutable {
+ // Build a session if the device's key has a specific trust level.
+ if (!acceptedTrustLevels.testFlag(trustLevel)) {
+ q->debug("Session could not be created for JID '" % jid %
+ "' with device ID '" % QString::number(deviceId) %
+ "' because its key's trust level '" %
+ QString::number(int(trustLevel)) % "' is not accepted");
+ controlDeviceProcessing(false);
+ } else if (!buildSession(address.data(), deviceBundle)) {
+ warning("Session could not be created for JID '" % jid % "' and device ID '" % QString::number(deviceId) % "'");
+ controlDeviceProcessing(false);
+ } else {
+ addOmemoEnvelope(true);
+ }
+ };
+
+ // If the key ID is not stored (empty), the device bundle must be retrieved
+ // first.
+ // Afterwards, the bundle can be used to determine the key's trust level and
+ // to build the session.
+ // If the key ID is stored (not empty), the trust level can be directly
+ // determined and the session built.
+ if (device.keyId.isEmpty()) {
+ auto future = requestDeviceBundle(jid, deviceId);
+ await(future, q, [=](std::optional<QXmppOmemoDeviceBundle> optionalDeviceBundle) mutable {
+ // Process the device bundle only if one could be fetched and the
+ // corresponding device has not been removed by another method in
+ // the meantime.
+ if (optionalDeviceBundle && devices.value(jid).contains(deviceId)) {
+ auto &deviceBeingModified = devices[jid][deviceId];
+ const auto &deviceBundle = *optionalDeviceBundle;
+ const auto key = deviceBundle.publicIdentityKey();
+ deviceBeingModified.keyId = createKeyId(key);
+
+ auto future = q->trustLevel(jid, deviceBeingModified.keyId);
+ await(future, q, [=](TrustLevel trustLevel) mutable {
+ // Store the retrieved key's trust level if it is not stored
+ // yet.
+ if (trustLevel == TrustLevel::Undecided) {
+ auto future = storeKeyDependingOnSecurityPolicy(jid, key);
+ await(future, q, [=](TrustLevel trustLevel) mutable {
+ omemoStorage->addDevice(jid, deviceId, deviceBeingModified);
+ emit q->deviceChanged(jid, deviceId);
+ buildSessionDependingOnTrustLevel(deviceBundle, trustLevel);
+ });
+ } else {
+ omemoStorage->addDevice(jid, deviceId, deviceBeingModified);
+ emit q->deviceChanged(jid, deviceId);
+ buildSessionDependingOnTrustLevel(deviceBundle, trustLevel);
+ }
+ });
+ } else {
+ warning("OMEMO envelope could not be created because no device bundle could be fetched");
+ controlDeviceProcessing(false);
+ }
+ });
+ } else {
+ auto future = q->trustLevel(jid, device.keyId);
+ await(future, q, [=](TrustLevel trustLevel) mutable {
+ // Create only OMEMO envelopes for devices that have keys with
+ // specific trust levels.
+ if (acceptedTrustLevels.testFlag(trustLevel)) {
+ // Build a new session if none is stored.
+ // Otherwise, use the existing session.
+ if (device.session.isEmpty()) {
+ auto future = requestDeviceBundle(jid, deviceId);
+ await(future, q, [=](std::optional<QXmppOmemoDeviceBundle> optionalDeviceBundle) mutable {
+ if (optionalDeviceBundle) {
+ const auto &deviceBundle = *optionalDeviceBundle;
+ buildSessionDependingOnTrustLevel(deviceBundle, trustLevel);
+ } else {
+ warning("OMEMO envelope could not be created because no device bundle could be fetched");
+ controlDeviceProcessing(false);
+ }
+ });
+ } else {
+ addOmemoEnvelope();
+ }
+ } else {
+ q->debug("OMEMO envelope could not be created for JID '" % jid %
+ "' and device ID '" % QString::number(deviceId) %
+ "' because the device's key has an unaccepted trust level '" %
+ QString::number(int(trustLevel)) % "'");
+ controlDeviceProcessing(false);
+ }
+ });
+ }
+ }
+ }
+ } else {
+ warning("OMEMO element could not be created because no recipient devices could be found");
+ reportFinishedResult(interface, {});
+ }
+ } else {
+ warning("OMEMO payload could not be encrypted");
+ reportFinishedResult(interface, {});
+ }
+
+ return interface.future();
+}
+
+template QFuture<std::optional<QXmppOmemoElement>> ManagerPrivate::encryptStanza<QXmppIq>(const QXmppIq &, const QVector<QString> &, TrustLevels);
+template QFuture<std::optional<QXmppOmemoElement>> ManagerPrivate::encryptStanza<QXmppMessage>(const QXmppMessage &, const QVector<QString> &, TrustLevels);
+
+//
+// Encrypts a payload symmetrically.
+//
+// \param payload payload being symmetrically encrypted
+//
+// \return the data used for encryption and the result
+//
+std::optional<PayloadEncryptionResult> ManagerPrivate::encryptPayload(const QByteArray &payload) const
+{
+ auto hkdfKey = QCA::SecureArray(QCA::Random::randomArray(HKDF_KEY_SIZE));
+ const auto hkdfSalt = QCA::InitializationVector(QCA::SecureArray(HKDF_SALT_SIZE));
+ const auto hkdfInfo = QCA::InitializationVector(QCA::SecureArray(HKDF_INFO));
+ auto hkdfOutput = QCA::HKDF().makeKey(hkdfKey, hkdfSalt, hkdfInfo, HKDF_OUTPUT_SIZE);
+
+ // first part of hkdfKey
+ auto encryptionKey = QCA::SymmetricKey(hkdfOutput);
+ encryptionKey.resize(PAYLOAD_KEY_SIZE);
+
+ // middle part of hkdfKey
+ auto authenticationKey = QCA::SymmetricKey(PAYLOAD_AUTHENTICATION_KEY_SIZE);
+ const auto authenticationKeyOffset = hkdfOutput.data() + PAYLOAD_KEY_SIZE;
+ std::copy(authenticationKeyOffset, authenticationKeyOffset + PAYLOAD_AUTHENTICATION_KEY_SIZE, authenticationKey.data());
+
+ // last part of hkdfKey
+ auto initializationVector = QCA::InitializationVector(PAYLOAD_INITIALIZATION_VECTOR_SIZE);
+ const auto initializationVectorOffset = hkdfOutput.data() + PAYLOAD_KEY_SIZE + PAYLOAD_AUTHENTICATION_KEY_SIZE;
+ std::copy(initializationVectorOffset, initializationVectorOffset + PAYLOAD_INITIALIZATION_VECTOR_SIZE, initializationVector.data());
+
+ QCA::Cipher cipher(PAYLOAD_CIPHER_TYPE, PAYLOAD_CIPHER_MODE, PAYLOAD_CIPHER_PADDING, QCA::Encode, encryptionKey, initializationVector);
+ auto encryptedPayload = cipher.process(QCA::MemoryRegion(payload));
+
+ if (encryptedPayload.isEmpty()) {
+ warning("Following payload could not be encrypted: " % QString::fromUtf8(payload));
+ return {};
+ }
+
+ if (!QCA::MessageAuthenticationCode::supportedTypes().contains(PAYLOAD_MESSAGE_AUTHENTICATION_CODE_TYPE)) {
+ warning("Message authentication code type '" % QString(PAYLOAD_MESSAGE_AUTHENTICATION_CODE_TYPE) % "' is not supported by this system");
+ return {};
+ }
+
+ auto messageAuthenticationCodeGenerator = QCA::MessageAuthenticationCode(PAYLOAD_MESSAGE_AUTHENTICATION_CODE_TYPE, authenticationKey);
+ auto messageAuthenticationCode = QCA::SecureArray(messageAuthenticationCodeGenerator.process(encryptedPayload));
+ messageAuthenticationCode.resize(PAYLOAD_MESSAGE_AUTHENTICATION_CODE_SIZE);
+
+ PayloadEncryptionResult payloadEncryptionData;
+ payloadEncryptionData.decryptionData = hkdfKey.append(messageAuthenticationCode);
+ payloadEncryptionData.encryptedPayload = encryptedPayload.toByteArray();
+
+ return payloadEncryptionData;
+}
+
+//
+// Creates the SCE envelope as defined in \xep{0420, Stanza Content Encryption} for a message
+// or IQ stanza.
+//
+// The stanza's content that should be encrypted is put into the SCE content and that is added
+// to the SCE envelope.
+// Additionally, the standard SCE affix elements are added to the SCE envelope.
+//
+// \param stanza stanza for whom the SCE envelope is created
+//
+// \return the serialized SCE envelope
+//
+template<typename T>
+QByteArray ManagerPrivate::createSceEnvelope(const T &stanza)
+{
+ QByteArray serializedSceEnvelope;
+ QXmlStreamWriter writer(&serializedSceEnvelope);
+ QXmppSceEnvelopeWriter sceEnvelopeWriter(writer);
+ sceEnvelopeWriter.start();
+ sceEnvelopeWriter.writeTimestamp(QDateTime::currentDateTimeUtc());
+ sceEnvelopeWriter.writeTo(QXmppUtils::jidToBareJid(stanza.to()));
+ sceEnvelopeWriter.writeFrom(q->client()->configuration().jidBare());
+ sceEnvelopeWriter.writeRpad(generateRandomBytes(SCE_RPAD_SIZE_MIN, SCE_RPAD_SIZE_MAX).toBase64());
+ sceEnvelopeWriter.writeContent([&writer, &stanza] {
+ if constexpr (std::is_same_v<T, QXmppMessage>) {
+ stanza.serializeExtensions(&writer, SceSensitive, ns_client);
+ } else {
+ // If the IQ stanza contains an error (i.e., it is an error response), that error is
+ // serialized instead of actual content.
+ const auto error = stanza.error();
+ if (error.typeOpt()) {
+ error.toXml(&writer);
+ } else {
+ stanza.toXmlElementFromChild(&writer);
+ }
+ }
+ });
+ sceEnvelopeWriter.end();
+
+ return serializedSceEnvelope;
+}
+
+//
+// Creates the data of an OMEMO envelope.
+//
+// Encrypts the data used for a symmetric encryption of a payload asymmetrically with the
+// recipient device's key.
+//
+// \param address address of a recipient device
+// \param payloadDecryptionData data used for symmetric encryption being asymmetrically
+// encrypted
+//
+// \return the encrypted and serialized OMEMO envelope data or a default-constructed byte array
+// on failure
+//
+QByteArray ManagerPrivate::createOmemoEnvelopeData(const signal_protocol_address &address, const QCA::SecureArray &payloadDecryptionData) const
+{
+ SessionCipherPtr sessionCipher;
+
+ if (session_cipher_create(sessionCipher.ptrRef(), storeContext.get(), &address, globalContext.get()) < 0) {
+ warning("Session cipher could not be created");
+ return {};
+ }
+
+ session_cipher_set_version(sessionCipher.get(), CIPHERTEXT_OMEMO_VERSION);
+
+ RefCountedPtr<ciphertext_message> encryptedOmemoEnvelopeData;
+ if (session_cipher_encrypt(sessionCipher.get(), reinterpret_cast<const uint8_t *>(payloadDecryptionData.constData()), payloadDecryptionData.size(), encryptedOmemoEnvelopeData.ptrRef()) != SG_SUCCESS) {
+ warning("Payload decryption data could not be encrypted");
+ return {};
+ }
+
+ signal_buffer *serializedEncryptedOmemoEnvelopeData = ciphertext_message_get_serialized(encryptedOmemoEnvelopeData.get());
+
+ return {
+ reinterpret_cast<const char *>(signal_buffer_data(serializedEncryptedOmemoEnvelopeData)),
+ int(signal_buffer_len(serializedEncryptedOmemoEnvelopeData))
+ };
+}
+
+//
+// Decrypts a message stanza.
+//
+// In case of an empty (i.e., without payload) OMEMO message for session initiation, only the
+// dummy payload's decryption data is decrypted but no payload.
+// In case of a normal OMEMO message (i.e., with payload), the payload is decrypted and set as
+// the content (i.e., first child element) of the returned stanza.
+//
+// \param stanza message stanza to be decrypted
+//
+// \return the decrypted stanza if it could be decrypted
+//
+QFuture<std::optional<QXmppMessage>> ManagerPrivate::decryptMessage(QXmppMessage stanza)
+{
+ QFutureInterface<std::optional<QXmppMessage>> interface(QFutureInterfaceBase::Started);
+
+ // At this point, the stanza has always an OMEMO element.
+ const auto omemoElement = *stanza.omemoElement();
+
+ if (auto optionalOmemoEnvelope = omemoElement.searchEnvelope(ownBareJid(), ownDevice.id)) {
+ const auto senderJid = QXmppUtils::jidToBareJid(stanza.from());
+ const auto senderDeviceId = omemoElement.senderDeviceId();
+ const auto omemoEnvelope = *optionalOmemoEnvelope;
+ const auto omemoPayload = omemoElement.payload();
+
+ subscribeToNewDeviceLists(senderJid, senderDeviceId);
+
+ // Process empty OMEMO messages sent by a receiver of this device's first OMEMO message
+ // for it after building the initial session or sent by devices to build a new session
+ // with this device.
+ if (omemoPayload.isEmpty()) {
+ auto future = extractPayloadDecryptionData(senderJid, senderDeviceId, omemoEnvelope);
+ await(future, q, [=](QCA::SecureArray payloadDecryptionData) mutable {
+ if (payloadDecryptionData.isEmpty()) {
+ warning("Empty OMEMO message could not be successfully processed");
+ } else {
+ q->debug("Successfully processed empty OMEMO message");
+ }
+
+ reportFinishedResult(interface, {});
+ });
+ } else {
+ auto future = decryptStanza(stanza, senderJid, senderDeviceId, omemoEnvelope, omemoPayload);
+ await(future, q, [=](std::optional<DecryptionResult> optionalDecryptionResult) mutable {
+ if (optionalDecryptionResult) {
+ const auto decryptionResult = std::move(*optionalDecryptionResult);
+ stanza.parseExtensions(decryptionResult.sceContent, SceSensitive);
+
+ // Remove the OMEMO element from the message because it is not needed
+ // anymore after decryption.
+ stanza.setOmemoElement({});
+
+ stanza.setE2eeMetadata(decryptionResult.e2eeMetadata);
+
+ reportFinishedResult(interface, { stanza });
+ } else {
+ reportFinishedResult(interface, {});
+ }
+ });
+ }
+ }
+
+ return interface.future();
+}
+
+//
+// Decrypts an IQ stanza.
+//
+// The payload is decrypted and set as the content (i.e., first child element) of the returned
+// stanza.
+//
+// \param iqElement DOM element of the IQ stanza to be decrypted. It MUST be an QXmppOmemoIq.
+//
+// \return the serialized decrypted stanza if it could be decrypted
+//
+QFuture<std::optional<IqDecryptionResult>> ManagerPrivate::decryptIq(const QDomElement &iqElement)
+{
+ using Result = std::optional<IqDecryptionResult>;
+
+ QXmppOmemoIq iq;
+ iq.parse(iqElement);
+ auto omemoElement = iq.omemoElement();
+
+ if (const auto envelope = omemoElement.searchEnvelope(ownBareJid(), ownDevice.id)) {
+ const auto senderJid = QXmppUtils::jidToBareJid(iq.from());
+ const auto senderDeviceId = omemoElement.senderDeviceId();
+
+ subscribeToNewDeviceLists(senderJid, senderDeviceId);
+
+ auto future = decryptStanza(iq, senderJid, senderDeviceId, *envelope, omemoElement.payload(), false);
+ return chain<Result>(future, q, [iqElement](auto result) -> Result {
+ if (result) {
+ auto decryptedElement = iqElement.cloneNode(true).toElement();
+ replaceChildElements(decryptedElement, result->sceContent);
+
+ return IqDecryptionResult { decryptedElement, result->e2eeMetadata };
+ }
+ return {};
+ });
+ }
+ return makeReadyFuture<Result>(std::nullopt);
+}
+
+//
+// Decrypts a message or IQ stanza.
+//
+// In case of an empty (i.e., without payload) OMEMO message for session initiation, only the
+// dummy payload decryption data is decrypted but no payload.
+// In case of a normal OMEMO stanza (i.e., with payload), the payload is decrypted and set as
+// the content (i.e., first child element) of the returned stanza.
+//
+// \param stanza message or IQ stanza being decrypted
+// \param senderJid JID of the stanza's sender
+// \param senderDeviceId device ID of the stanza's sender
+// \param omemoEnvelope OMEMO envelope within the OMEMO element
+// \param omemoPayload OMEMO payload within the OMEMO element
+// \param isMessageStanza whether the received stanza is a message stanza
+//
+// \return the result of the decryption if it succeeded
+//
+template<typename T>
+QFuture<std::optional<DecryptionResult>> ManagerPrivate::decryptStanza(T stanza, const QString &senderJid, uint32_t senderDeviceId, const QXmppOmemoEnvelope &omemoEnvelope, const QByteArray &omemoPayload, bool isMessageStanza)
+{
+ QFutureInterface<std::optional<DecryptionResult>> interface(QFutureInterfaceBase::Started);
+
+ auto future = extractSceEnvelope(senderJid, senderDeviceId, omemoEnvelope, omemoPayload, isMessageStanza);
+ await(future, q, [=](QByteArray serializedSceEnvelope) mutable {
+ if (serializedSceEnvelope.isEmpty()) {
+ warning("SCE envelope could not be extracted");
+ reportFinishedResult(interface, {});
+ } else {
+ QDomDocument document;
+ document.setContent(serializedSceEnvelope, true);
+ QXmppSceEnvelopeReader sceEnvelopeReader(document.documentElement());
+
+ if (sceEnvelopeReader.from() != senderJid) {
+ warning("Sender '" % senderJid % "' of stanza does not match SCE 'from' affix element '" % sceEnvelopeReader.from() % "'");
+ reportFinishedResult(interface, {});
+ } else {
+ const auto recipientJid = QXmppUtils::jidToBareJid(stanza.to());
+ auto isSceAffixElementValid = true;
+
+ if (isMessageStanza) {
+ if (const auto &message = dynamic_cast<const QXmppMessage &>(stanza); message.type() == QXmppMessage::GroupChat && (sceEnvelopeReader.to() != recipientJid)) {
+ warning("Recipient of group chat message does not match SCE affix element '<to/>'");
+ isSceAffixElementValid = false;
+ }
+ } else {
+ if (sceEnvelopeReader.to() != recipientJid) {
+ warning("Recipient of IQ does not match SCE affix element '<to/>'");
+ isSceAffixElementValid = false;
+ }
+ }
+
+ if (!isSceAffixElementValid) {
+ reportFinishedResult(interface, {});
+ } else {
+ auto &device = devices[senderJid][senderDeviceId];
+ device.unrespondedSentStanzasCount = 0;
+
+ // Send a heartbeat message to the sender if too many stanzas were
+ // received responding to none.
+ if (device.unrespondedReceivedStanzasCount == UNRESPONDED_STANZAS_UNTIL_HEARTBEAT_MESSAGE_IS_SENT) {
+ sendEmptyMessage(senderJid, senderDeviceId);
+ device.unrespondedReceivedStanzasCount = 0;
+ } else {
+ ++device.unrespondedReceivedStanzasCount;
+ }
+
+ QXmppE2eeMetadata e2eeMetadata;
+ e2eeMetadata.setSceTimestamp(sceEnvelopeReader.timestamp());
+ e2eeMetadata.setEncryption(QXmpp::Omemo2);
+ const auto &senderDevice = devices.value(senderJid).value(senderDeviceId);
+ e2eeMetadata.setSenderKey(senderDevice.keyId);
+
+ reportFinishedResult(interface, { { sceEnvelopeReader.contentElement(), e2eeMetadata } });
+ }
+ }
+ }
+ });
+
+ return interface.future();
+}
+
+//
+// Extracts the SCE envelope from an OMEMO payload.
+//
+// The data used to encrypt the payload is decrypted and then used to decrypt the payload which
+// contains the SCE envelope.
+//
+// \param senderJid bare JID of the stanza's sender
+// \param senderDeviceId device ID of the stanza's sender
+// \param omemoEnvelope OMEMO envelope containing the payload decryption data
+// \param omemoPayload OMEMO payload containing the SCE envelope
+// \param isMessageStanza whether the received stanza is a message stanza
+//
+// \return the serialized SCE envelope if it could be extracted, otherwise a
+// default-constructed byte array
+//
+QFuture<QByteArray> ManagerPrivate::extractSceEnvelope(const QString &senderJid, uint32_t senderDeviceId, const QXmppOmemoEnvelope &omemoEnvelope, const QByteArray &omemoPayload, bool isMessageStanza)
+{
+ QFutureInterface<QByteArray> interface(QFutureInterfaceBase::Started);
+
+ auto future = extractPayloadDecryptionData(senderJid, senderDeviceId, omemoEnvelope, isMessageStanza);
+ await(future, q, [=](QCA::SecureArray payloadDecryptionData) mutable {
+ if (payloadDecryptionData.isEmpty()) {
+ warning("Data for decrypting OMEMO payload could not be extracted");
+ reportFinishedResult(interface, {});
+ } else {
+ reportFinishedResult(interface, decryptPayload(payloadDecryptionData, omemoPayload));
+ }
+ });
+
+ return interface.future();
+}
+
+//
+// Extracts the data used to decrypt the OMEMO payload.
+//
+// Decrypts the the payload decryption data and handles the OMEMO sessions.
+//
+// \param senderJid bare JID of the stanza's sender
+// \param senderDeviceId device ID of the stanza's sender
+// \param omemoEnvelope OMEMO envelope containing the payload decryption data
+// \param isMessageStanza whether the received stanza is a message stanza
+//
+// \return the serialized payload decryption data if it could be extracted, otherwise a
+// default-constructed secure array
+//
+QFuture<QCA::SecureArray> ManagerPrivate::extractPayloadDecryptionData(const QString &senderJid, uint32_t senderDeviceId, const QXmppOmemoEnvelope &omemoEnvelope, bool isMessageStanza)
+{
+ QFutureInterface<QCA::SecureArray> interface(QFutureInterfaceBase::Started);
+
+ SessionCipherPtr sessionCipher;
+ const auto address = Address(senderJid, senderDeviceId);
+ const auto addressData = address.data();
+
+ if (session_cipher_create(sessionCipher.ptrRef(), storeContext.get(), &addressData, globalContext.get()) < 0) {
+ warning("Session cipher could not be created");
+ return {};
+ }
+
+ session_cipher_set_version(sessionCipher.get(), CIPHERTEXT_OMEMO_VERSION);
+
+ BufferSecurePtr payloadDecryptionDataBuffer;
+
+ auto reportResult = [=](const BufferSecurePtr &buffer) mutable {
+ // The buffer is copied into the SecureArray to avoid a QByteArray which is not secure.
+ // However, it would be simpler if SecureArray had an appropriate constructor for that.
+ const auto payloadDecryptionDataPointer = signal_buffer_data(buffer.get());
+ const auto payloadDecryptionDataBufferSize = signal_buffer_len(buffer.get());
+ auto payloadDecryptionData = QCA::SecureArray(payloadDecryptionDataBufferSize);
+ std::copy_n(payloadDecryptionDataPointer, payloadDecryptionDataBufferSize, payloadDecryptionData.data());
+
+ reportFinishedResult(interface, payloadDecryptionData);
+ };
+
+ // There are three cases:
+ // 1. If the stanza contains key exchange data, a new session is automatically built by the
+ // OMEMO library during decryption.
+ // 2. If the stanza does not contain key exchange data and there is no existing session, the
+ // stanza cannot be decrypted but a new session is built for future communication.
+ // 3. If the stanza does not contain key exchange data and there is an existing session,
+ // that session is used to decrypt the stanza.
+ if (omemoEnvelope.isUsedForKeyExchange()) {
+ RefCountedPtr<pre_key_signal_message> omemoEnvelopeData;
+ const auto serializedOmemoEnvelopeData = omemoEnvelope.data();
+
+ if (pre_key_signal_message_deserialize_omemo(omemoEnvelopeData.ptrRef(),
+ reinterpret_cast<const uint8_t *>(serializedOmemoEnvelopeData.data()),
+ serializedOmemoEnvelopeData.size(),
+ senderDeviceId,
+ globalContext.get()) < 0) {
+ warning("OMEMO envelope data could not be deserialized");
+ reportFinishedResult(interface, {});
+ } else {
+ BufferPtr publicIdentityKeyBuffer;
+
+ if (ec_public_key_serialize(publicIdentityKeyBuffer.ptrRef(), pre_key_signal_message_get_identity_key(omemoEnvelopeData.get())) < 0) {
+ warning("Public Identity key could not be retrieved");
+ reportFinishedResult(interface, {});
+ } else {
+ const auto key = publicIdentityKeyBuffer.toByteArray();
+ auto &device = devices[senderJid][senderDeviceId];
+ auto &storedKeyId = device.keyId;
+ const auto createdKeyId = createKeyId(key);
+
+ // Store the key if its ID has changed.
+ if (storedKeyId != createdKeyId) {
+ storedKeyId = createdKeyId;
+ omemoStorage->addDevice(senderJid, senderDeviceId, device);
+ emit q->deviceChanged(senderJid, senderDeviceId);
+ }
+
+ // Decrypt the OMEMO envelope data and build a session.
+ switch (session_cipher_decrypt_pre_key_signal_message(sessionCipher.get(), omemoEnvelopeData.get(), nullptr, payloadDecryptionDataBuffer.ptrRef())) {
+ case SG_ERR_INVALID_MESSAGE:
+ warning("OMEMO envelope data for key exchange is not valid");
+ reportFinishedResult(interface, {});
+ break;
+ case SG_ERR_DUPLICATE_MESSAGE:
+ warning("OMEMO envelope data for key exchange is already received");
+ reportFinishedResult(interface, {});
+ break;
+ case SG_ERR_LEGACY_MESSAGE:
+ warning("OMEMO envelope data for key exchange format is deprecated");
+ reportFinishedResult(interface, {});
+ break;
+ case SG_ERR_INVALID_KEY_ID: {
+ const auto preKeyId = QString::number(pre_key_signal_message_get_pre_key_id(omemoEnvelopeData.get()));
+ warning("Pre key with ID '" % preKeyId %
+ "' of OMEMO envelope data for key exchange could not be found locally");
+ reportFinishedResult(interface, {});
+ break;
+ }
+ case SG_ERR_INVALID_KEY:
+ warning("OMEMO envelope data for key exchange is incorrectly formatted");
+ reportFinishedResult(interface, {});
+ break;
+ case SG_ERR_UNTRUSTED_IDENTITY:
+ warning("Identity key of OMEMO envelope data for key exchange is not trusted by OMEMO library");
+ reportFinishedResult(interface, {});
+ break;
+ case SG_SUCCESS:
+ reportResult(payloadDecryptionDataBuffer);
+
+ // Send an empty message back to the sender in order to notify the sender's
+ // device that the session initiation is completed.
+ // Do not send an empty message if the received stanza is an IQ stanza
+ // because a response is already directly sent.
+ if (isMessageStanza) {
+ sendEmptyMessage(senderJid, senderDeviceId);
+ }
+
+ // Store the key's trust level if it is not stored yet.
+ auto future = q->trustLevel(senderJid, storedKeyId);
+ await(future, q, [=](TrustLevel trustLevel) mutable {
+ if (trustLevel == TrustLevel::Undecided) {
+ auto future = storeKeyDependingOnSecurityPolicy(senderJid, key);
+ await(future, q, [=](auto) mutable {
+ interface.reportFinished();
+ });
+ } else {
+ interface.reportFinished();
+ }
+ });
+ }
+ }
+ }
+ } else if (auto &device = devices[senderJid][senderDeviceId]; device.session.isEmpty()) {
+ warning("Received OMEMO stanza cannot be decrypted because there is no session with "
+ "sending device, new session is being built");
+
+ auto future = buildSessionWithDeviceBundle(senderJid, senderDeviceId, device);
+ await(future, q, [=](auto) mutable {
+ reportFinishedResult(interface, {});
+ });
+ } else {
+ RefCountedPtr<signal_message> omemoEnvelopeData;
+ const auto serializedOmemoEnvelopeData = omemoEnvelope.data();
+
+ if (signal_message_deserialize_omemo(omemoEnvelopeData.ptrRef(), reinterpret_cast<const uint8_t *>(serializedOmemoEnvelopeData.data()), serializedOmemoEnvelopeData.size(), globalContext.get()) < 0) {
+ warning("OMEMO envelope data could not be deserialized");
+ reportFinishedResult(interface, {});
+ } else {
+ // Decrypt the OMEMO envelope data.
+ switch (session_cipher_decrypt_signal_message(sessionCipher.get(), omemoEnvelopeData.get(), nullptr, payloadDecryptionDataBuffer.ptrRef())) {
+ case SG_ERR_INVALID_MESSAGE:
+ warning("OMEMO envelope data is not valid");
+ reportFinishedResult(interface, {});
+ break;
+ case SG_ERR_DUPLICATE_MESSAGE:
+ warning("OMEMO envelope data is already received");
+ reportFinishedResult(interface, {});
+ break;
+ case SG_ERR_LEGACY_MESSAGE:
+ warning("OMEMO envelope data format is deprecated");
+ reportFinishedResult(interface, {});
+ break;
+ case SG_ERR_NO_SESSION:
+ warning("Session for OMEMO envelope data could not be found");
+ reportFinishedResult(interface, {});
+ case SG_SUCCESS:
+ reportResult(payloadDecryptionDataBuffer);
+ }
+ }
+ }
+
+ return interface.future();
+}
+
+//
+// Decrypts the OMEMO payload.
+//
+// \param payloadDecryptionData data needed to decrypt the payload
+// \param payload payload to be decrypted
+//
+// \return the decrypted payload or a default-constructed byte array on failure
+//
+QByteArray ManagerPrivate::decryptPayload(const QCA::SecureArray &payloadDecryptionData, const QByteArray &payload) const
+{
+ auto hkdfKey = QCA::SecureArray(payloadDecryptionData);
+ hkdfKey.resize(HKDF_KEY_SIZE);
+ const auto hkdfSalt = QCA::InitializationVector(QCA::SecureArray(HKDF_SALT_SIZE));
+ const auto hkdfInfo = QCA::InitializationVector(QCA::SecureArray(HKDF_INFO));
+ auto hkdfOutput = QCA::HKDF().makeKey(hkdfKey, hkdfSalt, hkdfInfo, HKDF_OUTPUT_SIZE);
+
+ // first part of hkdfKey
+ auto encryptionKey = QCA::SymmetricKey(hkdfOutput);
+ encryptionKey.resize(PAYLOAD_KEY_SIZE);
+
+ // middle part of hkdfKey
+ auto authenticationKey = QCA::SymmetricKey(PAYLOAD_AUTHENTICATION_KEY_SIZE);
+ const auto authenticationKeyOffset = hkdfOutput.data() + PAYLOAD_KEY_SIZE;
+ std::copy(authenticationKeyOffset, authenticationKeyOffset + PAYLOAD_AUTHENTICATION_KEY_SIZE, authenticationKey.data());
+
+ // last part of hkdfKey
+ auto initializationVector = QCA::InitializationVector(PAYLOAD_INITIALIZATION_VECTOR_SIZE);
+ const auto initializationVectorOffset = hkdfOutput.data() + PAYLOAD_KEY_SIZE + PAYLOAD_AUTHENTICATION_KEY_SIZE;
+ std::copy(initializationVectorOffset, initializationVectorOffset + PAYLOAD_INITIALIZATION_VECTOR_SIZE, initializationVector.data());
+
+ if (!QCA::MessageAuthenticationCode::supportedTypes().contains(PAYLOAD_MESSAGE_AUTHENTICATION_CODE_TYPE)) {
+ warning("Message authentication code type '" % QString(PAYLOAD_MESSAGE_AUTHENTICATION_CODE_TYPE) % "' is not supported by this system");
+ return {};
+ }
+
+ auto messageAuthenticationCodeGenerator = QCA::MessageAuthenticationCode(PAYLOAD_MESSAGE_AUTHENTICATION_CODE_TYPE, authenticationKey);
+ auto messageAuthenticationCode = QCA::SecureArray(messageAuthenticationCodeGenerator.process(payload));
+ messageAuthenticationCode.resize(PAYLOAD_MESSAGE_AUTHENTICATION_CODE_SIZE);
+
+ auto expectedMessageAuthenticationCode = QCA::SecureArray(payloadDecryptionData.toByteArray().right(PAYLOAD_MESSAGE_AUTHENTICATION_CODE_SIZE));
+
+ if (messageAuthenticationCode != expectedMessageAuthenticationCode) {
+ warning("Message authentication code does not match expected one");
+ return {};
+ }
+
+ QCA::Cipher cipher(PAYLOAD_CIPHER_TYPE, PAYLOAD_CIPHER_MODE, PAYLOAD_CIPHER_PADDING, QCA::Decode, encryptionKey, initializationVector);
+ auto decryptedPayload = cipher.process(QCA::MemoryRegion(payload));
+
+ if (decryptedPayload.isEmpty()) {
+ warning("Following payload could not be decrypted: " % QString(payload));
+ return {};
+ }
+
+ return decryptedPayload.toByteArray();
+}
+
+//
+// Publishes the OMEMO data for this device.
+//
+// \return whether it succeeded
+//
+QFuture<bool> ManagerPrivate::publishOmemoData()
+{
+ QFutureInterface<bool> interface(QFutureInterfaceBase::Started);
+
+ auto future = pubSubManager->requestPepFeatures();
+ await(future, q, [=](QXmppPubSubManager::FeaturesResult result) mutable {
+ if (const auto error = std::get_if<Error>(&result)) {
+ warning("Features of PEP service '" % ownBareJid() % "' could not be retrieved" % errorToString(*error));
+ warning("Device bundle and device list could not be published");
+ reportFinishedResult(interface, false);
+ } else {
+ const auto &pepServiceFeatures = std::get<QVector<QString>>(result);
+
+ // Check if the PEP service supports publishing items at all and also publishing
+ // multiple items.
+ // The support for publishing multiple items is needed to publish multiple device
+ // bundles to the corresponding node.
+ // It is checked here because if that is not possible, the publication of the device
+ // element must not be published.
+ // TODO: Uncomment the following line and remove the other one once ejabberd released version > 21.12
+ // if (pepServiceFeatures.contains(ns_pubsub_publish) && pepServiceFeatures.contains(ns_pubsub_multi_items)) {
+ if (pepServiceFeatures.contains(ns_pubsub_publish)) {
+ auto future = pubSubManager->fetchPepNodes();
+ await(future, q, [=](QXmppPubSubManager::NodesResult result) mutable {
+ if (const auto error = std::get_if<Error>(&result)) {
+ warning("Nodes of JID '" % ownBareJid() % "' could not be fetched to check if nodes '" %
+ QString(ns_omemo_2_bundles) % "' and '" % QString(ns_omemo_2_devices) %
+ "' exist" % errorToString(*error));
+ warning("Device bundle and device list could not be published");
+ reportFinishedResult(interface, false);
+ } else {
+ const auto &nodes = std::get<QVector<QString>>(result);
+
+ const auto deviceListNodeExists = nodes.contains(ns_omemo_2_devices);
+ const auto arePublishOptionsSupported = pepServiceFeatures.contains(ns_pubsub_publish_options);
+ const auto isAutomaticCreationSupported = pepServiceFeatures.contains(ns_pubsub_auto_create);
+ const auto isCreationAndConfigurationSupported = pepServiceFeatures.contains(ns_pubsub_create_and_configure);
+ const auto isCreationSupported = pepServiceFeatures.contains(ns_pubsub_create_nodes);
+ const auto isConfigurationSupported = pepServiceFeatures.contains(ns_pubsub_config_node);
+
+ // The device bundle is published before the device data is published.
+ // That way, it ensures that other devices are notified about this new
+ // device only after the corresponding device bundle is published.
+ auto handleResult = [=, this](bool isPublished) mutable {
+ if (isPublished) {
+ publishDeviceElement(deviceListNodeExists,
+ arePublishOptionsSupported,
+ isAutomaticCreationSupported,
+ isCreationAndConfigurationSupported,
+ isCreationSupported,
+ isConfigurationSupported,
+ [=](bool isPublished) mutable {
+ if (!isPublished) {
+ warning("Device element could not be published");
+ }
+ reportFinishedResult(interface, isPublished);
+ });
+ } else {
+ warning("Device bundle could not be published");
+ reportFinishedResult(interface, false);
+ }
+ };
+ publishDeviceBundle(nodes.contains(ns_omemo_2_bundles),
+ arePublishOptionsSupported,
+ isAutomaticCreationSupported,
+ isCreationAndConfigurationSupported,
+ isCreationSupported,
+ isConfigurationSupported,
+ pepServiceFeatures.contains(ns_pubsub_config_node_max),
+ handleResult);
+ }
+ });
+ } else {
+ warning("Publishing (multiple) items to PEP node '" % ownBareJid() % "' is not supported");
+ warning("Device bundle and device list could not be published");
+ reportFinishedResult(interface, false);
+ }
+ }
+ });
+
+ return interface.future();
+}
+
+//
+// Publishes this device's bundle.
+//
+// If no node for device bundles exists, a new one is created.
+//
+// \param isDeviceBundlesNodeExistent whether the PEP node for device bundles exists
+// \param arePublishOptionsSupported whether publish options are supported by the PEP service
+// \param isAutomaticCreationSupported whether the PEP service supports the automatic creation
+// of nodes when new items are published
+// \param isCreationAndConfigurationSupported whether the PEP service supports the
+// configuration of nodes during their creation
+// \param isCreationSupported whether the PEP service supports creating nodes
+// \param isConfigurationSupported whether the PEP service supports configuring existing
+// nodes
+// \param isConfigNodeMaxSupported whether the PEP service supports to set the maximum number
+// of allowed items per node to the maximum it supports
+// \param continuation function to be called with the bool value whether it succeeded
+//
+template<typename Function>
+void ManagerPrivate::publishDeviceBundle(bool isDeviceBundlesNodeExistent,
+ bool arePublishOptionsSupported,
+ bool isAutomaticCreationSupported,
+ bool isCreationAndConfigurationSupported,
+ bool isCreationSupported,
+ bool isConfigurationSupported,
+ bool isConfigNodeMaxSupported,
+ Function continuation)
+{
+ // Check if the PEP service supports configuration of nodes during publication of items.
+ if (arePublishOptionsSupported) {
+ if (isAutomaticCreationSupported || isDeviceBundlesNodeExistent) {
+ // The supported publish options cannot be determined because they are not announced
+ // via Service Discovery.
+ // Especially, there is no feature like ns_pubsub_multi_items and no error case
+ // specified for the usage of
+ // QXmppPubSubNodeConfig::ItemLimit as a publish option.
+ // Thus, it simply tries to publish the item with that publish option.
+ // If that fails, it tries to manually create and configure the node and publish the
+ // item.
+ publishDeviceBundleItemWithOptions([=](bool isPublished) mutable {
+ if (isPublished) {
+ continuation(true);
+ } else {
+ auto handleResult = [this, continuation = std::move(continuation)](bool isPublished) mutable {
+ if (!isPublished) {
+ q->debug("PEP service '" % ownBareJid() %
+ "' does not support feature '" %
+ QString(ns_pubsub_publish_options) %
+ "' for all publish options, also not '" %
+ QString(ns_pubsub_create_and_configure) %
+ "', '" % QString(ns_pubsub_create_nodes) % "', '" %
+ QString(ns_pubsub_config_node) % "' and the node does not exist");
+ }
+ continuation(isPublished);
+ };
+ publishDeviceBundleWithoutOptions(isDeviceBundlesNodeExistent,
+ isCreationAndConfigurationSupported,
+ isCreationSupported,
+ // TODO: Uncomment the following line and remove the other one once ejabberd released version > 21.12
+ // isConfigurationSupported,
+ true,
+ isConfigNodeMaxSupported,
+ handleResult);
+ }
+ });
+ } else if (isCreationSupported) {
+ // Create a node manually if the PEP service does not support creation of nodes
+ // during publication of items and no node already
+ // exists.
+ createDeviceBundlesNode([=](bool isCreated) mutable {
+ if (isCreated) {
+ // The supported publish options cannot be determined because they are not
+ // announced via Service Discovery.
+ // Especially, there is no feature like ns_pubsub_multi_items and no error
+ // case specified for the usage of QXmppPubSubNodeConfig::ItemLimit as a
+ // publish option.
+ // Thus, it simply tries to publish the item with that publish option.
+ // If that fails, it tries to manually configure the node and publish the
+ // item.
+ publishDeviceBundleItemWithOptions([=](bool isPublished) mutable {
+ if (isPublished) {
+ continuation(true);
+ } else if (isConfigurationSupported) {
+ configureNodeAndPublishDeviceBundle(isConfigNodeMaxSupported, continuation);
+ } else {
+ q->debug("PEP service '" % ownBareJid() %
+ "' does not support feature '" %
+ QString(ns_pubsub_publish_options) %
+ "' for all publish options and also not '" %
+ QString(ns_pubsub_config_node) % "'");
+ continuation(false);
+ }
+ });
+ } else {
+ continuation(false);
+ }
+ });
+ } else {
+ q->debug("PEP service '" % ownBareJid() % "' does not support features '" %
+ QString(ns_pubsub_auto_create) % "', '" % QString(ns_pubsub_create_nodes) %
+ "' and the node does not exist");
+ continuation(false);
+ }
+ } else {
+ auto handleResult = [this, continuation = std::move(continuation)](bool isPublished) mutable {
+ if (!isPublished) {
+ q->debug("PEP service '" % ownBareJid() % "' does not support features '" %
+ QString(ns_pubsub_publish_options) % "', '" %
+ QString(ns_pubsub_create_and_configure) % "', '" %
+ QString(ns_pubsub_create_nodes) % "', '" %
+ QString(ns_pubsub_config_node) % "' and the node does not exist");
+ }
+ continuation(isPublished);
+ };
+ publishDeviceBundleWithoutOptions(isDeviceBundlesNodeExistent,
+ isCreationAndConfigurationSupported,
+ isCreationSupported,
+ // TODO: Uncomment the following line and remove the other one once ejabberd released version > 21.12
+ // isConfigurationSupported,
+ true,
+ isConfigNodeMaxSupported,
+ handleResult);
+ }
+}
+
+//
+// Publish this device's bundle without publish options.
+//
+// If no node for device bundles exists, a new one is created.
+//
+// \param isDeviceBundlesNodeExistent whether the PEP node for device bundles exists
+// \param isCreationAndConfigurationSupported whether the PEP service supports the
+// configuration of nodes during their creation
+// \param isCreationSupported whether the PEP service supports creating nodes
+// \param isConfigurationSupported whether the PEP service supports configuring existing
+// nodes
+// \param isConfigNodeMaxSupported whether the PEP service supports to set the maximum number
+// of allowed items per node to the maximum it supports
+// \param continuation function to be called with the bool value whether it succeeded
+//
+template<typename Function>
+void ManagerPrivate::publishDeviceBundleWithoutOptions(bool isDeviceBundlesNodeExistent,
+ bool isCreationAndConfigurationSupported,
+ bool isCreationSupported,
+ bool isConfigurationSupported,
+ bool isConfigNodeMaxSupported,
+ Function continuation)
+{
+ if (isDeviceBundlesNodeExistent && isConfigurationSupported) {
+ configureNodeAndPublishDeviceBundle(isConfigNodeMaxSupported, continuation);
+ } else if (isCreationAndConfigurationSupported) {
+ createAndConfigureDeviceBundlesNode(isConfigNodeMaxSupported, [=](bool isCreatedAndConfigured) mutable {
+ if (isCreatedAndConfigured) {
+ publishDeviceBundleItem(continuation);
+ } else {
+ continuation(false);
+ }
+ });
+ } else if (isCreationSupported && isConfigurationSupported) {
+ createDeviceBundlesNode([=](bool isCreated) mutable {
+ if (isCreated) {
+ configureNodeAndPublishDeviceBundle(isConfigNodeMaxSupported, continuation);
+ } else {
+ continuation(false);
+ }
+ });
+ } else {
+ continuation(false);
+ }
+}
+
+//
+// Configures the existing PEP node for device bundles and publishes this device's bundle on it.
+//
+// \param isConfigNodeMaxSupported whether the PEP service supports to set the maximum number
+// of allowed items per node to the maximum it supports
+// \param continuation function to be called with the bool value whether it succeeded
+//
+template<typename Function>
+void ManagerPrivate::configureNodeAndPublishDeviceBundle(bool isConfigNodeMaxSupported, Function continuation)
+{
+ configureDeviceBundlesNode(isConfigNodeMaxSupported, [=](bool isConfigured) mutable {
+ if (isConfigured) {
+ publishDeviceBundleItem(continuation);
+ } else {
+ continuation(false);
+ }
+ });
+}
+
+//
+// Creates a PEP node for device bundles and configures it accordingly.
+//
+// \param isConfigNodeMaxSupported whether the PEP service supports to set the maximum number
+// of allowed items per node to the maximum it supports
+// \param continuation function to be called with the bool value whether it succeeded
+//
+template<typename Function>
+void ManagerPrivate::createAndConfigureDeviceBundlesNode(bool isConfigNodeMaxSupported, Function continuation)
+{
+ if (isConfigNodeMaxSupported) {
+ createNode(ns_omemo_2_bundles, deviceBundlesNodeConfig(), continuation);
+ } else {
+ createNode(ns_omemo_2_bundles, deviceBundlesNodeConfig(PUBSUB_NODE_MAX_ITEMS_1), [=](bool isCreated) mutable {
+ if (isCreated) {
+ continuation(true);
+ } else {
+ createNode(ns_omemo_2_bundles, deviceBundlesNodeConfig(PUBSUB_NODE_MAX_ITEMS_2), [=](bool isCreated) mutable {
+ if (isCreated) {
+ continuation(true);
+ } else {
+ createNode(ns_omemo_2_bundles, deviceBundlesNodeConfig(PUBSUB_NODE_MAX_ITEMS_3), continuation);
+ }
+ });
+ }
+ });
+ }
+}
+
+//
+// Creates a PEP node for device bundles.
+//
+// \param continuation function to be called with the bool value whether it succeeded
+//
+template<typename Function>
+void ManagerPrivate::createDeviceBundlesNode(Function continuation)
+{
+ createNode(ns_omemo_2_bundles, continuation);
+}
+
+//
+// Configures an existing PEP node for device bundles.
+//
+// There is no feature (like ns_pubsub_config_node_max as a config option) and no error case
+// specified for the usage of \c QXmppPubSubNodeConfig::Max() as the value for the config
+// option \c QXmppPubSubNodeConfig::ItemLimit.
+// Thus, it tries to configure the node with that config option's value and if it fails, it
+// tries again with pre-defined values.
+// Each pre-defined value can exceed the maximum supported by the PEP service.
+// Therefore, multiple values are tried.
+//
+// \param isConfigNodeMaxSupported whether the PEP service supports to set the
+// maximum number of allowed items per node to the maximum it supports
+// \param continuation function to be called with the bool value whether it succeeded
+//
+template<typename Function>
+void ManagerPrivate::configureDeviceBundlesNode(bool isConfigNodeMaxSupported, Function continuation)
+{
+ if (isConfigNodeMaxSupported) {
+ configureNode(ns_omemo_2_bundles, deviceBundlesNodeConfig(), continuation);
+ } else {
+ configureNode(ns_omemo_2_bundles, deviceBundlesNodeConfig(PUBSUB_NODE_MAX_ITEMS_1), [=](bool isConfigured) mutable {
+ if (isConfigured) {
+ continuation(true);
+ } else {
+ configureNode(ns_omemo_2_bundles, deviceBundlesNodeConfig(PUBSUB_NODE_MAX_ITEMS_2), [=](bool isConfigured) mutable {
+ if (isConfigured) {
+ continuation(true);
+ } else {
+ configureNode(ns_omemo_2_bundles, deviceBundlesNodeConfig(PUBSUB_NODE_MAX_ITEMS_3), continuation);
+ }
+ });
+ }
+ });
+ }
+}
+
+//
+// Publishes this device bundle's item on the corresponding existing PEP node.
+//
+// \param continuation function to be called with the bool value whether it succeeded
+//
+template<typename Function>
+void ManagerPrivate::publishDeviceBundleItem(Function continuation)
+{
+ publishItem(ns_omemo_2_bundles, deviceBundleItem(), continuation);
+}
+
+//
+// Publishes this device bundle's item with publish options.
+//
+// If no node for device bundles exists, a new one is created.
+//
+// There is no feature (like ns_pubsub_config_node_max as a config option) and no error case
+// specified for the usage of \c QXmppPubSubNodeConfig::Max() as the value for the publish
+// option \c QXmppPubSubNodeConfig::ItemLimit.
+// Thus, it tries to publish the item with that publish option's value and if it fails, it
+// tries again with pre-defined values.
+// Each pre-defined value can exceed the maximum supported by the PEP service.
+// Therefore, multiple values are tried.
+//
+// \param continuation function to be called with the bool value whether it succeeded
+//
+template<typename Function>
+void ManagerPrivate::publishDeviceBundleItemWithOptions(Function continuation)
+{
+ publishItem(ns_omemo_2_bundles, deviceBundleItem(), deviceBundlesNodePublishOptions(), [=](bool isPublished) mutable {
+ if (isPublished) {
+ continuation(true);
+ } else {
+ publishItem(ns_omemo_2_bundles, deviceBundleItem(), deviceBundlesNodePublishOptions(PUBSUB_NODE_MAX_ITEMS_1), [=](bool isPublished) mutable {
+ if (isPublished) {
+ continuation(true);
+ } else {
+ publishItem(ns_omemo_2_bundles, deviceBundleItem(), deviceBundlesNodePublishOptions(PUBSUB_NODE_MAX_ITEMS_2), [=](bool isPublished) mutable {
+ if (isPublished) {
+ continuation(true);
+ } else {
+ publishItem(ns_omemo_2_bundles, deviceBundleItem(), deviceBundlesNodePublishOptions(PUBSUB_NODE_MAX_ITEMS_3), continuation);
+ }
+ });
+ }
+ });
+ }
+ });
+}
+
+//
+// Creates a PEP item for this device's bundle.
+//
+// \return this device bundle's item
+//
+QXmppOmemoDeviceBundleItem ManagerPrivate::deviceBundleItem() const
+{
+ QXmppOmemoDeviceBundleItem item;
+ item.setId(QString::number(ownDevice.id));
+ item.setDeviceBundle(deviceBundle);
+
+ return item;
+}
+
+//
+// Requests a device bundle from a PEP service.
+//
+// \param deviceOwnerJid bare JID of the device's owner
+// \param deviceId ID of the device whose bundle is requested
+//
+// \return the device bundle on success, otherwise a nullptr
+//
+QFuture<std::optional<QXmppOmemoDeviceBundle>> ManagerPrivate::requestDeviceBundle(const QString &deviceOwnerJid, uint32_t deviceId) const
+{
+ QFutureInterface<std::optional<QXmppOmemoDeviceBundle>> interface(QFutureInterfaceBase::Started);
+
+ auto future = pubSubManager->requestItem<QXmppOmemoDeviceBundleItem>(deviceOwnerJid, ns_omemo_2_bundles, QString::number(deviceId));
+ await(future, q, [=](QXmppPubSubManager::ItemResult<QXmppOmemoDeviceBundleItem> result) mutable {
+ if (const auto error = std::get_if<Error>(&result)) {
+ warning("Device bundle for JID '" % deviceOwnerJid % "' and device ID '" %
+ QString::number(deviceId) % "' could not be retrieved" % errorToString(*error));
+ reportFinishedResult(interface, {});
+ } else {
+ const auto &item = std::get<QXmppOmemoDeviceBundleItem>(result);
+ reportFinishedResult(interface, { item.deviceBundle() });
+ }
+ });
+
+ return interface.future();
+}
+
+//
+// Removes the device bundle for this device or deletes the whole node if it would be empty
+// after the retraction.
+//
+// \param continuation function to be called with the bool value whether it succeeded
+//
+template<typename Function>
+void ManagerPrivate::deleteDeviceBundle(Function continuation)
+{
+ if (otherOwnDevices().isEmpty()) {
+ deleteNode(ns_omemo_2_bundles, continuation);
+ } else {
+ retractItem(ns_omemo_2_bundles, ownDevice.id, continuation);
+ }
+}
+
+//
+// Publishes this device's element within the device list.
+//
+// If no node for the device list exists, a new one is created.
+//
+// \param isDeviceListNodeExistent whether the PEP node for the device list exists
+// \param arePublishOptionsSupported whether publish options are supported by the PEP service
+// \param isAutomaticCreationSupported whether the PEP service supports the automatic creation
+// of nodes when new items are published
+// \param isCreationAndConfigurationSupported whether the PEP service supports the
+// configuration of nodes during their creation
+// \param isCreationSupported whether the PEP service supports creating nodes
+// \param isConfigurationSupported whether the PEP service supports configuring existing
+// nodes
+// \param continuation function to be called with the bool value whether it succeeded
+//
+template<typename Function>
+void ManagerPrivate::publishDeviceElement(bool isDeviceListNodeExistent,
+ bool arePublishOptionsSupported,
+ bool isAutomaticCreationSupported,
+ bool isCreationAndConfigurationSupported,
+ bool isCreationSupported,
+ bool isConfigurationSupported,
+ Function continuation)
+{
+ updateOwnDevicesLocally(isDeviceListNodeExistent, [=](bool isUpdated) mutable {
+ if (isUpdated) {
+ // Check if the PEP service supports configuration of nodes during
+ // publication of items.
+ if (arePublishOptionsSupported) {
+ if (isAutomaticCreationSupported || isDeviceListNodeExistent) {
+ // The supported publish options cannot be determined because they
+ // are not announced via Service Discovery.
+ // Thus, it simply tries to publish the item with the specified
+ // publish options.
+ // If that fails, it tries to manually create and configure the node
+ // and publish the item.
+ publishDeviceListItemWithOptions([=](bool isPublished) mutable {
+ if (isPublished) {
+ continuation(true);
+ } else {
+ auto handleResult = [this, continuation = std::move(continuation)](bool isPublished) mutable {
+ if (!isPublished) {
+ q->debug("PEP service '" % ownBareJid() % "' does not support feature '" % QString(ns_pubsub_publish_options) % "' for all publish options, also not '" % QString(ns_pubsub_create_and_configure) % "', '" % QString(ns_pubsub_create_nodes) % "', '" % QString(ns_pubsub_config_node) % "' and the node does not exist");
+ }
+ continuation(isPublished);
+ };
+ publishDeviceElementWithoutOptions(isDeviceListNodeExistent,
+ isCreationAndConfigurationSupported,
+ isCreationSupported,
+ // TODO: Uncomment the following line and remove the other one once ejabberd released version > 21.12
+ // isConfigurationSupported);
+ true,
+ handleResult);
+ }
+ });
+ } else if (isCreationSupported) {
+ // Create a node manually if the PEP service does not support creation of
+ // nodes during publication of items and no node already exists.
+ createDeviceListNode([=](bool isCreated) mutable {
+ if (isCreated) {
+ // The supported publish options cannot be determined because they
+ // are not announced via Service Discovery.
+ // Thus, it simply tries to publish the item with the specified
+ // publish options.
+ // If that fails, it tries to manually configure the node and
+ // publish the item.
+ publishDeviceListItemWithOptions([=, continuation = std::move(continuation)](bool isPublished) mutable {
+ if (isPublished) {
+ continuation(true);
+ } else if (isConfigurationSupported) {
+ configureNodeAndPublishDeviceElement(continuation);
+ } else {
+ q->debug("PEP service '" % ownBareJid() %
+ "' does not support feature '" %
+ QString(ns_pubsub_publish_options) %
+ "' for all publish options and also not '" %
+ QString(ns_pubsub_config_node) % "'");
+ continuation(false);
+ }
+ });
+ } else {
+ continuation(false);
+ }
+ });
+ } else {
+ q->debug("PEP service '" % ownBareJid() % "' does not support features '" %
+ QString(ns_pubsub_auto_create) % "', '" %
+ QString(ns_pubsub_create_nodes) % "' and the node does not exist");
+ continuation(false);
+ }
+ } else {
+ auto handleResult = [=](bool isPublished) mutable {
+ if (!isPublished) {
+ q->debug("PEP service '" % ownBareJid() % "' does not support features '" % QString(ns_pubsub_publish_options) % "', '" % QString(ns_pubsub_create_and_configure) % "', '" % QString(ns_pubsub_create_nodes) % "', '" % QString(ns_pubsub_config_node) % "' and the node does not exist");
+ }
+ continuation(isPublished);
+ };
+ publishDeviceElementWithoutOptions(isDeviceListNodeExistent,
+ isCreationAndConfigurationSupported,
+ isCreationSupported,
+ // TODO: Uncomment the following line and remove the other one once ejabberd released version > 21.12
+ // isConfigurationSupported);
+ true,
+ handleResult);
+ }
+ } else {
+ continuation(false);
+ }
+ });
+}
+
+//
+// Publish this device's element without publish options.
+//
+// If no node for the device list exists, a new one is created.
+//
+// \param isDeviceListNodeExistent whether the PEP node for the device list exists
+// \param isCreationAndConfigurationSupported whether the PEP service supports the
+// configuration of nodes during their creation
+// \param isCreationSupported whether the PEP service supports creating nodes
+// \param isConfigurationSupported whether the PEP service supports configuring existing
+// nodes
+// \param continuation function to be called with the bool value whether it succeeded
+//
+template<typename Function>
+void ManagerPrivate::publishDeviceElementWithoutOptions(bool isDeviceListNodeExistent, bool isCreationAndConfigurationSupported, bool isCreationSupported, bool isConfigurationSupported, Function continuation)
+{
+ if (isDeviceListNodeExistent && isConfigurationSupported) {
+ configureNodeAndPublishDeviceElement(continuation);
+ } else if (isCreationAndConfigurationSupported) {
+ createAndConfigureDeviceListNode([=](bool isCreatedAndConfigured) mutable {
+ if (isCreatedAndConfigured) {
+ publishDeviceListItem(true, continuation);
+ } else {
+ continuation(false);
+ }
+ });
+ } else if (isCreationSupported && isConfigurationSupported) {
+ createDeviceListNode([=](bool isCreated) mutable {
+ if (isCreated) {
+ configureNodeAndPublishDeviceElement(continuation);
+ } else {
+ continuation(false);
+ }
+ });
+ } else {
+ continuation(false);
+ }
+}
+
+//
+// Configures the existing PEP node for the device list and publishes this device's element on
+// it.
+//
+// \param continuation function to be called with the bool value whether it succeeded
+//
+template<typename Function>
+void ManagerPrivate::configureNodeAndPublishDeviceElement(Function continuation)
+{
+ configureDeviceListNode([=](bool isConfigured) mutable {
+ if (isConfigured) {
+ publishDeviceListItem(true, continuation);
+ } else {
+ continuation(false);
+ }
+ });
+}
+
+//
+// Creates a PEP node for the device list and configures it accordingly.
+//
+// \param continuation function to be called with the bool value whether it succeeded
+//
+template<typename Function>
+void ManagerPrivate::createAndConfigureDeviceListNode(Function continuation)
+{
+ createNode(ns_omemo_2_devices, deviceListNodeConfig(), continuation);
+}
+
+//
+// Creates a PEP node for the device list.
+//
+// \param continuation function to be called with the bool value whether it succeeded
+//
+template<typename Function>
+void ManagerPrivate::createDeviceListNode(Function continuation)
+{
+ createNode(ns_omemo_2_devices, continuation);
+}
+
+//
+// Configures an existing PEP node for the device list.
+//
+// \param continuation function to be called with the bool value whether it succeeded
+//
+template<typename Function>
+void ManagerPrivate::configureDeviceListNode(Function continuation)
+{
+ configureNode(ns_omemo_2_devices, deviceListNodeConfig(), std::move(continuation));
+}
+
+//
+// Publishes the device list item containing this device's element on the corresponding existing
+// PEP node.
+//
+// \param continuation function to be called with the bool value whether it succeeded
+//
+template<typename Function>
+void ManagerPrivate::publishDeviceListItem(bool addOwnDevice, Function continuation)
+{
+ publishItem(ns_omemo_2_devices, deviceListItem(addOwnDevice), continuation);
+}
+
+//
+// Publishes the device list item containing this device's element with publish options.
+//
+// If no node for the device list exists, a new one is created.
+//
+// \param continuation function to be called with the bool value whether it succeeded
+//
+template<typename Function>
+void ManagerPrivate::publishDeviceListItemWithOptions(Function continuation)
+{
+ publishItem(ns_omemo_2_devices, deviceListItem(), deviceListNodePublishOptions(), continuation);
+}
+
+//
+// Creates a PEP item for the device list containing this device's element.
+//
+// \return the device list item
+//
+QXmppOmemoDeviceListItem ManagerPrivate::deviceListItem(bool addOwnDevice)
+{
+ QXmppOmemoDeviceList deviceList;
+
+ // Add this device to the device list.
+ if (addOwnDevice) {
+ QXmppOmemoDeviceElement deviceElement;
+ deviceElement.setId(ownDevice.id);
+ deviceElement.setLabel(ownDevice.label);
+ deviceList.append(deviceElement);
+ }
+
+ // Add all remaining own devices to the device list.
+ const auto ownDevices = otherOwnDevices();
+ for (auto itr = ownDevices.cbegin(); itr != ownDevices.cend(); ++itr) {
+ const auto &deviceId = itr.key();
+ const auto &device = itr.value();
+
+ QXmppOmemoDeviceElement deviceElement;
+ deviceElement.setId(deviceId);
+ deviceElement.setLabel(device.label);
+ deviceList.append(deviceElement);
+ }
+
+ QXmppOmemoDeviceListItem item;
+ item.setId(QXmppPubSubManager::standardItemIdToString(QXmppPubSubManager::Current));
+ item.setDeviceList(deviceList);
+
+ return item;
+}
+
+//
+// Updates the own locally stored devices by requesting the current device list from the own
+// PEP service.
+//
+// \param isDeviceListNodeExistent whether the node for the device list exists
+// \param continuation function to be called with the bool value whether it succeeded
+//
+template<typename Function>
+void ManagerPrivate::updateOwnDevicesLocally(bool isDeviceListNodeExistent, Function continuation)
+{
+ if (isDeviceListNodeExistent && otherOwnDevices().isEmpty()) {
+ auto future = pubSubManager->requestPepItem<QXmppOmemoDeviceListItem>(ns_omemo_2_devices, QXmppPubSubManager::Current);
+ await(future, q, [=](QXmppPubSubManager::ItemResult<QXmppOmemoDeviceListItem> result) mutable {
+ if (const auto error = std::get_if<Error>(&result)) {
+ warning("Device list for JID '" % ownBareJid() %
+ "' could not be retrieved and thus not updated" %
+ errorToString(*error));
+ continuation(false);
+ } else {
+ const auto &deviceListItem = std::get<QXmppOmemoDeviceListItem>(result);
+ QList<QXmppOmemoDeviceElement> deviceList = deviceListItem.deviceList();
+
+ if (auto devicesCount = deviceList.size()) {
+ // Do not exceed the maximum of manageable devices.
+ if (devicesCount > maximumDevicesPerJid) {
+ warning(u"Received own OMEMO device list could not be stored locally "
+ "completely because the devices are more than the maximum of "
+ "manageable devices " %
+ QString::number(maximumDevicesPerJid) %
+ u" - Use 'QXmppOmemoManager::setMaximumDevicesPerJid()' to "
+ "increase the maximum");
+ deviceList = deviceList.mid(0, maximumDevicesPerJid);
+ devicesCount = maximumDevicesPerJid;
+ }
+
+ auto processedDevicesCount = std::make_shared<int>(0);
+
+ // Store all device elements retrieved from the device list locally as
+ // devices.
+ // The own device (i.e., a device element in the device list with the same
+ // ID as of this device) is skipped.
+ for (const auto &deviceElement : std::as_const(deviceList)) {
+ if (const auto deviceId = deviceElement.id(); deviceId != ownDevice.id) {
+ const auto jid = ownBareJid();
+ auto &device = devices[jid][deviceId];
+ device.label = deviceElement.label();
+
+ auto future = omemoStorage->addDevice(jid, deviceId, device);
+ await(future, q, [=, &device]() mutable {
+ auto future = buildSessionForNewDevice(jid, deviceId, device);
+ await(future, q, [=](auto) mutable {
+ emit q->deviceAdded(jid, deviceId);
+
+ if (++(*processedDevicesCount) == devicesCount) {
+ continuation(true);
+ }
+ });
+ });
+ }
+ }
+ } else {
+ continuation(true);
+ }
+ }
+ });
+ } else {
+ continuation(true);
+ }
+}
+
+//
+// Updates all locally stored devices by a passed device list item.
+//
+// \param deviceOwnerJid bare JID of the devices' owner
+// \param deviceListItem PEP item containing the device list
+//
+void ManagerPrivate::updateDevices(const QString &deviceOwnerJid, const QXmppOmemoDeviceListItem &deviceListItem)
+{
+ const auto isOwnDeviceListNode = ownBareJid() == deviceOwnerJid;
+ QList<QXmppOmemoDeviceElement> deviceList = deviceListItem.deviceList();
+ auto isOwnDeviceListIncorrect = false;
+
+ // Do not exceed the maximum of manageable devices.
+ if (deviceList.size() > maximumDevicesPerJid) {
+ warning(u"Received OMEMO device list of JID '" % deviceOwnerJid %
+ "' could not be stored locally completely because the devices are more than the "
+ "maximum of manageable devices " %
+ QString::number(maximumDevicesPerJid) %
+ u" - Use 'QXmppOmemoManager::setMaximumDevicesPerJid()' to increase the maximum");
+ deviceList = deviceList.mid(0, maximumDevicesPerJid);
+ }
+
+ if (isOwnDeviceListNode) {
+ QList<uint32_t> deviceIds;
+
+ // Search for inconsistencies in the device list to keep it
+ // correct.
+ // The following problems are corrected:
+ // * Multiple device elements have the same IDs.
+ // * There is no device element for this device.
+ // * There are device elements with the same ID as this device
+ // but different labels.
+ for (auto itr = deviceList.begin(); itr != deviceList.end();) {
+ const auto deviceElementId = itr->id();
+
+ if (deviceIds.contains(deviceElementId)) {
+ isOwnDeviceListIncorrect = true;
+ itr = deviceList.erase(itr);
+ } else {
+ deviceIds.append(deviceElementId);
+
+ if (itr->id() == ownDevice.id) {
+ if (itr->label() != ownDevice.label) {
+ isOwnDeviceListIncorrect = true;
+ }
+
+ itr = deviceList.erase(itr);
+ } else {
+ ++itr;
+ }
+ }
+ }
+ }
+
+ // Set a timestamp for locally stored devices that are removed later if
+ // they are not included in the device list (i.e., they were removed
+ // by their owner).
+ auto &ownerDevices = devices[deviceOwnerJid];
+ for (auto itr = ownerDevices.begin(); itr != ownerDevices.end(); ++itr) {
+ const auto &deviceId = itr.key();
+ auto &device = itr.value();
+ auto isDeviceFound = false;
+
+ for (const auto &deviceElement : std::as_const(deviceList)) {
+ if (deviceId == deviceElement.id()) {
+ isDeviceFound = true;
+ break;
+ }
+ }
+
+ if (!isDeviceFound) {
+ device.removalFromDeviceListDate = QDateTime::currentDateTimeUtc();
+ omemoStorage->addDevice(deviceOwnerJid, deviceId, device);
+ }
+ }
+
+ // Update locally stored devices if they are modified in the device
+ // list or store devices locally if they are new in the device list.
+ for (const auto &deviceElement : std::as_const(deviceList)) {
+ auto isDeviceFound = false;
+
+ for (auto itr = ownerDevices.begin(); itr != ownerDevices.end(); ++itr) {
+ const auto &deviceId = itr.key();
+ auto &device = itr.value();
+
+ if (deviceId == deviceElement.id()) {
+ auto isDeviceModified = false;
+ auto isDeviceLabelModified = false;
+
+ // Reset the date of removal from server, if it has been
+ // removed before.
+ if (!device.removalFromDeviceListDate.isNull()) {
+ device.removalFromDeviceListDate = {};
+ isDeviceModified = true;
+ }
+
+ // Update the stored label if it differs from the new
+ // one.
+ if (device.label != deviceElement.label()) {
+ device.label = deviceElement.label();
+ isDeviceModified = true;
+ isDeviceLabelModified = true;
+ }
+
+ // Store the modifications.
+ if (isDeviceModified) {
+ omemoStorage->addDevice(deviceOwnerJid, deviceId, device);
+
+ if (isDeviceLabelModified) {
+ emit q->deviceChanged(deviceOwnerJid, deviceId);
+ }
+ }
+
+ isDeviceFound = true;
+ break;
+ }
+ }
+
+ // Create a new entry and store it if there is no such entry
+ // yet.
+ if (!isDeviceFound) {
+ const auto deviceId = deviceElement.id();
+ auto &device = ownerDevices[deviceId];
+ device.label = deviceElement.label();
+ omemoStorage->addDevice(deviceOwnerJid, deviceId, device);
+
+ auto future = buildSessionForNewDevice(deviceOwnerJid, deviceId, device);
+ await(future, q, [=](auto) {
+ emit q->deviceAdded(deviceOwnerJid, deviceId);
+ });
+ }
+ }
+
+ // Publish an own correct device list if the PEP service's one is incorrect
+ // and the devices are already set up locally.
+ if (isOwnDeviceListIncorrect) {
+ if (!this->devices.isEmpty()) {
+ publishDeviceListItem(true, [=](bool isPublished) {
+ if (!isPublished) {
+ warning("Own device list item could not be published in order to correct the PEP service's one");
+ }
+ });
+ }
+ }
+}
+
+//
+// Corrects the own device list on the PEP service by the locally stored
+// devices or set a contact device to be removed locally in the future.
+//
+// \param deviceOwnerJid bare JID of the devices' owner
+//
+void ManagerPrivate::handleIrregularDeviceListChanges(const QString &deviceOwnerJid)
+{
+ const auto isOwnDeviceListNode = ownBareJid() == deviceOwnerJid;
+
+ if (isOwnDeviceListNode) {
+ // Publish a new device list for the own devices if their device list
+ // item is removed, if their device list node is removed or if all
+ // the node's items are removed.
+ auto future = pubSubManager->deletePepNode(ns_omemo_2_devices);
+ await(future, q, [=](QXmppPubSubManager::Result result) {
+ if (const auto error = std::get_if<Error>(&result)) {
+ warning("Node '" % QString(ns_omemo_2_devices) % "' of JID '" % deviceOwnerJid %
+ "' could not be deleted in order to recover from an inconsistent node" %
+ errorToString(*error));
+ } else {
+ auto future = pubSubManager->requestPepFeatures();
+ await(future, q, [=](QXmppPubSubManager::FeaturesResult result) {
+ if (const auto error = std::get_if<Error>(&result)) {
+ warning("Features of PEP service '" % deviceOwnerJid %
+ "' could not be retrieved" % errorToString(*error));
+ warning("Device list could not be published");
+ } else {
+ const auto &pepServiceFeatures = std::get<QVector<QString>>(result);
+
+ const auto arePublishOptionsSupported = pepServiceFeatures.contains(ns_pubsub_publish_options);
+ const auto isAutomaticCreationSupported = pepServiceFeatures.contains(ns_pubsub_auto_create);
+ const auto isCreationAndConfigurationSupported = pepServiceFeatures.contains(ns_pubsub_create_and_configure);
+ const auto isCreationSupported = pepServiceFeatures.contains(ns_pubsub_create_nodes);
+ const auto isConfigurationSupported = pepServiceFeatures.contains(ns_pubsub_config_node);
+
+ publishDeviceElement(false,
+ arePublishOptionsSupported,
+ isAutomaticCreationSupported,
+ isCreationAndConfigurationSupported,
+ isCreationSupported,
+ isConfigurationSupported,
+ [=](bool isPublished) {
+ if (!isPublished) {
+ warning("Device element could not be published");
+ }
+ });
+ }
+ });
+ }
+ });
+ } else {
+ auto &ownerDevices = this->devices[deviceOwnerJid];
+
+ // Set a timestamp for locally stored contact devices being removed
+ // later if their device list item is removed, if their device list node
+ // is removed or if all the node's items are removed.
+ for (auto itr = ownerDevices.begin(); itr != ownerDevices.end(); ++itr) {
+ const auto &deviceId = itr.key();
+ auto &device = itr.value();
+
+ device.removalFromDeviceListDate = QDateTime::currentDateTimeUtc();
+
+ // Store the modification.
+ omemoStorage->addDevice(deviceOwnerJid, deviceId, device);
+ }
+ }
+}
+
+//
+// Removes the device element for this device or deletes the whole PEP node if
+// it would be empty after the retraction.
+//
+// \param continuation function to be called with the bool value whether it succeeded
+//
+template<typename Function>
+void ManagerPrivate::deleteDeviceElement(Function continuation)
+{
+ if (otherOwnDevices().isEmpty()) {
+ deleteNode(ns_omemo_2_devices, std::move(continuation));
+ } else {
+ publishDeviceListItem(false, std::move(continuation));
+ }
+}
+
+//
+// Creates a PEP node.
+//
+// \param node node to be created
+// \param continuation function to be called with the bool value whether it succeeded
+//
+template<typename Function>
+void ManagerPrivate::createNode(const QString &node, Function continuation)
+{
+ runPubSubQueryWithContinuation(pubSubManager->createPepNode(node),
+ "Node '" % node % "' of JID '" % ownBareJid() % "' could not be created",
+ std::move(continuation));
+}
+
+//
+// Creates a PEP node with a configuration.
+//
+// \param node node to be created
+// \param config configuration to be applied
+// \param continuation function to be called with the bool value whether it succeeded
+//
+template<typename Function>
+void ManagerPrivate::createNode(const QString &node, const QXmppPubSubNodeConfig &config, Function continuation)
+{
+ runPubSubQueryWithContinuation(pubSubManager->createPepNode(node, config),
+ "Node '" % node % "' of JID '" % ownBareJid() % "' could not be created",
+ std::move(continuation));
+}
+
+//
+// Configures an existing PEP node.
+//
+// \param node node to be configured
+// \param config configuration to be applied
+// \param continuation function to be called with the bool value whether it succeeded
+//
+template<typename Function>
+void ManagerPrivate::configureNode(const QString &node, const QXmppPubSubNodeConfig &config, Function continuation)
+{
+ runPubSubQueryWithContinuation(pubSubManager->configurePepNode(node, config),
+ "Node '" % node % "' of JID '" % ownBareJid() % "' could not be configured",
+ std::move(continuation));
+}
+
+//
+// Retracts an item from a PEP node.
+//
+// \param node node containing the item
+// \param itemId ID of the item
+// \param continuation function to be called with the bool value whether it succeeded
+//
+template<typename Function>
+void ManagerPrivate::retractItem(const QString &node, uint32_t itemId, Function continuation)
+{
+ const auto itemIdString = QString::number(itemId);
+ runPubSubQueryWithContinuation(pubSubManager->retractPepItem(node, itemIdString),
+ "Item '" % itemIdString % "' of node '" % node % "' and JID '" % ownBareJid() % "' could not be retracted",
+ std::move(continuation));
+}
+
+//
+// Deletes a PEP node.
+//
+// \param node node to be deleted
+// \param continuation function to be called with the bool value whether it succeeded
+//
+template<typename Function>
+void ManagerPrivate::deleteNode(const QString &node, Function continuation)
+{
+ auto future = pubSubManager->deletePepNode(node);
+ await(future, q, [=, continuation = std::move(continuation)](QXmppPubSubManager::Result result) mutable {
+ const auto error = std::get_if<Error>(&result);
+ if (error) {
+ const auto errorType = error->type();
+ const auto errorCondition = error->condition();
+
+ // Skip the error handling if the node is already deleted.
+ if (!(errorType == Error::Cancel && errorCondition == Error::ItemNotFound)) {
+ warning("Node '" % node % "' of JID '" % ownBareJid() % "' could not be deleted" %
+ errorToString(*error));
+ continuation(false);
+ } else {
+ continuation(true);
+ }
+ } else {
+ continuation(true);
+ }
+ });
+}
+
+//
+// Publishes a PEP item.
+//
+// \param node node containing the item
+// \param item item to be published
+// \param continuation function to be called with the bool value whether it succeeded
+//
+template<typename T, typename Function>
+void ManagerPrivate::publishItem(const QString &node, const T &item, Function continuation)
+{
+ runPubSubQueryWithContinuation(pubSubManager->publishPepItem(node, item),
+ "Item with ID '" % item.id() %
+ "' could not be published to node '" % node % "' of JID '" %
+ ownBareJid() % "'",
+ std::move(continuation));
+}
+
+//
+// Publishes a PEP item with publish options.
+//
+// \param node node containing the item
+// \param item item to be published
+// \param publishOptions publish options to be applied
+// \param continuation function to be called with the bool value whether it succeeded
+//
+template<typename T, typename Function>
+void ManagerPrivate::publishItem(const QString &node, const T &item, const QXmppPubSubPublishOptions &publishOptions, Function continuation)
+{
+ runPubSubQueryWithContinuation(pubSubManager->publishPepItem(node, item, publishOptions),
+ "Item with ID '" % item.id() % "' could not be published to node '" % node % "' of JID '" % ownBareJid() % "'",
+ std::move(continuation));
+}
+
+//
+// Runs a PubSub query and processes a continuation function.
+//
+// \param future PubSub query to be run
+// \param errorMessage message to be logged in case of an error
+// \param continuation function to be called after the PubSub query
+//
+template<typename T, typename Function>
+void QXmppOmemoManagerPrivate::runPubSubQueryWithContinuation(QFuture<T> future, const QString &errorMessage, Function continuation)
+{
+ await(future, q, [this, errorMessage, continuation = std::move(continuation)](auto result) mutable {
+ if (auto error = std::get_if<Error>(&result)) {
+ warning(errorMessage % u": " % errorToString(*error));
+ continuation(false);
+ } else {
+ continuation(true);
+ }
+ });
+}
+
+// See QXmppOmemoManager for documentation
+QFuture<bool> ManagerPrivate::changeDeviceLabel(const QString &deviceLabel)
+{
+ QFutureInterface<bool> interface(QFutureInterfaceBase::Started);
+
+ ownDevice.label = deviceLabel;
+
+ if (isStarted) {
+ auto future = omemoStorage->setOwnDevice(ownDevice);
+ await(future, q, [=]() mutable {
+ publishDeviceListItem(true, [=](bool isPublished) mutable {
+ reportFinishedResult(interface, isPublished);
+ });
+ });
+ } else {
+ reportFinishedResult(interface, true);
+ }
+
+ return interface.future();
+}
+
+//
+// Requests the device list of a contact manually and stores it locally.
+//
+// This should be called for offline contacts whose servers do not distribute
+// the last published PubSub item if that contact is offline (e.g., with at
+// least ejabberd version <= 21.12)
+//
+// \param jid JID of the contact whose device list is being requested
+//
+// \return the result of the request
+//
+QFuture<QXmppPubSubManager::ItemResult<QXmppOmemoDeviceListItem>> ManagerPrivate::requestDeviceList(const QString &jid)
+{
+ auto future = pubSubManager->requestItem<QXmppOmemoDeviceListItem>(jid, ns_omemo_2_devices, QXmppPubSubManager::Current);
+ await(future, q, [this, jid](QXmppPubSubManager::ItemResult<QXmppOmemoDeviceListItem> result) mutable {
+ if (const auto error = std::get_if<Error>(&result)) {
+ warning("Device list for JID '" % jid % "' could not be retrieved: " % errorToString(*error));
+ } else {
+ const auto &item = std::get<QXmppOmemoDeviceListItem>(result);
+ updateDevices(jid, item);
+ }
+ });
+ return future;
+}
+
+//
+// Subscribes to the device list of a contact if the contact's device is not stored yet.
+//
+// \param jid JID of the contact whose device list is being subscribed
+// \param deviceId ID of the device that is checked
+//
+void ManagerPrivate::subscribeToNewDeviceLists(const QString &jid, uint32_t deviceId)
+{
+ if (!devices.value(jid).contains(deviceId)) {
+ subscribeToDeviceList(jid);
+ }
+}
+
+//
+// Subscribes the current user's resource to a device list manually.
+//
+// A server may not send the last published item automatically.
+// To ensure that the subscribed device list can be stored locally in any case,
+// the current PubSub item containing the device list is requested manually.
+//
+// \param jid JID of the contact whose device list is being subscribed
+//
+// \return the result of the subscription and manual request
+//
+QFuture<QXmppPubSubManager::Result> ManagerPrivate::subscribeToDeviceList(const QString &jid)
+{
+ QFutureInterface<QXmppPubSubManager::Result> interface(QFutureInterfaceBase::Started);
+
+ auto future = pubSubManager->subscribeToNode(jid, ns_omemo_2_devices, ownFullJid());
+ await(future, q, [=](QXmppPubSubManager::Result result) mutable {
+ if (const auto error = std::get_if<Error>(&result)) {
+ warning("Device list for JID '" % jid % "' could not be subscribed: " % errorToString(*error));
+ reportFinishedResult(interface, { *error });
+ } else {
+ jidsOfManuallySubscribedDevices.append(jid);
+
+ auto future = requestDeviceList(jid);
+ await(future, q, [=](auto result) mutable {
+ reportFinishedResult(interface, mapToSuccess(std::move(result)));
+ });
+ }
+ });
+
+ return interface.future();
+}
+
+//
+// Unsubscribes the current user's resource from device lists that were
+// manually subscribed by
+// \c QXmppOmemoManagerPrivate::subscribeToDeviceList().
+//
+// \param jids JIDs of the contacts whose device lists are being
+// unsubscribed
+//
+// \return the results of each unsubscribe request
+//
+QFuture<Manager::DevicesResult> ManagerPrivate::unsubscribeFromDeviceLists(const QList<QString> &jids)
+{
+ QFutureInterface<Manager::DevicesResult> interface = (QFutureInterfaceBase::Started);
+
+ const auto jidsCount = jids.size();
+ auto processedJidsCount = std::make_shared<int>(0);
+
+ if (jidsCount == 0) {
+ interface.reportFinished();
+ }
+
+ for (const auto &jid : jids) {
+ auto future = unsubscribeFromDeviceList(jid);
+ await(future, q, [=](QXmppPubSubManager::Result result) mutable {
+ Manager::DevicesResult devicesResult;
+ devicesResult.jid = jid;
+ devicesResult.result = result;
+ interface.reportResult(devicesResult);
+
+ if (++(*processedJidsCount) == jidsCount) {
+ interface.reportFinished();
+ }
+ });
+ }
+
+ return interface.future();
+}
+
+//
+// Unsubscribes the current user's resource from a device list that were
+// manually subscribed by
+// \c QXmppOmemoManagerPrivate::subscribeToDeviceList().
+//
+// \param jid JID of the contact whose device list is being unsubscribed
+//
+// \return the result of the unsubscription
+//
+QFuture<QXmppPubSubManager::Result> ManagerPrivate::unsubscribeFromDeviceList(const QString &jid)
+{
+ QFutureInterface<QXmppPubSubManager::Result> interface(QFutureInterfaceBase::Started);
+
+ auto future = pubSubManager->unsubscribeFromNode(jid, ns_omemo_2_devices, ownFullJid());
+ await(future, q, [=](QXmppPubSubManager::Result result) mutable {
+ if (const auto error = std::get_if<Error>(&result)) {
+ warning("Device list for JID '" % jid % "' could not be unsubscribed: " % errorToString(*error));
+ } else {
+ jidsOfManuallySubscribedDevices.removeAll(jid);
+ }
+
+ reportFinishedResult(interface, result);
+ });
+
+ return interface.future();
+}
+
+// See QXmppOmemoManager for documentation
+QFuture<bool> ManagerPrivate::resetOwnDevice()
+{
+ QFutureInterface<bool> interface(QFutureInterfaceBase::Started);
+
+ isStarted = false;
+
+ auto future = trustManager->resetAll(ns_omemo_2);
+ await(future, q, [=]() mutable {
+ auto future = omemoStorage->resetAll();
+ await(future, q, [=]() mutable {
+ deleteDeviceElement([=](bool isDeviceElementDeleted) mutable {
+ if (isDeviceElementDeleted) {
+ deleteDeviceBundle([=](bool isDeviceBundleDeleted) mutable {
+ if (isDeviceBundleDeleted) {
+ ownDevice = {};
+ preKeyPairs.clear();
+ signedPreKeyPairs.clear();
+ deviceBundle = {};
+ devices.clear();
+
+ emit q->allDevicesRemoved();
+ }
+
+ reportFinishedResult(interface, isDeviceBundleDeleted);
+ });
+ } else {
+ reportFinishedResult(interface, false);
+ }
+ });
+ });
+ });
+
+ return interface.future();
+}
+
+// See QXmppOmemoManager for documentation
+QFuture<bool> ManagerPrivate::resetAll()
+{
+ QFutureInterface<bool> interface(QFutureInterfaceBase::Started);
+
+ isStarted = false;
+
+ auto future = trustManager->resetAll(ns_omemo_2);
+ await(future, q, [this, interface]() mutable {
+ auto future = omemoStorage->resetAll();
+ await(future, q, [this, interface]() mutable {
+ deleteNode(ns_omemo_2_devices, [this, interface](bool isDevicesNodeDeleted) mutable {
+ if (isDevicesNodeDeleted) {
+ deleteNode(ns_omemo_2_bundles, [this, interface](bool isBundlesNodeDeleted) mutable {
+ if (isBundlesNodeDeleted) {
+ ownDevice = {};
+ preKeyPairs.clear();
+ signedPreKeyPairs.clear();
+ deviceBundle = {};
+ devices.clear();
+
+ emit q->allDevicesRemoved();
+ }
+
+ reportFinishedResult(interface, isBundlesNodeDeleted);
+ });
+ } else {
+ reportFinishedResult(interface, false);
+ }
+ });
+ });
+ });
+
+ return interface.future();
+}
+
+//
+// Builds a new session for a new received device if that is enabled.
+//
+// \see QXmppOmemoManager::setNewDeviceAutoSessionBuildingEnabled()
+//
+// \param jid JID of the device's owner
+// \param deviceId ID of the device
+// \param device locally stored device which will be modified
+//
+// \return true if a session could be built or it is not enabled, otherwise
+// false
+//
+QFuture<bool> ManagerPrivate::buildSessionForNewDevice(const QString &jid, uint32_t deviceId, QXmppOmemoStorage::Device &device)
+{
+ if (isNewDeviceAutoSessionBuildingEnabled) {
+ return buildSessionWithDeviceBundle(jid, deviceId, device);
+ } else {
+ return makeReadyFuture(true);
+ }
+}
+
+//
+// Requests a device bundle and builds a new session with it.
+//
+// \param jid JID of the device's owner
+// \param deviceId ID of the device
+// \param device locally stored device which will be modified
+//
+// \return whether a session could be built
+//
+QFuture<bool> ManagerPrivate::buildSessionWithDeviceBundle(const QString &jid, uint32_t deviceId, QXmppOmemoStorage::Device &device)
+{
+ QFutureInterface<bool> interface(QFutureInterfaceBase::Started);
+
+ auto future = requestDeviceBundle(jid, deviceId);
+ await(future, q, [=, &device](std::optional<QXmppOmemoDeviceBundle> optionalDeviceBundle) mutable {
+ if (optionalDeviceBundle) {
+ const auto &deviceBundle = *optionalDeviceBundle;
+ const auto key = deviceBundle.publicIdentityKey();
+ device.keyId = createKeyId(key);
+
+ auto future = q->trustLevel(jid, device.keyId);
+ await(future, q, [=](TrustLevel trustLevel) mutable {
+ auto buildSessionDependingOnTrustLevel = [=](TrustLevel trustLevel) mutable {
+ // Build a session if the device's key has a specific trust
+ // level and send an empty OMEMO (key exchange) message to
+ // make the receiving device build a new session too.
+ if (!acceptedSessionBuildingTrustLevels.testFlag(trustLevel)) {
+ warning("Session could not be created for JID '" % jid % "' with device ID '" %
+ QString::number(deviceId) % "' because its key's trust level '" %
+ QString::number(int(trustLevel)) % "' is not accepted");
+ reportFinishedResult(interface, false);
+ } else if (const auto address = Address(jid, deviceId); !buildSession(address.data(), deviceBundle)) {
+ warning("Session could not be created for JID '" % jid % "' and device ID '" %
+ QString::number(deviceId) % "'");
+ reportFinishedResult(interface, false);
+ } else {
+ auto future = sendEmptyMessage(jid, deviceId, true);
+ await(future, q, [=](QXmpp::SendResult result) mutable {
+ if (std::holds_alternative<QXmpp::SendError>(result)) {
+ warning("Session could be created but empty message could not be sent to JID '" %
+ jid % "' and device ID '" % QString::number(deviceId) % "'");
+ reportFinishedResult(interface, false);
+ } else {
+ reportFinishedResult(interface, true);
+ }
+ });
+ }
+ };
+
+ if (trustLevel == TrustLevel::Undecided) {
+ // Store the key's trust level if it is not stored yet.
+ auto future = storeKeyDependingOnSecurityPolicy(jid, key);
+ await(future, q, [=](TrustLevel trustLevel) mutable {
+ buildSessionDependingOnTrustLevel(trustLevel);
+ });
+ } else {
+ buildSessionDependingOnTrustLevel(trustLevel);
+ }
+ });
+ } else {
+ warning("Session could not be created because no device bundle could be fetched for "
+ "JID '" %
+ jid % "' and device ID '" % QString::number(deviceId) % "'");
+ reportFinishedResult(interface, false);
+ }
+ });
+
+ return interface.future();
+}
+
+//
+// Builds an OMEMO session.
+//
+// A session is used for encryption and decryption.
+//
+// \param address address of the device for whom the session is built
+// \param deviceBundle device bundle containing all data to build the session
+//
+// \return whether it succeeded
+//
+bool ManagerPrivate::buildSession(signal_protocol_address address, const QXmppOmemoDeviceBundle &deviceBundle)
+{
+ QFutureInterface<bool> interface(QFutureInterfaceBase::Started);
+
+ // Choose a pre key randomly.
+ const auto publicPreKeys = deviceBundle.publicPreKeys();
+ if (publicPreKeys.isEmpty()) {
+ warning("No public pre key could be found in device bundle");
+ }
+ const auto publicPreKeyIds = publicPreKeys.keys();
+#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
+ const auto publicPreKeyIndex = QRandomGenerator::system()->bounded(publicPreKeyIds.size());
+#else
+ const auto publicPreKeyIndex = qrand() % publicPreKeyIds.size();
+#endif
+ const auto publicPreKeyId = publicPreKeyIds.at(publicPreKeyIndex);
+ const auto publicPreKey = publicPreKeys.value(publicPreKeyId);
+
+ SessionBuilderPtr sessionBuilder;
+ if (session_builder_create(sessionBuilder.ptrRef(), storeContext.get(), &address, globalContext.get()) < 0) {
+ warning("Session builder could not be created");
+ return false;
+ }
+ session_builder_set_version(sessionBuilder.get(), CIPHERTEXT_OMEMO_VERSION);
+
+ RefCountedPtr<session_pre_key_bundle> sessionBundle;
+
+ if (!createSessionBundle(sessionBundle.ptrRef(),
+ deviceBundle.publicIdentityKey(),
+ deviceBundle.signedPublicPreKey(),
+ deviceBundle.signedPublicPreKeyId(),
+ deviceBundle.signedPublicPreKeySignature(),
+ publicPreKey,
+ publicPreKeyId)) {
+ warning("Session bundle could not be created");
+ return false;
+ }
+
+ if (session_builder_process_pre_key_bundle(sessionBuilder.get(), sessionBundle.get()) != SG_SUCCESS) {
+ warning("Session bundle could not be processed");
+ return false;
+ }
+
+ return true;
+}
+
+//
+// Creates a session bundle.
+//
+// \param sessionBundle session bundle location
+// \param serializedPublicIdentityKey serialized public identity key
+// \param serializedSignedPublicPreKey serialized signed public pre key
+// \param signedPublicPreKeyId ID of the signed public pre key
+// \param serializedSignedPublicPreKeySignature serialized signature of the
+// signed public pre key
+// \param serializedPublicPreKey serialized public pre key
+// \param publicPreKeyId ID of the public pre key
+//
+// \return whether it succeeded
+//
+bool ManagerPrivate::createSessionBundle(session_pre_key_bundle **sessionBundle,
+ const QByteArray &serializedPublicIdentityKey,
+ const QByteArray &serializedSignedPublicPreKey,
+ uint32_t signedPublicPreKeyId,
+ const QByteArray &serializedSignedPublicPreKeySignature,
+ const QByteArray &serializedPublicPreKey,
+ uint32_t publicPreKeyId)
+{
+ RefCountedPtr<ec_public_key> publicIdentityKey;
+ RefCountedPtr<ec_public_key> signedPublicPreKey;
+ RefCountedPtr<const uint8_t> signedPublicPreKeySignature;
+ int signedPublicPreKeySignatureSize;
+ RefCountedPtr<ec_public_key> publicPreKey;
+
+ if (deserializePublicIdentityKey(publicIdentityKey.ptrRef(), serializedPublicIdentityKey) &&
+ deserializeSignedPublicPreKey(signedPublicPreKey.ptrRef(), serializedSignedPublicPreKey) &&
+ (signedPublicPreKeySignatureSize = deserializeSignedPublicPreKeySignature(signedPublicPreKeySignature.ptrRef(), serializedSignedPublicPreKeySignature)) &&
+ deserializePublicPreKey(publicPreKey.ptrRef(), serializedPublicPreKey)) {
+
+ // "0" is passed as "device_id" to the OMEMO library because it is not
+ // used by OMEMO.
+ // Only the device ID is of interest which is used as "registration_id"
+ // within the OMEMO library.
+ if (session_pre_key_bundle_create(sessionBundle,
+ ownDevice.id,
+ 0,
+ publicPreKeyId,
+ publicPreKey.get(),
+ signedPublicPreKeyId,
+ signedPublicPreKey.get(),
+ signedPublicPreKeySignature.get(),
+ signedPublicPreKeySignatureSize,
+ publicIdentityKey.get()) < 0) {
+ return false;
+ }
+
+ return true;
+ } else {
+ warning("Session bundle data could not be deserialized");
+ return false;
+ }
+}
+
+//
+// Deserializes a public identity key.
+//
+// \param publicIdentityKey public identity key location
+// \param serializedPublicIdentityKey serialized public identity key
+//
+// \return whether it succeeded
+//
+bool ManagerPrivate::deserializePublicIdentityKey(ec_public_key **publicIdentityKey, const QByteArray &serializedPublicIdentityKey) const
+{
+ BufferPtr publicIdentityKeyBuffer = BufferPtr::fromByteArray(serializedPublicIdentityKey);
+
+ if (!publicIdentityKeyBuffer) {
+ warning("Buffer for serialized public identity key could not be created");
+ return false;
+ }
+
+ if (curve_decode_point(publicIdentityKey, signal_buffer_data(publicIdentityKeyBuffer.get()), signal_buffer_len(publicIdentityKeyBuffer.get()), globalContext.get()) < 0) {
+ warning("Public identity key could not be deserialized");
+ return false;
+ }
+
+ return true;
+}
+
+//
+// Deserializes a signed public pre key.
+//
+// \param signedPublicPreKey signed public pre key location
+// \param serializedSignedPublicPreKey serialized signed public pre key
+//
+// \return whether it succeeded
+//
+bool ManagerPrivate::deserializeSignedPublicPreKey(ec_public_key **signedPublicPreKey, const QByteArray &serializedSignedPublicPreKey) const
+{
+ BufferPtr signedPublicPreKeyBuffer = BufferPtr::fromByteArray(serializedSignedPublicPreKey);
+
+ if (!signedPublicPreKeyBuffer) {
+ warning("Buffer for serialized signed public pre key could not be created");
+ return false;
+ }
+
+ if (curve_decode_point(signedPublicPreKey, signal_buffer_data(signedPublicPreKeyBuffer.get()), signal_buffer_len(signedPublicPreKeyBuffer.get()), globalContext.get()) < 0) {
+ warning("Signed public pre key could not be deserialized");
+ return false;
+ }
+
+ return true;
+}
+
+//
+// Deserializes a public pre key.
+//
+// \param publicPreKey public pre key location
+// \param serializedPublicPreKey serialized public pre key
+//
+// \return whether it succeeded
+//
+bool ManagerPrivate::deserializePublicPreKey(ec_public_key **publicPreKey, const QByteArray &serializedPublicPreKey) const
+{
+ auto publicPreKeyBuffer = BufferPtr::fromByteArray(serializedPublicPreKey);
+
+ if (!publicPreKeyBuffer) {
+ warning("Buffer for serialized public pre key could not be created");
+ return false;
+ }
+
+ if (curve_decode_point(publicPreKey, signal_buffer_data(publicPreKeyBuffer.get()), signal_buffer_len(publicPreKeyBuffer.get()), globalContext.get()) < 0) {
+ warning("Public pre key could not be deserialized");
+ return false;
+ }
+
+ return true;
+}
+
+//
+// Sends an empty OMEMO message.
+//
+// An empty OMEMO message is a message without an OMEMO payload.
+// It is used to trigger the completion, rebuilding or refreshing of OMEMO
+// sessions.
+//
+// \param recipientJid JID of the message's recipient
+// \param recipientDeviceId ID of the recipient's device
+// \param isKeyExchange whether the message is used to build a new session
+//
+// \return the result of the sending
+//
+QFuture<QXmpp::SendResult> ManagerPrivate::sendEmptyMessage(const QString &recipientJid, uint32_t recipientDeviceId, bool isKeyExchange) const
+{
+ QFutureInterface<QXmpp::SendResult> interface(QFutureInterfaceBase::Started);
+
+ const auto address = Address(recipientJid, recipientDeviceId);
+ const auto decryptionData = QCA::SecureArray(EMPTY_MESSAGE_DECRYPTION_DATA_SIZE);
+
+ if (const auto data = createOmemoEnvelopeData(address.data(), decryptionData); data.isEmpty()) {
+ warning("OMEMO envelope for recipient JID '" % recipientJid % "' and device ID '" %
+ QString::number(recipientDeviceId) %
+ "' could not be created because its data could not be encrypted");
+ SendError error = { QStringLiteral("OMEMO envelope could not be created"), SendError::EncryptionError };
+ reportFinishedResult(interface, { error });
+ } else {
+ QXmppOmemoEnvelope omemoEnvelope;
+ omemoEnvelope.setRecipientDeviceId(recipientDeviceId);
+ if (isKeyExchange) {
+ omemoEnvelope.setIsUsedForKeyExchange(true);
+ }
+ omemoEnvelope.setData(data);
+
+ QXmppOmemoElement omemoElement;
+ omemoElement.addEnvelope(recipientJid, omemoEnvelope);
+ omemoElement.setSenderDeviceId(ownDevice.id);
+
+ QXmppMessage message;
+ message.setTo(recipientJid);
+ message.addHint(QXmppMessage::Store);
+ message.setOmemoElement(omemoElement);
+
+ auto future = q->client()->sendUnencrypted(std::move(message));
+ await(future, q, [=](QXmpp::SendResult result) mutable {
+ reportFinishedResult(interface, result);
+ });
+ }
+
+ return interface.future();
+}
+
+//
+// Sets the key of this client instance's device.
+//
+// The first byte representing a version string used by the OMEMO library but
+// not needed for trust management is removed before storing it.
+// It corresponds to the fingerprint shown to users which also does not contain
+// the first byte.
+//
+QFuture<void> ManagerPrivate::storeOwnKey() const
+{
+ QFutureInterface<void> interface(QFutureInterfaceBase::Started);
+
+ auto future = trustManager->setOwnKey(ns_omemo_2, createKeyId(ownDevice.publicIdentityKey));
+ await(future, q, [=]() mutable {
+ interface.reportFinished();
+ });
+
+ return interface.future();
+}
+
+//
+// Stores a key while its trust level is determined by the used security
+// policy.
+//
+// \param keyOwnerJid bare JID of the key owner
+// \param key key to store
+//
+// \return the trust level of the stored key
+//
+QFuture<TrustLevel> ManagerPrivate::storeKeyDependingOnSecurityPolicy(const QString &keyOwnerJid, const QByteArray &key)
+{
+ QFutureInterface<TrustLevel> interface(QFutureInterfaceBase::Started);
+
+ auto awaitStoreKey = [=](const QFuture<TrustLevel> &future) mutable {
+ await(future, q, [=](TrustLevel trustLevel) mutable {
+ reportFinishedResult(interface, trustLevel);
+ });
+ };
+
+ auto future = q->securityPolicy();
+ await(future, q, [=](TrustSecurityPolicy securityPolicy) mutable {
+ switch (securityPolicy) {
+ case NoSecurityPolicy: {
+ auto future = storeKey(keyOwnerJid, key);
+ awaitStoreKey(future);
+ break;
+ }
+ case Toakafa: {
+ auto future = trustManager->hasKey(ns_omemo_2, keyOwnerJid, TrustLevel::Authenticated);
+ await(future, q, [=](bool hasAuthenticatedKey) mutable {
+ if (hasAuthenticatedKey) {
+ // If there is at least one authenticated key, add the
+ // new key as an automatically distrusted one.
+ auto future = storeKey(keyOwnerJid, key);
+ awaitStoreKey(future);
+ } else {
+ // If no key is authenticated yet, add the new key as an
+ // automatically trusted one.
+ auto future = storeKey(keyOwnerJid, key, TrustLevel::AutomaticallyTrusted);
+ awaitStoreKey(future);
+ }
+ });
+ break;
+ }
+ default:
+ Q_UNREACHABLE();
+ }
+ });
+
+ return interface.future();
+}
+
+//
+// Stores a key.
+//
+// \param keyOwnerJid bare JID of the key owner
+// \param key key to store
+// \param trustLevel trust level of the key
+//
+// \return the trust level of the stored key
+//
+QFuture<TrustLevel> ManagerPrivate::storeKey(const QString &keyOwnerJid, const QByteArray &key, TrustLevel trustLevel) const
+{
+ QFutureInterface<TrustLevel> interface(QFutureInterfaceBase::Started);
+
+ auto future = trustManager->addKeys(ns_omemo_2, keyOwnerJid, { createKeyId(key) }, trustLevel);
+ await(future, q, [=]() mutable {
+ emit q->trustLevelsChanged({ { keyOwnerJid, key } });
+ reportFinishedResult(interface, trustLevel);
+ });
+
+ return interface.future();
+}
+
+//
+// Returns the own bare JID set in the client's configuration.
+//
+// \return the own bare JID
+//
+QString ManagerPrivate::ownBareJid() const
+{
+ return q->client()->configuration().jidBare();
+}
+
+//
+// Returns the own full JID set in the client's configuration.
+//
+// \return the own full JID
+//
+QString ManagerPrivate::ownFullJid() const
+{
+ return q->client()->configuration().jid();
+}
+
+//
+// Returns the devices with the own JID except the device of this client
+// instance.
+//
+// \return the other own devices
+//
+QHash<uint32_t, QXmppOmemoStorage::Device> ManagerPrivate::otherOwnDevices()
+{
+ return devices.value(ownBareJid());
+}
+
+//
+// Calls the logger warning method.
+//
+// \param msg warning message
+//
+void ManagerPrivate::warning(const QString &msg) const
+{
+ q->warning(msg);
+}
+
+/// \endcond
diff --git a/src/client/QXmppOmemoManager_p.h b/src/client/QXmppOmemoManager_p.h
new file mode 100644
index 00000000..7f697ed2
--- /dev/null
+++ b/src/client/QXmppOmemoManager_p.h
@@ -0,0 +1,342 @@
+// SPDX-FileCopyrightText: 2022 Melvin Keskin <melvo@olomono.de>
+// SPDX-FileCopyrightText: 2022 Linus Jahn <lnj@kaidan.im>
+//
+// 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 <QDomElement>
+#include <QTimer>
+#include <QtCrypto>
+
+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<int32_t>::max();
+constexpr uint32_t SIGNED_PRE_KEY_ID_MAX = std::numeric_limits<int32_t>::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 <rpad/> 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<QXmpp::Success, QXmppStanza::Error>;
+
+ 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<uint32_t, QByteArray> preKeyPairs;
+ QHash<uint32_t, QXmppOmemoStorage::SignedPreKeyPair> 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<QString, QHash<uint32_t, QXmppOmemoStorage::Device>> devices;
+
+ QList<QString> 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<bool> 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<QXmppE2eeExtension::MessageEncryptResult> encryptMessageForRecipients(QXmppMessage &&message,
+ QVector<QString> recipientJids,
+ TrustLevels acceptedTrustLevels);
+ template<typename T>
+ QFuture<std::optional<QXmppOmemoElement>> encryptStanza(const T &stanza, const QVector<QString> &recipientJids, TrustLevels acceptedTrustLevels);
+ std::optional<PayloadEncryptionResult> encryptPayload(const QByteArray &payload) const;
+ template<typename T>
+ QByteArray createSceEnvelope(const T &stanza);
+ QByteArray createOmemoEnvelopeData(const signal_protocol_address &address, const QCA::SecureArray &payloadDecryptionData) const;
+
+ QFuture<std::optional<QXmppMessage>> decryptMessage(QXmppMessage stanza);
+ QFuture<std::optional<IqDecryptionResult>> decryptIq(const QDomElement &iqElement);
+ template<typename T>
+ QFuture<std::optional<DecryptionResult>> decryptStanza(T stanza,
+ const QString &senderJid,
+ uint32_t senderDeviceId,
+ const QXmppOmemoEnvelope &omemoEnvelope,
+ const QByteArray &omemoPayload,
+ bool isMessageStanza = true);
+ QFuture<QByteArray> extractSceEnvelope(const QString &senderJid,
+ uint32_t senderDeviceId,
+ const QXmppOmemoEnvelope &omemoEnvelope,
+ const QByteArray &omemoPayload,
+ bool isMessageStanza);
+ QFuture<QCA::SecureArray> extractPayloadDecryptionData(const QString &senderJid,
+ uint32_t senderDeviceId,
+ const QXmppOmemoEnvelope &omemoEnvelope,
+ bool isMessageStanza = true);
+ QByteArray decryptPayload(const QCA::SecureArray &payloadDecryptionData, const QByteArray &payload) const;
+
+ QFuture<bool> publishOmemoData();
+
+ template<typename Function>
+ void publishDeviceBundle(bool isDeviceBundlesNodeExistent,
+ bool arePublishOptionsSupported,
+ bool isAutomaticCreationSupported,
+ bool isCreationAndConfigurationSupported,
+ bool isCreationSupported,
+ bool isConfigurationSupported,
+ bool isConfigNodeMaxSupported,
+ Function continuation);
+ template<typename Function>
+ void publishDeviceBundleWithoutOptions(bool isDeviceBundlesNodeExistent,
+ bool isCreationAndConfigurationSupported,
+ bool isCreationSupported,
+ bool isConfigurationSupported,
+ bool isConfigNodeMaxSupported,
+ Function continuation);
+ template<typename Function>
+ void configureNodeAndPublishDeviceBundle(bool isConfigNodeMaxSupported, Function continuation);
+ template<typename Function>
+ void createAndConfigureDeviceBundlesNode(bool isConfigNodeMaxSupported, Function continuation);
+ template<typename Function>
+ void createDeviceBundlesNode(Function continuation);
+ template<typename Function>
+ void configureDeviceBundlesNode(bool isConfigNodeMaxSupported, Function continuation);
+ template<typename Function>
+ void publishDeviceBundleItem(Function continuation);
+ template<typename Function>
+ void publishDeviceBundleItemWithOptions(Function continuation);
+ QXmppOmemoDeviceBundleItem deviceBundleItem() const;
+ QFuture<std::optional<QXmppOmemoDeviceBundle>> requestDeviceBundle(const QString &deviceOwnerJid, uint32_t deviceId) const;
+ template<typename Function>
+ void deleteDeviceBundle(Function continuation);
+
+ template<typename Function>
+ void publishDeviceElement(bool isDeviceListNodeExistent,
+ bool arePublishOptionsSupported,
+ bool isAutomaticCreationSupported,
+ bool isCreationAndConfigurationSupported,
+ bool isCreationSupported,
+ bool isConfigurationSupported,
+ Function continuation);
+ template<typename Function>
+ void publishDeviceElementWithoutOptions(bool isDeviceListNodeExistent,
+ bool isCreationAndConfigurationSupported,
+ bool isCreationSupported,
+ bool isConfigurationSupported,
+ Function continuation);
+ template<typename Function>
+ void configureNodeAndPublishDeviceElement(Function continuation);
+ template<typename Function>
+ void createAndConfigureDeviceListNode(Function continuation);
+ template<typename Function>
+ void createDeviceListNode(Function continuation);
+ template<typename Function>
+ void configureDeviceListNode(Function continuation);
+ template<typename Function>
+ void publishDeviceListItem(bool addOwnDevice, Function continuation);
+ template<typename Function>
+ void publishDeviceListItemWithOptions(Function continuation);
+ QXmppOmemoDeviceListItem deviceListItem(bool addOwnDevice = true);
+ template<typename Function>
+ void updateOwnDevicesLocally(bool isDeviceListNodeExistent, Function continuation);
+ void updateDevices(const QString &deviceOwnerJid, const QXmppOmemoDeviceListItem &deviceListItem);
+ void handleIrregularDeviceListChanges(const QString &deviceOwnerJid);
+ template<typename Function>
+ void deleteDeviceElement(Function continuation);
+
+ template<typename Function>
+ void createNode(const QString &node, Function continuation);
+ template<typename Function>
+ void createNode(const QString &node, const QXmppPubSubNodeConfig &config, Function continuation);
+ template<typename Function>
+ void configureNode(const QString &node, const QXmppPubSubNodeConfig &config, Function continuation);
+ template<typename Function>
+ void retractItem(const QString &node, uint32_t itemId, Function continuation);
+ template<typename Function>
+ void deleteNode(const QString &node, Function continuation);
+
+ template<typename T, typename Function>
+ void publishItem(const QString &node, const T &item, Function continuation);
+ template<typename T, typename Function>
+ void publishItem(const QString &node, const T &item, const QXmppPubSubPublishOptions &publishOptions, Function continuation);
+
+ template<typename T, typename Function>
+ void runPubSubQueryWithContinuation(QFuture<T> future, const QString &errorMessage, Function continuation);
+
+ QFuture<bool> changeDeviceLabel(const QString &deviceLabel);
+
+ QFuture<QXmppPubSubManager::ItemResult<QXmppOmemoDeviceListItem>> requestDeviceList(const QString &jid);
+ void subscribeToNewDeviceLists(const QString &jid, uint32_t deviceId);
+ QFuture<Result> subscribeToDeviceList(const QString &jid);
+ QFuture<QXmppOmemoManager::DevicesResult> unsubscribeFromDeviceLists(const QList<QString> &jids);
+ QFuture<Result> unsubscribeFromDeviceList(const QString &jid);
+
+ QFuture<bool> resetOwnDevice();
+ QFuture<bool> resetAll();
+
+ QFuture<bool> buildSessionForNewDevice(const QString &jid, uint32_t deviceId, QXmppOmemoStorage::Device &device);
+ QFuture<bool> 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<QXmpp::SendResult> sendEmptyMessage(const QString &recipientJid, uint32_t recipientDeviceId, bool isKeyExchange = false) const;
+ QFuture<void> storeOwnKey() const;
+ QFuture<TrustLevel> storeKeyDependingOnSecurityPolicy(const QString &keyOwnerJid, const QByteArray &key);
+ QFuture<TrustLevel> storeKey(const QString &keyOwnerJid, const QByteArray &key, TrustLevel trustLevel = TrustLevel::AutomaticallyDistrusted) const;
+ QString ownBareJid() const;
+ QString ownFullJid() const;
+ QHash<uint32_t, QXmppOmemoStorage::Device> otherOwnDevices();
+
+ void warning(const QString &msg) const;
+};
+
+#endif // QXMPPOMEMOMANAGER_P_H
diff --git a/src/client/QXmppOmemoMemoryStorage.cpp b/src/client/QXmppOmemoMemoryStorage.cpp
new file mode 100644
index 00000000..6ff81002
--- /dev/null
+++ b/src/client/QXmppOmemoMemoryStorage.cpp
@@ -0,0 +1,119 @@
+// SPDX-FileCopyrightText: 2022 Melvin Keskin <melvo@olomono.de>
+//
+// 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<QXmppOmemoStorage::OwnDevice> ownDevice;
+
+ // IDs of pre key pairs mapped to pre key pairs
+ QHash<uint32_t, QByteArray> preKeyPairs;
+
+ // IDs of signed pre key pairs mapped to signed pre key pairs
+ QHash<uint32_t, QXmppOmemoStorage::SignedPreKeyPair> signedPreKeyPairs;
+
+ // recipient JID mapped to device ID mapped to device
+ QHash<QString, QHash<uint32_t, QXmppOmemoStorage::Device>> devices;
+};
+
+///
+/// Constructs an OMEMO memory storage.
+///
+QXmppOmemoMemoryStorage::QXmppOmemoMemoryStorage()
+ : d(new QXmppOmemoMemoryStoragePrivate)
+{
+}
+
+QXmppOmemoMemoryStorage::~QXmppOmemoMemoryStorage() = default;
+
+/// \cond
+QFuture<QXmppOmemoStorage::OmemoData> QXmppOmemoMemoryStorage::allData()
+{
+ return makeReadyFuture(std::move(OmemoData { d->ownDevice,
+ d->signedPreKeyPairs,
+ d->preKeyPairs,
+ d->devices }));
+}
+
+QFuture<void> QXmppOmemoMemoryStorage::setOwnDevice(const std::optional<OwnDevice> &device)
+{
+ d->ownDevice = device;
+ return makeReadyFuture();
+}
+
+QFuture<void> QXmppOmemoMemoryStorage::addSignedPreKeyPair(const uint32_t keyId, const SignedPreKeyPair &keyPair)
+{
+ d->signedPreKeyPairs.insert(keyId, keyPair);
+ return makeReadyFuture();
+}
+
+QFuture<void> QXmppOmemoMemoryStorage::removeSignedPreKeyPair(const uint32_t keyId)
+{
+ d->signedPreKeyPairs.remove(keyId);
+ return makeReadyFuture();
+}
+
+QFuture<void> QXmppOmemoMemoryStorage::addPreKeyPairs(const QHash<uint32_t, QByteArray> &keyPairs)
+{
+ d->preKeyPairs.insert(keyPairs);
+ return makeReadyFuture();
+}
+
+QFuture<void> QXmppOmemoMemoryStorage::removePreKeyPair(const uint32_t keyId)
+{
+ d->preKeyPairs.remove(keyId);
+ return makeReadyFuture();
+}
+
+QFuture<void> QXmppOmemoMemoryStorage::addDevice(const QString &jid, const uint32_t deviceId, const QXmppOmemoStorage::Device &device)
+{
+ d->devices[jid].insert(deviceId, device);
+ return makeReadyFuture();
+}
+
+QFuture<void> 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<void> QXmppOmemoMemoryStorage::removeDevices(const QString &jid)
+{
+ d->devices.remove(jid);
+ return makeReadyFuture();
+}
+
+QFuture<void> QXmppOmemoMemoryStorage::resetAll()
+{
+ d.reset(new QXmppOmemoMemoryStoragePrivate());
+ return makeReadyFuture();
+}
+/// \endcond
diff --git a/src/client/QXmppOmemoMemoryStorage.h b/src/client/QXmppOmemoMemoryStorage.h
new file mode 100644
index 00000000..dc935de6
--- /dev/null
+++ b/src/client/QXmppOmemoMemoryStorage.h
@@ -0,0 +1,43 @@
+// SPDX-FileCopyrightText: 2022 Melvin Keskin <melvo@olomono.de>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#ifndef QXMPPOMEMOMEMORYSTORAGE_H
+#define QXMPPOMEMOMEMORYSTORAGE_H
+
+#include "QXmppGlobal.h"
+#include "QXmppOmemoStorage.h"
+
+#include <memory>
+
+class QXmppOmemoMemoryStoragePrivate;
+
+class QXMPP_EXPORT QXmppOmemoMemoryStorage : public QXmppOmemoStorage
+{
+public:
+ QXmppOmemoMemoryStorage();
+ ~QXmppOmemoMemoryStorage() override;
+
+ /// \cond
+ QFuture<OmemoData> allData() override;
+
+ QFuture<void> setOwnDevice(const std::optional<OwnDevice> &device) override;
+
+ QFuture<void> addSignedPreKeyPair(uint32_t keyId, const SignedPreKeyPair &keyPair) override;
+ QFuture<void> removeSignedPreKeyPair(uint32_t keyId) override;
+
+ QFuture<void> addPreKeyPairs(const QHash<uint32_t, QByteArray> &keyPairs) override;
+ QFuture<void> removePreKeyPair(uint32_t keyId) override;
+
+ QFuture<void> addDevice(const QString &jid, uint32_t deviceId, const Device &device) override;
+ QFuture<void> removeDevice(const QString &jid, uint32_t deviceId) override;
+ QFuture<void> removeDevices(const QString &jid) override;
+
+ QFuture<void> resetAll() override;
+ /// \endcond
+
+private:
+ std::unique_ptr<QXmppOmemoMemoryStoragePrivate> d;
+};
+
+#endif // QXMPPOMEMOMEMORYSTORAGE_H
diff --git a/src/client/QXmppOmemoStorage.cpp b/src/client/QXmppOmemoStorage.cpp
new file mode 100644
index 00000000..97b66dbe
--- /dev/null
+++ b/src/client/QXmppOmemoStorage.cpp
@@ -0,0 +1,97 @@
+// SPDX-FileCopyrightText: 2022 Melvin Keskin <melvo@olomono.de>
+//
+// 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<OwnDevice> &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<uint32_t, QByteArray> &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
new file mode 100644
index 00000000..6c1388b4
--- /dev/null
+++ b/src/client/QXmppOmemoStorage.h
@@ -0,0 +1,174 @@
+// SPDX-FileCopyrightText: 2022 Melvin Keskin <melvo@olomono.de>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#ifndef QXMPPOMEMOSTORAGE_H
+#define QXMPPOMEMOSTORAGE_H
+
+#include "QXmppGlobal.h"
+
+#include <optional>
+
+#include <QDateTime>
+#include <QFuture>
+
+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<int32_t>::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<int32_t>::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<int32_t>::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> ownDevice;
+
+ ///
+ /// Key IDs mapped to their signed pre key pairs
+ ///
+ QHash<uint32_t, SignedPreKeyPair> signedPreKeyPairs;
+
+ ///
+ /// Key IDs mapped to their pre key pairs
+ ///
+ QHash<uint32_t, QByteArray> preKeyPairs;
+
+ ///
+ /// JIDs of the device owners mapped to device IDs mapped to the other
+ /// devices (i.e., all devices except the own one)
+ ///
+ QHash<QString, QHash<uint32_t, Device>> devices;
+ };
+
+ virtual ~QXmppOmemoStorage() = default;
+
+ virtual QFuture<OmemoData> allData() = 0;
+
+ virtual QFuture<void> setOwnDevice(const std::optional<OwnDevice> &device) = 0;
+
+ virtual QFuture<void> addSignedPreKeyPair(uint32_t keyId, const SignedPreKeyPair &keyPair) = 0;
+ virtual QFuture<void> removeSignedPreKeyPair(uint32_t keyId) = 0;
+
+ virtual QFuture<void> addPreKeyPairs(const QHash<uint32_t, QByteArray> &keyPairs) = 0;
+ virtual QFuture<void> removePreKeyPair(uint32_t keyId) = 0;
+
+ virtual QFuture<void> addDevice(const QString &jid, uint32_t deviceId, const Device &device) = 0;
+ virtual QFuture<void> removeDevice(const QString &jid, uint32_t deviceId) = 0;
+ virtual QFuture<void> removeDevices(const QString &jid) = 0;
+
+ virtual QFuture<void> resetAll() = 0;
+};
+
+#endif // QXMPPOMEMOSTORAGE_H
diff --git a/src/client/QXmppTrustMemoryStorage.cpp b/src/client/QXmppTrustMemoryStorage.cpp
index daab2e73..aca9dcb9 100644
--- a/src/client/QXmppTrustMemoryStorage.cpp
+++ b/src/client/QXmppTrustMemoryStorage.cpp
@@ -6,6 +6,8 @@
#include "QXmppFutureUtils_p.h"
+#include <QMultiHash>
+
using namespace QXmpp;
using namespace QXmpp::Private;
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 4155f42c..cf6def66 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -76,8 +76,17 @@ if(WITH_GSTREAMER)
add_simple_test(qxmppcallmanager)
endif()
+if(WITH_OMEMO)
+ if(BUILD_INTERNAL_TESTS)
+ add_simple_test(qxmppomemodata)
+ endif()
+ add_simple_test(qxmppomemomemorystorage)
+
+ add_simple_test(qxmppomemomanager)
+ target_link_libraries(tst_qxmppomemomanager PkgConfig::OmemoC qca-qt${QT_VERSION_MAJOR})
+endif()
+
if(BUILD_INTERNAL_TESTS)
- add_simple_test(qxmppomemodata)
add_simple_test(qxmppsasl)
add_simple_test(qxmppstreaminitiationiq)
endif()
diff --git a/tests/qxmppomemodata/tst_qxmppomemodata.cpp b/tests/qxmppomemodata/tst_qxmppomemodata.cpp
index 778e9cea..2048cfcf 100644
--- a/tests/qxmppomemodata/tst_qxmppomemodata.cpp
+++ b/tests/qxmppomemodata/tst_qxmppomemodata.cpp
@@ -660,7 +660,7 @@ void tst_QXmppOmemoData::testOmemoIq()
QXmppOmemoIq omemoIq2;
omemoIq2.setOmemoElement(omemoElement);
- QCOMPARE(omemoIq1.omemoElement().payload(), QByteArray::fromBase64(omemoPayload));
+ QCOMPARE(omemoIq2.omemoElement().payload(), QByteArray::fromBase64(omemoPayload));
serializePacket(omemoIq2, xmlOmemoIq);
}
diff --git a/tests/qxmppomemomanager/tst_qxmppomemomanager.cpp b/tests/qxmppomemomanager/tst_qxmppomemomanager.cpp
new file mode 100644
index 00000000..f0cf2447
--- /dev/null
+++ b/tests/qxmppomemomanager/tst_qxmppomemomanager.cpp
@@ -0,0 +1,513 @@
+// SPDX-FileCopyrightText: 2022 Melvin Keskin <melvo@olomono.de>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "QXmppAtmManager.h"
+#include "QXmppAtmTrustMemoryStorage.h"
+#include "QXmppBitsOfBinaryContentId.h"
+#include "QXmppBitsOfBinaryIq.h"
+#include "QXmppCarbonManager.h"
+#include "QXmppClient.h"
+#include "QXmppDiscoveryManager.h"
+#include "QXmppE2eeMetadata.h"
+#include "QXmppMessage.h"
+#include "QXmppOmemoElement_p.h"
+#include "QXmppOmemoEnvelope_p.h"
+#include "QXmppOmemoManager.h"
+#include "QXmppOmemoManager_p.h"
+#include "QXmppOmemoMemoryStorage.h"
+#include "QXmppPubSubIq.h"
+#include "QXmppPubSubItem.h"
+#include "QXmppPubSubManager.h"
+
+#include "IntegrationTesting.h"
+#include "util.h"
+#include <QObject>
+
+using namespace QXmpp;
+using namespace QXmpp::Private;
+
+struct OmemoUser
+{
+ QXmppClient client;
+ QXmppLogger logger;
+ QXmppOmemoManager *manager;
+ QXmppCarbonManager *carbonManager;
+ QXmppDiscoveryManager *discoveryManager;
+ QXmppPubSubManager *pubSubManager;
+ std::unique_ptr<QXmppOmemoMemoryStorage> omemoStorage;
+ std::unique_ptr<QXmppAtmTrustStorage> trustStorage;
+ QXmppAtmManager *trustManager;
+};
+
+class OmemoIqHandler : public QXmppClientExtension
+{
+public:
+ OmemoIqHandler(const QXmppBitsOfBinaryIq &requestIq, const QXmppBitsOfBinaryIq &responseIq)
+ {
+ m_requestIq = requestIq;
+ m_responseIq = responseIq;
+ }
+
+ bool handleStanza(const QDomElement &stanza, const std::optional<QXmppE2eeMetadata> &e2eeMetadata) override
+ {
+ if (stanza.tagName() == "iq" && QXmppBitsOfBinaryIq::isBitsOfBinaryIq(stanza)) {
+ QXmppBitsOfBinaryIq iq;
+ iq.parse(stanza);
+
+ if (iq.cid().toContentId() != m_requestIq.cid().toContentId()) {
+ return false;
+ }
+
+ m_responseIq.setId(iq.id());
+ client()->reply(std::move(m_responseIq), e2eeMetadata);
+ return true;
+ }
+
+ return false;
+ };
+
+private:
+ QXmppBitsOfBinaryIq m_requestIq;
+ QXmppBitsOfBinaryIq m_responseIq;
+};
+
+class tst_QXmppOmemoManager : public QObject
+{
+ Q_OBJECT
+
+private slots:
+ void initTestCase();
+ void testSecurityPolicies();
+ void testTrustLevels();
+ void initOmemoUser(OmemoUser &omemoUser);
+ void testInit();
+ void testSetUp();
+ void testLoad();
+ void testSendMessage();
+ void testSendIq();
+ void finish(OmemoUser &omemoUser);
+
+private:
+ OmemoUser m_alice1;
+ OmemoUser m_alice2;
+};
+
+void tst_QXmppOmemoManager::initTestCase()
+{
+ initOmemoUser(m_alice1);
+ initOmemoUser(m_alice2);
+}
+
+void tst_QXmppOmemoManager::testSecurityPolicies()
+{
+ auto futureSecurityPolicy = m_alice1.manager->securityPolicy();
+ QVERIFY(futureSecurityPolicy.isFinished());
+ auto resultSecurityPolicy = futureSecurityPolicy.result();
+ QCOMPARE(resultSecurityPolicy, NoSecurityPolicy);
+
+ m_alice1.manager->setSecurityPolicy(Toakafa);
+
+ futureSecurityPolicy = m_alice1.manager->securityPolicy();
+ QVERIFY(futureSecurityPolicy.isFinished());
+ resultSecurityPolicy = futureSecurityPolicy.result();
+ QCOMPARE(resultSecurityPolicy, Toakafa);
+}
+
+void tst_QXmppOmemoManager::testTrustLevels()
+{
+ auto futureTrustLevel = m_alice1.manager->trustLevel(QStringLiteral("alice@example.org"),
+ QByteArray::fromBase64(QByteArrayLiteral("AZ/cF4OrUOILKO1gQBf62pQevOhBJ2NyHnXLwM4FDZU=")));
+ QVERIFY(futureTrustLevel.isFinished());
+ auto resultTrustLevel = futureTrustLevel.result();
+ QCOMPARE(resultTrustLevel, TrustLevel::Undecided);
+
+ m_alice1.manager->setTrustLevel(
+ { { QStringLiteral("alice@example.org"),
+ QByteArray::fromBase64(QByteArrayLiteral("AZ/cF4OrUOILKO1gQBf62pQevOhBJ2NyHnXLwM4FDZU=")) },
+ { QStringLiteral("bob@example.com"),
+ QByteArray::fromBase64(QByteArrayLiteral("9E51lG3vVmUn8CM7/AIcmIlLP2HPl6Ao0/VSf4VT/oA=")) } },
+ TrustLevel::Authenticated);
+
+ futureTrustLevel = m_alice1.manager->trustLevel(QStringLiteral("alice@example.org"),
+ QByteArray::fromBase64(QByteArrayLiteral("AZ/cF4OrUOILKO1gQBf62pQevOhBJ2NyHnXLwM4FDZU=")));
+ QVERIFY(futureTrustLevel.isFinished());
+ resultTrustLevel = futureTrustLevel.result();
+ QCOMPARE(resultTrustLevel, TrustLevel::Authenticated);
+}
+
+void tst_QXmppOmemoManager::initOmemoUser(OmemoUser &omemoUser)
+{
+ omemoUser.discoveryManager = new QXmppDiscoveryManager;
+ omemoUser.client.addExtension(omemoUser.discoveryManager);
+
+ omemoUser.pubSubManager = new QXmppPubSubManager;
+ omemoUser.client.addExtension(omemoUser.pubSubManager);
+
+ omemoUser.trustStorage = std::make_unique<QXmppAtmTrustMemoryStorage>();
+ omemoUser.trustManager = new QXmppAtmManager(omemoUser.trustStorage.get());
+ omemoUser.client.addExtension(omemoUser.trustManager);
+
+ omemoUser.omemoStorage = std::make_unique<QXmppOmemoMemoryStorage>();
+ omemoUser.manager = new QXmppOmemoManager(omemoUser.omemoStorage.get());
+ omemoUser.client.addExtension(omemoUser.manager);
+
+ omemoUser.carbonManager = new QXmppCarbonManager;
+ omemoUser.client.addExtension(omemoUser.carbonManager);
+
+ connect(omemoUser.carbonManager, &QXmppCarbonManager::messageSent, omemoUser.manager, &QXmppOmemoManager::handleMessage);
+ connect(omemoUser.carbonManager, &QXmppCarbonManager::messageReceived, omemoUser.manager, &QXmppOmemoManager::handleMessage);
+
+ omemoUser.logger.setLoggingType(QXmppLogger::SignalLogging);
+ omemoUser.client.setLogger(&omemoUser.logger);
+}
+
+void tst_QXmppOmemoManager::testInit()
+{
+ auto omemoStorage = std::make_unique<QXmppOmemoMemoryStorage>();
+ auto manager = std::make_unique<QXmppOmemoManager>(omemoStorage.get());
+ QVERIFY(manager->d->initGlobalContext());
+ QVERIFY(manager->d->initLocking());
+ QVERIFY(manager->d->initCryptoProvider());
+ // TODO: Test initStores()
+}
+
+void tst_QXmppOmemoManager::testSetUp()
+{
+ SKIP_IF_INTEGRATION_TESTS_DISABLED();
+
+ auto isManagerSetUp = false;
+ const QObject context;
+
+ connect(&m_alice1.client, &QXmppClient::connected, &context, [=, &isManagerSetUp]() {
+ auto future = m_alice1.manager->setUp();
+ await(future, this, [=, &isManagerSetUp](bool isSetUp) {
+ if (isSetUp) {
+ isManagerSetUp = true;
+ }
+ });
+ });
+
+ connect(&m_alice1.logger, &QXmppLogger::message, &context, [=](QXmppLogger::MessageType type, const QString &text) {
+ if (type == QXmppLogger::SentMessage) {
+ qDebug() << "SENT: " << text;
+ } else {
+ qDebug() << "RECEIVED: " << text;
+ }
+ });
+
+ m_alice1.client.connectToServer(IntegrationTests::clientConfiguration());
+
+ QTRY_VERIFY(isManagerSetUp);
+ finish(m_alice1);
+}
+
+void tst_QXmppOmemoManager::testLoad()
+{
+ auto future = m_alice1.manager->load();
+ while (!future.isFinished()) {
+ QCoreApplication::processEvents();
+ }
+ auto result = future.result();
+ QVERIFY(!result);
+
+ const QXmppOmemoStorage::OwnDevice ownDevice = { 1,
+ QStringLiteral("notebook"),
+ QByteArray::fromBase64(QByteArrayLiteral("OU5HM3loYnFjZVVaYmpSbHdab0FPTDhJVHRzUFVUcFMK")),
+ QByteArray::fromBase64(QByteArrayLiteral("TkhodEZ6cnFDeGtENWRuT1ZZdUsyaGIwQkRPdHFRSE8K")),
+ 2,
+ 3 };
+ m_alice1.omemoStorage->setOwnDevice(ownDevice);
+ m_alice1.omemoStorage->addSignedPreKeyPair(2,
+ { QDateTime::currentDateTimeUtc(),
+ QByteArray::fromBase64(QByteArrayLiteral("VEZBOTZFRjNQSVRzVE1OcnIzYmV2ZFFuM0R3WmduUWwK")) });
+ m_alice1.omemoStorage->addPreKeyPairs({ { 3,
+ QByteArray::fromBase64(QByteArrayLiteral("RmVmQ0RTTzB0Z2R2T0ZjckQ4N29PN01VTGFFMVZjUmIK")) } });
+
+ future = m_alice1.manager->load();
+ while (!future.isFinished()) {
+ QCoreApplication::processEvents();
+ }
+ result = future.result();
+ QVERIFY(result);
+
+ const auto storedOwnDevice = m_alice1.manager->ownDevice();
+ // QCOMPARE(storedOwnDevice.keyId(), m_alice1.manager->d->createKeyId(ownDevice.publicIdentityKey));
+ QCOMPARE(storedOwnDevice.label(), ownDevice.label);
+
+ m_alice1.omemoStorage->resetAll();
+}
+
+void tst_QXmppOmemoManager::testSendMessage()
+{
+ SKIP_IF_INTEGRATION_TESTS_DISABLED()
+
+ QSignalSpy disconnectedAlice1Spy(&m_alice1.client, &QXmppClient::disconnected);
+
+ auto isFirstMessageSentByAlice1 = false;
+ auto isFirstMessageDecryptedByAlice2 = false;
+ auto isEmptyOmemoMessageReceivedByAlice1 = false;
+ auto isSecondMessageSentByAlice1 = false;
+ auto isSecondMessageDecryptedByAlice2 = false;
+
+ const auto config1 = IntegrationTests::clientConfiguration();
+ auto config2 = config1;
+ config2.setResource(config2.resource() % QStringLiteral("2"));
+
+ const QObject context;
+ QString recipient = "bob@" % config1.domain();
+
+ QXmppMessage message1;
+ message1.setTo(recipient);
+ message1.setBody("Hello Bob!");
+
+ QXmppMessage message2;
+ message2.setTo(recipient);
+ message2.setBody("Hello Bob again!");
+
+ connect(&m_alice1.client, &QXmppClient::connected, &context, [=]() {
+ auto future = m_alice1.manager->setUp();
+ await(future, this, [=](bool isSetUp) {
+ if (isSetUp) {
+ m_alice1.carbonManager->setCarbonsEnabled(true);
+
+ auto future = m_alice1.manager->setSecurityPolicy(Toakafa);
+ await(future, this, [=]() {
+ auto future = m_alice2.manager->setSecurityPolicy(Toakafa);
+ await(future, this, [=]() {
+ m_alice2.client.connectToServer(config2);
+ });
+ });
+ }
+ });
+ });
+
+ connect(&m_alice2.client, &QXmppClient::connected, &context, [=]() {
+ auto future = m_alice2.manager->setUp();
+ await(future, this, [=](bool isSetUp) {
+ if (isSetUp) {
+ m_alice2.carbonManager->setCarbonsEnabled(true);
+ }
+ });
+ });
+
+ connect(&m_alice2.logger, &QXmppLogger::message, &context, [=](QXmppLogger::MessageType type, const QString &text) {
+ if (type == QXmppLogger::SentMessage) {
+ qDebug() << "Alice 2 - SENT: " << text;
+ } else {
+ qDebug() << "Alice 2 - RECEIVED: " << text;
+ }
+ });
+
+ connect(&m_alice2.client, &QXmppClient::messageReceived, &context, [=, &isFirstMessageDecryptedByAlice2, &isSecondMessageDecryptedByAlice2](const QXmppMessage &receivedMessage) {
+ // Process only encrypted stanzas.
+ if (receivedMessage.e2eeMetadata()) {
+ qDebug() << "Decrypted message:" << receivedMessage.body();
+ if (receivedMessage.body() == message1.body()) {
+ isFirstMessageDecryptedByAlice2 = true;
+ } else if (receivedMessage.body() == message2.body()) {
+ isSecondMessageDecryptedByAlice2 = true;
+ }
+ }
+ });
+
+ connect(&m_alice1.logger, &QXmppLogger::message, &context, [=, &isEmptyOmemoMessageReceivedByAlice1, &isSecondMessageSentByAlice1](QXmppLogger::MessageType type, const QString &text) mutable {
+ if (type == QXmppLogger::SentMessage) {
+ qDebug() << "Alice - SENT: " << text;
+ } else if (type == QXmppLogger::ReceivedMessage) {
+ qDebug() << "Alice - RECEIVED: " << text;
+
+ // Check if Alice 1 received an empty OMEMO message from Alice 2.
+ // If that is the case, send a second message to Alice 2.
+ // The empty OMEMO message is not emitted via QXmppClient::messageReceived().
+ // Thus, it must be parsed manually here.
+ const auto content = text.toUtf8();
+ if (content.startsWith(QByteArrayLiteral("<message "))) {
+ QXmppMessage message;
+ parsePacket(message, text.toUtf8());
+
+ if (const auto optionalOmemoElement = message.omemoElement(); optionalOmemoElement && optionalOmemoElement.value().payload().isEmpty()) {
+ isEmptyOmemoMessageReceivedByAlice1 = true;
+
+ auto future = m_alice1.client.send(std::move(message2), QXmppSendStanzaParams());
+ await(future, this, [=, &isSecondMessageSentByAlice1](QXmpp::SendResult result) {
+ if (std::get_if<QXmpp::SendSuccess>(&result)) {
+ isSecondMessageSentByAlice1 = true;
+ }
+ });
+ }
+ }
+ }
+ });
+
+ // Wait for receiving the device of Alice 2 in order to send a message to Bob and a message
+ // carbon to Alice 2.
+ connect(m_alice1.manager, &QXmppOmemoManager::deviceAdded, &context, [=, &isFirstMessageSentByAlice1](const QString &jid, uint32_t) mutable {
+ if (jid == m_alice2.client.configuration().jidBare()) {
+ if (!isFirstMessageSentByAlice1) {
+ auto future = m_alice1.client.send(std::move(message1), QXmppSendStanzaParams());
+ await(future, this, [=, &isFirstMessageSentByAlice1](QXmpp::SendResult result) {
+ if (std::get_if<QXmpp::SendSuccess>(&result)) {
+ isFirstMessageSentByAlice1 = true;
+ }
+ });
+ }
+ }
+ });
+
+ m_alice1.client.connectToServer(config1);
+
+ QTRY_VERIFY_WITH_TIMEOUT(isFirstMessageSentByAlice1, 10000);
+ QTRY_VERIFY_WITH_TIMEOUT(isFirstMessageDecryptedByAlice2, 10000);
+ QTRY_VERIFY_WITH_TIMEOUT(isEmptyOmemoMessageReceivedByAlice1, 10000);
+ QTRY_VERIFY_WITH_TIMEOUT(isSecondMessageSentByAlice1, 10000);
+ QTRY_VERIFY_WITH_TIMEOUT(isSecondMessageDecryptedByAlice2, 10000);
+
+ m_alice1.client.disconnectFromServer();
+ QVERIFY2(disconnectedAlice1Spy.wait(), "Could not disconnect from server!");
+ finish(m_alice2);
+}
+
+void tst_QXmppOmemoManager::testSendIq()
+{
+ SKIP_IF_INTEGRATION_TESTS_DISABLED()
+
+ QSignalSpy disconnectedAlice1Spy(&m_alice1.client, &QXmppClient::disconnected);
+
+ auto isFirstRequestSent = false;
+ auto isErrorResponseReceived = false;
+ auto isSecondRequestSent = false;
+ auto isResultResponseReceived = false;
+
+ const auto config1 = IntegrationTests::clientConfiguration();
+ auto config2 = config1;
+ config2.setResource(config2.resource() % QStringLiteral("2"));
+
+ const QObject context;
+
+ QXmppBitsOfBinaryIq requestIq;
+ requestIq.setTo(config2.jid());
+ requestIq.setCid(QXmppBitsOfBinaryContentId::fromContentId(QStringLiteral("sha1+8f35fef110ffc5df08d579a50083ff9308fb6242@bob.xmpp.org")));
+
+ QXmppBitsOfBinaryIq responseIq;
+ responseIq.setType(QXmppIq::Result);
+ responseIq.setTo(config1.jid());
+ responseIq.setData(QByteArray::fromBase64(QByteArrayLiteral(
+ "iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAMAAAC67D+PAAAAclBMVEUAAADYZArfaA9GIAoBAAGN"
+ "QA3MXgniaAiEOgZMIATDXRXZZhHUZBHIXhDrbQ6sUQ7OYA2TRAubRwqMQQq7VQlKHgMAAAK5WRfJ"
+ "YBOORBFoMBCwUQ/ycA6FPgvbZQpeKglNJQmrTQeOPgQyFwR6MwACAABRPE/oAAAAW0lEQVQI1xXI"
+ "Rw6EMBTAUP8kJKENnaF37n9FQPLCekAgzklhgCwfrlNHEXhrvCsxaU/SwLGAFuIWZFpBERtKm9Xf"
+ "JqH+vVWh4POqgHrsAtht095b+geYRSl57QHSPgP3+CwvAAAAAABJRU5ErkJggg==")));
+
+ OmemoIqHandler iqHandler(requestIq, responseIq);
+
+ connect(&m_alice1.client, &QXmppClient::connected, &context, [=]() {
+ auto future = m_alice1.manager->setUp();
+ await(future, this, [=](bool isSetUp) {
+ if (isSetUp) {
+ auto future = m_alice1.manager->setSecurityPolicy(Toakafa);
+ await(future, this, [=]() {
+ auto future = m_alice2.manager->setSecurityPolicy(Toakafa);
+ await(future, this, [=]() {
+ m_alice2.client.connectToServer(config2);
+ });
+ });
+ }
+ });
+ });
+
+ connect(&m_alice2.client, &QXmppClient::connected, &context, [=]() {
+ m_alice2.manager->setUp();
+ });
+
+ connect(&m_alice1.logger, &QXmppLogger::message, &context, [=](QXmppLogger::MessageType type, const QString &text) {
+ if (type == QXmppLogger::SentMessage) {
+ qDebug() << "Alice - SENT: " << text;
+ } else if (type == QXmppLogger::ReceivedMessage) {
+ qDebug() << "Alice - RECEIVED: " << text;
+ }
+ });
+
+ connect(&m_alice2.logger, &QXmppLogger::message, &context, [=](QXmppLogger::MessageType type, const QString &text) {
+ if (type == QXmppLogger::SentMessage) {
+ qDebug() << "Alice 2 - SENT: " << text;
+ } else {
+ qDebug() << "Alice 2 - RECEIVED: " << text;
+ }
+ });
+
+ // Wait for receiving the device of Alice 2 in order to send a request to it.
+ connect(m_alice1.manager, &QXmppOmemoManager::deviceAdded, &context, [=, &isFirstRequestSent, &isErrorResponseReceived, &isSecondRequestSent, &isResultResponseReceived, &iqHandler](const QString &jid, uint32_t) {
+ if (jid != m_alice2.client.configuration().jidBare()) {
+ return;
+ }
+ if (!isFirstRequestSent && !isSecondRequestSent) {
+ auto requestIqCopy = requestIq;
+ auto future = m_alice1.client.sendSensitiveIq(std::move(requestIqCopy));
+ await(future, this, [=, &isFirstRequestSent, &isErrorResponseReceived, &isSecondRequestSent, &isResultResponseReceived, &iqHandler](QXmppClient::IqResult result) {
+ if (const auto response = std::get_if<QDomElement>(&result)) {
+ isFirstRequestSent = true;
+
+ QXmppIq iq;
+ iq.parse(*response);
+
+ QCOMPARE(iq.type(), QXmppIq::Error);
+ const auto error = iq.error();
+ QCOMPARE(error.type(), QXmppStanza::Error::Cancel);
+ QCOMPARE(error.condition(), QXmppStanza::Error::FeatureNotImplemented);
+ isErrorResponseReceived = true;
+
+ m_alice2.client.addExtension(&iqHandler);
+
+ auto requestIqCopy = requestIq;
+ auto future = m_alice1.client.sendSensitiveIq(std::move(requestIqCopy));
+ await(future, this, [=, &isSecondRequestSent, &isResultResponseReceived](QXmppClient::IqResult result) {
+ if (const auto response = std::get_if<QDomElement>(&result)) {
+ isSecondRequestSent = true;
+
+ if (QXmppBitsOfBinaryIq::isBitsOfBinaryIq(*response)) {
+ QXmppBitsOfBinaryIq iq;
+ iq.parse(*response);
+ QCOMPARE(iq.data(), responseIq.data());
+ isResultResponseReceived = true;
+ }
+ }
+ });
+ }
+ });
+ }
+ });
+
+ m_alice1.client.connectToServer(config1);
+
+ QTRY_VERIFY_WITH_TIMEOUT(isFirstRequestSent, 20'000);
+ QTRY_VERIFY(isErrorResponseReceived);
+ QTRY_VERIFY(isSecondRequestSent);
+ QTRY_VERIFY(isResultResponseReceived);
+
+ m_alice1.client.disconnectFromServer();
+ QVERIFY2(disconnectedAlice1Spy.wait(), "Could not disconnect from server!");
+ finish(m_alice2);
+}
+
+void tst_QXmppOmemoManager::finish(OmemoUser &omemoUser)
+{
+ QSignalSpy disconnectedSpy(&omemoUser.client, &QXmppClient::disconnected);
+
+ bool isManagerReset = false;
+
+ auto future = omemoUser.manager->resetAll();
+ await(future, this, [=, &isManagerReset, &omemoUser](bool isReset) {
+ if (isReset) {
+ isManagerReset = true;
+ }
+
+ omemoUser.client.disconnectFromServer();
+ });
+
+ QVERIFY2(disconnectedSpy.wait(), "Could not disconnect from server!");
+ QTRY_VERIFY(isManagerReset);
+}
+
+QTEST_MAIN(tst_QXmppOmemoManager)
+#include "tst_qxmppomemomanager.moc"
diff --git a/tests/qxmppomemomemorystorage/tst_qxmppomemomemorystorage.cpp b/tests/qxmppomemomemorystorage/tst_qxmppomemomemorystorage.cpp
new file mode 100644
index 00000000..cb751ae5
--- /dev/null
+++ b/tests/qxmppomemomemorystorage/tst_qxmppomemomemorystorage.cpp
@@ -0,0 +1,310 @@
+// SPDX-FileCopyrightText: 2022 Melvin Keskin <melvo@olomono.de>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "QXmppOmemoMemoryStorage.h"
+
+#include <QtTest>
+
+class tst_QXmppOmemoMemoryStorage : public QObject
+{
+ Q_OBJECT
+
+private slots:
+ void testOwnDevice();
+ void testSignedPreKeyPairs();
+ void testPreKeyPairs();
+ void testDevices();
+ void testResetAll();
+
+private:
+ QXmppOmemoMemoryStorage m_omemoStorage;
+};
+
+void tst_QXmppOmemoMemoryStorage::testOwnDevice()
+{
+ auto future = m_omemoStorage.allData();
+ QVERIFY(future.isFinished());
+ auto optionalResult = future.result().ownDevice;
+ QVERIFY(!optionalResult);
+
+ QXmppOmemoStorage::OwnDevice ownDevice;
+
+ m_omemoStorage.setOwnDevice(ownDevice);
+
+ // Check the default values.
+ future = m_omemoStorage.allData();
+ QVERIFY(future.isFinished());
+ optionalResult = future.result().ownDevice;
+ QVERIFY(optionalResult);
+ auto result = optionalResult.value();
+ QCOMPARE(result.id, 0);
+ QVERIFY(result.label.isEmpty());
+ QVERIFY(result.privateIdentityKey.isEmpty());
+ QVERIFY(result.publicIdentityKey.isEmpty());
+ QCOMPARE(result.latestSignedPreKeyId, 1);
+ QCOMPARE(result.latestPreKeyId, 1);
+
+ ownDevice.id = 1;
+ ownDevice.label = QStringLiteral("Notebook");
+ ownDevice.privateIdentityKey = QByteArray::fromBase64(QByteArrayLiteral("ZDVNZFdJeFFUa3N6ZWdSUG9scUdoQXFpWERGbHRsZTIK"));
+ ownDevice.publicIdentityKey = QByteArray::fromBase64(QByteArrayLiteral("dUsxSTJyM2tKVHE1TzNXbk1Xd0tpMGY0TnFleDRYUGkK"));
+ ownDevice.latestSignedPreKeyId = 2;
+ ownDevice.latestPreKeyId = 100;
+
+ m_omemoStorage.setOwnDevice(ownDevice);
+
+ // Check the set values.
+ future = m_omemoStorage.allData();
+ QVERIFY(future.isFinished());
+ optionalResult = future.result().ownDevice;
+ QVERIFY(optionalResult);
+ result = optionalResult.value();
+ QCOMPARE(result.id, 1);
+ QCOMPARE(result.label, QStringLiteral("Notebook"));
+ QCOMPARE(result.privateIdentityKey, QByteArray::fromBase64(QByteArrayLiteral("ZDVNZFdJeFFUa3N6ZWdSUG9scUdoQXFpWERGbHRsZTIK")));
+ QCOMPARE(result.publicIdentityKey, QByteArray::fromBase64(QByteArrayLiteral("dUsxSTJyM2tKVHE1TzNXbk1Xd0tpMGY0TnFleDRYUGkK")));
+ QCOMPARE(result.latestSignedPreKeyId, 2);
+ QCOMPARE(result.latestPreKeyId, 100);
+}
+
+void tst_QXmppOmemoMemoryStorage::testSignedPreKeyPairs()
+{
+ auto future = m_omemoStorage.allData();
+ QVERIFY(future.isFinished());
+ auto result = future.result().signedPreKeyPairs;
+ QVERIFY(result.isEmpty());
+
+ QXmppOmemoStorage::SignedPreKeyPair signedPreKeyPair1;
+ signedPreKeyPair1.creationDate = QDateTime(QDate(2022, 01, 01), QTime());
+ signedPreKeyPair1.data = QByteArrayLiteral("FaZmWjwqppAoMff72qTzUIktGUbi4pAmds1Cuh6OElmi");
+
+ QXmppOmemoStorage::SignedPreKeyPair signedPreKeyPair2;
+ signedPreKeyPair2.creationDate = QDateTime(QDate(2022, 01, 02), QTime());
+ signedPreKeyPair2.data = QByteArrayLiteral("jsrj4UYQqaHJrlysNu0uoHgmAU8ffknPpwKJhdqLYgIU");
+
+ QHash<uint32_t, QXmppOmemoStorage::SignedPreKeyPair> signedPreKeyPairs = { { 1, signedPreKeyPair1 },
+ { 2, signedPreKeyPair2 } };
+
+ m_omemoStorage.addSignedPreKeyPair(1, signedPreKeyPair1);
+ m_omemoStorage.addSignedPreKeyPair(2, signedPreKeyPair2);
+
+ future = m_omemoStorage.allData();
+ QVERIFY(future.isFinished());
+ result = future.result().signedPreKeyPairs;
+ const auto signedPreKeyPairResult1 = result.value(1);
+ QCOMPARE(signedPreKeyPairResult1.creationDate, QDateTime(QDate(2022, 01, 01), QTime()));
+ QCOMPARE(signedPreKeyPairResult1.data, QByteArrayLiteral("FaZmWjwqppAoMff72qTzUIktGUbi4pAmds1Cuh6OElmi"));
+ const auto signedPreKeyPairResult2 = result.value(2);
+ QCOMPARE(signedPreKeyPairResult2.creationDate, QDateTime(QDate(2022, 01, 02), QTime()));
+ QCOMPARE(signedPreKeyPairResult2.data, QByteArrayLiteral("jsrj4UYQqaHJrlysNu0uoHgmAU8ffknPpwKJhdqLYgIU"));
+
+ signedPreKeyPairs.remove(1);
+ m_omemoStorage.removeSignedPreKeyPair(1);
+
+ future = m_omemoStorage.allData();
+ QVERIFY(future.isFinished());
+ result = future.result().signedPreKeyPairs;
+ const auto signedPreKeyPairResult = result.value(2);
+ QCOMPARE(signedPreKeyPairResult.creationDate, QDateTime(QDate(2022, 01, 02), QTime()));
+ QCOMPARE(signedPreKeyPairResult.data, QByteArrayLiteral("jsrj4UYQqaHJrlysNu0uoHgmAU8ffknPpwKJhdqLYgIU"));
+}
+
+void tst_QXmppOmemoMemoryStorage::testPreKeyPairs()
+{
+ auto future = m_omemoStorage.allData();
+ QVERIFY(future.isFinished());
+ auto result = future.result().preKeyPairs;
+ QVERIFY(result.isEmpty());
+
+ const QHash<uint32_t, QByteArray> preKeyPairs1 = { { 1, QByteArrayLiteral("RZLgD0lmL2WpJbskbGKFRMZL4zqSSvU0rElmO7UwGSVt") },
+ { 2, QByteArrayLiteral("3PGPNsf9P7pPitp9dt2uvZYT4HkxdHJAbWqLvOPXUeca") } };
+ const QHash<uint32_t, QByteArray> preKeyPairs2 = { { 3, QByteArrayLiteral("LpLBVXejfU4d0qcPOJCRNDDg9IMbOujpV3UTYtZU9LTy") } };
+
+ QHash<uint32_t, QByteArray> preKeyPairs;
+ preKeyPairs.insert(preKeyPairs1);
+ preKeyPairs.insert(preKeyPairs2);
+
+ m_omemoStorage.addPreKeyPairs(preKeyPairs1);
+ m_omemoStorage.addPreKeyPairs(preKeyPairs2);
+
+ future = m_omemoStorage.allData();
+ QVERIFY(future.isFinished());
+ result = future.result().preKeyPairs;
+ QCOMPARE(result, preKeyPairs);
+
+ preKeyPairs.remove(1);
+ m_omemoStorage.removePreKeyPair(1);
+
+ future = m_omemoStorage.allData();
+ QVERIFY(future.isFinished());
+ result = future.result().preKeyPairs;
+ QCOMPARE(result, preKeyPairs);
+}
+
+void tst_QXmppOmemoMemoryStorage::testDevices()
+{
+ auto future = m_omemoStorage.allData();
+ QVERIFY(future.isFinished());
+ auto result = future.result().devices;
+ QVERIFY(result.isEmpty());
+
+ QXmppOmemoStorage::Device deviceAlice;
+ deviceAlice.label = QStringLiteral("Desktop");
+ deviceAlice.keyId = QByteArray::fromBase64(QByteArrayLiteral("bEFLaDRQRkFlYXdyakE2aURoN0wyMzk2NTJEM2hRMgo="));
+ deviceAlice.session = QByteArray::fromBase64(QByteArrayLiteral("Cs8CCAQSIQWIhBRMdJ80tLVT7ius0H1LutRLeXBid68NH90M/kwhGxohBT+2kM/wVQ2UrZZPJBRmGZP0ZoCCWiET7KxA3ieAa888IiBSTWnp4qrTeo7z9kfKRaAFy+fYwPBI2HCSOxfC0anyPigAMmsKIQXZ95Xs7I+tOsg76eLtp266XTuCF8STa+VZkXPPJ00WSRIgmJ73wjhXPZqIt9ofB0NVwbWOKnYzQ90SHJEd/hyBHkUaJAgAEiDxXDT00+zpJd+TKJrD6nWQxQZhB8I7vCRdD/Oxw61MYjpJCiEFmTV1l+cOLEytoTp17VOEunYlCZmDqn/qoUYI/8P9ZQsaJAgBEiB/QP+9Lb0YOhSQmIr/X75Vs1FME1qzmohSzqBVTzbfZFCnf1jsR2AAaiEFPxj3VK+knGrndOjcgMXI4wEfH/0VrbgJqobGWbewYyA="));
+ deviceAlice.unrespondedSentStanzasCount = 10;
+ deviceAlice.unrespondedReceivedStanzasCount = 11;
+ deviceAlice.removalFromDeviceListDate = QDateTime(QDate(2022, 01, 01), QTime());
+
+ QXmppOmemoStorage::Device deviceBob1;
+ deviceBob1.label = QStringLiteral("Phone");
+ deviceBob1.keyId = QByteArray::fromBase64(QByteArrayLiteral("WTV6c3B2UFhYbE9OQ1d0N0ZScUhLWXpmYnY2emJoego="));
+ deviceBob1.session = QByteArray::fromBase64(QByteArrayLiteral("CvgCCAQSIQXZwE+G9R6ECMxKWPMidwcx3lPboUT2KEoea3B2T3vjUBohBQ7qW+Fb9Gi/SLsuQTv2TRixF0zLx2/mw0V4arjYSmgHIiCwuvEP2eyFU7FsbtSZBWKt+hH/DwBF7C0WrfxDrSu1bSgAMmsKIQXm5tRa73ZcUWn7fQa2YlDv+yLw1copPjdRZCrGcK7cNRIg0OXBvqBTAfyiUlLKW3LDIiSMHkRYYWDyknSJz3s+81oaJAgAEiAQlSKV+70EMYAjjW88dO52dp9e/aDhT8YUDHNFaCFUxTpJCiEF2OE4fb7Quwg0PMeJfT1uXmq/YXVaos9A7bn37TySiWkaJAgAEiDJlr5w0mBHBHZzttfVyvd2y2IzBV7bGdoX+lKHaEGIoUonCAwSIQXN7Y76Vwcsaubw8EHYaIPnBB11WjEEYcEPalwlgEUECRgCUMgnWMgnYABqIQXN7Y76Vwcsaubw8EHYaIPnBB11WjEEYcEPalwlgEUECQ=="));
+ deviceBob1.unrespondedSentStanzasCount = 20;
+ deviceBob1.unrespondedReceivedStanzasCount = 21;
+ deviceBob1.removalFromDeviceListDate = QDateTime(QDate(2022, 01, 02), QTime());
+
+ QXmppOmemoStorage::Device deviceBob2;
+ deviceBob2.label = QStringLiteral("Tablet");
+ deviceBob2.keyId = QByteArray::fromBase64(QByteArrayLiteral("U0tXcUlSVHVISzZLYUdGcW53czBtdXYxTEt2blVsbQo="));
+ deviceBob2.session = QByteArray::fromBase64(QByteArrayLiteral("CvgCCAQSIQU/tpDP8FUNlK2WTyQUZhmT9GaAglohE+ysQN4ngGvPPBohBdnAT4b1HoQIzEpY8yJ3BzHeU9uhRPYoSh5rcHZPe+NQIiBNmwyjLm5xdbf5f9ab9AASopfdiSybMFMdS4SQR5pSTygAMmsKIQW5FhVKpKUzKlhUCfoCmMwoo5jUFn7+NrcOQl6CQYraZRIgkNHGSWgeoLUvYMM8wsgqU4RUv8ymv/Kv4LLJb8q4vlEaJAgAEiA/GmWir7/6tWyOTrGXsehUnnPZhFs6zGvTDNe1LZaIeTpJCiEFa7t/sVQV2uofS36GbijY63d2B4yJKFGDu6K96cU5PFsaJAgAEiA6kX2jqwfZkN0AmNOZGLPg9J8ryrSSpo74DxU85z0q/konCE4SIQWZRzzFf3M1/gzbg9/xUsNcyiUnr5jAjLpSPOj7BOW6BBgCUKd/WKd/YABqIQWZRzzFf3M1/gzbg9/xUsNcyiUnr5jAjLpSPOj7BOW6BA=="));
+ deviceBob2.unrespondedSentStanzasCount = 30;
+ deviceBob2.unrespondedReceivedStanzasCount = 31;
+ deviceBob2.removalFromDeviceListDate = QDateTime(QDate(2022, 01, 03), QTime());
+
+ m_omemoStorage.addDevice(QStringLiteral("alice@example.org"), 1, deviceAlice);
+ m_omemoStorage.addDevice(QStringLiteral("bob@example.com"), 1, deviceBob1);
+ m_omemoStorage.addDevice(QStringLiteral("bob@example.com"), 2, deviceBob2);
+
+ future = m_omemoStorage.allData();
+ QVERIFY(future.isFinished());
+ result = future.result().devices;
+ QCOMPARE(result.size(), 2);
+
+ auto resultDevicesAlice = result.value(QStringLiteral("alice@example.org"));
+ QCOMPARE(resultDevicesAlice.size(), 1);
+
+ auto resultDeviceAlice = resultDevicesAlice.value(1);
+ QCOMPARE(resultDeviceAlice.label, QStringLiteral("Desktop"));
+ QCOMPARE(resultDeviceAlice.keyId, QByteArray::fromBase64(QByteArrayLiteral("bEFLaDRQRkFlYXdyakE2aURoN0wyMzk2NTJEM2hRMgo=")));
+ QCOMPARE(resultDeviceAlice.session, QByteArray::fromBase64(QByteArrayLiteral("Cs8CCAQSIQWIhBRMdJ80tLVT7ius0H1LutRLeXBid68NH90M/kwhGxohBT+2kM/wVQ2UrZZPJBRmGZP0ZoCCWiET7KxA3ieAa888IiBSTWnp4qrTeo7z9kfKRaAFy+fYwPBI2HCSOxfC0anyPigAMmsKIQXZ95Xs7I+tOsg76eLtp266XTuCF8STa+VZkXPPJ00WSRIgmJ73wjhXPZqIt9ofB0NVwbWOKnYzQ90SHJEd/hyBHkUaJAgAEiDxXDT00+zpJd+TKJrD6nWQxQZhB8I7vCRdD/Oxw61MYjpJCiEFmTV1l+cOLEytoTp17VOEunYlCZmDqn/qoUYI/8P9ZQsaJAgBEiB/QP+9Lb0YOhSQmIr/X75Vs1FME1qzmohSzqBVTzbfZFCnf1jsR2AAaiEFPxj3VK+knGrndOjcgMXI4wEfH/0VrbgJqobGWbewYyA=")));
+ QCOMPARE(resultDeviceAlice.unrespondedSentStanzasCount, 10);
+ QCOMPARE(resultDeviceAlice.unrespondedReceivedStanzasCount, 11);
+ QCOMPARE(resultDeviceAlice.removalFromDeviceListDate, QDateTime(QDate(2022, 01, 01), QTime()));
+
+ auto resultDevicesBob = result.value(QStringLiteral("bob@example.com"));
+ QCOMPARE(resultDevicesBob.size(), 2);
+
+ auto resultDeviceBob1 = resultDevicesBob.value(1);
+ QCOMPARE(resultDeviceBob1.label, QStringLiteral("Phone"));
+ QCOMPARE(resultDeviceBob1.keyId, QByteArray::fromBase64(QByteArrayLiteral("WTV6c3B2UFhYbE9OQ1d0N0ZScUhLWXpmYnY2emJoego=")));
+ QCOMPARE(resultDeviceBob1.session, QByteArray::fromBase64(QByteArrayLiteral("CvgCCAQSIQXZwE+G9R6ECMxKWPMidwcx3lPboUT2KEoea3B2T3vjUBohBQ7qW+Fb9Gi/SLsuQTv2TRixF0zLx2/mw0V4arjYSmgHIiCwuvEP2eyFU7FsbtSZBWKt+hH/DwBF7C0WrfxDrSu1bSgAMmsKIQXm5tRa73ZcUWn7fQa2YlDv+yLw1copPjdRZCrGcK7cNRIg0OXBvqBTAfyiUlLKW3LDIiSMHkRYYWDyknSJz3s+81oaJAgAEiAQlSKV+70EMYAjjW88dO52dp9e/aDhT8YUDHNFaCFUxTpJCiEF2OE4fb7Quwg0PMeJfT1uXmq/YXVaos9A7bn37TySiWkaJAgAEiDJlr5w0mBHBHZzttfVyvd2y2IzBV7bGdoX+lKHaEGIoUonCAwSIQXN7Y76Vwcsaubw8EHYaIPnBB11WjEEYcEPalwlgEUECRgCUMgnWMgnYABqIQXN7Y76Vwcsaubw8EHYaIPnBB11WjEEYcEPalwlgEUECQ==")));
+ QCOMPARE(resultDeviceBob1.unrespondedSentStanzasCount, 20);
+ QCOMPARE(resultDeviceBob1.unrespondedReceivedStanzasCount, 21);
+ QCOMPARE(resultDeviceBob1.removalFromDeviceListDate, QDateTime(QDate(2022, 01, 02), QTime()));
+
+ auto resultDeviceBob2 = resultDevicesBob.value(2);
+ QCOMPARE(resultDeviceBob2.label, QStringLiteral("Tablet"));
+ QCOMPARE(resultDeviceBob2.keyId, QByteArray::fromBase64(QByteArrayLiteral("U0tXcUlSVHVISzZLYUdGcW53czBtdXYxTEt2blVsbQo=")));
+ QCOMPARE(resultDeviceBob2.session, QByteArray::fromBase64(QByteArrayLiteral("CvgCCAQSIQU/tpDP8FUNlK2WTyQUZhmT9GaAglohE+ysQN4ngGvPPBohBdnAT4b1HoQIzEpY8yJ3BzHeU9uhRPYoSh5rcHZPe+NQIiBNmwyjLm5xdbf5f9ab9AASopfdiSybMFMdS4SQR5pSTygAMmsKIQW5FhVKpKUzKlhUCfoCmMwoo5jUFn7+NrcOQl6CQYraZRIgkNHGSWgeoLUvYMM8wsgqU4RUv8ymv/Kv4LLJb8q4vlEaJAgAEiA/GmWir7/6tWyOTrGXsehUnnPZhFs6zGvTDNe1LZaIeTpJCiEFa7t/sVQV2uofS36GbijY63d2B4yJKFGDu6K96cU5PFsaJAgAEiA6kX2jqwfZkN0AmNOZGLPg9J8ryrSSpo74DxU85z0q/konCE4SIQWZRzzFf3M1/gzbg9/xUsNcyiUnr5jAjLpSPOj7BOW6BBgCUKd/WKd/YABqIQWZRzzFf3M1/gzbg9/xUsNcyiUnr5jAjLpSPOj7BOW6BA==")));
+ QCOMPARE(resultDeviceBob2.unrespondedSentStanzasCount, 30);
+ QCOMPARE(resultDeviceBob2.unrespondedReceivedStanzasCount, 31);
+ QCOMPARE(resultDeviceBob2.removalFromDeviceListDate, QDateTime(QDate(2022, 01, 03), QTime()));
+
+ m_omemoStorage.removeDevice(QStringLiteral("bob@example.com"), 2);
+
+ future = m_omemoStorage.allData();
+ QVERIFY(future.isFinished());
+ result = future.result().devices;
+ QCOMPARE(result.size(), 2);
+
+ resultDevicesAlice = result.value(QStringLiteral("alice@example.org"));
+ QCOMPARE(resultDevicesAlice.size(), 1);
+
+ resultDeviceAlice = resultDevicesAlice.value(1);
+ QCOMPARE(resultDeviceAlice.label, QStringLiteral("Desktop"));
+ QCOMPARE(resultDeviceAlice.keyId, QByteArray::fromBase64(QByteArrayLiteral("bEFLaDRQRkFlYXdyakE2aURoN0wyMzk2NTJEM2hRMgo=")));
+ QCOMPARE(resultDeviceAlice.session, QByteArray::fromBase64(QByteArrayLiteral("Cs8CCAQSIQWIhBRMdJ80tLVT7ius0H1LutRLeXBid68NH90M/kwhGxohBT+2kM/wVQ2UrZZPJBRmGZP0ZoCCWiET7KxA3ieAa888IiBSTWnp4qrTeo7z9kfKRaAFy+fYwPBI2HCSOxfC0anyPigAMmsKIQXZ95Xs7I+tOsg76eLtp266XTuCF8STa+VZkXPPJ00WSRIgmJ73wjhXPZqIt9ofB0NVwbWOKnYzQ90SHJEd/hyBHkUaJAgAEiDxXDT00+zpJd+TKJrD6nWQxQZhB8I7vCRdD/Oxw61MYjpJCiEFmTV1l+cOLEytoTp17VOEunYlCZmDqn/qoUYI/8P9ZQsaJAgBEiB/QP+9Lb0YOhSQmIr/X75Vs1FME1qzmohSzqBVTzbfZFCnf1jsR2AAaiEFPxj3VK+knGrndOjcgMXI4wEfH/0VrbgJqobGWbewYyA=")));
+ QCOMPARE(resultDeviceAlice.unrespondedSentStanzasCount, 10);
+ QCOMPARE(resultDeviceAlice.unrespondedReceivedStanzasCount, 11);
+ QCOMPARE(resultDeviceAlice.removalFromDeviceListDate, QDateTime(QDate(2022, 01, 01), QTime()));
+
+ resultDevicesBob = result.value(QStringLiteral("bob@example.com"));
+ QCOMPARE(resultDevicesBob.size(), 1);
+
+ resultDeviceBob1 = resultDevicesBob.value(1);
+ QCOMPARE(resultDeviceBob1.label, QStringLiteral("Phone"));
+ QCOMPARE(resultDeviceBob1.keyId, QByteArray::fromBase64(QByteArrayLiteral("WTV6c3B2UFhYbE9OQ1d0N0ZScUhLWXpmYnY2emJoego=")));
+ QCOMPARE(resultDeviceBob1.session, QByteArray::fromBase64(QByteArrayLiteral("CvgCCAQSIQXZwE+G9R6ECMxKWPMidwcx3lPboUT2KEoea3B2T3vjUBohBQ7qW+Fb9Gi/SLsuQTv2TRixF0zLx2/mw0V4arjYSmgHIiCwuvEP2eyFU7FsbtSZBWKt+hH/DwBF7C0WrfxDrSu1bSgAMmsKIQXm5tRa73ZcUWn7fQa2YlDv+yLw1copPjdRZCrGcK7cNRIg0OXBvqBTAfyiUlLKW3LDIiSMHkRYYWDyknSJz3s+81oaJAgAEiAQlSKV+70EMYAjjW88dO52dp9e/aDhT8YUDHNFaCFUxTpJCiEF2OE4fb7Quwg0PMeJfT1uXmq/YXVaos9A7bn37TySiWkaJAgAEiDJlr5w0mBHBHZzttfVyvd2y2IzBV7bGdoX+lKHaEGIoUonCAwSIQXN7Y76Vwcsaubw8EHYaIPnBB11WjEEYcEPalwlgEUECRgCUMgnWMgnYABqIQXN7Y76Vwcsaubw8EHYaIPnBB11WjEEYcEPalwlgEUECQ==")));
+ QCOMPARE(resultDeviceBob1.unrespondedSentStanzasCount, 20);
+ QCOMPARE(resultDeviceBob1.unrespondedReceivedStanzasCount, 21);
+ QCOMPARE(resultDeviceBob1.removalFromDeviceListDate, QDateTime(QDate(2022, 01, 02), QTime()));
+
+ m_omemoStorage.removeDevice(QStringLiteral("alice@example.org"), 1);
+
+ future = m_omemoStorage.allData();
+ QVERIFY(future.isFinished());
+ result = future.result().devices;
+ QCOMPARE(result.size(), 1);
+
+ resultDevicesBob = result.value(QStringLiteral("bob@example.com"));
+ QCOMPARE(resultDevicesBob.size(), 1);
+
+ resultDeviceBob1 = resultDevicesBob.value(1);
+ QCOMPARE(resultDeviceBob1.label, QStringLiteral("Phone"));
+ QCOMPARE(resultDeviceBob1.keyId, QByteArray::fromBase64(QByteArrayLiteral("WTV6c3B2UFhYbE9OQ1d0N0ZScUhLWXpmYnY2emJoego=")));
+ QCOMPARE(resultDeviceBob1.session, QByteArray::fromBase64(QByteArrayLiteral("CvgCCAQSIQXZwE+G9R6ECMxKWPMidwcx3lPboUT2KEoea3B2T3vjUBohBQ7qW+Fb9Gi/SLsuQTv2TRixF0zLx2/mw0V4arjYSmgHIiCwuvEP2eyFU7FsbtSZBWKt+hH/DwBF7C0WrfxDrSu1bSgAMmsKIQXm5tRa73ZcUWn7fQa2YlDv+yLw1copPjdRZCrGcK7cNRIg0OXBvqBTAfyiUlLKW3LDIiSMHkRYYWDyknSJz3s+81oaJAgAEiAQlSKV+70EMYAjjW88dO52dp9e/aDhT8YUDHNFaCFUxTpJCiEF2OE4fb7Quwg0PMeJfT1uXmq/YXVaos9A7bn37TySiWkaJAgAEiDJlr5w0mBHBHZzttfVyvd2y2IzBV7bGdoX+lKHaEGIoUonCAwSIQXN7Y76Vwcsaubw8EHYaIPnBB11WjEEYcEPalwlgEUECRgCUMgnWMgnYABqIQXN7Y76Vwcsaubw8EHYaIPnBB11WjEEYcEPalwlgEUECQ==")));
+ QCOMPARE(resultDeviceBob1.unrespondedSentStanzasCount, 20);
+ QCOMPARE(resultDeviceBob1.unrespondedReceivedStanzasCount, 21);
+ QCOMPARE(resultDeviceBob1.removalFromDeviceListDate, QDateTime(QDate(2022, 01, 02), QTime()));
+
+ m_omemoStorage.addDevice(QStringLiteral("alice@example.org"), 1, deviceAlice);
+ m_omemoStorage.addDevice(QStringLiteral("bob@example.com"), 2, deviceBob2);
+ m_omemoStorage.removeDevices(QStringLiteral("bob@example.com"));
+
+ future = m_omemoStorage.allData();
+ QVERIFY(future.isFinished());
+ result = future.result().devices;
+ QCOMPARE(result.size(), 1);
+
+ resultDevicesAlice = result.value(QStringLiteral("alice@example.org"));
+ QCOMPARE(resultDevicesAlice.size(), 1);
+
+ resultDeviceAlice = resultDevicesAlice.value(1);
+ QCOMPARE(resultDeviceAlice.label, QStringLiteral("Desktop"));
+ QCOMPARE(resultDeviceAlice.keyId, QByteArray::fromBase64(QByteArrayLiteral("bEFLaDRQRkFlYXdyakE2aURoN0wyMzk2NTJEM2hRMgo=")));
+ QCOMPARE(resultDeviceAlice.session, QByteArray::fromBase64(QByteArrayLiteral("Cs8CCAQSIQWIhBRMdJ80tLVT7ius0H1LutRLeXBid68NH90M/kwhGxohBT+2kM/wVQ2UrZZPJBRmGZP0ZoCCWiET7KxA3ieAa888IiBSTWnp4qrTeo7z9kfKRaAFy+fYwPBI2HCSOxfC0anyPigAMmsKIQXZ95Xs7I+tOsg76eLtp266XTuCF8STa+VZkXPPJ00WSRIgmJ73wjhXPZqIt9ofB0NVwbWOKnYzQ90SHJEd/hyBHkUaJAgAEiDxXDT00+zpJd+TKJrD6nWQxQZhB8I7vCRdD/Oxw61MYjpJCiEFmTV1l+cOLEytoTp17VOEunYlCZmDqn/qoUYI/8P9ZQsaJAgBEiB/QP+9Lb0YOhSQmIr/X75Vs1FME1qzmohSzqBVTzbfZFCnf1jsR2AAaiEFPxj3VK+knGrndOjcgMXI4wEfH/0VrbgJqobGWbewYyA=")));
+ QCOMPARE(resultDeviceAlice.unrespondedSentStanzasCount, 10);
+ QCOMPARE(resultDeviceAlice.unrespondedReceivedStanzasCount, 11);
+ QCOMPARE(resultDeviceAlice.removalFromDeviceListDate, QDateTime(QDate(2022, 01, 01), QTime()));
+}
+
+void tst_QXmppOmemoMemoryStorage::testResetAll()
+{
+ m_omemoStorage.setOwnDevice(QXmppOmemoStorage::OwnDevice());
+
+ QXmppOmemoStorage::SignedPreKeyPair signedPreKeyPair;
+ signedPreKeyPair.creationDate = QDateTime(QDate(2022, 01, 01), QTime());
+ signedPreKeyPair.data = QByteArrayLiteral("FaZmWjwqppAoMff72qTzUIktGUbi4pAmds1Cuh6OElmi");
+ m_omemoStorage.addSignedPreKeyPair(1, signedPreKeyPair);
+
+ m_omemoStorage.addPreKeyPairs({ { 1, QByteArrayLiteral("RZLgD0lmL2WpJbskbGKFRMZL4zqSSvU0rElmO7UwGSVt") },
+ { 2, QByteArrayLiteral("3PGPNsf9P7pPitp9dt2uvZYT4HkxdHJAbWqLvOPXUeca") } });
+ m_omemoStorage.addDevice(QStringLiteral("alice@example.org"),
+ 123,
+ QXmppOmemoStorage::Device());
+
+ m_omemoStorage.resetAll();
+
+ auto future = m_omemoStorage.allData();
+ QVERIFY(future.isFinished());
+ auto result = future.result();
+ QVERIFY(!result.ownDevice);
+ QVERIFY(result.signedPreKeyPairs.isEmpty());
+ QVERIFY(result.preKeyPairs.isEmpty());
+ QVERIFY(result.devices.isEmpty());
+}
+
+QTEST_MAIN(tst_QXmppOmemoMemoryStorage)
+#include "tst_qxmppomemomemorystorage.moc"