diff options
-rw-r--r-- | lib/connection.cpp | 5 | ||||
-rw-r--r-- | lib/encryptionmanager.cpp | 72 | ||||
-rw-r--r-- | lib/room.cpp | 195 |
3 files changed, 215 insertions, 57 deletions
diff --git a/lib/connection.cpp b/lib/connection.cpp index 98686ed0..4a220e0d 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -464,6 +464,7 @@ void Connection::Private::completeSetup(const QString& mxId) AccountSettings(data->userId()).setEncryptionAccountPickle(std::get<QByteArray>(pickle)); //TODO handle errors }); + encryptionManager = new EncryptionManager(q); if (accountSettings.encryptionAccountPickle().isEmpty()) { // create new account and save unpickle data @@ -1891,9 +1892,9 @@ void Connection::Private::saveDevicesList() QFile outFile { q->stateCacheDir().filePath("deviceslist.json") }; if (!outFile.open(QFile::WriteOnly)) { - qCWarning(MAIN) << "Error opening" << outFile.fileName() << ":" + qCWarning(E2EE) << "Error opening" << outFile.fileName() << ":" << outFile.errorString(); - qCWarning(MAIN) << "Caching the rooms state disabled"; + qCWarning(E2EE) << "Caching the rooms state disabled"; cacheState = false; return; } diff --git a/lib/encryptionmanager.cpp b/lib/encryptionmanager.cpp index 48e6701c..d36d5a7a 100644 --- a/lib/encryptionmanager.cpp +++ b/lib/encryptionmanager.cpp @@ -31,7 +31,6 @@ using std::move; class EncryptionManager::Private { public: explicit Private() - : q(nullptr) { } ~Private() = default; @@ -51,6 +50,73 @@ public: } } } + void loadSessions() { + QFile file { static_cast<Connection *>(q->parent())->stateCacheDir().filePath("olmsessions.json") }; + if(!file.exists() || !file.open(QIODevice::ReadOnly)) { + qCDebug(E2EE) << "No sessions cache exists."; + return; + } + auto data = file.readAll(); + const auto json = data.startsWith('{') + ? QJsonDocument::fromJson(data).object() +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + : QCborValue::fromCbor(data).toJsonValue().toObject() +#else + : QJsonDocument::fromBinaryData(data).object() +#endif + ; + if (json.isEmpty()) { + qCWarning(MAIN) << "Sessions cache is empty"; + return; + } + for(const auto &senderKey : json["sessions"].toObject().keys()) { + auto pickle = json["sessions"].toObject()[senderKey].toString(); + auto sessionResult = QOlmSession::unpickle(pickle.toLatin1(), Unencrypted{}); + if(std::holds_alternative<QOlmError>(sessionResult)) { + qCWarning(E2EE) << "Failed to unpickle olm session"; + continue; + } + sessions[senderKey] = std::move(std::get<std::unique_ptr<QOlmSession>>(sessionResult)); + } + } + void saveSessions() { + QFile outFile { static_cast<Connection *>(q->parent())->stateCacheDir().filePath("olmsessions.json") }; + if (!outFile.open(QFile::WriteOnly)) { + qCWarning(E2EE) << "Error opening" << outFile.fileName() << ":" + << outFile.errorString(); + qCWarning(E2EE) << "Failed to write olm sessions"; + return; + } + + QJsonObject rootObj { + { QStringLiteral("cache_version"), + QJsonObject { + { QStringLiteral("major"), 1 }, + { QStringLiteral("minor"), 0 } } } + }; + { + QJsonObject sessionsJson; + for (const auto &session : sessions) { + auto pickleResult = session.second->pickle(Unencrypted{}); + if(std::holds_alternative<QOlmError>(pickleResult)) { + qCWarning(E2EE) << "Failed to pickle session"; + continue; + } + sessionsJson[session.first] = QString(std::get<QByteArray>(pickleResult)); + } + rootObj.insert(QStringLiteral("sessions"), sessionsJson); + } + + #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + const auto data = QJsonDocument(rootObj).toJson(QJsonDocument::Compact); + #else + QJsonDocument json { rootObj }; + const auto data = json.toJson(QJsonDocument::Compact); + #endif + + outFile.write(data.data(), data.size()); + qCDebug(E2EE) << "Sessions saved to" << outFile.fileName(); + } QString sessionDecryptPrekey(const QOlmMessage& message, const QString &senderKey, std::unique_ptr<QOlmAccount>& olmAccount) { Q_ASSERT(message.type() == QOlmMessage::PreKey); @@ -60,6 +126,7 @@ public: qCDebug(E2EE) << "Found inbound session"; const auto result = session.second->decrypt(message); if(std::holds_alternative<QString>(result)) { + saveSessions(); return std::get<QString>(result); } else { qCDebug(E2EE) << "Failed to decrypt prekey message"; @@ -79,6 +146,7 @@ public: const auto result = newSession->decrypt(message); sessions[senderKey] = std::move(newSession); if(std::holds_alternative<QString>(result)) { + saveSessions(); return std::get<QString>(result); } else { qCDebug(E2EE) << "Failed to decrypt prekey message with new session"; @@ -91,6 +159,7 @@ public: for(auto& session : sessions) { const auto result = session.second->decrypt(message); if(std::holds_alternative<QString>(result)) { + saveSessions(); return std::get<QString>(result); } } @@ -104,6 +173,7 @@ EncryptionManager::EncryptionManager(QObject* parent) , d(std::make_unique<Private>()) { d->q = this; + d->loadSessions(); } EncryptionManager::~EncryptionManager() = default; diff --git a/lib/room.cpp b/lib/room.cpp index 57914db4..5fedd861 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -369,37 +369,95 @@ public: // A map from senderKey to a map of sessionId to InboundGroupSession // Not using QMultiHash, because we want to quickly return // a number of relations for a given event without enumerating them. - QHash<QPair<QString, QString>, QOlmInboundGroupSession*> groupSessions; // TODO: - // cache + std::map<QPair<QString, QString>, std::unique_ptr<QOlmInboundGroupSession>> groupSessions; + + void loadMegOlmSessions() { + QFile file { connection->stateCacheDir().filePath("megolmsessions.json") }; + if(!file.exists() || !file.open(QIODevice::ReadOnly)) { + qCDebug(E2EE) << "No megolm sessions cache exists."; + return; + } + auto data = file.readAll(); + const auto json = data.startsWith('{') + ? QJsonDocument::fromJson(data).object() +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + : QCborValue::fromCbor(data).toJsonValue().toObject() +#else + : QJsonDocument::fromBinaryData(data).object() +#endif + ; + if (json.isEmpty()) { + qCWarning(MAIN) << "Megolm sessions cache is empty"; + return; + } + for(const auto &s : json["sessions"].toArray()) { + auto pickle = s.toObject()["pickle"].toString().toLatin1(); + auto senderKey = s.toObject()["sender_key"].toString(); + auto sessionId = s.toObject()["session_id"].toString(); + auto sessionResult = QOlmInboundGroupSession::unpickle(pickle, Unencrypted{}); + if(std::holds_alternative<QOlmError>(sessionResult)) { + qCWarning(E2EE) << "Failed to unpickle olm session"; + continue; + } + groupSessions[qMakePair(senderKey, sessionId)] = std::move(std::get<std::unique_ptr<QOlmInboundGroupSession>>(sessionResult)); + } + } + void saveMegOlmSessions() { + QFile outFile { connection->stateCacheDir().filePath("megolmsessions.json") }; + if (!outFile.open(QFile::WriteOnly)) { + qCWarning(E2EE) << "Error opening" << outFile.fileName() << ":" + << outFile.errorString(); + qCWarning(E2EE) << "Failed to write megolm sessions"; + return; + } + + QJsonObject rootObj { + { QStringLiteral("cache_version"), + QJsonObject { + { QStringLiteral("major"), 1 }, + { QStringLiteral("minor"), 0 } } } + }; + { + QJsonArray sessionsJson; + for (const auto &session : groupSessions) { + auto pickleResult = session.second->pickle(Unencrypted{}); + sessionsJson += QJsonObject { + {QStringLiteral("sender_key"), session.first.first}, + {QStringLiteral("session_id"), session.first.second}, + {QStringLiteral("pickle"), QString(pickleResult)} + }; + } + rootObj.insert(QStringLiteral("sessions"), sessionsJson); + } + +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + const auto data = QJsonDocument(rootObj).toJson(QJsonDocument::Compact); +#else + QJsonDocument json { rootObj }; + const auto data = json.toJson(QJsonDocument::Compact); +#endif + + outFile.write(data.data(), data.size()); + qCDebug(E2EE) << "Megolm sessions saved to" << outFile.fileName(); + } bool addInboundGroupSession(QString senderKey, QString sessionId, QString sessionKey) { - // new e2ee TODO: - /* - if (groupSessions.contains({ senderKey, sessionId })) { - qCDebug(E2EE) << "Inbound Megolm session" << sessionId + if (groupSessions.find(qMakePair(senderKey, sessionId)) != groupSessions.end()) { + qCWarning(E2EE) << "Inbound Megolm session" << sessionId << "with senderKey" << senderKey << "already exists"; return false; } - QOlmInboundGroupSession* megolmSession; - try { - megolmSession = new QOlmInboundGroupSession(sessionKey.toLatin1(), - InboundGroupSession::Init, - q); - } catch (QOlmError* e) { - qCDebug(E2EE) << "Unable to create new InboundGroupSession" - << e->what(); + std::unique_ptr<QOlmInboundGroupSession> megolmSession = QOlmInboundGroupSession::create(sessionKey.toLatin1()); + if (megolmSession->sessionId() != sessionId) { + qCWarning(E2EE) << "Session ID mismatch in m.room_key event sent " + "from sender with key" << senderKey; return false; } - if (megolmSession->id() != sessionId) { - qCDebug(E2EE) << "Session ID mismatch in m.room_key event sent " - "from sender with key" - << senderKey; - return false; - } - groupSessions.insert({ senderKey, sessionId }, megolmSession); - */ + qCWarning(E2EE) << "Adding inbound session"; + groupSessions[qMakePair(senderKey, sessionId)] = std::move(megolmSession); + saveMegOlmSessions(); return true; } @@ -409,46 +467,33 @@ public: const QString& eventId, QDateTime timestamp) { - std::pair<QString, uint32_t> decrypted; - // new e2ee TODO: - /* QPair<QString, QString> senderSessionPairKey = qMakePair(senderKey, sessionId); - if (!groupSessions.contains(senderSessionPairKey)) { - qCDebug(E2EE) << "Unable to decrypt event" << eventId + if (groupSessions.find(senderSessionPairKey) == groupSessions.end()) { + qCWarning(E2EE) << "Unable to decrypt event" << eventId << "The sender's device has not sent us the keys for " "this message"; return QString(); } - QOlmInboundGroupSession* senderSession = - groupSessions.value(senderSessionPairKey); - if (!senderSession) { - qCDebug(E2EE) << "Unable to decrypt event" << eventId - << "senderSessionPairKey:" << senderSessionPairKey; + auto& senderSession = groupSessions[senderSessionPairKey]; + auto decryptResult = senderSession->decrypt(cipher); + if(std::holds_alternative<QOlmError>(decryptResult)) { + qCWarning(E2EE) << "Unable to decrypt event" << eventId + << "with matching megolm session:" << std::get<QOlmError>(decryptResult); return QString(); } - try { - decrypted = senderSession->decrypt(cipher); - } catch (QOlmError* e) { - qCDebug(E2EE) << "Unable to decrypt event" << eventId - << "with matching megolm session:" << e->what(); - return QString(); - } - QPair<QString, QDateTime> properties = groupSessionIndexRecord.value( - qMakePair(senderSession->id(), decrypted.second)); + std::pair<QString, uint32_t> decrypted = std::get<std::pair<QString, uint32_t>>(decryptResult); + QPair<QString, QDateTime> properties = groupSessionIndexRecord.value(qMakePair(senderSession->sessionId(), decrypted.second)); if (properties.first.isEmpty()) { - groupSessionIndexRecord.insert(qMakePair(senderSession->id(), - decrypted.second), - qMakePair(eventId, timestamp)); + groupSessionIndexRecord.insert(qMakePair(senderSession->sessionId(), decrypted.second), qMakePair(eventId, timestamp)); } else { - if ((properties.first != eventId) - || (properties.second != timestamp)) { - qCDebug(E2EE) << "Detected a replay attack on event" << eventId; + if ((properties.first != eventId) || (properties.second != timestamp)) { + qCWarning(E2EE) << "Detected a replay attack on event" << eventId; return QString(); } } - */ - + //TODO is this necessary? + saveMegOlmSessions(); return decrypted.first; } #endif // Quotient_E2EE_ENABLED @@ -475,6 +520,7 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState) emit baseStateLoaded(); return this == r; // loadedRoomState fires only once per room }); + qCDebug(STATE) << "New" << initialJoinState << "Room:" << id; #ifdef Quotient_E2EE_ENABLED connectSingleShot(this, &Room::encryption, this, [=](){ connection->encryptionUpdate(this); @@ -484,6 +530,7 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState) connection->encryptionUpdate(this); } }); + d->loadMegOlmSessions(); #endif qCDebug(STATE) << "New" << terse << initialJoinState << "Room:" << id; } @@ -1504,13 +1551,29 @@ RoomEventPtr Room::decryptMessage(const EncryptedEvent& encryptedEvent) encryptedEvent.sessionId(), encryptedEvent.id(), encryptedEvent.originTimestamp()); if (decrypted.isEmpty()) { + qCWarning(E2EE) << "Encrypted message is empty"; return {}; } - return makeEvent<RoomMessageEvent>( - QJsonDocument::fromJson(decrypted.toUtf8()).object()); + QJsonObject eventObject = QJsonDocument::fromJson(decrypted.toUtf8()).object(); + eventObject["event_id"] = encryptedEvent.id(); + eventObject["sender"] = encryptedEvent.senderId(); + eventObject["origin_server_ts"] = encryptedEvent.originTimestamp().toMSecsSinceEpoch(); + if(encryptedEvent.contentJson().contains("m.relates_to")) { + auto relates = encryptedEvent.contentJson()["m.relates_to"].toObject(); + auto content = eventObject["content"].toObject(); + content["m.relates_to"] = relates; + eventObject["content"] = content; + } + if(encryptedEvent.unsignedJson().contains("redacts")) { + auto redacts = encryptedEvent.unsignedJson()["redacts"].toString(); + auto unsign = eventObject["unsigned"].toObject(); + unsign["redacts"] = redacts; + eventObject["unsigned"] = unsign; + } + return makeEvent<RoomMessageEvent>(eventObject); } qCDebug(E2EE) << "Algorithm of the encrypted event with id" - << encryptedEvent.id() << "is not for the current device"; + << encryptedEvent.id() << "is not decryptable by the current device"; return {}; #endif // Quotient_E2EE_ENABLED } @@ -1529,8 +1592,8 @@ void Room::handleRoomKeyEvent(const RoomKeyEvent& roomKeyEvent, } if (d->addInboundGroupSession(senderKey, roomKeyEvent.sessionId(), roomKeyEvent.sessionKey())) { - qCDebug(E2EE) << "added new inboundGroupSession:" - << d->groupSessions.count(); + qCWarning(E2EE) << "added new inboundGroupSession:" + << d->groupSessions.size(); } #endif // Quotient_E2EE_ENABLED } @@ -2590,6 +2653,18 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) QElapsedTimer et; et.start(); + + //TODO should this be done before dropDuplicateEvents? + for(long unsigned int i = 0; i < events.size(); i++) { + if(auto* encrypted = eventCast<EncryptedEvent>(events[i])) { + qDebug() << "Encrypted Event"; + auto decrypted = q->decryptMessage(*encrypted); + if(decrypted) { + events[i] = std::move(decrypted); + } + } + } + { // Pre-process redactions and edits so that events that get // redacted/replaced in the same batch landed in the timeline already @@ -2742,6 +2817,18 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) return; Changes changes {}; + + //TODO should this be done before dropDuplicateEvents? + for(long unsigned int i = 0; i < events.size(); i++) { + if(auto* encrypted = eventCast<EncryptedEvent>(events[i])) { + qDebug() << "Encrypted Event"; + auto decrypted = q->decryptMessage(*encrypted); + if(decrypted) { + events[i] = std::move(decrypted); + } + } + } + // In case of lazy-loading new members may be loaded with historical // messages. Also, the cache doesn't store events with empty content; // so when such events show up in the timeline they should be properly |