From 2de9cb11b4157eebacc3906df79a8d6d4fb43651 Mon Sep 17 00:00:00 2001 From: Alexey Andreyev Date: Wed, 14 Aug 2019 12:43:22 +0300 Subject: E2EE: Fix EncryptionManager initialization place Signed-off-by: Alexey Andreev --- lib/connection.cpp | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 5bddbb83..75c459d5 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -243,15 +243,6 @@ 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()); - } - d->encryptionManager->uploadIdentityKeys(this); d->encryptionManager->uploadOneTimeKeys(this); }); @@ -309,6 +300,13 @@ 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); + encryptionManager.reset( + new EncryptionManager(accountSettings.encryptionAccountPickle())); + if (accountSettings.encryptionAccountPickle().isEmpty()) { + accountSettings.setEncryptionAccountPickle( + encryptionManager->olmAccountPickle()); + } emit q->stateChanged(); emit q->connected(); q->reloadCapabilities(); -- cgit v1.2.3 From 60c0f079f0366e501de6658b5fb56ec905da0c31 Mon Sep 17 00:00:00 2001 From: Alexey Andreyev Date: Fri, 23 Aug 2019 13:23:42 +0300 Subject: E2EE: introduce RoomKeyEvent Signed-off-by: Alexey Andreev --- CMakeLists.txt | 1 + lib/events/roomkeyevent.cpp | 11 +++++++++++ lib/events/roomkeyevent.h | 25 +++++++++++++++++++++++++ 3 files changed, 37 insertions(+) create mode 100644 lib/events/roomkeyevent.cpp create mode 100644 lib/events/roomkeyevent.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 9fbf3a9b..9fc1ee6c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -159,6 +159,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 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 -- cgit v1.2.3 From 283208f8f891aafaaa0ae573bd8b9fcda783da12 Mon Sep 17 00:00:00 2001 From: Alexey Andreyev Date: Fri, 23 Aug 2019 17:03:14 +0300 Subject: E2EE: implement SyncData::deviceOneTimeKeysCount Signed-off-by: Alexey Andreev --- lib/syncdata.cpp | 7 +++++++ lib/syncdata.h | 5 +++++ 2 files changed, 12 insertions(+) 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(json, "account_data"_ls); toDeviceEvents = load(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& deviceOneTimeKeysCount() const + { + return deviceOneTimeKeysCount_; + } SyncDataList&& takeRoomData(); QString nextBatch() const { return nextBatch_; } @@ -108,6 +112,7 @@ private: Events toDeviceEvents; SyncDataList roomData; QStringList unresolvedRoomIds; + QHash deviceOneTimeKeysCount_; static QJsonObject loadJson(const QString& fileName); }; -- cgit v1.2.3 From f341e4a3c60cf4a6f6f4c986f8fe68c82feba1dd Mon Sep 17 00:00:00 2001 From: Alexey Andreyev Date: Fri, 23 Aug 2019 17:07:36 +0300 Subject: E2EE: EncryptedEvent constructor debug message Signed-off-by: Alexey Andreev --- lib/events/encryptedevent.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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(); } -- cgit v1.2.3 From 3c293b287466e8de188fc62e73efc4730ab4dd31 Mon Sep 17 00:00:00 2001 From: Alexey Andreyev Date: Fri, 23 Aug 2019 17:10:01 +0300 Subject: E2EE: fix olm session decrypt, move to EncryptionManager Signed-off-by: Alexey Andreev --- lib/encryptionmanager.cpp | 175 ++++++++++++++++++++++++++++++++++++++++++---- lib/encryptionmanager.h | 7 ++ 2 files changed, 167 insertions(+), 15 deletions(-) diff --git a/lib/encryptionmanager.cpp b/lib/encryptionmanager.cpp index 22387cf9..e2834c45 100644 --- a/lib/encryptionmanager.cpp +++ b/lib/encryptionmanager.cpp @@ -9,6 +9,10 @@ #include #include // QtOlm +#include // QtOlm +#include // QtOlm +#include // QtOlm +#include // QtOlm #include #include @@ -20,7 +24,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 +49,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 olmAccount; @@ -74,6 +84,95 @@ public: } QHash oneTimeKeysToUploadCounts; QHash targetOneTimeKeyCounts; + + // A map from senderKey to InboundSession + QMap sessions; // TODO: cache + void updateDeviceKeys( + const QHash>& + 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 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 +182,9 @@ EncryptionManager::EncryptionManager(const QByteArray& encryptionAccountPickle, , d(std::make_unique(std::move(encryptionAccountPickle), std::move(signedKeysProportion), std::move(oneTimeKeyThreshold))) -{} +{ + d->q = this; +} EncryptionManager::~EncryptionManager() = default; @@ -132,20 +233,19 @@ void EncryptionManager::uploadIdentityKeys(Connection* connection) d->olmAccount->sign(deviceKeysJsonObject) } } } }; + d->uploadIdentityKeysJob = connection->callApi(deviceKeys); connect(d->uploadIdentityKeysJob, &BaseJob::success, this, [this] { d->setOneTimeKeyCounts(d->uploadIdentityKeysJob->oneTimeKeyCounts()); - qDebug() << QString("Uploaded identity keys."); }); - d->uploadIdentityKeysJob = connection->callApi(deviceKeys); } void EncryptionManager::uploadOneTimeKeys(Connection* connection, bool forceUpdate) { if (forceUpdate || d->oneTimeKeyCounts.isEmpty()) { - auto job = connection->callApi(); - connect(job, &BaseJob::success, this, [job, this] { - d->setOneTimeKeyCounts(job->oneTimeKeyCounts()); + d->uploadOneTimeKeysInitJob = connection->callApi(); + connect(d->uploadOneTimeKeysInitJob, &BaseJob::success, this, [this] { + d->setOneTimeKeyCounts(d->uploadIdentityKeysJob->oneTimeKeyCounts()); }); } @@ -170,9 +270,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 +288,50 @@ void EncryptionManager::uploadOneTimeKeys(Connection* connection, ++oneTimeKeysCounter; oneTimeKeys.insert(QString("%1:%2").arg(keyType).arg(keyId), key); } - - d->uploadOneTimeKeysJob = connection->callApi(none, - oneTimeKeys); + d->uploadOneTimeKeysJob = + connection->callApi(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& deviceOneTimeKeysCount) +{ + d->oneTimeKeyCounts = deviceOneTimeKeysCount; + if (d->oneTimeKeyShouldUpload()) { + qCDebug(E2EE) << "Uploading new one-time keys."; + uploadOneTimeKeys(connection); + } +} + +void Quotient::EncryptionManager::updateDeviceKeys( + Connection* connection, const QHash& deviceKeys) +{ + d->queryKeysJob = connection->callApi(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(&preKeyMessage), + senderKey); + } else if (type == 1) { + Message message { body }; + decrypted = d->sessionDecrypt(&message, senderKey); + } + return decrypted; } QByteArray EncryptionManager::olmAccountPickle() diff --git a/lib/encryptionmanager.h b/lib/encryptionmanager.h index b210a85a..8f346d37 100644 --- a/lib/encryptionmanager.h +++ b/lib/encryptionmanager.h @@ -26,6 +26,13 @@ public: void uploadIdentityKeys(Connection* connection); void uploadOneTimeKeys(Connection* connection, bool forceUpdate = false); + void + updateOneTimeKeyCounts(Connection* connection, + const QHash& deviceOneTimeKeysCount); + void updateDeviceKeys(Connection* connection, + const QHash& deviceKeys); + QString sessionDecryptMessage(const QJsonObject& personalCipherObject, + const QByteArray& senderKey); QByteArray olmAccountPickle(); QtOlm::Account* account() const; -- cgit v1.2.3 From 715a0c0aa094eedbe24516e19a7b37dde71ba994 Mon Sep 17 00:00:00 2001 From: Alexey Andreyev Date: Fri, 23 Aug 2019 17:14:09 +0300 Subject: E2EE: add connection session decrypt, handle to-device and device_one_time_keys_count Signed-off-by: Alexey Andreev --- lib/connection.cpp | 112 +++++++++++++++++++++++++++++++++++++++++++++++++++++ lib/room.cpp | 5 +++ lib/room.h | 2 + 3 files changed, 119 insertions(+) diff --git a/lib/connection.cpp b/lib/connection.cpp index 75c459d5..98c8a4bc 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -43,6 +43,8 @@ #include "jobs/mediathumbnailjob.h" #include "jobs/syncjob.h" +#include "account.h" // QtOlm + #include #include #include @@ -148,6 +150,65 @@ public: { return q->stateCacheDir().filePath("state.json"); } + + RoomEventPtr sessionDecryptMessage(const EncryptedEvent& encryptedEvent) + { + 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( + 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; + } }; Connection::Connection(const QUrl& server, QObject* parent) @@ -533,6 +594,57 @@ void Connection::onSyncSuccess(SyncData&& data, bool fromCache) d->dcLocalAdditions.clear(); d->dcLocalRemovals.clear(); } + // handling m.room_key to-device encrypted event + for (auto&& toDeviceEvent : data.takeToDeviceEvents()) { + if (toDeviceEvent->type() == EncryptedEvent::typeId()) { + event_ptr_tt encryptedEvent = + makeEvent(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 = + makeEvent(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); + } } void Connection::stopSync() diff --git a/lib/room.cpp b/lib/room.cpp index b29f6f48..45c1be8b 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1233,6 +1233,11 @@ QString Room::decryptMessage(QByteArray cipher, const QString& senderKey, return decrypted; } +void Room::handleRoomKeyEvent(RoomKeyEvent *roomKeyEvent, QString senderKey) +{ + // TODO +} + int Room::joinedCount() const { return d->summary.joinedMemberCount.value_or(d->membersMap.size()); diff --git a/lib/room.h b/lib/room.h index ad19792e..2243ec6b 100644 --- a/lib/room.h +++ b/lib/room.h @@ -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" @@ -215,6 +216,7 @@ public: const QString& sessionId) const; QString decryptMessage(QByteArray cipher, const QString& senderKey, const QString& deviceId, const QString& sessionId); + void handleRoomKeyEvent(RoomKeyEvent* roomKeyEvent, QString senderKey); int joinedCount() const; int invitedCount() const; int totalMemberCount() const; -- cgit v1.2.3 From 6a3cd81b5179a38b5731f09b62f56b24ada7ff0f Mon Sep 17 00:00:00 2001 From: Alexey Andreyev Date: Fri, 23 Aug 2019 17:15:44 +0300 Subject: E2EE: implement megolm inbound session decrypt for room Signed-off-by: Alexey Andreev --- lib/room.cpp | 256 ++++++++++++++++++++++++++++++++--------------------------- lib/room.h | 8 +- 2 files changed, 140 insertions(+), 124 deletions(-) diff --git a/lib/room.cpp b/lib/room.cpp index 45c1be8b..11a50dde 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -68,11 +68,13 @@ #include #include #include + +#include // QtOlm +#include // QtOlm #include // QtOlm -#include // QtOlm -#include // QtOlm using namespace Quotient; +using namespace QtOlm; using namespace std::placeholders; using std::move; #if !(defined __GLIBCXX__ && __GLIBCXX__ <= 20150123) @@ -340,6 +342,89 @@ public: QJsonObject toJson() const; + // A map from to + QHash, QPair> + 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, 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 decrypted; + QPair 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 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; + } + private: using users_shortlist_t = std::array; template @@ -502,8 +587,8 @@ void Room::setJoinState(JoinState state) if (state == oldState) return; d->joinState = state; - qCDebug(STATE) << "Room" << id() << "changed state: " << int(oldState) - << "->" << int(state); + qCDebug(MAIN) << "Room" << id() << "changed state: " << int(oldState) + << "->" << int(state); emit changed(Change::JoinStateChange); emit joinStateChanged(oldState, state); } @@ -559,12 +644,12 @@ void Room::Private::updateUnreadCount(rev_iter_t from, rev_iter_t to) unreadMessages = 0; unreadMessages += newUnreadMessages; - qCDebug(MESSAGES) << "Room" << q->objectName() << "has gained" - << newUnreadMessages << "unread message(s)," - << (q->readMarker() == timeline.crend() - ? "in total at least" - : "in total") - << unreadMessages << "unread message(s)"; + qCDebug(MAIN) << "Room" << q->objectName() << "has gained" + << newUnreadMessages << "unread message(s)," + << (q->readMarker() == timeline.crend() + ? "in total at least" + : "in total") + << unreadMessages << "unread message(s)"; emit q->unreadMessagesChanged(q); } } @@ -606,11 +691,11 @@ Room::Changes Room::Private::promoteReadMarker(User* u, rev_iter_t newMarker, if (force || unreadMessages != oldUnreadCount) { if (unreadMessages == -1) { - qCDebug(MESSAGES) + qCDebug(MAIN) << "Room" << displayname << "has no more unread messages"; } else - qCDebug(MESSAGES) << "Room" << displayname << "still has" - << unreadMessages << "unread message(s)"; + qCDebug(MAIN) << "Room" << displayname << "still has" + << unreadMessages << "unread message(s)"; emit q->unreadMessagesChanged(q); changes |= Change::UnreadNotifsChange; } @@ -623,7 +708,7 @@ Room::Changes Room::Private::markMessagesAsRead(rev_iter_t upToMarker) const auto prevMarker = q->readMarker(); auto changes = promoteReadMarker(q->localUser(), upToMarker); if (prevMarker != upToMarker) - qCDebug(MESSAGES) << "Marked messages as read until" << *q->readMarker(); + qCDebug(MAIN) << "Marked messages as read until" << *q->readMarker(); // We shouldn't send read receipts for the local user's own messages - so // search earlier messages for the latest message not from the local user @@ -1151,91 +1236,35 @@ 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"; + if (encryptedEvent.algorithm() == MegolmV1AesSha2AlgoKey) { + QString decrypted = d->groupSessionDecryptMessage( + encryptedEvent.ciphertext(), encryptedEvent.senderKey(), + encryptedEvent.sessionId(), encryptedEvent.id(), + encryptedEvent.timestamp()); + if (decrypted.isEmpty()) { return {}; } - return makeEvent(decryptMessage( - personalCipherObject, encryptedEvent->senderKey().toLatin1())); - } - if (encryptedEvent->algorithm() == MegolmV1AesSha2AlgoKey) { - return makeEvent(decryptMessage( - encryptedEvent->ciphertext(), encryptedEvent->senderKey(), - encryptedEvent->deviceId(), encryptedEvent->sessionId())); + return makeEvent( + QJsonDocument::fromJson(decrypted.toUtf8()).object()); } + qCDebug(E2EE) << "Algorithm of the encrypted event with id" + << encryptedEvent.id() << "is not for the current device"; return {}; } -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(); - } + 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; -} - -void Room::handleRoomKeyEvent(RoomKeyEvent *roomKeyEvent, QString senderKey) -{ - // TODO } int Room::joinedCount() const @@ -1428,20 +1457,17 @@ void Room::updateData(SyncRoomData&& data, bool fromCache) // See https://github.com/quotient-im/libQuotient/wiki/unread_count if (data.unreadCount != -2 && data.unreadCount != d->unreadMessages) { - qCDebug(MESSAGES) << "Setting unread_count to" << data.unreadCount; + qCDebug(MAIN) << "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) { @@ -1544,13 +1570,12 @@ QString Room::retryMessage(const QString& txnId) if (transferIt != d->fileTransfers.end()) { Q_ASSERT(transferIt->isUpload); if (transferIt->status == FileTransferInfo::Completed) { - qCDebug(MESSAGES) - << "File for transaction" << txnId - << "has already been uploaded, bypassing re-upload"; + qCDebug(MAIN) << "File for transaction" << txnId + << "has already been uploaded, bypassing re-upload"; } else { if (isJobRunning(transferIt->job)) { - qCDebug(MESSAGES) << "Abandoning the upload job for transaction" - << txnId << "and starting again"; + qCDebug(MAIN) << "Abandoning the upload job for transaction" + << txnId << "and starting again"; transferIt->job->abandon(); emit fileTransferFailed(txnId, tr("File upload will be retried")); @@ -2036,15 +2061,15 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction) auto& ti = timeline[Timeline::size_type(*pIdx - q->minTimelineIndex())]; if (ti->isRedacted() && ti->redactedBecause()->id() == redaction.id()) { - qCDebug(EVENTS) << "Redaction" << redaction.id() << "of event" - << ti->id() << "already done, skipping"; + qCDebug(MAIN) << "Redaction" << redaction.id() << "of event" << ti->id() + << "already done, skipping"; 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(makeRedacted(*ti, redaction)); - qCDebug(EVENTS) << "Redacted" << oldEvent->id() << "with" << redaction.id(); + qCDebug(MAIN) << "Redacted" << oldEvent->id() << "with" << redaction.id(); if (oldEvent->isStateEvent()) { const StateEventKey evtKey { oldEvent->matrixType(), oldEvent->stateKey() }; @@ -2052,7 +2077,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(MAIN).nospace() << "Redacting state " << oldEvent->matrixType() << "/" << oldEvent->stateKey(); // Retarget the current state to the newly made event. @@ -2106,7 +2131,7 @@ 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" + qCDebug(MAIN) << "Event" << ti->id() << "is already replaced with" << newEvent.id(); return true; } @@ -2114,7 +2139,7 @@ bool Room::Private::processReplacement(const RoomMessageEvent& newEvent) // 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(MAIN) << "Replaced" << oldEvent->id() << "with" << newEvent.id(); emit q->replacedEvent(ti.event(), rawPtr(oldEvent)); return true; } @@ -2163,7 +2188,7 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) }); targetIt != events.end()) *targetIt = makeRedacted(**targetIt, *r); else - qCDebug(EVENTS) + qCDebug(MAIN) << "Redaction" << r->id() << "ignored: target event" << r->redactedEvent() << "is not found"; // If the target event comes later, it comes already redacted. @@ -2263,9 +2288,8 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) } } - qCDebug(MESSAGES) << "Room" << q->objectName() << "received" - << totalInserted << "new events; the last event is now" - << timeline.back(); + qCDebug(MAIN) << "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 @@ -2276,9 +2300,8 @@ 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(MAIN) << "Auto-promoted read marker for" << firstWriter->id() + << "to" << *q->readMarker(firstWriter); } updateUnreadCount(timeline.crbegin(), rev_iter_t(from)); @@ -2315,9 +2338,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(MAIN) << "Room" << displayname << "received" << insertedSize + << "past events; the oldest event is now" << timeline.front(); q->onAddHistoricalTimelineEvents(from); emit q->addedMessages(timeline.front().index(), from->index()); @@ -2412,7 +2434,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) { @@ -2559,7 +2581,7 @@ Room::Changes Room::processAccountDataEvent(EventPtr&& event) if (auto* evt = eventCast(event)) { auto readEventId = evt->event_id(); - qCDebug(STATE) << "Server-side read marker at" << readEventId; + qCDebug(MAIN) << "Server-side read marker at" << readEventId; d->serverReadMarker = readEventId; const auto newMarker = findInTimeline(readEventId); changes |= newMarker != timelineEdge() @@ -2573,7 +2595,7 @@ Room::Changes Room::processAccountDataEvent(EventPtr&& event) if (!currentData || currentData->contentJson() != event->contentJson()) { emit accountDataAboutToChange(event->matrixType()); currentData = move(event); - qCDebug(STATE) << "Updated account data of type" + qCDebug(MAIN) << "Updated account data of type" << currentData->matrixType(); emit accountDataChanged(currentData->matrixType()); return Change::AccountDataChange; diff --git a/lib/room.h b/lib/room.h index 2243ec6b..d78a7bfc 100644 --- a/lib/room.h +++ b/lib/room.h @@ -209,13 +209,7 @@ 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; -- cgit v1.2.3 From 8ff15ee94fc39eef1279d479d12b1cbdec9f4ac5 Mon Sep 17 00:00:00 2001 From: Alexey Andreyev Date: Fri, 23 Aug 2019 18:00:53 +0300 Subject: Room: fix merge regression Signed-off-by: Alexey Andreev --- lib/room.cpp | 90 +++++++++++++++++++++++++++++++----------------------------- 1 file changed, 47 insertions(+), 43 deletions(-) diff --git a/lib/room.cpp b/lib/room.cpp index 11a50dde..ecb5a7ad 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -449,7 +449,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; } @@ -587,8 +587,8 @@ void Room::setJoinState(JoinState state) if (state == oldState) return; d->joinState = state; - qCDebug(MAIN) << "Room" << id() << "changed state: " << int(oldState) - << "->" << int(state); + qCDebug(STATE) << "Room" << id() << "changed state: " << int(oldState) + << "->" << int(state); emit changed(Change::JoinStateChange); emit joinStateChanged(oldState, state); } @@ -644,12 +644,12 @@ void Room::Private::updateUnreadCount(rev_iter_t from, rev_iter_t to) unreadMessages = 0; unreadMessages += newUnreadMessages; - qCDebug(MAIN) << "Room" << q->objectName() << "has gained" - << newUnreadMessages << "unread message(s)," - << (q->readMarker() == timeline.crend() - ? "in total at least" - : "in total") - << unreadMessages << "unread message(s)"; + qCDebug(MESSAGES) << "Room" << q->objectName() << "has gained" + << newUnreadMessages << "unread message(s)," + << (q->readMarker() == timeline.crend() + ? "in total at least" + : "in total") + << unreadMessages << "unread message(s)"; emit q->unreadMessagesChanged(q); } } @@ -691,11 +691,11 @@ Room::Changes Room::Private::promoteReadMarker(User* u, rev_iter_t newMarker, if (force || unreadMessages != oldUnreadCount) { if (unreadMessages == -1) { - qCDebug(MAIN) + qCDebug(MESSAGES) << "Room" << displayname << "has no more unread messages"; } else - qCDebug(MAIN) << "Room" << displayname << "still has" - << unreadMessages << "unread message(s)"; + qCDebug(MESSAGES) << "Room" << displayname << "still has" + << unreadMessages << "unread message(s)"; emit q->unreadMessagesChanged(q); changes |= Change::UnreadNotifsChange; } @@ -708,7 +708,7 @@ Room::Changes Room::Private::markMessagesAsRead(rev_iter_t upToMarker) const auto prevMarker = q->readMarker(); auto changes = promoteReadMarker(q->localUser(), upToMarker); if (prevMarker != upToMarker) - qCDebug(MAIN) << "Marked messages as read until" << *q->readMarker(); + qCDebug(MESSAGES) << "Marked messages as read until" << *q->readMarker(); // We shouldn't send read receipts for the local user's own messages - so // search earlier messages for the latest message not from the local user @@ -1287,7 +1287,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; @@ -1457,7 +1457,7 @@ void Room::updateData(SyncRoomData&& data, bool fromCache) // See https://github.com/quotient-im/libQuotient/wiki/unread_count if (data.unreadCount != -2 && data.unreadCount != d->unreadMessages) { - qCDebug(MAIN) << "Setting unread_count to" << data.unreadCount; + qCDebug(MESSAGES) << "Setting unread_count to" << data.unreadCount; d->unreadMessages = data.unreadCount; emit unreadMessagesChanged(this); } @@ -1570,12 +1570,13 @@ QString Room::retryMessage(const QString& txnId) if (transferIt != d->fileTransfers.end()) { Q_ASSERT(transferIt->isUpload); if (transferIt->status == FileTransferInfo::Completed) { - qCDebug(MAIN) << "File for transaction" << txnId - << "has already been uploaded, bypassing re-upload"; + qCDebug(MESSAGES) + << "File for transaction" << txnId + << "has already been uploaded, bypassing re-upload"; } else { if (isJobRunning(transferIt->job)) { - qCDebug(MAIN) << "Abandoning the upload job for transaction" - << txnId << "and starting again"; + qCDebug(MESSAGES) << "Abandoning the upload job for transaction" + << txnId << "and starting again"; transferIt->job->abandon(); emit fileTransferFailed(txnId, tr("File upload will be retried")); @@ -1767,10 +1768,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"; } } @@ -2061,15 +2063,15 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction) auto& ti = timeline[Timeline::size_type(*pIdx - q->minTimelineIndex())]; if (ti->isRedacted() && ti->redactedBecause()->id() == redaction.id()) { - qCDebug(MAIN) << "Redaction" << redaction.id() << "of event" << ti->id() - << "already done, skipping"; + qCDebug(EVENTS) << "Redaction" << redaction.id() << "of event" + << ti->id() << "already done, skipping"; 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(makeRedacted(*ti, redaction)); - qCDebug(MAIN) << "Redacted" << oldEvent->id() << "with" << redaction.id(); + qCDebug(EVENTS) << "Redacted" << oldEvent->id() << "with" << redaction.id(); if (oldEvent->isStateEvent()) { const StateEventKey evtKey { oldEvent->matrixType(), oldEvent->stateKey() }; @@ -2077,7 +2079,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(MAIN).nospace() + qCDebug(STATE).nospace() << "Redacting state " << oldEvent->matrixType() << "/" << oldEvent->stateKey(); // Retarget the current state to the newly made event. @@ -2131,15 +2133,15 @@ bool Room::Private::processReplacement(const RoomMessageEvent& newEvent) auto& ti = timeline[Timeline::size_type(*pIdx - q->minTimelineIndex())]; if (ti->replacedBy() == newEvent.id()) { - qCDebug(MAIN) << "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(MAIN) << "Replaced" << oldEvent->id() << "with" << newEvent.id(); + qCDebug(STATE) << "Replaced" << oldEvent->id() << "with" << newEvent.id(); emit q->replacedEvent(ti.event(), rawPtr(oldEvent)); return true; } @@ -2188,7 +2190,7 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) }); targetIt != events.end()) *targetIt = makeRedacted(**targetIt, *r); else - qCDebug(MAIN) + qCDebug(STATE) << "Redaction" << r->id() << "ignored: target event" << r->redactedEvent() << "is not found"; // If the target event comes later, it comes already redacted. @@ -2227,10 +2229,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 }; @@ -2288,8 +2290,9 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) } } - qCDebug(MAIN) << "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 @@ -2300,8 +2303,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(MAIN) << "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)); @@ -2338,8 +2342,8 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) const auto insertedSize = moveEventsToTimeline(events, Older); const auto from = timeline.crend() - insertedSize; - qCDebug(MAIN) << "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()); @@ -2581,7 +2585,7 @@ Room::Changes Room::processAccountDataEvent(EventPtr&& event) if (auto* evt = eventCast(event)) { auto readEventId = evt->event_id(); - qCDebug(MAIN) << "Server-side read marker at" << readEventId; + qCDebug(STATE) << "Server-side read marker at" << readEventId; d->serverReadMarker = readEventId; const auto newMarker = findInTimeline(readEventId); changes |= newMarker != timelineEdge() @@ -2595,8 +2599,8 @@ Room::Changes Room::processAccountDataEvent(EventPtr&& event) if (!currentData || currentData->contentJson() != event->contentJson()) { emit accountDataAboutToChange(event->matrixType()); currentData = move(event); - qCDebug(MAIN) << "Updated account data of type" - << currentData->matrixType(); + qCDebug(STATE) << "Updated account data of type" + << currentData->matrixType(); emit accountDataChanged(currentData->matrixType()); return Change::AccountDataChange; } -- cgit v1.2.3 From 56d9a0addaabf2cec78e1c82a9846997a3669736 Mon Sep 17 00:00:00 2001 From: Alexey Andreev Date: Tue, 25 Feb 2020 20:06:19 +0300 Subject: E2EE: Make building E2EE optional. Contributes to #369 Signed-off-by: Alexey Andreev --- CMakeLists.txt | 68 ++++++++++++++++++++++++++++------------------- lib/connection.cpp | 25 +++++++++++++++++ lib/connection.h | 2 ++ lib/encryptionmanager.cpp | 2 ++ lib/encryptionmanager.h | 2 ++ lib/room.cpp | 17 ++++++++++++ libquotient.pri | 11 +++++++- 7 files changed, 99 insertions(+), 28 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9fc1ee6c..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 ) @@ -224,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} @@ -238,7 +249,10 @@ target_include_directories(${PROJECT_NAME} PUBLIC $ $ ) -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 98c8a4bc..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,7 +45,9 @@ #include "jobs/mediathumbnailjob.h" #include "jobs/syncjob.h" +#ifdef Quotient_E2EE_ENABLED #include "account.h" // QtOlm +#endif // Quotient_E2EE_ENABLED #include #include @@ -107,7 +111,9 @@ public: GetCapabilitiesJob* capabilitiesJob = nullptr; GetCapabilitiesJob::Capabilities capabilities; +#ifdef Quotient_E2EE_ENABLED QScopedPointer encryptionManager; +#endif // Quotient_E2EE_ENABLED SyncJob* syncJob = nullptr; @@ -153,6 +159,10 @@ public: 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 {}; @@ -208,6 +218,7 @@ public: } return decryptedEvent; +#endif // Quotient_E2EE_ENABLED } }; @@ -304,8 +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()); +#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()); @@ -362,12 +377,16 @@ void Connection::Private::connectWithToken(const QString& userId, 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(); @@ -594,6 +613,9 @@ 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()) { @@ -645,6 +667,7 @@ void Connection::onSyncSuccess(SyncData&& data, bool fromCache) d->encryptionManager->updateOneTimeKeyCounts(this, deviceOneTimeKeysCount); } +#endif // Quotient_E2EE_ENABLED } void Connection::stopSync() @@ -1068,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 e2834c45..0895fae9 100644 --- a/lib/encryptionmanager.cpp +++ b/lib/encryptionmanager.cpp @@ -1,3 +1,4 @@ +#ifdef Quotient_E2EE_ENABLED #include "encryptionmanager.h" #include "connection.h" @@ -366,3 +367,4 @@ bool EncryptionManager::Private::oneTimeKeyShouldUpload() } return false; } +#endif // Quotient_E2EE_ENABLED diff --git a/lib/encryptionmanager.h b/lib/encryptionmanager.h index 8f346d37..5df15e83 100644 --- a/lib/encryptionmanager.h +++ b/lib/encryptionmanager.h @@ -1,3 +1,4 @@ +#ifdef Quotient_E2EE_ENABLED #pragma once #include @@ -43,3 +44,4 @@ private: }; } // namespace Quotient +#endif // Quotient_E2EE_ENABLED diff --git a/lib/room.cpp b/lib/room.cpp index ecb5a7ad..5a966ceb 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -69,9 +69,11 @@ #include #include +#ifdef Quotient_E2EE_ENABLED #include // QtOlm #include // QtOlm #include // QtOlm +#endif // Quotient_E2EE_ENABLED using namespace Quotient; using namespace QtOlm; @@ -342,6 +344,7 @@ public: QJsonObject toJson() const; +#ifdef Quotient_E2EE_ENABLED // A map from to QHash, QPair> groupSessionIndexRecord; // TODO: cache @@ -424,6 +427,7 @@ public: return decrypted.first; } +#endif // Quotient_E2EE_ENABLED private: using users_shortlist_t = std::array; @@ -1238,6 +1242,11 @@ const StateEventBase* Room::getCurrentState(const QString& evtType, RoomEventPtr Room::decryptMessage(const EncryptedEvent& encryptedEvent) { +#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(), @@ -1252,10 +1261,17 @@ RoomEventPtr Room::decryptMessage(const EncryptedEvent& encryptedEvent) qCDebug(E2EE) << "Algorithm of the encrypted event with id" << encryptedEvent.id() << "is not for the current device"; return {}; +#endif // Quotient_E2EE_ENABLED } void Room::handleRoomKeyEvent(RoomKeyEvent* roomKeyEvent, QString senderKey) { +#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"; @@ -1265,6 +1281,7 @@ void Room::handleRoomKeyEvent(RoomKeyEvent* roomKeyEvent, QString senderKey) qCDebug(E2EE) << "added new inboundGroupSession:" << d->groupSessions.count(); } +#endif // Quotient_E2EE_ENABLED } int Room::joinedCount() const 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 \ -- cgit v1.2.3