aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--autotests/CMakeLists.txt1
-rw-r--r--autotests/testfilecrypto.cpp17
-rw-r--r--autotests/testfilecrypto.h12
-rw-r--r--lib/connection.cpp70
-rw-r--r--lib/connection.h14
-rw-r--r--lib/database.cpp112
-rw-r--r--lib/database.h18
-rw-r--r--lib/e2ee/qolmoutboundsession.cpp24
-rw-r--r--lib/e2ee/qolmoutboundsession.h10
-rw-r--r--lib/eventitem.cpp10
-rw-r--r--lib/eventitem.h3
-rw-r--r--lib/events/encryptedevent.cpp7
-rw-r--r--lib/events/encryptedevent.h2
-rw-r--r--lib/events/encryptedfile.cpp30
-rw-r--r--lib/events/encryptedfile.h1
-rw-r--r--lib/events/roomkeyevent.cpp13
-rw-r--r--lib/events/roomkeyevent.h1
-rw-r--r--lib/room.cpp248
-rw-r--r--lib/room.h2
19 files changed, 566 insertions, 29 deletions
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/connection.cpp b/lib/connection.cpp
index d99ab64d..28377dd9 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 "e2ee/qolmaccount.h"
@@ -223,13 +224,23 @@ public:
std::pair<QString, QString> sessionDecryptPrekey(const QOlmMessage& message, const QString &senderKey, std::unique_ptr<QOlmAccount>& olmAccount)
{
Q_ASSERT(message.type() == QOlmMessage::PreKey);
- for(auto& session : olmSessions[senderKey]) {
+ for (size_t i = 0; i < olmSessions[senderKey].size(); i++) {
+ auto& session = olmSessions[senderKey][i];
const auto matches = session->matchesInboundSessionFrom(senderKey, message);
if(std::holds_alternative<bool>(matches) && std::get<bool>(matches)) {
qCDebug(E2EE) << "Found inbound session";
const auto result = session->decrypt(message);
if(std::holds_alternative<QString>(result)) {
q->database()->setOlmSessionLastReceived(QString(session->sessionId()), QDateTime::currentDateTime());
+ auto pickle = session->pickle(q->picklingMode());
+ if (std::holds_alternative<QByteArray>(pickle)) {
+ q->database()->updateOlmSession(senderKey, session->sessionId(), std::get<QByteArray>(pickle));
+ } else {
+ qCWarning(E2EE) << "Failed to pickle olm session.";
+ }
+ auto s = std::move(session);
+ olmSessions[senderKey].erase(olmSessions[senderKey].begin() + i);
+ olmSessions[senderKey].insert(olmSessions[senderKey].begin(), std::move(s));
return { std::get<QString>(result), session->sessionId() };
} else {
qCDebug(E2EE) << "Failed to decrypt prekey message";
@@ -251,7 +262,7 @@ public:
const auto result = newSession->decrypt(message);
QString sessionId = newSession->sessionId();
saveSession(newSession, senderKey);
- olmSessions[senderKey].push_back(std::move(newSession));
+ olmSessions[senderKey].insert(olmSessions[senderKey].begin(), std::move(newSession));
if(std::holds_alternative<QString>(result)) {
return { std::get<QString>(result), sessionId };
} else {
@@ -262,10 +273,20 @@ public:
std::pair<QString, QString> sessionDecryptGeneral(const QOlmMessage& message, const QString &senderKey)
{
Q_ASSERT(message.type() == QOlmMessage::General);
- for(auto& session : olmSessions[senderKey]) {
+ for (size_t i = 0; i < olmSessions[senderKey].size(); i++) {
+ auto& session = olmSessions[senderKey][i];
const auto result = session->decrypt(message);
if(std::holds_alternative<QString>(result)) {
q->database()->setOlmSessionLastReceived(QString(session->sessionId()), QDateTime::currentDateTime());
+ auto pickle = session->pickle(q->picklingMode());
+ if (std::holds_alternative<QByteArray>(pickle)) {
+ q->database()->updateOlmSession(senderKey, session->sessionId(), std::get<QByteArray>(pickle));
+ } else {
+ qCWarning(E2EE) << "Failed to pickle olm session.";
+ }
+ auto s = std::move(session);
+ olmSessions[senderKey].erase(olmSessions[senderKey].begin() + i);
+ olmSessions[senderKey].insert(olmSessions[senderKey].begin(), std::move(s));
return { std::get<QString>(result), session->sessionId() };
}
}
@@ -1338,7 +1359,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,
@@ -2225,6 +2246,47 @@ QString Connection::edKeyForUserDevice(const QString& user, const QString& devic
return d->deviceKeys[user][device].keys["ed25519:" % device];
}
+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)
+{
+ 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());
+}
+
+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);
+ 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)));
+}
+
+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);
+}
+
bool Connection::isKnownCurveKey(const QString& user, const QString& curveKey)
{
auto query = database()->prepareQuery(QStringLiteral("SELECT * FROM tracked_devices WHERE matrixId=:matrixId AND curveKey=:curveKey"));
diff --git a/lib/connection.h b/lib/connection.h
index 29731593..12db2e30 100644
--- a/lib/connection.h
+++ b/lib/connection.h
@@ -24,6 +24,8 @@
#ifdef Quotient_E2EE_ENABLED
#include "e2ee/e2ee.h"
+#include "e2ee/qolmmessage.h"
+#include "e2ee/qolmoutboundsession.h"
#endif
Q_DECLARE_METATYPE(Quotient::GetLoginFlowsJob::LoginFlow)
@@ -132,7 +134,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,
@@ -317,6 +319,16 @@ public:
#ifdef Quotient_E2EE_ENABLED
QOlmAccount* olmAccount() const;
Database* database();
+ bool hasOlmSession(User* 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);
+ void createOlmSession(const QString& theirIdentityKey, const QString& theirOneTimeKey);
+
UnorderedMap<QString, QOlmInboundGroupSessionPtr> loadRoomMegolmSessions(Room* room);
void saveMegolmSession(Room* room, QOlmInboundGroupSession* session);
#endif // Quotient_E2EE_ENABLED
diff --git a/lib/database.cpp b/lib/database.cpp
index 3189b3f1..902d0487 100644
--- a/lib/database.cpp
+++ b/lib/database.cpp
@@ -9,10 +9,14 @@
#include <QtCore/QStandardPaths>
#include <QtCore/QDebug>
#include <QtCore/QDir>
+#include <ctime>
#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)
@@ -30,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();
}
}
@@ -100,10 +105,9 @@ void Database::migrateTo2()
{
qCDebug(DATABASE) << "Migrating database to version 2";
transaction();
- //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)"));
@@ -127,6 +131,18 @@ void Database::migrateTo3()
commit();
}
+void Database::migrateTo4()
+{
+ qCDebug(DATABASE) << "Migrating database to ersion 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 = 4;"));
+ commit();
+}
+
QByteArray Database::accountPickle()
{
auto query = prepareQuery(QStringLiteral("SELECT pickle FROM accounts;"));
@@ -178,7 +194,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();
@@ -290,3 +306,93 @@ 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;
+}
+
+void Database::setDevicesReceivedKey(const QString& roomId, QHash<User *, QStringList> 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]) {
+ 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()) {
+ 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;
+}
+
+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 08fe49f3..3eb26b0a 100644
--- a/lib/database.h
+++ b/lib/database.h
@@ -7,8 +7,16 @@
#include <QtSql/QSqlQuery>
#include <QtCore/QVector>
+#include <QtCore/QHash>
+
#include "e2ee/e2ee.h"
+
+#include "e2ee/qolmoutboundsession.h"
+
namespace Quotient {
+class User;
+class Room;
+
class QUOTIENT_API Database : public QObject
{
Q_OBJECT
@@ -26,7 +34,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);
@@ -34,11 +42,19 @@ 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);
+ 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);
private:
void migrateTo1();
void migrateTo2();
void migrateTo3();
+ void migrateTo4();
QString m_matrixId;
};
diff --git a/lib/e2ee/qolmoutboundsession.cpp b/lib/e2ee/qolmoutboundsession.cpp
index da32417b..8852bcf3 100644
--- a/lib/e2ee/qolmoutboundsession.cpp
+++ b/lib/e2ee/qolmoutboundsession.cpp
@@ -61,13 +61,13 @@ std::variant<QByteArray, QOlmError> QOlmOutboundGroupSession::pickle(const Pickl
return pickledBuf;
}
-std::variant<std::unique_ptr<QOlmOutboundGroupSession>, QOlmError> QOlmOutboundGroupSession::unpickle(QByteArray &pickled, const PicklingMode &mode)
+std::variant<std::unique_ptr<QOlmOutboundGroupSession>, QOlmError> 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()]);
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 @@ std::variant<QByteArray, QOlmError> 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 32ba2b3b..10ca35c0 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 std::variant<std::unique_ptr<QOlmOutboundGroupSession>, QOlmError>
- unpickle(QByteArray& pickled, const PicklingMode& mode);
+ unpickle(const QByteArray& pickled, const PicklingMode& mode);
//! Encrypts a plaintext message using the session.
std::variant<QByteArray, QOlmError> encrypt(const QString &plaintext);
@@ -44,8 +44,16 @@ public:
//! ratchet key that will be used for the next message.
std::variant<QByteArray, QOlmError> 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();
};
using QOlmOutboundGroupSessionPtr = std::unique_ptr<QOlmOutboundGroupSession>;
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/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/events/encryptedfile.cpp b/lib/events/encryptedfile.cpp
index d4a517bd..bb4e26c7 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,31 @@ QByteArray EncryptedFile::decryptFile(const QByteArray& ciphertext) const
#endif
}
+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);
+ 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};
+#else
+ return {{}, {}};
+#endif
+}
+
void JsonObjectConverter<EncryptedFile>::dumpTo(QJsonObject& jo,
const EncryptedFile& pod)
{
diff --git a/lib/events/encryptedfile.h b/lib/events/encryptedfile.h
index 0558563f..b2808395 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/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 c4df7936..cb3fe7e7 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); }
diff --git a/lib/room.cpp b/lib/room.cpp
index 183e242a..61f57245 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
@@ -339,6 +341,8 @@ public:
#ifdef Quotient_E2EE_ENABLED
UnorderedMap<QString, QOlmInboundGroupSessionPtr> groupSessions;
+ QOlmOutboundGroupSessionPtr currentOutboundMegolmSession = nullptr;
+
bool addInboundGroupSession(QString sessionId, QString sessionKey, const QString& senderId, const QString& olmSessionId)
{
if (groupSessions.find(sessionId) != groupSessions.end()) {
@@ -396,6 +400,156 @@ public:
}
return content;
}
+
+ bool shouldRotateMegolmSession() const
+ {
+ if (!q->usesEncryption()) {
+ return false;
+ }
+ return currentOutboundMegolmSession->messageCount() >= rotationMessageCount() || currentOutboundMegolmSession->creationTime().addMSecs(rotationInterval()) < QDateTime::currentDateTime();
+ }
+
+ 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();
+ connection->saveCurrentOutboundMegolmSession(q, currentOutboundMegolmSession);
+
+ const auto sessionKey = currentOutboundMegolmSession->sessionKey();
+ if(std::holds_alternative<QOlmError>(sessionKey)) {
+ 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));
+ }
+
+ 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();
+ 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);
+ }
+
+ 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 : devices.keys()) {
+ QHash<QString, QString> u;
+ 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";
+ }
+ }
+ if (!u.isEmpty()) {
+ 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;
+ 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);
+ if (!connection->hasOlmSession(user, device)) {
+ qCDebug(E2EE) << "Creating a new session for" << user << device;
+ if(data["one_time_keys"][user->id()][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();
+ 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)) {
+ qCWarning(E2EE) << "Failed to verify one-time-key signature for" << user->id() << device << ". Skipping this device.";
+ continue;
+ } else {
+ }
+ connection->createOlmSession(recipientCurveKey, oneTimeKey);
+ }
+ usersToDevicesToEvents[user->id()][device] = payloadForUserDevice(user, device, sessionId, sessionKey);
+ }
+ }
+ if (!usersToDevicesToEvents.empty()) {
+ connection->sendToDevices("m.room.encrypted", usersToDevicesToEvents);
+ connection->database()->setDevicesReceivedKey(q->id(), devices, sessionId, index);
+ }
+ });
+ }
+
+ void sendMegolmSession(const QHash<User *, QStringList>& devices) {
+ // Save the session to this device
+ const auto sessionId = currentOutboundMegolmSession->sessionId();
+ const auto _sessionKey = currentOutboundMegolmSession->sessionKey();
+ if(std::holds_alternative<QOlmError>(_sessionKey)) {
+ qCWarning(E2EE) << "Error loading session key";
+ return;
+ }
+ const auto sessionKey = std::get<QByteArray>(_sessionKey);
+ const auto senderKey = q->connection()->olmAccount()->identityKeys().curve25519;
+
+ // Send the session to other people
+ sendRoomKeyToDevices(sessionId, sessionKey, devices, currentOutboundMegolmSession->sessionMessageIndex());
+ }
+
#endif // Quotient_E2EE_ENABLED
private:
@@ -430,6 +584,18 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState)
}
});
d->groupSessions = connection->loadRoomMegolmSessions(this);
+ 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";
+
+ });
connect(this, &Room::beforeDestruction, this, [=](){
connection->database()->clearRoomData(id);
@@ -1897,26 +2063,54 @@ RoomEvent* Room::Private::addAsPending(RoomEventPtr&& event)
QString Room::Private::sendEvent(RoomEventPtr&& event)
{
- if (q->usesEncryption()) {
- qCCritical(MAIN) << "Room" << q->objectName()
- << "enforces encryption; sending encrypted messages "
- "is not supported yet";
+ if (!q->successorId().isEmpty()) {
+ qCWarning(MAIN) << q << "has been upgraded, event won't be sent";
+ return {};
}
- 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)
{
const auto txnId = pEvent->transactionId();
// TODO, #133: Enqueue the job rather than immediately trigger it.
+ 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);
+
+ 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);
+ 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());
+ 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;
+#endif
+ }
+
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()) {
@@ -1930,7 +2124,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, _event] {
auto it = q->findPendingEvent(txnId);
if (it != unsyncedEvents.end()) {
if (it->deliveryStatus() != EventStatus::ReachedServer) {
@@ -1942,6 +2136,9 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent)
<< "already merged";
emit q->messageSent(txnId, call->eventId());
+ if (q->usesEncryption()){
+ delete _event;
+ }
});
} else
onEventSendingFailure(txnId);
@@ -2065,13 +2262,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());
@@ -2306,6 +2506,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 };
@@ -2314,9 +2528,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 6ba7feac..f5199eff 100644
--- a/lib/room.h
+++ b/lib/room.h
@@ -997,7 +997,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