From ff54bf2d0979dc6b9b3b77bba827ae7f3baa9f58 Mon Sep 17 00:00:00 2001
From: Tobias Fella <fella@posteo.de>
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<QString>("algorithm"_ls); }
     QString roomId() const { return contentPart<QString>(RoomIdKeyL); }
-- 
cgit v1.2.3


From 20bd96ac67c06617e619460c4cd07b5e15cc74d7 Mon Sep 17 00:00:00 2001
From: Tobias Fella <fella@posteo.de>
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<SendToDeviceJob>(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<QOlmMessage::Type, QByteArray> 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<QOlmError>(session)) {
+        //TODO something
+        qCWarning(E2EE) << "Failed to create olm session for " << theirIdentityKey << std::get<QOlmError>(session);
+    }
+    d->saveSession(std::get<std::unique_ptr<QOlmSession>>(session), theirIdentityKey);
+    d->olmSessions[theirIdentityKey].push_back(std::move(std::get<std::unique_ptr<QOlmSession>>(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<QString, UnorderedMap<QString, const Event&>>;
+        UnorderedMap<QString, UnorderedMap<QString, std::unique_ptr<Event>>>;
 
     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<QOlmMessage::Type, QByteArray> 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<QString, QOlmInboundGroupSessionPtr> groupSessions;
+    int currentMegolmSessionMessageCount = 0;
+    //TODO save this to database
+    unsigned long long currentMegolmSessionCreationTimestamp = 0;
+    std::unique_ptr<QOlmOutboundGroupSession> 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<EncryptionEvent>()->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<EncryptionEvent>()->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<EncryptedEvent> 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<RoomKeyEvent>("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<EncryptedEvent>(encrypted, connection->olmAccount()->identityKeys().curve25519);
+    }
+
+    void sendRoomKeyToDevices(const QByteArray& sessionId, const QByteArray& sessionKey)
+    {
+        qWarning() << "Sending room key to devices" << sessionId, sessionKey.toHex();
+        QHash<QString, QHash<QString, QString>> hash;
+        for (const auto& user : q->users()) {
+            QHash<QString, QString> 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<ClaimKeysJob>(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<QOlmError>(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<QOlmError>(_sessionKey)) {
+            qCWarning(E2EE) << "Session error";
+            //TODO something
+        }
+        const auto sessionKey = std::get<QByteArray>(_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<QOlmError>(encrypted)) {
+            //TODO something
+            qWarning(E2EE) << "Error encrypting message" << std::get<QOlmError>(encrypted);
+            return {};
+        }
+        auto encryptedEvent = new EncryptedEvent(std::get<QByteArray>(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 <fella@posteo.de>
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<QOlmMessage::Type, QByteArray> 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<std::unique_ptr<QOlmSession>>(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<QOlmMessage::Type, QByteArray> 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<QByteArray>(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<QByteArray>(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<QOlmOutboundGroupSessionPtr>(sessionResult)) {
+            auto session = std::move(std::get<QOlmOutboundGroupSessionPtr>(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 <QtCore/QVector>
 
 #include "e2ee/e2ee.h"
+
 namespace Quotient {
 class QUOTIENT_API Database : public QObject
 {
@@ -34,6 +35,8 @@ public:
     std::pair<QString, qint64> 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<QOlmOutboundGroupSessionPtr> 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<QByteArray> 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<QOlmOutboundGroupSessionPtr> unpickle(
         QByteArray& pickled, const PicklingMode& mode);
+
     //! Encrypts a plaintext message using the session.
     QOlmExpected<QByteArray> encrypt(const QString& plaintext);
 
@@ -44,8 +45,16 @@ public:
     //! ratchet key that will be used for the next message.
     QOlmExpected<QByteArray> 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<QOlmOutboundGroupSession> 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<EncryptedEvent> 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<QString, QHash<QString, QString>> hash;
         for (const auto& user : q->users()) {
             QHash<QString, QString> u;
@@ -493,7 +491,7 @@ public:
         auto job = connection->callApi<ClaimKeysJob>(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<QOlmError>(encrypted)) {
             //TODO something
             qWarning(E2EE) << "Error encrypting message" << std::get<QOlmError>(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 <fella@posteo.de>
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<QOlmMessage::Type, QByteArray> 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<User *, QStringList> devices, const QString& sessionId, int index)
+{
+    //TODO this better
+    auto connection = dynamic_cast<Connection *>(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<QString, QStringList> Database::devicesWithoutKey(Room* room, const QString &sessionId)
+{
+    auto connection = dynamic_cast<Connection *>(parent());
+    QHash<QString, QStringList> 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 <QtSql/QSqlQuery>
 #include <QtCore/QVector>
 
+#include <QtCore/QHash>
+
 #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<QString, QStringList> devicesWithoutKey(Room* room, const QString &sessionId);
+    void setDevicesReceivedKey(const QString& roomId, QHash<User *, QStringList> 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<QOlmError>(sessionKey)) {
+            qCWarning(E2EE) << "Session error";
+            //TODO something
+        }
+        addInboundGroupSession(q->connection()->olmAccount()->identityKeys().curve25519, currentOutboundMegolmSession->sessionId(), std::get<QByteArray>(sessionKey), QString(connection->olmAccount()->identityKeys().ed25519));
     }
 
     std::unique_ptr<EncryptedEvent> payloadForUserDevice(User* user, const QString& device, const QByteArray& sessionId, const QByteArray& sessionKey)
@@ -472,13 +479,23 @@ public:
         return makeEvent<EncryptedEvent>(encrypted, connection->olmAccount()->identityKeys().curve25519);
     }
 
-    void sendRoomKeyToDevices(const QByteArray& sessionId, const QByteArray& sessionKey)
+    QHash<User*, QStringList> getDevicesWithoutKey() const
+    {
+        QHash<User*, QStringList> 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<User*, QStringList> devices, int index)
     {
         qCDebug(E2EE) << "Sending room key to devices" << sessionId, sessionKey.toHex();
         QHash<QString, QHash<QString, QString>> hash;
-        for (const auto& user : q->users()) {
+        for (const auto& user : devices.keys()) {
             QHash<QString, QString> 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<ClaimKeysJob>(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<QOlmError>(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<User *, QStringList>& 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<QByteArray>(_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 <fella@posteo.de>
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 <fella@posteo.de>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "testfilecrypto.h"
+#include "events/encryptedfile.h"
+#include <qtest.h>
+
+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 <fella@posteo.de>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include <QtTest/QtTest>
+
+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<RoomMessageEvent>()) {
+        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 <any>
 #include <utility>
 
+#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 <openssl/evp.h>
 #include <QtCore/QCryptographicHash>
+#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, QByteArray> 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<const unsigned char*>(k.data()),reinterpret_cast<const unsigned char*>(iv.data()));
+    EVP_EncryptUpdate(ctx, reinterpret_cast<unsigned char*>(cipherText.data()), &length, reinterpret_cast<const unsigned char*>(plainText.data()), plainText.size());
+    EVP_EncryptFinal_ex(ctx, reinterpret_cast<unsigned char*>(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<EncryptedFile>::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<EncryptedFile, QByteArray> 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<QOlmError>(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<SendMessageJob>(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> 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> 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> 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 <fella@posteo.de>
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<RoomEvent>(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<QString>(DeviceIdKeyL); }
     QString sessionId() const { return contentPart<QString>(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 <fella@posteo.de>
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 <variant>
 
 #ifdef Quotient_E2EE_ENABLED
 #    include "database.h"
@@ -2246,21 +2247,24 @@ bool Connection::hasOlmSession(User* user, const QString& deviceId) const
 
 QPair<QOlmMessage::Type, QByteArray> 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<QByteArray>(pickle)) {
+        database()->updateOlmSession(curveKey, d->olmSessions[curveKey][0]->sessionId(), std::get<QByteArray>(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<QOlmError>(session)) {
-        //TODO something
         qCWarning(E2EE) << "Failed to create olm session for " << theirIdentityKey << std::get<QOlmError>(session);
+        return;
     }
     d->saveSession(std::get<std::unique_ptr<QOlmSession>>(session), theirIdentityKey);
     d->olmSessions[theirIdentityKey].push_back(std::move(std::get<std::unique_ptr<QOlmSession>>(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<QOlmMessage::Type, QByteArray> 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 <QtCore/QStandardPaths>
 #include <QtCore/QDebug>
 #include <QtCore/QDir>
+#include <ctime>
 
 #include "e2ee/e2ee.h"
 #include "e2ee/qolmsession.h"
@@ -182,7 +183,7 @@ void Database::saveOlmSession(const QString& senderKey, const QString& sessionId
 
 UnorderedMap<QString, std::vector<QOlmSessionPtr>> 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<User *, QStringList> devices, const QString& sessionId, int index)
 {
-    //TODO this better
     auto connection = dynamic_cast<Connection *>(parent());
     transaction();
     for (const auto& user : devices.keys()) {
@@ -360,7 +360,7 @@ QHash<QString, QStringList> Database::devicesWithoutKey(Room* room, const QStrin
 {
     auto connection = dynamic_cast<Connection *>(parent());
     QHash<QString, QStringList> 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<QString, QStringList> 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<QString, std::vector<QOlmSessionPtr>> loadOlmSessions(const PicklingMode& picklingMode);
     UnorderedMap<QString, QOlmInboundGroupSessionPtr> 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<QString, QStringList> 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, QByteArray> 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, QByteArray> 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<EncryptedFile>::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<QOlmError>(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<QByteArray>(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<RoomKeyEvent>("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<ClaimKeysJob>(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<QOlmError>(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<QOlmError>(_sessionKey)) {
-            qCWarning(E2EE) << "Session error";
-            //TODO something
+            qCWarning(E2EE) << "Error loading session key";
+            return;
         }
         const auto sessionKey = std::get<QByteArray>(_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<QOlmError>(encrypted)) {
-            //TODO something
             qWarning(E2EE) << "Error encrypting message" << std::get<QOlmError>(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<EncryptedFile, QByteArray> encryptFile(const QByteArray &plainText);
+    static std::pair<EncryptedFile, QByteArray> encryptFile(const QByteArray& plainText);
 };
 
 template <>
-- 
cgit v1.2.3


From 8af39e510e550d001e207bdc0177a1480f6ebcba Mon Sep 17 00:00:00 2001
From: Tobias Fella <fella@posteo.de>
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 <fella@posteo.de>
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<QByteArray>(sessionKey), QString(connection->olmAccount()->identityKeys().ed25519));
+        addInboundGroupSession(currentOutboundMegolmSession->sessionId(), std::get<QByteArray>(sessionKey), q->localUser()->id(), "SELF"_ls);
     }
 
     std::unique_ptr<EncryptedEvent> 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 <fella@posteo.de>
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<QOlmMessage::Type, QByteArray> 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<QByteArray>(pickle)) {
-        database()->updateOlmSession(curveKey, d->olmSessions[curveKey][0]->sessionId(), std::get<QByteArray>(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<QOlmMessage::Type, QByteArray> 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<QOlmError>(session)) {
-        qCWarning(E2EE) << "Failed to create olm session for " << theirIdentityKey << std::get<QOlmError>(session);
+    if (!session) {
+        qCWarning(E2EE) << "Failed to create olm session for " << theirIdentityKey << session.error();
         return;
     }
-    d->saveSession(std::get<std::unique_ptr<QOlmSession>>(session), theirIdentityKey);
-    d->olmSessions[theirIdentityKey].push_back(std::move(std::get<std::unique_ptr<QOlmSession>>(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<QByteArray>(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<QByteArray>(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<QOlmOutboundGroupSessionPtr>(sessionResult)) {
-            auto session = std::move(std::get<QOlmOutboundGroupSessionPtr>(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<QByteArray> QOlmOutboundGroupSession::pickle(const PicklingMode &mo
     return pickledBuf;
 }
 
-QOlmExpected<QOlmOutboundGroupSessionPtr> QOlmOutboundGroupSession::unpickle(QByteArray &pickled, const PicklingMode &mode)
+QOlmExpected<QOlmOutboundGroupSessionPtr> 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<QOlmOutboundGroupSessionPtr> unpickle(
-        QByteArray& pickled, const PicklingMode& mode);
+        const QByteArray& pickled, const PicklingMode& mode);
 
     //! Encrypts a plaintext message using the session.
     QOlmExpected<QByteArray> 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<QOlmError>(sessionKey)) {
+        if(!sessionKey) {
             qCWarning(E2EE) << "Failed to load key for new megolm session";
             return;
         }
-        addInboundGroupSession(currentOutboundMegolmSession->sessionId(), std::get<QByteArray>(sessionKey), q->localUser()->id(), "SELF"_ls);
+        addInboundGroupSession(currentOutboundMegolmSession->sessionId(), *sessionKey, q->localUser()->id(), "SELF"_ls);
     }
 
     std::unique_ptr<EncryptedEvent> 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<QOlmError>(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<QOlmError>(_sessionKey)) {
+        if(!_sessionKey) {
             qCWarning(E2EE) << "Error loading session key";
             return;
         }
-        const auto sessionKey = std::get<QByteArray>(_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<QOlmError>(encrypted)) {
-            qWarning(E2EE) << "Error encrypting message" << std::get<QOlmError>(encrypted);
+        if(!encrypted) {
+            qWarning(E2EE) << "Error encrypting message" << encrypted.error();
             return {};
         }
-        auto encryptedEvent = new EncryptedEvent(std::get<QByteArray>(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 <Kitsune-Ral@users.sf.net>
---
 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 <variant>
 
 #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<QOlmMessage::Type, QByteArray> Connection::olmEncryptMessage(User* user, const QString& device, const QByteArray& message)
@@ -2254,9 +2253,9 @@ QPair<QOlmMessage::Type, QByteArray> 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<QString, UnorderedMap<QString, std::unique_ptr<Event>>>;
+        UnorderedMap<QString, UnorderedMap<QString, EventPtr>>;
 
     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 <QtCore/QStandardPaths>
 #include <QtCore/QDebug>
 #include <QtCore/QDir>
-#include <ctime>
 
 #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, QByteArray> 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> encryptedFile = std::nullopt);
+    void fileTransferCompleted(QString id, QUrl localFile, QUrl mxcUrl, Omittable<EncryptedFile> 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 <Kitsune-Ral@users.sf.net>
---
 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 <fella@posteo.de>
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<QOlmMessage::Type, QByteArray> Connection::olmEncryptMessage(User* user, const QString& device, const QByteArray& message)
+QPair<QOlmMessage::Type, QByteArray> 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<QOlmMessage::Type, QByteArray> olmEncryptMessage(User* user, const QString& device, const QByteArray& message);
+    QPair<QOlmMessage::Type, QByteArray> 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 &notification);
 
-    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<User *, QStringList> devices, const QString& sessionId, int index)
+void Database::setDevicesReceivedKey(const QString& roomId, const QHash<QString, QList<std::pair<QString, QString>>>& devices, const QString& sessionId, int index)
 {
-    auto connection = dynamic_cast<Connection *>(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<User *, QStrin
     commit();
 }
 
-QHash<QString, QStringList> Database::devicesWithoutKey(Room* room, const QString &sessionId)
+QHash<QString, QStringList> Database::devicesWithoutKey(const QString& roomId, QHash<QString, QStringList>& devices, const QString &sessionId)
 {
-    auto connection = dynamic_cast<Connection *>(parent());
-    QHash<QString, QStringList> 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<QString, QStringList> devicesWithoutKey(Room* room, const QString &sessionId);
-    void setDevicesReceivedKey(const QString& roomId, QHash<User *, QStringList> devices, const QString& sessionId, int index);
+    // Returns a map UserId -> [DeviceId] that have not received key yet
+    QHash<QString, QStringList> devicesWithoutKey(const QString& roomId, QHash<QString, QStringList>& devices, const QString &sessionId);
+    void setDevicesReceivedKey(const QString& roomId, const QHash<QString, QList<std::pair<QString, QString>>>& 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<EncryptedEvent> payloadForUserDevice(User* user, const QString& device, const QByteArray& sessionId, const QByteArray& sessionKey)
+    std::unique_ptr<EncryptedEvent> 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<RoomKeyEvent>("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<EncryptedEvent>(encrypted, connection->olmAccount()->identityKeys().curve25519);
     }
 
-    QHash<User*, QStringList> getDevicesWithoutKey() const
+    QHash<QString, QStringList> getDevicesWithoutKey() const
     {
-        QHash<User*, QStringList> 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<QString, QStringList> 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<User*, QStringList> devices, int index)
+    void sendRoomKeyToDevices(const QByteArray& sessionId, const QByteArray& sessionKey, const QHash<QString, QStringList> devices, int index)
     {
         qCDebug(E2EE) << "Sending room key to devices" << sessionId, sessionKey.toHex();
         QHash<QString, QHash<QString, QString>> 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<QString, QList<std::pair<QString, QString>>> 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<User *, QStringList>& devices) {
+    void sendMegolmSession(const QHash<QString, QStringList>& 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 <fella@posteo.de>
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<QString, QList<std::pair<QString, QString>>>& devices, const QString& sessionId, int index)
+void Database::setDevicesReceivedKey(const QString& roomId, const QVector<std::tuple<QString, QString, QString>>& 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<QString, QStringList> devicesWithoutKey(const QString& roomId, QHash<QString, QStringList>& devices, const QString &sessionId);
-    void setDevicesReceivedKey(const QString& roomId, const QHash<QString, QList<std::pair<QString, QString>>>& devices, const QString& sessionId, int index);
+    void setDevicesReceivedKey(const QString& roomId, const QVector<std::tuple<QString, QString, QString>>& 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<QString, QList<std::pair<QString, QString>>> receivedDevices;
+                QVector<std::tuple<QString, QString, QString>> 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 <Kitsune-Ral@users.sf.net>
---
 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> encryptedFile = std::nullopt;
+    Omittable<EncryptedFile> 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 <fella@posteo.de>
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> encryptedFile = none);
+    void fileTransferCompleted(QString id, QUrl localFile, QUrl mxcUrl, Omittable<EncryptedFile> 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 <fella@posteo.de>
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> 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<EncryptedEvent>(*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 <fella@posteo.de>
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<QString, QStringList> devicesWithoutKey(const QString& roomId, QHash<QString, QStringList>& devices, const QString &sessionId);
+    // 'devices' contains tuples {userId, deviceId, curveKey}
     void setDevicesReceivedKey(const QString& roomId, const QVector<std::tuple<QString, QString, QString>>& devices, const QString& sessionId, int index);
 
 private:
-- 
cgit v1.2.3