aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlexey Rusakov <Kitsune-Ral@users.sf.net>2022-05-19 16:14:14 +0200
committerGitHub <noreply@github.com>2022-05-19 16:14:14 +0200
commit0f2c40957b438d0a2f5ce7cb2ba033bab077cbeb (patch)
treef6d147201d13885819d2c34462cf13f499fac944
parent77b190d822c1e980b98b84999f0cfb609ed05a49 (diff)
parent5df53b8d5c8b21228ecf9938330dd4d85d3de6af (diff)
downloadlibquotient-0f2c40957b438d0a2f5ce7cb2ba033bab077cbeb.tar.gz
libquotient-0f2c40957b438d0a2f5ce7cb2ba033bab077cbeb.zip
Merge pull request #540 from TobiasFella/sendmessages
Implement sending encrypted messages
-rw-r--r--autotests/CMakeLists.txt1
-rw-r--r--autotests/testfilecrypto.cpp17
-rw-r--r--autotests/testfilecrypto.h12
-rw-r--r--lib/connection.cpp47
-rw-r--r--lib/connection.h15
-rw-r--r--lib/database.cpp99
-rw-r--r--lib/database.h17
-rw-r--r--lib/e2ee/qolmoutboundsession.cpp24
-rw-r--r--lib/e2ee/qolmoutboundsession.h11
-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.cpp256
-rw-r--r--lib/room.h2
19 files changed, 539 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 2528b70b..dba18cb1 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,
@@ -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,4 +2238,45 @@ bool Connection::isKnownCurveKey(const QString& user, const QString& curveKey)
return query.next();
}
+bool Connection::hasOlmSession(const QString& user, const QString& deviceId) const
+{
+ const auto& curveKey = curveKeyForUserDevice(user, deviceId);
+ return d->olmSessions.contains(curveKey) && !d->olmSessions[curveKey].empty();
+}
+
+QPair<QOlmMessage::Type, QByteArray> Connection::olmEncryptMessage(const QString& user, const QString& device, const QByteArray& message)
+{
+ 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());
+ if (pickle) {
+ database()->updateOlmSession(curveKey, d->olmSessions[curveKey][0]->sessionId(), *pickle);
+ } else {
+ qCWarning(E2EE) << "Failed to pickle olm session: " << pickle.error();
+ }
+ return { type, result.toCiphertext() };
+}
+
+void Connection::createOlmSession(const QString& theirIdentityKey, const QString& theirOneTimeKey)
+{
+ auto session = QOlmSession::createOutboundSession(olmAccount(), theirIdentityKey, theirOneTimeKey);
+ if (!session) {
+ qCWarning(E2EE) << "Failed to create olm session for " << theirIdentityKey << session.error();
+ return;
+ }
+ d->saveSession(**session, theirIdentityKey);
+ d->olmSessions[theirIdentityKey].push_back(std::move(*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 b75bd5b5..f8744752 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, EventPtr>>;
enum RoomVisibility {
PublishRoom,
@@ -321,6 +323,15 @@ public:
const Room* room);
void saveMegolmSession(const Room* room,
const QOlmInboundGroupSession& session);
+ 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(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;
Q_INVOKABLE int millisToReconnect() const;
@@ -683,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 e2e7acc9..0119b35c 100644
--- a/lib/database.cpp
+++ b/lib/database.cpp
@@ -13,6 +13,7 @@
#include "e2ee/e2ee.h"
#include "e2ee/qolmsession.h"
#include "e2ee/qolminboundsession.h"
+#include "e2ee/qolmoutboundsession.h"
using namespace Quotient;
Database::Database(const QString& matrixId, const QString& deviceId, QObject* parent)
@@ -30,6 +31,7 @@ Database::Database(const QString& matrixId, const QString& deviceId, QObject* pa
case 0: migrateTo1();
case 1: migrateTo2();
case 2: migrateTo3();
+ case 3: migrateTo4();
}
}
@@ -103,7 +105,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)"));
@@ -127,6 +129,18 @@ void Database::migrateTo3()
commit();
}
+void Database::migrateTo4()
+{
+ 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 = 4;"));
+ commit();
+}
+
QByteArray Database::accountPickle()
{
auto query = prepareQuery(QStringLiteral("SELECT pickle FROM accounts;"));
@@ -178,7 +192,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();
@@ -292,3 +306,84 @@ 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 (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", pickle.value());
+ 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 (sessionResult) {
+ auto session = std::move(*sessionResult);
+ session->setCreationTime(query.value("creationTime").toDateTime());
+ session->setMessageCount(query.value("messageCount").toInt());
+ return session;
+ }
+ }
+ return nullptr;
+}
+
+void Database::setDevicesReceivedKey(const QString& roomId, const QVector<std::tuple<QString, QString, QString>>& devices, const QString& sessionId, int index)
+{
+ transaction();
+ 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();
+}
+
+QHash<QString, QStringList> Database::devicesWithoutKey(const QString& roomId, QHash<QString, QStringList>& devices, const QString &sessionId)
+{
+ auto query = prepareQuery(QStringLiteral("SELECT userId, deviceId FROM sent_megolm_sessions WHERE roomId=:roomId AND sessionId=:sessionId"));
+ query.bindValue(":roomId", roomId);
+ 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..45348c8d 100644
--- a/lib/database.h
+++ b/lib/database.h
@@ -7,8 +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
@@ -26,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);
@@ -34,11 +40,20 @@ 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 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:
void migrateTo1();
void migrateTo2();
void migrateTo3();
+ void migrateTo4();
QString m_matrixId;
};
diff --git a/lib/e2ee/qolmoutboundsession.cpp b/lib/e2ee/qolmoutboundsession.cpp
index 96bad344..76188d08 100644
--- a/lib/e2ee/qolmoutboundsession.cpp
+++ b/lib/e2ee/qolmoutboundsession.cpp
@@ -60,13 +60,13 @@ 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()]);
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..c20613d3 100644
--- a/lib/e2ee/qolmoutboundsession.h
+++ b/lib/e2ee/qolmoutboundsession.h
@@ -25,7 +25,8 @@ 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);
@@ -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/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..c97ccc16 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 = contentJson();
+ 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..d35ee28f 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 d0c4a030..022ac91e 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 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); }
diff --git a/lib/room.cpp b/lib/room.cpp
index 1314803e..1f29d551 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
@@ -338,6 +340,10 @@ public:
#ifdef Quotient_E2EE_ENABLED
UnorderedMap<QString, QOlmInboundGroupSessionPtr> groupSessions;
+ int currentMegolmSessionMessageCount = 0;
+ //TODO save this to database
+ unsigned long long currentMegolmSessionCreationTimestamp = 0;
+ QOlmOutboundGroupSessionPtr currentOutboundMegolmSession = nullptr;
bool addInboundGroupSession(QString sessionId, QByteArray sessionKey,
const QString& senderId,
@@ -402,6 +408,161 @@ 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(!sessionKey) {
+ qCWarning(E2EE) << "Failed to load key for new megolm session";
+ return;
+ }
+ addInboundGroupSession(currentOutboundMegolmSession->sessionId(), *sessionKey, q->localUser()->id(), "SELF"_ls);
+ }
+
+ 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;
+ payloadJson["sender"] = connection->user()->id();
+ QJsonObject recipientObject;
+ recipientObject["ed25519"] = connection->edKeyForUserDevice(user, 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, device)] = QJsonObject{{"type", cipherText.first}, {"body", QString(cipherText.second)}};
+
+ return makeEvent<EncryptedEvent>(encrypted, connection->olmAccount()->identityKeys().curve25519);
+ }
+
+ QHash<QString, QStringList> getDevicesWithoutKey() const
+ {
+ QHash<QString, QStringList> devices;
+ for (const auto& user : q->users()) {
+ devices[user->id()] = q->connection()->devicesForUser(user->id());
+ }
+ return q->connection()->database()->devicesWithoutKey(q->id(), devices, QString(currentOutboundMegolmSession->sessionId()));
+ }
+
+ 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;
+ 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] = 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, device);
+ if (!connection->hasOlmSession(user, device)) {
+ qCDebug(E2EE) << "Creating a new session for" << user << device;
+ 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][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, device).toLatin1(), QJsonDocument(signedData).toJson(QJsonDocument::Compact), signature);
+ if (!signatureMatch) {
+ qCWarning(E2EE) << "Failed to verify one-time-key signature for" << user << device << ". Skipping this device.";
+ continue;
+ } else {
+ }
+ connection->createOlmSession(recipientCurveKey, oneTimeKey);
+ }
+ usersToDevicesToEvents[user][device] = payloadForUserDevice(user, device, sessionId, sessionKey);
+ }
+ }
+ if (!usersToDevicesToEvents.empty()) {
+ connection->sendToDevices("m.room.encrypted", usersToDevicesToEvents);
+ 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) };
+ }
+ }
+ connection->database()->setDevicesReceivedKey(q->id(), receivedDevices, sessionId, index);
+ }
+ });
+ }
+
+ void sendMegolmSession(const QHash<QString, QStringList>& devices) {
+ // Save the session to this device
+ const auto sessionId = currentOutboundMegolmSession->sessionId();
+ const auto _sessionKey = currentOutboundMegolmSession->sessionKey();
+ if(!_sessionKey) {
+ qCWarning(E2EE) << "Error loading session key";
+ return;
+ }
+ const auto sessionKey = *_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:
@@ -431,6 +592,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);
@@ -1905,26 +2078,55 @@ 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;
+ std::unique_ptr<EncryptedEvent> encryptedEvent;
+
+ 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(!encrypted) {
+ qWarning(E2EE) << "Error encrypting message" << encrypted.error();
+ return {};
+ }
+ 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());
+ 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.get();
+#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()) {
@@ -1938,7 +2140,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) {
@@ -2073,13 +2275,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());
@@ -2315,6 +2520,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 { none };
+#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 };
@@ -2323,9 +2542,16 @@ 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()), none);
+ }
+
});
connect(job, &BaseJob::failure, this,
std::bind(&Private::failedTransfer, d, id, job->errorString()));
@@ -2393,7 +2619,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 6e6071f0..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);
+ 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