From 5477ea0fe1654c8236980e375d10983f71e17aef Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sat, 25 May 2024 22:24:34 +0200 Subject: [PATCH 01/20] Implement key export --- Quotient/database.cpp | 16 ++++++++++++ Quotient/database.h | 3 +++ Quotient/keyimport.cpp | 58 +++++++++++++++++++++++++++++++++++++++++- Quotient/keyimport.h | 1 + Quotient/room.cpp | 32 +++++++++++++++++++++++ Quotient/room.h | 2 ++ 6 files changed, 111 insertions(+), 1 deletion(-) diff --git a/Quotient/database.cpp b/Quotient/database.cpp index 82faebc8a..f74c537d6 100644 --- a/Quotient/database.cpp +++ b/Quotient/database.cpp @@ -621,3 +621,19 @@ QString Database::selfSigningPublicKey() execute(query); return query.next() ? query.value("key"_ls).toString() : QString(); } + +QString Database::edKeyForMegolmSession(const QString& sessionId) +{ + auto query = prepareQuery(QStringLiteral("SELECT senderClaimedEd25519Key FROM inbound_megolm_sessions WHERE sessionId=:sessionId;")); + query.bindValue(":sessionId"_ls, sessionId); + execute(query); + return query.next() ? query.value("senderClaimedEd25519Key"_ls).toString() : QString(); +} + +QString Database::senderKeyForMegolmSession(const QString& sessionId) +{ + auto query = prepareQuery(QStringLiteral("SELECT senderKey FROM inbound_megolm_sessions WHERE sessionId=:sessionId;")); + query.bindValue(":sessionId"_ls, sessionId); + execute(query); + return query.next() ? query.value("senderKey"_ls).toString() : QString(); +} diff --git a/Quotient/database.h b/Quotient/database.h index 69ef807e8..96a0e257f 100644 --- a/Quotient/database.h +++ b/Quotient/database.h @@ -79,6 +79,9 @@ class QUOTIENT_API Database QString userSigningPublicKey(); QString selfSigningPublicKey(); + QString edKeyForMegolmSession(const QString& sessionId); + QString senderKeyForMegolmSession(const QString& sessionId); + private: void migrateTo1(); void migrateTo2(); diff --git a/Quotient/keyimport.cpp b/Quotient/keyimport.cpp index fe88e6298..b6b1600f8 100644 --- a/Quotient/keyimport.cpp +++ b/Quotient/keyimport.cpp @@ -87,8 +87,64 @@ KeyImport::Error KeyImport::importKeys(QString data, const QString& passphrase, room->addMegolmSessionFromBackup( keyObject["session_id"_ls].toString().toLatin1(), keyObject["session_key"_ls].toString().toLatin1(), 0, - keyObject[SenderKeyKey].toVariant().toByteArray()); + keyObject[SenderKeyKey].toVariant().toByteArray(), + keyObject["sender_claimed_keys"_ls]["ed25519"_ls].toString().toLatin1() + ); } return Success; } +Quotient::Expected KeyImport::exportKeys(const QString& passphrase, Connection* connection) +{ + QJsonArray sessions; + for (const auto& room : connection->allRooms()) { + for (const auto &session : room->exportMegolmSessions()) { + sessions += session; + } + } + auto plainText = QJsonDocument(sessions).toJson(QJsonDocument::Compact); + + auto salt = getRandom(AesBlockSize); + auto iv = getRandom(AesBlockSize); + quint32 rounds = 200000; // spec: "N should be at least 100,000"; + + auto keys = pbkdf2HmacSha512<64>(passphrase.toLatin1(), salt.viewAsByteArray(), rounds); + if (!keys.has_value()) { + qCWarning(E2EE) << "Failed to calculate pbkdf:" << keys.error(); + return OtherError; + } + + auto result = aesCtr256Encrypt(plainText, byte_view_t(keys.value().begin(), Aes256KeySize), asCBytes(iv.viewAsByteArray())); + + if (!result.has_value()) { + qCWarning(E2EE) << "Failed to encrypt export" << result.error(); + return OtherError; + } + + QByteArray data; + data.append("\x01"); + data.append(salt.viewAsByteArray()); + data.append(iv.viewAsByteArray()); + QByteArray roundsData(4, u'\x0'); + qToBigEndian(rounds, roundsData.data()); + data.append(roundsData); + data.append(result.value()); + auto mac = hmacSha256(key_view_t(keys.value().begin() + 32, 32), data); + if (!mac.has_value()) { + qCWarning(E2EE) << "Failed to calculate MAC" << mac.error(); + return OtherError; + } + data.append(mac.value()); + + QByteArray base64 = data.toBase64(); + + // Limit base64 line length. 96 is chosen arbitrarily. + for (auto i = 96; i < base64.length(); i += 96) { + base64.insert(i, "\n"); + i++; + } + base64.prepend("-----BEGIN MEGOLM SESSION DATA-----\n"_ls); + base64.append("\n-----END MEGOLM SESSION DATA-----\n"_ls); + return base64; +} + diff --git a/Quotient/keyimport.h b/Quotient/keyimport.h index e74f78520..91f6e59cb 100644 --- a/Quotient/keyimport.h +++ b/Quotient/keyimport.h @@ -35,6 +35,7 @@ class QUOTIENT_API KeyImport : public QObject Q_INVOKABLE Error importKeys(QString data, const QString& passphrase, const Quotient::Connection* connection); + Q_INVOKABLE Quotient::Expected exportKeys(const QString& passphrase, Quotient::Connection* connection); friend class ::TestKeyImport; private: diff --git a/Quotient/room.cpp b/Quotient/room.cpp index 0f79d8371..5431a5cf0 100644 --- a/Quotient/room.cpp +++ b/Quotient/room.cpp @@ -3444,3 +3444,35 @@ void Room::startVerification() d->pendingKeyVerificationSession = new KeyVerificationSession(this); emit d->connection->newKeyVerificationSession(d->pendingKeyVerificationSession); } + +QJsonArray Room::exportMegolmSessions() +{ + QJsonArray sessions; + for (auto &[key, value] : d->groupSessions) { + auto session = value.exportSession(value.firstKnownIndex()); + if (!session.has_value()) { + qWarning() << "Failed to export session" << session.error(); + continue; + } + + const auto json = QJsonObject { + {"algorithm"_ls, "m.megolm.v1.aes-sha2"_ls}, + {"forwarding_curve25519_key_chain"_ls, QJsonArray()}, + {"room_id"_ls, id()}, + {"sender_claimed_keys"_ls, QJsonObject{ + {"ed25519"_ls, connection()->database()->edKeyForMegolmSession(QString::fromLatin1(value.sessionId()))}, + }}, + {"sender_key"_ls, connection()->database()->senderKeyForMegolmSession(QString::fromLatin1(value.sessionId()))}, + {"session_id"_ls, QString::fromLatin1(value.sessionId())}, + {"session_key"_ls, QString::fromLatin1(session.value())}, + }; + if (json["sender_claimed_keys"_ls]["ed25519"_ls].toString().isEmpty() || json["sender_key"_ls].toString().isEmpty()) { + // These are edge-cases for some sessions that were added before libquotient started storing these fields. + // Some clients refuse to the entire file if this is missing for one key, so we shouldn't export the session in this case. + continue; + } + sessions.append(json); + } + return sessions; +} + diff --git a/Quotient/room.h b/Quotient/room.h index 47e7179f0..2ffb5705d 100644 --- a/Quotient/room.h +++ b/Quotient/room.h @@ -683,6 +683,8 @@ class QUOTIENT_API Room : public QObject { Q_INVOKABLE void startVerification(); + QJsonArray exportMegolmSessions(); + public Q_SLOTS: /** Check whether the room should be upgraded */ void checkVersion(); From 02948e97fbefcc08088761bda864f2a26eb9c89e Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Thu, 16 May 2024 18:31:45 +0200 Subject: [PATCH 02/20] Also store ed key for incoming sessions I don't know why it would be needed, except for backup / export... --- Quotient/database.cpp | 38 +++++++++++++++++++++++++++++++++++++- Quotient/database.h | 1 + 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/Quotient/database.cpp b/Quotient/database.cpp index f74c537d6..303df57be 100644 --- a/Quotient/database.cpp +++ b/Quotient/database.cpp @@ -46,7 +46,8 @@ Database::Database(const QString& userId, const QString& deviceId, case 5: migrateTo6(); [[fallthrough]]; case 6: migrateTo7(); [[fallthrough]]; case 7: migrateTo8(); [[fallthrough]]; - case 8: migrateTo9(); + case 8: migrateTo9(); [[fallthrough]]; + case 9: migrateTo10(); } } @@ -238,6 +239,41 @@ void Database::migrateTo9() commit(); } +void Database::migrateTo10() +{ + qCDebug(DATABASE) << "Migrating database to version 10"; + + transaction(); + + execute(QStringLiteral("ALTER TABLE inbound_megolm_sessions ADD senderClaimedEd25519Key TEXT;")); + + auto query = prepareQuery("SELECT senderKey FROM inbound_megolm_sessions;"_ls); + execute(query); + + QSet keys; + while (query.next()) { + keys += query.value("senderKey"_ls).toString(); + } + for (const auto& key : keys) { + auto query = prepareQuery("SELECT edKey FROM tracked_devices WHERE curveKey=:curveKey;"_ls); + query.bindValue(":curveKey"_ls, key); + execute(query); + if (!query.next()) { + continue; + } + const auto &edKey = query.value("edKey"_ls).toByteArray(); + + auto updateQuery = prepareQuery("UPDATE inbound_megolm_sessions SET senderClaimedEd25519Key=:senderClaimedEd25519Key WHERE senderKey=:senderKey;"_ls); + updateQuery.bindValue(":senderKey"_ls, key.toLatin1()); + updateQuery.bindValue(":senderClaimedEd25519Key"_ls, edKey); + execute(updateQuery); + } + + execute(QStringLiteral("pragma user_version = 10")); + commit(); + +} + void Database::storeOlmAccount(const QOlmAccount& olmAccount) { auto deleteQuery = prepareQuery(QStringLiteral("DELETE FROM accounts;")); diff --git a/Quotient/database.h b/Quotient/database.h index 96a0e257f..69af47392 100644 --- a/Quotient/database.h +++ b/Quotient/database.h @@ -92,6 +92,7 @@ class QUOTIENT_API Database void migrateTo7(); void migrateTo8(); void migrateTo9(); + void migrateTo10(); QString m_userId; QString m_deviceId; From 0ba78a49538f6afb568c0407e678e7cdbd0d283a Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Thu, 16 May 2024 19:09:03 +0200 Subject: [PATCH 03/20] Store ed key for incoming megolm sessions --- Quotient/connection.cpp | 4 ++-- Quotient/connection.h | 2 +- Quotient/connectionencryptiondata_p.cpp | 4 ++-- Quotient/database.cpp | 5 +++-- Quotient/database.h | 3 ++- Quotient/e2ee/sssshandler.cpp | 2 +- Quotient/room.cpp | 17 ++++++++++------- Quotient/room.h | 5 +++-- 8 files changed, 24 insertions(+), 18 deletions(-) diff --git a/Quotient/connection.cpp b/Quotient/connection.cpp index 0419914f0..1474ddc38 100644 --- a/Quotient/connection.cpp +++ b/Quotient/connection.cpp @@ -1743,9 +1743,9 @@ std::unordered_map Connection::loadRoomMego } void Connection::saveMegolmSession(const Room* room, - const QOlmInboundGroupSession& session, const QByteArray& senderKey) const + const QOlmInboundGroupSession& session, const QByteArray& senderKey, const QByteArray& senderEdKey) const { - database()->saveMegolmSession(room->id(), session, senderKey); + database()->saveMegolmSession(room->id(), session, senderKey, senderEdKey); } QStringList Connection::devicesForUser(const QString& userId) const diff --git a/Quotient/connection.h b/Quotient/connection.h index a2585d58d..f38c165f7 100644 --- a/Quotient/connection.h +++ b/Quotient/connection.h @@ -333,7 +333,7 @@ class QUOTIENT_API Connection : public QObject { std::unordered_map loadRoomMegolmSessions( const Room* room) const; void saveMegolmSession(const Room* room, - const QOlmInboundGroupSession& session, const QByteArray &senderKey) const; + const QOlmInboundGroupSession& session, const QByteArray &senderKey, const QByteArray& senderEdKey) const; QString edKeyForUserDevice(const QString& userId, const QString& deviceId) const; diff --git a/Quotient/connectionencryptiondata_p.cpp b/Quotient/connectionencryptiondata_p.cpp index 7b4421c8e..fcff09692 100644 --- a/Quotient/connectionencryptiondata_p.cpp +++ b/Quotient/connectionencryptiondata_p.cpp @@ -381,8 +381,8 @@ void ConnectionEncryptionData::handleEncryptedToDeviceEvent(const EncryptedEvent decryptedEvent->switchOnType( [this, &event, olmSessionId](const RoomKeyEvent& roomKeyEvent) { if (auto* detectedRoom = q->room(roomKeyEvent.roomId())) { - detectedRoom->handleRoomKeyEvent(roomKeyEvent, event.senderId(), olmSessionId, - event.senderKey().toLatin1()); + detectedRoom->handleRoomKeyEvent(roomKeyEvent, event.senderId(), + olmSessionId, event.senderKey().toLatin1(), q->edKeyForUserDevice(event.senderId(), event.deviceId()).toLatin1()); } else { qCDebug(E2EE) << "Encrypted event room id" << roomKeyEvent.roomId() diff --git a/Quotient/database.cpp b/Quotient/database.cpp index 303df57be..9ffbae3c8 100644 --- a/Quotient/database.cpp +++ b/Quotient/database.cpp @@ -385,19 +385,20 @@ std::unordered_map Database::loadMegolmSess } void Database::saveMegolmSession(const QString& roomId, - const QOlmInboundGroupSession& session, const QByteArray &senderKey) + const QOlmInboundGroupSession& session, const QByteArray &senderKey, const QByteArray& senderClaimedEdKey) { auto deleteQuery = prepareQuery(QStringLiteral("DELETE FROM inbound_megolm_sessions WHERE roomId=:roomId AND sessionId=:sessionId;")); deleteQuery.bindValue(":roomId"_ls, roomId); deleteQuery.bindValue(":sessionId"_ls, session.sessionId()); auto query = prepareQuery( - QStringLiteral("INSERT INTO inbound_megolm_sessions(roomId, sessionId, pickle, senderId, olmSessionId, senderKey) VALUES(:roomId, :sessionId, :pickle, :senderId, :olmSessionId, :senderKey);")); + QStringLiteral("INSERT INTO inbound_megolm_sessions(roomId, sessionId, pickle, senderId, olmSessionId, senderKey, sender_) VALUES(:roomId, :sessionId, :pickle, :senderId, :olmSessionId, :senderKey, :senderClaimedEd25519Key);")); query.bindValue(":roomId"_ls, roomId); query.bindValue(":sessionId"_ls, session.sessionId()); query.bindValue(":pickle"_ls, session.pickle(m_picklingKey)); query.bindValue(":senderId"_ls, session.senderId()); query.bindValue(":olmSessionId"_ls, session.olmSessionId()); query.bindValue(":senderKey"_ls, senderKey); + query.bindValue(":senderClaimedEd25519Key"_ls, senderClaimedEdKey); transaction(); execute(deleteQuery); execute(query); diff --git a/Quotient/database.h b/Quotient/database.h index 69af47392..4f23289ab 100644 --- a/Quotient/database.h +++ b/Quotient/database.h @@ -42,7 +42,8 @@ class QUOTIENT_API Database const QString& roomId); void saveMegolmSession(const QString& roomId, const QOlmInboundGroupSession& session, - const QByteArray& senderKey); + const QByteArray& senderKey, + const QByteArray& senderClaimedEdKey); void addGroupSessionIndexRecord(const QString& roomId, const QString& sessionId, uint32_t index, const QString& eventId, qint64 ts); diff --git a/Quotient/e2ee/sssshandler.cpp b/Quotient/e2ee/sssshandler.cpp index 7ab5fd378..75f603e59 100644 --- a/Quotient/e2ee/sssshandler.cpp +++ b/Quotient/e2ee/sssshandler.cpp @@ -214,7 +214,7 @@ void SSSSHandler::loadMegolmBackup(const QByteArray& megolmDecryptionKey) const auto data = QJsonDocument::fromJson(result.value()).object(); m_connection->room(roomId)->addMegolmSessionFromBackup( sessionId.toLatin1(), data["session_key"_ls].toString().toLatin1(), - static_cast(backupData.firstMessageIndex), data["sender_key"_ls].toVariant().toByteArray()); + static_cast(backupData.firstMessageIndex), data["sender_key"_ls].toVariant().toByteArray(), data["sender_claimed_keys"_ls]["ed25519"_ls].toVariant().toByteArray()); } } emit finished(); diff --git a/Quotient/room.cpp b/Quotient/room.cpp index 5431a5cf0..870af3a80 100644 --- a/Quotient/room.cpp +++ b/Quotient/room.cpp @@ -340,7 +340,7 @@ class Q_DECL_HIDDEN Room::Private { bool addInboundGroupSession(QByteArray sessionId, QByteArray sessionKey, const QString& senderId, - const QByteArray& olmSessionId, const QByteArray& senderKey) + const QByteArray& olmSessionId, const QByteArray& senderKey, const QByteArray& senderEdKey) { if (groupSessions.contains(sessionId)) { qCWarning(E2EE) << "Inbound Megolm session" << sessionId << "already exists"; @@ -357,7 +357,7 @@ class Q_DECL_HIDDEN Room::Private { megolmSession.setSenderId(senderId); megolmSession.setOlmSessionId(olmSessionId); qCWarning(E2EE) << "Adding inbound session" << sessionId; - connection->saveMegolmSession(q, megolmSession, senderKey); + connection->saveMegolmSession(q, megolmSession, senderKey, senderEdKey); groupSessions.try_emplace(sessionId, std::move(megolmSession)); return true; } @@ -434,7 +434,9 @@ class Q_DECL_HIDDEN Room::Private { addInboundGroupSession(currentOutboundMegolmSession->sessionId(), currentOutboundMegolmSession->sessionKey(), - q->localMember().id(), QByteArrayLiteral("SELF"), connection->curveKeyForUserDevice(connection->userId(), connection->deviceId()).toLatin1()); + q->localMember().id(), QByteArrayLiteral("SELF"), + connection->curveKeyForUserDevice(connection->userId(), connection->deviceId()).toLatin1(), + connection->edKeyForUserDevice(connection->userId(), connection->deviceId()).toLatin1()); } QMultiHash getDevicesWithoutKey() const @@ -1551,7 +1553,8 @@ RoomEventPtr Room::decryptMessage(const EncryptedEvent& encryptedEvent) void Room::handleRoomKeyEvent(const RoomKeyEvent& roomKeyEvent, const QString& senderId, const QByteArray& olmSessionId, - const QByteArray& senderKey) + const QByteArray& senderKey, + const QByteArray& senderEdKey) { if (roomKeyEvent.algorithm() != MegolmV1AesSha2AlgoKey) { qCWarning(E2EE) << "Ignoring unsupported algorithm" @@ -1559,7 +1562,7 @@ void Room::handleRoomKeyEvent(const RoomKeyEvent& roomKeyEvent, } if (d->addInboundGroupSession(roomKeyEvent.sessionId().toLatin1(), roomKeyEvent.sessionKey(), senderId, - olmSessionId, senderKey)) { + olmSessionId, senderKey, senderEdKey)) { qCWarning(E2EE) << "added new inboundGroupSession:" << d->groupSessions.size(); const auto undecryptedEvents = @@ -3415,7 +3418,7 @@ void Room::activateEncryption() setState(EncryptionType::MegolmV1AesSha2); } -void Room::addMegolmSessionFromBackup(const QByteArray& sessionId, const QByteArray& sessionKey, uint32_t index, const QByteArray& senderKey) +void Room::addMegolmSessionFromBackup(const QByteArray& sessionId, const QByteArray& sessionKey, uint32_t index, const QByteArray& senderKey, const QByteArray& senderEdKey) { const auto sessionIt = d->groupSessions.find(sessionId); if (sessionIt != d->groupSessions.end() && sessionIt->second.firstKnownIndex() <= index) @@ -3433,7 +3436,7 @@ void Room::addMegolmSessionFromBackup(const QByteArray& sessionId, const QByteAr ? QByteArrayLiteral("BACKUP_VERIFIED") : QByteArrayLiteral("BACKUP")); session.setSenderId("BACKUP"_ls); - d->connection->saveMegolmSession(this, session, senderKey); + d->connection->saveMegolmSession(this, session, senderKey, senderEdKey); } void Room::startVerification() diff --git a/Quotient/room.h b/Quotient/room.h index 2ffb5705d..c5e0a3ccf 100644 --- a/Quotient/room.h +++ b/Quotient/room.h @@ -265,7 +265,8 @@ class QUOTIENT_API Room : public QObject { void handleRoomKeyEvent(const RoomKeyEvent& roomKeyEvent, const QString& senderId, const QByteArray& olmSessionId, - const QByteArray& senderKey); + const QByteArray& senderKey, + const QByteArray& senderEdKey); int joinedCount() const; int invitedCount() const; int totalMemberCount() const; @@ -679,7 +680,7 @@ class QUOTIENT_API Room : public QObject { return setState(EvT(std::forward(args)...)); } - void addMegolmSessionFromBackup(const QByteArray &sessionId, const QByteArray &sessionKey, uint32_t index, const QByteArray& senderKey); + void addMegolmSessionFromBackup(const QByteArray &sessionId, const QByteArray &sessionKey, uint32_t index, const QByteArray& senderKey, const QByteArray& senderEdKey); Q_INVOKABLE void startVerification(); From 27780d31396827f429c432574ede603f327d528a Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sat, 25 May 2024 23:36:29 +0200 Subject: [PATCH 04/20] fixup! Implement key export --- Quotient/room.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Quotient/room.cpp b/Quotient/room.cpp index 870af3a80..87229c518 100644 --- a/Quotient/room.cpp +++ b/Quotient/room.cpp @@ -3454,7 +3454,7 @@ QJsonArray Room::exportMegolmSessions() for (auto &[key, value] : d->groupSessions) { auto session = value.exportSession(value.firstKnownIndex()); if (!session.has_value()) { - qWarning() << "Failed to export session" << session.error(); + qCWarning(E2EE) << "Failed to export session" << session.error(); continue; } @@ -3472,6 +3472,7 @@ QJsonArray Room::exportMegolmSessions() if (json["sender_claimed_keys"_ls]["ed25519"_ls].toString().isEmpty() || json["sender_key"_ls].toString().isEmpty()) { // These are edge-cases for some sessions that were added before libquotient started storing these fields. // Some clients refuse to the entire file if this is missing for one key, so we shouldn't export the session in this case. + qCWarning(E2EE) << "Session" << value.sessionId() << "has unknown sender key."; continue; } sessions.append(json); From 5d6289b89de37ebf34417423af75eeb05bae83c9 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Wed, 19 Jun 2024 22:54:21 +0200 Subject: [PATCH 05/20] Add test and fix --- Quotient/database.cpp | 6 +++--- Quotient/keyimport.cpp | 21 ++++++++++++++------- Quotient/keyimport.h | 1 + autotests/testkeyimport.cpp | 29 +++++++++++++++++++++++++++++ 4 files changed, 47 insertions(+), 10 deletions(-) diff --git a/Quotient/database.cpp b/Quotient/database.cpp index 9ffbae3c8..b2e181fc2 100644 --- a/Quotient/database.cpp +++ b/Quotient/database.cpp @@ -391,7 +391,7 @@ void Database::saveMegolmSession(const QString& roomId, deleteQuery.bindValue(":roomId"_ls, roomId); deleteQuery.bindValue(":sessionId"_ls, session.sessionId()); auto query = prepareQuery( - QStringLiteral("INSERT INTO inbound_megolm_sessions(roomId, sessionId, pickle, senderId, olmSessionId, senderKey, sender_) VALUES(:roomId, :sessionId, :pickle, :senderId, :olmSessionId, :senderKey, :senderClaimedEd25519Key);")); + QStringLiteral("INSERT INTO inbound_megolm_sessions(roomId, sessionId, pickle, senderId, olmSessionId, senderKey, senderClaimedEd25519Key) VALUES(:roomId, :sessionId, :pickle, :senderId, :olmSessionId, :senderKey, :senderClaimedEd25519Key);")); query.bindValue(":roomId"_ls, roomId); query.bindValue(":sessionId"_ls, session.sessionId()); query.bindValue(":pickle"_ls, session.pickle(m_picklingKey)); @@ -662,7 +662,7 @@ QString Database::selfSigningPublicKey() QString Database::edKeyForMegolmSession(const QString& sessionId) { auto query = prepareQuery(QStringLiteral("SELECT senderClaimedEd25519Key FROM inbound_megolm_sessions WHERE sessionId=:sessionId;")); - query.bindValue(":sessionId"_ls, sessionId); + query.bindValue(":sessionId"_ls, sessionId.toLatin1()); execute(query); return query.next() ? query.value("senderClaimedEd25519Key"_ls).toString() : QString(); } @@ -670,7 +670,7 @@ QString Database::edKeyForMegolmSession(const QString& sessionId) QString Database::senderKeyForMegolmSession(const QString& sessionId) { auto query = prepareQuery(QStringLiteral("SELECT senderKey FROM inbound_megolm_sessions WHERE sessionId=:sessionId;")); - query.bindValue(":sessionId"_ls, sessionId); + query.bindValue(":sessionId"_ls, sessionId.toLatin1()); execute(query); return query.next() ? query.value("senderKey"_ls).toString() : QString(); } diff --git a/Quotient/keyimport.cpp b/Quotient/keyimport.cpp index b6b1600f8..6f8a75de2 100644 --- a/Quotient/keyimport.cpp +++ b/Quotient/keyimport.cpp @@ -94,14 +94,8 @@ KeyImport::Error KeyImport::importKeys(QString data, const QString& passphrase, return Success; } -Quotient::Expected KeyImport::exportKeys(const QString& passphrase, Connection* connection) +Quotient::Expected KeyImport::encrypt(QJsonArray sessions, const QString& passphrase) { - QJsonArray sessions; - for (const auto& room : connection->allRooms()) { - for (const auto &session : room->exportMegolmSessions()) { - sessions += session; - } - } auto plainText = QJsonDocument(sessions).toJson(QJsonDocument::Compact); auto salt = getRandom(AesBlockSize); @@ -148,3 +142,16 @@ Quotient::Expected KeyImport::exportKeys(const QSt return base64; } + +Quotient::Expected KeyImport::exportKeys(const QString& passphrase, Connection* connection) +{ + QJsonArray sessions; + for (const auto& room : connection->allRooms()) { + for (const auto &session : room->exportMegolmSessions()) { + return {}; + sessions += session; + } + } + return encrypt(sessions, passphrase); +} + diff --git a/Quotient/keyimport.h b/Quotient/keyimport.h index 91f6e59cb..3d305c1b6 100644 --- a/Quotient/keyimport.h +++ b/Quotient/keyimport.h @@ -40,6 +40,7 @@ class QUOTIENT_API KeyImport : public QObject friend class ::TestKeyImport; private: Quotient::Expected decrypt(QString data, const QString& passphrase); + Quotient::Expected encrypt(QJsonArray sessions, const QString& passphrase); }; } diff --git a/autotests/testkeyimport.cpp b/autotests/testkeyimport.cpp index 21823f5e8..f4bdae716 100644 --- a/autotests/testkeyimport.cpp +++ b/autotests/testkeyimport.cpp @@ -12,6 +12,7 @@ class TestKeyImport : public QObject Q_OBJECT private slots: void testImport(); + void testExport(); }; using namespace Quotient; @@ -33,5 +34,33 @@ void TestKeyImport::testImport() QCOMPARE(json.size(), 2); } +void TestKeyImport::testExport() +{ + KeyImport keyImport; + + QJsonArray sessions; + sessions += QJsonObject { + {"algorithm"_ls, "m.megolm.v1.aes-sha2"_ls}, + {"forwarding_curve25519_key_chain"_ls, QJsonArray()}, + {"room_id"_ls, "!asdf:foo.bar"_ls}, + {"sender_claimed_keys"_ls, QJsonObject { + {"ed25519"_ls, "asdfkey"_ls} + }}, + {"sender_key"_ls, "senderkey"_ls}, + {"session_id"_ls, "sessionidasdf"_ls}, + {"session_key"_ls, "sessionkeyfoo"_ls}, + + }; + auto result = keyImport.encrypt(sessions, QStringLiteral("a passphrase")); + QVERIFY(result.has_value()); + QVERIFY(result.value().size() > 0); + + auto plain = keyImport.decrypt(QString::fromLatin1(result.value()), "a passphrase"_ls); + QVERIFY(plain.has_value()); + auto value = plain.value(); + QCOMPARE(value.size(), 1); + QCOMPARE(value[0]["algorithm"_ls].toString(), "m.megolm.v1.aes-sha2"_ls); +} + QTEST_GUILESS_MAIN(TestKeyImport) #include "testkeyimport.moc" From a6c41422416bb535b95a97e842303306d58fd2de Mon Sep 17 00:00:00 2001 From: Tobias Fella <9750016+TobiasFella@users.noreply.github.com> Date: Wed, 26 Jun 2024 21:01:31 +0200 Subject: [PATCH 06/20] Update Quotient/keyimport.cpp Co-authored-by: Alexey Rusakov --- Quotient/keyimport.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Quotient/keyimport.cpp b/Quotient/keyimport.cpp index 6f8a75de2..1a25cf3ac 100644 --- a/Quotient/keyimport.cpp +++ b/Quotient/keyimport.cpp @@ -100,7 +100,7 @@ Quotient::Expected KeyImport::encrypt(QJsonArray s auto salt = getRandom(AesBlockSize); auto iv = getRandom(AesBlockSize); - quint32 rounds = 200000; // spec: "N should be at least 100,000"; + quint32 rounds = 200'000; // spec: "N should be at least 100,000"; auto keys = pbkdf2HmacSha512<64>(passphrase.toLatin1(), salt.viewAsByteArray(), rounds); if (!keys.has_value()) { From 53b5db16467e6c8604cec61b7918f7808b92302e Mon Sep 17 00:00:00 2001 From: Tobias Fella <9750016+TobiasFella@users.noreply.github.com> Date: Wed, 26 Jun 2024 21:09:29 +0200 Subject: [PATCH 07/20] Update Quotient/database.cpp Co-authored-by: Alexey Rusakov --- Quotient/database.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Quotient/database.cpp b/Quotient/database.cpp index b2e181fc2..0f733dfab 100644 --- a/Quotient/database.cpp +++ b/Quotient/database.cpp @@ -255,13 +255,13 @@ void Database::migrateTo10() keys += query.value("senderKey"_ls).toString(); } for (const auto& key : keys) { - auto query = prepareQuery("SELECT edKey FROM tracked_devices WHERE curveKey=:curveKey;"_ls); - query.bindValue(":curveKey"_ls, key); - execute(query); - if (!query.next()) { + auto edKeyQuery = prepareQuery("SELECT edKey FROM tracked_devices WHERE curveKey=:curveKey;"_ls); + edKeyquery.bindValue(":curveKey"_ls, key); + execute(edKeyQuery); + if (!edKeyQuery.next()) { continue; } - const auto &edKey = query.value("edKey"_ls).toByteArray(); + const auto &edKey = edKeyQuery.value("edKey"_ls).toByteArray(); auto updateQuery = prepareQuery("UPDATE inbound_megolm_sessions SET senderClaimedEd25519Key=:senderClaimedEd25519Key WHERE senderKey=:senderKey;"_ls); updateQuery.bindValue(":senderKey"_ls, key.toLatin1()); From e93b5e44957900a2a9f04cc62926e37f38fdb4eb Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Wed, 26 Jun 2024 21:45:37 +0200 Subject: [PATCH 08/20] oops --- Quotient/database.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Quotient/database.cpp b/Quotient/database.cpp index 0f733dfab..f407156e1 100644 --- a/Quotient/database.cpp +++ b/Quotient/database.cpp @@ -256,7 +256,7 @@ void Database::migrateTo10() } for (const auto& key : keys) { auto edKeyQuery = prepareQuery("SELECT edKey FROM tracked_devices WHERE curveKey=:curveKey;"_ls); - edKeyquery.bindValue(":curveKey"_ls, key); + edKeyQuery.bindValue(":curveKey"_ls, key); execute(edKeyQuery); if (!edKeyQuery.next()) { continue; From 06a64a89b56322f145655d64bfdfbd1797253581 Mon Sep 17 00:00:00 2001 From: Tobias Fella <9750016+TobiasFella@users.noreply.github.com> Date: Sat, 29 Jun 2024 11:01:25 +0200 Subject: [PATCH 09/20] Update Quotient/keyimport.cpp Co-authored-by: Alexey Rusakov --- Quotient/keyimport.cpp | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/Quotient/keyimport.cpp b/Quotient/keyimport.cpp index 1a25cf3ac..29562c396 100644 --- a/Quotient/keyimport.cpp +++ b/Quotient/keyimport.cpp @@ -130,16 +130,10 @@ Quotient::Expected KeyImport::encrypt(QJsonArray s } data.append(mac.value()); - QByteArray base64 = data.toBase64(); - - // Limit base64 line length. 96 is chosen arbitrarily. - for (auto i = 96; i < base64.length(); i += 96) { - base64.insert(i, "\n"); - i++; - } - base64.prepend("-----BEGIN MEGOLM SESSION DATA-----\n"_ls); - base64.append("\n-----END MEGOLM SESSION DATA-----\n"_ls); - return base64; + return "-----BEGIN MEGOLM SESSION DATA-----\n"_ls + % (std::views::chunk(data.toBase64(), 96) | std::views::join_with('\n') + | std::ranges::to()) + % "\n-----END MEGOLM SESSION DATA-----\n"_ls; } From 1e6f7eff32ce3b950211d04d497bc5691bea144d Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sat, 29 Jun 2024 11:12:39 +0200 Subject: [PATCH 10/20] Use DISTINCT --- Quotient/database.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Quotient/database.cpp b/Quotient/database.cpp index f407156e1..cc1764c84 100644 --- a/Quotient/database.cpp +++ b/Quotient/database.cpp @@ -247,10 +247,10 @@ void Database::migrateTo10() execute(QStringLiteral("ALTER TABLE inbound_megolm_sessions ADD senderClaimedEd25519Key TEXT;")); - auto query = prepareQuery("SELECT senderKey FROM inbound_megolm_sessions;"_ls); + auto query = prepareQuery("SELECT DISTINCT senderKey FROM inbound_megolm_sessions;"_ls); execute(query); - QSet keys; + QStringList keys; while (query.next()) { keys += query.value("senderKey"_ls).toString(); } From 9a94be7041121a7dc19e585c964a623b8d45702e Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sat, 29 Jun 2024 11:13:13 +0200 Subject: [PATCH 11/20] Fix --- Quotient/keyimport.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Quotient/keyimport.cpp b/Quotient/keyimport.cpp index 29562c396..17ffae74c 100644 --- a/Quotient/keyimport.cpp +++ b/Quotient/keyimport.cpp @@ -3,6 +3,8 @@ #include "keyimport.h" +#include + #include #include @@ -13,6 +15,7 @@ #include "logging_categories_p.h" using namespace Quotient; +using namespace Qt::Literals::StringLiterals; const auto VersionLength = 1; const auto SaltOffset = VersionLength; @@ -130,10 +133,10 @@ Quotient::Expected KeyImport::encrypt(QJsonArray s } data.append(mac.value()); - return "-----BEGIN MEGOLM SESSION DATA-----\n"_ls + return "-----BEGIN MEGOLM SESSION DATA-----\n"_ba % (std::views::chunk(data.toBase64(), 96) | std::views::join_with('\n') | std::ranges::to()) - % "\n-----END MEGOLM SESSION DATA-----\n"_ls; + % "\n-----END MEGOLM SESSION DATA-----\n"_ba; } From c9923f7505bd0d95fc4cac73d1d56d587af8cbb0 Mon Sep 17 00:00:00 2001 From: Tobias Fella <9750016+TobiasFella@users.noreply.github.com> Date: Sat, 29 Jun 2024 11:14:27 +0200 Subject: [PATCH 12/20] Update Quotient/keyimport.h Co-authored-by: Alexey Rusakov --- Quotient/keyimport.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Quotient/keyimport.h b/Quotient/keyimport.h index 3d305c1b6..04cadf358 100644 --- a/Quotient/keyimport.h +++ b/Quotient/keyimport.h @@ -35,7 +35,7 @@ class QUOTIENT_API KeyImport : public QObject Q_INVOKABLE Error importKeys(QString data, const QString& passphrase, const Quotient::Connection* connection); - Q_INVOKABLE Quotient::Expected exportKeys(const QString& passphrase, Quotient::Connection* connection); + Q_INVOKABLE Quotient::Expected exportKeys(const QString& passphrase, const Quotient::Connection* connection); friend class ::TestKeyImport; private: From 94c6a171c96468648e65d37abdb24dd3dcb06457 Mon Sep 17 00:00:00 2001 From: Tobias Fella <9750016+TobiasFella@users.noreply.github.com> Date: Sat, 29 Jun 2024 11:14:34 +0200 Subject: [PATCH 13/20] Update Quotient/keyimport.cpp Co-authored-by: Alexey Rusakov --- Quotient/keyimport.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Quotient/keyimport.cpp b/Quotient/keyimport.cpp index 17ffae74c..85d9eb5be 100644 --- a/Quotient/keyimport.cpp +++ b/Quotient/keyimport.cpp @@ -140,7 +140,7 @@ Quotient::Expected KeyImport::encrypt(QJsonArray s } -Quotient::Expected KeyImport::exportKeys(const QString& passphrase, Connection* connection) +Quotient::Expected KeyImport::exportKeys(const QString& passphrase, const Connection* connection) { QJsonArray sessions; for (const auto& room : connection->allRooms()) { From 045fe6a777839efd224ca66f903f2922d0ad8efb Mon Sep 17 00:00:00 2001 From: Tobias Fella <9750016+TobiasFella@users.noreply.github.com> Date: Sun, 30 Jun 2024 20:41:34 +0200 Subject: [PATCH 14/20] Update Quotient/room.cpp Co-authored-by: Alexey Rusakov --- Quotient/room.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Quotient/room.cpp b/Quotient/room.cpp index 87229c518..49f8b99b1 100644 --- a/Quotient/room.cpp +++ b/Quotient/room.cpp @@ -3451,7 +3451,7 @@ void Room::startVerification() QJsonArray Room::exportMegolmSessions() { QJsonArray sessions; - for (auto &[key, value] : d->groupSessions) { + for (const auto& [key, value] : d->groupSessions) { auto session = value.exportSession(value.firstKnownIndex()); if (!session.has_value()) { qCWarning(E2EE) << "Failed to export session" << session.error(); From c6d5e67faab524d4f4c73a633b197621601c877c Mon Sep 17 00:00:00 2001 From: Tobias Fella <9750016+TobiasFella@users.noreply.github.com> Date: Sun, 30 Jun 2024 20:42:16 +0200 Subject: [PATCH 15/20] Update Quotient/keyimport.cpp Co-authored-by: Alexey Rusakov --- Quotient/keyimport.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Quotient/keyimport.cpp b/Quotient/keyimport.cpp index 85d9eb5be..8654988dd 100644 --- a/Quotient/keyimport.cpp +++ b/Quotient/keyimport.cpp @@ -101,8 +101,8 @@ Quotient::Expected KeyImport::encrypt(QJsonArray s { auto plainText = QJsonDocument(sessions).toJson(QJsonDocument::Compact); - auto salt = getRandom(AesBlockSize); - auto iv = getRandom(AesBlockSize); + auto salt = getRandom(); + auto iv = getRandom(); quint32 rounds = 200'000; // spec: "N should be at least 100,000"; auto keys = pbkdf2HmacSha512<64>(passphrase.toLatin1(), salt.viewAsByteArray(), rounds); From 1c0cb3a9681a19166fcb1429346e58d0af9f7b59 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sun, 30 Jun 2024 20:42:47 +0200 Subject: [PATCH 16/20] Remove debug line --- Quotient/keyimport.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/Quotient/keyimport.cpp b/Quotient/keyimport.cpp index 8654988dd..be2867bf0 100644 --- a/Quotient/keyimport.cpp +++ b/Quotient/keyimport.cpp @@ -145,7 +145,6 @@ Quotient::Expected KeyImport::exportKeys(const QSt QJsonArray sessions; for (const auto& room : connection->allRooms()) { for (const auto &session : room->exportMegolmSessions()) { - return {}; sessions += session; } } From 34ed6adc5907185f97915a54e5fc27022b45c87e Mon Sep 17 00:00:00 2001 From: Tobias Fella <9750016+TobiasFella@users.noreply.github.com> Date: Sun, 30 Jun 2024 20:43:18 +0200 Subject: [PATCH 17/20] Update Quotient/keyimport.cpp Co-authored-by: Alexey Rusakov --- Quotient/keyimport.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Quotient/keyimport.cpp b/Quotient/keyimport.cpp index be2867bf0..ebae22d8c 100644 --- a/Quotient/keyimport.cpp +++ b/Quotient/keyimport.cpp @@ -133,10 +133,12 @@ Quotient::Expected KeyImport::encrypt(QJsonArray s } data.append(mac.value()); - return "-----BEGIN MEGOLM SESSION DATA-----\n"_ba - % (std::views::chunk(data.toBase64(), 96) | std::views::join_with('\n') - | std::ranges::to()) - % "\n-----END MEGOLM SESSION DATA-----\n"_ba; + // TODO: use std::ranges::to() once it's available from all stdlibs Quotient builds with + return "-----BEGIN MEGOLM SESSION DATA-----\n"_ba + % std::ranges::fold_left(std::views::chunk(data.toBase64(), 96) + | std::views::join_with('\n'), + QByteArray{}, std::plus<>()) + % "\n-----END MEGOLM SESSION DATA-----\n"_ba; } From a1e3e2e8349da5608c34354a95376d6bd87450d7 Mon Sep 17 00:00:00 2001 From: Tobias Fella <9750016+TobiasFella@users.noreply.github.com> Date: Sun, 30 Jun 2024 20:43:31 +0200 Subject: [PATCH 18/20] Update Quotient/room.cpp Co-authored-by: Alexey Rusakov --- Quotient/room.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Quotient/room.cpp b/Quotient/room.cpp index 49f8b99b1..f06a1c8a6 100644 --- a/Quotient/room.cpp +++ b/Quotient/room.cpp @@ -3458,18 +3458,18 @@ QJsonArray Room::exportMegolmSessions() continue; } + const auto senderClaimedKey = connection()->database()->edKeyForMegolmSession(QString::fromLatin1(value.sessionId())); + const auto senderKey = connection()->database()->senderKeyForMegolmSession(QString::fromLatin1(value.sessionId())); const auto json = QJsonObject { {"algorithm"_ls, "m.megolm.v1.aes-sha2"_ls}, {"forwarding_curve25519_key_chain"_ls, QJsonArray()}, {"room_id"_ls, id()}, - {"sender_claimed_keys"_ls, QJsonObject{ - {"ed25519"_ls, connection()->database()->edKeyForMegolmSession(QString::fromLatin1(value.sessionId()))}, - }}, - {"sender_key"_ls, connection()->database()->senderKeyForMegolmSession(QString::fromLatin1(value.sessionId()))}, + {"sender_claimed_keys"_ls, QJsonObject{ {"ed25519"_ls, senderClaimedKey} }}, + {"sender_key"_ls, senderKey}, {"session_id"_ls, QString::fromLatin1(value.sessionId())}, {"session_key"_ls, QString::fromLatin1(session.value())}, }; - if (json["sender_claimed_keys"_ls]["ed25519"_ls].toString().isEmpty() || json["sender_key"_ls].toString().isEmpty()) { + if (senderClaimedKey.isEmpty() || senderKey.isEmpty()) { // These are edge-cases for some sessions that were added before libquotient started storing these fields. // Some clients refuse to the entire file if this is missing for one key, so we shouldn't export the session in this case. qCWarning(E2EE) << "Session" << value.sessionId() << "has unknown sender key."; From f8b2c2a249b4cf329e17a9bc512db16d95aef155 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Sun, 30 Jun 2024 20:51:13 +0200 Subject: [PATCH 19/20] Fix compilation --- Quotient/room.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Quotient/room.cpp b/Quotient/room.cpp index f06a1c8a6..be7d2e196 100644 --- a/Quotient/room.cpp +++ b/Quotient/room.cpp @@ -3451,7 +3451,7 @@ void Room::startVerification() QJsonArray Room::exportMegolmSessions() { QJsonArray sessions; - for (const auto& [key, value] : d->groupSessions) { + for (auto& [key, value] : d->groupSessions) { auto session = value.exportSession(value.firstKnownIndex()); if (!session.has_value()) { qCWarning(E2EE) << "Failed to export session" << session.error(); From 964c76d4350ca3a1acb7dff047e152a5867fe1bf Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 2 Jul 2024 10:49:05 +0200 Subject: [PATCH 20/20] Make line wrapping code work on macOS --- Quotient/keyimport.cpp | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/Quotient/keyimport.cpp b/Quotient/keyimport.cpp index ebae22d8c..9eb422b25 100644 --- a/Quotient/keyimport.cpp +++ b/Quotient/keyimport.cpp @@ -97,6 +97,21 @@ KeyImport::Error KeyImport::importKeys(QString data, const QString& passphrase, return Success; } +inline QByteArray lineWrapped(QByteArray text, int wrapAt) +{ +#if defined(__cpp_lib_ranges_chunk) && defined(__cpp_lib_ranges_join_with) \ + && defined(__cpp_lib_ranges_to_container) + using namespace std::ranges; + return views::chunk(std::move(text), wrapAt) | views::join_with('\n') | to(); +#else // Xcode 15 and older; libc++ 17 and older + for (auto i = 96; i < text.size(); i += 96) { + text.insert(i, "\n"); + i++; + } + return text; +#endif +} + Quotient::Expected KeyImport::encrypt(QJsonArray sessions, const QString& passphrase) { auto plainText = QJsonDocument(sessions).toJson(QJsonDocument::Compact); @@ -134,11 +149,8 @@ Quotient::Expected KeyImport::encrypt(QJsonArray s data.append(mac.value()); // TODO: use std::ranges::to() once it's available from all stdlibs Quotient builds with - return "-----BEGIN MEGOLM SESSION DATA-----\n"_ba - % std::ranges::fold_left(std::views::chunk(data.toBase64(), 96) - | std::views::join_with('\n'), - QByteArray{}, std::plus<>()) - % "\n-----END MEGOLM SESSION DATA-----\n"_ba; + return "-----BEGIN MEGOLM SESSION DATA-----\n"_ba % lineWrapped(data.toBase64(), 96) + % "\n-----END MEGOLM SESSION DATA-----\n"_ba; } @@ -152,4 +164,3 @@ Quotient::Expected KeyImport::exportKeys(const QSt } return encrypt(sessions, passphrase); } -