aboutsummaryrefslogtreecommitdiff
path: root/lib/connection.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'lib/connection.cpp')
-rw-r--r--lib/connection.cpp1167
1 files changed, 931 insertions, 236 deletions
diff --git a/lib/connection.cpp b/lib/connection.cpp
index 853053bd..4547474a 100644
--- a/lib/connection.cpp
+++ b/lib/connection.cpp
@@ -1,55 +1,55 @@
-/******************************************************************************
- * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-FileCopyrightText: 2017 Roman Plášil <me@rplasil.name>
+// SPDX-FileCopyrightText: 2019 Ville Ranki <ville.ranki@iki.fi>
+// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#include "connection.h"
+#include "accountregistry.h"
#include "connectiondata.h"
-#ifdef Quotient_E2EE_ENABLED
-# include "encryptionmanager.h"
-#endif // Quotient_E2EE_ENABLED
+#include "qt_connection_util.h"
#include "room.h"
#include "settings.h"
#include "user.h"
+// NB: since Qt 6, moc_connection.cpp needs Room and User fully defined
+#include "moc_connection.cpp"
+
#include "csapi/account-data.h"
#include "csapi/capabilities.h"
#include "csapi/joining.h"
#include "csapi/leaving.h"
#include "csapi/logout.h"
-#include "csapi/receipts.h"
#include "csapi/room_send.h"
#include "csapi/to_device.h"
-#include "csapi/versions.h"
#include "csapi/voip.h"
#include "csapi/wellknown.h"
+#include "csapi/whoami.h"
#include "events/directchatevent.h"
-#include "events/eventloader.h"
#include "jobs/downloadfilejob.h"
#include "jobs/mediathumbnailjob.h"
#include "jobs/syncjob.h"
+#include <variant>
#ifdef Quotient_E2EE_ENABLED
-# include "account.h" // QtOlm
+# include "database.h"
+# include "keyverificationsession.h"
+
+# include "e2ee/qolmaccount.h"
+# include "e2ee/qolminboundsession.h"
+# include "e2ee/qolmsession.h"
+# include "e2ee/qolmutility.h"
+# include "e2ee/qolmutils.h"
+
+# include "events/keyverificationevent.h"
#endif // Quotient_E2EE_ENABLED
-#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
-# include <QtCore/QCborValue>
+#if QT_VERSION_MAJOR >= 6
+# include <qt6keychain/keychain.h>
+#else
+# include <qt5keychain/keychain.h>
#endif
#include <QtCore/QCoreApplication>
@@ -66,7 +66,7 @@ using namespace Quotient;
// This is very much Qt-specific; STL iterators don't have key() and value()
template <typename HashT, typename Pred>
-HashT erase_if(HashT& hashMap, Pred pred)
+HashT remove_if(HashT& hashMap, Pred pred)
{
HashT removals;
for (auto it = hashMap.begin(); it != hashMap.end();) {
@@ -84,8 +84,6 @@ public:
explicit Private(std::unique_ptr<ConnectionData>&& connection)
: data(move(connection))
{}
- Q_DISABLE_COPY(Private)
- DISABLE_MOVE(Private)
Connection* q = nullptr;
std::unique_ptr<ConnectionData> data;
@@ -93,12 +91,11 @@ public:
// state is Invited. The spec mandates to keep Invited room state
// separately; specifically, we should keep objects for Invite and
// Leave state of the same room if the two happen to co-exist.
- QHash<QPair<QString, bool>, Room*> roomMap;
+ QHash<std::pair<QString, bool>, Room*> roomMap;
/// Mapping from serverparts to alias/room id mappings,
/// as of the last sync
QHash<QString, QString> roomAliasMap;
QVector<QString> roomIdsToForget;
- QVector<Room*> firstTimeRooms;
QVector<QString> pendingStateRoomIds;
QMap<QString, User*> userMap;
DirectChatsMap directChats;
@@ -111,13 +108,34 @@ 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;
+ QHash<QString, int> oneTimeKeysCount;
+ std::vector<std::unique_ptr<EncryptedEvent>> pendingEncryptedEvents;
+ void handleEncryptedToDeviceEvent(const EncryptedEvent& event);
+ bool processIfVerificationEvent(const Event &evt, bool encrypted);
+
+ // A map from SenderKey to vector of InboundSession
+ UnorderedMap<QString, std::vector<QOlmSessionPtr>> olmSessions;
+
+ QHash<QString, KeyVerificationSession*> verificationSessions;
+#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;
+ bool firstSync = true;
#endif // Quotient_E2EE_ENABLED
QPointer<GetWellknownJob> resolverJob = nullptr;
@@ -133,11 +151,6 @@ public:
!= "json";
bool lazyLoading = false;
- /// \brief Stop resolving and login flows jobs, and clear login flows
- ///
- /// Prepares the class to set or resolve a new homeserver
- void clearResolvingContext();
-
/** \brief Check the homeserver and resolve it if needed, before connecting
*
* A single entry for functions that need to check whether the homeserver
@@ -155,25 +168,17 @@ public:
*/
void checkAndConnect(const QString &userId,
const std::function<void ()> &connectFn,
- const std::optional<LoginFlows::LoginFlow> &flow = none);
+ const std::optional<LoginFlow> &flow = none);
template <typename... LoginArgTs>
void loginToServer(LoginArgTs&&... loginArgs);
- void completeSetup(const QString& mxId);
+ void completeSetup(const QString &mxId);
void removeRoom(const QString& roomId);
void consumeRoomData(SyncDataList&& roomDataList, bool fromCache);
void consumeAccountData(Events&& accountDataEvents);
void consumePresenceData(Events&& presenceData);
void consumeToDeviceEvents(Events&& toDeviceEvents);
-
- template <typename EventT>
- EventT* unpackAccountData() const
- {
- const auto& eventIt = accountData.find(EventT::matrixTypeId());
- return eventIt == accountData.end()
- ? nullptr
- : weakPtrCast<EventT>(eventIt->second);
- }
+ void consumeDevicesList(DevicesList&& devicesList);
void packAndSendAccountData(EventPtr&& event)
{
@@ -184,7 +189,7 @@ public:
emit q->accountDataChanged(eventType);
}
- template <typename EventT, typename ContentT>
+ template <EventClass EventT, typename ContentT>
void packAndSendAccountData(ContentT&& content)
{
packAndSendAccountData(
@@ -195,36 +200,115 @@ public:
return q->stateCacheDir().filePath("state.json");
}
- EventPtr sessionDecryptMessage(const EncryptedEvent& encryptedEvent)
+#ifdef Quotient_E2EE_ENABLED
+ void loadSessions() {
+ olmSessions = q->database()->loadOlmSessions(picklingMode);
+ }
+ void saveSession(const QOlmSession& session, const QString& senderKey) const
+ {
+ q->database()->saveOlmSession(senderKey, session.sessionId(),
+ session.pickle(picklingMode),
+ QDateTime::currentDateTime());
+ }
+
+ template <typename FnT>
+ std::pair<QString, QString> doDecryptMessage(const QOlmSession& session,
+ const QOlmMessage& message,
+ FnT&& andThen) const
+ {
+ const auto expectedMessage = session.decrypt(message);
+ if (expectedMessage) {
+ const auto result =
+ std::make_pair(*expectedMessage, session.sessionId());
+ andThen();
+ return result;
+ }
+ const auto errorLine = message.type() == QOlmMessage::PreKey
+ ? "Failed to decrypt prekey message:"
+ : "Failed to decrypt message:";
+ qCDebug(E2EE) << errorLine << expectedMessage.error();
+ return {};
+ }
+
+ std::pair<QString, QString> sessionDecryptMessage(
+ const QJsonObject& personalCipherObject, const QByteArray& senderKey)
+ {
+ const auto msgType = static_cast<QOlmMessage::Type>(
+ personalCipherObject.value(TypeKeyL).toInt(-1));
+ if (msgType != QOlmMessage::General && msgType != QOlmMessage::PreKey) {
+ qCWarning(E2EE) << "Olm message has incorrect type" << msgType;
+ return {};
+ }
+ QOlmMessage message {
+ personalCipherObject.value(BodyKeyL).toString().toLatin1(), msgType
+ };
+ for (const auto& session : olmSessions[senderKey])
+ if (msgType == QOlmMessage::General
+ || session->matchesInboundSessionFrom(senderKey, message)) {
+ return doDecryptMessage(*session, message, [this, &session] {
+ q->database()->setOlmSessionLastReceived(
+ session->sessionId(), QDateTime::currentDateTime());
+ });
+ }
+
+ if (msgType == QOlmMessage::General) {
+ qCWarning(E2EE) << "Failed to decrypt message";
+ return {};
+ }
+
+ qCDebug(E2EE) << "Creating new inbound session"; // Pre-key messages only
+ auto newSessionResult =
+ olmAccount->createInboundSessionFrom(senderKey, message);
+ if (!newSessionResult) {
+ qCWarning(E2EE)
+ << "Failed to create inbound session for" << senderKey
+ << "with error" << newSessionResult.error();
+ return {};
+ }
+ auto newSession = std::move(*newSessionResult);
+ if (olmAccount->removeOneTimeKeys(*newSession) != OLM_SUCCESS) {
+ qWarning(E2EE) << "Failed to remove one time key for session"
+ << newSession->sessionId();
+ // Keep going though
+ }
+ return doDecryptMessage(
+ *newSession, message, [this, &senderKey, &newSession] {
+ saveSession(*newSession, senderKey);
+ olmSessions[senderKey].push_back(std::move(newSession));
+ });
+ }
+#endif
+
+ std::pair<EventPtr, QString> 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, olmSessionId] =
+ sessionDecryptMessage(personalCipherObject,
+ encryptedEvent.senderKey().toLatin1());
if (decrypted.isEmpty()) {
qCDebug(E2EE) << "Problem with new session from senderKey:"
<< encryptedEvent.senderKey()
- << encryptionManager->account()->oneTimeKeys();
+ << olmAccount->oneTimeKeys().keys;
return {};
}
auto&& decryptedEvent =
fromJson<EventPtr>(QJsonDocument::fromJson(decrypted.toUtf8()));
- if (auto sender = decryptedEvent->fullJson()["sender"_ls].toString();
+ if (auto sender = decryptedEvent->fullJson()[SenderKeyL].toString();
sender != encryptedEvent.senderId()) {
qCWarning(E2EE) << "Found user" << sender
<< "instead of sender" << encryptedEvent.senderId()
@@ -232,36 +316,97 @@ public:
return {};
}
+ auto query = database->prepareQuery(QStringLiteral("SELECT edKey FROM tracked_devices WHERE curveKey=:curveKey;"));
+ query.bindValue(":curveKey", encryptedEvent.contentJson()["sender_key"].toString());
+ database->execute(query);
+ if (!query.next()) {
+ qCWarning(E2EE) << "Received olm message from unknown device" << encryptedEvent.contentJson()["sender_key"].toString();
+ return {};
+ }
+ auto edKey = decryptedEvent->fullJson()["keys"]["ed25519"].toString();
+ if (edKey.isEmpty() || query.value(QStringLiteral("edKey")).toString() != edKey) {
+ qCDebug(E2EE) << "Received olm message with invalid ed key";
+ return {};
+ }
+
// 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 {};
}
- return std::move(decryptedEvent);
+ return { std::move(decryptedEvent), olmSessionId };
#endif // Quotient_E2EE_ENABLED
}
+#ifdef Quotient_E2EE_ENABLED
+ bool isKnownCurveKey(const QString& userId, const QString& curveKey) const;
+
+ void loadOutdatedUserDevices();
+ void saveDevicesList();
+ void loadDevicesList();
+
+ // This function assumes that an olm session with (user, device) exists
+ std::pair<QOlmMessage::Type, QByteArray> olmEncryptMessage(
+ const QString& userId, const QString& device,
+ const QByteArray& message) const;
+ bool createOlmSession(const QString& targetUserId,
+ const QString& targetDeviceId,
+ const OneTimeKeys &oneTimeKeyObject);
+ QString curveKeyForUserDevice(const QString& userId,
+ const QString& device) const;
+ QJsonObject assembleEncryptedContent(QJsonObject payloadJson,
+ const QString& targetUserId,
+ const QString& targetDeviceId) const;
+#endif
+
+ void saveAccessTokenToKeychain() const
+ {
+ qCDebug(MAIN) << "Saving access token to keychain for" << q->userId();
+ auto job = new QKeychain::WritePasswordJob(qAppName());
+ job->setAutoDelete(true);
+ job->setKey(q->userId());
+ job->setBinaryData(data->accessToken());
+ job->start();
+ //TODO error handling
+ }
+
+ void dropAccessToken()
+ {
+ qCDebug(MAIN) << "Removing access token from keychain for" << q->userId();
+ auto job = new QKeychain::DeletePasswordJob(qAppName());
+ job->setAutoDelete(true);
+ job->setKey(q->userId());
+ job->start();
+
+ auto pickleJob = new QKeychain::DeletePasswordJob(qAppName());
+ pickleJob->setAutoDelete(true);
+ pickleJob->setKey(q->userId() + "-Pickle"_ls);
+ pickleJob->start();
+ //TODO error handling
+
+ data->setToken({});
+ }
};
Connection::Connection(const QUrl& server, QObject* parent)
- : QObject(parent), d(new Private(std::make_unique<ConnectionData>(server)))
+ : 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
}
@@ -271,11 +416,13 @@ Connection::~Connection()
{
qCDebug(MAIN) << "deconstructing connection object for" << userId();
stopSync();
+ Accounts.drop(this);
}
void Connection::resolveServer(const QString& mxid)
{
- d->clearResolvingContext();
+ if (isJobPending(d->resolverJob))
+ d->resolverJob->abandon();
auto maybeBaseUrl = QUrl::fromUserInput(serverPart(mxid));
maybeBaseUrl.setScheme("https"); // Instead of the Qt-default "http"
@@ -298,7 +445,7 @@ void Connection::resolveServer(const QString& mxid)
if (d->resolverJob->error() == BaseJob::Abandoned)
return;
- if (d->resolverJob->error() != BaseJob::NotFoundError) {
+ if (d->resolverJob->error() != BaseJob::NotFound) {
if (!d->resolverJob->status().good()) {
qCWarning(MAIN)
<< "Fetching .well-known file failed, FAIL_PROMPT";
@@ -326,12 +473,6 @@ void Connection::resolveServer(const QString& mxid)
setHomeserver(maybeBaseUrl);
}
Q_ASSERT(d->loginFlowsJob != nullptr); // Ensured by setHomeserver()
- connect(d->loginFlowsJob, &BaseJob::success, this,
- &Connection::resolved);
- connect(d->loginFlowsJob, &BaseJob::failure, this, [this] {
- qCWarning(MAIN) << "Homeserver base URL sanity check failed";
- emit resolveError(tr("The homeserver doesn't seem to be working"));
- });
});
}
@@ -353,7 +494,7 @@ void Connection::loginWithPassword(const QString& userId,
const QString& initialDeviceName,
const QString& deviceId)
{
- d->checkAndConnect(userId, [=] {
+ d->checkAndConnect(userId, [=,this] {
d->loginToServer(LoginFlows::Password.type, makeUserIdentifier(userId),
password, /*token*/ "", deviceId, initialDeviceName);
}, LoginFlows::Password);
@@ -380,8 +521,18 @@ void Connection::assumeIdentity(const QString& mxId, const QString& accessToken,
{
d->checkAndConnect(mxId, [this, mxId, accessToken, deviceId] {
d->data->setToken(accessToken.toLatin1());
- d->data->setDeviceId(deviceId);
- d->completeSetup(mxId);
+ d->data->setDeviceId(deviceId); // Can't we deduce this from access_token?
+ auto* job = callApi<GetTokenOwnerJob>();
+ connect(job, &BaseJob::success, this, [this, job, mxId] {
+ if (mxId != job->userId())
+ qCWarning(MAIN).nospace()
+ << "The access_token owner (" << job->userId()
+ << ") is different from passed MXID (" << mxId << ")!";
+ d->completeSetup(job->userId());
+ });
+ connect(job, &BaseJob::failure, this, [this, job] {
+ emit loginError(job->errorString(), job->rawDataSample());
+ });
});
}
@@ -403,7 +554,7 @@ void Connection::reloadCapabilities()
" disabling version upgrade recommendations to reduce noise";
});
connect(d->capabilitiesJob, &BaseJob::failure, this, [this] {
- if (d->capabilitiesJob->error() == BaseJob::IncorrectRequestError)
+ if (d->capabilitiesJob->error() == BaseJob::IncorrectRequest)
qCDebug(MAIN) << "Server doesn't support /capabilities;"
" version upgrade recommendations won't be issued";
});
@@ -425,12 +576,10 @@ void Connection::Private::loginToServer(LoginArgTs&&... loginArgs)
data->setToken(loginJob->accessToken().toLatin1());
data->setDeviceId(loginJob->deviceId());
completeSetup(loginJob->userId());
-#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);
-#endif // Quotient_E2EE_ENABLED
+ saveAccessTokenToKeychain();
+#ifdef Quotient_E2EE_ENABLED
+ database->clear();
+#endif
});
connect(loginJob, &BaseJob::failure, q, [this, loginJob] {
emit q->loginError(loginJob->errorString(), loginJob->rawDataSample());
@@ -445,15 +594,63 @@ void Connection::Private::completeSetup(const QString& mxId)
qCDebug(MAIN) << "Using server" << data->baseUrl().toDisplayString()
<< "by user" << data->userId()
<< "from device" << data->deviceId();
+ Accounts.add(q);
+ connect(qApp, &QCoreApplication::aboutToQuit, q, &Connection::saveState);
#ifndef Quotient_E2EE_ENABLED
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 { RandomBuffer(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(), data->deviceId(), q);
+
+ // init olmAccount
+ olmAccount = std::make_unique<QOlmAccount>(data->userId(), data->deviceId(), q);
+ connect(olmAccount.get(), &QOlmAccount::needsSave, q, &Connection::saveOlmAccount);
+
+ loadSessions();
+
+ 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
+ if (!olmAccount->unpickle(database->accountPickle(), picklingMode))
+ qWarning(E2EE)
+ << "Could not unpickle Olm account, E2EE won't be available";
}
#endif // Quotient_E2EE_ENABLED
emit q->stateChanged();
@@ -463,7 +660,7 @@ void Connection::Private::completeSetup(const QString& mxId)
void Connection::Private::checkAndConnect(const QString& userId,
const std::function<void()>& connectFn,
- const std::optional<LoginFlows::LoginFlow>& flow)
+ const std::optional<LoginFlow>& flow)
{
if (data->baseUrl().isValid() && (!flow || loginFlows.contains(*flow))) {
connectFn();
@@ -479,10 +676,11 @@ void Connection::Private::checkAndConnect(const QString& userId,
connectFn();
else
emit q->loginError(
+ tr("Unsupported login flow"),
tr("The homeserver at %1 does not support"
" the login flow '%2'")
- .arg(data->baseUrl().toDisplayString()),
- flow->type);
+ .arg(data->baseUrl().toDisplayString(),
+ flow->type));
});
else
connectSingleShot(q, &Connection::homeserverChanged, q, connectFn);
@@ -513,8 +711,10 @@ void Connection::logout()
|| d->logoutJob->error() == BaseJob::ContentAccessError) {
if (d->syncLoopConnection)
disconnect(d->syncLoopConnection);
- d->data->setToken({});
+ SettingsGroup("Accounts").remove(userId());
+ d->dropAccessToken();
emit loggedOut();
+ deleteLater();
} else { // logout() somehow didn't proceed - restore the session state
emit stateChanged();
if (wasSyncing)
@@ -606,24 +806,39 @@ QJsonObject toJson(const DirectChatsMap& directChats)
void Connection::onSyncSuccess(SyncData&& data, bool fromCache)
{
+#ifdef Quotient_E2EE_ENABLED
+ d->oneTimeKeysCount = data.deviceOneTimeKeysCount();
+ if (d->oneTimeKeysCount[SignedCurve25519Key] < 0.4 * d->olmAccount->maxNumberOfOneTimeKeys()
+ && !d->isUploadingKeys) {
+ d->isUploadingKeys = true;
+ d->olmAccount->generateOneTimeKeys(
+ d->olmAccount->maxNumberOfOneTimeKeys() / 2 - d->oneTimeKeysCount[SignedCurve25519Key]);
+ 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; });
+ }
+ if(d->firstSync) {
+ d->loadDevicesList();
+ d->firstSync = false;
+ }
+
+ d->consumeDevicesList(data.takeDevicesList());
+#endif // Quotient_E2EE_ENABLED
+ d->consumeToDeviceEvents(data.takeToDeviceEvents());
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,
@@ -641,21 +856,19 @@ void Connection::Private::consumeRoomData(SyncDataList&& roomDataList,
}
qWarning(MAIN) << "Room" << roomData.roomId
<< "has just been forgotten but /sync returned it in"
- << toCString(roomData.joinState)
+ << terse << roomData.joinState
<< "state - suspiciously fast turnaround";
}
if (auto* r = q->provideRoom(roomData.roomId, roomData.joinState)) {
pendingStateRoomIds.removeOne(roomData.roomId);
- r->updateData(std::move(roomData), fromCache);
- if (firstTimeRooms.removeOne(r)) {
- emit q->loadedRoomState(r);
- if (capabilities.roomVersions)
- r->checkVersion();
- // Otherwise, the version will be checked in reloadCapabilities()
- }
+ // Update rooms one by one, giving time to update the UI.
+ QMetaObject::invokeMethod(
+ r,
+ [r, rd = std::move(roomData), fromCache] () mutable {
+ r->updateData(std::move(rd), fromCache);
+ },
+ Qt::QueuedConnection);
}
- // Let UI update itself after updating each room
- QCoreApplication::processEvents();
}
}
@@ -664,21 +877,21 @@ void Connection::Private::consumeAccountData(Events&& accountDataEvents)
// After running this loop, the account data events not saved in
// accountData (see the end of the loop body) are auto-cleaned away
for (auto&& eventPtr: accountDataEvents) {
- visit(*eventPtr,
+ switchOnType(*eventPtr,
[this](const DirectChatEvent& dce) {
// https://github.com/quotient-im/libQuotient/wiki/Handling-direct-chat-events
const auto& usersToDCs = dce.usersToDirectChats();
DirectChatsMap remoteRemovals =
- erase_if(directChats, [&usersToDCs, this](auto it) {
+ remove_if(directChats, [&usersToDCs, this](auto it) {
return !(
usersToDCs.contains(it.key()->id(), it.value())
|| dcLocalAdditions.contains(it.key(), it.value()));
});
- erase_if(directChatUsers, [&remoteRemovals](auto it) {
+ remove_if(directChatUsers, [&remoteRemovals](auto it) {
return remoteRemovals.contains(it.value(), it.key());
});
// Remove from dcLocalRemovals what the server already has.
- erase_if(dcLocalRemovals, [&remoteRemovals](auto it) {
+ remove_if(dcLocalRemovals, [&remoteRemovals](auto it) {
return remoteRemovals.contains(it.key(), it.value());
});
if (MAIN().isDebugEnabled())
@@ -691,7 +904,7 @@ void Connection::Private::consumeAccountData(Events&& accountDataEvents)
DirectChatsMap remoteAdditions;
for (auto it = usersToDCs.begin(); it != usersToDCs.end(); ++it) {
- if (auto* const u = q->user(it.key())) {
+ if (auto* u = q->user(it.key())) {
if (!directChats.contains(u, it.value())
&& !dcLocalRemovals.contains(u, it.value())) {
Q_ASSERT(!directChatUsers.contains(it.value(), u));
@@ -706,7 +919,7 @@ void Connection::Private::consumeAccountData(Events&& accountDataEvents)
<< "Couldn't get a user object for" << it.key();
}
// Remove from dcLocalAdditions what the server already has.
- erase_if(dcLocalAdditions, [&remoteAdditions](auto it) {
+ remove_if(dcLocalAdditions, [&remoteAdditions](auto it) {
return remoteAdditions.contains(it.key(), it.value());
});
if (!remoteAdditions.isEmpty() || !remoteRemovals.isEmpty())
@@ -751,34 +964,105 @@ 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";
+ for (auto&& tdEvt : toDeviceEvents) {
+ if (processIfVerificationEvent(*tdEvt, false))
+ continue;
+ if (auto&& event = eventCast<EncryptedEvent>(std::move(tdEvt))) {
+ if (event->algorithm() != OlmV1Curve25519AesSha2AlgoKey) {
+ qCDebug(E2EE) << "Unsupported algorithm" << event->id()
+ << "for event" << event->algorithm();
+ return;
+ }
+ if (isKnownCurveKey(event->senderId(), event->senderKey())) {
+ handleEncryptedToDeviceEvent(*event);
+ return;
+ }
+ trackedUsers += event->senderId();
+ outdatedUsers += event->senderId();
+ encryptionUpdateRequired = true;
+ pendingEncryptedEvents.push_back(std::move(event));
+ }
}
+ }
+#endif
+}
- // 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();
-
- visit(*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();
- });
- });
+#ifdef Quotient_E2EE_ENABLED
+bool Connection::Private::processIfVerificationEvent(const Event& evt,
+ bool encrypted)
+{
+ return switchOnType(evt,
+ [this, encrypted](const KeyVerificationRequestEvent& reqEvt) {
+ const auto sessionIter = verificationSessions.insert(
+ reqEvt.transactionId(),
+ new KeyVerificationSession(q->userId(), reqEvt, q, encrypted));
+ emit q->newKeyVerificationSession(*sessionIter);
+ return true;
+ },
+ [](const KeyVerificationDoneEvent&) {
+ return true;
+ },
+ [this](const KeyVerificationEvent& kvEvt) {
+ if (auto* const session =
+ verificationSessions.value(kvEvt.transactionId())) {
+ session->handleEvent(kvEvt);
+ emit q->keyVerificationStateChanged(session, session->state());
+ }
+ return true;
+ },
+ false);
+}
+
+void Connection::Private::handleEncryptedToDeviceEvent(const EncryptedEvent& event)
+{
+ const auto [decryptedEvent, olmSessionId] = sessionDecryptMessage(event);
+ if(!decryptedEvent) {
+ qCWarning(E2EE) << "Failed to decrypt event" << event.id();
+ return;
+ }
+
+ if (processIfVerificationEvent(*decryptedEvent, true))
+ return;
+ switchOnType(*decryptedEvent,
+ [this, &event,
+ olmSessionId = olmSessionId](const RoomKeyEvent& roomKeyEvent) {
+ if (auto* detectedRoom = q->room(roomKeyEvent.roomId())) {
+ detectedRoom->handleRoomKeyEvent(roomKeyEvent, event.senderId(),
+ olmSessionId);
+ } else {
+ qCDebug(E2EE)
+ << "Encrypted event room id" << roomKeyEvent.roomId()
+ << "is not found at the connection" << q->objectName();
+ }
+ },
+ [](const Event& evt) {
+ qCWarning(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
}
@@ -796,11 +1080,6 @@ void Connection::stopSync()
QString Connection::nextBatchToken() const { return d->data->lastEvent(); }
-PostReceiptJob* Connection::postReceipt(Room* room, RoomEvent* event)
-{
- return callApi<PostReceiptJob>(room->id(), "m.read", event->id());
-}
-
JoinRoomJob* Connection::joinRoom(const QString& roomAlias,
const QStringList& serverNames)
{
@@ -844,6 +1123,15 @@ inline auto splitMediaId(const QString& mediaId)
return idParts;
}
+QUrl Connection::makeMediaUrl(QUrl mxcUrl) const
+{
+ Q_ASSERT(mxcUrl.scheme() == "mxc");
+ QUrlQuery q(mxcUrl.query());
+ q.addQueryItem(QStringLiteral("user_id"), userId());
+ mxcUrl.setQuery(q);
+ return mxcUrl;
+}
+
MediaThumbnailJob* Connection::getThumbnail(const QString& mediaId,
QSize requestedSize,
RunningPolicy policy)
@@ -914,6 +1202,18 @@ DownloadFileJob* Connection::downloadFile(const QUrl& url,
return job;
}
+#ifdef Quotient_E2EE_ENABLED
+DownloadFileJob* Connection::downloadFile(
+ const QUrl& url, const EncryptedFileMetadata& fileMetadata,
+ const QString& localFilename)
+{
+ auto mediaId = url.authority() + url.path();
+ auto idParts = splitMediaId(mediaId);
+ return callApi<DownloadFileJob>(idParts.front(), idParts.back(),
+ fileMetadata, localFilename);
+}
+#endif
+
CreateRoomJob*
Connection::createRoom(RoomVisibility visibility, const QString& alias,
const QString& name, const QString& topic,
@@ -924,12 +1224,6 @@ Connection::createRoom(RoomVisibility visibility, const QString& alias,
const QJsonObject& creationContent)
{
invites.removeOne(userId()); // The creator is by definition in the room
- for (const auto& i : invites)
- if (!user(i)) {
- qCWarning(MAIN) << "Won't create a room with malformed invitee ids";
- return nullptr;
- }
-
auto job = callApi<CreateRoomJob>(visibility == PublishRoom
? QStringLiteral("public")
: QStringLiteral("private"),
@@ -964,7 +1258,7 @@ void Connection::requestDirectChat(User* u)
void Connection::doInDirectChat(const QString& userId,
const std::function<void(Room*)>& operation)
{
- if (auto* const u = user(userId))
+ if (auto* u = user(userId))
doInDirectChat(u, operation);
else
qCCritical(MAIN)
@@ -1060,7 +1354,7 @@ ForgetRoomJob* Connection::forgetRoom(const QString& id)
connect(leaveJob, &BaseJob::result, this,
[this, leaveJob, forgetJob, room] {
if (leaveJob->error() == BaseJob::Success
- || leaveJob->error() == BaseJob::NotFoundError) {
+ || leaveJob->error() == BaseJob::NotFound) {
run(forgetJob);
// If the matching /sync response hasn't arrived yet,
// mark the room for explicit deletion
@@ -1079,7 +1373,7 @@ ForgetRoomJob* Connection::forgetRoom(const QString& id)
connect(forgetJob, &BaseJob::result, this, [this, id, forgetJob] {
// Leave room in case of success, or room not known by server
if (forgetJob->error() == BaseJob::Success
- || forgetJob->error() == BaseJob::NotFoundError)
+ || forgetJob->error() == BaseJob::NotFound)
d->removeRoom(id); // Delete the room from roomMap
else
qCWarning(MAIN).nospace() << "Error forgetting room " << id << ": "
@@ -1088,26 +1382,11 @@ ForgetRoomJob* Connection::forgetRoom(const QString& id)
return forgetJob;
}
-SendToDeviceJob*
-Connection::sendToDevices(const QString& eventType,
- const UsersToDevicesToEvents& eventsMap)
-{
- QHash<QString, QHash<QString, QJsonObject>> json;
- json.reserve(int(eventsMap.size()));
- std::for_each(eventsMap.begin(), eventsMap.end(),
- [&json](const auto& userTodevicesToEvents) {
- auto& jsonUser = json[userTodevicesToEvents.first];
- const auto& devicesToEvents = userTodevicesToEvents.second;
- std::for_each(devicesToEvents.begin(),
- devicesToEvents.end(),
- [&jsonUser](const auto& deviceToEvents) {
- jsonUser.insert(
- deviceToEvents.first,
- deviceToEvents.second.contentJson());
- });
- });
+SendToDeviceJob* Connection::sendToDevices(
+ const QString& eventType, const UsersToDevicesToContent& contents)
+{
return callApi<SendToDeviceJob>(BackgroundRequest, eventType,
- generateTxnId(), json);
+ generateTxnId(), contents);
}
SendMessageJob* Connection::sendMessage(const QString& roomId,
@@ -1200,12 +1479,14 @@ User* Connection::user(const QString& uId)
{
if (uId.isEmpty())
return nullptr;
+ if (const auto v = d->userMap.value(uId, nullptr))
+ return v;
+ // Before creating a user object, check that the user id is well-formed
+ // (it's faster to just do a lookup above before validation)
if (!uId.startsWith('@') || serverPart(uId).isEmpty()) {
qCCritical(MAIN) << "Malformed userId:" << uId;
return nullptr;
}
- if (d->userMap.contains(uId))
- return d->userMap.value(uId);
auto* user = userFactory()(this, uId);
d->userMap.insert(uId, user);
emit newUser(user);
@@ -1227,15 +1508,15 @@ QByteArray Connection::accessToken() const
{
// The logout job needs access token to do its job; so the token is
// kept inside d->data but no more exposed to the outside world.
- return isJobRunning(d->logoutJob) ? QByteArray() : d->data->accessToken();
+ return isJobPending(d->logoutJob) ? QByteArray() : d->data->accessToken();
}
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
@@ -1246,20 +1527,6 @@ int Connection::millisToReconnect() const
return d->syncJob ? d->syncJob->millisToRetry() : 0;
}
-QHash<QPair<QString, bool>, Room*> Connection::roomMap() const
-{
- // Copy-on-write-and-remove-elements is faster than copying elements one by
- // one.
- QHash<QPair<QString, bool>, Room*> roomMap = d->roomMap;
- for (auto it = roomMap.begin(); it != roomMap.end();) {
- if (it.value()->joinState() == JoinState::Leave)
- it = roomMap.erase(it);
- else
- ++it;
- }
- return roomMap;
-}
-
QVector<Room*> Connection::allRooms() const
{
QVector<Room*> result;
@@ -1362,8 +1629,8 @@ void Connection::Private::removeRoom(const QString& roomId)
{
for (auto f : { false, true })
if (auto r = roomMap.take({ roomId, f })) {
- qCDebug(MAIN) << "Room" << r->objectName() << "in state"
- << toCString(r->joinState()) << "will be deleted";
+ qCDebug(MAIN) << "Room" << r->objectName() << "in state" << terse
+ << r->joinState() << "will be deleted";
emit r->beforeDestruction(r);
r->deleteLater();
}
@@ -1395,7 +1662,7 @@ void Connection::removeFromDirectChats(const QString& roomId, User* user)
removals.insert(user, roomId);
d->dcLocalRemovals.insert(user, roomId);
} else {
- removals = erase_if(d->directChats,
+ removals = remove_if(d->directChats,
[&roomId](auto it) { return it.value() == roomId; });
d->directChatUsers.remove(roomId);
d->dcLocalRemovals += removals;
@@ -1421,8 +1688,8 @@ bool Connection::isIgnored(const User* user) const
IgnoredUsersList Connection::ignoredUsers() const
{
- const auto* event = d->unpackAccountData<IgnoredUsersEvent>();
- return event ? event->ignored_users() : IgnoredUsersList();
+ const auto* event = accountData<IgnoredUsersEvent>();
+ return event ? event->ignoredUsers() : IgnoredUsersList();
}
void Connection::addToIgnoredUsers(const User* user)
@@ -1461,7 +1728,7 @@ Room* Connection::provideRoom(const QString& id, Omittable<JoinState> joinState)
Q_ASSERT_X(!id.isEmpty(), __FUNCTION__, "Empty room id");
// If joinState is empty, all joinState == comparisons below are false.
- const auto roomKey = qMakePair(id, joinState == JoinState::Invite);
+ const std::pair roomKey { id, joinState == JoinState::Invite };
auto* room = d->roomMap.value(roomKey, nullptr);
if (room) {
// Leave is a special case because in transition (5a) (see the .h file)
@@ -1486,9 +1753,14 @@ Room* Connection::provideRoom(const QString& id, Omittable<JoinState> joinState)
return nullptr;
}
d->roomMap.insert(roomKey, room);
- d->firstTimeRooms.push_back(room);
connect(room, &Room::beforeDestruction, this,
&Connection::aboutToDeleteRoom);
+ connect(room, &Room::baseStateLoaded, this, [this, room] {
+ emit loadedRoomState(room);
+ if (d->capabilities.roomVersions)
+ room->checkVersion();
+ // Otherwise, the version will be checked in reloadCapabilities()
+ });
emit newRoom(room);
}
if (!joinState)
@@ -1534,27 +1806,21 @@ room_factory_t Connection::roomFactory() { return _roomFactory; }
user_factory_t Connection::userFactory() { return _userFactory; }
-room_factory_t Connection::_roomFactory = defaultRoomFactory<>();
-user_factory_t Connection::_userFactory = defaultUserFactory<>();
+room_factory_t Connection::_roomFactory = defaultRoomFactory<>;
+user_factory_t Connection::_userFactory = defaultUserFactory<>;
QByteArray Connection::generateTxnId() const
{
return d->data->generateTxnId();
}
-void Connection::Private::clearResolvingContext()
-{
- if (isJobRunning(resolverJob))
- resolverJob->abandon();
- if (isJobRunning(loginFlowsJob))
- loginFlowsJob->abandon();
- loginFlows.clear();
-
-}
-
void Connection::setHomeserver(const QUrl& url)
{
- d->clearResolvingContext();
+ if (isJobPending(d->resolverJob))
+ d->resolverJob->abandon();
+ if (isJobPending(d->loginFlowsJob))
+ d->loginFlowsJob->abandon();
+ d->loginFlows.clear();
if (homeserver() != url) {
d->data->setBaseUrl(url);
@@ -1581,16 +1847,10 @@ void Connection::saveRoomState(Room* r) const
QFile outRoomFile { stateCacheDir().filePath(
SyncData::fileNameForRoom(r->id())) };
if (outRoomFile.open(QFile::WriteOnly)) {
-#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
const auto data =
d->cacheToBinary
? QCborValue::fromJsonValue(r->toJson()).toCbor()
: QJsonDocument(r->toJson()).toJson(QJsonDocument::Compact);
-#else
- QJsonDocument json { r->toJson() };
- const auto data = d->cacheToBinary ? json.toBinaryData()
- : json.toJson(QJsonDocument::Compact);
-#endif
outRoomFile.write(data.data(), data.size());
qCDebug(MAIN) << "Room state cache saved to" << outRoomFile.fileName();
} else {
@@ -1643,26 +1903,26 @@ void Connection::saveState() const
}
{
QJsonArray accountDataEvents {
- basicEventJson(QStringLiteral("m.direct"), toJson(d->directChats))
+ Event::basicJson(QStringLiteral("m.direct"), toJson(d->directChats))
};
for (const auto& e : d->accountData)
accountDataEvents.append(
- basicEventJson(e.first, e.second->contentJson()));
+ Event::basicJson(e.first, e.second->contentJson()));
rootObj.insert(QStringLiteral("account_data"),
QJsonObject {
{ QStringLiteral("events"), accountDataEvents } });
}
+#ifdef Quotient_E2EE_ENABLED
+ {
+ QJsonObject keysJson = toJson(d->oneTimeKeysCount);
+ rootObj.insert(QStringLiteral("device_one_time_keys_count"), keysJson);
+ }
+#endif
-#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
const auto data =
d->cacheToBinary ? QCborValue::fromJsonValue(rootObj).toCbor()
: QJsonDocument(rootObj).toJson(QJsonDocument::Compact);
-#else
- QJsonDocument json { rootObj };
- const auto data = d->cacheToBinary ? json.toBinaryData()
- : json.toJson(QJsonDocument::Compact);
-#endif
qCDebug(PROFILER) << "Cache for" << userId() << "generated in" << et;
outFile.write(data.data(), data.size());
@@ -1738,7 +1998,7 @@ void Connection::getTurnServers()
{
auto job = callApi<GetTurnServerJob>();
connect(job, &GetTurnServerJob::success, this,
- [=] { emit turnServersChanged(job->data()); });
+ [this,job] { emit turnServersChanged(job->data()); });
}
const QString Connection::SupportedRoomVersion::StableTag =
@@ -1763,6 +2023,14 @@ QStringList Connection::stableRoomVersions() const
return l;
}
+bool Connection::canChangePassword() const
+{
+ // By default assume we can
+ return d->capabilities.changePassword
+ ? d->capabilities.changePassword->enabled
+ : true;
+}
+
inline bool roomVersionLess(const Connection::SupportedRoomVersion& v1,
const Connection::SupportedRoomVersion& v2)
{
@@ -1790,3 +2058,430 @@ 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)) {
+ QHash<QString, Quotient::DeviceKeys> oldDevices = deviceKeys[user];
+ 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 (!std::all_of(device.algorithms.cbegin(),
+ device.algorithms.cend(),
+ isSupportedAlgorithm)) {
+ 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;
+ }
+ if (oldDevices.contains(device.deviceId)) {
+ if (oldDevices[device.deviceId].keys["ed25519:" % device.deviceId] != device.keys["ed25519:" % device.deviceId]) {
+ qCDebug(E2EE) << "Device reuse detected. Skipping this device";
+ continue;
+ }
+ }
+ deviceKeys[user][device.deviceId] = SLICE(device, DeviceKeys);
+ }
+ outdatedUsers -= user;
+ }
+ saveDevicesList();
+
+ for(size_t i = 0; i < pendingEncryptedEvents.size();) {
+ if (isKnownCurveKey(
+ pendingEncryptedEvents[i]->fullJson()[SenderKeyL].toString(),
+ pendingEncryptedEvents[i]->contentPart<QString>("sender_key"_ls))) {
+ handleEncryptedToDeviceEvent(*pendingEncryptedEvents[i]);
+ pendingEncryptedEvents.erase(pendingEncryptedEvents.begin() + i);
+ } else
+ ++i;
+ }
+ });
+}
+
+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, verified) "
+ "SELECT :matrixId, :deviceId, :curveKeyId, :curveKey, :edKeyId, :edKey, :verified WHERE NOT EXISTS(SELECT 1 FROM tracked_devices WHERE matrixId=:matrixId AND deviceId=:deviceId);"
+ ));
+ 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]);
+ // If the device gets saved here, it can't be verified
+ query.bindValue(":verified", false);
+
+ 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()
+{
+#ifdef Quotient_E2EE_ENABLED
+ qCDebug(E2EE) << "Saving olm account";
+ d->database->setAccountPickle(d->olmAccount->pickle(d->picklingMode));
+#endif
+}
+
+#ifdef Quotient_E2EE_ENABLED
+QJsonObject Connection::decryptNotification(const QJsonObject &notification)
+{
+ if (auto r = room(notification["room_id"].toString()))
+ if (auto event =
+ loadEvent<EncryptedEvent>(notification["event"].toObject()))
+ if (const auto decrypted = r->decryptMessage(*event))
+ return decrypted->fullJson();
+ return QJsonObject();
+}
+
+Database* Connection::database() const
+{
+ return d->database;
+}
+
+UnorderedMap<QString, QOlmInboundGroupSessionPtr>
+Connection::loadRoomMegolmSessions(const Room* room) const
+{
+ return database()->loadMegolmSessions(room->id(), picklingMode());
+}
+
+void Connection::saveMegolmSession(const Room* room,
+ const QOlmInboundGroupSession& session) const
+{
+ database()->saveMegolmSession(room->id(), session.sessionId(),
+ session.pickle(picklingMode()),
+ session.senderId(), session.olmSessionId());
+}
+
+QStringList Connection::devicesForUser(const QString& userId) const
+{
+ return d->deviceKeys[userId].keys();
+}
+
+QString Connection::Private::curveKeyForUserDevice(const QString& userId,
+ const QString& device) const
+{
+ return deviceKeys[userId][device].keys["curve25519:" % device];
+}
+
+QString Connection::edKeyForUserDevice(const QString& userId,
+ const QString& deviceId) const
+{
+ return d->deviceKeys[userId][deviceId].keys["ed25519:" % deviceId];
+}
+
+bool Connection::Private::isKnownCurveKey(const QString& userId,
+ const QString& curveKey) const
+{
+ auto query = database->prepareQuery(
+ QStringLiteral("SELECT * FROM tracked_devices WHERE matrixId=:matrixId "
+ "AND curveKey=:curveKey"));
+ query.bindValue(":matrixId", userId);
+ query.bindValue(":curveKey", curveKey);
+ database->execute(query);
+ return query.next();
+}
+
+bool Connection::hasOlmSession(const QString& user,
+ const QString& deviceId) const
+{
+ const auto& curveKey = d->curveKeyForUserDevice(user, deviceId);
+ return d->olmSessions.contains(curveKey) && !d->olmSessions[curveKey].empty();
+}
+
+std::pair<QOlmMessage::Type, QByteArray> Connection::Private::olmEncryptMessage(
+ const QString& userId, const QString& device,
+ const QByteArray& message) const
+{
+ const auto& curveKey = curveKeyForUserDevice(userId, device);
+ const auto& olmSession = olmSessions.at(curveKey).front();
+ const auto result = olmSession->encrypt(message);
+ database->updateOlmSession(curveKey, olmSession->sessionId(),
+ olmSession->pickle(picklingMode));
+ return { result.type(), result.toCiphertext() };
+}
+
+bool Connection::Private::createOlmSession(const QString& targetUserId,
+ const QString& targetDeviceId,
+ const OneTimeKeys& oneTimeKeyObject)
+{
+ static QOlmUtility verifier;
+ qDebug(E2EE) << "Creating a new session for" << targetUserId
+ << targetDeviceId;
+ if (oneTimeKeyObject.isEmpty()) {
+ qWarning(E2EE) << "No one time key for" << targetUserId
+ << targetDeviceId;
+ return false;
+ }
+ auto* signedOneTimeKey =
+ std::get_if<SignedOneTimeKey>(&*oneTimeKeyObject.begin());
+ if (!signedOneTimeKey) {
+ qWarning(E2EE) << "No signed one time key for" << targetUserId
+ << targetDeviceId;
+ return false;
+ }
+ // Verify contents of signedOneTimeKey - for that, drop `signatures` and
+ // `unsigned` and then verify the object against the respective signature
+ const auto signature =
+ signedOneTimeKey->signature(targetUserId, targetDeviceId);
+ if (!verifier.ed25519Verify(
+ q->edKeyForUserDevice(targetUserId, targetDeviceId).toLatin1(),
+ signedOneTimeKey->toJsonForVerification(),
+ signature)) {
+ qWarning(E2EE) << "Failed to verify one-time-key signature for"
+ << targetUserId << targetDeviceId
+ << ". Skipping this device.";
+ return false;
+ }
+ const auto recipientCurveKey =
+ curveKeyForUserDevice(targetUserId, targetDeviceId).toLatin1();
+ auto session =
+ QOlmSession::createOutboundSession(olmAccount.get(), recipientCurveKey,
+ signedOneTimeKey->key());
+ if (!session) {
+ qCWarning(E2EE) << "Failed to create olm session for "
+ << recipientCurveKey << session.error();
+ return false;
+ }
+ saveSession(**session, recipientCurveKey);
+ olmSessions[recipientCurveKey].push_back(std::move(*session));
+ return true;
+}
+
+QJsonObject Connection::Private::assembleEncryptedContent(
+ QJsonObject payloadJson, const QString& targetUserId,
+ const QString& targetDeviceId) const
+{
+ payloadJson.insert(SenderKeyL, data->userId());
+// eventJson.insert("sender_device"_ls, data->deviceId());
+ payloadJson.insert("keys"_ls,
+ QJsonObject{
+ { Ed25519Key,
+ QString(olmAccount->identityKeys().ed25519) } });
+ payloadJson.insert("recipient"_ls, targetUserId);
+ payloadJson.insert(
+ "recipient_keys"_ls,
+ QJsonObject{ { Ed25519Key,
+ q->edKeyForUserDevice(targetUserId, targetDeviceId) } });
+ const auto [type, cipherText] = olmEncryptMessage(
+ targetUserId, targetDeviceId,
+ QJsonDocument(payloadJson).toJson(QJsonDocument::Compact));
+ QJsonObject encrypted {
+ { curveKeyForUserDevice(targetUserId, targetDeviceId),
+ QJsonObject { { "type"_ls, type },
+ { "body"_ls, QString(cipherText) } } }
+ };
+ return EncryptedEvent(encrypted, olmAccount->identityKeys().curve25519)
+ .contentJson();
+}
+
+void Connection::sendSessionKeyToDevices(
+ const QString& roomId, const QByteArray& sessionId,
+ const QByteArray& sessionKey, const QMultiHash<QString, QString>& devices,
+ int index)
+{
+ qDebug(E2EE) << "Sending room key to devices:" << sessionId
+ << sessionKey.toHex();
+ QHash<QString, QHash<QString, QString>> hash;
+ for (const auto& [userId, deviceId] : asKeyValueRange(devices))
+ if (!hasOlmSession(userId, deviceId)) {
+ hash[userId].insert(deviceId, "signed_curve25519"_ls);
+ qDebug(E2EE) << "Adding" << userId << deviceId
+ << "to keys to claim";
+ }
+
+ if (hash.isEmpty())
+ return;
+
+ auto job = callApi<ClaimKeysJob>(hash);
+ connect(job, &BaseJob::success, this, [job, this, roomId, sessionId, sessionKey, devices, index] {
+ QHash<QString, QHash<QString, QJsonObject>> usersToDevicesToContent;
+ for (const auto oneTimeKeys = job->oneTimeKeys();
+ const auto& [targetUserId, targetDeviceId] :
+ asKeyValueRange(devices)) {
+ if (!hasOlmSession(targetUserId, targetDeviceId)
+ && !d->createOlmSession(
+ targetUserId, targetDeviceId,
+ oneTimeKeys[targetUserId][targetDeviceId]))
+ continue;
+
+ // Noisy but nice for debugging
+// qDebug(E2EE) << "Creating the payload for" << targetUserId
+// << targetDeviceId << sessionId << sessionKey.toHex();
+ const auto keyEventJson = RoomKeyEvent(MegolmV1AesSha2AlgoKey,
+ roomId, sessionId, sessionKey)
+ .fullJson();
+
+ usersToDevicesToContent[targetUserId][targetDeviceId] =
+ d->assembleEncryptedContent(keyEventJson, targetUserId,
+ targetDeviceId);
+ }
+ if (!usersToDevicesToContent.empty()) {
+ sendToDevices(EncryptedEvent::TypeId, usersToDevicesToContent);
+ QVector<std::tuple<QString, QString, QString>> receivedDevices;
+ receivedDevices.reserve(devices.size());
+ for (const auto& [user, device] : asKeyValueRange(devices))
+ receivedDevices.push_back(
+ { user, device, d->curveKeyForUserDevice(user, device) });
+
+ database()->setDevicesReceivedKey(roomId, receivedDevices,
+ sessionId, index);
+ }
+ });
+}
+
+QOlmOutboundGroupSessionPtr Connection::loadCurrentOutboundMegolmSession(
+ const QString& roomId) const
+{
+ return d->database->loadCurrentOutboundMegolmSession(roomId,
+ d->picklingMode);
+}
+
+void Connection::saveCurrentOutboundMegolmSession(
+ const QString& roomId, const QOlmOutboundGroupSession& session) const
+{
+ d->database->saveCurrentOutboundMegolmSession(roomId, d->picklingMode,
+ session);
+}
+
+void Connection::startKeyVerificationSession(const QString& deviceId)
+{
+ auto* const session = new KeyVerificationSession(userId(), deviceId, this);
+ emit newKeyVerificationSession(session);
+}
+
+void Connection::sendToDevice(const QString& targetUserId,
+ const QString& targetDeviceId, const Event& event,
+ bool encrypted)
+{
+ const auto contentJson =
+ encrypted ? d->assembleEncryptedContent(event.fullJson(), targetUserId,
+ targetDeviceId)
+ : event.contentJson();
+ sendToDevices(encrypted ? EncryptedEvent::TypeId : event.type(),
+ { { targetUserId, { { targetDeviceId, contentJson } } } });
+}
+
+bool Connection::isVerifiedSession(const QString& megolmSessionId) const
+{
+ auto query = database()->prepareQuery("SELECT olmSessionId FROM inbound_megolm_sessions WHERE sessionId=:sessionId;"_ls);
+ query.bindValue(":sessionId", megolmSessionId);
+ database()->execute(query);
+ if (!query.next()) {
+ return false;
+ }
+ auto olmSessionId = query.value("olmSessionId").toString();
+ query.prepare("SELECT senderKey FROM olm_sessions WHERE sessionId=:sessionId;"_ls);
+ query.bindValue(":sessionId", olmSessionId);
+ database()->execute(query);
+ if (!query.next()) {
+ return false;
+ }
+ auto curveKey = query.value("senderKey"_ls).toString();
+ query.prepare("SELECT verified FROM tracked_devices WHERE curveKey=:curveKey;"_ls);
+ query.bindValue(":curveKey", curveKey);
+ database()->execute(query);
+ return query.next() && query.value("verified").toBool();
+}
+#endif