From 946cd4cb73f95526aa09777afc2a493610d9696d Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Fri, 27 May 2022 21:03:31 +0200 Subject: Load and store accounts in the keychain --- CMakeLists.txt | 8 +++--- lib/accountregistry.cpp | 67 +++++++++++++++++++++++++++++++++++++++++++++++-- lib/accountregistry.h | 20 +++++++++++++++ lib/connection.cpp | 39 ++++++++++++++++++++++++---- 4 files changed, 123 insertions(+), 11 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 404ba87c..7900235c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -87,6 +87,8 @@ find_package(${Qt} ${QtMinVersion} REQUIRED Core Network Gui Test ${QtExtraModul get_filename_component(Qt_Prefix "${${Qt}_DIR}/../../../.." ABSOLUTE) message(STATUS "Using Qt ${${Qt}_VERSION} at ${Qt_Prefix}") +find_package(${Qt}Keychain REQUIRED) + if (${PROJECT_NAME}_ENABLE_E2EE) find_package(${Qt} ${QtMinVersion} REQUIRED Sql) find_package(Olm 3.1.3 REQUIRED) @@ -108,7 +110,6 @@ if (${PROJECT_NAME}_ENABLE_E2EE) if (OpenSSL_FOUND) message(STATUS "Using OpenSSL ${OpenSSL_VERSION} at ${OpenSSL_DIR}") endif() - find_package(${Qt}Keychain REQUIRED) endif() @@ -309,14 +310,13 @@ if (${PROJECT_NAME}_ENABLE_E2EE) target_link_libraries(${PROJECT_NAME} Olm::Olm OpenSSL::Crypto OpenSSL::SSL - ${Qt}::Sql - ${QTKEYCHAIN_LIBRARIES}) + ${Qt}::Sql) set(FIND_DEPS "find_dependency(Olm) find_dependency(OpenSSL) find_dependency(${Qt}Sql)") # For QuotientConfig.cmake.in endif() -target_link_libraries(${PROJECT_NAME} ${Qt}::Core ${Qt}::Network ${Qt}::Gui) +target_link_libraries(${PROJECT_NAME} ${Qt}::Core ${Qt}::Network ${Qt}::Gui ${QTKEYCHAIN_LIBRARIES}) if (Qt STREQUAL Qt5) # See #483 target_link_libraries(${PROJECT_NAME} ${Qt}::Multimedia) diff --git a/lib/accountregistry.cpp b/lib/accountregistry.cpp index 616b54b4..9b6114d9 100644 --- a/lib/accountregistry.cpp +++ b/lib/accountregistry.cpp @@ -5,7 +5,13 @@ #include "accountregistry.h" #include "connection.h" +#include +#if QT_VERSION_MAJOR >= 6 +# include +#else +# include +#endif using namespace Quotient; void AccountRegistry::add(Connection* a) @@ -15,6 +21,7 @@ void AccountRegistry::add(Connection* a) beginInsertRows(QModelIndex(), size(), size()); push_back(a); endInsertRows(); + Q_EMIT accountCountChanged(); } void AccountRegistry::drop(Connection* a) @@ -54,8 +61,6 @@ QHash AccountRegistry::roleNames() const return { { AccountRole, "connection" } }; } - - Connection* AccountRegistry::get(const QString& userId) { for (const auto &connection : *this) { @@ -64,3 +69,61 @@ Connection* AccountRegistry::get(const QString& userId) } return nullptr; } + +QKeychain::ReadPasswordJob* AccountRegistry::loadAccessTokenFromKeychain(const QString& userId) +{ + qCDebug(MAIN) << "Reading access token from keychain for" << userId; + auto job = new QKeychain::ReadPasswordJob(qAppName(), this); + job->setKey(userId); + + connect(job, &QKeychain::Job::finished, this, [job]() { + if (job->error() == QKeychain::Error::NoError) { + return; + } + //TODO error handling + }); + job->start(); + + return job; +} + +void AccountRegistry::invokeLogin() +{ + const auto accounts = SettingsGroup("Accounts").childGroups(); + for (const auto& accountId : accounts) { + AccountSettings account{accountId}; + m_accountsLoading += accountId; + Q_EMIT accountsLoadingChanged(); + + if (!account.homeserver().isEmpty()) { + auto accessTokenLoadingJob = loadAccessTokenFromKeychain(account.userId()); + connect(accessTokenLoadingJob, &QKeychain::Job::finished, this, [accountId, this, accessTokenLoadingJob]() { + AccountSettings account{accountId}; + if (accessTokenLoadingJob->error() != QKeychain::Error::NoError) { + //TODO error handling + return; + } + + auto connection = new Connection(account.homeserver()); + connect(connection, &Connection::connected, this, [connection] { + connection->loadState(); + connection->setLazyLoading(true); + + connection->syncLoop(); + }); + connect(connection, &Connection::loginError, this, [](const QString& error, const QString&) { + //TODO error handling + }); + connect(connection, &Connection::networkError, this, [](const QString& error, const QString&, int, int) { + //TODO error handling + }); + connection->assumeIdentity(account.userId(), accessTokenLoadingJob->binaryData(), account.deviceId()); + }); + } + } +} + +QStringList AccountRegistry::accountsLoading() const +{ + return m_accountsLoading; +} diff --git a/lib/accountregistry.h b/lib/accountregistry.h index 2f6dffdf..ab337303 100644 --- a/lib/accountregistry.h +++ b/lib/accountregistry.h @@ -5,15 +5,25 @@ #pragma once #include "quotient_export.h" +#include "settings.h" #include +namespace QKeychain { +class ReadPasswordJob; +} + namespace Quotient { class Connection; class QUOTIENT_API AccountRegistry : public QAbstractListModel, private QVector { Q_OBJECT + /// Number of accounts that are currently fully loaded + Q_PROPERTY(int accountCount READ rowCount NOTIFY accountCountChanged) + /// List of accounts that are currently in some stage of being loaded (Reading token from keychain, trying to contact server, etc). + /// Can be used to inform the user or to show a login screen if size() == 0 and no accounts are loaded + Q_PROPERTY(QStringList accountsLoading READ accountsLoading NOTIFY accountsLoadingChanged) public: using const_iterator = QVector::const_iterator; using const_reference = QVector::const_reference; @@ -52,6 +62,16 @@ public: [[nodiscard]] int rowCount( const QModelIndex& parent = QModelIndex()) const override; [[nodiscard]] QHash roleNames() const override; + + QStringList accountsLoading() const; + + void invokeLogin(); +Q_SIGNALS: + void accountCountChanged(); + void accountsLoadingChanged(); +private: + QKeychain::ReadPasswordJob* loadAccessTokenFromKeychain(const QString &userId); + QStringList m_accountsLoading; }; inline QUOTIENT_API AccountRegistry Accounts {}; diff --git a/lib/connection.cpp b/lib/connection.cpp index dba18cb1..526feb26 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -41,12 +41,12 @@ # include "e2ee/qolmsession.h" # include "e2ee/qolmutils.h" -# if QT_VERSION_MAJOR >= 6 -# include -# else -# include -# endif #endif // Quotient_E2EE_ENABLED +#if QT_VERSION_MAJOR >= 6 +# include +#else +# include +#endif #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) # include @@ -368,6 +368,32 @@ public: void saveDevicesList(); void loadDevicesList(); #endif + + void saveAccessTokenToKeychain() + { + qCDebug(MAIN) << "Saving access token to keychain for" << q->userId(); + auto job = new QKeychain::WritePasswordJob(qAppName()); + job->setAutoDelete(false); + job->setKey(q->userId()); + job->setBinaryData(data->accessToken()); + job->start(); + //TODO error handling + } + + void removeAccessTokenFromKeychain() + { + 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 + } }; Connection::Connection(const QUrl& server, QObject* parent) @@ -546,6 +572,7 @@ void Connection::Private::loginToServer(LoginArgTs&&... loginArgs) data->setToken(loginJob->accessToken().toLatin1()); data->setDeviceId(loginJob->deviceId()); completeSetup(loginJob->userId()); + saveAccessTokenToKeychain(); #ifndef Quotient_E2EE_ENABLED qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; #else // Quotient_E2EE_ENABLED @@ -684,6 +711,8 @@ void Connection::logout() disconnect(d->syncLoopConnection); d->data->setToken({}); emit loggedOut(); + SettingsGroup("Accounts").remove(userId()); + d->removeAccessTokenFromKeychain(); deleteLater(); } else { // logout() somehow didn't proceed - restore the session state emit stateChanged(); -- cgit v1.2.3