aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/connection.cpp495
-rw-r--r--lib/connection.h28
-rw-r--r--lib/database.cpp244
-rw-r--r--lib/database.h43
-rw-r--r--lib/e2ee.h35
-rw-r--r--lib/e2ee/e2ee.h132
-rw-r--r--lib/e2ee/qolmaccount.cpp328
-rw-r--r--lib/e2ee/qolmaccount.h123
-rw-r--r--lib/e2ee/qolmerrors.cpp25
-rw-r--r--lib/e2ee/qolmerrors.h26
-rw-r--r--lib/e2ee/qolminboundsession.cpp151
-rw-r--r--lib/e2ee/qolminboundsession.h48
-rw-r--r--lib/e2ee/qolmmessage.cpp35
-rw-r--r--lib/e2ee/qolmmessage.h41
-rw-r--r--lib/e2ee/qolmoutboundsession.cpp125
-rw-r--r--lib/e2ee/qolmoutboundsession.h52
-rw-r--r--lib/e2ee/qolmsession.cpp251
-rw-r--r--lib/e2ee/qolmsession.h76
-rw-r--r--lib/e2ee/qolmutility.cpp61
-rw-r--r--lib/e2ee/qolmutility.h45
-rw-r--r--lib/e2ee/qolmutils.cpp23
-rw-r--r--lib/e2ee/qolmutils.h15
-rw-r--r--lib/encryptionmanager.cpp373
-rw-r--r--lib/encryptionmanager.h50
-rw-r--r--lib/events/encryptedevent.cpp21
-rw-r--r--lib/events/encryptedevent.h3
-rw-r--r--lib/events/encryptedfile.cpp31
-rw-r--r--lib/events/encryptedfile.h2
-rw-r--r--lib/events/encryptionevent.cpp2
-rw-r--r--lib/events/eventcontent.cpp1
-rw-r--r--lib/events/keyverificationevent.cpp164
-rw-r--r--lib/events/keyverificationevent.h167
-rw-r--r--lib/events/roomevent.cpp15
-rw-r--r--lib/events/roomevent.h10
-rw-r--r--lib/jobs/downloadfilejob.cpp73
-rw-r--r--lib/jobs/downloadfilejob.h4
-rw-r--r--lib/logging.cpp1
-rw-r--r--lib/logging.h1
-rw-r--r--lib/mxcreply.cpp36
-rw-r--r--lib/networkaccessmanager.cpp11
-rw-r--r--lib/networkaccessmanager.h1
-rw-r--r--lib/room.cpp202
-rw-r--r--lib/settings.cpp6
-rw-r--r--lib/syncdata.cpp34
-rw-r--r--lib/syncdata.h23
45 files changed, 3005 insertions, 628 deletions
diff --git a/lib/connection.cpp b/lib/connection.cpp
index 67bc83f9..14188ace 100644
--- a/lib/connection.cpp
+++ b/lib/connection.cpp
@@ -7,9 +7,6 @@
#include "connection.h"
#include "connectiondata.h"
-#ifdef Quotient_E2EE_ENABLED
-# include "encryptionmanager.h"
-#endif // Quotient_E2EE_ENABLED
#include "room.h"
#include "settings.h"
#include "user.h"
@@ -38,7 +35,16 @@
#include "jobs/syncjob.h"
#ifdef Quotient_E2EE_ENABLED
-# include "account.h" // QtOlm
+# include "e2ee/qolmaccount.h"
+# include "e2ee/qolmutils.h"
+# include "database.h"
+# include "e2ee/qolminboundsession.h"
+
+#if QT_VERSION_MAJOR >= 6
+# include <qt6keychain/keychain.h>
+#else
+# include <qt5keychain/keychain.h>
+#endif
#endif // Quotient_E2EE_ENABLED
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
@@ -55,6 +61,7 @@
#include <QtCore/QStringBuilder>
#include <QtNetwork/QDnsLookup>
+
using namespace Quotient;
// This is very much Qt-specific; STL iterators don't have key() and value()
@@ -102,13 +109,28 @@ public:
QMetaObject::Connection syncLoopConnection {};
int syncTimeout = -1;
+#ifdef Quotient_E2EE_ENABLED
+ QSet<QString> trackedUsers;
+ QSet<QString> outdatedUsers;
+ QHash<QString, QHash<QString, DeviceKeys>> deviceKeys;
+ QueryKeysJob *currentQueryKeysJob = nullptr;
+ bool encryptionUpdateRequired = false;
+ PicklingMode picklingMode = Unencrypted {};
+ Database *database = nullptr;
+
+ // A map from SenderKey to vector of InboundSession
+ UnorderedMap<QString, std::vector<QOlmSessionPtr>> olmSessions;
+
+#endif
+
GetCapabilitiesJob* capabilitiesJob = nullptr;
GetCapabilitiesJob::Capabilities capabilities;
QVector<GetLoginFlowsJob::LoginFlow> loginFlows;
#ifdef Quotient_E2EE_ENABLED
- QScopedPointer<EncryptionManager> encryptionManager;
+ std::unique_ptr<QOlmAccount> olmAccount;
+ bool isUploadingKeys = false;
#endif // Quotient_E2EE_ENABLED
QPointer<GetWellknownJob> resolverJob = nullptr;
@@ -151,6 +173,7 @@ public:
void consumeAccountData(Events&& accountDataEvents);
void consumePresenceData(Events&& presenceData);
void consumeToDeviceEvents(Events&& toDeviceEvents);
+ void consumeDevicesList(DevicesList&& devicesList);
template <typename EventT>
EventT* unpackAccountData() const
@@ -181,29 +204,107 @@ public:
return q->stateCacheDir().filePath("state.json");
}
+#ifdef Quotient_E2EE_ENABLED
+ void loadSessions() {
+ olmSessions = q->database()->loadOlmSessions(q->picklingMode());
+ }
+ void saveSession(QOlmSessionPtr& session, const QString &senderKey) {
+ auto pickleResult = session->pickle(q->picklingMode());
+ if (std::holds_alternative<QOlmError>(pickleResult)) {
+ qCWarning(E2EE) << "Failed to pickle olm session. Error" << std::get<QOlmError>(pickleResult);
+ return;
+ }
+ q->database()->saveOlmSession(senderKey, session->sessionId(), std::get<QByteArray>(pickleResult));
+ }
+ QString sessionDecryptPrekey(const QOlmMessage& message, const QString &senderKey, std::unique_ptr<QOlmAccount>& olmAccount)
+ {
+ Q_ASSERT(message.type() == QOlmMessage::PreKey);
+ for(auto& session : olmSessions[senderKey]) {
+ const auto matches = session->matchesInboundSessionFrom(senderKey, message);
+ if(std::holds_alternative<bool>(matches) && std::get<bool>(matches)) {
+ qCDebug(E2EE) << "Found inbound session";
+ const auto result = session->decrypt(message);
+ if(std::holds_alternative<QString>(result)) {
+ return std::get<QString>(result);
+ } else {
+ qCDebug(E2EE) << "Failed to decrypt prekey message";
+ return {};
+ }
+ }
+ }
+ qCDebug(E2EE) << "Creating new inbound session";
+ auto newSessionResult = olmAccount->createInboundSessionFrom(senderKey.toUtf8(), message);
+ if(std::holds_alternative<QOlmError>(newSessionResult)) {
+ qCWarning(E2EE) << "Failed to create inbound session for" << senderKey << std::get<QOlmError>(newSessionResult);
+ return {};
+ }
+ auto newSession = std::move(std::get<QOlmSessionPtr>(newSessionResult));
+ auto error = olmAccount->removeOneTimeKeys(newSession);
+ if (error) {
+ qWarning(E2EE) << "Failed to remove one time key for session" << newSession->sessionId();
+ }
+ const auto result = newSession->decrypt(message);
+ saveSession(newSession, senderKey);
+ olmSessions[senderKey].push_back(std::move(newSession));
+ if(std::holds_alternative<QString>(result)) {
+ return std::get<QString>(result);
+ } else {
+ qCDebug(E2EE) << "Failed to decrypt prekey message with new session";
+ return {};
+ }
+ }
+ QString sessionDecryptGeneral(const QOlmMessage& message, const QString &senderKey)
+ {
+ Q_ASSERT(message.type() == QOlmMessage::General);
+ for(auto& session : olmSessions[senderKey]) {
+ const auto result = session->decrypt(message);
+ if(std::holds_alternative<QString>(result)) {
+ return std::get<QString>(result);
+ }
+ }
+ qCWarning(E2EE) << "Failed to decrypt message";
+ return {};
+ }
+
+ QString sessionDecryptMessage(
+ const QJsonObject& personalCipherObject, const QByteArray& senderKey, std::unique_ptr<QOlmAccount>& account)
+ {
+ QString decrypted;
+ int type = personalCipherObject.value(TypeKeyL).toInt(-1);
+ QByteArray body = personalCipherObject.value(BodyKeyL).toString().toLatin1();
+ if (type == 0) {
+ QOlmMessage preKeyMessage(body, QOlmMessage::PreKey);
+ decrypted = sessionDecryptPrekey(preKeyMessage, senderKey, account);
+ } else if (type == 1) {
+ QOlmMessage message(body, QOlmMessage::General);
+ decrypted = sessionDecryptGeneral(message, senderKey);
+ }
+ return decrypted;
+ }
+#endif
+
EventPtr sessionDecryptMessage(const EncryptedEvent& encryptedEvent)
{
#ifndef Quotient_E2EE_ENABLED
qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off.";
return {};
-#else // Quotient_E2EE_ENABLED
+#else
if (encryptedEvent.algorithm() != OlmV1Curve25519AesSha2AlgoKey)
return {};
- const auto identityKey =
- encryptionManager->account()->curve25519IdentityKey();
+ const auto identityKey = olmAccount->identityKeys().curve25519;
const auto personalCipherObject =
encryptedEvent.ciphertext(identityKey);
if (personalCipherObject.isEmpty()) {
qCDebug(E2EE) << "Encrypted event is not for the current device";
return {};
}
- const auto decrypted = encryptionManager->sessionDecryptMessage(
- personalCipherObject, encryptedEvent.senderKey().toLatin1());
+ const auto decrypted = sessionDecryptMessage(
+ personalCipherObject, encryptedEvent.senderKey().toLatin1(), olmAccount);
if (decrypted.isEmpty()) {
qCDebug(E2EE) << "Problem with new session from senderKey:"
<< encryptedEvent.senderKey()
- << encryptionManager->account()->oneTimeKeys();
+ << olmAccount->oneTimeKeys().keys;
return {};
}
@@ -220,22 +321,18 @@ public:
// TODO: keys to constants
const auto decryptedEventObject = decryptedEvent->fullJson();
- const auto recipient =
- decryptedEventObject.value("recipient"_ls).toString();
+ const auto recipient = decryptedEventObject.value("recipient"_ls).toString();
if (recipient != data->userId()) {
qCDebug(E2EE) << "Found user" << recipient << "instead of us"
<< data->userId() << "in Olm plaintext";
return {};
}
- const auto ourKey =
- decryptedEventObject.value("recipient_keys"_ls).toObject()
- .value(Ed25519Key).toString();
- if (ourKey
- != QString::fromUtf8(
- encryptionManager->account()->ed25519IdentityKey())) {
+ const auto ourKey = decryptedEventObject.value("recipient_keys"_ls).toObject()
+ .value(Ed25519Key).toString();
+ if (ourKey != QString::fromUtf8(olmAccount->identityKeys().ed25519)) {
qCDebug(E2EE) << "Found key" << ourKey
<< "instead of ours own ed25519 key"
- << encryptionManager->account()->ed25519IdentityKey()
+ << olmAccount->identityKeys().ed25519
<< "in Olm plaintext";
return {};
}
@@ -243,12 +340,20 @@ public:
return std::move(decryptedEvent);
#endif // Quotient_E2EE_ENABLED
}
+#ifdef Quotient_E2EE_ENABLED
+ void loadOutdatedUserDevices();
+ void saveDevicesList();
+ void loadDevicesList();
+#endif
};
Connection::Connection(const QUrl& server, QObject* parent)
: QObject(parent)
, d(makeImpl<Private>(std::make_unique<ConnectionData>(server)))
{
+#ifdef Quotient_E2EE_ENABLED
+ //connect(qApp, &QCoreApplication::aboutToQuit, this, &Connection::saveOlmAccount);
+#endif
d->q = this; // All d initialization should occur before this line
}
@@ -421,8 +526,7 @@ void Connection::Private::loginToServer(LoginArgTs&&... loginArgs)
#ifndef Quotient_E2EE_ENABLED
qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off.";
#else // Quotient_E2EE_ENABLED
- encryptionManager->uploadIdentityKeys(q);
- encryptionManager->uploadOneTimeKeys(q);
+ database->clear();
#endif // Quotient_E2EE_ENABLED
});
connect(loginJob, &BaseJob::failure, q, [this, loginJob] {
@@ -443,11 +547,58 @@ void Connection::Private::completeSetup(const QString& mxId)
qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off.";
#else // Quotient_E2EE_ENABLED
AccountSettings accountSettings(data->userId());
- encryptionManager.reset(
- new EncryptionManager(accountSettings.encryptionAccountPickle()));
- if (accountSettings.encryptionAccountPickle().isEmpty()) {
- accountSettings.setEncryptionAccountPickle(
- encryptionManager->olmAccountPickle());
+
+ QKeychain::ReadPasswordJob job(qAppName());
+ job.setAutoDelete(false);
+ job.setKey(accountSettings.userId() + QStringLiteral("-Pickle"));
+ QEventLoop loop;
+ QKeychain::ReadPasswordJob::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
+ job.start();
+ loop.exec();
+
+ if (job.error() == QKeychain::Error::EntryNotFound) {
+ picklingMode = Encrypted { getRandom(128) };
+ QKeychain::WritePasswordJob job(qAppName());
+ job.setAutoDelete(false);
+ job.setKey(accountSettings.userId() + QStringLiteral("-Pickle"));
+ job.setBinaryData(std::get<Encrypted>(picklingMode).key);
+ QEventLoop loop;
+ QKeychain::WritePasswordJob::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
+ job.start();
+ loop.exec();
+
+ if (job.error()) {
+ qCWarning(E2EE) << "Could not save pickling key to keychain: " << job.errorString();
+ }
+ } else if(job.error() != QKeychain::Error::NoError) {
+ //TODO Error, do something
+ qCWarning(E2EE) << "Error loading pickling key from keychain:" << job.error();
+ } else {
+ qCDebug(E2EE) << "Successfully loaded pickling key from keychain";
+ picklingMode = Encrypted { job.binaryData() };
+ }
+
+ database = new Database(data->userId(), q);
+
+ // init olmAccount
+ olmAccount = std::make_unique<QOlmAccount>(data->userId(), data->deviceId(), q);
+ connect(olmAccount.get(), &QOlmAccount::needsSave, q, &Connection::saveOlmAccount);
+
+#ifdef Quotient_E2EE_ENABLED
+ loadSessions();
+#endif
+
+ if (database->accountPickle().isEmpty()) {
+ // create new account and save unpickle data
+ olmAccount->createNewAccount();
+ auto job = q->callApi<UploadKeysJob>(olmAccount->deviceKeys());
+ connect(job, &BaseJob::failure, q, [job]{
+ qCWarning(E2EE) << "Failed to upload device keys:" << job->errorString();
+ });
+ } else {
+ // account already existing
+ auto pickle = database->accountPickle();
+ olmAccount->unpickle(pickle, picklingMode);
}
#endif // Quotient_E2EE_ENABLED
emit q->stateChanged();
@@ -602,24 +753,39 @@ QJsonObject toJson(const DirectChatsMap& directChats)
void Connection::onSyncSuccess(SyncData&& data, bool fromCache)
{
+#ifdef Quotient_E2EE_ENABLED
+ if(data.deviceOneTimeKeysCount()["signed_curve25519"] < 0.4 * d->olmAccount->maxNumberOfOneTimeKeys() && !d->isUploadingKeys) {
+ d->isUploadingKeys = true;
+ d->olmAccount->generateOneTimeKeys(d->olmAccount->maxNumberOfOneTimeKeys() / 2 - data.deviceOneTimeKeysCount()["signed_curve25519"]);
+ auto keys = d->olmAccount->oneTimeKeys();
+ auto job = d->olmAccount->createUploadKeyRequest(keys);
+ run(job, ForegroundRequest);
+ connect(job, &BaseJob::success, this, [this](){
+ d->olmAccount->markKeysAsPublished();
+ });
+ connect(job, &BaseJob::result, this, [this](){
+ d->isUploadingKeys = false;
+ });
+ }
+ static bool first = true;
+ if(first) {
+ d->loadDevicesList();
+ first = false;
+ }
+
+ d->consumeDevicesList(data.takeDevicesList());
+#endif // Quotient_E2EE_ENABLED
d->data->setLastEvent(data.nextBatch());
d->consumeRoomData(data.takeRoomData(), fromCache);
d->consumeAccountData(data.takeAccountData());
d->consumePresenceData(data.takePresenceData());
d->consumeToDeviceEvents(data.takeToDeviceEvents());
#ifdef Quotient_E2EE_ENABLED
- // handling device_one_time_keys_count
- if (!d->encryptionManager)
- {
- qCDebug(E2EE) << "Encryption manager is not there yet, updating "
- "one-time key counts will be skipped";
- return;
+ if(d->encryptionUpdateRequired) {
+ d->loadOutdatedUserDevices();
+ d->encryptionUpdateRequired = false;
}
- if (const auto deviceOneTimeKeysCount = data.deviceOneTimeKeysCount();
- !deviceOneTimeKeysCount.isEmpty())
- d->encryptionManager->updateOneTimeKeyCounts(this,
- deviceOneTimeKeysCount);
-#endif // Quotient_E2EE_ENABLED
+#endif
}
void Connection::Private::consumeRoomData(SyncDataList&& roomDataList,
@@ -747,34 +913,55 @@ void Connection::Private::consumePresenceData(Events&& presenceData)
void Connection::Private::consumeToDeviceEvents(Events&& toDeviceEvents)
{
#ifdef Quotient_E2EE_ENABLED
- // handling m.room_key to-device encrypted event
- visitEach(toDeviceEvents, [this](const EncryptedEvent& ee) {
- if (ee.algorithm() != OlmV1Curve25519AesSha2AlgoKey) {
- qCDebug(E2EE) << "Encrypted event" << ee.id() << "algorithm"
- << ee.algorithm() << "is not supported";
- return;
- }
+ if (!toDeviceEvents.empty()) {
+ qCDebug(E2EE) << "Consuming" << toDeviceEvents.size() << "to-device events";
+ visitEach(toDeviceEvents, [this](const EncryptedEvent& event) {
+ if (event.algorithm() != OlmV1Curve25519AesSha2AlgoKey) {
+ qCDebug(E2EE) << "Unsupported algorithm" << event.id() << "for event" << event.algorithm();
+ return;
+ }
+ const auto decryptedEvent = sessionDecryptMessage(event);
+ if(!decryptedEvent) {
+ qCWarning(E2EE) << "Failed to decrypt event" << event.id();
+ return;
+ }
- // TODO: full maintaining of the device keys
- // with device_lists sync extention and /keys/query
- qCDebug(E2EE) << "Getting device keys for the m.room_key sender:"
- << ee.senderId();
- // encryptionManager->updateDeviceKeys();
-
- switchOnType(*sessionDecryptMessage(ee),
- [this, senderKey = ee.senderKey()](const RoomKeyEvent& roomKeyEvent) {
- if (auto* detectedRoom = q->room(roomKeyEvent.roomId()))
- detectedRoom->handleRoomKeyEvent(roomKeyEvent, senderKey);
- else
- qCDebug(E2EE)
- << "Encrypted event room id" << roomKeyEvent.roomId()
- << "is not found at the connection" << q->objectName();
- },
- [](const Event& evt) {
- qCDebug(E2EE) << "Skipping encrypted to_device event, type"
- << evt.matrixType();
- });
- });
+ switchOnType(*decryptedEvent,
+ [this, senderKey = event.senderKey()](const RoomKeyEvent& roomKeyEvent) {
+ if (auto* detectedRoom = q->room(roomKeyEvent.roomId())) {
+ detectedRoom->handleRoomKeyEvent(roomKeyEvent, senderKey);
+ } else {
+ qCDebug(E2EE) << "Encrypted event room id" << roomKeyEvent.roomId()
+ << "is not found at the connection" << q->objectName();
+ }
+ },
+ [](const Event& evt) {
+ qCDebug(E2EE) << "Skipping encrypted to_device event, type"
+ << evt.matrixType();
+ });
+ });
+ }
+#endif
+}
+
+void Connection::Private::consumeDevicesList(DevicesList&& devicesList)
+{
+#ifdef Quotient_E2EE_ENABLED
+ bool hasNewOutdatedUser = false;
+ for(const auto &changed : devicesList.changed) {
+ if(trackedUsers.contains(changed)) {
+ outdatedUsers += changed;
+ hasNewOutdatedUser = true;
+ }
+ }
+ for(const auto &left : devicesList.left) {
+ trackedUsers -= left;
+ outdatedUsers -= left;
+ deviceKeys.remove(left);
+ }
+ if(hasNewOutdatedUser) {
+ loadOutdatedUserDevices();
+ }
#endif
}
@@ -914,6 +1101,19 @@ DownloadFileJob* Connection::downloadFile(const QUrl& url,
return job;
}
+#ifdef Quotient_E2EE_ENABLED
+DownloadFileJob* Connection::downloadFile(const QUrl& url,
+ const EncryptedFile& file,
+ const QString& localFilename)
+{
+ auto mediaId = url.authority() + url.path();
+ auto idParts = splitMediaId(mediaId);
+ auto* job =
+ callApi<DownloadFileJob>(idParts.front(), idParts.back(), file, localFilename);
+ return job;
+}
+#endif
+
CreateRoomJob*
Connection::createRoom(RoomVisibility visibility, const QString& alias,
const QString& name, const QString& topic,
@@ -1227,9 +1427,9 @@ QByteArray Connection::accessToken() const
bool Connection::isLoggedIn() const { return !accessToken().isEmpty(); }
#ifdef Quotient_E2EE_ENABLED
-QtOlm::Account* Connection::olmAccount() const
+QOlmAccount *Connection::olmAccount() const
{
- return d->encryptionManager->account();
+ return d->olmAccount.get();
}
#endif // Quotient_E2EE_ENABLED
@@ -1772,3 +1972,164 @@ QVector<Connection::SupportedRoomVersion> Connection::availableRoomVersions() co
}
return result;
}
+
+#ifdef Quotient_E2EE_ENABLED
+void Connection::Private::loadOutdatedUserDevices()
+{
+ QHash<QString, QStringList> users;
+ for(const auto &user : outdatedUsers) {
+ users[user] += QStringList();
+ }
+ if(currentQueryKeysJob) {
+ currentQueryKeysJob->abandon();
+ currentQueryKeysJob = nullptr;
+ }
+ auto queryKeysJob = q->callApi<QueryKeysJob>(users);
+ currentQueryKeysJob = queryKeysJob;
+ connect(queryKeysJob, &BaseJob::success, q, [this, queryKeysJob](){
+ currentQueryKeysJob = nullptr;
+ const auto data = queryKeysJob->deviceKeys();
+ for(const auto &[user, keys] : asKeyValueRange(data)) {
+ deviceKeys[user].clear();
+ for(const auto &device : keys) {
+ if(device.userId != user) {
+ qCWarning(E2EE) << "mxId mismatch during device key verification:" << device.userId << user;
+ continue;
+ }
+ if(!device.algorithms.contains("m.olm.v1.curve25519-aes-sha2") || !device.algorithms.contains("m.megolm.v1.aes-sha2")) {
+ qCWarning(E2EE) << "Unsupported encryption algorithms found" << device.algorithms;
+ continue;
+ }
+ if(!verifyIdentitySignature(device, device.deviceId, device.userId)) {
+ qCWarning(E2EE) << "Failed to verify devicekeys signature. Skipping this device";
+ continue;
+ }
+ deviceKeys[user][device.deviceId] = device;
+ }
+ outdatedUsers -= user;
+ }
+ saveDevicesList();
+ });
+}
+
+void Connection::Private::saveDevicesList()
+{
+ q->database()->transaction();
+ auto query = q->database()->prepareQuery(QStringLiteral("DELETE FROM tracked_users"));
+ q->database()->execute(query);
+ query.prepare(QStringLiteral("INSERT INTO tracked_users(matrixId) VALUES(:matrixId);"));
+ for (const auto& user : trackedUsers) {
+ query.bindValue(":matrixId", user);
+ q->database()->execute(query);
+ }
+
+ query.prepare(QStringLiteral("DELETE FROM outdated_users"));
+ q->database()->execute(query);
+ query.prepare(QStringLiteral("INSERT INTO outdated_users(matrixId) VALUES(:matrixId);"));
+ for (const auto& user : outdatedUsers) {
+ query.bindValue(":matrixId", user);
+ q->database()->execute(query);
+ }
+
+ query.prepare(QStringLiteral("INSERT INTO tracked_devices(matrixId, deviceId, curveKeyId, curveKey, edKeyId, edKey) VALUES(:matrixId, :deviceId, :curveKeyId, :curveKey, :edKeyId, :edKey);"));
+ for (const auto& user : deviceKeys.keys()) {
+ for (const auto& device : deviceKeys[user]) {
+ auto keys = device.keys.keys();
+ auto curveKeyId = keys[0].startsWith(QLatin1String("curve")) ? keys[0] : keys[1];
+ auto edKeyId = keys[0].startsWith(QLatin1String("ed")) ? keys[0] : keys[1];
+
+ query.bindValue(":matrixId", user);
+ query.bindValue(":deviceId", device.deviceId);
+ query.bindValue(":curveKeyId", curveKeyId);
+ query.bindValue(":curveKey", device.keys[curveKeyId]);
+ query.bindValue(":edKeyId", edKeyId);
+ query.bindValue(":edKey", device.keys[edKeyId]);
+
+ q->database()->execute(query);
+ }
+ }
+ q->database()->commit();
+}
+
+void Connection::Private::loadDevicesList()
+{
+ auto query = q->database()->prepareQuery(QStringLiteral("SELECT * FROM tracked_users;"));
+ q->database()->execute(query);
+ while(query.next()) {
+ trackedUsers += query.value(0).toString();
+ }
+
+ query = q->database()->prepareQuery(QStringLiteral("SELECT * FROM outdated_users;"));
+ q->database()->execute(query);
+ while(query.next()) {
+ outdatedUsers += query.value(0).toString();
+ }
+
+ query = q->database()->prepareQuery(QStringLiteral("SELECT * FROM tracked_devices;"));
+ q->database()->execute(query);
+ while(query.next()) {
+ deviceKeys[query.value("matrixId").toString()][query.value("deviceId").toString()] = DeviceKeys {
+ query.value("matrixId").toString(),
+ query.value("deviceId").toString(),
+ { "m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"},
+ {{query.value("curveKeyId").toString(), query.value("curveKey").toString()},
+ {query.value("edKeyId").toString(), query.value("edKey").toString()}},
+ {} // Signatures are not saved/loaded as they are not needed after initial validation
+ };
+ }
+
+}
+
+void Connection::encryptionUpdate(Room *room)
+{
+ for(const auto &user : room->users()) {
+ if(!d->trackedUsers.contains(user->id())) {
+ d->trackedUsers += user->id();
+ d->outdatedUsers += user->id();
+ d->encryptionUpdateRequired = true;
+ }
+ }
+}
+
+PicklingMode Connection::picklingMode() const
+{
+ return d->picklingMode;
+}
+#endif
+
+void Connection::saveOlmAccount()
+{
+ qCDebug(E2EE) << "Saving olm account";
+#ifdef Quotient_E2EE_ENABLED
+ auto pickle = d->olmAccount->pickle(d->picklingMode);
+ d->database->setAccountPickle(std::get<QByteArray>(pickle));
+#endif
+}
+
+#ifdef Quotient_E2EE_ENABLED
+QJsonObject Connection::decryptNotification(const QJsonObject &notification)
+{
+ auto room = this->room(notification["room_id"].toString());
+ auto event = makeEvent<EncryptedEvent>(notification["event"].toObject());
+ auto decrypted = room->decryptMessage(*event);
+ if(!decrypted) {
+ return QJsonObject();
+ }
+ return decrypted->fullJson();
+}
+
+Database* Connection::database()
+{
+ return d->database;
+}
+
+UnorderedMap<std::pair<QString, QString>, QOlmInboundGroupSessionPtr> Connection::loadRoomMegolmSessions(Room* room)
+{
+ return database()->loadMegolmSessions(room->id(), picklingMode());
+}
+
+void Connection::saveMegolmSession(Room* room, const QString& senderKey, QOlmInboundGroupSession* session)
+{
+ database()->saveMegolmSession(room->id(), senderKey, session->sessionId(), session->pickle(picklingMode()));
+}
+#endif
diff --git a/lib/connection.h b/lib/connection.h
index dc2eaad1..165d8d68 100644
--- a/lib/connection.h
+++ b/lib/connection.h
@@ -22,9 +22,9 @@
#include <functional>
-namespace QtOlm {
-class Account;
-}
+#ifdef Quotient_E2EE_ENABLED
+#include "e2ee/e2ee.h"
+#endif
Q_DECLARE_METATYPE(Quotient::GetLoginFlowsJob::LoginFlow)
@@ -48,6 +48,11 @@ class DownloadFileJob;
class SendToDeviceJob;
class SendMessageJob;
class LeaveRoomJob;
+class Database;
+struct EncryptedFile;
+
+class QOlmAccount;
+class QOlmInboundGroupSession;
using LoginFlow = GetLoginFlowsJob::LoginFlow;
@@ -310,7 +315,10 @@ public:
QByteArray accessToken() const;
bool isLoggedIn() const;
#ifdef Quotient_E2EE_ENABLED
- QtOlm::Account* olmAccount() const;
+ QOlmAccount* olmAccount() const;
+ Database* database();
+ UnorderedMap<std::pair<QString, QString>, QOlmInboundGroupSessionPtr> loadRoomMegolmSessions(Room* room);
+ void saveMegolmSession(Room* room, const QString& senderKey, QOlmInboundGroupSession* session);
#endif // Quotient_E2EE_ENABLED
Q_INVOKABLE Quotient::SyncJob* syncJob() const;
Q_INVOKABLE int millisToReconnect() const;
@@ -489,6 +497,9 @@ public:
setUserFactory(defaultUserFactory<T>);
}
+ /// Saves the olm account data to disk. Usually doesn't need to be called manually.
+ void saveOlmAccount();
+
public Q_SLOTS:
/// \brief Set the homeserver base URL and retrieve its login flows
///
@@ -576,6 +587,10 @@ public Q_SLOTS:
DownloadFileJob* downloadFile(const QUrl& url,
const QString& localFilename = {});
+#ifdef Quotient_E2EE_ENABLED
+ DownloadFileJob* downloadFile(const QUrl& url, const EncryptedFile& file,
+ const QString& localFilename = {});
+#endif
/**
* \brief Create a room (generic method)
* This method allows to customize room entirely to your liking,
@@ -661,6 +676,11 @@ public Q_SLOTS:
/** \deprecated Do not use this directly, use Room::leaveRoom() instead */
virtual LeaveRoomJob* leaveRoom(Room* room);
+#ifdef Quotient_E2EE_ENABLED
+ void encryptionUpdate(Room *room);
+ PicklingMode picklingMode() const;
+ QJsonObject decryptNotification(const QJsonObject &notification);
+#endif
Q_SIGNALS:
/// \brief Initial server resolution has failed
///
diff --git a/lib/database.cpp b/lib/database.cpp
new file mode 100644
index 00000000..b91b6ef1
--- /dev/null
+++ b/lib/database.cpp
@@ -0,0 +1,244 @@
+// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "database.h"
+
+#include <QtSql/QSqlDatabase>
+#include <QtSql/QSqlQuery>
+#include <QtSql/QSqlError>
+#include <QtCore/QStandardPaths>
+#include <QtCore/QDebug>
+#include <QtCore/QDir>
+
+#include "e2ee/e2ee.h"
+#include "e2ee/qolmsession.h"
+#include "e2ee/qolminboundsession.h"
+
+using namespace Quotient;
+Database::Database(const QString& matrixId, QObject* parent)
+ : QObject(parent)
+ , m_matrixId(matrixId)
+{
+ m_matrixId.replace(':', '_');
+ QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), QStringLiteral("Quotient_%1").arg(m_matrixId));
+ QString databasePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/%1").arg(m_matrixId);
+ QDir(databasePath).mkpath(databasePath);
+ database().setDatabaseName(databasePath + QStringLiteral("/quotient.db3"));
+ database().open();
+
+ switch(version()) {
+ case 0: migrateTo1();
+ }
+}
+
+int Database::version()
+{
+ auto query = execute(QStringLiteral("PRAGMA user_version;"));
+ if (query.next()) {
+ bool ok;
+ int value = query.value(0).toInt(&ok);
+ qCDebug(DATABASE) << "Database version" << value;
+ if (ok)
+ return value;
+ } else {
+ qCritical() << "Failed to check database version";
+ }
+ return -1;
+}
+
+QSqlQuery Database::execute(const QString &queryString)
+{
+ auto query = database().exec(queryString);
+ if (query.lastError().type() != QSqlError::NoError) {
+ qCritical() << "Failed to execute query";
+ qCritical() << query.lastQuery();
+ qCritical() << query.lastError();
+ }
+ return query;
+}
+
+QSqlQuery Database::execute(QSqlQuery &query)
+{
+ if (!query.exec()) {
+ qCritical() << "Failed to execute query";
+ qCritical() << query.lastQuery();
+ qCritical() << query.lastError();
+ }
+ return query;
+}
+
+void Database::transaction()
+{
+ database().transaction();
+}
+
+void Database::commit()
+{
+ database().commit();
+}
+
+void Database::migrateTo1()
+{
+ qCDebug(DATABASE) << "Migrating database to version 1";
+ transaction();
+ execute(QStringLiteral("CREATE TABLE accounts (pickle TEXT);"));
+ execute(QStringLiteral("CREATE TABLE olm_sessions (senderKey TEXT, sessionId TEXT, pickle TEXT);"));
+ execute(QStringLiteral("CREATE TABLE inbound_megolm_sessions (roomId TEXT, senderKey TEXT, sessionId TEXT, pickle TEXT);"));
+ execute(QStringLiteral("CREATE TABLE outbound_megolm_sessions (roomId TEXT, senderKey TEXT, sessionId TEXT, pickle TEXT);"));
+ execute(QStringLiteral("CREATE TABLE group_session_record_index (roomId TEXT, sessionId TEXT, i INTEGER, eventId TEXT, ts INTEGER);"));
+ execute(QStringLiteral("CREATE TABLE tracked_users (matrixId TEXT);"));
+ execute(QStringLiteral("CREATE TABLE outdated_users (matrixId TEXT);"));
+ execute(QStringLiteral("CREATE TABLE tracked_devices (matrixId TEXT, deviceId TEXT, curveKeyId TEXT, curveKey TEXT, edKeyId TEXT, edKey TEXT);"));
+
+ execute(QStringLiteral("PRAGMA user_version = 1;"));
+ commit();
+}
+
+QByteArray Database::accountPickle()
+{
+ auto query = prepareQuery(QStringLiteral("SELECT pickle FROM accounts;"));
+ execute(query);
+ if (query.next()) {
+ return query.value(QStringLiteral("pickle")).toByteArray();
+ }
+ return {};
+}
+
+void Database::setAccountPickle(const QByteArray &pickle)
+{
+ auto deleteQuery = prepareQuery(QStringLiteral("DELETE FROM accounts;"));
+ auto query = prepareQuery(QStringLiteral("INSERT INTO accounts(pickle) VALUES(:pickle);"));
+ query.bindValue(":pickle", pickle);
+ transaction();
+ execute(deleteQuery);
+ execute(query);
+ commit();
+}
+
+void Database::clear()
+{
+ auto query = prepareQuery(QStringLiteral("DELETE FROM accounts;"));
+ auto sessionsQuery = prepareQuery(QStringLiteral("DELETE FROM olm_sessions;"));
+ auto megolmSessionsQuery = prepareQuery(QStringLiteral("DELETE FROM inbound_megolm_sessions;"));
+ auto groupSessionIndexRecordQuery = prepareQuery(QStringLiteral("DELETE FROM group_session_record_index;"));
+
+ transaction();
+ execute(query);
+ execute(sessionsQuery);
+ execute(megolmSessionsQuery);
+ execute(groupSessionIndexRecordQuery);
+ commit();
+
+}
+
+void Database::saveOlmSession(const QString& senderKey, const QString& sessionId, const QByteArray &pickle)
+{
+ auto query = prepareQuery(QStringLiteral("INSERT INTO olm_sessions(senderKey, sessionId, pickle) VALUES(:senderKey, :sessionId, :pickle);"));
+ query.bindValue(":senderKey", senderKey);
+ query.bindValue(":sessionId", sessionId);
+ query.bindValue(":pickle", pickle);
+ transaction();
+ execute(query);
+ commit();
+}
+
+UnorderedMap<QString, std::vector<QOlmSessionPtr>> Database::loadOlmSessions(const PicklingMode& picklingMode)
+{
+ auto query = prepareQuery(QStringLiteral("SELECT * FROM olm_sessions;"));
+ transaction();
+ execute(query);
+ commit();
+ UnorderedMap<QString, std::vector<QOlmSessionPtr>> sessions;
+ while (query.next()) {
+ auto session = QOlmSession::unpickle(query.value("pickle").toByteArray(), picklingMode);
+ if (std::holds_alternative<QOlmError>(session)) {
+ qCWarning(E2EE) << "Failed to unpickle olm session";
+ continue;
+ }
+ sessions[query.value("senderKey").toString()].push_back(std::move(std::get<QOlmSessionPtr>(session)));
+ }
+ return sessions;
+}
+
+UnorderedMap<std::pair<QString, QString>, QOlmInboundGroupSessionPtr> Database::loadMegolmSessions(const QString& roomId, const PicklingMode& picklingMode)
+{
+ auto query = prepareQuery(QStringLiteral("SELECT * FROM inbound_megolm_sessions WHERE roomId=:roomId;"));
+ query.bindValue(":roomId", roomId);
+ transaction();
+ execute(query);
+ commit();
+ UnorderedMap<std::pair<QString, QString>, QOlmInboundGroupSessionPtr> sessions;
+ while (query.next()) {
+ auto session = QOlmInboundGroupSession::unpickle(query.value("pickle").toByteArray(), picklingMode);
+ if (std::holds_alternative<QOlmError>(session)) {
+ qCWarning(E2EE) << "Failed to unpickle megolm session";
+ continue;
+ }
+ sessions[{query.value("senderKey").toString(), query.value("sessionId").toString()}] = std::move(std::get<QOlmInboundGroupSessionPtr>(session));
+ }
+ return sessions;
+}
+
+void Database::saveMegolmSession(const QString& roomId, const QString& senderKey, const QString& sessionId, const QByteArray& pickle)
+{
+ auto query = prepareQuery(QStringLiteral("INSERT INTO inbound_megolm_sessions(roomId, senderKey, sessionId, pickle) VALUES(:roomId, :senderKey, :sessionId, :pickle);"));
+ query.bindValue(":roomId", roomId);
+ query.bindValue(":senderKey", senderKey);
+ query.bindValue(":sessionId", sessionId);
+ query.bindValue(":pickle", pickle);
+ transaction();
+ execute(query);
+ commit();
+}
+
+void Database::addGroupSessionIndexRecord(const QString& roomId, const QString& sessionId, uint32_t index, const QString& eventId, qint64 ts)
+{
+ auto query = prepareQuery("INSERT INTO group_session_record_index(roomId, sessionId, i, eventId, ts) VALUES(:roomId, :sessionId, :index, :eventId, :ts);");
+ query.bindValue(":roomId", roomId);
+ query.bindValue(":sessionId", sessionId);
+ query.bindValue(":index", index);
+ query.bindValue(":eventId", eventId);
+ query.bindValue(":ts", ts);
+ transaction();
+ execute(query);
+ commit();
+}
+
+std::pair<QString, qint64> Database::groupSessionIndexRecord(const QString& roomId, const QString& sessionId, qint64 index)
+{
+ auto query = prepareQuery(QStringLiteral("SELECT * FROM group_session_record_index WHERE roomId=:roomId AND sessionId=:sessionId AND i=:index;"));
+ query.bindValue(":roomId", roomId);
+ query.bindValue(":sessionId", sessionId);
+ query.bindValue(":index", index);
+ transaction();
+ execute(query);
+ commit();
+ if (!query.next()) {
+ return {};
+ }
+ return {query.value("eventId").toString(), query.value("ts").toLongLong()};
+}
+
+QSqlDatabase Database::database()
+{
+ return QSqlDatabase::database(QStringLiteral("Quotient_%1").arg(m_matrixId));
+}
+
+QSqlQuery Database::prepareQuery(const QString& queryString)
+{
+ QSqlQuery query(database());
+ query.prepare(queryString);
+ return query;
+}
+
+void Database::clearRoomData(const QString& roomId)
+{
+ auto query = prepareQuery(QStringLiteral("DELETE FROM inbound_megolm_sessions WHERE roomId=:roomId;"));
+ auto query2 = prepareQuery(QStringLiteral("DELETE FROM outbound_megolm_sessions WHERE roomId=:roomId;"));
+ auto query3 = prepareQuery(QStringLiteral("DELETE FROM group_session_record_index WHERE roomId=:roomId;"));
+ transaction();
+ execute(query);
+ execute(query2);
+ execute(query3);
+ commit();
+}
diff --git a/lib/database.h b/lib/database.h
new file mode 100644
index 00000000..96256a55
--- /dev/null
+++ b/lib/database.h
@@ -0,0 +1,43 @@
+// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include <QtCore/QObject>
+#include <QtSql/QSqlQuery>
+#include <QtCore/QVector>
+
+#include "e2ee/e2ee.h"
+
+namespace Quotient {
+class Database : public QObject
+{
+ Q_OBJECT
+
+public:
+ Database(const QString& matrixId, QObject* parent);
+
+ int version();
+ void transaction();
+ void commit();
+ QSqlQuery execute(const QString &queryString);
+ QSqlQuery execute(QSqlQuery &query);
+ QSqlDatabase database();
+ QSqlQuery prepareQuery(const QString& quaryString);
+
+ QByteArray accountPickle();
+ void setAccountPickle(const QByteArray &pickle);
+ void clear();
+ void saveOlmSession(const QString& senderKey, const QString& sessionId, const QByteArray &pickle);
+ UnorderedMap<QString, std::vector<QOlmSessionPtr>> loadOlmSessions(const PicklingMode& picklingMode);
+ UnorderedMap<std::pair<QString, QString>, QOlmInboundGroupSessionPtr> loadMegolmSessions(const QString& roomId, const PicklingMode& picklingMode);
+ void saveMegolmSession(const QString& roomId, const QString& senderKey, const QString& sessionKey, const QByteArray& pickle);
+ void addGroupSessionIndexRecord(const QString& roomId, const QString& sessionId, uint32_t index, const QString& eventId, qint64 ts);
+ std::pair<QString, qint64> groupSessionIndexRecord(const QString& roomId, const QString& sessionId, qint64 index);
+ void clearRoomData(const QString& roomId);
+
+private:
+ void migrateTo1();
+ QString m_matrixId;
+};
+}
diff --git a/lib/e2ee.h b/lib/e2ee.h
deleted file mode 100644
index 4044aa02..00000000
--- a/lib/e2ee.h
+++ /dev/null
@@ -1,35 +0,0 @@
-// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru>
-// SPDX-FileCopyrightText: 2019 Kitsune Ral <Kitsune-Ral@users.sf.net>
-// SPDX-License-Identifier: LGPL-2.1-or-later
-
-#pragma once
-
-#include "util.h"
-
-#include <QtCore/QStringList>
-
-namespace Quotient {
-inline const auto CiphertextKeyL = "ciphertext"_ls;
-inline const auto SenderKeyKeyL = "sender_key"_ls;
-inline const auto DeviceIdKeyL = "device_id"_ls;
-inline const auto SessionIdKeyL = "session_id"_ls;
-
-inline const auto AlgorithmKeyL = "algorithm"_ls;
-inline const auto RotationPeriodMsKeyL = "rotation_period_ms"_ls;
-inline const auto RotationPeriodMsgsKeyL = "rotation_period_msgs"_ls;
-
-inline const auto AlgorithmKey = QStringLiteral("algorithm");
-inline const auto RotationPeriodMsKey = QStringLiteral("rotation_period_ms");
-inline const auto RotationPeriodMsgsKey =
- QStringLiteral("rotation_period_msgs");
-
-inline const auto Ed25519Key = QStringLiteral("ed25519");
-inline const auto Curve25519Key = QStringLiteral("curve25519");
-inline const auto SignedCurve25519Key = QStringLiteral("signed_curve25519");
-inline const auto OlmV1Curve25519AesSha2AlgoKey =
- QStringLiteral("m.olm.v1.curve25519-aes-sha2");
-inline const auto MegolmV1AesSha2AlgoKey =
- QStringLiteral("m.megolm.v1.aes-sha2");
-inline const QStringList SupportedAlgorithms = { OlmV1Curve25519AesSha2AlgoKey,
- MegolmV1AesSha2AlgoKey };
-} // namespace Quotient
diff --git a/lib/e2ee/e2ee.h b/lib/e2ee/e2ee.h
new file mode 100644
index 00000000..41cd2878
--- /dev/null
+++ b/lib/e2ee/e2ee.h
@@ -0,0 +1,132 @@
+// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru>
+// SPDX-FileCopyrightText: 2019 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include <optional>
+#include <string>
+#include "converters.h"
+#include <variant>
+
+#include <QMap>
+#include <QHash>
+#include <QStringList>
+#include <QMetaType>
+
+#include "util.h"
+
+namespace Quotient {
+
+inline const auto CiphertextKeyL = "ciphertext"_ls;
+inline const auto SenderKeyKeyL = "sender_key"_ls;
+inline const auto DeviceIdKeyL = "device_id"_ls;
+inline const auto SessionIdKeyL = "session_id"_ls;
+
+inline const auto AlgorithmKeyL = "algorithm"_ls;
+inline const auto RotationPeriodMsKeyL = "rotation_period_ms"_ls;
+inline const auto RotationPeriodMsgsKeyL = "rotation_period_msgs"_ls;
+
+inline const auto AlgorithmKey = QStringLiteral("algorithm");
+inline const auto RotationPeriodMsKey = QStringLiteral("rotation_period_ms");
+inline const auto RotationPeriodMsgsKey =
+ QStringLiteral("rotation_period_msgs");
+
+inline const auto Ed25519Key = QStringLiteral("ed25519");
+inline const auto Curve25519Key = QStringLiteral("curve25519");
+inline const auto SignedCurve25519Key = QStringLiteral("signed_curve25519");
+inline const auto OlmV1Curve25519AesSha2AlgoKey =
+ QStringLiteral("m.olm.v1.curve25519-aes-sha2");
+inline const auto MegolmV1AesSha2AlgoKey =
+ QStringLiteral("m.megolm.v1.aes-sha2");
+inline const QStringList SupportedAlgorithms = { OlmV1Curve25519AesSha2AlgoKey,
+ MegolmV1AesSha2AlgoKey };
+struct Unencrypted {};
+struct Encrypted {
+ QByteArray key;
+};
+
+using PicklingMode = std::variant<Unencrypted, Encrypted>;
+
+class QOlmSession;
+using QOlmSessionPtr = std::unique_ptr<QOlmSession>;
+
+class QOlmInboundGroupSession;
+using QOlmInboundGroupSessionPtr = std::unique_ptr<QOlmInboundGroupSession>;
+
+template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
+template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;
+
+struct IdentityKeys
+{
+ QByteArray curve25519;
+ QByteArray ed25519;
+};
+
+//! Struct representing the one-time keys.
+struct OneTimeKeys
+{
+ QMap<QString, QMap<QString, QString>> keys;
+
+ //! Get the HashMap containing the curve25519 one-time keys.
+ QMap<QString, QString> curve25519() const;
+
+ //! Get a reference to the hashmap corresponding to given key type.
+ std::optional<QMap<QString, QString>> get(QString keyType) const;
+};
+
+//! Struct representing the signed one-time keys.
+class SignedOneTimeKey
+{
+public:
+ SignedOneTimeKey() = default;
+ SignedOneTimeKey(const SignedOneTimeKey &) = default;
+ SignedOneTimeKey &operator=(const SignedOneTimeKey &) = default;
+ //! Required. The unpadded Base64-encoded 32-byte Curve25519 public key.
+ QString key;
+
+ //! Required. Signatures of the key object.
+ //! The signature is calculated using the process described at Signing JSON.
+ QHash<QString, QHash<QString, QString>> signatures;
+};
+
+
+template <>
+struct JsonObjectConverter<SignedOneTimeKey> {
+ static void fillFrom(const QJsonObject& jo,
+ SignedOneTimeKey& result)
+ {
+ fromJson(jo.value("key"_ls), result.key);
+ fromJson(jo.value("signatures"_ls), result.signatures);
+ }
+
+ static void dumpTo(QJsonObject &jo, const SignedOneTimeKey &result)
+ {
+ addParam<>(jo, QStringLiteral("key"), result.key);
+ addParam<>(jo, QStringLiteral("signatures"), result.signatures);
+ }
+};
+
+bool operator==(const IdentityKeys& lhs, const IdentityKeys& rhs);
+
+template <typename T>
+class asKeyValueRange
+{
+public:
+ asKeyValueRange(T &data)
+ : m_data{data}
+ {
+ }
+
+ auto begin() { return m_data.keyValueBegin(); }
+
+ auto end() { return m_data.keyValueEnd(); }
+
+private:
+ T &m_data;
+};
+
+} // namespace Quotient
+
+Q_DECLARE_METATYPE(Quotient::SignedOneTimeKey)
diff --git a/lib/e2ee/qolmaccount.cpp b/lib/e2ee/qolmaccount.cpp
new file mode 100644
index 00000000..34ee7ea0
--- /dev/null
+++ b/lib/e2ee/qolmaccount.cpp
@@ -0,0 +1,328 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "qolmaccount.h"
+#include "connection.h"
+#include "csapi/keys.h"
+#include "e2ee/qolmutils.h"
+#include "e2ee/qolmutility.h"
+#include <QJsonObject>
+#include <QJsonDocument>
+#include <QDebug>
+#include <iostream>
+
+using namespace Quotient;
+
+QMap<QString, QString> OneTimeKeys::curve25519() const
+{
+ return keys[QStringLiteral("curve25519")];
+}
+
+std::optional<QMap<QString, QString>> OneTimeKeys::get(QString keyType) const
+{
+ if (!keys.contains(keyType)) {
+ return std::nullopt;
+ }
+ return keys[keyType];
+}
+
+bool operator==(const IdentityKeys& lhs, const IdentityKeys& rhs)
+{
+ return lhs.curve25519 == rhs.curve25519 && lhs.ed25519 == rhs.ed25519;
+}
+
+// Convert olm error to enum
+QOlmError lastError(OlmAccount *account) {
+ return fromString(olm_account_last_error(account));
+}
+
+QByteArray getRandom(size_t bufferSize)
+{
+ QByteArray buffer(bufferSize, '0');
+ std::generate(buffer.begin(), buffer.end(), std::rand);
+ return buffer;
+}
+
+QOlmAccount::QOlmAccount(const QString &userId, const QString &deviceId, QObject *parent)
+ : QObject(parent)
+ , m_userId(userId)
+ , m_deviceId(deviceId)
+{
+}
+
+QOlmAccount::~QOlmAccount()
+{
+ olm_clear_account(m_account);
+ delete[](reinterpret_cast<uint8_t *>(m_account));
+}
+
+void QOlmAccount::createNewAccount()
+{
+ m_account = olm_account(new uint8_t[olm_account_size()]);
+ size_t randomSize = olm_create_account_random_length(m_account);
+ QByteArray randomData = getRandom(randomSize);
+ const auto error = olm_create_account(m_account, randomData.data(), randomSize);
+ if (error == olm_error()) {
+ throw lastError(m_account);
+ }
+ Q_EMIT needsSave();
+}
+
+void QOlmAccount::unpickle(QByteArray &pickled, const PicklingMode &mode)
+{
+ m_account = olm_account(new uint8_t[olm_account_size()]);
+ const QByteArray key = toKey(mode);
+ const auto error = olm_unpickle_account(m_account, key.data(), key.length(), pickled.data(), pickled.size());
+ if (error == olm_error()) {
+ qCWarning(E2EE) << "Failed to unpickle olm account";
+ //TODO: Do something that is not dying
+ // Probably log the user out since we have no way of getting to the keys
+ //throw lastError(m_account);
+ }
+}
+
+std::variant<QByteArray, QOlmError> QOlmAccount::pickle(const PicklingMode &mode)
+{
+ const QByteArray key = toKey(mode);
+ const size_t pickleLength = olm_pickle_account_length(m_account);
+ QByteArray pickleBuffer(pickleLength, '0');
+ const auto error = olm_pickle_account(m_account, key.data(),
+ key.length(), pickleBuffer.data(), pickleLength);
+ if (error == olm_error()) {
+ return lastError(m_account);
+ }
+ return pickleBuffer;
+}
+
+IdentityKeys QOlmAccount::identityKeys() const
+{
+ const size_t keyLength = olm_account_identity_keys_length(m_account);
+ QByteArray keyBuffer(keyLength, '0');
+ const auto error = olm_account_identity_keys(m_account, keyBuffer.data(), keyLength);
+ if (error == olm_error()) {
+ throw lastError(m_account);
+ }
+ const QJsonObject key = QJsonDocument::fromJson(keyBuffer).object();
+ return IdentityKeys {
+ key.value(QStringLiteral("curve25519")).toString().toUtf8(),
+ key.value(QStringLiteral("ed25519")).toString().toUtf8()
+ };
+}
+
+QByteArray QOlmAccount::sign(const QByteArray &message) const
+{
+ QByteArray signatureBuffer(olm_account_signature_length(m_account), '0');
+
+ const auto error = olm_account_sign(m_account, message.data(), message.length(),
+ signatureBuffer.data(), signatureBuffer.length());
+
+ if (error == olm_error()) {
+ throw lastError(m_account);
+ }
+ return signatureBuffer;
+}
+
+QByteArray QOlmAccount::sign(const QJsonObject &message) const
+{
+ return sign(QJsonDocument(message).toJson(QJsonDocument::Compact));
+}
+
+QByteArray QOlmAccount::signIdentityKeys() const
+{
+ const auto keys = identityKeys();
+ QJsonObject body
+ {
+ {"algorithms", QJsonArray{"m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"}},
+ {"user_id", m_userId},
+ {"device_id", m_deviceId},
+ {"keys",
+ QJsonObject{
+ {QStringLiteral("curve25519:") + m_deviceId, QString::fromUtf8(keys.curve25519)},
+ {QStringLiteral("ed25519:") + m_deviceId, QString::fromUtf8(keys.ed25519)}
+ }
+ }
+ };
+ return sign(QJsonDocument(body).toJson(QJsonDocument::Compact));
+
+}
+
+size_t QOlmAccount::maxNumberOfOneTimeKeys() const
+{
+ return olm_account_max_number_of_one_time_keys(m_account);
+}
+
+size_t QOlmAccount::generateOneTimeKeys(size_t numberOfKeys) const
+{
+ const size_t randomLength = olm_account_generate_one_time_keys_random_length(m_account, numberOfKeys);
+ QByteArray randomBuffer = getRandom(randomLength);
+ const auto error = olm_account_generate_one_time_keys(m_account, numberOfKeys, randomBuffer.data(), randomLength);
+
+ if (error == olm_error()) {
+ throw lastError(m_account);
+ }
+ Q_EMIT needsSave();
+ return error;
+}
+
+OneTimeKeys QOlmAccount::oneTimeKeys() const
+{
+ const size_t oneTimeKeyLength = olm_account_one_time_keys_length(m_account);
+ QByteArray oneTimeKeysBuffer(oneTimeKeyLength, '0');
+
+ const auto error = olm_account_one_time_keys(m_account, oneTimeKeysBuffer.data(), oneTimeKeyLength);
+ if (error == olm_error()) {
+ throw lastError(m_account);
+ }
+ const auto json = QJsonDocument::fromJson(oneTimeKeysBuffer).object();
+ OneTimeKeys oneTimeKeys;
+
+ for (const QString& key1 : json.keys()) {
+ auto oneTimeKeyObject = json[key1].toObject();
+ auto keyMap = QMap<QString, QString>();
+ for (const QString &key2 : oneTimeKeyObject.keys()) {
+ keyMap[key2] = oneTimeKeyObject[key2].toString();
+ }
+ oneTimeKeys.keys[key1] = keyMap;
+ }
+ return oneTimeKeys;
+}
+
+QMap<QString, SignedOneTimeKey> QOlmAccount::signOneTimeKeys(const OneTimeKeys &keys) const
+{
+ QMap<QString, SignedOneTimeKey> signedOneTimeKeys;
+ for (const auto &keyid : keys.curve25519().keys()) {
+ const auto oneTimeKey = keys.curve25519()[keyid];
+ QByteArray sign = signOneTimeKey(oneTimeKey);
+ signedOneTimeKeys["signed_curve25519:" + keyid] = signedOneTimeKey(oneTimeKey.toUtf8(), sign);
+ }
+ return signedOneTimeKeys;
+}
+
+SignedOneTimeKey QOlmAccount::signedOneTimeKey(const QByteArray &key, const QString &signature) const
+{
+ SignedOneTimeKey sign{};
+ sign.key = key;
+ sign.signatures = {{m_userId, {{"ed25519:" + m_deviceId, signature}}}};
+ return sign;
+}
+
+QByteArray QOlmAccount::signOneTimeKey(const QString &key) const
+{
+ QJsonDocument j(QJsonObject{{"key", key}});
+ return sign(j.toJson(QJsonDocument::Compact));
+}
+
+std::optional<QOlmError> QOlmAccount::removeOneTimeKeys(const QOlmSessionPtr &session) const
+{
+ const auto error = olm_remove_one_time_keys(m_account, session->raw());
+
+ if (error == olm_error()) {
+ return lastError(m_account);
+ }
+ Q_EMIT needsSave();
+ return std::nullopt;
+}
+
+OlmAccount *QOlmAccount::data()
+{
+ return m_account;
+}
+
+DeviceKeys QOlmAccount::deviceKeys() const
+{
+ DeviceKeys deviceKeys;
+ deviceKeys.userId = m_userId;
+ deviceKeys.deviceId = m_deviceId;
+ deviceKeys.algorithms = QStringList {"m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"};
+
+ const auto idKeys = identityKeys();
+ deviceKeys.keys["curve25519:" + m_deviceId] = idKeys.curve25519;
+ deviceKeys.keys["ed25519:" + m_deviceId] = idKeys.ed25519;
+
+ const auto sign = signIdentityKeys();
+ deviceKeys.signatures[m_userId]["ed25519:" + m_deviceId] = sign;
+
+ return deviceKeys;
+}
+
+UploadKeysJob *QOlmAccount::createUploadKeyRequest(const OneTimeKeys &oneTimeKeys)
+{
+ auto keys = deviceKeys();
+
+ if (oneTimeKeys.curve25519().isEmpty()) {
+ return new UploadKeysJob(keys);
+ }
+
+ // Sign & append the one time keys.
+ auto temp = signOneTimeKeys(oneTimeKeys);
+ QHash<QString, QVariant> oneTimeKeysSigned;
+ for (const auto &[keyId, key] : asKeyValueRange(temp)) {
+ oneTimeKeysSigned[keyId] = QVariant::fromValue(toJson(key));
+ }
+
+ return new UploadKeysJob(keys, oneTimeKeysSigned);
+}
+
+std::variant<QOlmSessionPtr, QOlmError> QOlmAccount::createInboundSession(const QOlmMessage &preKeyMessage)
+{
+ Q_ASSERT(preKeyMessage.type() == QOlmMessage::PreKey);
+ return QOlmSession::createInboundSession(this, preKeyMessage);
+}
+
+std::variant<QOlmSessionPtr, QOlmError> QOlmAccount::createInboundSessionFrom(const QByteArray &theirIdentityKey, const QOlmMessage &preKeyMessage)
+{
+ Q_ASSERT(preKeyMessage.type() == QOlmMessage::PreKey);
+ return QOlmSession::createInboundSessionFrom(this, theirIdentityKey, preKeyMessage);
+}
+
+std::variant<QOlmSessionPtr, QOlmError> QOlmAccount::createOutboundSession(const QByteArray &theirIdentityKey, const QByteArray &theirOneTimeKey)
+{
+ return QOlmSession::createOutboundSession(this, theirIdentityKey, theirOneTimeKey);
+}
+
+void QOlmAccount::markKeysAsPublished()
+{
+ olm_account_mark_keys_as_published(m_account);
+ Q_EMIT needsSave();
+}
+
+bool Quotient::verifyIdentitySignature(const DeviceKeys &deviceKeys,
+ const QString &deviceId,
+ const QString &userId)
+{
+ const auto signKeyId = "ed25519:" + deviceId;
+ const auto signingKey = deviceKeys.keys[signKeyId];
+ const auto signature = deviceKeys.signatures[userId][signKeyId];
+
+ if (signature.isEmpty()) {
+ return false;
+ }
+
+ return ed25519VerifySignature(signingKey, toJson(deviceKeys), signature);
+}
+
+bool Quotient::ed25519VerifySignature(const QString &signingKey,
+ const QJsonObject &obj,
+ const QString &signature)
+{
+ if (signature.isEmpty()) {
+ return false;
+ }
+ QJsonObject obj1 = obj;
+
+ obj1.remove("unsigned");
+ obj1.remove("signatures");
+
+ auto canonicalJson = QJsonDocument(obj1).toJson(QJsonDocument::Compact);
+
+ QByteArray signingKeyBuf = signingKey.toUtf8();
+ QOlmUtility utility;
+ auto signatureBuf = signature.toUtf8();
+ auto result = utility.ed25519Verify(signingKeyBuf, canonicalJson, signatureBuf);
+ if (std::holds_alternative<QOlmError>(result)) {
+ return false;
+ }
+
+ return std::get<bool>(result);
+}
diff --git a/lib/e2ee/qolmaccount.h b/lib/e2ee/qolmaccount.h
new file mode 100644
index 00000000..00afc0e6
--- /dev/null
+++ b/lib/e2ee/qolmaccount.h
@@ -0,0 +1,123 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+
+#pragma once
+
+#include "csapi/keys.h"
+#include "e2ee/e2ee.h"
+#include "e2ee/qolmerrors.h"
+#include "e2ee/qolmmessage.h"
+#include "e2ee/qolmsession.h"
+#include <QObject>
+
+struct OlmAccount;
+
+namespace Quotient {
+
+class QOlmSession;
+class Connection;
+
+using QOlmSessionPtr = std::unique_ptr<QOlmSession>;
+
+//! An olm account manages all cryptographic keys used on a device.
+//! \code{.cpp}
+//! const auto olmAccount = new QOlmAccount(this);
+//! \endcode
+class QOlmAccount : public QObject
+{
+ Q_OBJECT
+public:
+ QOlmAccount(const QString &userId, const QString &deviceId, QObject *parent = nullptr);
+ ~QOlmAccount();
+
+ //! Creates a new instance of OlmAccount. During the instantiation
+ //! the Ed25519 fingerprint key pair and the Curve25519 identity key
+ //! pair are generated. For more information see <a
+ //! href="https://matrix.org/docs/guides/e2e_implementation.html#keys-used-in-end-to-end-encryption">here</a>.
+ //! This needs to be called before any other action or use unpickle() instead.
+ void createNewAccount();
+
+ //! Deserialises from encrypted Base64 that was previously obtained by pickling a `QOlmAccount`.
+ //! This needs to be called before any other action or use createNewAccount() instead.
+ void unpickle(QByteArray &pickled, const PicklingMode &mode);
+
+ //! Serialises an OlmAccount to encrypted Base64.
+ std::variant<QByteArray, QOlmError> pickle(const PicklingMode &mode);
+
+ //! Returns the account's public identity keys already formatted as JSON
+ IdentityKeys identityKeys() const;
+
+ //! Returns the signature of the supplied message.
+ QByteArray sign(const QByteArray &message) const;
+ QByteArray sign(const QJsonObject& message) const;
+
+ //! Sign identity keys.
+ QByteArray signIdentityKeys() const;
+
+ //! Maximum number of one time keys that this OlmAccount can
+ //! currently hold.
+ size_t maxNumberOfOneTimeKeys() const;
+
+ //! Generates the supplied number of one time keys.
+ size_t generateOneTimeKeys(size_t numberOfKeys) const;
+
+ //! Gets the OlmAccount's one time keys formatted as JSON.
+ OneTimeKeys oneTimeKeys() const;
+
+ //! Sign all one time keys.
+ QMap<QString, SignedOneTimeKey> signOneTimeKeys(const OneTimeKeys &keys) const;
+
+ //! Sign one time key.
+ QByteArray signOneTimeKey(const QString &key) const;
+
+ SignedOneTimeKey signedOneTimeKey(const QByteArray &key, const QString &signature) const;
+
+ UploadKeysJob *createUploadKeyRequest(const OneTimeKeys &oneTimeKeys);
+
+ DeviceKeys deviceKeys() const;
+
+ //! Remove the one time key used to create the supplied session.
+ [[nodiscard]] std::optional<QOlmError> removeOneTimeKeys(const QOlmSessionPtr &session) const;
+
+ //! Creates an inbound session for sending/receiving messages from a received 'prekey' message.
+ //!
+ //! \param message An Olm pre-key message that was encrypted for this account.
+ std::variant<QOlmSessionPtr, QOlmError> createInboundSession(const QOlmMessage &preKeyMessage);
+
+ //! Creates an inbound session for sending/receiving messages from a received 'prekey' message.
+ //!
+ //! \param theirIdentityKey - The identity key of the Olm account that
+ //! encrypted this Olm message.
+ std::variant<QOlmSessionPtr, QOlmError> createInboundSessionFrom(const QByteArray &theirIdentityKey, const QOlmMessage &preKeyMessage);
+
+ //! Creates an outbound session for sending messages to a specific
+ /// identity and one time key.
+ std::variant<QOlmSessionPtr, QOlmError> createOutboundSession(const QByteArray &theirIdentityKey, const QByteArray &theirOneTimeKey);
+
+ void markKeysAsPublished();
+
+ // HACK do not use directly
+ QOlmAccount(OlmAccount *account);
+ OlmAccount *data();
+
+Q_SIGNALS:
+ void needsSave() const;
+
+private:
+ OlmAccount *m_account = nullptr; // owning
+ QString m_userId;
+ QString m_deviceId;
+};
+
+bool verifyIdentitySignature(const DeviceKeys &deviceKeys,
+ const QString &deviceId,
+ const QString &userId);
+
+//! checks if the signature is signed by the signing_key
+bool ed25519VerifySignature(const QString &signingKey,
+ const QJsonObject &obj,
+ const QString &signature);
+
+} // namespace Quotient
diff --git a/lib/e2ee/qolmerrors.cpp b/lib/e2ee/qolmerrors.cpp
new file mode 100644
index 00000000..5a60b7e6
--- /dev/null
+++ b/lib/e2ee/qolmerrors.cpp
@@ -0,0 +1,25 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+
+#include "qolmerrors.h"
+#include "util.h"
+#include <QtCore/QLatin1String>
+
+Quotient::QOlmError Quotient::fromString(const char* error_raw) {
+ const QLatin1String error { error_raw };
+ if (error_raw == "BAD_ACCOUNT_KEY"_ls) {
+ return QOlmError::BadAccountKey;
+ } else if (error_raw == "BAD_MESSAGE_KEY_ID"_ls) {
+ return QOlmError::BadMessageKeyId;
+ } else if (error_raw == "INVALID_BASE64"_ls) {
+ return QOlmError::InvalidBase64;
+ } else if (error_raw == "NOT_ENOUGH_RANDOM"_ls) {
+ return QOlmError::NotEnoughRandom;
+ } else if (error_raw == "OUTPUT_BUFFER_TOO_SMALL"_ls) {
+ return QOlmError::OutputBufferTooSmall;
+ } else {
+ return QOlmError::Unknown;
+ }
+}
diff --git a/lib/e2ee/qolmerrors.h b/lib/e2ee/qolmerrors.h
new file mode 100644
index 00000000..24e87d95
--- /dev/null
+++ b/lib/e2ee/qolmerrors.h
@@ -0,0 +1,26 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+namespace Quotient {
+//! All errors that could be caused by an operation regarding Olm
+//! Errors are named exactly like the ones in libolm.
+enum QOlmError
+{
+ BadAccountKey,
+ BadMessageFormat,
+ BadMessageKeyId,
+ BadMessageMac,
+ BadMessageVersion,
+ InvalidBase64,
+ NotEnoughRandom,
+ OutputBufferTooSmall,
+ UnknownMessageIndex,
+ Unknown,
+};
+
+QOlmError fromString(const char* error_raw);
+
+} //namespace Quotient
diff --git a/lib/e2ee/qolminboundsession.cpp b/lib/e2ee/qolminboundsession.cpp
new file mode 100644
index 00000000..2e9cc716
--- /dev/null
+++ b/lib/e2ee/qolminboundsession.cpp
@@ -0,0 +1,151 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "e2ee/qolminboundsession.h"
+#include <iostream>
+#include <cstring>
+
+using namespace Quotient;
+
+QOlmError lastError(OlmInboundGroupSession *session) {
+ return fromString(olm_inbound_group_session_last_error(session));
+}
+
+QOlmInboundGroupSession::QOlmInboundGroupSession(OlmInboundGroupSession *session)
+ : m_groupSession(session)
+{
+}
+
+QOlmInboundGroupSession::~QOlmInboundGroupSession()
+{
+ olm_clear_inbound_group_session(m_groupSession);
+ //delete[](reinterpret_cast<uint8_t *>(m_groupSession));
+}
+
+std::unique_ptr<QOlmInboundGroupSession> QOlmInboundGroupSession::create(const QByteArray &key)
+{
+ const auto olmInboundGroupSession = olm_inbound_group_session(new uint8_t[olm_inbound_group_session_size()]);
+ const auto error = olm_init_inbound_group_session(olmInboundGroupSession,
+ reinterpret_cast<const uint8_t *>(key.constData()), key.size());
+
+ if (error == olm_error()) {
+ throw lastError(olmInboundGroupSession);
+ }
+
+ return std::make_unique<QOlmInboundGroupSession>(olmInboundGroupSession);
+}
+
+std::unique_ptr<QOlmInboundGroupSession> QOlmInboundGroupSession::import(const QByteArray &key)
+{
+ const auto olmInboundGroupSession = olm_inbound_group_session(new uint8_t[olm_inbound_group_session_size()]);
+ QByteArray keyBuf = key;
+
+ const auto error = olm_import_inbound_group_session(olmInboundGroupSession,
+ reinterpret_cast<const uint8_t *>(keyBuf.data()), keyBuf.size());
+ if (error == olm_error()) {
+ throw lastError(olmInboundGroupSession);
+ }
+
+ return std::make_unique<QOlmInboundGroupSession>(olmInboundGroupSession);
+}
+
+QByteArray toKey(const PicklingMode &mode)
+{
+ if (std::holds_alternative<Unencrypted>(mode)) {
+ return "";
+ }
+ return std::get<Encrypted>(mode).key;
+}
+
+QByteArray QOlmInboundGroupSession::pickle(const PicklingMode &mode) const
+{
+ QByteArray pickledBuf(olm_pickle_inbound_group_session_length(m_groupSession), '0');
+ const QByteArray key = toKey(mode);
+ const auto error = olm_pickle_inbound_group_session(m_groupSession, key.data(), key.length(), pickledBuf.data(),
+ pickledBuf.length());
+ if (error == olm_error()) {
+ throw lastError(m_groupSession);
+ }
+ return pickledBuf;
+}
+
+std::variant<std::unique_ptr<QOlmInboundGroupSession>, QOlmError> QOlmInboundGroupSession::unpickle(const QByteArray &pickled, const PicklingMode &mode)
+{
+ QByteArray pickledBuf = pickled;
+ const auto groupSession = olm_inbound_group_session(new uint8_t[olm_inbound_group_session_size()]);
+ QByteArray key = toKey(mode);
+ const auto error = olm_unpickle_inbound_group_session(groupSession, key.data(), key.length(),
+ pickledBuf.data(), pickledBuf.size());
+ if (error == olm_error()) {
+ return lastError(groupSession);
+ }
+ key.clear();
+
+ return std::make_unique<QOlmInboundGroupSession>(groupSession);
+}
+
+std::variant<std::pair<QString, uint32_t>, QOlmError> QOlmInboundGroupSession::decrypt(const QByteArray &message)
+{
+ // This is for capturing the output of olm_group_decrypt
+ uint32_t messageIndex = 0;
+
+ // We need to clone the message because
+ // olm_decrypt_max_plaintext_length destroys the input buffer
+ QByteArray messageBuf(message.length(), '0');
+ std::copy(message.begin(), message.end(), messageBuf.begin());
+
+ QByteArray plaintextBuf(olm_group_decrypt_max_plaintext_length(m_groupSession,
+ reinterpret_cast<uint8_t *>(messageBuf.data()), messageBuf.length()), '0');
+
+ messageBuf = QByteArray(message.length(), '0');
+ std::copy(message.begin(), message.end(), messageBuf.begin());
+
+ const auto plaintextLen = olm_group_decrypt(m_groupSession, reinterpret_cast<uint8_t *>(messageBuf.data()),
+ messageBuf.length(), reinterpret_cast<uint8_t *>(plaintextBuf.data()), plaintextBuf.length(), &messageIndex);
+
+ // Error code or plaintext length is returned
+ const auto decryptError = plaintextLen;
+
+ if (decryptError == olm_error()) {
+ return lastError(m_groupSession);
+ }
+
+ QByteArray output(plaintextLen, '0');
+ std::memcpy(output.data(), plaintextBuf.data(), plaintextLen);
+
+ return std::make_pair<QString, qint32>(QString(output), messageIndex);
+}
+
+std::variant<QByteArray, QOlmError> QOlmInboundGroupSession::exportSession(uint32_t messageIndex)
+{
+ const auto keyLength = olm_export_inbound_group_session_length(m_groupSession);
+ QByteArray keyBuf(keyLength, '0');
+ const auto error = olm_export_inbound_group_session(m_groupSession, reinterpret_cast<uint8_t *>(keyBuf.data()), keyLength, messageIndex);
+
+ if (error == olm_error()) {
+ return lastError(m_groupSession);
+ }
+ return keyBuf;
+}
+
+uint32_t QOlmInboundGroupSession::firstKnownIndex() const
+{
+ return olm_inbound_group_session_first_known_index(m_groupSession);
+}
+
+QByteArray QOlmInboundGroupSession::sessionId() const
+{
+ QByteArray sessionIdBuf(olm_inbound_group_session_id_length(m_groupSession), '0');
+ const auto error = olm_inbound_group_session_id(m_groupSession, reinterpret_cast<uint8_t *>(sessionIdBuf.data()),
+ sessionIdBuf.length());
+ if (error == olm_error()) {
+ throw lastError(m_groupSession);
+ }
+ return sessionIdBuf;
+}
+
+bool QOlmInboundGroupSession::isVerified() const
+{
+ return olm_inbound_group_session_is_verified(m_groupSession) != 0;
+}
diff --git a/lib/e2ee/qolminboundsession.h b/lib/e2ee/qolminboundsession.h
new file mode 100644
index 00000000..7d52991c
--- /dev/null
+++ b/lib/e2ee/qolminboundsession.h
@@ -0,0 +1,48 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include <QByteArray>
+#include <variant>
+#include <memory>
+#include "olm/olm.h"
+#include "e2ee/qolmerrors.h"
+#include "e2ee/e2ee.h"
+
+namespace Quotient {
+
+//! An in-bound group session is responsible for decrypting incoming
+//! communication in a Megolm session.
+struct QOlmInboundGroupSession
+{
+public:
+ ~QOlmInboundGroupSession();
+ //! Creates a new instance of `OlmInboundGroupSession`.
+ static std::unique_ptr<QOlmInboundGroupSession> create(const QByteArray &key);
+ //! Import an inbound group session, from a previous export.
+ static std::unique_ptr<QOlmInboundGroupSession> import(const QByteArray &key);
+ //! Serialises an `OlmInboundGroupSession` to encrypted Base64.
+ QByteArray pickle(const PicklingMode &mode) const;
+ //! Deserialises from encrypted Base64 that was previously obtained by pickling
+ //! an `OlmInboundGroupSession`.
+ static std::variant<std::unique_ptr<QOlmInboundGroupSession>, QOlmError> unpickle(const QByteArray &picked, const PicklingMode &mode);
+ //! Decrypts ciphertext received for this group session.
+ std::variant<std::pair<QString, uint32_t>, QOlmError> decrypt(const QByteArray &message);
+ //! Export the base64-encoded ratchet key for this session, at the given index,
+ //! in a format which can be used by import.
+ std::variant<QByteArray, QOlmError> exportSession(uint32_t messageIndex);
+ //! Get the first message index we know how to decrypt.
+ uint32_t firstKnownIndex() const;
+ //! Get a base64-encoded identifier for this session.
+ QByteArray sessionId() const;
+ bool isVerified() const;
+ QOlmInboundGroupSession(OlmInboundGroupSession *session);
+private:
+ OlmInboundGroupSession *m_groupSession;
+};
+
+using QOlmInboundGroupSessionPtr = std::unique_ptr<QOlmInboundGroupSession>;
+using OlmInboundGroupSessionPtr = std::unique_ptr<OlmInboundGroupSession>;
+} // namespace Quotient
diff --git a/lib/e2ee/qolmmessage.cpp b/lib/e2ee/qolmmessage.cpp
new file mode 100644
index 00000000..15008b75
--- /dev/null
+++ b/lib/e2ee/qolmmessage.cpp
@@ -0,0 +1,35 @@
+// SPDX-FileCopyrightText: 2021 Alexey Andreyev <aa13q@ya.ru>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "qolmmessage.h"
+
+using namespace Quotient;
+
+QOlmMessage::QOlmMessage(const QByteArray &ciphertext, QOlmMessage::Type type)
+ : QByteArray(std::move(ciphertext))
+ , m_messageType(type)
+{
+ Q_ASSERT_X(!ciphertext.isEmpty(), "olm message", "Ciphertext is empty");
+}
+
+QOlmMessage::QOlmMessage(const QOlmMessage &message)
+ : QByteArray(message)
+ , m_messageType(message.type())
+{
+}
+
+QOlmMessage::Type QOlmMessage::type() const
+{
+ return m_messageType;
+}
+
+QByteArray QOlmMessage::toCiphertext() const
+{
+ return QByteArray(*this);
+}
+
+QOlmMessage QOlmMessage::fromCiphertext(const QByteArray &ciphertext)
+{
+ return QOlmMessage(ciphertext, QOlmMessage::General);
+}
diff --git a/lib/e2ee/qolmmessage.h b/lib/e2ee/qolmmessage.h
new file mode 100644
index 00000000..52aba78c
--- /dev/null
+++ b/lib/e2ee/qolmmessage.h
@@ -0,0 +1,41 @@
+// SPDX-FileCopyrightText: 2021 Alexey Andreyev <aa13q@ya.ru>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include <QObject>
+#include <QByteArray>
+
+namespace Quotient {
+
+/*! \brief A wrapper around an olm encrypted message
+ *
+ * This class encapsulates a Matrix olm encrypted message,
+ * passed in either of 2 forms: a general message or a pre-key message.
+ *
+ * The class provides functions to get a type and the ciphertext.
+ */
+class QOlmMessage : public QByteArray {
+ Q_GADGET
+public:
+ enum Type {
+ General,
+ PreKey,
+ };
+ Q_ENUM(Type)
+
+ QOlmMessage() = default;
+ explicit QOlmMessage(const QByteArray &ciphertext, Type type = General);
+ explicit QOlmMessage(const QOlmMessage &message);
+
+ static QOlmMessage fromCiphertext(const QByteArray &ciphertext);
+
+ Q_INVOKABLE Type type() const;
+ Q_INVOKABLE QByteArray toCiphertext() const;
+
+private:
+ Type m_messageType = General;
+};
+
+} //namespace Quotient
diff --git a/lib/e2ee/qolmoutboundsession.cpp b/lib/e2ee/qolmoutboundsession.cpp
new file mode 100644
index 00000000..da32417b
--- /dev/null
+++ b/lib/e2ee/qolmoutboundsession.cpp
@@ -0,0 +1,125 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "e2ee/qolmoutboundsession.h"
+#include "e2ee/qolmutils.h"
+
+using namespace Quotient;
+
+QOlmError lastError(OlmOutboundGroupSession *session) {
+ return fromString(olm_outbound_group_session_last_error(session));
+}
+
+QOlmOutboundGroupSession::QOlmOutboundGroupSession(OlmOutboundGroupSession *session)
+ : m_groupSession(session)
+{
+}
+
+QOlmOutboundGroupSession::~QOlmOutboundGroupSession()
+{
+ olm_clear_outbound_group_session(m_groupSession);
+ delete[](reinterpret_cast<uint8_t *>(m_groupSession));
+}
+
+std::unique_ptr<QOlmOutboundGroupSession> QOlmOutboundGroupSession::create()
+{
+ auto *olmOutboundGroupSession = olm_outbound_group_session(new uint8_t[olm_outbound_group_session_size()]);
+ const auto randomLength = olm_init_outbound_group_session_random_length(olmOutboundGroupSession);
+ QByteArray randomBuf = getRandom(randomLength);
+
+ const auto error = olm_init_outbound_group_session(olmOutboundGroupSession,
+ reinterpret_cast<uint8_t *>(randomBuf.data()), randomBuf.length());
+
+ if (error == olm_error()) {
+ throw lastError(olmOutboundGroupSession);
+ }
+
+ const auto keyMaxLength = olm_outbound_group_session_key_length(olmOutboundGroupSession);
+ QByteArray keyBuffer(keyMaxLength, '0');
+ olm_outbound_group_session_key(olmOutboundGroupSession, reinterpret_cast<uint8_t *>(keyBuffer.data()),
+ keyMaxLength);
+
+ randomBuf.clear();
+
+ return std::make_unique<QOlmOutboundGroupSession>(olmOutboundGroupSession);
+}
+
+std::variant<QByteArray, QOlmError> QOlmOutboundGroupSession::pickle(const PicklingMode &mode)
+{
+ QByteArray pickledBuf(olm_pickle_outbound_group_session_length(m_groupSession), '0');
+ QByteArray key = toKey(mode);
+ const auto error = olm_pickle_outbound_group_session(m_groupSession, key.data(), key.length(),
+ pickledBuf.data(), pickledBuf.length());
+
+ if (error == olm_error()) {
+ return lastError(m_groupSession);
+ }
+
+ key.clear();
+
+ return pickledBuf;
+}
+
+std::variant<std::unique_ptr<QOlmOutboundGroupSession>, QOlmError> QOlmOutboundGroupSession::unpickle(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());
+ if (error == olm_error()) {
+ return lastError(olmOutboundGroupSession);
+ }
+ const auto idMaxLength = olm_outbound_group_session_id_length(olmOutboundGroupSession);
+ QByteArray idBuffer(idMaxLength, '0');
+ olm_outbound_group_session_id(olmOutboundGroupSession, reinterpret_cast<uint8_t *>(idBuffer.data()),
+ idBuffer.length());
+
+ key.clear();
+ return std::make_unique<QOlmOutboundGroupSession>(olmOutboundGroupSession);
+}
+
+std::variant<QByteArray, QOlmError> QOlmOutboundGroupSession::encrypt(const QString &plaintext)
+{
+ QByteArray plaintextBuf = plaintext.toUtf8();
+ const auto messageMaxLength = olm_group_encrypt_message_length(m_groupSession, plaintextBuf.length());
+ QByteArray messageBuf(messageMaxLength, '0');
+ const auto error = olm_group_encrypt(m_groupSession, reinterpret_cast<uint8_t *>(plaintextBuf.data()),
+ plaintextBuf.length(), reinterpret_cast<uint8_t *>(messageBuf.data()), messageBuf.length());
+
+ if (error == olm_error()) {
+ return lastError(m_groupSession);
+ }
+
+ return messageBuf;
+}
+
+uint32_t QOlmOutboundGroupSession::sessionMessageIndex() const
+{
+ return olm_outbound_group_session_message_index(m_groupSession);
+}
+
+QByteArray QOlmOutboundGroupSession::sessionId() const
+{
+ const auto idMaxLength = olm_outbound_group_session_id_length(m_groupSession);
+ QByteArray idBuffer(idMaxLength, '0');
+ const auto error = olm_outbound_group_session_id(m_groupSession, reinterpret_cast<uint8_t *>(idBuffer.data()),
+ idBuffer.length());
+ if (error == olm_error()) {
+ throw lastError(m_groupSession);
+ }
+ return idBuffer;
+}
+
+std::variant<QByteArray, QOlmError> QOlmOutboundGroupSession::sessionKey() const
+{
+ const auto keyMaxLength = olm_outbound_group_session_key_length(m_groupSession);
+ QByteArray keyBuffer(keyMaxLength, '0');
+ const auto error = olm_outbound_group_session_key(m_groupSession, reinterpret_cast<uint8_t *>(keyBuffer.data()),
+ keyMaxLength);
+ if (error == olm_error()) {
+ return lastError(m_groupSession);
+ }
+ return keyBuffer;
+}
diff --git a/lib/e2ee/qolmoutboundsession.h b/lib/e2ee/qolmoutboundsession.h
new file mode 100644
index 00000000..39263c77
--- /dev/null
+++ b/lib/e2ee/qolmoutboundsession.h
@@ -0,0 +1,52 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include "olm/olm.h"
+#include "e2ee/qolmerrors.h"
+#include "e2ee/e2ee.h"
+#include <memory>
+
+namespace Quotient {
+
+//! An out-bound group session is responsible for encrypting outgoing
+//! communication in a Megolm session.
+class QOlmOutboundGroupSession
+{
+public:
+ ~QOlmOutboundGroupSession();
+ //! Creates a new instance of `QOlmOutboundGroupSession`.
+ //! Throw OlmError on errors
+ static std::unique_ptr<QOlmOutboundGroupSession> create();
+ //! Serialises a `QOlmOutboundGroupSession` to encrypted Base64.
+ std::variant<QByteArray, QOlmError> pickle(const PicklingMode &mode);
+ //! Deserialises from encrypted Base64 that was previously obtained by
+ //! pickling a `QOlmOutboundGroupSession`.
+ static std::variant<std::unique_ptr<QOlmOutboundGroupSession>, QOlmError> unpickle(QByteArray &pickled, const PicklingMode &mode);
+ //! Encrypts a plaintext message using the session.
+ std::variant<QByteArray, QOlmError> encrypt(const QString &plaintext);
+
+ //! Get the current message index for this session.
+ //!
+ //! Each message is sent with an increasing index; this returns the
+ //! index for the next message.
+ uint32_t sessionMessageIndex() const;
+
+ //! Get a base64-encoded identifier for this session.
+ QByteArray sessionId() const;
+
+ //! Get the base64-encoded current ratchet key for this session.
+ //!
+ //! Each message is sent with a different ratchet key. This function returns the
+ //! ratchet key that will be used for the next message.
+ std::variant<QByteArray, QOlmError> sessionKey() const;
+ QOlmOutboundGroupSession(OlmOutboundGroupSession *groupSession);
+private:
+ OlmOutboundGroupSession *m_groupSession;
+};
+
+using QOlmOutboundGroupSessionPtr = std::unique_ptr<QOlmOutboundGroupSession>;
+using OlmOutboundGroupSessionPtr = std::unique_ptr<OlmOutboundGroupSession>;
+}
diff --git a/lib/e2ee/qolmsession.cpp b/lib/e2ee/qolmsession.cpp
new file mode 100644
index 00000000..e575ff39
--- /dev/null
+++ b/lib/e2ee/qolmsession.cpp
@@ -0,0 +1,251 @@
+// SPDX-FileCopyrightText: 2021 Alexey Andreyev <aa13q@ya.ru>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "qolmsession.h"
+#include "e2ee/qolmutils.h"
+#include "logging.h"
+#include <cstring>
+#include <QDebug>
+
+using namespace Quotient;
+
+QOlmError lastError(OlmSession* session) {
+ return fromString(olm_session_last_error(session));
+}
+
+Quotient::QOlmSession::~QOlmSession()
+{
+ olm_clear_session(m_session);
+ delete[](reinterpret_cast<uint8_t *>(m_session));
+}
+
+OlmSession* QOlmSession::create()
+{
+ return olm_session(new uint8_t[olm_session_size()]);
+}
+
+std::variant<QOlmSessionPtr, QOlmError> QOlmSession::createInbound(QOlmAccount *account, const QOlmMessage &preKeyMessage, bool from, const QString &theirIdentityKey)
+{
+ if (preKeyMessage.type() != QOlmMessage::PreKey) {
+ qCCritical(E2EE) << "The message is not a pre-key in when creating inbound session" << BadMessageFormat;
+ }
+
+ const auto olmSession = create();
+
+ QByteArray oneTimeKeyMessageBuf = preKeyMessage.toCiphertext();
+ QByteArray theirIdentityKeyBuf = theirIdentityKey.toUtf8();
+ size_t error = 0;
+ if (from) {
+ error = olm_create_inbound_session_from(olmSession, account->data(), theirIdentityKeyBuf.data(), theirIdentityKeyBuf.length(), oneTimeKeyMessageBuf.data(), oneTimeKeyMessageBuf.length());
+ } else {
+ error = olm_create_inbound_session(olmSession, account->data(), oneTimeKeyMessageBuf.data(), oneTimeKeyMessageBuf.length());
+ }
+
+ if (error == olm_error()) {
+ const auto lastErr = lastError(olmSession);
+ qCWarning(E2EE) << "Error when creating inbound session" << lastErr;
+ return lastErr;
+ }
+
+ return std::make_unique<QOlmSession>(olmSession);
+}
+
+std::variant<QOlmSessionPtr, QOlmError> QOlmSession::createInboundSession(QOlmAccount *account, const QOlmMessage &preKeyMessage)
+{
+ return createInbound(account, preKeyMessage);
+}
+
+std::variant<QOlmSessionPtr, QOlmError> QOlmSession::createInboundSessionFrom(QOlmAccount *account, const QString &theirIdentityKey, const QOlmMessage &preKeyMessage)
+{
+ return createInbound(account, preKeyMessage, true, theirIdentityKey);
+}
+
+std::variant<QOlmSessionPtr, QOlmError> QOlmSession::createOutboundSession(QOlmAccount *account, const QString &theirIdentityKey, const QString &theirOneTimeKey)
+{
+ auto *olmOutboundSession = create();
+ const auto randomLen = olm_create_outbound_session_random_length(olmOutboundSession);
+ QByteArray randomBuf = getRandom(randomLen);
+
+ QByteArray theirIdentityKeyBuf = theirIdentityKey.toUtf8();
+ QByteArray theirOneTimeKeyBuf = theirOneTimeKey.toUtf8();
+ const auto error = olm_create_outbound_session(olmOutboundSession,
+ account->data(),
+ reinterpret_cast<uint8_t *>(theirIdentityKeyBuf.data()), theirIdentityKeyBuf.length(),
+ reinterpret_cast<uint8_t *>(theirOneTimeKeyBuf.data()), theirOneTimeKeyBuf.length(),
+ reinterpret_cast<uint8_t *>(randomBuf.data()), randomBuf.length());
+
+ if (error == olm_error()) {
+ const auto lastErr = lastError(olmOutboundSession);
+ if (lastErr == QOlmError::NotEnoughRandom) {
+ throw lastErr;
+ }
+ return lastErr;
+ }
+
+ randomBuf.clear();
+ return std::make_unique<QOlmSession>(olmOutboundSession);
+}
+
+std::variant<QByteArray, QOlmError> QOlmSession::pickle(const PicklingMode &mode)
+{
+ QByteArray pickledBuf(olm_pickle_session_length(m_session), '0');
+ QByteArray key = toKey(mode);
+ const auto error = olm_pickle_session(m_session, key.data(), key.length(),
+ pickledBuf.data(), pickledBuf.length());
+
+ if (error == olm_error()) {
+ return lastError(m_session);
+ }
+
+ key.clear();
+
+ return pickledBuf;
+}
+
+std::variant<QOlmSessionPtr, QOlmError> QOlmSession::unpickle(const QByteArray &pickled, const PicklingMode &mode)
+{
+ QByteArray pickledBuf = pickled;
+ auto *olmSession = create();
+ QByteArray key = toKey(mode);
+ const auto error = olm_unpickle_session(olmSession, key.data(), key.length(),
+ pickledBuf.data(), pickledBuf.length());
+ if (error == olm_error()) {
+ return lastError(olmSession);
+ }
+
+ key.clear();
+ return std::make_unique<QOlmSession>(olmSession);
+}
+
+QOlmMessage QOlmSession::encrypt(const QString &plaintext)
+{
+ QByteArray plaintextBuf = plaintext.toUtf8();
+ const auto messageMaxLen = olm_encrypt_message_length(m_session, plaintextBuf.length());
+ QByteArray messageBuf(messageMaxLen, '0');
+ const auto messageType = encryptMessageType();
+ const auto randomLen = olm_encrypt_random_length(m_session);
+ QByteArray randomBuf = getRandom(randomLen);
+ const auto error = olm_encrypt(m_session,
+ reinterpret_cast<uint8_t *>(plaintextBuf.data()), plaintextBuf.length(),
+ reinterpret_cast<uint8_t *>(randomBuf.data()), randomBuf.length(),
+ reinterpret_cast<uint8_t *>(messageBuf.data()), messageBuf.length());
+
+ if (error == olm_error()) {
+ throw lastError(m_session);
+ }
+
+ return QOlmMessage(messageBuf, messageType);
+}
+
+std::variant<QString, QOlmError> QOlmSession::decrypt(const QOlmMessage &message) const
+{
+ const auto messageType = message.type();
+ const auto ciphertext = message.toCiphertext();
+ const auto messageTypeValue = messageType == QOlmMessage::Type::General
+ ? OLM_MESSAGE_TYPE_MESSAGE : OLM_MESSAGE_TYPE_PRE_KEY;
+
+ // We need to clone the message because
+ // olm_decrypt_max_plaintext_length destroys the input buffer
+ QByteArray messageBuf(ciphertext.length(), '0');
+ std::copy(message.begin(), message.end(), messageBuf.begin());
+
+ const auto plaintextMaxLen = olm_decrypt_max_plaintext_length(m_session, messageTypeValue,
+ reinterpret_cast<uint8_t *>(messageBuf.data()), messageBuf.length());
+
+ if (plaintextMaxLen == olm_error()) {
+ return lastError(m_session);
+ }
+
+ QByteArray plaintextBuf(plaintextMaxLen, '0');
+ QByteArray messageBuf2(ciphertext.length(), '0');
+ std::copy(message.begin(), message.end(), messageBuf2.begin());
+
+ const auto plaintextResultLen = olm_decrypt(m_session, messageTypeValue,
+ reinterpret_cast<uint8_t *>(messageBuf2.data()), messageBuf2.length(),
+ reinterpret_cast<uint8_t *>(plaintextBuf.data()), plaintextMaxLen);
+
+ if (plaintextResultLen == olm_error()) {
+ const auto lastErr = lastError(m_session);
+ if (lastErr == QOlmError::OutputBufferTooSmall) {
+ throw lastErr;
+ }
+ return lastErr;
+ }
+ QByteArray output(plaintextResultLen, '0');
+ std::memcpy(output.data(), plaintextBuf.data(), plaintextResultLen);
+ plaintextBuf.clear();
+ return output;
+}
+
+QOlmMessage::Type QOlmSession::encryptMessageType()
+{
+ const auto messageTypeResult = olm_encrypt_message_type(m_session);
+ if (messageTypeResult == olm_error()) {
+ throw lastError(m_session);
+ }
+ if (messageTypeResult == OLM_MESSAGE_TYPE_PRE_KEY) {
+ return QOlmMessage::PreKey;
+ }
+ return QOlmMessage::General;
+}
+
+QByteArray QOlmSession::sessionId() const
+{
+ const auto idMaxLength = olm_session_id_length(m_session);
+ QByteArray idBuffer(idMaxLength, '0');
+ const auto error = olm_session_id(m_session, reinterpret_cast<uint8_t *>(idBuffer.data()),
+ idBuffer.length());
+ if (error == olm_error()) {
+ throw lastError(m_session);
+ }
+ return idBuffer;
+}
+
+bool QOlmSession::hasReceivedMessage() const
+{
+ return olm_session_has_received_message(m_session);
+}
+
+std::variant<bool, QOlmError> QOlmSession::matchesInboundSession(const QOlmMessage &preKeyMessage) const
+{
+ Q_ASSERT(preKeyMessage.type() == QOlmMessage::Type::PreKey);
+ QByteArray oneTimeKeyBuf(preKeyMessage.data());
+ const auto matchesResult = olm_matches_inbound_session(m_session, oneTimeKeyBuf.data(), oneTimeKeyBuf.length());
+
+ if (matchesResult == olm_error()) {
+ return lastError(m_session);
+ }
+ switch (matchesResult) {
+ case 0:
+ return false;
+ case 1:
+ return true;
+ default:
+ return QOlmError::Unknown;
+ }
+}
+std::variant<bool, QOlmError> QOlmSession::matchesInboundSessionFrom(const QString &theirIdentityKey, const QOlmMessage &preKeyMessage) const
+{
+ const auto theirIdentityKeyBuf = theirIdentityKey.toUtf8();
+ auto oneTimeKeyMessageBuf = preKeyMessage.toCiphertext();
+ const auto error = olm_matches_inbound_session_from(m_session, theirIdentityKeyBuf.data(), theirIdentityKeyBuf.length(),
+ oneTimeKeyMessageBuf.data(), oneTimeKeyMessageBuf.length());
+
+ if (error == olm_error()) {
+ return lastError(m_session);
+ }
+ switch (error) {
+ case 0:
+ return false;
+ case 1:
+ return true;
+ default:
+ return QOlmError::Unknown;
+ }
+}
+
+QOlmSession::QOlmSession(OlmSession *session)
+ : m_session(session)
+{
+}
diff --git a/lib/e2ee/qolmsession.h b/lib/e2ee/qolmsession.h
new file mode 100644
index 00000000..1febfa0f
--- /dev/null
+++ b/lib/e2ee/qolmsession.h
@@ -0,0 +1,76 @@
+// SPDX-FileCopyrightText: 2021 Alexey Andreyev <aa13q@ya.ru>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include <QDebug>
+#include <olm/olm.h> // FIXME: OlmSession
+#include "e2ee/e2ee.h"
+#include "e2ee/qolmmessage.h"
+#include "e2ee/qolmerrors.h"
+#include "e2ee/qolmaccount.h"
+
+namespace Quotient {
+
+class QOlmAccount;
+class QOlmSession;
+
+
+//! Either an outbound or inbound session for secure communication.
+class QOlmSession
+{
+public:
+ ~QOlmSession();
+ //! Creates an inbound session for sending/receiving messages from a received 'prekey' message.
+ static std::variant<std::unique_ptr<QOlmSession>, QOlmError> createInboundSession(QOlmAccount *account, const QOlmMessage &preKeyMessage);
+ static std::variant<std::unique_ptr<QOlmSession>, QOlmError> createInboundSessionFrom(QOlmAccount *account, const QString &theirIdentityKey, const QOlmMessage &preKeyMessage);
+ static std::variant<std::unique_ptr<QOlmSession>, QOlmError> createOutboundSession(QOlmAccount *account, const QString &theirIdentityKey, const QString &theirOneTimeKey);
+ //! Serialises an `QOlmSession` to encrypted Base64.
+ std::variant<QByteArray, QOlmError> pickle(const PicklingMode &mode);
+ //! Deserialises from encrypted Base64 that was previously obtained by pickling a `QOlmSession`.
+ static std::variant<std::unique_ptr<QOlmSession>, QOlmError> unpickle(const QByteArray &pickled, const PicklingMode &mode);
+ //! Encrypts a plaintext message using the session.
+ QOlmMessage encrypt(const QString &plaintext);
+
+ //! Decrypts a message using this session. Decoding is lossy, meaing if
+ //! the decrypted plaintext contains invalid UTF-8 symbols, they will
+ //! be returned as `U+FFFD` (�).
+ std::variant<QString, QOlmError> decrypt(const QOlmMessage &message) const;
+
+ //! Get a base64-encoded identifier for this session.
+ QByteArray sessionId() const;
+
+ //! The type of the next message that will be returned from encryption.
+ QOlmMessage::Type encryptMessageType();
+
+ //! Checker for any received messages for this session.
+ bool hasReceivedMessage() const;
+
+ //! Checks if the 'prekey' message is for this in-bound session.
+ std::variant<bool, QOlmError> matchesInboundSession(const QOlmMessage &preKeyMessage) const;
+
+ //! Checks if the 'prekey' message is for this in-bound session.
+ std::variant<bool, QOlmError> matchesInboundSessionFrom(const QString &theirIdentityKey, const QOlmMessage &preKeyMessage) const;
+
+ friend bool operator<(const QOlmSession& lhs, const QOlmSession& rhs)
+ {
+ return lhs.sessionId() < rhs.sessionId();
+ }
+
+ friend bool operator<(const std::unique_ptr<QOlmSession> &lhs, const std::unique_ptr<QOlmSession> &rhs) {
+ return *lhs < *rhs;
+ }
+
+ OlmSession *raw() const
+ {
+ return m_session;
+ }
+ QOlmSession(OlmSession* session);
+private:
+ //! Helper function for creating new sessions and handling errors.
+ static OlmSession* create();
+ static std::variant<std::unique_ptr<QOlmSession>, QOlmError> createInbound(QOlmAccount *account, const QOlmMessage& preKeyMessage, bool from = false, const QString& theirIdentityKey = "");
+ OlmSession* m_session;
+};
+} //namespace Quotient
diff --git a/lib/e2ee/qolmutility.cpp b/lib/e2ee/qolmutility.cpp
new file mode 100644
index 00000000..303f6d75
--- /dev/null
+++ b/lib/e2ee/qolmutility.cpp
@@ -0,0 +1,61 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "e2ee/qolmutility.h"
+#include "olm/olm.h"
+#include <QDebug>
+
+using namespace Quotient;
+
+// Convert olm error to enum
+QOlmError lastError(OlmUtility *utility) {
+ return fromString(olm_utility_last_error(utility));
+}
+
+QOlmUtility::QOlmUtility()
+{
+ auto utility = new uint8_t[olm_utility_size()];
+ m_utility = olm_utility(utility);
+}
+
+QOlmUtility::~QOlmUtility()
+{
+ olm_clear_utility(m_utility);
+ delete[](reinterpret_cast<uint8_t *>(m_utility));
+}
+
+QString QOlmUtility::sha256Bytes(const QByteArray &inputBuf) const
+{
+ const auto outputLen = olm_sha256_length(m_utility);
+ QByteArray outputBuf(outputLen, '0');
+ olm_sha256(m_utility, inputBuf.data(), inputBuf.length(),
+ outputBuf.data(), outputBuf.length());
+
+ return QString::fromUtf8(outputBuf);
+}
+
+QString QOlmUtility::sha256Utf8Msg(const QString &message) const
+{
+ return sha256Bytes(message.toUtf8());
+}
+
+std::variant<bool, QOlmError> QOlmUtility::ed25519Verify(const QByteArray &key,
+ const QByteArray &message, const QByteArray &signature)
+{
+ QByteArray signatureBuf(signature.length(), '0');
+ std::copy(signature.begin(), signature.end(), signatureBuf.begin());
+
+ const auto ret = olm_ed25519_verify(m_utility, key.data(), key.size(),
+ message.data(), message.size(), (void *)signatureBuf.data(), signatureBuf.size());
+
+ const auto error = ret;
+ if (error == olm_error()) {
+ return lastError(m_utility);
+ }
+
+ if (ret != 0) {
+ return false;
+ }
+ return true;
+}
diff --git a/lib/e2ee/qolmutility.h b/lib/e2ee/qolmutility.h
new file mode 100644
index 00000000..b360d625
--- /dev/null
+++ b/lib/e2ee/qolmutility.h
@@ -0,0 +1,45 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include <QObject>
+#include <variant>
+#include "e2ee/qolmerrors.h"
+
+struct OlmUtility;
+
+namespace Quotient {
+
+class QOlmSession;
+class Connection;
+
+//! Allows you to make use of crytographic hashing via SHA-2 and
+//! verifying ed25519 signatures.
+class QOlmUtility
+{
+public:
+ QOlmUtility();
+ ~QOlmUtility();
+
+ //! Returns a sha256 of the supplied byte slice.
+ QString sha256Bytes(const QByteArray &inputBuf) const;
+
+ //! Convenience function that converts the UTF-8 message
+ //! to bytes and then calls `sha256Bytes()`, returning its output.
+ QString sha256Utf8Msg(const QString &message) const;
+
+ //! Verify a ed25519 signature.
+ //! \param key QByteArray The public part of the ed25519 key that signed the message.
+ //! \param message QByteArray The message that was signed.
+ //! \param signature QByteArray The signature of the message.
+ std::variant<bool, QOlmError> ed25519Verify(const QByteArray &key,
+ const QByteArray &message, const QByteArray &signature);
+
+
+private:
+ OlmUtility *m_utility;
+
+};
+}
diff --git a/lib/e2ee/qolmutils.cpp b/lib/e2ee/qolmutils.cpp
new file mode 100644
index 00000000..6f7937e8
--- /dev/null
+++ b/lib/e2ee/qolmutils.cpp
@@ -0,0 +1,23 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "e2ee/qolmutils.h"
+#include <QtCore/QRandomGenerator>
+
+using namespace Quotient;
+
+QByteArray Quotient::toKey(const Quotient::PicklingMode &mode)
+{
+ if (std::holds_alternative<Quotient::Unencrypted>(mode)) {
+ return {};
+ }
+ return std::get<Quotient::Encrypted>(mode).key;
+}
+
+QByteArray Quotient::getRandom(size_t bufferSize)
+{
+ QByteArray buffer(bufferSize, '0');
+ QRandomGenerator::system()->generate(buffer.begin(), buffer.end());
+ return buffer;
+}
diff --git a/lib/e2ee/qolmutils.h b/lib/e2ee/qolmutils.h
new file mode 100644
index 00000000..bbd71332
--- /dev/null
+++ b/lib/e2ee/qolmutils.h
@@ -0,0 +1,15 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include <QByteArray>
+
+#include "e2ee/e2ee.h"
+
+namespace Quotient {
+// Convert PicklingMode to key
+QByteArray toKey(const PicklingMode &mode);
+QByteArray getRandom(size_t bufferSize);
+}
diff --git a/lib/encryptionmanager.cpp b/lib/encryptionmanager.cpp
deleted file mode 100644
index 37f3b7c3..00000000
--- a/lib/encryptionmanager.cpp
+++ /dev/null
@@ -1,373 +0,0 @@
-// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru>
-// SPDX-FileCopyrightText: 2019 Kitsune Ral <Kitsune-Ral@users.sf.net>
-// SPDX-License-Identifier: LGPL-2.1-or-later
-
-#ifdef Quotient_E2EE_ENABLED
-#include "encryptionmanager.h"
-
-#include "connection.h"
-#include "e2ee.h"
-
-#include "csapi/keys.h"
-
-#include <QtCore/QHash>
-#include <QtCore/QStringBuilder>
-
-#include <account.h> // QtOlm
-#include <session.h> // QtOlm
-#include <message.h> // QtOlm
-#include <errors.h> // QtOlm
-#include <utils.h> // QtOlm
-#include <functional>
-#include <memory>
-
-using namespace Quotient;
-using namespace QtOlm;
-using std::move;
-
-class EncryptionManager::Private {
-public:
- explicit Private(const QByteArray& encryptionAccountPickle,
- float signedKeysProportion, float oneTimeKeyThreshold)
- : q(nullptr)
- , signedKeysProportion(move(signedKeysProportion))
- , oneTimeKeyThreshold(move(oneTimeKeyThreshold))
- {
- Q_ASSERT((0 <= signedKeysProportion) && (signedKeysProportion <= 1));
- Q_ASSERT((0 <= oneTimeKeyThreshold) && (oneTimeKeyThreshold <= 1));
- if (encryptionAccountPickle.isEmpty()) {
- olmAccount.reset(new Account());
- } else {
- olmAccount.reset(
- new Account(encryptionAccountPickle)); // TODO: passphrase even
- // with qtkeychain?
- }
- /*
- * Note about targetKeysNumber:
- *
- * From: https://github.com/Zil0/matrix-python-sdk/
- * File: matrix_client/crypto/olm_device.py
- *
- * Try to maintain half the number of one-time keys libolm can hold
- * uploaded on the HS. This is because some keys will be claimed by
- * peers but not used instantly, and we want them to stay in libolm,
- * until the limit is reached and it starts discarding keys, starting by
- * the oldest.
- */
- targetKeysNumber = olmAccount->maxOneTimeKeys() / 2;
- targetOneTimeKeyCounts = {
- { SignedCurve25519Key,
- qRound(signedKeysProportion * targetKeysNumber) },
- { Curve25519Key,
- qRound((1 - signedKeysProportion) * targetKeysNumber) }
- };
- updateKeysToUpload();
- }
- ~Private() = default;
-
- EncryptionManager* q;
-
- UploadKeysJob* uploadIdentityKeysJob = nullptr;
- UploadKeysJob* uploadOneTimeKeysInitJob = nullptr;
- UploadKeysJob* uploadOneTimeKeysJob = nullptr;
- QueryKeysJob* queryKeysJob = nullptr;
-
- QScopedPointer<Account> olmAccount;
-
- float signedKeysProportion;
- float oneTimeKeyThreshold;
- int targetKeysNumber;
-
- void updateKeysToUpload();
- bool oneTimeKeyShouldUpload();
-
- QHash<QString, int> oneTimeKeyCounts;
- void setOneTimeKeyCounts(const QHash<QString, int> oneTimeKeyCountsNewValue)
- {
- oneTimeKeyCounts = oneTimeKeyCountsNewValue;
- updateKeysToUpload();
- }
- QHash<QString, int> oneTimeKeysToUploadCounts;
- QHash<QString, int> targetOneTimeKeyCounts;
-
- // A map from senderKey to InboundSession
- QMap<QString, InboundSession*> sessions; // TODO: cache
- void updateDeviceKeys(
- const QHash<QString,
- QHash<QString, QueryKeysJob::DeviceInformation>>& deviceKeys)
- {
- for (auto userId : deviceKeys.keys()) {
- for (auto deviceId : deviceKeys.value(userId).keys()) {
- auto info = deviceKeys.value(userId).value(deviceId);
- // TODO: ed25519Verify, etc
- }
- }
- }
- QString sessionDecrypt(Message* message, const QString& senderKey)
- {
- QString decrypted;
- QList<InboundSession*> senderSessions = sessions.values(senderKey);
- // Try to decrypt message body using one of the known sessions for that
- // device
- bool sessionsPassed = false;
- for (auto senderSession : senderSessions) {
- if (senderSession == senderSessions.last()) {
- sessionsPassed = true;
- }
- try {
- decrypted = senderSession->decrypt(message);
- qCDebug(E2EE)
- << "Success decrypting Olm event using existing session"
- << senderSession->id();
- break;
- } catch (OlmError* e) {
- if (message->messageType() == 0) {
- PreKeyMessage preKeyMessage =
- PreKeyMessage(message->cipherText());
- if (senderSession->matches(&preKeyMessage, senderKey)) {
- // We had a matching session for a pre-key message, but
- // it didn't work. This means something is wrong, so we
- // fail now.
- qCDebug(E2EE)
- << "Error decrypting pre-key message with existing "
- "Olm session"
- << senderSession->id() << "reason:" << e->what();
- return QString();
- }
- }
- // Simply keep trying otherwise
- }
- }
- if (sessionsPassed || senderSessions.empty()) {
- if (message->messageType() > 0) {
- // Not a pre-key message, we should have had a matching session
- if (!sessions.empty()) {
- qCDebug(E2EE) << "Error decrypting with existing sessions";
- return QString();
- }
- qCDebug(E2EE) << "No existing sessions";
- return QString();
- }
- // We have a pre-key message without any matching session, in this
- // case we should try to create one.
- InboundSession* newSession;
- qCDebug(E2EE) << "try to establish new InboundSession with" << senderKey;
- PreKeyMessage preKeyMessage = PreKeyMessage(message->cipherText());
- try {
- newSession = new InboundSession(olmAccount.data(),
- &preKeyMessage,
- senderKey.toLatin1(), q);
- } catch (OlmError* e) {
- qCDebug(E2EE) << "Error decrypting pre-key message when trying "
- "to establish a new session:"
- << e->what();
- return QString();
- }
- qCDebug(E2EE) << "Created new Olm session" << newSession->id();
- try {
- decrypted = newSession->decrypt(message);
- } catch (OlmError* e) {
- qCDebug(E2EE)
- << "Error decrypting pre-key message with new session"
- << e->what();
- return QString();
- }
- olmAccount->removeOneTimeKeys(newSession);
- sessions.insert(senderKey, newSession);
- }
- return decrypted;
- }
-};
-
-EncryptionManager::EncryptionManager(const QByteArray& encryptionAccountPickle,
- float signedKeysProportion,
- float oneTimeKeyThreshold, QObject* parent)
- : QObject(parent)
- , d(std::make_unique<Private>(std::move(encryptionAccountPickle),
- std::move(signedKeysProportion),
- std::move(oneTimeKeyThreshold)))
-{
- d->q = this;
-}
-
-EncryptionManager::~EncryptionManager() = default;
-
-void EncryptionManager::uploadIdentityKeys(Connection* connection)
-{
- // https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-keys-upload
- DeviceKeys deviceKeys {
- /*
- * The ID of the user the device belongs to. Must match the user ID used
- * when logging in. The ID of the device these keys belong to. Must
- * match the device ID used when logging in. The encryption algorithms
- * supported by this device.
- */
- connection->userId(),
- connection->deviceId(),
- SupportedAlgorithms,
- /*
- * Public identity keys. The names of the properties should be in the
- * format <algorithm>:<device_id>. The keys themselves should be encoded
- * as specified by the key algorithm.
- */
- { { Curve25519Key + QStringLiteral(":") + connection->deviceId(),
- d->olmAccount->curve25519IdentityKey() },
- { Ed25519Key + QStringLiteral(":") + connection->deviceId(),
- d->olmAccount->ed25519IdentityKey() } },
- /* signatures should be provided after the unsigned deviceKeys
- generation */
- {}
- };
-
- QJsonObject deviceKeysJsonObject = toJson(deviceKeys);
- /* additionally removing signatures key,
- * since we could not initialize deviceKeys
- * without an empty signatures value:
- */
- deviceKeysJsonObject.remove(QStringLiteral("signatures"));
- /*
- * Signatures for the device key object.
- * A map from user ID, to a map from <algorithm>:<device_id> to the
- * signature. The signature is calculated using the process called Signing
- * JSON.
- */
- deviceKeys.signatures = {
- { connection->userId(),
- { { Ed25519Key + QStringLiteral(":") + connection->deviceId(),
- d->olmAccount->sign(deviceKeysJsonObject) } } }
- };
-
- d->uploadIdentityKeysJob = connection->callApi<UploadKeysJob>(deviceKeys);
- connect(d->uploadIdentityKeysJob, &BaseJob::success, this, [this] {
- d->setOneTimeKeyCounts(d->uploadIdentityKeysJob->oneTimeKeyCounts());
- });
-}
-
-void EncryptionManager::uploadOneTimeKeys(Connection* connection,
- bool forceUpdate)
-{
- if (forceUpdate || d->oneTimeKeyCounts.isEmpty()) {
- d->uploadOneTimeKeysInitJob = connection->callApi<UploadKeysJob>();
- connect(d->uploadOneTimeKeysInitJob, &BaseJob::success, this, [this] {
- d->setOneTimeKeyCounts(d->uploadOneTimeKeysInitJob->oneTimeKeyCounts());
- });
- }
-
- int signedKeysToUploadCount =
- d->oneTimeKeysToUploadCounts.value(SignedCurve25519Key, 0);
- int unsignedKeysToUploadCount =
- d->oneTimeKeysToUploadCounts.value(Curve25519Key, 0);
-
- d->olmAccount->generateOneTimeKeys(signedKeysToUploadCount
- + unsignedKeysToUploadCount);
-
- QHash<QString, QVariant> oneTimeKeys = {};
- const auto& olmAccountCurve25519OneTimeKeys =
- d->olmAccount->curve25519OneTimeKeys();
-
- int oneTimeKeysCounter = 0;
- for (auto it = olmAccountCurve25519OneTimeKeys.cbegin();
- it != olmAccountCurve25519OneTimeKeys.cend(); ++it) {
- QString keyId = it.key();
- QString keyType;
- QVariant key;
- if (oneTimeKeysCounter < signedKeysToUploadCount) {
- QJsonObject message { { QStringLiteral("key"),
- it.value().toString() } };
-
- QByteArray signedMessage = d->olmAccount->sign(message);
- QJsonObject signatures {
- { connection->userId(),
- QJsonObject { { Ed25519Key + QStringLiteral(":")
- + connection->deviceId(),
- QString::fromUtf8(signedMessage) } } }
- };
- message.insert(QStringLiteral("signatures"), signatures);
- key = message;
- keyType = SignedCurve25519Key;
- } else {
- key = it.value();
- keyType = Curve25519Key;
- }
- ++oneTimeKeysCounter;
- oneTimeKeys.insert(QString("%1:%2").arg(keyType).arg(keyId), key);
- }
- d->uploadOneTimeKeysJob =
- connection->callApi<UploadKeysJob>(none, oneTimeKeys);
- connect(d->uploadOneTimeKeysJob, &BaseJob::success, this, [this] {
- d->setOneTimeKeyCounts(d->uploadOneTimeKeysJob->oneTimeKeyCounts());
- });
- d->olmAccount->markKeysAsPublished();
- qCDebug(E2EE) << QString("Uploaded new one-time keys: %1 signed, %2 unsigned.")
- .arg(signedKeysToUploadCount)
- .arg(unsignedKeysToUploadCount);
-}
-
-void EncryptionManager::updateOneTimeKeyCounts(
- Connection* connection, const QHash<QString, int>& deviceOneTimeKeysCount)
-{
- d->oneTimeKeyCounts = deviceOneTimeKeysCount;
- if (d->oneTimeKeyShouldUpload()) {
- qCDebug(E2EE) << "Uploading new one-time keys.";
- uploadOneTimeKeys(connection);
- }
-}
-
-void Quotient::EncryptionManager::updateDeviceKeys(
- Connection* connection, const QHash<QString, QStringList>& deviceKeys)
-{
- d->queryKeysJob = connection->callApi<QueryKeysJob>(deviceKeys);
- connect(d->queryKeysJob, &BaseJob::success, this,
- [this] { d->updateDeviceKeys(d->queryKeysJob->deviceKeys()); });
-}
-
-QString EncryptionManager::sessionDecryptMessage(
- const QJsonObject& personalCipherObject, const QByteArray& senderKey)
-{
- QString decrypted;
- int type = personalCipherObject.value(TypeKeyL).toInt(-1);
- QByteArray body = personalCipherObject.value(BodyKeyL).toString().toLatin1();
- if (type == 0) {
- PreKeyMessage preKeyMessage { body };
- decrypted = d->sessionDecrypt(reinterpret_cast<Message*>(&preKeyMessage),
- senderKey);
- } else if (type == 1) {
- Message message { body };
- decrypted = d->sessionDecrypt(&message, senderKey);
- }
- return decrypted;
-}
-
-QByteArray EncryptionManager::olmAccountPickle()
-{
- return d->olmAccount->pickle(); // TODO: passphrase even with qtkeychain?
-}
-
-QtOlm::Account* EncryptionManager::account() const
-{
- return d->olmAccount.data();
-}
-
-void EncryptionManager::Private::updateKeysToUpload()
-{
- for (auto it = targetOneTimeKeyCounts.cbegin();
- it != targetOneTimeKeyCounts.cend(); ++it) {
- int numKeys = oneTimeKeyCounts.value(it.key(), 0);
- int numToCreate = qMax(it.value() - numKeys, 0);
- oneTimeKeysToUploadCounts.insert(it.key(), numToCreate);
- }
-}
-
-bool EncryptionManager::Private::oneTimeKeyShouldUpload()
-{
- if (oneTimeKeyCounts.empty())
- return true;
- for (auto it = targetOneTimeKeyCounts.cbegin();
- it != targetOneTimeKeyCounts.cend(); ++it) {
- if (oneTimeKeyCounts.value(it.key(), 0)
- < it.value() * oneTimeKeyThreshold)
- return true;
- }
- return false;
-}
-#endif // Quotient_E2EE_ENABLED
diff --git a/lib/encryptionmanager.h b/lib/encryptionmanager.h
deleted file mode 100644
index 714f95fd..00000000
--- a/lib/encryptionmanager.h
+++ /dev/null
@@ -1,50 +0,0 @@
-// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru>
-// SPDX-License-Identifier: LGPL-2.1-or-later
-
-#ifdef Quotient_E2EE_ENABLED
-#pragma once
-
-#include <QtCore/QObject>
-
-#include <functional>
-#include <memory>
-
-namespace QtOlm {
-class Account;
-}
-
-namespace Quotient {
-class Connection;
-
-class EncryptionManager : public QObject {
- Q_OBJECT
-
-public:
- // TODO: store constats separately?
- // TODO: 0.5 oneTimeKeyThreshold instead of 0.1?
- explicit EncryptionManager(
- const QByteArray& encryptionAccountPickle = QByteArray(),
- float signedKeysProportion = 1, float oneTimeKeyThreshold = float(0.1),
- QObject* parent = nullptr);
- ~EncryptionManager();
-
- void uploadIdentityKeys(Connection* connection);
- void uploadOneTimeKeys(Connection* connection, bool forceUpdate = false);
- void
- updateOneTimeKeyCounts(Connection* connection,
- const QHash<QString, int>& deviceOneTimeKeysCount);
- void updateDeviceKeys(Connection* connection,
- const QHash<QString, QStringList>& deviceKeys);
- QString sessionDecryptMessage(const QJsonObject& personalCipherObject,
- const QByteArray& senderKey);
- QByteArray olmAccountPickle();
-
- QtOlm::Account* account() const;
-
-private:
- class Private;
- std::unique_ptr<Private> d;
-};
-
-} // namespace Quotient
-#endif // Quotient_E2EE_ENABLED
diff --git a/lib/events/encryptedevent.cpp b/lib/events/encryptedevent.cpp
index 0290f973..1b5e4441 100644
--- a/lib/events/encryptedevent.cpp
+++ b/lib/events/encryptedevent.cpp
@@ -2,6 +2,8 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
#include "encryptedevent.h"
+#include "roommessageevent.h"
+#include "events/eventloader.h"
using namespace Quotient;
@@ -30,3 +32,22 @@ EncryptedEvent::EncryptedEvent(const QJsonObject& obj)
{
qCDebug(E2EE) << "Encrypted event from" << senderId();
}
+
+RoomEventPtr EncryptedEvent::createDecrypted(const QString &decrypted) const
+{
+ auto eventObject = QJsonDocument::fromJson(decrypted.toUtf8()).object();
+ eventObject["event_id"] = id();
+ eventObject["sender"] = senderId();
+ eventObject["origin_server_ts"] = originTimestamp().toMSecsSinceEpoch();
+ if (const auto relatesToJson = contentPart("m.relates_to"_ls); !relatesToJson.isUndefined()) {
+ auto content = eventObject["content"].toObject();
+ content["m.relates_to"] = relatesToJson.toObject();
+ eventObject["content"] = content;
+ }
+ if (const auto redactsJson = unsignedPart("redacts"_ls); !redactsJson.isUndefined()) {
+ auto unsign = eventObject["unsigned"].toObject();
+ unsign["redacts"] = redactsJson.toString();
+ eventObject["unsigned"] = unsign;
+ }
+ return loadEvent<RoomEvent>(eventObject);
+}
diff --git a/lib/events/encryptedevent.h b/lib/events/encryptedevent.h
index 81343a29..c838bbd8 100644
--- a/lib/events/encryptedevent.h
+++ b/lib/events/encryptedevent.h
@@ -3,7 +3,7 @@
#pragma once
-#include "e2ee.h"
+#include "e2ee/e2ee.h"
#include "roomevent.h"
namespace Quotient {
@@ -61,6 +61,7 @@ public:
/* device_id and session_id are required with Megolm */
QString deviceId() const { return contentPart<QString>(DeviceIdKeyL); }
QString sessionId() const { return contentPart<QString>(SessionIdKeyL); }
+ RoomEventPtr createDecrypted(const QString &decrypted) const;
};
REGISTER_EVENT_TYPE(EncryptedEvent)
diff --git a/lib/events/encryptedfile.cpp b/lib/events/encryptedfile.cpp
new file mode 100644
index 00000000..74119127
--- /dev/null
+++ b/lib/events/encryptedfile.cpp
@@ -0,0 +1,31 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "encryptedfile.h"
+#include "logging.h"
+
+#include <openssl/evp.h>
+#include <QtCore/QCryptographicHash>
+
+using namespace Quotient;
+
+QByteArray EncryptedFile::decryptFile(const QByteArray &ciphertext) const
+{
+ QString _key = key.k;
+ _key = QByteArray::fromBase64(_key.replace(QLatin1Char('_'), QLatin1Char('/')).replace(QLatin1Char('-'), QLatin1Char('+')).toLatin1());
+ const auto sha256 = QByteArray::fromBase64(hashes["sha256"].toLatin1());
+ if(sha256 != QCryptographicHash::hash(ciphertext, QCryptographicHash::Sha256)) {
+ qCWarning(E2EE) << "Hash verification failed for file";
+ return QByteArray();
+ }
+ QByteArray plaintext(ciphertext.size(), 0);
+ EVP_CIPHER_CTX *ctx;
+ int length;
+ ctx = EVP_CIPHER_CTX_new();
+ EVP_DecryptInit_ex(ctx, EVP_aes_256_ctr(), NULL, (const unsigned char *)_key.data(), (const unsigned char *)iv.toLatin1().data());
+ EVP_DecryptUpdate(ctx, (unsigned char *)plaintext.data(), &length, (const unsigned char *)ciphertext.data(), ciphertext.size());
+ EVP_DecryptFinal_ex(ctx, (unsigned char *)plaintext.data() + length, &length);
+ EVP_CIPHER_CTX_free(ctx);
+ return plaintext;
+}
diff --git a/lib/events/encryptedfile.h b/lib/events/encryptedfile.h
index 24ac9de1..6199be8e 100644
--- a/lib/events/encryptedfile.h
+++ b/lib/events/encryptedfile.h
@@ -44,6 +44,8 @@ public:
QString iv;
QHash<QString, QString> hashes;
QString v;
+
+ QByteArray decryptFile(const QByteArray &ciphertext) const;
};
template <>
diff --git a/lib/events/encryptionevent.cpp b/lib/events/encryptionevent.cpp
index aa05a96e..6272c668 100644
--- a/lib/events/encryptionevent.cpp
+++ b/lib/events/encryptionevent.cpp
@@ -4,7 +4,7 @@
#include "encryptionevent.h"
-#include "e2ee.h"
+#include "e2ee/e2ee.h"
#include <array>
diff --git a/lib/events/eventcontent.cpp b/lib/events/eventcontent.cpp
index 4ce130a6..9d7edf20 100644
--- a/lib/events/eventcontent.cpp
+++ b/lib/events/eventcontent.cpp
@@ -74,6 +74,7 @@ void FileInfo::fillInfoJson(QJsonObject* infoJson) const
infoJson->insert(QStringLiteral("size"), payloadSize);
if (mimeType.isValid())
infoJson->insert(QStringLiteral("mimetype"), mimeType.name());
+ //TODO add encryptedfile
}
ImageInfo::ImageInfo(const QFileInfo& fi, QSize imageSize)
diff --git a/lib/events/keyverificationevent.cpp b/lib/events/keyverificationevent.cpp
new file mode 100644
index 00000000..4803955d
--- /dev/null
+++ b/lib/events/keyverificationevent.cpp
@@ -0,0 +1,164 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "keyverificationevent.h"
+
+using namespace Quotient;
+
+KeyVerificationRequestEvent::KeyVerificationRequestEvent(const QJsonObject &obj)
+ : Event(typeId(), obj)
+{}
+
+QString KeyVerificationRequestEvent::fromDevice() const
+{
+ return contentPart<QString>("from_device"_ls);
+}
+
+QString KeyVerificationRequestEvent::transactionId() const
+{
+ return contentPart<QString>("transaction_id"_ls);
+}
+
+QStringList KeyVerificationRequestEvent::methods() const
+{
+ return contentPart<QStringList>("methods"_ls);
+}
+
+uint64_t KeyVerificationRequestEvent::timestamp() const
+{
+ return contentPart<double>("timestamp"_ls);
+}
+
+KeyVerificationStartEvent::KeyVerificationStartEvent(const QJsonObject &obj)
+ : Event(typeId(), obj)
+{}
+
+QString KeyVerificationStartEvent::fromDevice() const
+{
+ return contentPart<QString>("from_device"_ls);
+}
+
+QString KeyVerificationStartEvent::transactionId() const
+{
+ return contentPart<QString>("transaction_id"_ls);
+}
+
+QString KeyVerificationStartEvent::method() const
+{
+ return contentPart<QString>("method"_ls);
+}
+
+Omittable<QString> KeyVerificationStartEvent::nextMethod() const
+{
+ return contentPart<Omittable<QString>>("method_ls");
+}
+
+QStringList KeyVerificationStartEvent::keyAgreementProtocols() const
+{
+ Q_ASSERT(method() == QStringLiteral("m.sas.v1"));
+ return contentPart<QStringList>("key_agreement_protocols"_ls);
+}
+
+QStringList KeyVerificationStartEvent::hashes() const
+{
+ Q_ASSERT(method() == QStringLiteral("m.sas.v1"));
+ return contentPart<QStringList>("hashes"_ls);
+
+}
+
+QStringList KeyVerificationStartEvent::messageAuthenticationCodes() const
+{
+ Q_ASSERT(method() == QStringLiteral("m.sas.v1"));
+ return contentPart<QStringList>("message_authentication_codes"_ls);
+}
+
+QString KeyVerificationStartEvent::shortAuthenticationString() const
+{
+ return contentPart<QString>("short_authentification_string"_ls);
+}
+
+KeyVerificationAcceptEvent::KeyVerificationAcceptEvent(const QJsonObject &obj)
+ : Event(typeId(), obj)
+{}
+
+QString KeyVerificationAcceptEvent::transactionId() const
+{
+ return contentPart<QString>("transaction_id"_ls);
+}
+
+QString KeyVerificationAcceptEvent::method() const
+{
+ return contentPart<QString>("method"_ls);
+}
+
+QString KeyVerificationAcceptEvent::keyAgreementProtocol() const
+{
+ return contentPart<QString>("key_agreement_protocol"_ls);
+}
+
+QString KeyVerificationAcceptEvent::hashData() const
+{
+ return contentPart<QString>("hash"_ls);
+}
+
+QStringList KeyVerificationAcceptEvent::shortAuthenticationString() const
+{
+ return contentPart<QStringList>("short_authentification_string"_ls);
+}
+
+QString KeyVerificationAcceptEvent::commitement() const
+{
+ return contentPart<QString>("commitment"_ls);
+}
+
+KeyVerificationCancelEvent::KeyVerificationCancelEvent(const QJsonObject &obj)
+ : Event(typeId(), obj)
+{}
+
+QString KeyVerificationCancelEvent::transactionId() const
+{
+ return contentPart<QString>("transaction_id"_ls);
+}
+
+QString KeyVerificationCancelEvent::reason() const
+{
+ return contentPart<QString>("reason"_ls);
+}
+
+QString KeyVerificationCancelEvent::code() const
+{
+ return contentPart<QString>("code"_ls);
+}
+
+KeyVerificationKeyEvent::KeyVerificationKeyEvent(const QJsonObject &obj)
+ : Event(typeId(), obj)
+{}
+
+QString KeyVerificationKeyEvent::transactionId() const
+{
+ return contentPart<QString>("transaction_id"_ls);
+}
+
+QString KeyVerificationKeyEvent::key() const
+{
+ return contentPart<QString>("key"_ls);
+}
+
+KeyVerificationMacEvent::KeyVerificationMacEvent(const QJsonObject &obj)
+ : Event(typeId(), obj)
+{}
+
+QString KeyVerificationMacEvent::transactionId() const
+{
+ return contentPart<QString>("transaction_id"_ls);
+}
+
+QString KeyVerificationMacEvent::keys() const
+{
+ return contentPart<QString>("keys"_ls);
+}
+
+QHash<QString, QString> KeyVerificationMacEvent::mac() const
+{
+ return contentPart<QHash<QString, QString>>("mac"_ls);
+}
diff --git a/lib/events/keyverificationevent.h b/lib/events/keyverificationevent.h
new file mode 100644
index 00000000..13e7dcdd
--- /dev/null
+++ b/lib/events/keyverificationevent.h
@@ -0,0 +1,167 @@
+// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "event.h"
+
+namespace Quotient {
+
+/// Requests a key verification with another user's devices.
+/// Typically sent as a to-device event.
+class KeyVerificationRequestEvent : public Event {
+ Q_GADGET
+public:
+ DEFINE_EVENT_TYPEID("m.key.verification.request", KeyVerificationRequestEvent)
+
+ explicit KeyVerificationRequestEvent(const QJsonObject& obj);
+
+ /// The device ID which is initiating the request.
+ QString fromDevice() const;
+
+ /// An opaque identifier for the verification request. Must
+ /// be unique with respect to the devices involved.
+ QString transactionId() const;
+
+ /// The verification methods supported by the sender.
+ QStringList methods() const;
+
+ /// The POSIX timestamp in milliseconds for when the request was
+ /// made. If the request is in the future by more than 5 minutes or
+ /// more than 10 minutes in the past, the message should be ignored
+ /// by the receiver.
+ uint64_t timestamp() const;
+};
+REGISTER_EVENT_TYPE(KeyVerificationRequestEvent)
+
+/// Begins a key verification process.
+class KeyVerificationStartEvent : public Event {
+ Q_GADGET
+public:
+ DEFINE_EVENT_TYPEID("m.key.verification.start", KeyVerificationStartEvent)
+
+ explicit KeyVerificationStartEvent(const QJsonObject &obj);
+
+ /// The device ID which is initiating the process.
+ QString fromDevice() const;
+
+ /// An opaque identifier for the verification request. Must
+ /// be unique with respect to the devices involved.
+ QString transactionId() const;
+
+ /// The verification method to use.
+ QString method() const;
+
+ /// Optional method to use to verify the other user's key with.
+ Omittable<QString> nextMethod() const;
+
+ // SAS.V1 methods
+
+ /// The key agreement protocols the sending device understands.
+ /// \note Only exist if method is m.sas.v1
+ QStringList keyAgreementProtocols() const;
+
+ /// The hash methods the sending device understands.
+ /// \note Only exist if method is m.sas.v1
+ QStringList hashes() const;
+
+ /// The message authentication codes that the sending device understands.
+ /// \note Only exist if method is m.sas.v1
+ QStringList messageAuthenticationCodes() const;
+
+ /// The SAS methods the sending device (and the sending device's
+ /// user) understands.
+ /// \note Only exist if method is m.sas.v1
+ QString shortAuthenticationString() const;
+};
+REGISTER_EVENT_TYPE(KeyVerificationStartEvent)
+
+/// Accepts a previously sent m.key.verification.start message.
+/// Typically sent as a to-device event.
+class KeyVerificationAcceptEvent : public Event {
+ Q_GADGET
+public:
+ DEFINE_EVENT_TYPEID("m.key.verification.accept", KeyVerificationAcceptEvent)
+
+ explicit KeyVerificationAcceptEvent(const QJsonObject& obj);
+
+ /// An opaque identifier for the verification process.
+ QString transactionId() const;
+
+ /// The verification method to use. Must be 'm.sas.v1'.
+ QString method() const;
+
+ /// The key agreement protocol the device is choosing to use, out of
+ /// the options in the m.key.verification.start message.
+ QString keyAgreementProtocol() const;
+
+ /// The hash method the device is choosing to use, out of the
+ /// options in the m.key.verification.start message.
+ QString hashData() const;
+
+ /// The message authentication code the device is choosing to use, out
+ /// of the options in the m.key.verification.start message.
+ QString messageAuthenticationCode() const;
+
+ /// The SAS methods both devices involved in the verification process understand.
+ QStringList shortAuthenticationString() const;
+
+ /// The hash (encoded as unpadded base64) of the concatenation of the
+ /// device's ephemeral public key (encoded as unpadded base64) and the
+ /// canonical JSON representation of the m.key.verification.start message.
+ QString commitement() const;
+};
+REGISTER_EVENT_TYPE(KeyVerificationAcceptEvent)
+
+class KeyVerificationCancelEvent : public Event {
+ Q_GADGET
+public:
+ DEFINE_EVENT_TYPEID("m.key.verification.cancel", KeyVerificationCancelEvent)
+
+ explicit KeyVerificationCancelEvent(const QJsonObject &obj);
+
+ /// An opaque identifier for the verification process.
+ QString transactionId() const;
+
+ /// A human readable description of the code. The client should only
+ /// rely on this string if it does not understand the code.
+ QString reason() const;
+
+ /// The error code for why the process/request was cancelled by the user.
+ QString code() const;
+};
+REGISTER_EVENT_TYPE(KeyVerificationCancelEvent)
+
+/// Sends the ephemeral public key for a device to the partner device.
+/// Typically sent as a to-device event.
+class KeyVerificationKeyEvent : public Event {
+ Q_GADGET
+public:
+ DEFINE_EVENT_TYPEID("m.key.verification.key", KeyVerificationKeyEvent)
+
+ explicit KeyVerificationKeyEvent(const QJsonObject &obj);
+
+ /// An opaque identifier for the verification process.
+ QString transactionId() const;
+
+ /// The device's ephemeral public key, encoded as unpadded base64.
+ QString key() const;
+};
+REGISTER_EVENT_TYPE(KeyVerificationKeyEvent)
+
+/// Sends the MAC of a device's key to the partner device.
+class KeyVerificationMacEvent : public Event {
+ Q_GADGET
+public:
+ DEFINE_EVENT_TYPEID("m.key.verification.mac", KeyVerificationMacEvent)
+
+ explicit KeyVerificationMacEvent(const QJsonObject &obj);
+
+ /// An opaque identifier for the verification process.
+ QString transactionId() const;
+
+ /// The device's ephemeral public key, encoded as unpadded base64.
+ QString keys() const;
+
+ QHash<QString, QString> mac() const;
+};
+REGISTER_EVENT_TYPE(KeyVerificationMacEvent)
+} // namespace Quotient
diff --git a/lib/events/roomevent.cpp b/lib/events/roomevent.cpp
index 3502e3f7..2f482871 100644
--- a/lib/events/roomevent.cpp
+++ b/lib/events/roomevent.cpp
@@ -122,3 +122,18 @@ CallEventBase::CallEventBase(Event::Type type, const QJsonObject& json)
if (callId().isEmpty())
qCWarning(EVENTS) << id() << "is a call event with an empty call id";
}
+
+#ifdef Quotient_E2EE_ENABLED
+void RoomEvent::setOriginalEvent(event_ptr_tt<RoomEvent>&& originalEvent)
+{
+ _originalEvent = std::move(originalEvent);
+}
+
+const QJsonObject RoomEvent::encryptedJson() const
+{
+ if(!_originalEvent) {
+ return {};
+ }
+ return _originalEvent->fullJson();
+}
+#endif
diff --git a/lib/events/roomevent.h b/lib/events/roomevent.h
index dcee1170..c4b0131a 100644
--- a/lib/events/roomevent.h
+++ b/lib/events/roomevent.h
@@ -60,11 +60,21 @@ public:
//! callback for that in RoomEvent.
void addId(const QString& newId);
+#ifdef Quotient_E2EE_ENABLED
+ void setOriginalEvent(event_ptr_tt<RoomEvent>&& originalEvent);
+ const RoomEvent* originalEvent() { return _originalEvent.get(); }
+ const QJsonObject encryptedJson() const;
+#endif
+
protected:
void dumpTo(QDebug dbg) const override;
private:
event_ptr_tt<RedactionEvent> _redactedBecause;
+
+#ifdef Quotient_E2EE_ENABLED
+ event_ptr_tt<RoomEvent> _originalEvent;
+#endif
};
using RoomEventPtr = event_ptr_tt<RoomEvent>;
using RoomEvents = EventsArray<RoomEvent>;
diff --git a/lib/jobs/downloadfilejob.cpp b/lib/jobs/downloadfilejob.cpp
index 4a507ebd..634e5fb9 100644
--- a/lib/jobs/downloadfilejob.cpp
+++ b/lib/jobs/downloadfilejob.cpp
@@ -7,8 +7,12 @@
#include <QtCore/QTemporaryFile>
#include <QtNetwork/QNetworkReply>
-using namespace Quotient;
+#ifdef Quotient_E2EE_ENABLED
+# include <QtCore/QCryptographicHash>
+# include "events/encryptedfile.h"
+#endif
+using namespace Quotient;
class DownloadFileJob::Private {
public:
Private() : tempFile(new QTemporaryFile()) {}
@@ -20,6 +24,10 @@ public:
QScopedPointer<QFile> targetFile;
QScopedPointer<QFile> tempFile;
+
+#ifdef Quotient_E2EE_ENABLED
+ Omittable<EncryptedFile> encryptedFile;
+#endif
};
QUrl DownloadFileJob::makeRequestUrl(QUrl baseUrl, const QUrl& mxcUri)
@@ -38,6 +46,19 @@ DownloadFileJob::DownloadFileJob(const QString& serverName,
setObjectName(QStringLiteral("DownloadFileJob"));
}
+#ifdef Quotient_E2EE_ENABLED
+DownloadFileJob::DownloadFileJob(const QString& serverName,
+ const QString& mediaId,
+ const EncryptedFile& file,
+ const QString& localFilename)
+ : GetContentJob(serverName, mediaId)
+ , d(localFilename.isEmpty() ? makeImpl<Private>()
+ : makeImpl<Private>(localFilename))
+{
+ setObjectName(QStringLiteral("DownloadFileJob"));
+ d->encryptedFile = file;
+}
+#endif
QString DownloadFileJob::targetFileName() const
{
return (d->targetFile ? d->targetFile : d->tempFile)->fileName();
@@ -52,7 +73,7 @@ void DownloadFileJob::doPrepare()
setStatus(FileError, "Could not open the target file for writing");
return;
}
- if (!d->tempFile->isReadable() && !d->tempFile->open(QIODevice::WriteOnly)) {
+ if (!d->tempFile->isReadable() && !d->tempFile->open(QIODevice::ReadWrite)) {
qCWarning(JOBS) << "Couldn't open the temporary file"
<< d->tempFile->fileName() << "for writing";
setStatus(FileError, "Could not open the temporary download file");
@@ -100,18 +121,46 @@ void DownloadFileJob::beforeAbandon()
BaseJob::Status DownloadFileJob::prepareResult()
{
if (d->targetFile) {
- d->targetFile->close();
- if (!d->targetFile->remove()) {
- qCWarning(JOBS) << "Failed to remove the target file placeholder";
- return { FileError, "Couldn't finalise the download" };
+#ifdef Quotient_E2EE_ENABLED
+ if (d->encryptedFile.has_value()) {
+ d->tempFile->seek(0);
+ QByteArray encrypted = d->tempFile->readAll();
+
+ EncryptedFile file = *d->encryptedFile;
+ auto decrypted = file.decryptFile(encrypted);
+ d->targetFile->write(decrypted);
+ d->tempFile->remove();
+ } else {
+#endif
+ d->targetFile->close();
+ if (!d->targetFile->remove()) {
+ qCWarning(JOBS) << "Failed to remove the target file placeholder";
+ return { FileError, "Couldn't finalise the download" };
+ }
+ if (!d->tempFile->rename(d->targetFile->fileName())) {
+ qCWarning(JOBS) << "Failed to rename" << d->tempFile->fileName()
+ << "to" << d->targetFile->fileName();
+ return { FileError, "Couldn't finalise the download" };
+ }
+#ifdef Quotient_E2EE_ENABLED
}
- if (!d->tempFile->rename(d->targetFile->fileName())) {
- qCWarning(JOBS) << "Failed to rename" << d->tempFile->fileName()
- << "to" << d->targetFile->fileName();
- return { FileError, "Couldn't finalise the download" };
+#endif
+ } else {
+#ifdef Quotient_E2EE_ENABLED
+ if (d->encryptedFile.has_value()) {
+ d->tempFile->seek(0);
+ auto encrypted = d->tempFile->readAll();
+
+ EncryptedFile file = *d->encryptedFile;
+ auto decrypted = file.decryptFile(encrypted);
+ d->tempFile->write(decrypted);
+ } else {
+#endif
+ d->tempFile->close();
+#ifdef Quotient_E2EE_ENABLED
}
- } else
- d->tempFile->close();
+#endif
+ }
qCDebug(JOBS) << "Saved a file as" << targetFileName();
return Success;
}
diff --git a/lib/jobs/downloadfilejob.h b/lib/jobs/downloadfilejob.h
index f8c62e4b..ffa3d055 100644
--- a/lib/jobs/downloadfilejob.h
+++ b/lib/jobs/downloadfilejob.h
@@ -4,6 +4,7 @@
#pragma once
#include "csapi/content-repo.h"
+#include "events/encryptedfile.h"
namespace Quotient {
class QUOTIENT_API DownloadFileJob : public GetContentJob {
@@ -14,6 +15,9 @@ public:
DownloadFileJob(const QString& serverName, const QString& mediaId,
const QString& localFilename = {});
+#ifdef Quotient_E2EE_ENABLED
+ DownloadFileJob(const QString& serverName, const QString& mediaId, const EncryptedFile& file, const QString& localFilename = {});
+#endif
QString targetFileName() const;
private:
diff --git a/lib/logging.cpp b/lib/logging.cpp
index 15eac69d..460caced 100644
--- a/lib/logging.cpp
+++ b/lib/logging.cpp
@@ -19,3 +19,4 @@ LOGGING_CATEGORY(SYNCJOB, "quotient.jobs.sync")
LOGGING_CATEGORY(THUMBNAILJOB, "quotient.jobs.thumbnail")
LOGGING_CATEGORY(NETWORK, "quotient.network")
LOGGING_CATEGORY(PROFILER, "quotient.profiler")
+LOGGING_CATEGORY(DATABASE, "quotient.database")
diff --git a/lib/logging.h b/lib/logging.h
index 5bf050a9..fc0a4c99 100644
--- a/lib/logging.h
+++ b/lib/logging.h
@@ -19,6 +19,7 @@ Q_DECLARE_LOGGING_CATEGORY(SYNCJOB)
Q_DECLARE_LOGGING_CATEGORY(THUMBNAILJOB)
Q_DECLARE_LOGGING_CATEGORY(NETWORK)
Q_DECLARE_LOGGING_CATEGORY(PROFILER)
+Q_DECLARE_LOGGING_CATEGORY(DATABASE)
namespace Quotient {
// QDebug manipulators
diff --git a/lib/mxcreply.cpp b/lib/mxcreply.cpp
index d3cc3c37..1d40c5e1 100644
--- a/lib/mxcreply.cpp
+++ b/lib/mxcreply.cpp
@@ -3,8 +3,15 @@
#include "mxcreply.h"
+#include <QtCore/QBuffer>
+#include "accountregistry.h"
+#include "connection.h"
#include "room.h"
+#ifdef Quotient_E2EE_ENABLED
+#include "events/encryptedfile.h"
+#endif
+
using namespace Quotient;
class MxcReply::Private
@@ -14,11 +21,14 @@ public:
: m_reply(r)
{}
QNetworkReply* m_reply;
+ Omittable<EncryptedFile> m_encryptedFile;
+ QIODevice* m_device = nullptr;
};
MxcReply::MxcReply(QNetworkReply* reply)
: d(makeImpl<Private>(reply))
{
+ d->m_device = d->m_reply;
reply->setParent(this);
connect(d->m_reply, &QNetworkReply::finished, this, [this]() {
setError(d->m_reply->error(), d->m_reply->errorString());
@@ -31,11 +41,33 @@ MxcReply::MxcReply(QNetworkReply* reply, Room* room, const QString &eventId)
: d(makeImpl<Private>(reply))
{
reply->setParent(this);
- connect(d->m_reply, &QNetworkReply::finished, this, [this, room, eventId]() {
+ connect(d->m_reply, &QNetworkReply::finished, this, [this]() {
setError(d->m_reply->error(), d->m_reply->errorString());
+
+#ifdef Quotient_E2EE_ENABLED
+ if(!d->m_encryptedFile.has_value()) {
+ d->m_device = d->m_reply;
+ } else {
+ EncryptedFile file = *d->m_encryptedFile;
+ auto buffer = new QBuffer(this);
+ buffer->setData(file.decryptFile(d->m_reply->readAll()));
+ buffer->open(ReadOnly);
+ d->m_device = buffer;
+ }
setOpenMode(ReadOnly);
emit finished();
+#else
+ d->m_device = d->m_reply;
+#endif
});
+
+#ifdef Quotient_E2EE_ENABLED
+ auto eventIt = room->findInTimeline(eventId);
+ if(eventIt != room->historyEdge()) {
+ auto event = eventIt->viewAs<RoomMessageEvent>();
+ d->m_encryptedFile = event->content()->fileInfo()->file;
+ }
+#endif
}
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
@@ -62,7 +94,7 @@ MxcReply::MxcReply()
qint64 MxcReply::readData(char *data, qint64 maxSize)
{
- return d->m_reply->read(data, maxSize);
+ return d->m_device->read(data, maxSize);
}
void MxcReply::abort()
diff --git a/lib/networkaccessmanager.cpp b/lib/networkaccessmanager.cpp
index 58c3cc3a..f4e7b1af 100644
--- a/lib/networkaccessmanager.cpp
+++ b/lib/networkaccessmanager.cpp
@@ -47,6 +47,17 @@ QList<QSslError> NetworkAccessManager::ignoredSslErrors() const
return d->ignoredSslErrors;
}
+void NetworkAccessManager::ignoreSslErrors(bool ignore) const
+{
+ if (ignore) {
+ connect(this, &QNetworkAccessManager::sslErrors, this, [](QNetworkReply *reply, const QList<QSslError> &errors) {
+ reply->ignoreSslErrors();
+ });
+ } else {
+ disconnect(this, &QNetworkAccessManager::sslErrors, this, nullptr);
+ }
+}
+
void NetworkAccessManager::addIgnoredSslError(const QSslError& error)
{
d->ignoredSslErrors << error;
diff --git a/lib/networkaccessmanager.h b/lib/networkaccessmanager.h
index 8ff1c6b5..01b0599d 100644
--- a/lib/networkaccessmanager.h
+++ b/lib/networkaccessmanager.h
@@ -17,6 +17,7 @@ public:
QList<QSslError> ignoredSslErrors() const;
void addIgnoredSslError(const QSslError& error);
void clearIgnoredSslErrors();
+ void ignoreSslErrors(bool ignore = true) const;
/** Get a pointer to the singleton */
static NetworkAccessManager* instance();
diff --git a/lib/room.cpp b/lib/room.cpp
index abd6110c..ea9915c3 100644
--- a/lib/room.cpp
+++ b/lib/room.cpp
@@ -12,7 +12,6 @@
#include "avatar.h"
#include "connection.h"
#include "converters.h"
-#include "e2ee.h"
#include "syncdata.h"
#include "user.h"
#include "eventstats.h"
@@ -66,13 +65,15 @@
#include <functional>
#ifdef Quotient_E2EE_ENABLED
-#include <account.h> // QtOlm
-#include <errors.h> // QtOlm
-#include <groupsession.h> // QtOlm
+#include "e2ee/e2ee.h"
+#include "e2ee/qolmaccount.h"
+#include "e2ee/qolmerrors.h"
+#include "e2ee/qolminboundsession.h"
+#include "database.h"
#endif // Quotient_E2EE_ENABLED
+
using namespace Quotient;
-using namespace QtOlm;
using namespace std::placeholders;
using std::move;
#if !(defined __GLIBCXX__ && __GLIBCXX__ <= 20150123)
@@ -138,6 +139,8 @@ public:
QString prevBatch;
QPointer<GetRoomEventsJob> eventsHistoryJob;
QPointer<GetMembersByRoomJob> allMembersJob;
+ // Map from megolm sessionId to set of eventIds
+ UnorderedMap<QString, QSet<QString>> undecryptedEvents;
struct FileTransferPrivateInfo {
FileTransferPrivateInfo() = default;
@@ -334,40 +337,27 @@ public:
bool isLocalUser(const User* u) const { return u == q->localUser(); }
#ifdef Quotient_E2EE_ENABLED
- // A map from <sessionId, messageIndex> to <event_id, origin_server_ts>
- QHash<QPair<QString, uint32_t>, QPair<QString, QDateTime>>
- groupSessionIndexRecord; // TODO: cache
- // 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>, InboundGroupSession*> groupSessions; // TODO:
- // cache
+ // A map from (senderKey, sessionId) to InboundGroupSession
+ UnorderedMap<std::pair<QString, QString>, QOlmInboundGroupSessionPtr> groupSessions;
+
bool addInboundGroupSession(QString senderKey, QString sessionId,
QString sessionKey)
{
- if (groupSessions.contains({ senderKey, sessionId })) {
- qCDebug(E2EE) << "Inbound Megolm session" << sessionId
+ if (groupSessions.find({senderKey, sessionId}) != groupSessions.end()) {
+ qCWarning(E2EE) << "Inbound Megolm session" << sessionId
<< "with senderKey" << senderKey << "already exists";
return false;
}
- InboundGroupSession* megolmSession;
- try {
- megolmSession = new InboundGroupSession(sessionKey.toLatin1(),
- InboundGroupSession::Init,
- q);
- } catch (OlmError* e) {
- qCDebug(E2EE) << "Unable to create new InboundGroupSession"
- << e->what();
+ auto 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";
+ connection->saveMegolmSession(q, senderKey, megolmSession.get());
+ groupSessions[{senderKey, sessionId}] = std::move(megolmSession);
return true;
}
@@ -377,44 +367,31 @@ public:
const QString& eventId,
QDateTime timestamp)
{
- std::pair<QString, uint32_t> decrypted;
- QPair<QString, QString> senderSessionPairKey =
- qMakePair(senderKey, sessionId);
- if (!groupSessions.contains(senderSessionPairKey)) {
- qCDebug(E2EE) << "Unable to decrypt event" << eventId
- << "The sender's device has not sent us the keys for "
- "this message";
+ auto groupSessionIt = groupSessions.find({ senderKey, sessionId });
+ if (groupSessionIt == groupSessions.end()) {
+ // qCWarning(E2EE) << "Unable to decrypt event" << eventId
+ // << "The sender's device has not sent us the keys for "
+ // "this message";
return QString();
}
- InboundGroupSession* senderSession =
- groupSessions.value(senderSessionPairKey);
- if (!senderSession) {
- qCDebug(E2EE) << "Unable to decrypt event" << eventId
- << "senderSessionPairKey:" << senderSessionPairKey;
+ auto& senderSession = groupSessionIt->second;
+ 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 (OlmError* 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));
- if (properties.first.isEmpty()) {
- groupSessionIndexRecord.insert(qMakePair(senderSession->id(),
- decrypted.second),
- qMakePair(eventId, timestamp));
+ const auto& [content, index] = std::get<std::pair<QString, uint32_t>>(decryptResult);
+ const auto& [recordEventId, ts] = q->connection()->database()->groupSessionIndexRecord(q->id(), senderSession->sessionId(), index);
+ if (recordEventId.isEmpty()) {
+ q->connection()->database()->addGroupSessionIndexRecord(q->id(), senderSession->sessionId(), index, eventId, timestamp.toMSecsSinceEpoch());
} else {
- if ((properties.first != eventId)
- || (properties.second != timestamp)) {
- qCDebug(E2EE) << "Detected a replay attack on event" << eventId;
+ if ((eventId != recordEventId) || (ts != timestamp.toMSecsSinceEpoch())) {
+ qCWarning(E2EE) << "Detected a replay attack on event" << eventId;
return QString();
}
}
-
- return decrypted.first;
+ return content;
}
#endif // Quotient_E2EE_ENABLED
@@ -440,6 +417,21 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState)
emit baseStateLoaded();
return this == r; // loadedRoomState fires only once per room
});
+#ifdef Quotient_E2EE_ENABLED
+ connectSingleShot(this, &Room::encryption, this, [this, connection](){
+ connection->encryptionUpdate(this);
+ });
+ connect(this, &Room::userAdded, this, [this, connection](){
+ if(usesEncryption()) {
+ connection->encryptionUpdate(this);
+ }
+ });
+ d->groupSessions = connection->loadRoomMegolmSessions(this);
+
+ connect(this, &Room::beforeDestruction, this, [=](){
+ connection->database()->clearRoomData(id);
+ });
+#endif
qCDebug(STATE) << "New" << terse << initialJoinState << "Room:" << id;
}
@@ -1482,20 +1474,20 @@ RoomEventPtr Room::decryptMessage(const EncryptedEvent& encryptedEvent)
qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off.";
return {};
#else // Quotient_E2EE_ENABLED
- if (encryptedEvent.algorithm() == MegolmV1AesSha2AlgoKey) {
- QString decrypted = d->groupSessionDecryptMessage(
- encryptedEvent.ciphertext(), encryptedEvent.senderKey(),
- encryptedEvent.sessionId(), encryptedEvent.id(),
- encryptedEvent.originTimestamp());
- if (decrypted.isEmpty()) {
- return {};
- }
- return makeEvent<RoomMessageEvent>(
- QJsonDocument::fromJson(decrypted.toUtf8()).object());
+ if (encryptedEvent.algorithm() != MegolmV1AesSha2AlgoKey) {
+ qWarning(E2EE) << "Algorithm of the encrypted event with id"
+ << encryptedEvent.id() << "is not decryptable by the current device";
+ return {};
}
- qCDebug(E2EE) << "Algorithm of the encrypted event with id"
- << encryptedEvent.id() << "is not for the current device";
- return {};
+ QString decrypted = d->groupSessionDecryptMessage(
+ encryptedEvent.ciphertext(), encryptedEvent.senderKey(),
+ encryptedEvent.sessionId(), encryptedEvent.id(),
+ encryptedEvent.originTimestamp());
+ if (decrypted.isEmpty()) {
+ // qCWarning(E2EE) << "Encrypted message is empty";
+ return {};
+ }
+ return encryptedEvent.createDecrypted(decrypted);
#endif // Quotient_E2EE_ENABLED
}
@@ -1513,8 +1505,25 @@ 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();
+ for (const auto& eventId : d->undecryptedEvents[roomKeyEvent.sessionId()]) {
+ const auto pIdx = d->eventsIndex.constFind(eventId);
+ if (pIdx == d->eventsIndex.cend())
+ continue;
+ auto& ti = d->timeline[Timeline::size_type(*pIdx - minTimelineIndex())];
+ if (auto encryptedEvent = ti.viewAs<EncryptedEvent>()) {
+ auto decrypted = decryptMessage(*encryptedEvent);
+ if(decrypted) {
+ // The reference will survive the pointer being moved
+ auto& decryptedEvent = *decrypted;
+ auto oldEvent = ti.replaceEvent(std::move(decrypted));
+ decryptedEvent.setOriginalEvent(std::move(oldEvent));
+ emit replacedEvent(ti.event(), decrypted->originalEvent());
+ d->undecryptedEvents[roomKeyEvent.sessionId()] -= eventId;
+ }
+ }
+ }
}
#endif // Quotient_E2EE_ENABLED
}
@@ -1905,7 +1914,6 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent)
return;
}
it->setDeparted();
- qCDebug(EVENTS) << "Event txn" << txnId << "has departed";
emit q->pendingEventChanged(int(it - unsyncedEvents.begin()));
});
Room::connect(call, &BaseJob::failure, q,
@@ -2343,7 +2351,17 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename)
filePath = QDir::tempPath() % '/' % filePath;
qDebug(MAIN) << "File path:" << filePath;
}
- auto job = connection()->downloadFile(fileUrl, filePath);
+ DownloadFileJob *job = nullptr;
+#ifdef Quotient_E2EE_ENABLED
+ if(fileInfo->file.has_value()) {
+ auto file = *fileInfo->file;
+ job = connection()->downloadFile(fileUrl, file, filePath);
+ } else {
+#endif
+ job = connection()->downloadFile(fileUrl, filePath);
+#ifdef Quotient_E2EE_ENABLED
+ }
+#endif
if (isJobPending(job)) {
// If there was a previous transfer (completed or failed), overwrite it.
d->fileTransfers[eventId] = { job, job->targetFileName() };
@@ -2595,6 +2613,21 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events)
QElapsedTimer et;
et.start();
+
+#ifdef Quotient_E2EE_ENABLED
+ for(long unsigned int i = 0; i < events.size(); i++) {
+ if(auto* encrypted = eventCast<EncryptedEvent>(events[i])) {
+ auto decrypted = q->decryptMessage(*encrypted);
+ if(decrypted) {
+ auto oldEvent = std::exchange(events[i], std::move(decrypted));
+ events[i]->setOriginalEvent(std::move(oldEvent));
+ } else {
+ undecryptedEvents[encrypted->sessionId()] += encrypted->id();
+ }
+ }
+ }
+#endif
+
{
// Pre-process redactions and edits so that events that get
// redacted/replaced in the same batch landed in the timeline already
@@ -2747,6 +2780,21 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events)
return;
Changes changes {};
+
+#ifdef Quotient_E2EE_ENABLED
+ for(long unsigned int i = 0; i < events.size(); i++) {
+ if(auto* encrypted = eventCast<EncryptedEvent>(events[i])) {
+ auto decrypted = q->decryptMessage(*encrypted);
+ if(decrypted) {
+ auto oldEvent = std::exchange(events[i], std::move(decrypted));
+ events[i]->setOriginalEvent(std::move(oldEvent));
+ } else {
+ undecryptedEvents[encrypted->sessionId()] += encrypted->id();
+ }
+ }
+ }
+#endif
+
// 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
diff --git a/lib/settings.cpp b/lib/settings.cpp
index 2491d89d..510d253c 100644
--- a/lib/settings.cpp
+++ b/lib/settings.cpp
@@ -138,18 +138,12 @@ void AccountSettings::clearAccessToken()
QByteArray AccountSettings::encryptionAccountPickle()
{
- QString passphrase = ""; // FIXME: add QtKeychain
return value("encryption_account_pickle", "").toByteArray();
}
void AccountSettings::setEncryptionAccountPickle(
const QByteArray& encryptionAccountPickle)
{
- qCWarning(MAIN)
- << "Saving encryption_account_pickle to QSettings is insecure."
- " Developers, do it manually or contribute to share QtKeychain "
- "logic to libQuotient.";
- QString passphrase = ""; // FIXME: add QtKeychain
setValue("encryption_account_pickle", encryptionAccountPickle);
}
diff --git a/lib/syncdata.cpp b/lib/syncdata.cpp
index b0cd8e4d..78957cbe 100644
--- a/lib/syncdata.cpp
+++ b/lib/syncdata.cpp
@@ -99,6 +99,34 @@ SyncRoomData::SyncRoomData(QString roomId_, JoinState joinState,
fromJson(unreadJson.value(HighlightCountKey), highlightCount);
}
+QDebug Quotient::operator<<(QDebug dbg, const DevicesList& devicesList)
+{
+ QDebugStateSaver _(dbg);
+ QStringList sl;
+ if (!devicesList.changed.isEmpty())
+ sl << QStringLiteral("changed: %1").arg(devicesList.changed.join(", "));
+ if (!devicesList.left.isEmpty())
+ sl << QStringLiteral("left %1").arg(devicesList.left.join(", "));
+ dbg.nospace().noquote() << sl.join(QStringLiteral("; "));
+ return dbg;
+}
+
+void JsonObjectConverter<DevicesList>::dumpTo(QJsonObject& jo,
+ const DevicesList& rs)
+{
+ addParam<IfNotEmpty>(jo, QStringLiteral("changed"),
+ rs.changed);
+ addParam<IfNotEmpty>(jo, QStringLiteral("left"),
+ rs.left);
+}
+
+void JsonObjectConverter<DevicesList>::fillFrom(const QJsonObject& jo,
+ DevicesList& rs)
+{
+ fromJson(jo["changed"_ls], rs.changed);
+ fromJson(jo["left"_ls], rs.left);
+}
+
SyncData::SyncData(const QString& cacheFileName)
{
QFileInfo cacheFileInfo { cacheFileName };
@@ -133,6 +161,8 @@ std::pair<int, int> SyncData::cacheVersion()
return { MajorCacheVersion, 2 };
}
+DevicesList&& SyncData::takeDevicesList() { return std::move(devicesList); }
+
QJsonObject SyncData::loadJson(const QString& fileName)
{
QFile roomFile { fileName };
@@ -175,6 +205,10 @@ void SyncData::parseJson(const QJsonObject& json, const QString& baseDir)
fromJson(json.value("device_one_time_keys_count"_ls),
deviceOneTimeKeysCount_);
+ if(json.contains("device_lists")) {
+ fromJson(json.value("device_lists"), devicesList);
+ }
+
auto rooms = json.value("rooms"_ls).toObject();
auto totalRooms = 0;
auto totalEvents = 0;
diff --git a/lib/syncdata.h b/lib/syncdata.h
index e29540c2..633f4b52 100644
--- a/lib/syncdata.h
+++ b/lib/syncdata.h
@@ -41,6 +41,27 @@ struct JsonObjectConverter<RoomSummary> {
static void fillFrom(const QJsonObject& jo, RoomSummary& rs);
};
+/// Information on e2e device updates. Note: only present on an
+/// incremental sync.
+struct DevicesList {
+ /// List of users who have updated their device identity keys, or who
+ /// now share an encrypted room with the client since the previous
+ /// sync response.
+ QStringList changed;
+
+ /// List of users with whom we do not share any encrypted rooms
+ /// anymore since the previous sync response.
+ QStringList left;
+};
+
+QDebug operator<<(QDebug dhg, const DevicesList &devicesList);
+
+template <>
+struct JsonObjectConverter<DevicesList> {
+ static void dumpTo(QJsonObject &jo, const DevicesList &dev);
+ static void fillFrom(const QJsonObject& jo, DevicesList& rs);
+};
+
class SyncRoomData {
public:
QString roomId;
@@ -85,6 +106,7 @@ public:
return deviceOneTimeKeysCount_;
}
SyncDataList&& takeRoomData();
+ DevicesList&& takeDevicesList();
QString nextBatch() const { return nextBatch_; }
@@ -102,6 +124,7 @@ private:
SyncDataList roomData;
QStringList unresolvedRoomIds;
QHash<QString, int> deviceOneTimeKeysCount_;
+ DevicesList devicesList;
static QJsonObject loadJson(const QString& fileName);
};