From ff54bf2d0979dc6b9b3b77bba827ae7f3baa9f58 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sun, 27 Feb 2022 19:15:16 +0100 Subject: Add constructor for creating roomkeyevents --- lib/events/roomkeyevent.cpp | 13 +++++++++++++ lib/events/roomkeyevent.h | 1 + 2 files changed, 14 insertions(+) diff --git a/lib/events/roomkeyevent.cpp b/lib/events/roomkeyevent.cpp index 332be3f7..68962950 100644 --- a/lib/events/roomkeyevent.cpp +++ b/lib/events/roomkeyevent.cpp @@ -10,3 +10,16 @@ RoomKeyEvent::RoomKeyEvent(const QJsonObject &obj) : Event(typeId(), obj) if (roomId().isEmpty()) qCWarning(E2EE) << "Room key event has empty room id"; } + +RoomKeyEvent::RoomKeyEvent(const QString& algorithm, const QString& roomId, const QString& sessionId, const QString& sessionKey, const QString& senderId) + : Event(typeId(), { + {"content", QJsonObject{ + {"algorithm", algorithm}, + {"room_id", roomId}, + {"session_id", sessionId}, + {"session_key", sessionKey}, + }}, + {"sender", senderId}, + {"type", "m.room_key"}, + }) +{} diff --git a/lib/events/roomkeyevent.h b/lib/events/roomkeyevent.h index ed4c9440..2bda3086 100644 --- a/lib/events/roomkeyevent.h +++ b/lib/events/roomkeyevent.h @@ -12,6 +12,7 @@ public: DEFINE_EVENT_TYPEID("m.room_key", RoomKeyEvent) explicit RoomKeyEvent(const QJsonObject& obj); + explicit RoomKeyEvent(const QString& algorithm, const QString& roomId, const QString &sessionId, const QString& sessionKey, const QString& senderId); QString algorithm() const { return contentPart("algorithm"_ls); } QString roomId() const { return contentPart(RoomIdKeyL); } -- cgit v1.2.3 From 20bd96ac67c06617e619460c4cd07b5e15cc74d7 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Wed, 2 Mar 2022 00:54:49 +0100 Subject: Implement sending encrypted messages --- lib/connection.cpp | 30 +++++++- lib/connection.h | 9 ++- lib/room.cpp | 199 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 226 insertions(+), 12 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 2528b70b..a66a4168 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -1332,7 +1332,7 @@ Connection::sendToDevices(const QString& eventType, [&jsonUser](const auto& deviceToEvents) { jsonUser.insert( deviceToEvents.first, - deviceToEvents.second.contentJson()); + deviceToEvents.second->contentJson()); }); }); return callApi(BackgroundRequest, eventType, @@ -2238,4 +2238,32 @@ bool Connection::isKnownCurveKey(const QString& user, const QString& curveKey) return query.next(); } +bool Connection::hasOlmSession(User* user, const QString& deviceId) const +{ + const auto& curveKey = curveKeyForUserDevice(user->id(), deviceId); + return d->olmSessions.contains(curveKey) && d->olmSessions[curveKey].size() > 0; +} + +QPair Connection::olmEncryptMessage(User* user, const QString& device, const QByteArray& message) +{ + //TODO be smarter about choosing a session; see e2ee impl guide + //TODO create session? + const auto& curveKey = curveKeyForUserDevice(user->id(), device); + QOlmMessage::Type type = d->olmSessions[curveKey][0]->encryptMessageType(); + auto result = d->olmSessions[curveKey][0]->encrypt(message); + return qMakePair(type, result.toCiphertext()); +} + +//TODO be more consistent with curveKey and identityKey +void Connection::createOlmSession(const QString& theirIdentityKey, const QString& theirOneTimeKey) +{ + auto session = QOlmSession::createOutboundSession(olmAccount(), theirIdentityKey, theirOneTimeKey); + if (std::holds_alternative(session)) { + //TODO something + qCWarning(E2EE) << "Failed to create olm session for " << theirIdentityKey << std::get(session); + } + d->saveSession(std::get>(session), theirIdentityKey); + d->olmSessions[theirIdentityKey].push_back(std::move(std::get>(session))); +} + #endif diff --git a/lib/connection.h b/lib/connection.h index b75bd5b5..afa4a657 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -24,6 +24,7 @@ #ifdef Quotient_E2EE_ENABLED #include "e2ee/e2ee.h" +#include "e2ee/qolmmessage.h" #endif Q_DECLARE_METATYPE(Quotient::GetLoginFlowsJob::LoginFlow) @@ -132,7 +133,7 @@ class QUOTIENT_API Connection : public QObject { public: using UsersToDevicesToEvents = - UnorderedMap>; + UnorderedMap>>; enum RoomVisibility { PublishRoom, @@ -321,6 +322,12 @@ public: const Room* room); void saveMegolmSession(const Room* room, const QOlmInboundGroupSession& session); + bool hasOlmSession(User* user, const QString& deviceId) const; + + //This currently assumes that an olm session with (user, device) exists + //TODO make this return an event? + QPair olmEncryptMessage(User* user, const QString& device, const QByteArray& message); + void createOlmSession(const QString& theirIdentityKey, const QString& theirOneTimeKey); #endif // Quotient_E2EE_ENABLED Q_INVOKABLE Quotient::SyncJob* syncJob() const; Q_INVOKABLE int millisToReconnect() const; diff --git a/lib/room.cpp b/lib/room.cpp index 1314803e..e8b63235 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -12,6 +12,7 @@ #include "avatar.h" #include "connection.h" #include "converters.h" +#include "e2ee/qolmoutboundsession.h" #include "syncdata.h" #include "user.h" #include "eventstats.h" @@ -69,6 +70,7 @@ #include "e2ee/qolmaccount.h" #include "e2ee/qolmerrors.h" #include "e2ee/qolminboundsession.h" +#include "e2ee/qolmutility.h" #include "database.h" #endif // Quotient_E2EE_ENABLED @@ -297,7 +299,8 @@ public: RoomEvent* addAsPending(RoomEventPtr&& event); - QString doSendEvent(const RoomEvent* pEvent); + //TODO deleteWhenFinishedis ugly, find out if there's something nicer + QString doSendEvent(const RoomEvent* pEvent, bool deleteWhenFinished = false); void onEventSendingFailure(const QString& txnId, BaseJob* call = nullptr); SetRoomStateWithKeyJob* requestSetState(const QString& evtType, @@ -338,6 +341,10 @@ public: #ifdef Quotient_E2EE_ENABLED UnorderedMap groupSessions; + int currentMegolmSessionMessageCount = 0; + //TODO save this to database + unsigned long long currentMegolmSessionCreationTimestamp = 0; + std::unique_ptr currentOutboundMegolmSession = nullptr; bool addInboundGroupSession(QString sessionId, QByteArray sessionKey, const QString& senderId, @@ -402,6 +409,144 @@ public: } return content; } + + bool shouldRotateMegolmSession() const + { + if (!q->usesEncryption()) { + return false; + } + return currentMegolmSessionMessageCount >= rotationMessageCount() || (currentMegolmSessionCreationTimestamp + rotationInterval()) < QDateTime::currentMSecsSinceEpoch(); + } + + bool hasValidMegolmSession() const + { + if (!q->usesEncryption()) { + return false; + } + return currentOutboundMegolmSession != nullptr; + } + + /// Time in milliseconds after which the outgoing megolmsession should be replaced + unsigned int rotationInterval() const + { + if (!q->usesEncryption()) { + return 0; + } + return q->getCurrentState()->rotationPeriodMs(); + } + + // Number of messages sent by this user after which the outgoing megolm session should be replaced + int rotationMessageCount() const + { + if (!q->usesEncryption()) { + return 0; + } + return q->getCurrentState()->rotationPeriodMsgs(); + } + void createMegolmSession() { + qCDebug(E2EE) << "Creating new outbound megolm session for room " << q->id(); + currentOutboundMegolmSession = QOlmOutboundGroupSession::create(); + currentMegolmSessionMessageCount = 0; + currentMegolmSessionCreationTimestamp = QDateTime::currentMSecsSinceEpoch(); + //TODO store megolm session to database + } + + std::unique_ptr payloadForUserDevice(User* user, const QString& device, const QByteArray& sessionId, const QByteArray& sessionKey) + { + // Noisy but nice for debugging + //qCDebug(E2EE) << "Creating the payload for" << user->id() << device << sessionId << sessionKey.toHex(); + //TODO: store {user->id(), device, sessionId, theirIdentityKey}; required for key requests + const auto event = makeEvent("m.megolm.v1.aes-sha2", q->id(), sessionId, sessionKey, q->localUser()->id()); + QJsonObject payloadJson = event->fullJson(); + payloadJson["recipient"] = user->id(); + payloadJson["sender"] = connection->user()->id(); + QJsonObject recipientObject; + recipientObject["ed25519"] = connection->edKeyForUserDevice(user->id(), device); + payloadJson["recipient_keys"] = recipientObject; + QJsonObject senderObject; + senderObject["ed25519"] = QString(connection->olmAccount()->identityKeys().ed25519); + payloadJson["keys"] = senderObject; + payloadJson["sender_device"] = connection->deviceId(); + auto cipherText = connection->olmEncryptMessage(user, device, QJsonDocument(payloadJson).toJson(QJsonDocument::Compact)); + QJsonObject encrypted; + encrypted[connection->curveKeyForUserDevice(user->id(), device)] = QJsonObject{{"type", cipherText.first}, {"body", QString(cipherText.second)}}; + + return makeEvent(encrypted, connection->olmAccount()->identityKeys().curve25519); + } + + void sendRoomKeyToDevices(const QByteArray& sessionId, const QByteArray& sessionKey) + { + qWarning() << "Sending room key to devices" << sessionId, sessionKey.toHex(); + QHash> hash; + for (const auto& user : q->users()) { + QHash u; + for(const auto &device : connection->devicesForUser(user)) { + if (!connection->hasOlmSession(user, device)) { + u[device] = "signed_curve25519"_ls; + qCDebug(E2EE) << "Adding" << user << device << "to keys to claim"; + } + } + if (!u.isEmpty()) { + hash[user->id()] = u; + } + } + auto job = connection->callApi(hash); + connect(job, &BaseJob::success, q, [job, this, sessionId, sessionKey](){ + Connection::UsersToDevicesToEvents usersToDevicesToEvents; + auto data = job->jsonData(); + for(const auto &user : q->users()) { + for(const auto &device : connection->devicesForUser(user)) { + const auto recipientCurveKey = connection->curveKeyForUserDevice(user->id(), device); + if (!connection->hasOlmSession(user, device)) { + qCDebug(E2EE) << "Creating a new session for" << user << device; + if(data["one_time_keys"].toObject()[user->id()].toObject()[device].toObject().isEmpty()) { + qWarning() << "No one time key for" << user << device; + continue; + } + auto keyId = data["one_time_keys"].toObject()[user->id()].toObject()[device].toObject().keys()[0]; + auto oneTimeKey = data["one_time_keys"].toObject()[user->id()].toObject()[device].toObject()[keyId].toObject()["key"].toString(); + auto signature = data["one_time_keys"].toObject()[user->id()].toObject()[device].toObject()[keyId].toObject()["signatures"].toObject()[user->id()].toObject()[QStringLiteral("ed25519:") + device].toString().toLatin1(); + auto signedData = data["one_time_keys"].toObject()[user->id()].toObject()[device].toObject()[keyId].toObject(); + signedData.remove("unsigned"); + signedData.remove("signatures"); + auto signatureMatch = QOlmUtility().ed25519Verify(connection->edKeyForUserDevice(user->id(), device).toLatin1(), QJsonDocument(signedData).toJson(QJsonDocument::Compact), signature); + if (std::holds_alternative(signatureMatch)) { + //TODO i think there are more failed signature checks than expected. Investigate + qDebug() << signedData; + qCWarning(E2EE) << "Failed to verify one-time-key signature for" << user->id() << device << ". Skipping this device."; + //Q_ASSERT(false); + continue; + } else { + } + connection->createOlmSession(recipientCurveKey, oneTimeKey); + } + usersToDevicesToEvents[user->id()][device] = payloadForUserDevice(user, device, sessionId, sessionKey); + } + } + connection->sendToDevices("m.room.encrypted", usersToDevicesToEvents); + }); + } + + //TODO load outbound megolm sessions from database + + void sendMegolmSession() { + // Save the session to this device + const auto sessionId = currentOutboundMegolmSession->sessionId(); + const auto _sessionKey = currentOutboundMegolmSession->sessionKey(); + if(std::holds_alternative(_sessionKey)) { + qCWarning(E2EE) << "Session error"; + //TODO something + } + const auto sessionKey = std::get(_sessionKey); + const auto senderKey = q->connection()->olmAccount()->identityKeys().curve25519; + + // Send to key to ourself at this device + addInboundGroupSession(senderKey, sessionId, sessionKey); + + // Send the session to other people + sendRoomKeyToDevices(sessionId, sessionKey); + } + #endif // Quotient_E2EE_ENABLED private: @@ -428,9 +573,20 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState) connect(this, &Room::userAdded, this, [this, connection](){ if(usesEncryption()) { connection->encryptionUpdate(this); + //TODO key at currentIndex to all user devices } }); d->groupSessions = connection->loadRoomMegolmSessions(this); + //TODO load outbound session + connect(this, &Room::userRemoved, this, [this](){ + if (!usesEncryption()) { + return; + } + d->currentOutboundMegolmSession = nullptr; + qCDebug(E2EE) << "Invalidating current megolm session because user left"; + //TODO save old session probably + + }); connect(this, &Room::beforeDestruction, this, [=](){ connection->database()->clearRoomData(id); @@ -1905,19 +2061,39 @@ RoomEvent* Room::Private::addAsPending(RoomEventPtr&& event) QString Room::Private::sendEvent(RoomEventPtr&& event) { + if (!q->successorId().isEmpty()) { + qCWarning(MAIN) << q << "has been upgraded, event won't be sent"; + return {}; + } if (q->usesEncryption()) { - qCCritical(MAIN) << "Room" << q->objectName() - << "enforces encryption; sending encrypted messages " - "is not supported yet"; + if (!hasValidMegolmSession() || shouldRotateMegolmSession()) { + createMegolmSession(); + sendMegolmSession(); + } + //TODO check if this is necessary + //TODO check if we increment the sent message count + event->setRoomId(id); + const auto encrypted = currentOutboundMegolmSession->encrypt(QJsonDocument(event->fullJson()).toJson()); + if(std::holds_alternative(encrypted)) { + //TODO something + qWarning(E2EE) << "Error encrypting message" << std::get(encrypted); + return {}; + } + auto encryptedEvent = new EncryptedEvent(std::get(encrypted), q->connection()->olmAccount()->identityKeys().curve25519, q->connection()->deviceId(), currentOutboundMegolmSession->sessionId()); + encryptedEvent->setTransactionId(connection->generateTxnId()); + encryptedEvent->setRoomId(id); + encryptedEvent->setSender(connection->userId()); + event->setTransactionId(encryptedEvent->transactionId()); + currentMegolmSessionMessageCount++; + // We show the unencrypted event locally while pending. The echo check will throw the encrypted version out + addAsPending(std::move(event)); + return doSendEvent(encryptedEvent, true); } - if (q->successorId().isEmpty()) - return doSendEvent(addAsPending(std::move(event))); - qCWarning(MAIN) << q << "has been upgraded, event won't be sent"; - return {}; + return doSendEvent(addAsPending(std::move(event))); } -QString Room::Private::doSendEvent(const RoomEvent* pEvent) +QString Room::Private::doSendEvent(const RoomEvent* pEvent, bool deleteWhenFinished) { const auto txnId = pEvent->transactionId(); // TODO, #133: Enqueue the job rather than immediately trigger it. @@ -1938,7 +2114,7 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent) Room::connect(call, &BaseJob::failure, q, std::bind(&Room::Private::onEventSendingFailure, this, txnId, call)); - Room::connect(call, &BaseJob::success, q, [this, call, txnId] { + Room::connect(call, &BaseJob::success, q, [this, call, txnId, deleteWhenFinished, pEvent] { auto it = q->findPendingEvent(txnId); if (it != unsyncedEvents.end()) { if (it->deliveryStatus() != EventStatus::ReachedServer) { @@ -1950,6 +2126,9 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent) << "already merged"; emit q->messageSent(txnId, call->eventId()); + if (deleteWhenFinished){ + delete pEvent; + } }); } else onEventSendingFailure(txnId); -- cgit v1.2.3 From 3eb7ad8b0a1ac0f6f9cda679108937a01268f184 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Mon, 16 May 2022 20:46:34 +0200 Subject: Save and load outgoing megolm session --- lib/connection.cpp | 11 ++++++++++- lib/connection.h | 5 +++++ lib/database.cpp | 41 +++++++++++++++++++++++++++++++++++++++- lib/database.h | 3 +++ lib/e2ee/qolmoutboundsession.cpp | 22 ++++++++++++++++++++- lib/e2ee/qolmoutboundsession.h | 9 +++++++++ lib/room.cpp | 23 +++++++++++----------- 7 files changed, 99 insertions(+), 15 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index a66a4168..b11ec731 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -2247,7 +2247,6 @@ bool Connection::hasOlmSession(User* user, const QString& deviceId) const QPair Connection::olmEncryptMessage(User* user, const QString& device, const QByteArray& message) { //TODO be smarter about choosing a session; see e2ee impl guide - //TODO create session? const auto& curveKey = curveKeyForUserDevice(user->id(), device); QOlmMessage::Type type = d->olmSessions[curveKey][0]->encryptMessageType(); auto result = d->olmSessions[curveKey][0]->encrypt(message); @@ -2266,4 +2265,14 @@ void Connection::createOlmSession(const QString& theirIdentityKey, const QString d->olmSessions[theirIdentityKey].push_back(std::move(std::get>(session))); } +QOlmOutboundGroupSessionPtr Connection::loadCurrentOutboundMegolmSession(Room* room) +{ + return d->database->loadCurrentOutboundMegolmSession(room->id(), d->picklingMode); +} + +void Connection::saveCurrentOutboundMegolmSession(Room *room, const QOlmOutboundGroupSessionPtr& data) +{ + d->database->saveCurrentOutboundMegolmSession(room->id(), d->picklingMode, data); +} + #endif diff --git a/lib/connection.h b/lib/connection.h index afa4a657..8bed55da 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -25,6 +25,7 @@ #ifdef Quotient_E2EE_ENABLED #include "e2ee/e2ee.h" #include "e2ee/qolmmessage.h" +#include "e2ee/qolmoutboundsession.h" #endif Q_DECLARE_METATYPE(Quotient::GetLoginFlowsJob::LoginFlow) @@ -324,6 +325,10 @@ public: const QOlmInboundGroupSession& session); bool hasOlmSession(User* user, const QString& deviceId) const; + QOlmOutboundGroupSessionPtr loadCurrentOutboundMegolmSession(Room* room); + void saveCurrentOutboundMegolmSession(Room *room, const QOlmOutboundGroupSessionPtr& data); + + //This currently assumes that an olm session with (user, device) exists //TODO make this return an event? QPair olmEncryptMessage(User* user, const QString& device, const QByteArray& message); diff --git a/lib/database.cpp b/lib/database.cpp index e2e7acc9..8cb3a9d1 100644 --- a/lib/database.cpp +++ b/lib/database.cpp @@ -86,7 +86,7 @@ void Database::migrateTo1() execute(QStringLiteral("CREATE TABLE accounts (pickle TEXT);")); execute(QStringLiteral("CREATE TABLE olm_sessions (senderKey TEXT, sessionId TEXT, pickle TEXT);")); execute(QStringLiteral("CREATE TABLE inbound_megolm_sessions (roomId TEXT, senderKey TEXT, sessionId TEXT, pickle TEXT);")); - execute(QStringLiteral("CREATE TABLE outbound_megolm_sessions (roomId TEXT, senderKey TEXT, sessionId TEXT, pickle TEXT);")); + execute(QStringLiteral("CREATE TABLE outbound_megolm_sessions (roomId TEXT, sessionId TEXT, pickle TEXT, creationTime TEXT, messageCount INTEGER);")); execute(QStringLiteral("CREATE TABLE group_session_record_index (roomId TEXT, sessionId TEXT, i INTEGER, eventId TEXT, ts INTEGER);")); execute(QStringLiteral("CREATE TABLE tracked_users (matrixId TEXT);")); execute(QStringLiteral("CREATE TABLE outdated_users (matrixId TEXT);")); @@ -292,3 +292,42 @@ void Database::setOlmSessionLastReceived(const QString& sessionId, const QDateTi execute(query); commit(); } + +void Database::saveCurrentOutboundMegolmSession(const QString& roomId, const PicklingMode& picklingMode, const QOlmOutboundGroupSessionPtr& session) +{ + const auto pickle = session->pickle(picklingMode); + if (std::holds_alternative(pickle)) { + auto deleteQuery = prepareQuery(QStringLiteral("DELETE FROM outbound_megolm_sessions WHERE roomId=:roomId AND sessionId=:sessionId;")); + deleteQuery.bindValue(":roomId", roomId); + deleteQuery.bindValue(":sessionId", session->sessionId()); + + auto insertQuery = prepareQuery(QStringLiteral("INSERT INTO outbound_megolm_sessions(roomId, sessionId, pickle, creationTime, messageCount) VALUES(:roomId, :sessionId, :pickle, :creationTime, :messageCount);")); + insertQuery.bindValue(":roomId", roomId); + insertQuery.bindValue(":sessionId", session->sessionId()); + insertQuery.bindValue(":pickle", std::get(pickle)); + insertQuery.bindValue(":creationTime", session->creationTime()); + insertQuery.bindValue(":messageCount", session->messageCount()); + + transaction(); + execute(deleteQuery); + execute(insertQuery); + commit(); + } +} + +QOlmOutboundGroupSessionPtr Database::loadCurrentOutboundMegolmSession(const QString& roomId, const PicklingMode& picklingMode) +{ + auto query = prepareQuery(QStringLiteral("SELECT * FROM outbound_megolm_sessions WHERE roomId=:roomId ORDER BY creationTime DESC;")); + query.bindValue(":roomId", roomId); + execute(query); + if (query.next()) { + auto sessionResult = QOlmOutboundGroupSession::unpickle(query.value("pickle").toByteArray(), picklingMode); + if (std::holds_alternative(sessionResult)) { + auto session = std::move(std::get(sessionResult)); + session->setCreationTime(query.value("creationTime").toDateTime()); + session->setMessageCount(query.value("messageCount").toInt()); + return session; + } + } + return nullptr; +} diff --git a/lib/database.h b/lib/database.h index 08fe49f3..751ebd1d 100644 --- a/lib/database.h +++ b/lib/database.h @@ -8,6 +8,7 @@ #include #include "e2ee/e2ee.h" + namespace Quotient { class QUOTIENT_API Database : public QObject { @@ -34,6 +35,8 @@ public: std::pair groupSessionIndexRecord(const QString& roomId, const QString& sessionId, qint64 index); void clearRoomData(const QString& roomId); void setOlmSessionLastReceived(const QString& sessionId, const QDateTime& timestamp); + QOlmOutboundGroupSessionPtr loadCurrentOutboundMegolmSession(const QString& roomId, const PicklingMode& picklingMode); + void saveCurrentOutboundMegolmSession(const QString& roomId, const PicklingMode& picklingMode, const QOlmOutboundGroupSessionPtr& data); private: void migrateTo1(); diff --git a/lib/e2ee/qolmoutboundsession.cpp b/lib/e2ee/qolmoutboundsession.cpp index 96bad344..10b0c4de 100644 --- a/lib/e2ee/qolmoutboundsession.cpp +++ b/lib/e2ee/qolmoutboundsession.cpp @@ -66,7 +66,7 @@ QOlmExpected QOlmOutboundGroupSession::unpickle(QBy auto *olmOutboundGroupSession = olm_outbound_group_session(new uint8_t[olm_outbound_group_session_size()]); QByteArray key = toKey(mode); const auto error = olm_unpickle_outbound_group_session(olmOutboundGroupSession, key.data(), key.length(), - pickled.data(), pickled.length()); + pickledBuf.data(), pickledBuf.length()); if (error == olm_error()) { return lastError(olmOutboundGroupSession); } @@ -123,3 +123,23 @@ QOlmExpected QOlmOutboundGroupSession::sessionKey() const } return keyBuffer; } + +int QOlmOutboundGroupSession::messageCount() const +{ + return m_messageCount; +} + +void QOlmOutboundGroupSession::setMessageCount(int messageCount) +{ + m_messageCount = messageCount; +} + +QDateTime QOlmOutboundGroupSession::creationTime() const +{ + return m_creationTime; +} + +void QOlmOutboundGroupSession::setCreationTime(const QDateTime& creationTime) +{ + m_creationTime = creationTime; +} diff --git a/lib/e2ee/qolmoutboundsession.h b/lib/e2ee/qolmoutboundsession.h index 8058bbb1..56b25974 100644 --- a/lib/e2ee/qolmoutboundsession.h +++ b/lib/e2ee/qolmoutboundsession.h @@ -26,6 +26,7 @@ public: //! pickling a `QOlmOutboundGroupSession`. static QOlmExpected unpickle( QByteArray& pickled, const PicklingMode& mode); + //! Encrypts a plaintext message using the session. QOlmExpected encrypt(const QString& plaintext); @@ -44,8 +45,16 @@ public: //! ratchet key that will be used for the next message. QOlmExpected sessionKey() const; QOlmOutboundGroupSession(OlmOutboundGroupSession *groupSession); + + int messageCount() const; + void setMessageCount(int messageCount); + + QDateTime creationTime() const; + void setCreationTime(const QDateTime& creationTime); private: OlmOutboundGroupSession *m_groupSession; + int m_messageCount = 0; + QDateTime m_creationTime = QDateTime::currentDateTime(); }; } // namespace Quotient diff --git a/lib/room.cpp b/lib/room.cpp index e8b63235..9e2bd7dd 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -344,7 +344,7 @@ public: int currentMegolmSessionMessageCount = 0; //TODO save this to database unsigned long long currentMegolmSessionCreationTimestamp = 0; - std::unique_ptr currentOutboundMegolmSession = nullptr; + QOlmOutboundGroupSessionPtr currentOutboundMegolmSession = nullptr; bool addInboundGroupSession(QString sessionId, QByteArray sessionKey, const QString& senderId, @@ -415,7 +415,7 @@ public: if (!q->usesEncryption()) { return false; } - return currentMegolmSessionMessageCount >= rotationMessageCount() || (currentMegolmSessionCreationTimestamp + rotationInterval()) < QDateTime::currentMSecsSinceEpoch(); + return currentOutboundMegolmSession->messageCount() >= rotationMessageCount() || currentOutboundMegolmSession->creationTime().addMSecs(rotationInterval()) < QDateTime::currentDateTime(); } bool hasValidMegolmSession() const @@ -446,9 +446,7 @@ public: void createMegolmSession() { qCDebug(E2EE) << "Creating new outbound megolm session for room " << q->id(); currentOutboundMegolmSession = QOlmOutboundGroupSession::create(); - currentMegolmSessionMessageCount = 0; - currentMegolmSessionCreationTimestamp = QDateTime::currentMSecsSinceEpoch(); - //TODO store megolm session to database + connection->saveCurrentOutboundMegolmSession(q, currentOutboundMegolmSession); } std::unique_ptr payloadForUserDevice(User* user, const QString& device, const QByteArray& sessionId, const QByteArray& sessionKey) @@ -476,7 +474,7 @@ public: void sendRoomKeyToDevices(const QByteArray& sessionId, const QByteArray& sessionKey) { - qWarning() << "Sending room key to devices" << sessionId, sessionKey.toHex(); + qCDebug(E2EE) << "Sending room key to devices" << sessionId, sessionKey.toHex(); QHash> hash; for (const auto& user : q->users()) { QHash u; @@ -493,7 +491,7 @@ public: auto job = connection->callApi(hash); connect(job, &BaseJob::success, q, [job, this, sessionId, sessionKey](){ Connection::UsersToDevicesToEvents usersToDevicesToEvents; - auto data = job->jsonData(); + const auto data = job->jsonData(); for(const auto &user : q->users()) { for(const auto &device : connection->devicesForUser(user)) { const auto recipientCurveKey = connection->curveKeyForUserDevice(user->id(), device); @@ -527,8 +525,6 @@ public: }); } - //TODO load outbound megolm sessions from database - void sendMegolmSession() { // Save the session to this device const auto sessionId = currentOutboundMegolmSession->sessionId(); @@ -577,14 +573,16 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState) } }); d->groupSessions = connection->loadRoomMegolmSessions(this); - //TODO load outbound session + d->currentOutboundMegolmSession = connection->loadCurrentOutboundMegolmSession(this); + if (d->shouldRotateMegolmSession()) { + d->currentOutboundMegolmSession = nullptr; + } connect(this, &Room::userRemoved, this, [this](){ if (!usesEncryption()) { return; } d->currentOutboundMegolmSession = nullptr; qCDebug(E2EE) << "Invalidating current megolm session because user left"; - //TODO save old session probably }); @@ -2074,6 +2072,8 @@ QString Room::Private::sendEvent(RoomEventPtr&& event) //TODO check if we increment the sent message count event->setRoomId(id); const auto encrypted = currentOutboundMegolmSession->encrypt(QJsonDocument(event->fullJson()).toJson()); + currentOutboundMegolmSession->setMessageCount(currentOutboundMegolmSession->messageCount() + 1); + connection->saveCurrentOutboundMegolmSession(q, currentOutboundMegolmSession); if(std::holds_alternative(encrypted)) { //TODO something qWarning(E2EE) << "Error encrypting message" << std::get(encrypted); @@ -2084,7 +2084,6 @@ QString Room::Private::sendEvent(RoomEventPtr&& event) encryptedEvent->setRoomId(id); encryptedEvent->setSender(connection->userId()); event->setTransactionId(encryptedEvent->transactionId()); - currentMegolmSessionMessageCount++; // We show the unencrypted event locally while pending. The echo check will throw the encrypted version out addAsPending(std::move(event)); return doSendEvent(encryptedEvent, true); -- cgit v1.2.3 From 6f5ac9b7315d75692960e5eac7b1eb6867c0d203 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sun, 6 Mar 2022 22:54:01 +0100 Subject: Keep log of where we send keys and send keys to new devices and users --- lib/connection.cpp | 1 + lib/database.cpp | 44 ++++++++++++++++++++++++++++++++++++++++++++ lib/database.h | 9 +++++++++ lib/room.cpp | 53 ++++++++++++++++++++++++++++++++++------------------- 4 files changed, 88 insertions(+), 19 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index b11ec731..2a1b39f9 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -2247,6 +2247,7 @@ bool Connection::hasOlmSession(User* user, const QString& deviceId) const QPair Connection::olmEncryptMessage(User* user, const QString& device, const QByteArray& message) { //TODO be smarter about choosing a session; see e2ee impl guide + //TODO do we need to save the olm session after sending a message? const auto& curveKey = curveKeyForUserDevice(user->id(), device); QOlmMessage::Type type = d->olmSessions[curveKey][0]->encryptMessageType(); auto result = d->olmSessions[curveKey][0]->encrypt(message); diff --git a/lib/database.cpp b/lib/database.cpp index 8cb3a9d1..4a28fd4c 100644 --- a/lib/database.cpp +++ b/lib/database.cpp @@ -13,6 +13,9 @@ #include "e2ee/e2ee.h" #include "e2ee/qolmsession.h" #include "e2ee/qolminboundsession.h" +#include "connection.h" +#include "user.h" +#include "room.h" using namespace Quotient; Database::Database(const QString& matrixId, const QString& deviceId, QObject* parent) @@ -91,6 +94,7 @@ void Database::migrateTo1() execute(QStringLiteral("CREATE TABLE tracked_users (matrixId TEXT);")); execute(QStringLiteral("CREATE TABLE outdated_users (matrixId TEXT);")); execute(QStringLiteral("CREATE TABLE tracked_devices (matrixId TEXT, deviceId TEXT, curveKeyId TEXT, curveKey TEXT, edKeyId TEXT, edKey TEXT);")); + execute(QStringLiteral("CREATE TABLE sent_megolm_sessions (roomId TEXT, userId TEXT, deviceId TEXT, identityKey TEXT, sessionId TEXT, i INTEGER);")); execute(QStringLiteral("PRAGMA user_version = 1;")); commit(); @@ -331,3 +335,43 @@ QOlmOutboundGroupSessionPtr Database::loadCurrentOutboundMegolmSession(const QSt } return nullptr; } + +void Database::setDevicesReceivedKey(const QString& roomId, QHash devices, const QString& sessionId, int index) +{ + //TODO this better + auto connection = dynamic_cast(parent()); + transaction(); + for (const auto& user : devices.keys()) { + for (const auto& device : devices[user]) { + auto query = prepareQuery(QStringLiteral("INSERT INTO sent_megolm_sessions(roomId, userId, deviceId, identityKey, sessionId, i) VALUES(:roomId, :userId, :deviceId, :identityKey, :sessionId, :i);")); + query.bindValue(":roomId", roomId); + query.bindValue(":userId", user->id()); + query.bindValue(":deviceId", device); + query.bindValue(":identityKey", connection->curveKeyForUserDevice(user->id(), device)); + query.bindValue(":sessionId", sessionId); + query.bindValue(":i", index); + execute(query); + } + } + commit(); +} + +QHash Database::devicesWithoutKey(Room* room, const QString &sessionId) +{ + auto connection = dynamic_cast(parent()); + QHash devices; + for (const auto& user : room->users()) {//TODO does this include invited & left? + devices[user->id()] = connection->devicesForUser(user); + } + + auto query = prepareQuery(QStringLiteral("SELECT userId, deviceId FROM sent_megolm_sessions WHERE roomId=:roomId AND sessionId=:sessionId")); + query.bindValue(":roomId", room->id()); + query.bindValue(":sessionId", sessionId); + transaction(); + execute(query); + commit(); + while (query.next()) { + devices[query.value("userId").toString()].removeAll(query.value("deviceId").toString()); + } + return devices; +} diff --git a/lib/database.h b/lib/database.h index 751ebd1d..30f2f203 100644 --- a/lib/database.h +++ b/lib/database.h @@ -7,9 +7,14 @@ #include #include +#include + #include "e2ee/e2ee.h" namespace Quotient { +class User; +class Room; + class QUOTIENT_API Database : public QObject { Q_OBJECT @@ -38,6 +43,10 @@ public: QOlmOutboundGroupSessionPtr loadCurrentOutboundMegolmSession(const QString& roomId, const PicklingMode& picklingMode); void saveCurrentOutboundMegolmSession(const QString& roomId, const PicklingMode& picklingMode, const QOlmOutboundGroupSessionPtr& data); + // Returns a map User -> [Device] that have not received key yet + QHash devicesWithoutKey(Room* room, const QString &sessionId); + void setDevicesReceivedKey(const QString& roomId, QHash devices, const QString& sessionId, int index); + private: void migrateTo1(); void migrateTo2(); diff --git a/lib/room.cpp b/lib/room.cpp index 9e2bd7dd..7db9f8e9 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -447,6 +447,13 @@ public: qCDebug(E2EE) << "Creating new outbound megolm session for room " << q->id(); currentOutboundMegolmSession = QOlmOutboundGroupSession::create(); connection->saveCurrentOutboundMegolmSession(q, currentOutboundMegolmSession); + + const auto sessionKey = currentOutboundMegolmSession->sessionKey(); + if(std::holds_alternative(sessionKey)) { + qCWarning(E2EE) << "Session error"; + //TODO something + } + addInboundGroupSession(q->connection()->olmAccount()->identityKeys().curve25519, currentOutboundMegolmSession->sessionId(), std::get(sessionKey), QString(connection->olmAccount()->identityKeys().ed25519)); } std::unique_ptr payloadForUserDevice(User* user, const QString& device, const QByteArray& sessionId, const QByteArray& sessionKey) @@ -472,13 +479,23 @@ public: return makeEvent(encrypted, connection->olmAccount()->identityKeys().curve25519); } - void sendRoomKeyToDevices(const QByteArray& sessionId, const QByteArray& sessionKey) + QHash getDevicesWithoutKey() const + { + QHash devices; + auto rawDevices = q->connection()->database()->devicesWithoutKey(q, QString(currentOutboundMegolmSession->sessionId())); + for (const auto& user : rawDevices.keys()) { + devices[q->connection()->user(user)] = rawDevices[user]; + } + return devices; + } + + void sendRoomKeyToDevices(const QByteArray& sessionId, const QByteArray& sessionKey, const QHash devices, int index) { qCDebug(E2EE) << "Sending room key to devices" << sessionId, sessionKey.toHex(); QHash> hash; - for (const auto& user : q->users()) { + for (const auto& user : devices.keys()) { QHash u; - for(const auto &device : connection->devicesForUser(user)) { + for(const auto &device : devices[user]) { if (!connection->hasOlmSession(user, device)) { u[device] = "signed_curve25519"_ls; qCDebug(E2EE) << "Adding" << user << device << "to keys to claim"; @@ -489,30 +506,28 @@ public: } } auto job = connection->callApi(hash); - connect(job, &BaseJob::success, q, [job, this, sessionId, sessionKey](){ + connect(job, &BaseJob::success, q, [job, this, sessionId, sessionKey, devices, index](){ Connection::UsersToDevicesToEvents usersToDevicesToEvents; const auto data = job->jsonData(); - for(const auto &user : q->users()) { - for(const auto &device : connection->devicesForUser(user)) { + for(const auto &user : devices.keys()) { + for(const auto &device : devices[user]) { const auto recipientCurveKey = connection->curveKeyForUserDevice(user->id(), device); if (!connection->hasOlmSession(user, device)) { qCDebug(E2EE) << "Creating a new session for" << user << device; - if(data["one_time_keys"].toObject()[user->id()].toObject()[device].toObject().isEmpty()) { + if(data["one_time_keys"][user->id()][device].toObject().isEmpty()) { qWarning() << "No one time key for" << user << device; continue; } - auto keyId = data["one_time_keys"].toObject()[user->id()].toObject()[device].toObject().keys()[0]; - auto oneTimeKey = data["one_time_keys"].toObject()[user->id()].toObject()[device].toObject()[keyId].toObject()["key"].toString(); - auto signature = data["one_time_keys"].toObject()[user->id()].toObject()[device].toObject()[keyId].toObject()["signatures"].toObject()[user->id()].toObject()[QStringLiteral("ed25519:") + device].toString().toLatin1(); - auto signedData = data["one_time_keys"].toObject()[user->id()].toObject()[device].toObject()[keyId].toObject(); + const auto keyId = data["one_time_keys"][user->id()][device].toObject().keys()[0]; + const auto oneTimeKey = data["one_time_keys"][user->id()][device][keyId]["key"].toString(); + const auto signature = data["one_time_keys"][user->id()][device][keyId]["signatures"][user->id()][QStringLiteral("ed25519:") + device].toString().toLatin1(); + auto signedData = data["one_time_keys"][user->id()][device][keyId].toObject(); signedData.remove("unsigned"); signedData.remove("signatures"); auto signatureMatch = QOlmUtility().ed25519Verify(connection->edKeyForUserDevice(user->id(), device).toLatin1(), QJsonDocument(signedData).toJson(QJsonDocument::Compact), signature); if (std::holds_alternative(signatureMatch)) { //TODO i think there are more failed signature checks than expected. Investigate - qDebug() << signedData; qCWarning(E2EE) << "Failed to verify one-time-key signature for" << user->id() << device << ". Skipping this device."; - //Q_ASSERT(false); continue; } else { } @@ -522,10 +537,11 @@ public: } } connection->sendToDevices("m.room.encrypted", usersToDevicesToEvents); + connection->database()->setDevicesReceivedKey(q->id(), devices, sessionId, index); }); } - void sendMegolmSession() { + void sendMegolmSession(const QHash& devices) { // Save the session to this device const auto sessionId = currentOutboundMegolmSession->sessionId(); const auto _sessionKey = currentOutboundMegolmSession->sessionKey(); @@ -536,11 +552,8 @@ public: const auto sessionKey = std::get(_sessionKey); const auto senderKey = q->connection()->olmAccount()->identityKeys().curve25519; - // Send to key to ourself at this device - addInboundGroupSession(senderKey, sessionId, sessionKey); - // Send the session to other people - sendRoomKeyToDevices(sessionId, sessionKey); + sendRoomKeyToDevices(sessionId, sessionKey, devices, currentOutboundMegolmSession->sessionMessageIndex()); } #endif // Quotient_E2EE_ENABLED @@ -2066,8 +2079,10 @@ QString Room::Private::sendEvent(RoomEventPtr&& event) if (q->usesEncryption()) { if (!hasValidMegolmSession() || shouldRotateMegolmSession()) { createMegolmSession(); - sendMegolmSession(); } + const auto devicesWithoutKey = getDevicesWithoutKey(); + sendMegolmSession(devicesWithoutKey); + //TODO check if this is necessary //TODO check if we increment the sent message count event->setRoomId(id); -- cgit v1.2.3 From efa450920e5fc338e771e653ca0889e948d04ee7 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Tue, 8 Mar 2022 00:06:36 +0100 Subject: Implement sending encrypted files --- autotests/CMakeLists.txt | 1 + autotests/testfilecrypto.cpp | 17 +++++++++++ autotests/testfilecrypto.h | 12 ++++++++ lib/eventitem.cpp | 10 +++++++ lib/eventitem.h | 3 ++ lib/events/encryptedfile.cpp | 26 +++++++++++++++-- lib/events/encryptedfile.h | 1 + lib/room.cpp | 67 +++++++++++++++++++++++++++++--------------- lib/room.h | 2 +- 9 files changed, 113 insertions(+), 26 deletions(-) create mode 100644 autotests/testfilecrypto.cpp create mode 100644 autotests/testfilecrypto.h diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index 671d6c08..c11901bf 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -18,4 +18,5 @@ if(${PROJECT_NAME}_ENABLE_E2EE) quotient_add_test(NAME testgroupsession) quotient_add_test(NAME testolmsession) quotient_add_test(NAME testolmutility) + quotient_add_test(NAME testfilecrypto) endif() diff --git a/autotests/testfilecrypto.cpp b/autotests/testfilecrypto.cpp new file mode 100644 index 00000000..e6bec1fe --- /dev/null +++ b/autotests/testfilecrypto.cpp @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2022 Tobias Fella +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "testfilecrypto.h" +#include "events/encryptedfile.h" +#include + +using namespace Quotient; +void TestFileCrypto::encryptDecryptData() +{ + QByteArray data = "ABCDEF"; + auto [file, cipherText] = EncryptedFile::encryptFile(data); + auto decrypted = file.decryptFile(cipherText); + QCOMPARE(data, decrypted); +} +QTEST_APPLESS_MAIN(TestFileCrypto) diff --git a/autotests/testfilecrypto.h b/autotests/testfilecrypto.h new file mode 100644 index 00000000..9096a8c7 --- /dev/null +++ b/autotests/testfilecrypto.h @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2022 Tobias Fella +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include + +class TestFileCrypto : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void encryptDecryptData(); +}; diff --git a/lib/eventitem.cpp b/lib/eventitem.cpp index a2d65d8d..302ae053 100644 --- a/lib/eventitem.cpp +++ b/lib/eventitem.cpp @@ -26,6 +26,16 @@ void PendingEventItem::setFileUploaded(const QUrl& remoteUrl) setStatus(EventStatus::FileUploaded); } +void PendingEventItem::setEncryptedFile(const EncryptedFile& encryptedFile) +{ + if (auto* rme = getAs()) { + Q_ASSERT(rme->hasFileContent()); + rme->editContent([encryptedFile](EventContent::TypedBase& ec) { + ec.fileInfo()->file = encryptedFile; + }); + } +} + // Not exactly sure why but this helps with the linker not finding // Quotient::EventStatus::staticMetaObject when building Quaternion #include "moc_eventitem.cpp" diff --git a/lib/eventitem.h b/lib/eventitem.h index f04ef323..d8313736 100644 --- a/lib/eventitem.h +++ b/lib/eventitem.h @@ -9,6 +9,8 @@ #include #include +#include "events/encryptedfile.h" + namespace Quotient { namespace EventStatus { @@ -114,6 +116,7 @@ public: void setDeparted() { setStatus(EventStatus::Departed); } void setFileUploaded(const QUrl& remoteUrl); + void setEncryptedFile(const EncryptedFile& encryptedFile); void setReachedServer(const QString& eventId) { setStatus(EventStatus::ReachedServer); diff --git a/lib/events/encryptedfile.cpp b/lib/events/encryptedfile.cpp index d4a517bd..e90be428 100644 --- a/lib/events/encryptedfile.cpp +++ b/lib/events/encryptedfile.cpp @@ -8,6 +8,7 @@ #ifdef Quotient_E2EE_ENABLED #include #include +#include "e2ee/qolmutils.h" #endif using namespace Quotient; @@ -27,7 +28,7 @@ QByteArray EncryptedFile::decryptFile(const QByteArray& ciphertext) const { int length; auto* ctx = EVP_CIPHER_CTX_new(); - QByteArray plaintext(ciphertext.size() + EVP_CIPHER_CTX_block_size(ctx) + QByteArray plaintext(ciphertext.size() + EVP_MAX_BLOCK_LENGTH - 1, '\0'); EVP_DecryptInit_ex(ctx, EVP_aes_256_ctr(), nullptr, @@ -44,7 +45,7 @@ QByteArray EncryptedFile::decryptFile(const QByteArray& ciphertext) const + length, &length); EVP_CIPHER_CTX_free(ctx); - return plaintext; + return plaintext.left(ciphertext.size()); } #else qWarning(MAIN) << "This build of libQuotient doesn't support E2EE, " @@ -53,6 +54,27 @@ QByteArray EncryptedFile::decryptFile(const QByteArray& ciphertext) const #endif } +std::pair EncryptedFile::encryptFile(const QByteArray &plainText) +{ + QByteArray k = getRandom(32); + auto kBase64 = k.toBase64(); + QByteArray iv = getRandom(16); + JWK key = {"oct"_ls, {"encrypt"_ls, "decrypt"_ls}, "A256CTR"_ls, QString(k.toBase64()).replace(u'/', u'_').replace(u'+', u'-').left(kBase64.indexOf('=')), true}; + + int length; + auto* ctx = EVP_CIPHER_CTX_new(); + QByteArray cipherText(plainText.size(), plainText.size() + EVP_MAX_BLOCK_LENGTH - 1); + EVP_EncryptInit_ex(ctx, EVP_aes_256_ctr(), nullptr, reinterpret_cast(k.data()),reinterpret_cast(iv.data())); + EVP_EncryptUpdate(ctx, reinterpret_cast(cipherText.data()), &length, reinterpret_cast(plainText.data()), plainText.size()); + EVP_EncryptFinal_ex(ctx, reinterpret_cast(cipherText.data()) + length, &length); + EVP_CIPHER_CTX_free(ctx); + + auto hash = QCryptographicHash::hash(cipherText, QCryptographicHash::Sha256).toBase64(); + auto ivBase64 = iv.toBase64(); + EncryptedFile file = {{}, key, ivBase64.left(ivBase64.indexOf('=')), {{QStringLiteral("sha256"), hash.left(hash.indexOf('='))}}, "v2"_ls}; + return {file, cipherText}; +} + void JsonObjectConverter::dumpTo(QJsonObject& jo, const EncryptedFile& pod) { diff --git a/lib/events/encryptedfile.h b/lib/events/encryptedfile.h index d0c4a030..2ce35086 100644 --- a/lib/events/encryptedfile.h +++ b/lib/events/encryptedfile.h @@ -46,6 +46,7 @@ public: QString v; QByteArray decryptFile(const QByteArray &ciphertext) const; + static std::pair encryptFile(const QByteArray &plainText); }; template <> diff --git a/lib/room.cpp b/lib/room.cpp index 7db9f8e9..763ca31c 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -299,8 +299,7 @@ public: RoomEvent* addAsPending(RoomEventPtr&& event); - //TODO deleteWhenFinishedis ugly, find out if there's something nicer - QString doSendEvent(const RoomEvent* pEvent, bool deleteWhenFinished = false); + QString doSendEvent(const RoomEvent* pEvent); void onEventSendingFailure(const QString& txnId, BaseJob* call = nullptr); SetRoomStateWithKeyJob* requestSetState(const QString& evtType, @@ -2076,6 +2075,16 @@ QString Room::Private::sendEvent(RoomEventPtr&& event) qCWarning(MAIN) << q << "has been upgraded, event won't be sent"; return {}; } + + return doSendEvent(addAsPending(std::move(event))); +} + +QString Room::Private::doSendEvent(const RoomEvent* pEvent) +{ + const auto txnId = pEvent->transactionId(); + // TODO, #133: Enqueue the job rather than immediately trigger it. + const RoomEvent* _event = pEvent; + if (q->usesEncryption()) { if (!hasValidMegolmSession() || shouldRotateMegolmSession()) { createMegolmSession(); @@ -2083,10 +2092,8 @@ QString Room::Private::sendEvent(RoomEventPtr&& event) const auto devicesWithoutKey = getDevicesWithoutKey(); sendMegolmSession(devicesWithoutKey); - //TODO check if this is necessary //TODO check if we increment the sent message count - event->setRoomId(id); - const auto encrypted = currentOutboundMegolmSession->encrypt(QJsonDocument(event->fullJson()).toJson()); + const auto encrypted = currentOutboundMegolmSession->encrypt(QJsonDocument(pEvent->fullJson()).toJson()); currentOutboundMegolmSession->setMessageCount(currentOutboundMegolmSession->messageCount() + 1); connection->saveCurrentOutboundMegolmSession(q, currentOutboundMegolmSession); if(std::holds_alternative(encrypted)) { @@ -2098,23 +2105,14 @@ QString Room::Private::sendEvent(RoomEventPtr&& event) encryptedEvent->setTransactionId(connection->generateTxnId()); encryptedEvent->setRoomId(id); encryptedEvent->setSender(connection->userId()); - event->setTransactionId(encryptedEvent->transactionId()); // We show the unencrypted event locally while pending. The echo check will throw the encrypted version out - addAsPending(std::move(event)); - return doSendEvent(encryptedEvent, true); + _event = encryptedEvent; } - return doSendEvent(addAsPending(std::move(event))); -} - -QString Room::Private::doSendEvent(const RoomEvent* pEvent, bool deleteWhenFinished) -{ - const auto txnId = pEvent->transactionId(); - // TODO, #133: Enqueue the job rather than immediately trigger it. if (auto call = connection->callApi(BackgroundRequest, id, - pEvent->matrixType(), txnId, - pEvent->contentJson())) { + _event->matrixType(), txnId, + _event->contentJson())) { Room::connect(call, &BaseJob::sentRequest, q, [this, txnId] { auto it = q->findPendingEvent(txnId); if (it == unsyncedEvents.end()) { @@ -2128,7 +2126,7 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent, bool deleteWhenFinis Room::connect(call, &BaseJob::failure, q, std::bind(&Room::Private::onEventSendingFailure, this, txnId, call)); - Room::connect(call, &BaseJob::success, q, [this, call, txnId, deleteWhenFinished, pEvent] { + Room::connect(call, &BaseJob::success, q, [this, call, txnId, _event] { auto it = q->findPendingEvent(txnId); if (it != unsyncedEvents.end()) { if (it->deliveryStatus() != EventStatus::ReachedServer) { @@ -2140,8 +2138,8 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent, bool deleteWhenFinis << "already merged"; emit q->messageSent(txnId, call->eventId()); - if (deleteWhenFinished){ - delete pEvent; + if (q->usesEncryption()){ + delete _event; } }); } else @@ -2266,13 +2264,16 @@ QString Room::Private::doPostFile(RoomEventPtr&& msgEvent, const QUrl& localUrl) // Below, the upload job is used as a context object to clean up connections const auto& transferJob = fileTransfers.value(txnId).job; connect(q, &Room::fileTransferCompleted, transferJob, - [this, txnId](const QString& tId, const QUrl&, const QUrl& mxcUri) { + [this, txnId](const QString& tId, const QUrl&, const QUrl& mxcUri, Omittable encryptedFile) { if (tId != txnId) return; const auto it = q->findPendingEvent(txnId); if (it != unsyncedEvents.end()) { it->setFileUploaded(mxcUri); + if (encryptedFile) { + it->setEncryptedFile(*encryptedFile); + } emit q->pendingEventChanged( int(it - unsyncedEvents.begin())); doSendEvent(it->get()); @@ -2508,6 +2509,20 @@ void Room::uploadFile(const QString& id, const QUrl& localFilename, Q_ASSERT_X(localFilename.isLocalFile(), __FUNCTION__, "localFilename should point at a local file"); auto fileName = localFilename.toLocalFile(); + Omittable encryptedFile = std::nullopt; +#ifdef Quotient_E2EE_ENABLED + QTemporaryFile tempFile; + if (usesEncryption()) { + tempFile.open(); + QFile file(localFilename.toLocalFile()); + file.open(QFile::ReadOnly); + auto [e, data] = EncryptedFile::encryptFile(file.readAll()); + tempFile.write(data); + tempFile.close(); + fileName = QFileInfo(tempFile).absoluteFilePath(); + encryptedFile = e; + } +#endif auto job = connection()->uploadFile(fileName, overrideContentType); if (isJobPending(job)) { d->fileTransfers[id] = { job, fileName, true }; @@ -2516,9 +2531,15 @@ void Room::uploadFile(const QString& id, const QUrl& localFilename, d->fileTransfers[id].update(sent, total); emit fileTransferProgress(id, sent, total); }); - connect(job, &BaseJob::success, this, [this, id, localFilename, job] { + connect(job, &BaseJob::success, this, [this, id, localFilename, job, encryptedFile] { d->fileTransfers[id].status = FileTransferInfo::Completed; - emit fileTransferCompleted(id, localFilename, QUrl(job->contentUri())); + if (encryptedFile) { + auto file = *encryptedFile; + file.url = QUrl(job->contentUri()); + emit fileTransferCompleted(id, localFilename, QUrl(job->contentUri()), file); + } else { + emit fileTransferCompleted(id, localFilename, QUrl(job->contentUri())); + } }); connect(job, &BaseJob::failure, this, std::bind(&Private::failedTransfer, d, id, job->errorString())); diff --git a/lib/room.h b/lib/room.h index 6e6071f0..d5a8366a 100644 --- a/lib/room.h +++ b/lib/room.h @@ -999,7 +999,7 @@ Q_SIGNALS: void newFileTransfer(QString id, QUrl localFile); void fileTransferProgress(QString id, qint64 progress, qint64 total); - void fileTransferCompleted(QString id, QUrl localFile, QUrl mxcUrl); + void fileTransferCompleted(QString id, QUrl localFile, QUrl mxcUrl, Omittable encryptedFile = std::nullopt); void fileTransferFailed(QString id, QString errorMessage = {}); // fileTransferCancelled() is no more here; use fileTransferFailed() and // check the transfer status instead -- cgit v1.2.3 From fcde81c8618fbe10c1cb91c0ec6887a3df705a23 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Tue, 8 Mar 2022 21:44:10 +0100 Subject: Properly create encrypted edits --- lib/events/encryptedevent.cpp | 7 +++++++ lib/events/encryptedevent.h | 2 ++ lib/room.cpp | 3 +++ 3 files changed, 12 insertions(+) diff --git a/lib/events/encryptedevent.cpp b/lib/events/encryptedevent.cpp index 9d07a35f..3af3d6ff 100644 --- a/lib/events/encryptedevent.cpp +++ b/lib/events/encryptedevent.cpp @@ -61,3 +61,10 @@ RoomEventPtr EncryptedEvent::createDecrypted(const QString &decrypted) const } return loadEvent(eventObject); } + +void EncryptedEvent::setRelation(const QJsonObject& relation) +{ + auto content = editJson()["content"_ls].toObject(); + content["m.relates_to"] = relation; + editJson()["content"] = content; +} diff --git a/lib/events/encryptedevent.h b/lib/events/encryptedevent.h index 72efffd4..ddd5e415 100644 --- a/lib/events/encryptedevent.h +++ b/lib/events/encryptedevent.h @@ -56,6 +56,8 @@ public: QString deviceId() const { return contentPart(DeviceIdKeyL); } QString sessionId() const { return contentPart(SessionIdKeyL); } RoomEventPtr createDecrypted(const QString &decrypted) const; + + void setRelation(const QJsonObject& relation); }; REGISTER_EVENT_TYPE(EncryptedEvent) diff --git a/lib/room.cpp b/lib/room.cpp index 763ca31c..a42b7184 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -2105,6 +2105,9 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent) encryptedEvent->setTransactionId(connection->generateTxnId()); encryptedEvent->setRoomId(id); encryptedEvent->setSender(connection->userId()); + if(pEvent->contentJson().contains("m.relates_to"_ls)) { + encryptedEvent->setRelation(pEvent->contentJson()["m.relates_to"_ls].toObject()); + } // We show the unencrypted event locally while pending. The echo check will throw the encrypted version out _event = encryptedEvent; } -- cgit v1.2.3 From e437c29654e8f988ad694083401bbef23fbbcb18 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Mon, 16 May 2022 20:51:41 +0200 Subject: More work; Update olm pickle & timestamps in database; Remove TODOs --- lib/connection.cpp | 12 ++++++++---- lib/connection.h | 3 +-- lib/database.cpp | 18 +++++++++++++++--- lib/database.h | 3 ++- lib/events/encryptedfile.cpp | 4 ++++ lib/room.cpp | 27 ++++++++++++++++----------- 6 files changed, 46 insertions(+), 21 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 2a1b39f9..82046d53 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -33,6 +33,7 @@ #include "jobs/downloadfilejob.h" #include "jobs/mediathumbnailjob.h" #include "jobs/syncjob.h" +#include #ifdef Quotient_E2EE_ENABLED # include "database.h" @@ -2246,21 +2247,24 @@ bool Connection::hasOlmSession(User* user, const QString& deviceId) const QPair Connection::olmEncryptMessage(User* user, const QString& device, const QByteArray& message) { - //TODO be smarter about choosing a session; see e2ee impl guide - //TODO do we need to save the olm session after sending a message? const auto& curveKey = curveKeyForUserDevice(user->id(), device); QOlmMessage::Type type = d->olmSessions[curveKey][0]->encryptMessageType(); auto result = d->olmSessions[curveKey][0]->encrypt(message); + auto pickle = d->olmSessions[curveKey][0]->pickle(picklingMode()); + if (std::holds_alternative(pickle)) { + database()->updateOlmSession(curveKey, d->olmSessions[curveKey][0]->sessionId(), std::get(pickle)); + } else { + qCWarning(E2EE) << "Failed to pickle olm session."; + } return qMakePair(type, result.toCiphertext()); } -//TODO be more consistent with curveKey and identityKey void Connection::createOlmSession(const QString& theirIdentityKey, const QString& theirOneTimeKey) { auto session = QOlmSession::createOutboundSession(olmAccount(), theirIdentityKey, theirOneTimeKey); if (std::holds_alternative(session)) { - //TODO something qCWarning(E2EE) << "Failed to create olm session for " << theirIdentityKey << std::get(session); + return; } d->saveSession(std::get>(session), theirIdentityKey); d->olmSessions[theirIdentityKey].push_back(std::move(std::get>(session))); diff --git a/lib/connection.h b/lib/connection.h index 8bed55da..5a1f1e5c 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -329,8 +329,7 @@ public: void saveCurrentOutboundMegolmSession(Room *room, const QOlmOutboundGroupSessionPtr& data); - //This currently assumes that an olm session with (user, device) exists - //TODO make this return an event? + //This assumes that an olm session with (user, device) exists QPair olmEncryptMessage(User* user, const QString& device, const QByteArray& message); void createOlmSession(const QString& theirIdentityKey, const QString& theirOneTimeKey); #endif // Quotient_E2EE_ENABLED diff --git a/lib/database.cpp b/lib/database.cpp index 4a28fd4c..74b56a02 100644 --- a/lib/database.cpp +++ b/lib/database.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include "e2ee/e2ee.h" #include "e2ee/qolmsession.h" @@ -182,7 +183,7 @@ void Database::saveOlmSession(const QString& senderKey, const QString& sessionId UnorderedMap> Database::loadOlmSessions(const PicklingMode& picklingMode) { - auto query = prepareQuery(QStringLiteral("SELECT * FROM olm_sessions;")); + auto query = prepareQuery(QStringLiteral("SELECT * FROM olm_sessions ORDER BY lastReceived DESC;")); transaction(); execute(query); commit(); @@ -338,7 +339,6 @@ QOlmOutboundGroupSessionPtr Database::loadCurrentOutboundMegolmSession(const QSt void Database::setDevicesReceivedKey(const QString& roomId, QHash devices, const QString& sessionId, int index) { - //TODO this better auto connection = dynamic_cast(parent()); transaction(); for (const auto& user : devices.keys()) { @@ -360,7 +360,7 @@ QHash Database::devicesWithoutKey(Room* room, const QStrin { auto connection = dynamic_cast(parent()); QHash devices; - for (const auto& user : room->users()) {//TODO does this include invited & left? + for (const auto& user : room->users()) { devices[user->id()] = connection->devicesForUser(user); } @@ -375,3 +375,15 @@ QHash Database::devicesWithoutKey(Room* room, const QStrin } return devices; } + +void Database::updateOlmSession(const QString& senderKey, const QString& sessionId, const QByteArray& pickle) +{ + auto query = prepareQuery(QStringLiteral("UPDATE olm_sessions SET pickle=:pickle WHERE senderKey=:senderKey AND sessionId=:sessionId;")); + query.bindValue(":pickle", pickle); + query.bindValue(":senderKey", senderKey); + query.bindValue(":sessionId", sessionId); + transaction(); + execute(query); + commit(); +} + diff --git a/lib/database.h b/lib/database.h index 30f2f203..8ddd7b6d 100644 --- a/lib/database.h +++ b/lib/database.h @@ -32,7 +32,7 @@ public: QByteArray accountPickle(); void setAccountPickle(const QByteArray &pickle); void clear(); - void saveOlmSession(const QString& senderKey, const QString& sessionId, const QByteArray &pickle, const QDateTime& timestamp); + void saveOlmSession(const QString& senderKey, const QString& sessionId, const QByteArray& pickle, const QDateTime& timestamp); UnorderedMap> loadOlmSessions(const PicklingMode& picklingMode); UnorderedMap loadMegolmSessions(const QString& roomId, const PicklingMode& picklingMode); void saveMegolmSession(const QString& roomId, const QString& sessionId, const QByteArray& pickle, const QString& senderId, const QString& olmSessionId); @@ -42,6 +42,7 @@ public: void setOlmSessionLastReceived(const QString& sessionId, const QDateTime& timestamp); QOlmOutboundGroupSessionPtr loadCurrentOutboundMegolmSession(const QString& roomId, const PicklingMode& picklingMode); void saveCurrentOutboundMegolmSession(const QString& roomId, const PicklingMode& picklingMode, const QOlmOutboundGroupSessionPtr& data); + void updateOlmSession(const QString& senderKey, const QString& sessionId, const QByteArray& pickle); // Returns a map User -> [Device] that have not received key yet QHash devicesWithoutKey(Room* room, const QString &sessionId); diff --git a/lib/events/encryptedfile.cpp b/lib/events/encryptedfile.cpp index e90be428..bb4e26c7 100644 --- a/lib/events/encryptedfile.cpp +++ b/lib/events/encryptedfile.cpp @@ -56,6 +56,7 @@ QByteArray EncryptedFile::decryptFile(const QByteArray& ciphertext) const std::pair EncryptedFile::encryptFile(const QByteArray &plainText) { +#ifdef Quotient_E2EE_ENABLED QByteArray k = getRandom(32); auto kBase64 = k.toBase64(); QByteArray iv = getRandom(16); @@ -73,6 +74,9 @@ std::pair EncryptedFile::encryptFile(const QByteArray auto ivBase64 = iv.toBase64(); EncryptedFile file = {{}, key, ivBase64.left(ivBase64.indexOf('=')), {{QStringLiteral("sha256"), hash.left(hash.indexOf('='))}}, "v2"_ls}; return {file, cipherText}; +#else + return {{}, {}}; +#endif } void JsonObjectConverter::dumpTo(QJsonObject& jo, diff --git a/lib/room.cpp b/lib/room.cpp index a42b7184..0ca8f648 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -449,8 +449,8 @@ public: const auto sessionKey = currentOutboundMegolmSession->sessionKey(); if(std::holds_alternative(sessionKey)) { - qCWarning(E2EE) << "Session error"; - //TODO something + qCWarning(E2EE) << "Failed to load key for new megolm session"; + return; } addInboundGroupSession(q->connection()->olmAccount()->identityKeys().curve25519, currentOutboundMegolmSession->sessionId(), std::get(sessionKey), QString(connection->olmAccount()->identityKeys().ed25519)); } @@ -459,7 +459,6 @@ public: { // Noisy but nice for debugging //qCDebug(E2EE) << "Creating the payload for" << user->id() << device << sessionId << sessionKey.toHex(); - //TODO: store {user->id(), device, sessionId, theirIdentityKey}; required for key requests const auto event = makeEvent("m.megolm.v1.aes-sha2", q->id(), sessionId, sessionKey, q->localUser()->id()); QJsonObject payloadJson = event->fullJson(); payloadJson["recipient"] = user->id(); @@ -504,6 +503,9 @@ public: hash[user->id()] = u; } } + if (hash.isEmpty()) { + return; + } auto job = connection->callApi(hash); connect(job, &BaseJob::success, q, [job, this, sessionId, sessionKey, devices, index](){ Connection::UsersToDevicesToEvents usersToDevicesToEvents; @@ -525,7 +527,6 @@ public: signedData.remove("signatures"); auto signatureMatch = QOlmUtility().ed25519Verify(connection->edKeyForUserDevice(user->id(), device).toLatin1(), QJsonDocument(signedData).toJson(QJsonDocument::Compact), signature); if (std::holds_alternative(signatureMatch)) { - //TODO i think there are more failed signature checks than expected. Investigate qCWarning(E2EE) << "Failed to verify one-time-key signature for" << user->id() << device << ". Skipping this device."; continue; } else { @@ -535,8 +536,10 @@ public: usersToDevicesToEvents[user->id()][device] = payloadForUserDevice(user, device, sessionId, sessionKey); } } - connection->sendToDevices("m.room.encrypted", usersToDevicesToEvents); - connection->database()->setDevicesReceivedKey(q->id(), devices, sessionId, index); + if (!usersToDevicesToEvents.empty()) { + connection->sendToDevices("m.room.encrypted", usersToDevicesToEvents); + connection->database()->setDevicesReceivedKey(q->id(), devices, sessionId, index); + } }); } @@ -545,8 +548,8 @@ public: const auto sessionId = currentOutboundMegolmSession->sessionId(); const auto _sessionKey = currentOutboundMegolmSession->sessionKey(); if(std::holds_alternative(_sessionKey)) { - qCWarning(E2EE) << "Session error"; - //TODO something + qCWarning(E2EE) << "Error loading session key"; + return; } const auto sessionKey = std::get(_sessionKey); const auto senderKey = q->connection()->olmAccount()->identityKeys().curve25519; @@ -581,7 +584,6 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState) connect(this, &Room::userAdded, this, [this, connection](){ if(usesEncryption()) { connection->encryptionUpdate(this); - //TODO key at currentIndex to all user devices } }); d->groupSessions = connection->loadRoomMegolmSessions(this); @@ -2086,18 +2088,20 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent) const RoomEvent* _event = pEvent; if (q->usesEncryption()) { +#ifndef Quotient_E2EE_ENABLED + qWarning() << "This build of libQuotient does not support E2EE."; + return {}; +#else if (!hasValidMegolmSession() || shouldRotateMegolmSession()) { createMegolmSession(); } const auto devicesWithoutKey = getDevicesWithoutKey(); sendMegolmSession(devicesWithoutKey); - //TODO check if we increment the sent message count const auto encrypted = currentOutboundMegolmSession->encrypt(QJsonDocument(pEvent->fullJson()).toJson()); currentOutboundMegolmSession->setMessageCount(currentOutboundMegolmSession->messageCount() + 1); connection->saveCurrentOutboundMegolmSession(q, currentOutboundMegolmSession); if(std::holds_alternative(encrypted)) { - //TODO something qWarning(E2EE) << "Error encrypting message" << std::get(encrypted); return {}; } @@ -2110,6 +2114,7 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent) } // We show the unencrypted event locally while pending. The echo check will throw the encrypted version out _event = encryptedEvent; +#endif } if (auto call = -- cgit v1.2.3 From 1b302abce0bfd9fb62cdc721bc7300dc61b1784f Mon Sep 17 00:00:00 2001 From: Tobias Fella <9750016+TobiasFella@users.noreply.github.com> Date: Thu, 10 Mar 2022 21:47:51 +0100 Subject: Update lib/events/encryptedfile.h --- lib/events/encryptedfile.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/events/encryptedfile.h b/lib/events/encryptedfile.h index 2ce35086..022ac91e 100644 --- a/lib/events/encryptedfile.h +++ b/lib/events/encryptedfile.h @@ -46,7 +46,7 @@ public: QString v; QByteArray decryptFile(const QByteArray &ciphertext) const; - static std::pair encryptFile(const QByteArray &plainText); + static std::pair encryptFile(const QByteArray& plainText); }; template <> -- cgit v1.2.3 From 8af39e510e550d001e207bdc0177a1480f6ebcba Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Wed, 23 Mar 2022 20:42:28 +0100 Subject: Add database migration --- lib/database.cpp | 18 +++++++++++++++--- lib/database.h | 1 + 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/database.cpp b/lib/database.cpp index 74b56a02..d2d33006 100644 --- a/lib/database.cpp +++ b/lib/database.cpp @@ -34,6 +34,7 @@ Database::Database(const QString& matrixId, const QString& deviceId, QObject* pa case 0: migrateTo1(); case 1: migrateTo2(); case 2: migrateTo3(); + case 3: migrateTo4(); } } @@ -90,12 +91,11 @@ void Database::migrateTo1() execute(QStringLiteral("CREATE TABLE accounts (pickle TEXT);")); execute(QStringLiteral("CREATE TABLE olm_sessions (senderKey TEXT, sessionId TEXT, pickle TEXT);")); execute(QStringLiteral("CREATE TABLE inbound_megolm_sessions (roomId TEXT, senderKey TEXT, sessionId TEXT, pickle TEXT);")); - execute(QStringLiteral("CREATE TABLE outbound_megolm_sessions (roomId TEXT, sessionId TEXT, pickle TEXT, creationTime TEXT, messageCount INTEGER);")); + execute(QStringLiteral("CREATE TABLE outbound_megolm_sessions (roomId TEXT, senderKey TEXT, sessionId TEXT, pickle TEXT);")); execute(QStringLiteral("CREATE TABLE group_session_record_index (roomId TEXT, sessionId TEXT, i INTEGER, eventId TEXT, ts INTEGER);")); execute(QStringLiteral("CREATE TABLE tracked_users (matrixId TEXT);")); execute(QStringLiteral("CREATE TABLE outdated_users (matrixId TEXT);")); execute(QStringLiteral("CREATE TABLE tracked_devices (matrixId TEXT, deviceId TEXT, curveKeyId TEXT, curveKey TEXT, edKeyId TEXT, edKey TEXT);")); - execute(QStringLiteral("CREATE TABLE sent_megolm_sessions (roomId TEXT, userId TEXT, deviceId TEXT, identityKey TEXT, sessionId TEXT, i INTEGER);")); execute(QStringLiteral("PRAGMA user_version = 1;")); commit(); @@ -108,7 +108,7 @@ void Database::migrateTo2() //TODO remove this column again - we don't need it after all execute(QStringLiteral("ALTER TABLE inbound_megolm_sessions ADD ed25519Key TEXT")); execute(QStringLiteral("ALTER TABLE olm_sessions ADD lastReceived TEXT")); - + // Add indexes for improving queries speed on larger database execute(QStringLiteral("CREATE INDEX sessions_session_idx ON olm_sessions(sessionId)")); execute(QStringLiteral("CREATE INDEX outbound_room_idx ON outbound_megolm_sessions(roomId)")); @@ -132,6 +132,18 @@ void Database::migrateTo3() commit(); } +void Database::migrateTo3() +{ + qCDebug(DATABASE) << "Migrating database to version 4"; + transaction(); + + execute(QStringLiteral("CREATE TABLE sent_megolm_sessions (roomId TEXT, userId TEXT, deviceId TEXT, identityKey TEXT, sessionId TEXT, i INTEGER);")); + execute(QStringLiteral("ALTER TABLE outbound_megolm_sessions ADD creationTime TEXT;")); + execute(QStringLiteral("ALTER TABLE outbound_megolm_sessions ADD messageCount INTEGER;")); + execute(QStringLiteral("PRAGMA user_version = 3;")); + commit(); +} + QByteArray Database::accountPickle() { auto query = prepareQuery(QStringLiteral("SELECT pickle FROM accounts;")); diff --git a/lib/database.h b/lib/database.h index 8ddd7b6d..00002204 100644 --- a/lib/database.h +++ b/lib/database.h @@ -52,6 +52,7 @@ private: void migrateTo1(); void migrateTo2(); void migrateTo3(); + void migrateTo4(); QString m_matrixId; }; -- cgit v1.2.3 From 6f961ff2726c87e679cc9f6c39ed27a92a31cb0d Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sat, 16 Apr 2022 23:32:59 +0200 Subject: Fixes --- lib/room.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/room.cpp b/lib/room.cpp index 0ca8f648..35de59ed 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -452,7 +452,7 @@ public: qCWarning(E2EE) << "Failed to load key for new megolm session"; return; } - addInboundGroupSession(q->connection()->olmAccount()->identityKeys().curve25519, currentOutboundMegolmSession->sessionId(), std::get(sessionKey), QString(connection->olmAccount()->identityKeys().ed25519)); + addInboundGroupSession(currentOutboundMegolmSession->sessionId(), std::get(sessionKey), q->localUser()->id(), "SELF"_ls); } std::unique_ptr payloadForUserDevice(User* user, const QString& device, const QByteArray& sessionId, const QByteArray& sessionKey) -- cgit v1.2.3 From 89d8f6c44f86a27df28b1d89a80564fb0d4d89fc Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Mon, 16 May 2022 21:26:14 +0200 Subject: Fix build failures --- lib/connection.cpp | 12 ++++++------ lib/database.cpp | 10 +++++----- lib/e2ee/qolmoutboundsession.cpp | 2 +- lib/e2ee/qolmoutboundsession.h | 2 +- lib/room.cpp | 16 ++++++++-------- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 82046d53..a5615f64 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -2251,8 +2251,8 @@ QPair Connection::olmEncryptMessage(User* user, c QOlmMessage::Type type = d->olmSessions[curveKey][0]->encryptMessageType(); auto result = d->olmSessions[curveKey][0]->encrypt(message); auto pickle = d->olmSessions[curveKey][0]->pickle(picklingMode()); - if (std::holds_alternative(pickle)) { - database()->updateOlmSession(curveKey, d->olmSessions[curveKey][0]->sessionId(), std::get(pickle)); + if (pickle) { + database()->updateOlmSession(curveKey, d->olmSessions[curveKey][0]->sessionId(), *pickle); } else { qCWarning(E2EE) << "Failed to pickle olm session."; } @@ -2262,12 +2262,12 @@ QPair Connection::olmEncryptMessage(User* user, c void Connection::createOlmSession(const QString& theirIdentityKey, const QString& theirOneTimeKey) { auto session = QOlmSession::createOutboundSession(olmAccount(), theirIdentityKey, theirOneTimeKey); - if (std::holds_alternative(session)) { - qCWarning(E2EE) << "Failed to create olm session for " << theirIdentityKey << std::get(session); + if (!session) { + qCWarning(E2EE) << "Failed to create olm session for " << theirIdentityKey << session.error(); return; } - d->saveSession(std::get>(session), theirIdentityKey); - d->olmSessions[theirIdentityKey].push_back(std::move(std::get>(session))); + d->saveSession(**session, theirIdentityKey); + d->olmSessions[theirIdentityKey].push_back(std::move(*session)); } QOlmOutboundGroupSessionPtr Connection::loadCurrentOutboundMegolmSession(Room* room) diff --git a/lib/database.cpp b/lib/database.cpp index d2d33006..87275e1f 100644 --- a/lib/database.cpp +++ b/lib/database.cpp @@ -132,7 +132,7 @@ void Database::migrateTo3() commit(); } -void Database::migrateTo3() +void Database::migrateTo4() { qCDebug(DATABASE) << "Migrating database to version 4"; transaction(); @@ -313,7 +313,7 @@ void Database::setOlmSessionLastReceived(const QString& sessionId, const QDateTi void Database::saveCurrentOutboundMegolmSession(const QString& roomId, const PicklingMode& picklingMode, const QOlmOutboundGroupSessionPtr& session) { const auto pickle = session->pickle(picklingMode); - if (std::holds_alternative(pickle)) { + if (pickle) { auto deleteQuery = prepareQuery(QStringLiteral("DELETE FROM outbound_megolm_sessions WHERE roomId=:roomId AND sessionId=:sessionId;")); deleteQuery.bindValue(":roomId", roomId); deleteQuery.bindValue(":sessionId", session->sessionId()); @@ -321,7 +321,7 @@ void Database::saveCurrentOutboundMegolmSession(const QString& roomId, const Pic auto insertQuery = prepareQuery(QStringLiteral("INSERT INTO outbound_megolm_sessions(roomId, sessionId, pickle, creationTime, messageCount) VALUES(:roomId, :sessionId, :pickle, :creationTime, :messageCount);")); insertQuery.bindValue(":roomId", roomId); insertQuery.bindValue(":sessionId", session->sessionId()); - insertQuery.bindValue(":pickle", std::get(pickle)); + insertQuery.bindValue(":pickle", pickle.value()); insertQuery.bindValue(":creationTime", session->creationTime()); insertQuery.bindValue(":messageCount", session->messageCount()); @@ -339,8 +339,8 @@ QOlmOutboundGroupSessionPtr Database::loadCurrentOutboundMegolmSession(const QSt execute(query); if (query.next()) { auto sessionResult = QOlmOutboundGroupSession::unpickle(query.value("pickle").toByteArray(), picklingMode); - if (std::holds_alternative(sessionResult)) { - auto session = std::move(std::get(sessionResult)); + if (sessionResult) { + auto session = std::move(*sessionResult); session->setCreationTime(query.value("creationTime").toDateTime()); session->setMessageCount(query.value("messageCount").toInt()); return session; diff --git a/lib/e2ee/qolmoutboundsession.cpp b/lib/e2ee/qolmoutboundsession.cpp index 10b0c4de..76188d08 100644 --- a/lib/e2ee/qolmoutboundsession.cpp +++ b/lib/e2ee/qolmoutboundsession.cpp @@ -60,7 +60,7 @@ QOlmExpected QOlmOutboundGroupSession::pickle(const PicklingMode &mo return pickledBuf; } -QOlmExpected QOlmOutboundGroupSession::unpickle(QByteArray &pickled, const PicklingMode &mode) +QOlmExpected QOlmOutboundGroupSession::unpickle(const QByteArray &pickled, const PicklingMode &mode) { QByteArray pickledBuf = pickled; auto *olmOutboundGroupSession = olm_outbound_group_session(new uint8_t[olm_outbound_group_session_size()]); diff --git a/lib/e2ee/qolmoutboundsession.h b/lib/e2ee/qolmoutboundsession.h index 56b25974..c20613d3 100644 --- a/lib/e2ee/qolmoutboundsession.h +++ b/lib/e2ee/qolmoutboundsession.h @@ -25,7 +25,7 @@ public: //! Deserialises from encrypted Base64 that was previously obtained by //! pickling a `QOlmOutboundGroupSession`. static QOlmExpected unpickle( - QByteArray& pickled, const PicklingMode& mode); + const QByteArray& pickled, const PicklingMode& mode); //! Encrypts a plaintext message using the session. QOlmExpected encrypt(const QString& plaintext); diff --git a/lib/room.cpp b/lib/room.cpp index 35de59ed..d77bf9ef 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -448,11 +448,11 @@ public: connection->saveCurrentOutboundMegolmSession(q, currentOutboundMegolmSession); const auto sessionKey = currentOutboundMegolmSession->sessionKey(); - if(std::holds_alternative(sessionKey)) { + if(!sessionKey) { qCWarning(E2EE) << "Failed to load key for new megolm session"; return; } - addInboundGroupSession(currentOutboundMegolmSession->sessionId(), std::get(sessionKey), q->localUser()->id(), "SELF"_ls); + addInboundGroupSession(currentOutboundMegolmSession->sessionId(), *sessionKey, q->localUser()->id(), "SELF"_ls); } std::unique_ptr payloadForUserDevice(User* user, const QString& device, const QByteArray& sessionId, const QByteArray& sessionKey) @@ -526,7 +526,7 @@ public: signedData.remove("unsigned"); signedData.remove("signatures"); auto signatureMatch = QOlmUtility().ed25519Verify(connection->edKeyForUserDevice(user->id(), device).toLatin1(), QJsonDocument(signedData).toJson(QJsonDocument::Compact), signature); - if (std::holds_alternative(signatureMatch)) { + if (!signatureMatch) { qCWarning(E2EE) << "Failed to verify one-time-key signature for" << user->id() << device << ". Skipping this device."; continue; } else { @@ -547,11 +547,11 @@ public: // Save the session to this device const auto sessionId = currentOutboundMegolmSession->sessionId(); const auto _sessionKey = currentOutboundMegolmSession->sessionKey(); - if(std::holds_alternative(_sessionKey)) { + if(!_sessionKey) { qCWarning(E2EE) << "Error loading session key"; return; } - const auto sessionKey = std::get(_sessionKey); + const auto sessionKey = *_sessionKey; const auto senderKey = q->connection()->olmAccount()->identityKeys().curve25519; // Send the session to other people @@ -2101,11 +2101,11 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent) const auto encrypted = currentOutboundMegolmSession->encrypt(QJsonDocument(pEvent->fullJson()).toJson()); currentOutboundMegolmSession->setMessageCount(currentOutboundMegolmSession->messageCount() + 1); connection->saveCurrentOutboundMegolmSession(q, currentOutboundMegolmSession); - if(std::holds_alternative(encrypted)) { - qWarning(E2EE) << "Error encrypting message" << std::get(encrypted); + if(!encrypted) { + qWarning(E2EE) << "Error encrypting message" << encrypted.error(); return {}; } - auto encryptedEvent = new EncryptedEvent(std::get(encrypted), q->connection()->olmAccount()->identityKeys().curve25519, q->connection()->deviceId(), currentOutboundMegolmSession->sessionId()); + auto encryptedEvent = new EncryptedEvent(*encrypted, q->connection()->olmAccount()->identityKeys().curve25519, q->connection()->deviceId(), currentOutboundMegolmSession->sessionId()); encryptedEvent->setTransactionId(connection->generateTxnId()); encryptedEvent->setRoomId(id); encryptedEvent->setSender(connection->userId()); -- cgit v1.2.3 From c671867a0a3e2a6ad0e7ae6e93fa09467c06188f Mon Sep 17 00:00:00 2001 From: Tobias Fella <9750016+TobiasFella@users.noreply.github.com> Date: Wed, 18 May 2022 22:02:50 +0200 Subject: Apply suggestions from code review Co-authored-by: Alexey Rusakov --- lib/connection.cpp | 7 +++---- lib/connection.h | 2 +- lib/database.cpp | 3 +-- lib/events/encryptedfile.cpp | 2 +- lib/room.h | 2 +- 5 files changed, 7 insertions(+), 9 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index a5615f64..66e21a2a 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -33,7 +33,6 @@ #include "jobs/downloadfilejob.h" #include "jobs/mediathumbnailjob.h" #include "jobs/syncjob.h" -#include #ifdef Quotient_E2EE_ENABLED # include "database.h" @@ -2242,7 +2241,7 @@ bool Connection::isKnownCurveKey(const QString& user, const QString& curveKey) bool Connection::hasOlmSession(User* user, const QString& deviceId) const { const auto& curveKey = curveKeyForUserDevice(user->id(), deviceId); - return d->olmSessions.contains(curveKey) && d->olmSessions[curveKey].size() > 0; + return d->olmSessions.contains(curveKey) && !d->olmSessions[curveKey].empty(); } QPair Connection::olmEncryptMessage(User* user, const QString& device, const QByteArray& message) @@ -2254,9 +2253,9 @@ QPair Connection::olmEncryptMessage(User* user, c if (pickle) { database()->updateOlmSession(curveKey, d->olmSessions[curveKey][0]->sessionId(), *pickle); } else { - qCWarning(E2EE) << "Failed to pickle olm session."; + qCWarning(E2EE) << "Failed to pickle olm session: " << pickle.error(); } - return qMakePair(type, result.toCiphertext()); + return { type, result.toCiphertext() }; } void Connection::createOlmSession(const QString& theirIdentityKey, const QString& theirOneTimeKey) diff --git a/lib/connection.h b/lib/connection.h index 5a1f1e5c..5b266aad 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -134,7 +134,7 @@ class QUOTIENT_API Connection : public QObject { public: using UsersToDevicesToEvents = - UnorderedMap>>; + UnorderedMap>; enum RoomVisibility { PublishRoom, diff --git a/lib/database.cpp b/lib/database.cpp index 87275e1f..99c6f358 100644 --- a/lib/database.cpp +++ b/lib/database.cpp @@ -9,7 +9,6 @@ #include #include #include -#include #include "e2ee/e2ee.h" #include "e2ee/qolmsession.h" @@ -140,7 +139,7 @@ void Database::migrateTo4() execute(QStringLiteral("CREATE TABLE sent_megolm_sessions (roomId TEXT, userId TEXT, deviceId TEXT, identityKey TEXT, sessionId TEXT, i INTEGER);")); execute(QStringLiteral("ALTER TABLE outbound_megolm_sessions ADD creationTime TEXT;")); execute(QStringLiteral("ALTER TABLE outbound_megolm_sessions ADD messageCount INTEGER;")); - execute(QStringLiteral("PRAGMA user_version = 3;")); + execute(QStringLiteral("PRAGMA user_version = 4;")); commit(); } diff --git a/lib/events/encryptedfile.cpp b/lib/events/encryptedfile.cpp index bb4e26c7..d35ee28f 100644 --- a/lib/events/encryptedfile.cpp +++ b/lib/events/encryptedfile.cpp @@ -75,7 +75,7 @@ std::pair EncryptedFile::encryptFile(const QByteArray EncryptedFile file = {{}, key, ivBase64.left(ivBase64.indexOf('=')), {{QStringLiteral("sha256"), hash.left(hash.indexOf('='))}}, "v2"_ls}; return {file, cipherText}; #else - return {{}, {}}; + return {}; #endif } diff --git a/lib/room.h b/lib/room.h index d5a8366a..b1201a6c 100644 --- a/lib/room.h +++ b/lib/room.h @@ -999,7 +999,7 @@ Q_SIGNALS: void newFileTransfer(QString id, QUrl localFile); void fileTransferProgress(QString id, qint64 progress, qint64 total); - void fileTransferCompleted(QString id, QUrl localFile, QUrl mxcUrl, Omittable encryptedFile = std::nullopt); + void fileTransferCompleted(QString id, QUrl localFile, QUrl mxcUrl, Omittable encryptedFile = none); void fileTransferFailed(QString id, QString errorMessage = {}); // fileTransferCancelled() is no more here; use fileTransferFailed() and // check the transfer status instead -- cgit v1.2.3 From 9c4cc1b9b065765843c81a0c555b3afa5122b61e Mon Sep 17 00:00:00 2001 From: Tobias Fella <9750016+TobiasFella@users.noreply.github.com> Date: Wed, 18 May 2022 22:05:48 +0200 Subject: Update lib/events/encryptedevent.cpp Co-authored-by: Alexey Rusakov --- 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 3af3d6ff..c97ccc16 100644 --- a/lib/events/encryptedevent.cpp +++ b/lib/events/encryptedevent.cpp @@ -64,7 +64,7 @@ RoomEventPtr EncryptedEvent::createDecrypted(const QString &decrypted) const void EncryptedEvent::setRelation(const QJsonObject& relation) { - auto content = editJson()["content"_ls].toObject(); + auto content = contentJson(); content["m.relates_to"] = relation; editJson()["content"] = content; } -- cgit v1.2.3 From b29eb3954b798ac9110906cd79c4f288deaa2596 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Wed, 18 May 2022 22:39:27 +0200 Subject: Make database independent of {Room, User, Connection} --- lib/connection.cpp | 12 ++++++------ lib/connection.h | 6 +++--- lib/database.cpp | 23 +++++++---------------- lib/database.h | 6 +++--- lib/room.cpp | 51 ++++++++++++++++++++++++++++----------------------- 5 files changed, 47 insertions(+), 51 deletions(-) diff --git a/lib/connection.cpp b/lib/connection.cpp index 66e21a2a..dba18cb1 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -2214,9 +2214,9 @@ void Connection::saveMegolmSession(const Room* room, session.senderId(), session.olmSessionId()); } -QStringList Connection::devicesForUser(User* user) const +QStringList Connection::devicesForUser(const QString& userId) const { - return d->deviceKeys[user->id()].keys(); + return d->deviceKeys[userId].keys(); } QString Connection::curveKeyForUserDevice(const QString& user, const QString& device) const @@ -2238,15 +2238,15 @@ bool Connection::isKnownCurveKey(const QString& user, const QString& curveKey) return query.next(); } -bool Connection::hasOlmSession(User* user, const QString& deviceId) const +bool Connection::hasOlmSession(const QString& user, const QString& deviceId) const { - const auto& curveKey = curveKeyForUserDevice(user->id(), deviceId); + const auto& curveKey = curveKeyForUserDevice(user, deviceId); return d->olmSessions.contains(curveKey) && !d->olmSessions[curveKey].empty(); } -QPair Connection::olmEncryptMessage(User* user, const QString& device, const QByteArray& message) +QPair Connection::olmEncryptMessage(const QString& user, const QString& device, const QByteArray& message) { - const auto& curveKey = curveKeyForUserDevice(user->id(), device); + const auto& curveKey = curveKeyForUserDevice(user, device); QOlmMessage::Type type = d->olmSessions[curveKey][0]->encryptMessageType(); auto result = d->olmSessions[curveKey][0]->encrypt(message); auto pickle = d->olmSessions[curveKey][0]->pickle(picklingMode()); diff --git a/lib/connection.h b/lib/connection.h index 5b266aad..f8744752 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -323,14 +323,14 @@ public: const Room* room); void saveMegolmSession(const Room* room, const QOlmInboundGroupSession& session); - bool hasOlmSession(User* user, const QString& deviceId) const; + bool hasOlmSession(const QString& user, const QString& deviceId) const; QOlmOutboundGroupSessionPtr loadCurrentOutboundMegolmSession(Room* room); void saveCurrentOutboundMegolmSession(Room *room, const QOlmOutboundGroupSessionPtr& data); //This assumes that an olm session with (user, device) exists - QPair olmEncryptMessage(User* user, const QString& device, const QByteArray& message); + QPair olmEncryptMessage(const QString& userId, const QString& device, const QByteArray& message); void createOlmSession(const QString& theirIdentityKey, const QString& theirOneTimeKey); #endif // Quotient_E2EE_ENABLED Q_INVOKABLE Quotient::SyncJob* syncJob() const; @@ -694,7 +694,7 @@ public Q_SLOTS: PicklingMode picklingMode() const; QJsonObject decryptNotification(const QJsonObject ¬ification); - QStringList devicesForUser(User* user) const; + QStringList devicesForUser(const QString& user) const; QString curveKeyForUserDevice(const QString &user, const QString& device) const; QString edKeyForUserDevice(const QString& user, const QString& device) const; bool isKnownCurveKey(const QString& user, const QString& curveKey); diff --git a/lib/database.cpp b/lib/database.cpp index 99c6f358..3255e5e7 100644 --- a/lib/database.cpp +++ b/lib/database.cpp @@ -13,9 +13,7 @@ #include "e2ee/e2ee.h" #include "e2ee/qolmsession.h" #include "e2ee/qolminboundsession.h" -#include "connection.h" -#include "user.h" -#include "room.h" +#include "e2ee/qolmoutboundsession.h" using namespace Quotient; Database::Database(const QString& matrixId, const QString& deviceId, QObject* parent) @@ -348,17 +346,16 @@ QOlmOutboundGroupSessionPtr Database::loadCurrentOutboundMegolmSession(const QSt return nullptr; } -void Database::setDevicesReceivedKey(const QString& roomId, QHash devices, const QString& sessionId, int index) +void Database::setDevicesReceivedKey(const QString& roomId, const QHash>>& devices, const QString& sessionId, int index) { - auto connection = dynamic_cast(parent()); transaction(); for (const auto& user : devices.keys()) { - for (const auto& device : devices[user]) { + for (const auto& [device, curveKey] : devices[user]) { auto query = prepareQuery(QStringLiteral("INSERT INTO sent_megolm_sessions(roomId, userId, deviceId, identityKey, sessionId, i) VALUES(:roomId, :userId, :deviceId, :identityKey, :sessionId, :i);")); query.bindValue(":roomId", roomId); - query.bindValue(":userId", user->id()); + query.bindValue(":userId", user); query.bindValue(":deviceId", device); - query.bindValue(":identityKey", connection->curveKeyForUserDevice(user->id(), device)); + query.bindValue(":identityKey", curveKey); query.bindValue(":sessionId", sessionId); query.bindValue(":i", index); execute(query); @@ -367,16 +364,10 @@ void Database::setDevicesReceivedKey(const QString& roomId, QHash Database::devicesWithoutKey(Room* room, const QString &sessionId) +QHash Database::devicesWithoutKey(const QString& roomId, QHash& devices, const QString &sessionId) { - auto connection = dynamic_cast(parent()); - QHash devices; - for (const auto& user : room->users()) { - devices[user->id()] = connection->devicesForUser(user); - } - auto query = prepareQuery(QStringLiteral("SELECT userId, deviceId FROM sent_megolm_sessions WHERE roomId=:roomId AND sessionId=:sessionId")); - query.bindValue(":roomId", room->id()); + query.bindValue(":roomId", roomId); query.bindValue(":sessionId", sessionId); transaction(); execute(query); diff --git a/lib/database.h b/lib/database.h index 00002204..8bef332f 100644 --- a/lib/database.h +++ b/lib/database.h @@ -44,9 +44,9 @@ public: void saveCurrentOutboundMegolmSession(const QString& roomId, const PicklingMode& picklingMode, const QOlmOutboundGroupSessionPtr& data); void updateOlmSession(const QString& senderKey, const QString& sessionId, const QByteArray& pickle); - // Returns a map User -> [Device] that have not received key yet - QHash devicesWithoutKey(Room* room, const QString &sessionId); - void setDevicesReceivedKey(const QString& roomId, QHash devices, const QString& sessionId, int index); + // Returns a map UserId -> [DeviceId] that have not received key yet + QHash devicesWithoutKey(const QString& roomId, QHash& devices, const QString &sessionId); + void setDevicesReceivedKey(const QString& roomId, const QHash>>& devices, const QString& sessionId, int index); private: void migrateTo1(); diff --git a/lib/room.cpp b/lib/room.cpp index d77bf9ef..5d3ae329 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -455,16 +455,16 @@ public: addInboundGroupSession(currentOutboundMegolmSession->sessionId(), *sessionKey, q->localUser()->id(), "SELF"_ls); } - std::unique_ptr payloadForUserDevice(User* user, const QString& device, const QByteArray& sessionId, const QByteArray& sessionKey) + std::unique_ptr payloadForUserDevice(QString user, const QString& device, const QByteArray& sessionId, const QByteArray& sessionKey) { // Noisy but nice for debugging //qCDebug(E2EE) << "Creating the payload for" << user->id() << device << sessionId << sessionKey.toHex(); const auto event = makeEvent("m.megolm.v1.aes-sha2", q->id(), sessionId, sessionKey, q->localUser()->id()); QJsonObject payloadJson = event->fullJson(); - payloadJson["recipient"] = user->id(); + payloadJson["recipient"] = user; payloadJson["sender"] = connection->user()->id(); QJsonObject recipientObject; - recipientObject["ed25519"] = connection->edKeyForUserDevice(user->id(), device); + recipientObject["ed25519"] = connection->edKeyForUserDevice(user, device); payloadJson["recipient_keys"] = recipientObject; QJsonObject senderObject; senderObject["ed25519"] = QString(connection->olmAccount()->identityKeys().ed25519); @@ -472,22 +472,21 @@ public: payloadJson["sender_device"] = connection->deviceId(); auto cipherText = connection->olmEncryptMessage(user, device, QJsonDocument(payloadJson).toJson(QJsonDocument::Compact)); QJsonObject encrypted; - encrypted[connection->curveKeyForUserDevice(user->id(), device)] = QJsonObject{{"type", cipherText.first}, {"body", QString(cipherText.second)}}; + encrypted[connection->curveKeyForUserDevice(user, device)] = QJsonObject{{"type", cipherText.first}, {"body", QString(cipherText.second)}}; return makeEvent(encrypted, connection->olmAccount()->identityKeys().curve25519); } - QHash getDevicesWithoutKey() const + QHash getDevicesWithoutKey() const { - QHash devices; - auto rawDevices = q->connection()->database()->devicesWithoutKey(q, QString(currentOutboundMegolmSession->sessionId())); - for (const auto& user : rawDevices.keys()) { - devices[q->connection()->user(user)] = rawDevices[user]; + QHash devices; + for (const auto& user : q->users()) { + devices[user->id()] = q->connection()->devicesForUser(user->id()); } - return devices; + return q->connection()->database()->devicesWithoutKey(q->id(), devices, QString(currentOutboundMegolmSession->sessionId())); } - void sendRoomKeyToDevices(const QByteArray& sessionId, const QByteArray& sessionKey, const QHash devices, int index) + void sendRoomKeyToDevices(const QByteArray& sessionId, const QByteArray& sessionKey, const QHash devices, int index) { qCDebug(E2EE) << "Sending room key to devices" << sessionId, sessionKey.toHex(); QHash> hash; @@ -500,7 +499,7 @@ public: } } if (!u.isEmpty()) { - hash[user->id()] = u; + hash[user] = u; } } if (hash.isEmpty()) { @@ -512,38 +511,44 @@ public: const auto data = job->jsonData(); for(const auto &user : devices.keys()) { for(const auto &device : devices[user]) { - const auto recipientCurveKey = connection->curveKeyForUserDevice(user->id(), device); + const auto recipientCurveKey = connection->curveKeyForUserDevice(user, device); if (!connection->hasOlmSession(user, device)) { qCDebug(E2EE) << "Creating a new session for" << user << device; - if(data["one_time_keys"][user->id()][device].toObject().isEmpty()) { + if(data["one_time_keys"][user][device].toObject().isEmpty()) { qWarning() << "No one time key for" << user << device; continue; } - const auto keyId = data["one_time_keys"][user->id()][device].toObject().keys()[0]; - const auto oneTimeKey = data["one_time_keys"][user->id()][device][keyId]["key"].toString(); - const auto signature = data["one_time_keys"][user->id()][device][keyId]["signatures"][user->id()][QStringLiteral("ed25519:") + device].toString().toLatin1(); - auto signedData = data["one_time_keys"][user->id()][device][keyId].toObject(); + const auto keyId = data["one_time_keys"][user][device].toObject().keys()[0]; + const auto oneTimeKey = data["one_time_keys"][user][device][keyId]["key"].toString(); + const auto signature = data["one_time_keys"][user][device][keyId]["signatures"][user][QStringLiteral("ed25519:") + device].toString().toLatin1(); + auto signedData = data["one_time_keys"][user][device][keyId].toObject(); signedData.remove("unsigned"); signedData.remove("signatures"); - auto signatureMatch = QOlmUtility().ed25519Verify(connection->edKeyForUserDevice(user->id(), device).toLatin1(), QJsonDocument(signedData).toJson(QJsonDocument::Compact), signature); + auto signatureMatch = QOlmUtility().ed25519Verify(connection->edKeyForUserDevice(user, device).toLatin1(), QJsonDocument(signedData).toJson(QJsonDocument::Compact), signature); if (!signatureMatch) { - qCWarning(E2EE) << "Failed to verify one-time-key signature for" << user->id() << device << ". Skipping this device."; + qCWarning(E2EE) << "Failed to verify one-time-key signature for" << user << device << ". Skipping this device."; continue; } else { } connection->createOlmSession(recipientCurveKey, oneTimeKey); } - usersToDevicesToEvents[user->id()][device] = payloadForUserDevice(user, device, sessionId, sessionKey); + usersToDevicesToEvents[user][device] = payloadForUserDevice(user, device, sessionId, sessionKey); } } if (!usersToDevicesToEvents.empty()) { connection->sendToDevices("m.room.encrypted", usersToDevicesToEvents); - connection->database()->setDevicesReceivedKey(q->id(), devices, sessionId, index); + QHash>> receivedDevices; + for (const auto& user : devices.keys()) { + for (const auto& device : devices[user]) { + receivedDevices[user] += {device, q->connection()->curveKeyForUserDevice(user, device) }; + } + } + connection->database()->setDevicesReceivedKey(q->id(), receivedDevices, sessionId, index); } }); } - void sendMegolmSession(const QHash& devices) { + void sendMegolmSession(const QHash& devices) { // Save the session to this device const auto sessionId = currentOutboundMegolmSession->sessionId(); const auto _sessionKey = currentOutboundMegolmSession->sessionKey(); -- cgit v1.2.3 From 41897df408c1398881bb8cf82ae0dc4503cefef7 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Thu, 19 May 2022 14:11:18 +0200 Subject: Use list of 3-tuple instead of map --- lib/database.cpp | 22 ++++++++++------------ lib/database.h | 2 +- lib/room.cpp | 4 ++-- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/lib/database.cpp b/lib/database.cpp index 3255e5e7..0119b35c 100644 --- a/lib/database.cpp +++ b/lib/database.cpp @@ -346,20 +346,18 @@ QOlmOutboundGroupSessionPtr Database::loadCurrentOutboundMegolmSession(const QSt return nullptr; } -void Database::setDevicesReceivedKey(const QString& roomId, const QHash>>& devices, const QString& sessionId, int index) +void Database::setDevicesReceivedKey(const QString& roomId, const QVector>& devices, const QString& sessionId, int index) { transaction(); - for (const auto& user : devices.keys()) { - for (const auto& [device, curveKey] : devices[user]) { - auto query = prepareQuery(QStringLiteral("INSERT INTO sent_megolm_sessions(roomId, userId, deviceId, identityKey, sessionId, i) VALUES(:roomId, :userId, :deviceId, :identityKey, :sessionId, :i);")); - query.bindValue(":roomId", roomId); - query.bindValue(":userId", user); - query.bindValue(":deviceId", device); - query.bindValue(":identityKey", curveKey); - query.bindValue(":sessionId", sessionId); - query.bindValue(":i", index); - execute(query); - } + for (const auto& [user, device, curveKey] : devices) { + auto query = prepareQuery(QStringLiteral("INSERT INTO sent_megolm_sessions(roomId, userId, deviceId, identityKey, sessionId, i) VALUES(:roomId, :userId, :deviceId, :identityKey, :sessionId, :i);")); + query.bindValue(":roomId", roomId); + query.bindValue(":userId", user); + query.bindValue(":deviceId", device); + query.bindValue(":identityKey", curveKey); + query.bindValue(":sessionId", sessionId); + query.bindValue(":i", index); + execute(query); } commit(); } diff --git a/lib/database.h b/lib/database.h index 8bef332f..ef251d66 100644 --- a/lib/database.h +++ b/lib/database.h @@ -46,7 +46,7 @@ public: // Returns a map UserId -> [DeviceId] that have not received key yet QHash devicesWithoutKey(const QString& roomId, QHash& devices, const QString &sessionId); - void setDevicesReceivedKey(const QString& roomId, const QHash>>& devices, const QString& sessionId, int index); + void setDevicesReceivedKey(const QString& roomId, const QVector>& devices, const QString& sessionId, int index); private: void migrateTo1(); diff --git a/lib/room.cpp b/lib/room.cpp index 5d3ae329..3696f808 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -537,10 +537,10 @@ public: } if (!usersToDevicesToEvents.empty()) { connection->sendToDevices("m.room.encrypted", usersToDevicesToEvents); - QHash>> receivedDevices; + QVector> receivedDevices; for (const auto& user : devices.keys()) { for (const auto& device : devices[user]) { - receivedDevices[user] += {device, q->connection()->curveKeyForUserDevice(user, device) }; + receivedDevices += {user, device, q->connection()->curveKeyForUserDevice(user, device) }; } } connection->database()->setDevicesReceivedKey(q->id(), receivedDevices, sessionId, index); -- cgit v1.2.3 From 146c2f73a22be32033a4999fd722cb92dcdf3c2f Mon Sep 17 00:00:00 2001 From: Tobias Fella <9750016+TobiasFella@users.noreply.github.com> Date: Thu, 19 May 2022 14:15:40 +0200 Subject: Update lib/room.cpp Co-authored-by: Alexey Rusakov --- lib/room.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/room.cpp b/lib/room.cpp index 3696f808..7a3c66b6 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -2522,7 +2522,7 @@ void Room::uploadFile(const QString& id, const QUrl& localFilename, Q_ASSERT_X(localFilename.isLocalFile(), __FUNCTION__, "localFilename should point at a local file"); auto fileName = localFilename.toLocalFile(); - Omittable encryptedFile = std::nullopt; + Omittable encryptedFile { none }; #ifdef Quotient_E2EE_ENABLED QTemporaryFile tempFile; if (usesEncryption()) { -- cgit v1.2.3 From 7b0de6473b6e23f1d74e7ad5739ad86c6b243797 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Thu, 19 May 2022 14:32:38 +0200 Subject: Apply Suggestions --- lib/room.cpp | 4 ++-- lib/room.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/room.cpp b/lib/room.cpp index 7a3c66b6..e0494f83 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -2551,7 +2551,7 @@ void Room::uploadFile(const QString& id, const QUrl& localFilename, file.url = QUrl(job->contentUri()); emit fileTransferCompleted(id, localFilename, QUrl(job->contentUri()), file); } else { - emit fileTransferCompleted(id, localFilename, QUrl(job->contentUri())); + emit fileTransferCompleted(id, localFilename, QUrl(job->contentUri()), none); } }); connect(job, &BaseJob::failure, this, @@ -2620,7 +2620,7 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename) connect(job, &BaseJob::success, this, [this, eventId, fileUrl, job] { d->fileTransfers[eventId].status = FileTransferInfo::Completed; emit fileTransferCompleted( - eventId, fileUrl, QUrl::fromLocalFile(job->targetFileName())); + eventId, fileUrl, QUrl::fromLocalFile(job->targetFileName()), none); }); connect(job, &BaseJob::failure, this, std::bind(&Private::failedTransfer, d, eventId, diff --git a/lib/room.h b/lib/room.h index b1201a6c..c3bdc4a0 100644 --- a/lib/room.h +++ b/lib/room.h @@ -999,7 +999,7 @@ Q_SIGNALS: void newFileTransfer(QString id, QUrl localFile); void fileTransferProgress(QString id, qint64 progress, qint64 total); - void fileTransferCompleted(QString id, QUrl localFile, QUrl mxcUrl, Omittable encryptedFile = none); + void fileTransferCompleted(QString id, QUrl localFile, QUrl mxcUrl, Omittable encryptedFile); void fileTransferFailed(QString id, QString errorMessage = {}); // fileTransferCancelled() is no more here; use fileTransferFailed() and // check the transfer status instead -- cgit v1.2.3 From 7a1283d2cc753781d4adbf4c69d3167651fce97b Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Thu, 19 May 2022 14:54:00 +0200 Subject: Apply suggestions --- lib/room.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/room.cpp b/lib/room.cpp index e0494f83..1f29d551 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -2091,6 +2091,7 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent) const auto txnId = pEvent->transactionId(); // TODO, #133: Enqueue the job rather than immediately trigger it. const RoomEvent* _event = pEvent; + std::unique_ptr encryptedEvent; if (q->usesEncryption()) { #ifndef Quotient_E2EE_ENABLED @@ -2110,7 +2111,7 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent) qWarning(E2EE) << "Error encrypting message" << encrypted.error(); return {}; } - auto encryptedEvent = new EncryptedEvent(*encrypted, q->connection()->olmAccount()->identityKeys().curve25519, q->connection()->deviceId(), currentOutboundMegolmSession->sessionId()); + encryptedEvent = makeEvent(*encrypted, q->connection()->olmAccount()->identityKeys().curve25519, q->connection()->deviceId(), currentOutboundMegolmSession->sessionId()); encryptedEvent->setTransactionId(connection->generateTxnId()); encryptedEvent->setRoomId(id); encryptedEvent->setSender(connection->userId()); @@ -2118,7 +2119,7 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent) encryptedEvent->setRelation(pEvent->contentJson()["m.relates_to"_ls].toObject()); } // We show the unencrypted event locally while pending. The echo check will throw the encrypted version out - _event = encryptedEvent; + _event = encryptedEvent.get(); #endif } @@ -2151,9 +2152,6 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent) << "already merged"; emit q->messageSent(txnId, call->eventId()); - if (q->usesEncryption()){ - delete _event; - } }); } else onEventSendingFailure(txnId); @@ -2553,6 +2551,7 @@ void Room::uploadFile(const QString& id, const QUrl& localFilename, } else { emit fileTransferCompleted(id, localFilename, QUrl(job->contentUri()), none); } + }); connect(job, &BaseJob::failure, this, std::bind(&Private::failedTransfer, d, id, job->errorString())); -- cgit v1.2.3 From 5df53b8d5c8b21228ecf9938330dd4d85d3de6af Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Thu, 19 May 2022 16:01:07 +0200 Subject: Document devices tuple --- lib/database.h | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/database.h b/lib/database.h index ef251d66..45348c8d 100644 --- a/lib/database.h +++ b/lib/database.h @@ -46,6 +46,7 @@ public: // Returns a map UserId -> [DeviceId] that have not received key yet QHash devicesWithoutKey(const QString& roomId, QHash& devices, const QString &sessionId); + // 'devices' contains tuples {userId, deviceId, curveKey} void setDevicesReceivedKey(const QString& roomId, const QVector>& devices, const QString& sessionId, int index); private: -- cgit v1.2.3