diff options
author | Alexey Rusakov <Kitsune-Ral@users.sf.net> | 2022-05-19 16:14:14 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-05-19 16:14:14 +0200 |
commit | 0f2c40957b438d0a2f5ce7cb2ba033bab077cbeb (patch) | |
tree | f6d147201d13885819d2c34462cf13f499fac944 /lib | |
parent | 77b190d822c1e980b98b84999f0cfb609ed05a49 (diff) | |
parent | 5df53b8d5c8b21228ecf9938330dd4d85d3de6af (diff) | |
download | libquotient-0f2c40957b438d0a2f5ce7cb2ba033bab077cbeb.tar.gz libquotient-0f2c40957b438d0a2f5ce7cb2ba033bab077cbeb.zip |
Merge pull request #540 from TobiasFella/sendmessages
Implement sending encrypted messages
Diffstat (limited to 'lib')
-rw-r--r-- | lib/connection.cpp | 47 | ||||
-rw-r--r-- | lib/connection.h | 15 | ||||
-rw-r--r-- | lib/database.cpp | 99 | ||||
-rw-r--r-- | lib/database.h | 17 | ||||
-rw-r--r-- | lib/e2ee/qolmoutboundsession.cpp | 24 | ||||
-rw-r--r-- | lib/e2ee/qolmoutboundsession.h | 11 | ||||
-rw-r--r-- | lib/eventitem.cpp | 10 | ||||
-rw-r--r-- | lib/eventitem.h | 3 | ||||
-rw-r--r-- | lib/events/encryptedevent.cpp | 7 | ||||
-rw-r--r-- | lib/events/encryptedevent.h | 2 | ||||
-rw-r--r-- | lib/events/encryptedfile.cpp | 30 | ||||
-rw-r--r-- | lib/events/encryptedfile.h | 1 | ||||
-rw-r--r-- | lib/events/roomkeyevent.cpp | 13 | ||||
-rw-r--r-- | lib/events/roomkeyevent.h | 1 | ||||
-rw-r--r-- | lib/room.cpp | 256 | ||||
-rw-r--r-- | lib/room.h | 2 |
16 files changed, 509 insertions, 29 deletions
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 ¬ification); - QStringList devicesForUser(User* user) const; + QStringList devicesForUser(const QString& user) const; QString curveKeyForUserDevice(const QString &user, const QString& device) const; QString edKeyForUserDevice(const QString& user, const QString& device) const; bool isKnownCurveKey(const QString& user, const QString& curveKey); diff --git a/lib/database.cpp b/lib/database.cpp index 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, @@ -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 |