diff options
author | Kitsune Ral <Kitsune-Ral@users.sf.net> | 2020-03-13 12:07:28 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-03-13 12:07:28 +0100 |
commit | 301d29e8db272938b0977af5db872c80f89fd708 (patch) | |
tree | 4da15b959ed522db941237826ea6aa4539f1773c | |
parent | ef81c79c4e7d24d63d1520ddefef347c11052235 (diff) | |
parent | 56d9a0addaabf2cec78e1c82a9846997a3669736 (diff) | |
download | libquotient-301d29e8db272938b0977af5db872c80f89fd708.tar.gz libquotient-301d29e8db272938b0977af5db872c80f89fd708.zip |
Merge pull request #346 from quotient-im/aa13q-e2ee-encrypted-msg
E2EE: implement receiving of the messages
-rw-r--r-- | CMakeLists.txt | 69 | ||||
-rw-r--r-- | lib/connection.cpp | 153 | ||||
-rw-r--r-- | lib/connection.h | 2 | ||||
-rw-r--r-- | lib/encryptionmanager.cpp | 177 | ||||
-rw-r--r-- | lib/encryptionmanager.h | 9 | ||||
-rw-r--r-- | lib/events/encryptedevent.cpp | 2 | ||||
-rw-r--r-- | lib/events/roomkeyevent.cpp | 11 | ||||
-rw-r--r-- | lib/events/roomkeyevent.h | 25 | ||||
-rw-r--r-- | lib/room.cpp | 248 | ||||
-rw-r--r-- | lib/room.h | 10 | ||||
-rw-r--r-- | lib/syncdata.cpp | 7 | ||||
-rw-r--r-- | lib/syncdata.h | 5 | ||||
-rw-r--r-- | libquotient.pri | 11 |
13 files changed, 569 insertions, 160 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 9fbf3a9b..26394c9d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,6 +7,8 @@ set(API_VERSION "0.6") project(Quotient VERSION "${API_VERSION}.0" LANGUAGES CXX) option(${PROJECT_NAME}_INSTALL_TESTS "install quotest (former qmc-example) application" ON) +# https://github.com/quotient-im/libQuotient/issues/369 +option(${PROJECT_NAME}_ENABLE_E2EE "end-to-end encryption (E2EE) support" OFF) include(CheckCXXCompilerFlag) if (NOT WIN32) @@ -55,22 +57,26 @@ endif() find_package(Qt5 5.9 REQUIRED Network Gui Multimedia Test) get_filename_component(Qt5_Prefix "${Qt5_DIR}/../../../.." ABSOLUTE) -if ((NOT DEFINED USE_INTREE_LIBQOLM OR USE_INTREE_LIBQOLM) - AND EXISTS ${PROJECT_SOURCE_DIR}/3rdparty/libQtOlm/lib/utils.h) - add_subdirectory(3rdparty/libQtOlm EXCLUDE_FROM_ALL) - include_directories(3rdparty/libQtOlm) - if (NOT DEFINED USE_INTREE_LIBQOLM) - set (USE_INTREE_LIBQOLM 1) +if (${PROJECT_NAME}_ENABLE_E2EE) + if ((NOT DEFINED USE_INTREE_LIBQOLM OR USE_INTREE_LIBQOLM) + AND EXISTS ${PROJECT_SOURCE_DIR}/3rdparty/libQtOlm/lib/utils.h) + add_subdirectory(3rdparty/libQtOlm EXCLUDE_FROM_ALL) + include_directories(3rdparty/libQtOlm) + if (NOT DEFINED USE_INTREE_LIBQOLM) + set (USE_INTREE_LIBQOLM 1) + endif () endif () -endif () -if (NOT USE_INTREE_LIBQOLM) - find_package(QtOlm 0.1.0 REQUIRED) - if (NOT QtOlm_FOUND) - message( WARNING "libQtOlm not found; configuration will most likely fail.") - message( WARNING "Make sure you have installed libQtOlm development files") - message( WARNING "as a package or checked out the library sources in lib/.") - message( WARNING "See also BUILDING.md") + if (NOT USE_INTREE_LIBQOLM) + find_package(QtOlm 0.1.0 REQUIRED) + if (NOT QtOlm_FOUND) + message( WARNING "libQtOlm not found; configuration will most likely fail.") + message( WARNING "Make sure you have installed libQtOlm development files") + message( WARNING "as a package or checked out the library sources in lib/.") + message( WARNING "See also BUILDING.md") + endif () endif () +else () + message( WARNING "End-to-end encryption (E2EE) support is turned off.") endif () if (GTAD_PATH) @@ -108,18 +114,20 @@ if (ABS_API_DEF_PATH AND ABS_GTAD_PATH) endif () endif () find_package(Git) -if (USE_INTREE_LIBQOLM) - message( STATUS "Using in-tree libQtOlm") - if (GIT_FOUND) - execute_process(COMMAND - "${GIT_EXECUTABLE}" rev-parse -q HEAD - WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/3rdparty/libQtOlm - OUTPUT_VARIABLE QTOLM_GIT_SHA1 - OUTPUT_STRIP_TRAILING_WHITESPACE) - message( STATUS " Library git SHA1: ${QTOLM_GIT_SHA1}") - endif (GIT_FOUND) -else () - message( STATUS "Using libQtOlm ${QtOlm_VERSION} at ${QtOlm_DIR}") +if (${PROJECT_NAME}_ENABLE_E2EE) + if (USE_INTREE_LIBQOLM) + message( STATUS "Using in-tree libQtOlm") + if (GIT_FOUND) + execute_process(COMMAND + "${GIT_EXECUTABLE}" rev-parse -q HEAD + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/3rdparty/libQtOlm + OUTPUT_VARIABLE QTOLM_GIT_SHA1 + OUTPUT_STRIP_TRAILING_WHITESPACE) + message( STATUS " Library git SHA1: ${QTOLM_GIT_SHA1}") + endif (GIT_FOUND) + else () + message( STATUS "Using libQtOlm ${QtOlm_VERSION} at ${QtOlm_DIR}") + endif () endif () message( STATUS "=============================================================================" ) message( STATUS ) @@ -159,6 +167,7 @@ set(lib_SRCS lib/events/directchatevent.cpp lib/events/encryptionevent.cpp lib/events/encryptedevent.cpp + lib/events/roomkeyevent.cpp lib/jobs/requestdata.cpp lib/jobs/basejob.cpp lib/jobs/syncjob.cpp @@ -223,6 +232,9 @@ endif() set(tests_SRCS tests/quotest.cpp) add_library(${PROJECT_NAME} ${lib_SRCS} ${api_SRCS}) +if (${PROJECT_NAME}_ENABLE_E2EE) + target_compile_definitions(${PROJECT_NAME} PUBLIC ${PROJECT_NAME}_E2EE_ENABLED) +endif() set_target_properties(${PROJECT_NAME} PROPERTIES VERSION "${PROJECT_VERSION}" SOVERSION ${API_VERSION} @@ -237,7 +249,10 @@ target_include_directories(${PROJECT_NAME} PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/lib> $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}> ) -target_link_libraries(${PROJECT_NAME} QtOlm Qt5::Core Qt5::Network Qt5::Gui Qt5::Multimedia) +if (${PROJECT_NAME}_ENABLE_E2EE) + target_link_libraries(${PROJECT_NAME} QtOlm) +endif() +target_link_libraries(${PROJECT_NAME} Qt5::Core Qt5::Network Qt5::Gui Qt5::Multimedia) set(TEST_BINARY quotest) add_executable(${TEST_BINARY} ${tests_SRCS}) diff --git a/lib/connection.cpp b/lib/connection.cpp index 5bddbb83..6ad24fba 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -19,7 +19,9 @@ #include "connection.h" #include "connectiondata.h" +#ifdef Quotient_E2EE_ENABLED #include "encryptionmanager.h" +#endif // Quotient_E2EE_ENABLED #include "room.h" #include "settings.h" #include "user.h" @@ -43,6 +45,10 @@ #include "jobs/mediathumbnailjob.h" #include "jobs/syncjob.h" +#ifdef Quotient_E2EE_ENABLED +#include "account.h" // QtOlm +#endif // Quotient_E2EE_ENABLED + #include <QtCore/QCoreApplication> #include <QtCore/QDir> #include <QtCore/QElapsedTimer> @@ -105,7 +111,9 @@ public: GetCapabilitiesJob* capabilitiesJob = nullptr; GetCapabilitiesJob::Capabilities capabilities; +#ifdef Quotient_E2EE_ENABLED QScopedPointer<EncryptionManager> encryptionManager; +#endif // Quotient_E2EE_ENABLED SyncJob* syncJob = nullptr; @@ -148,6 +156,70 @@ public: { return q->stateCacheDir().filePath("state.json"); } + + RoomEventPtr sessionDecryptMessage(const EncryptedEvent& encryptedEvent) + { +#ifndef Quotient_E2EE_ENABLED + qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; + return {}; +#else // Quotient_E2EE_ENABLED + if (encryptedEvent.algorithm() != OlmV1Curve25519AesSha2AlgoKey) + { + return {}; + } + QString identityKey = + encryptionManager->account()->curve25519IdentityKey(); + QJsonObject personalCipherObject = + encryptedEvent.ciphertext(identityKey); + if (personalCipherObject.isEmpty()) { + qCDebug(E2EE) << "Encrypted event is not for the current device"; + return {}; + } + QString decrypted = encryptionManager->sessionDecryptMessage( + personalCipherObject, encryptedEvent.senderKey().toLatin1()); + if (decrypted.isEmpty()) { + qCDebug(E2EE) << "Problem with new session from senderKey:" + << encryptedEvent.senderKey() + << encryptionManager->account()->oneTimeKeys(); + return {}; + } + + RoomEventPtr decryptedEvent = makeEvent<RoomMessageEvent>( + QJsonDocument::fromJson(decrypted.toUtf8()).object()); + + if (decryptedEvent->senderId() != encryptedEvent.senderId()) { + qCDebug(E2EE) << "Found user" << decryptedEvent->senderId() + << "instead of sender" << encryptedEvent.senderId() + << "in Olm plaintext"; + return {}; + } + + // TODO: keys to constants + QJsonObject decryptedEventObject = decryptedEvent->fullJson(); + QString recipient = + decryptedEventObject.value("recipient"_ls).toString(); + if (recipient != data->userId()) { + qCDebug(E2EE) << "Found user" << recipient << "instead of us" + << data->userId() << "in Olm plaintext"; + return {}; + } + QString ourKey = decryptedEventObject.value("recipient_keys"_ls) + .toObject() + .value(Ed25519Key) + .toString(); + if (ourKey + != QString::fromUtf8( + encryptionManager->account()->ed25519IdentityKey())) { + qCDebug(E2EE) << "Found key" << ourKey + << "instead of ours own ed25519 key" + << encryptionManager->account()->ed25519IdentityKey() + << "in Olm plaintext"; + return {}; + } + + return decryptedEvent; +#endif // Quotient_E2EE_ENABLED + } }; Connection::Connection(const QUrl& server, QObject* parent) @@ -243,17 +315,12 @@ void Connection::doConnectToServer(const QString& user, const QString& password, connect(loginJob, &BaseJob::success, this, [this, loginJob] { d->connectWithToken(loginJob->userId(), loginJob->accessToken(), loginJob->deviceId()); - - AccountSettings accountSettings(loginJob->userId()); - d->encryptionManager.reset( - new EncryptionManager(accountSettings.encryptionAccountPickle())); - if (accountSettings.encryptionAccountPickle().isEmpty()) { - accountSettings.setEncryptionAccountPickle( - d->encryptionManager->olmAccountPickle()); - } - +#ifndef Quotient_E2EE_ENABLED + qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; +#else // Quotient_E2EE_ENABLED d->encryptionManager->uploadIdentityKeys(this); d->encryptionManager->uploadOneTimeKeys(this); +#endif // Quotient_E2EE_ENABLED }); connect(loginJob, &BaseJob::failure, this, [this, loginJob] { emit loginError(loginJob->errorString(), loginJob->rawDataSample()); @@ -309,6 +376,17 @@ void Connection::Private::connectWithToken(const QString& userId, q->setObjectName(userId % '/' % deviceId); qCDebug(MAIN) << "Using server" << data->baseUrl().toDisplayString() << "by user" << userId << "from device" << deviceId; + AccountSettings accountSettings(userId); +#ifndef Quotient_E2EE_ENABLED + qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; +#else // Quotient_E2EE_ENABLED + encryptionManager.reset( + new EncryptionManager(accountSettings.encryptionAccountPickle())); + if (accountSettings.encryptionAccountPickle().isEmpty()) { + accountSettings.setEncryptionAccountPickle( + encryptionManager->olmAccountPickle()); + } +#endif // Quotient_E2EE_ENABLED emit q->stateChanged(); emit q->connected(); q->reloadCapabilities(); @@ -535,6 +613,61 @@ void Connection::onSyncSuccess(SyncData&& data, bool fromCache) d->dcLocalAdditions.clear(); d->dcLocalRemovals.clear(); } +#ifndef Quotient_E2EE_ENABLED + qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; +#else // Quotient_E2EE_ENABLED + // handling m.room_key to-device encrypted event + for (auto&& toDeviceEvent : data.takeToDeviceEvents()) { + if (toDeviceEvent->type() == EncryptedEvent::typeId()) { + event_ptr_tt<EncryptedEvent> encryptedEvent = + makeEvent<EncryptedEvent>(toDeviceEvent->fullJson()); + if (encryptedEvent->algorithm() != OlmV1Curve25519AesSha2AlgoKey) { + qCDebug(E2EE) + << "Encrypted event" << encryptedEvent->id() << "algorithm" + << encryptedEvent->algorithm() << "is not supported"; + return; + } + + // TODO: full maintaining of the device keys + // with device_lists sync extention and /keys/query + qCDebug(E2EE) << "Getting device keys for the m.room_key sender:" + << encryptedEvent->senderId(); + // d->encryptionManager->updateDeviceKeys(); + + RoomEventPtr decryptedEvent = + d->sessionDecryptMessage(*encryptedEvent.get()); + // since we are waiting for the RoomKeyEvent: + event_ptr_tt<RoomKeyEvent> roomKeyEvent = + makeEvent<RoomKeyEvent>(decryptedEvent->fullJson()); + if (!roomKeyEvent) { + qCDebug(E2EE) << "Failed to decrypt olm event from user" + << encryptedEvent->senderId(); + return; + } + Room* detectedRoom = room(roomKeyEvent->roomId()); + if (!detectedRoom) { + qCDebug(E2EE) + << "Encrypted event room id" << encryptedEvent->roomId() + << "is not found at the connection"; + return; + } + detectedRoom->handleRoomKeyEvent(roomKeyEvent.get(), + encryptedEvent->senderKey()); + } + } + // handling device_one_time_keys_count + auto deviceOneTimeKeysCount = data.deviceOneTimeKeysCount(); + if (!d->encryptionManager) + { + qCDebug(E2EE) << "Encryption manager is not there yet"; + return; + } + if (!deviceOneTimeKeysCount.isEmpty()) + { + d->encryptionManager->updateOneTimeKeyCounts(this, + deviceOneTimeKeysCount); + } +#endif // Quotient_E2EE_ENABLED } void Connection::stopSync() @@ -958,10 +1091,12 @@ QString Connection::deviceId() const { return d->data->deviceId(); } QByteArray Connection::accessToken() const { return d->data->accessToken(); } +#ifdef Quotient_E2EE_ENABLED QtOlm::Account* Connection::olmAccount() const { return d->encryptionManager->account(); } +#endif // Quotient_E2EE_ENABLED SyncJob* Connection::syncJob() const { return d->syncJob; } diff --git a/lib/connection.h b/lib/connection.h index e4109fd4..b57f0ca8 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -304,7 +304,9 @@ public: QString userId() const; QString deviceId() const; QByteArray accessToken() const; +#ifdef Quotient_E2EE_ENABLED QtOlm::Account* olmAccount() const; +#endif // Quotient_E2EE_ENABLED Q_INVOKABLE Quotient::SyncJob* syncJob() const; Q_INVOKABLE int millisToReconnect() const; diff --git a/lib/encryptionmanager.cpp b/lib/encryptionmanager.cpp index 22387cf9..0895fae9 100644 --- a/lib/encryptionmanager.cpp +++ b/lib/encryptionmanager.cpp @@ -1,3 +1,4 @@ +#ifdef Quotient_E2EE_ENABLED #include "encryptionmanager.h" #include "connection.h" @@ -9,6 +10,10 @@ #include <QtCore/QStringBuilder> #include <account.h> // QtOlm +#include <session.h> // QtOlm +#include <message.h> // QtOlm +#include <errors.h> // QtOlm +#include <utils.h> // QtOlm #include <functional> #include <memory> @@ -20,7 +25,8 @@ class EncryptionManager::Private { public: explicit Private(const QByteArray& encryptionAccountPickle, float signedKeysProportion, float oneTimeKeyThreshold) - : signedKeysProportion(move(signedKeysProportion)) + : q(nullptr) + , signedKeysProportion(move(signedKeysProportion)) , oneTimeKeyThreshold(move(oneTimeKeyThreshold)) { Q_ASSERT((0 <= signedKeysProportion) && (signedKeysProportion <= 1)); @@ -44,18 +50,23 @@ public: * until the limit is reached and it starts discarding keys, starting by * the oldest. */ - targetKeysNumber = olmAccount->maxOneTimeKeys(); // 2 // see note below + targetKeysNumber = olmAccount->maxOneTimeKeys() / 2; targetOneTimeKeyCounts = { { SignedCurve25519Key, qRound(signedKeysProportion * targetKeysNumber) }, { Curve25519Key, qRound((1 - signedKeysProportion) * targetKeysNumber) } }; + updateKeysToUpload(); } ~Private() = default; + EncryptionManager* q; + UploadKeysJob* uploadIdentityKeysJob = nullptr; + UploadKeysJob* uploadOneTimeKeysInitJob = nullptr; UploadKeysJob* uploadOneTimeKeysJob = nullptr; + QueryKeysJob* queryKeysJob = nullptr; QScopedPointer<Account> olmAccount; @@ -74,6 +85,95 @@ public: } QHash<QString, int> oneTimeKeysToUploadCounts; QHash<QString, int> targetOneTimeKeyCounts; + + // A map from senderKey to InboundSession + QMap<QString, InboundSession*> sessions; // TODO: cache + void updateDeviceKeys( + const QHash<QString, QHash<QString, QueryKeysJob::DeviceInformation>>& + deviceKeys) + { + for (auto userId : deviceKeys.keys()) { + for (auto deviceId : deviceKeys.value(userId).keys()) { + QueryKeysJob::DeviceInformation info = + deviceKeys.value(userId).value(deviceId); + // TODO: ed25519Verify, etc + } + } + } + QString sessionDecrypt(Message* message, const QString& senderKey) + { + QString decrypted; + QList<InboundSession*> senderSessions = sessions.values(senderKey); + // Try to decrypt message body using one of the known sessions for that + // device + bool sessionsPassed = false; + for (auto senderSession : senderSessions) { + if (senderSession == senderSessions.last()) { + sessionsPassed = true; + } + try { + decrypted = senderSession->decrypt(message); + qCDebug(E2EE) + << "Success decrypting Olm event using existing session" + << senderSession->id(); + break; + } catch (OlmError* e) { + if (message->messageType() == 0) { + PreKeyMessage preKeyMessage = + PreKeyMessage(message->cipherText()); + if (senderSession->matches(&preKeyMessage, senderKey)) { + // We had a matching session for a pre-key message, but + // it didn't work. This means something is wrong, so we + // fail now. + qCDebug(E2EE) + << "Error decrypting pre-key message with existing " + "Olm session" + << senderSession->id() << "reason:" << e->what(); + return QString(); + } + } + // Simply keep trying otherwise + } + } + if (sessionsPassed || senderSessions.empty()) { + if (message->messageType() > 0) { + // Not a pre-key message, we should have had a matching session + if (!sessions.empty()) { + qCDebug(E2EE) << "Error decrypting with existing sessions"; + return QString(); + } + qCDebug(E2EE) << "No existing sessions"; + return QString(); + } + // We have a pre-key message without any matching session, in this + // case we should try to create one. + InboundSession* newSession; + qCDebug(E2EE) << "try to establish new InboundSession with" << senderKey; + PreKeyMessage preKeyMessage = PreKeyMessage(message->cipherText()); + try { + newSession = new InboundSession(olmAccount.data(), + &preKeyMessage, + senderKey.toLatin1(), q); + } catch (OlmError* e) { + qCDebug(E2EE) << "Error decrypting pre-key message when trying " + "to establish a new session:" + << e->what(); + return QString(); + } + qCDebug(E2EE) << "Created new Olm session" << newSession->id(); + try { + decrypted = newSession->decrypt(message); + } catch (OlmError* e) { + qCDebug(E2EE) + << "Error decrypting pre-key message with new session" + << e->what(); + return QString(); + } + olmAccount->removeOneTimeKeys(newSession); + sessions.insert(senderKey, newSession); + } + return decrypted; + } }; EncryptionManager::EncryptionManager(const QByteArray& encryptionAccountPickle, @@ -83,7 +183,9 @@ EncryptionManager::EncryptionManager(const QByteArray& encryptionAccountPickle, , d(std::make_unique<Private>(std::move(encryptionAccountPickle), std::move(signedKeysProportion), std::move(oneTimeKeyThreshold))) -{} +{ + d->q = this; +} EncryptionManager::~EncryptionManager() = default; @@ -132,20 +234,19 @@ void EncryptionManager::uploadIdentityKeys(Connection* connection) d->olmAccount->sign(deviceKeysJsonObject) } } } }; + d->uploadIdentityKeysJob = connection->callApi<UploadKeysJob>(deviceKeys); connect(d->uploadIdentityKeysJob, &BaseJob::success, this, [this] { d->setOneTimeKeyCounts(d->uploadIdentityKeysJob->oneTimeKeyCounts()); - qDebug() << QString("Uploaded identity keys."); }); - d->uploadIdentityKeysJob = connection->callApi<UploadKeysJob>(deviceKeys); } void EncryptionManager::uploadOneTimeKeys(Connection* connection, bool forceUpdate) { if (forceUpdate || d->oneTimeKeyCounts.isEmpty()) { - auto job = connection->callApi<UploadKeysJob>(); - connect(job, &BaseJob::success, this, [job, this] { - d->setOneTimeKeyCounts(job->oneTimeKeyCounts()); + d->uploadOneTimeKeysInitJob = connection->callApi<UploadKeysJob>(); + connect(d->uploadOneTimeKeysInitJob, &BaseJob::success, this, [this] { + d->setOneTimeKeyCounts(d->uploadIdentityKeysJob->oneTimeKeyCounts()); }); } @@ -170,9 +271,17 @@ void EncryptionManager::uploadOneTimeKeys(Connection* connection, if (oneTimeKeysCounter < signedKeysToUploadCount) { QJsonObject message { { QStringLiteral("key"), it.value().toString() } }; - key = d->olmAccount->sign(message); - keyType = SignedCurve25519Key; + QByteArray signedMessage = d->olmAccount->sign(message); + QJsonObject signatures { + { connection->userId(), + QJsonObject { { Ed25519Key + QStringLiteral(":") + + connection->deviceId(), + QString::fromUtf8(signedMessage) } } } + }; + message.insert(QStringLiteral("signatures"), signatures); + key = message; + keyType = SignedCurve25519Key; } else { key = it.value(); keyType = Curve25519Key; @@ -180,13 +289,50 @@ void EncryptionManager::uploadOneTimeKeys(Connection* connection, ++oneTimeKeysCounter; oneTimeKeys.insert(QString("%1:%2").arg(keyType).arg(keyId), key); } - - d->uploadOneTimeKeysJob = connection->callApi<UploadKeysJob>(none, - oneTimeKeys); + d->uploadOneTimeKeysJob = + connection->callApi<UploadKeysJob>(none, oneTimeKeys); + connect(d->uploadOneTimeKeysJob, &BaseJob::success, this, [this] { + d->setOneTimeKeyCounts(d->uploadOneTimeKeysJob->oneTimeKeyCounts()); + }); d->olmAccount->markKeysAsPublished(); - qDebug() << QString("Uploaded new one-time keys: %1 signed, %2 unsigned.") + qCDebug(E2EE) << QString("Uploaded new one-time keys: %1 signed, %2 unsigned.") .arg(signedKeysToUploadCount) - .arg(unsignedKeysToUploadCount); + .arg(unsignedKeysToUploadCount); +} + +void EncryptionManager::updateOneTimeKeyCounts( + Connection* connection, const QHash<QString, int>& deviceOneTimeKeysCount) +{ + d->oneTimeKeyCounts = deviceOneTimeKeysCount; + if (d->oneTimeKeyShouldUpload()) { + qCDebug(E2EE) << "Uploading new one-time keys."; + uploadOneTimeKeys(connection); + } +} + +void Quotient::EncryptionManager::updateDeviceKeys( + Connection* connection, const QHash<QString, QStringList>& deviceKeys) +{ + d->queryKeysJob = connection->callApi<QueryKeysJob>(deviceKeys); + connect(d->queryKeysJob, &BaseJob::success, this, + [this] { d->updateDeviceKeys(d->queryKeysJob->deviceKeys()); }); +} + +QString EncryptionManager::sessionDecryptMessage( + const QJsonObject& personalCipherObject, const QByteArray& senderKey) +{ + QString decrypted; + int type = personalCipherObject.value(TypeKeyL).toInt(-1); + QByteArray body = personalCipherObject.value(BodyKeyL).toString().toLatin1(); + if (type == 0) { + PreKeyMessage preKeyMessage { body }; + decrypted = d->sessionDecrypt(reinterpret_cast<Message*>(&preKeyMessage), + senderKey); + } else if (type == 1) { + Message message { body }; + decrypted = d->sessionDecrypt(&message, senderKey); + } + return decrypted; } QByteArray EncryptionManager::olmAccountPickle() @@ -221,3 +367,4 @@ bool EncryptionManager::Private::oneTimeKeyShouldUpload() } return false; } +#endif // Quotient_E2EE_ENABLED diff --git a/lib/encryptionmanager.h b/lib/encryptionmanager.h index b210a85a..5df15e83 100644 --- a/lib/encryptionmanager.h +++ b/lib/encryptionmanager.h @@ -1,3 +1,4 @@ +#ifdef Quotient_E2EE_ENABLED #pragma once #include <QtCore/QObject> @@ -26,6 +27,13 @@ public: void uploadIdentityKeys(Connection* connection); void uploadOneTimeKeys(Connection* connection, bool forceUpdate = false); + void + updateOneTimeKeyCounts(Connection* connection, + const QHash<QString, int>& deviceOneTimeKeysCount); + void updateDeviceKeys(Connection* connection, + const QHash<QString, QStringList>& deviceKeys); + QString sessionDecryptMessage(const QJsonObject& personalCipherObject, + const QByteArray& senderKey); QByteArray olmAccountPickle(); QtOlm::Account* account() const; @@ -36,3 +44,4 @@ private: }; } // namespace Quotient +#endif // Quotient_E2EE_ENABLED diff --git a/lib/events/encryptedevent.cpp b/lib/events/encryptedevent.cpp index b5cedc69..dccfa540 100644 --- a/lib/events/encryptedevent.cpp +++ b/lib/events/encryptedevent.cpp @@ -28,5 +28,5 @@ EncryptedEvent::EncryptedEvent(QByteArray ciphertext, const QString& senderKey, EncryptedEvent::EncryptedEvent(const QJsonObject& obj) : RoomEvent(typeId(), obj) { - qCDebug(EVENTS) << "Encrypted event" << id(); + qCDebug(E2EE) << "Encrypted event from" << senderId(); } diff --git a/lib/events/roomkeyevent.cpp b/lib/events/roomkeyevent.cpp new file mode 100644 index 00000000..1fb2e9f5 --- /dev/null +++ b/lib/events/roomkeyevent.cpp @@ -0,0 +1,11 @@ +#include "roomkeyevent.h" + +using namespace Quotient; + +RoomKeyEvent::RoomKeyEvent(const QJsonObject &obj) : Event(typeId(), obj) +{ + _algorithm = contentJson()["algorithm"_ls].toString(); + _roomId = contentJson()["room_id"_ls].toString(); + _sessionId = contentJson()["session_id"_ls].toString(); + _sessionKey = contentJson()["session_key"_ls].toString(); +} diff --git a/lib/events/roomkeyevent.h b/lib/events/roomkeyevent.h new file mode 100644 index 00000000..e4bcfd71 --- /dev/null +++ b/lib/events/roomkeyevent.h @@ -0,0 +1,25 @@ +#pragma once + +#include "event.h" + +namespace Quotient { +class RoomKeyEvent : public Event +{ +public: + DEFINE_EVENT_TYPEID("m.room_key", RoomKeyEvent) + + RoomKeyEvent(const QJsonObject& obj); + + const QString algorithm() const { return _algorithm; } + const QString roomId() const { return _roomId; } + const QString sessionId() const { return _sessionId; } + const QString sessionKey() const { return _sessionKey; } + +private: + QString _algorithm; + QString _roomId; + QString _sessionId; + QString _sessionKey; +}; +REGISTER_EVENT_TYPE(RoomKeyEvent) +} // namespace Quotient diff --git a/lib/room.cpp b/lib/room.cpp index b29f6f48..5a966ceb 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -68,11 +68,15 @@ #include <array> #include <cmath> #include <functional> + +#ifdef Quotient_E2EE_ENABLED +#include <account.h> // QtOlm +#include <errors.h> // QtOlm #include <groupsession.h> // QtOlm -#include <message.h> // QtOlm -#include <session.h> // QtOlm +#endif // Quotient_E2EE_ENABLED using namespace Quotient; +using namespace QtOlm; using namespace std::placeholders; using std::move; #if !(defined __GLIBCXX__ && __GLIBCXX__ <= 20150123) @@ -340,6 +344,91 @@ public: QJsonObject toJson() const; +#ifdef Quotient_E2EE_ENABLED + // A map from <sessionId, messageIndex> to <event_id, origin_server_ts> + QHash<QPair<QString, uint32_t>, QPair<QString, QDateTime>> + groupSessionIndexRecord; // TODO: cache + // A map from senderKey to a map of sessionId to InboundGroupSession + // Not using QMultiHash, because we want to quickly return + // a number of relations for a given event without enumerating them. + QHash<QPair<QString, QString>, InboundGroupSession*> groupSessions; // TODO: + // cache + bool addInboundGroupSession(QString senderKey, QString sessionId, + QString sessionKey) + { + if (groupSessions.contains({ senderKey, sessionId })) { + qCDebug(E2EE) << "Inbound Megolm session" << sessionId + << "with senderKey" << senderKey << "already exists"; + return false; + } + + InboundGroupSession* megolmSession; + try { + megolmSession = new InboundGroupSession(sessionKey.toLatin1(), + InboundGroupSession::Init, + q); + } catch (OlmError* e) { + qCDebug(E2EE) << "Unable to create new InboundGroupSession" + << e->what(); + return false; + } + if (megolmSession->id() != sessionId) { + qCDebug(E2EE) << "Session ID mismatch in m.room_key event sent " + "from sender with key" + << senderKey; + return false; + } + groupSessions.insert({ senderKey, sessionId }, megolmSession); + return true; + } + + QString groupSessionDecryptMessage(QByteArray cipher, + const QString& senderKey, + const QString& sessionId, + const QString& eventId, + QDateTime timestamp) + { + std::pair<QString, uint32_t> decrypted; + QPair<QString, QString> senderSessionPairKey = + qMakePair(senderKey, sessionId); + if (!groupSessions.contains(senderSessionPairKey)) { + qCDebug(E2EE) << "Unable to decrypt event" << eventId + << "The sender's device has not sent us the keys for " + "this message"; + return QString(); + } + InboundGroupSession* senderSession = + groupSessions.value(senderSessionPairKey); + if (!senderSession) { + qCDebug(E2EE) << "Unable to decrypt event" << eventId + << "senderSessionPairKey:" << senderSessionPairKey; + return QString(); + } + try { + decrypted = senderSession->decrypt(cipher); + } catch (OlmError* e) { + qCDebug(E2EE) << "Unable to decrypt event" << eventId + << "with matching megolm session:" << e->what(); + return QString(); + } + QPair<QString, QDateTime> properties = groupSessionIndexRecord.value( + qMakePair(senderSession->id(), decrypted.second)); + if (properties.first.isEmpty()) { + groupSessionIndexRecord.insert(qMakePair(senderSession->id(), + decrypted.second), + qMakePair(eventId, timestamp)); + } else { + if ((properties.first != eventId) + || (properties.second != timestamp)) { + qCDebug(E2EE) << "Detected a replay attack on event" << eventId; + return QString(); + } + } + + return decrypted.first; + } +#endif // Quotient_E2EE_ENABLED + private: using users_shortlist_t = std::array<User*, 3>; template <typename ContT> @@ -364,7 +453,7 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState) emit baseStateLoaded(); return this == r; // loadedRoomState fires only once per room }); - qCDebug(MAIN) << "New" << toCString(initialJoinState) << "Room:" << id; + qCDebug(STATE) << "New" << toCString(initialJoinState) << "Room:" << id; } Room::~Room() { delete d; } @@ -1151,86 +1240,48 @@ const StateEventBase* Room::getCurrentState(const QString& evtType, return d->getCurrentState({ evtType, stateKey }); } -RoomEventPtr Room::decryptMessage(EncryptedEvent* encryptedEvent) +RoomEventPtr Room::decryptMessage(const EncryptedEvent& encryptedEvent) { - if (encryptedEvent->algorithm() == OlmV1Curve25519AesSha2AlgoKey) { - QString identityKey = - connection()->olmAccount()->curve25519IdentityKey(); - QJsonObject personalCipherObject = - encryptedEvent->ciphertext(identityKey); - if (personalCipherObject.isEmpty()) { - qCDebug(E2EE) << "Encrypted event is not for the current device"; +#ifndef Quotient_E2EE_ENABLED + Q_UNUSED(encryptedEvent); + qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; + return {}; +#else // Quotient_E2EE_ENABLED + if (encryptedEvent.algorithm() == MegolmV1AesSha2AlgoKey) { + QString decrypted = d->groupSessionDecryptMessage( + encryptedEvent.ciphertext(), encryptedEvent.senderKey(), + encryptedEvent.sessionId(), encryptedEvent.id(), + encryptedEvent.timestamp()); + if (decrypted.isEmpty()) { return {}; } - return makeEvent<RoomMessageEvent>(decryptMessage( - personalCipherObject, encryptedEvent->senderKey().toLatin1())); - } - if (encryptedEvent->algorithm() == MegolmV1AesSha2AlgoKey) { - return makeEvent<RoomMessageEvent>(decryptMessage( - encryptedEvent->ciphertext(), encryptedEvent->senderKey(), - encryptedEvent->deviceId(), encryptedEvent->sessionId())); + return makeEvent<RoomMessageEvent>( + QJsonDocument::fromJson(decrypted.toUtf8()).object()); } + qCDebug(E2EE) << "Algorithm of the encrypted event with id" + << encryptedEvent.id() << "is not for the current device"; return {}; +#endif // Quotient_E2EE_ENABLED } -QString Room::decryptMessage(QJsonObject personalCipherObject, - QByteArray senderKey) +void Room::handleRoomKeyEvent(RoomKeyEvent* roomKeyEvent, QString senderKey) { - QString decrypted; - - using namespace QtOlm; - // TODO: new objects to private fields: - InboundSession* session; - - int type = personalCipherObject.value(TypeKeyL).toInt(-1); - QByteArray body = personalCipherObject.value(BodyKeyL).toString().toLatin1(); - - PreKeyMessage preKeyMessage { body }; - session = - new InboundSession(connection()->olmAccount(), &preKeyMessage, senderKey, this); - if (type == 0) { - if (!session->matches(&preKeyMessage, senderKey)) { - connection()->olmAccount()->removeOneTimeKeys(session); - } - try { - decrypted = session->decrypt(&preKeyMessage); - } catch (std::runtime_error& e) { - qCWarning(EVENTS) << "Decrypt failed:" << e.what(); - } +#ifndef Quotient_E2EE_ENABLED + Q_UNUSED(roomKeyEvent); + Q_UNUSED(senderKey); + qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; + return; +#else // Quotient_E2EE_ENABLED + if (roomKeyEvent->algorithm() != MegolmV1AesSha2AlgoKey) { + qCWarning(E2EE) << "Ignoring unsupported algorithm" + << roomKeyEvent->algorithm() << "in m.room_key event"; } - else if (type == 1) { - Message message { body }; - if (!session->matches(&preKeyMessage, senderKey)) { - qCWarning(EVENTS) << "Invalid encrypted message"; - } - try { - decrypted = session->decrypt(&message); - } catch (std::runtime_error& e) { - qCWarning(EVENTS) << "Decrypt failed:" << e.what(); - } + if (d->addInboundGroupSession(senderKey, roomKeyEvent->sessionId(), + roomKeyEvent->sessionKey())) { + qCDebug(E2EE) << "added new inboundGroupSession:" + << d->groupSessions.count(); } - - return decrypted; -} - -QString Room::sessionKey(const QString& senderKey, const QString& deviceId, - const QString& sessionId) const -{ - // TODO: handling an m.room_key event - return ""; -} - -QString Room::decryptMessage(QByteArray cipher, const QString& senderKey, - const QString& deviceId, const QString& sessionId) -{ - QString decrypted; - using namespace QtOlm; - InboundGroupSession* groupSession; - groupSession = new InboundGroupSession( - sessionKey(senderKey, deviceId, sessionId).toLatin1()); - groupSession->decrypt(cipher); - // TODO: avoid replay attacks - return decrypted; +#endif // Quotient_E2EE_ENABLED } int Room::joinedCount() const @@ -1253,7 +1304,7 @@ Room::Changes Room::Private::setSummary(RoomSummary&& newSummary) { if (!summary.merge(newSummary)) return Change::NoChange; - qCDebug(MAIN).nospace().noquote() + qCDebug(STATE).nospace().noquote() << "Updated room summary for " << q->objectName() << ": " << summary; emit q->memberListChanged(); return Change::SummaryChange; @@ -1425,18 +1476,15 @@ void Room::updateData(SyncRoomData&& data, bool fromCache) if (data.unreadCount != -2 && data.unreadCount != d->unreadMessages) { qCDebug(MESSAGES) << "Setting unread_count to" << data.unreadCount; d->unreadMessages = data.unreadCount; - roomChanges |= Change::UnreadNotifsChange; emit unreadMessagesChanged(this); } if (data.highlightCount != d->highlightCount) { d->highlightCount = data.highlightCount; - roomChanges |= Change::UnreadNotifsChange; emit highlightCountChanged(); } if (data.notificationCount != d->notificationCount) { d->notificationCount = data.notificationCount; - roomChanges |= Change::UnreadNotifsChange; emit notificationCountChanged(); } if (roomChanges != Change::NoChange) { @@ -1737,10 +1785,11 @@ void Room::checkVersion() // or the server capabilities have been loaded. emit stabilityUpdated(defaultVersion, stableVersions); if (!stableVersions.contains(version())) { - qCDebug(MAIN) << this << "version is" << version() - << "which the server doesn't count as stable"; + qCDebug(STATE) << this << "version is" << version() + << "which the server doesn't count as stable"; if (canSwitchVersions()) - qCDebug(MAIN) << "The current user has enough privileges to fix it"; + qCDebug(STATE) + << "The current user has enough privileges to fix it"; } } @@ -2047,7 +2096,7 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction) if (currentState.value(evtKey) == oldEvent.get()) { Q_ASSERT(ti.index() >= 0); // Historical states can't be in // currentState - qCDebug(EVENTS).nospace() + qCDebug(STATE).nospace() << "Redacting state " << oldEvent->matrixType() << "/" << oldEvent->stateKey(); // Retarget the current state to the newly made event. @@ -2101,15 +2150,15 @@ bool Room::Private::processReplacement(const RoomMessageEvent& newEvent) auto& ti = timeline[Timeline::size_type(*pIdx - q->minTimelineIndex())]; if (ti->replacedBy() == newEvent.id()) { - qCDebug(EVENTS) << "Event" << ti->id() << "is already replaced with" - << newEvent.id(); + qCDebug(STATE) << "Event" << ti->id() << "is already replaced with" + << newEvent.id(); return true; } // Make a new event from the redacted JSON and put it in the timeline // instead of the redacted one. oldEvent will be deleted on return. auto oldEvent = ti.replaceEvent(makeReplaced(*ti, newEvent)); - qCDebug(EVENTS) << "Replaced" << oldEvent->id() << "with" << newEvent.id(); + qCDebug(STATE) << "Replaced" << oldEvent->id() << "with" << newEvent.id(); emit q->replacedEvent(ti.event(), rawPtr(oldEvent)); return true; } @@ -2158,7 +2207,7 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) }); targetIt != events.end()) *targetIt = makeRedacted(**targetIt, *r); else - qCDebug(EVENTS) + qCDebug(STATE) << "Redaction" << r->id() << "ignored: target event" << r->redactedEvent() << "is not found"; // If the target event comes later, it comes already redacted. @@ -2197,10 +2246,10 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) size_t totalInserted = 0; for (auto it = events.begin(); it != events.end();) { auto nextPendingPair = - findFirstOf(it, events.end(), unsyncedEvents.begin(), - unsyncedEvents.end(), isEchoEvent); - const auto& remoteEcho = nextPendingPair.first; - const auto& localEcho = nextPendingPair.second; + findFirstOf(it, events.end(), unsyncedEvents.begin(), + unsyncedEvents.end(), isEchoEvent); + const auto& remoteEcho = nextPendingPair.first; + const auto& localEcho = nextPendingPair.second; if (it != remoteEcho) { RoomEventsRange eventsSpan { it, remoteEcho }; @@ -2258,9 +2307,9 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) } } - qCDebug(MESSAGES) << "Room" << q->objectName() << "received" - << totalInserted << "new events; the last event is now" - << timeline.back(); + qCDebug(STATE) << "Room" << q->objectName() << "received" + << totalInserted << "new events; the last event is now" + << timeline.back(); // The first event in the just-added batch (referred to by `from`) // defines whose read marker can possibly be promoted any further over @@ -2271,9 +2320,9 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) auto firstWriter = q->user((*from)->senderId()); if (q->readMarker(firstWriter) != timeline.crend()) { roomChanges |= promoteReadMarker(firstWriter, rev_iter_t(from) - 1); - qCDebug(MESSAGES) - << "Auto-promoted read marker for" << firstWriter->id() << "to" - << *q->readMarker(firstWriter); + qCDebug(STATE) << "Auto-promoted read marker for" + << firstWriter->id() << "to" + << *q->readMarker(firstWriter); } updateUnreadCount(timeline.crbegin(), rev_iter_t(from)); @@ -2310,9 +2359,8 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) const auto insertedSize = moveEventsToTimeline(events, Older); const auto from = timeline.crend() - insertedSize; - qCDebug(MESSAGES) << "Room" << displayname << "received" << insertedSize - << "past events; the oldest event is now" - << timeline.front(); + qCDebug(STATE) << "Room" << displayname << "received" << insertedSize + << "past events; the oldest event is now" << timeline.front(); q->onAddHistoricalTimelineEvents(from); emit q->addedMessages(timeline.front().index(), from->index()); @@ -2407,7 +2455,7 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) break; case MembershipType::Join: if (evt.membership() == MembershipType::Invite) - qCWarning(STATE) << "Invalid membership change from " + qCWarning(MAIN) << "Invalid membership change from " "Join to Invite:" << evt; if (evt.membership() != prevMembership) { @@ -2569,7 +2617,7 @@ Room::Changes Room::processAccountDataEvent(EventPtr&& event) emit accountDataAboutToChange(event->matrixType()); currentData = move(event); qCDebug(STATE) << "Updated account data of type" - << currentData->matrixType(); + << currentData->matrixType(); emit accountDataChanged(currentData->matrixType()); return Change::AccountDataChange; } @@ -26,6 +26,7 @@ #include "events/accountdataevents.h" #include "events/encryptedevent.h" +#include "events/roomkeyevent.h" #include "events/roommessageevent.h" #include "events/roomcreateevent.h" #include "events/roomtombstoneevent.h" @@ -208,13 +209,8 @@ public: int memberCount() const; int timelineSize() const; bool usesEncryption() const; - RoomEventPtr decryptMessage(EncryptedEvent* encryptedEvent); - QString decryptMessage(QJsonObject personalCipherObject, - QByteArray senderKey); - QString sessionKey(const QString& senderKey, const QString& deviceId, - const QString& sessionId) const; - QString decryptMessage(QByteArray cipher, const QString& senderKey, - const QString& deviceId, const QString& sessionId); + RoomEventPtr decryptMessage(const EncryptedEvent& encryptedEvent); + void handleRoomKeyEvent(RoomKeyEvent* roomKeyEvent, QString senderKey); int joinedCount() const; int invitedCount() const; int totalMemberCount() const; diff --git a/lib/syncdata.cpp b/lib/syncdata.cpp index 89c512a2..6e68e2cd 100644 --- a/lib/syncdata.cpp +++ b/lib/syncdata.cpp @@ -178,6 +178,13 @@ void SyncData::parseJson(const QJsonObject& json, const QString& baseDir) accountData = load<Events>(json, "account_data"_ls); toDeviceEvents = load<Events>(json, "to_device"_ls); + auto deviceOneTimeKeysCountVariantHash = + json.value("device_one_time_keys_count"_ls).toObject().toVariantHash(); + for (auto key : deviceOneTimeKeysCountVariantHash.keys()) { + deviceOneTimeKeysCount_.insert( + key, deviceOneTimeKeysCountVariantHash.value(key).toInt()); + } + auto rooms = json.value("rooms"_ls).toObject(); JoinStates::Int ii = 1; // ii is used to make a JoinState value auto totalRooms = 0; diff --git a/lib/syncdata.h b/lib/syncdata.h index d55438d7..6e7183ee 100644 --- a/lib/syncdata.h +++ b/lib/syncdata.h @@ -92,6 +92,10 @@ public: Events&& takePresenceData(); Events&& takeAccountData(); Events&& takeToDeviceEvents(); + const QHash<QString, int>& deviceOneTimeKeysCount() const + { + return deviceOneTimeKeysCount_; + } SyncDataList&& takeRoomData(); QString nextBatch() const { return nextBatch_; } @@ -108,6 +112,7 @@ private: Events toDeviceEvents; SyncDataList roomData; QStringList unresolvedRoomIds; + QHash<QString, int> deviceOneTimeKeysCount_; static QJsonObject loadJson(const QString& fileName); }; diff --git a/libquotient.pri b/libquotient.pri index 5a1aa7cc..95d8694b 100644 --- a/libquotient.pri +++ b/libquotient.pri @@ -8,7 +8,14 @@ win32-msvc* { QMAKE_CXXFLAGS_WARN_ON += -Wno-unused-parameter } -include(3rdparty/libQtOlm/libQtOlm.pri) +contains(DEFINES, Quotient_E2EE_ENABLED=.) { + contains(DEFINES, USE_INTREE_LIBQOLM=.) { + include(3rdparty/libQtOlm/libQtOlm.pri) + } else { + CONFIG += link_pkgconfig + PKGCONFIG += QtOlm + } +} SRCPATH = $$PWD/lib INCLUDEPATH += $$SRCPATH @@ -45,6 +52,7 @@ HEADERS += \ $$SRCPATH/events/directchatevent.h \ $$SRCPATH/events/encryptionevent.h \ $$SRCPATH/events/encryptedevent.h \ + $$SRCPATH/events/roomkeyevent.h \ $$SRCPATH/events/redactionevent.h \ $$SRCPATH/events/eventloader.h \ $$SRCPATH/events/roompowerlevelsevent.h \ @@ -93,6 +101,7 @@ SOURCES += \ $$SRCPATH/events/directchatevent.cpp \ $$SRCPATH/events/encryptionevent.cpp \ $$SRCPATH/events/encryptedevent.cpp \ + $$SRCPATH/events/roomkeyevent.cpp \ $$SRCPATH/events/roompowerlevelsevent.cpp \ $$SRCPATH/jobs/requestdata.cpp \ $$SRCPATH/jobs/basejob.cpp \ |