Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement exporting megolm sessions to a file #761

Merged
merged 20 commits into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Quotient/connection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1743,9 +1743,9 @@ std::unordered_map<QByteArray, QOlmInboundGroupSession> 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
Expand Down
2 changes: 1 addition & 1 deletion Quotient/connection.h
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ class QUOTIENT_API Connection : public QObject {
std::unordered_map<QByteArray, QOlmInboundGroupSession> 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;
Expand Down
4 changes: 2 additions & 2 deletions Quotient/connectionencryptiondata_p.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
59 changes: 56 additions & 3 deletions Quotient/database.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}

Expand Down Expand Up @@ -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 DISTINCT senderKey FROM inbound_megolm_sessions;"_ls);
execute(query);

QStringList keys;
while (query.next()) {
keys += query.value("senderKey"_ls).toString();
}
for (const auto& key : keys) {
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 = 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());
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;"));
Expand Down Expand Up @@ -349,19 +385,20 @@ std::unordered_map<QByteArray, QOlmInboundGroupSession> 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, 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));
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);
Expand Down Expand Up @@ -621,3 +658,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.toLatin1());
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.toLatin1());
execute(query);
return query.next() ? query.value("senderKey"_ls).toString() : QString();
}
7 changes: 6 additions & 1 deletion Quotient/database.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -79,6 +80,9 @@ class QUOTIENT_API Database
QString userSigningPublicKey();
QString selfSigningPublicKey();

QString edKeyForMegolmSession(const QString& sessionId);
QString senderKeyForMegolmSession(const QString& sessionId);

private:
void migrateTo1();
void migrateTo2();
Expand All @@ -89,6 +93,7 @@ class QUOTIENT_API Database
void migrateTo7();
void migrateTo8();
void migrateTo9();
void migrateTo10();

QString m_userId;
QString m_deviceId;
Expand Down
2 changes: 1 addition & 1 deletion Quotient/e2ee/sssshandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<uint32_t>(backupData.firstMessageIndex), data["sender_key"_ls].toVariant().toByteArray());
static_cast<uint32_t>(backupData.firstMessageIndex), data["sender_key"_ls].toVariant().toByteArray(), data["sender_claimed_keys"_ls]["ed25519"_ls].toVariant().toByteArray());
}
}
emit finished();
Expand Down
74 changes: 73 additions & 1 deletion Quotient/keyimport.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

#include "keyimport.h"

#include <ranges>

#include <QtEndian>
#include <QDebug>

Expand All @@ -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;
Expand Down Expand Up @@ -87,8 +90,77 @@ 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;
}

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<QByteArray>();
#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<QByteArray, KeyImport::Error> KeyImport::encrypt(QJsonArray sessions, const QString& passphrase)
{
auto plainText = QJsonDocument(sessions).toJson(QJsonDocument::Compact);

auto salt = getRandom<AesBlockSize>();
auto iv = getRandom<AesBlockSize>();
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()) {
qCWarning(E2EE) << "Failed to calculate pbkdf:" << keys.error();
return OtherError;
}

auto result = aesCtr256Encrypt(plainText, byte_view_t<Aes256KeySize>(keys.value().begin(), Aes256KeySize), asCBytes<AesBlockSize>(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<quint32>(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());

// TODO: use std::ranges::to() once it's available from all stdlibs Quotient builds with
return "-----BEGIN MEGOLM SESSION DATA-----\n"_ba % lineWrapped(data.toBase64(), 96)
% "\n-----END MEGOLM SESSION DATA-----\n"_ba;
}


Quotient::Expected<QByteArray, KeyImport::Error> KeyImport::exportKeys(const QString& passphrase, const Connection* connection)
{
QJsonArray sessions;
for (const auto& room : connection->allRooms()) {
for (const auto &session : room->exportMegolmSessions()) {
sessions += session;
}
}
return encrypt(sessions, passphrase);
}
2 changes: 2 additions & 0 deletions Quotient/keyimport.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,12 @@ class QUOTIENT_API KeyImport : public QObject

Q_INVOKABLE Error importKeys(QString data, const QString& passphrase,
const Quotient::Connection* connection);
Q_INVOKABLE Quotient::Expected<QByteArray, Error> exportKeys(const QString& passphrase, const Quotient::Connection* connection);

friend class ::TestKeyImport;
private:
Quotient::Expected<QJsonArray, Error> decrypt(QString data, const QString& passphrase);
Quotient::Expected<QByteArray, Error> encrypt(QJsonArray sessions, const QString& passphrase);
};

}
50 changes: 43 additions & 7 deletions Quotient/room.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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;
}
Expand Down Expand Up @@ -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<QString, QString> getDevicesWithoutKey() const
Expand Down Expand Up @@ -1551,15 +1553,16 @@ 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"
<< roomKeyEvent.algorithm() << "in m.room_key event";
}
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 =
Expand Down Expand Up @@ -3415,7 +3418,7 @@ void Room::activateEncryption()
setState<EncryptionEvent>(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)
Expand All @@ -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()
Expand All @@ -3444,3 +3447,36 @@ 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()) {
qCWarning(E2EE) << "Failed to export session" << session.error();
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, senderClaimedKey} }},
{"sender_key"_ls, senderKey},
{"session_id"_ls, QString::fromLatin1(value.sessionId())},
{"session_key"_ls, QString::fromLatin1(session.value())},
};
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.";
continue;
}
sessions.append(json);
}
return sessions;
}

7 changes: 5 additions & 2 deletions Quotient/room.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -679,10 +680,12 @@ class QUOTIENT_API Room : public QObject {
return setState(EvT(std::forward<ArgTs>(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();

QJsonArray exportMegolmSessions();

public Q_SLOTS:
/** Check whether the room should be upgraded */
void checkVersion();
Expand Down
Loading
Loading