From 7b46aa2a6e4fdb71632128a94b6645613631d8d4 Mon Sep 17 00:00:00 2001
From: Nicolas Werner <nicolas.werner@hotmail.de>
Date: Wed, 16 Dec 2020 22:10:09 +0100
Subject: [PATCH] Store secrets in keychain

---
 CMakeLists.txt                     |   2 +
 src/Cache.cpp                      |  94 +++++++++++++++++++-
 src/Cache.h                        |   5 ++
 src/Cache_p.h                      |   5 ++
 src/ChatPage.cpp                   |  11 ++-
 src/MainWindow.cpp                 |   5 ++
 src/Olm.cpp                        | 133 ++++++++++++++++++++++++++++-
 src/RoomList.h                     |   6 +-
 src/UserSettingsPage.cpp           |  57 +++++++++++++
 src/UserSettingsPage.h             |   7 ++
 src/timeline/TimelineViewManager.h |   7 +-
 11 files changed, 320 insertions(+), 12 deletions(-)

diff --git a/CMakeLists.txt b/CMakeLists.txt
index d2689a97f..877d7d549 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -136,6 +136,7 @@ endif()
 find_package(Qt5 COMPONENTS Core Widgets LinguistTools Concurrent Svg Multimedia Qml QuickControls2 QuickWidgets REQUIRED)
 find_package(Qt5QuickCompiler)
 find_package(Qt5DBus)
+find_package(Qt5Keychain REQUIRED)
 
 if (APPLE)
 	find_package(Qt5MacExtras REQUIRED)
@@ -587,6 +588,7 @@ target_link_libraries(nheko PRIVATE
 	Qt5::Qml
 	Qt5::QuickControls2
 	Qt5::QuickWidgets
+	qt5keychain
 	nlohmann_json::nlohmann_json
 	lmdbxx::lmdbxx
 	liblmdb::lmdb
diff --git a/src/Cache.cpp b/src/Cache.cpp
index 05c2e4860..9da0d87df 100644
--- a/src/Cache.cpp
+++ b/src/Cache.cpp
@@ -24,9 +24,10 @@
 #include <QFile>
 #include <QHash>
 #include <QMap>
-#include <QSettings>
 #include <QStandardPaths>
 
+#include <qt5keychain/keychain.h>
+
 #include <mtx/responses/common.hpp>
 
 #include "Cache.h"
@@ -569,6 +570,64 @@ Cache::restoreOlmAccount()
         return std::string(pickled.data(), pickled.size());
 }
 
+void
+Cache::storeSecret(const std::string &name, const std::string &secret)
+{
+        QKeychain::WritePasswordJob job(QCoreApplication::applicationName());
+        job.setAutoDelete(false);
+        job.setInsecureFallback(true);
+        job.setKey(QString::fromStdString(name));
+        job.setTextData(QString::fromStdString(secret));
+        QEventLoop loop;
+        job.connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
+        job.start();
+        loop.exec();
+
+        if (job.error()) {
+                nhlog::db()->warn(
+                  "Storing secret '{}' failed: {}", name, job.errorString().toStdString());
+        } else {
+                emit secretChanged(name);
+        }
+}
+
+void
+Cache::deleteSecret(const std::string &name)
+{
+        QKeychain::DeletePasswordJob job(QCoreApplication::applicationName());
+        job.setAutoDelete(false);
+        job.setInsecureFallback(true);
+        job.setKey(QString::fromStdString(name));
+        QEventLoop loop;
+        job.connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
+        job.start();
+        loop.exec();
+
+        emit secretChanged(name);
+}
+
+std::optional<std::string>
+Cache::secret(const std::string &name)
+{
+        QKeychain::ReadPasswordJob job(QCoreApplication::applicationName());
+        job.setAutoDelete(false);
+        job.setInsecureFallback(true);
+        job.setKey(QString::fromStdString(name));
+        QEventLoop loop;
+        job.connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
+        job.start();
+        loop.exec();
+
+        const QString secret = job.textData();
+        if (job.error()) {
+                nhlog::db()->debug(
+                  "Restoring secret '{}' failed: {}", name, job.errorString().toStdString());
+                return std::nullopt;
+        }
+
+        return secret.toStdString();
+}
+
 //
 // Media Management
 //
@@ -726,10 +785,32 @@ void
 Cache::deleteData()
 {
         // TODO: We need to remove the env_ while not accepting new requests.
+        lmdb::dbi_close(env_, syncStateDb_);
+        lmdb::dbi_close(env_, roomsDb_);
+        lmdb::dbi_close(env_, invitesDb_);
+        lmdb::dbi_close(env_, mediaDb_);
+        lmdb::dbi_close(env_, readReceiptsDb_);
+        lmdb::dbi_close(env_, notificationsDb_);
+
+        lmdb::dbi_close(env_, devicesDb_);
+        lmdb::dbi_close(env_, deviceKeysDb_);
+
+        lmdb::dbi_close(env_, inboundMegolmSessionDb_);
+        lmdb::dbi_close(env_, outboundMegolmSessionDb_);
+
+        env_.close();
+
+        verification_storage.status.clear();
+
         if (!cacheDirectory_.isEmpty()) {
                 QDir(cacheDirectory_).removeRecursively();
                 nhlog::db()->info("deleted cache files from disk");
         }
+
+        deleteSecret(mtx::secret_storage::secrets::megolm_backup_v1);
+        deleteSecret(mtx::secret_storage::secrets::cross_signing_master);
+        deleteSecret(mtx::secret_storage::secrets::cross_signing_user_signing);
+        deleteSecret(mtx::secret_storage::secrets::cross_signing_self_signing);
 }
 
 //! migrates db to the current format
@@ -4262,4 +4343,15 @@ restoreOlmAccount()
 {
         return instance_->restoreOlmAccount();
 }
+
+void
+storeSecret(const std::string &name, const std::string &secret)
+{
+        instance_->storeSecret(name, secret);
+}
+std::optional<std::string>
+secret(const std::string &name)
+{
+        return instance_->secret(name);
+}
 } // namespace cache
diff --git a/src/Cache.h b/src/Cache.h
index f38f1960b..919567257 100644
--- a/src/Cache.h
+++ b/src/Cache.h
@@ -282,4 +282,9 @@ saveOlmAccount(const std::string &pickled);
 
 std::string
 restoreOlmAccount();
+
+void
+storeSecret(const std::string &name, const std::string &secret);
+std::optional<std::string>
+secret(const std::string &name);
 }
diff --git a/src/Cache_p.h b/src/Cache_p.h
index fab2d9643..059c1461a 100644
--- a/src/Cache_p.h
+++ b/src/Cache_p.h
@@ -269,6 +269,10 @@ public:
         void saveOlmAccount(const std::string &pickled);
         std::string restoreOlmAccount();
 
+        void storeSecret(const std::string &name, const std::string &secret);
+        void deleteSecret(const std::string &name);
+        std::optional<std::string> secret(const std::string &name);
+
 signals:
         void newReadReceipts(const QString &room_id, const std::vector<QString> &event_ids);
         void roomReadStatus(const std::map<QString, bool> &status);
@@ -276,6 +280,7 @@ signals:
         void userKeysUpdate(const std::string &sync_token,
                             const mtx::responses::QueryKeys &keyQuery);
         void verificationStatusChanged(const std::string &userid);
+        void secretChanged(const std::string name);
 
 private:
         //! Save an invited room.
diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index dab414a9a..2d2235844 100644
--- a/src/ChatPage.cpp
+++ b/src/ChatPage.cpp
@@ -372,9 +372,8 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
 void
 ChatPage::logout()
 {
-        deleteConfigs();
-
         resetUI();
+        deleteConfigs();
 
         emit closing();
         connectivityTimer_.stop();
@@ -385,12 +384,12 @@ ChatPage::dropToLoginPage(const QString &msg)
 {
         nhlog::ui()->info("dropping to the login page: {}", msg.toStdString());
 
-        deleteConfigs();
-        resetUI();
-
         http::client()->shutdown();
         connectivityTimer_.stop();
 
+        resetUI();
+        deleteConfigs();
+
         emit showLoginPage(msg);
 }
 
@@ -418,8 +417,8 @@ ChatPage::deleteConfigs()
         settings.remove("");
         settings.endGroup();
 
+        http::client()->shutdown();
         cache::deleteData();
-        http::client()->clear();
 }
 
 void
diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp
index 60b5168be..d056aca60 100644
--- a/src/MainWindow.cpp
+++ b/src/MainWindow.cpp
@@ -26,6 +26,7 @@
 #include <mtx/responses/login.hpp>
 
 #include "Cache.h"
+#include "Cache_p.h"
 #include "ChatPage.h"
 #include "Config.h"
 #include "Logging.h"
@@ -294,6 +295,10 @@ MainWindow::showChatPage()
 
         login_page_->reset();
         chat_page_->bootstrap(userid, homeserver, token);
+        connect(cache::client(),
+                &Cache::secretChanged,
+                userSettingsPage_,
+                &UserSettingsPage::updateSecretStatus);
 
         instance_ = this;
 }
diff --git a/src/Olm.cpp b/src/Olm.cpp
index 22df3911d..9dd4705ef 100644
--- a/src/Olm.cpp
+++ b/src/Olm.cpp
@@ -18,13 +18,13 @@
 #include "UserSettingsPage.h"
 #include "Utils.h"
 
-static const std::string STORAGE_SECRET_KEY("secret");
-constexpr auto MEGOLM_ALGO = "m.megolm.v1.aes-sha2";
-
 namespace {
 auto client_ = std::make_unique<mtx::crypto::OlmClient>();
 
 std::map<std::string, std::string> request_id_to_secret_name;
+
+const std::string STORAGE_SECRET_KEY("secret");
+constexpr auto MEGOLM_ALGO = "m.megolm.v1.aes-sha2";
 }
 
 namespace olm {
@@ -221,6 +221,133 @@ handle_olm_message(const OlmMessage &msg)
                         } else if (auto roomKey = std::get_if<DeviceEvent<msg::ForwardedRoomKey>>(
                                      &device_event)) {
                                 import_inbound_megolm_session(*roomKey);
+                        } else if (auto e =
+                                     std::get_if<DeviceEvent<msg::SecretSend>>(&device_event)) {
+                                auto local_user = http::client()->user_id();
+
+                                if (msg.sender != local_user.to_string())
+                                        continue;
+
+                                auto secret_name =
+                                  request_id_to_secret_name.find(e->content.request_id);
+
+                                if (secret_name != request_id_to_secret_name.end()) {
+                                        nhlog::crypto()->info("Received secret: {}",
+                                                              secret_name->second);
+
+                                        mtx::events::msg::SecretRequest secretRequest{};
+                                        secretRequest.action =
+                                          mtx::events::msg::RequestAction::Cancellation;
+                                        secretRequest.requesting_device_id =
+                                          http::client()->device_id();
+                                        secretRequest.request_id = e->content.request_id;
+
+                                        auto verificationStatus =
+                                          cache::verificationStatus(local_user.to_string());
+
+                                        if (!verificationStatus)
+                                                continue;
+
+                                        auto deviceKeys = cache::userKeys(local_user.to_string());
+                                        std::string sender_device_id;
+                                        if (deviceKeys) {
+                                                for (auto &[dev, key] : deviceKeys->device_keys) {
+                                                        if (key.keys["curve25519:" + dev] ==
+                                                            msg.sender_key) {
+                                                                sender_device_id = dev;
+                                                                break;
+                                                        }
+                                                }
+                                        }
+
+                                        std::map<
+                                          mtx::identifiers::User,
+                                          std::map<std::string, mtx::events::msg::SecretRequest>>
+                                          body;
+
+                                        for (const auto &dev :
+                                             verificationStatus->verified_devices) {
+                                                if (dev != secretRequest.requesting_device_id &&
+                                                    dev != sender_device_id)
+                                                        body[local_user][dev] = secretRequest;
+                                        }
+
+                                        http::client()
+                                          ->send_to_device<mtx::events::msg::SecretRequest>(
+                                            http::client()->generate_txn_id(),
+                                            body,
+                                            [name =
+                                               secret_name->second](mtx::http::RequestErr err) {
+                                                    if (err) {
+                                                            nhlog::net()->error(
+                                                              "Failed to send request cancellation "
+                                                              "for secrect "
+                                                              "'{}'",
+                                                              name);
+                                                            return;
+                                                    }
+                                            });
+
+                                        cache::client()->storeSecret(secret_name->second,
+                                                                     e->content.secret);
+
+                                        request_id_to_secret_name.erase(secret_name);
+                                }
+
+                        } else if (auto e =
+                                     std::get_if<DeviceEvent<msg::SecretRequest>>(&device_event)) {
+                                if (e->content.action != mtx::events::msg::RequestAction::Request)
+                                        continue;
+
+                                auto local_user = http::client()->user_id();
+
+                                if (msg.sender != local_user.to_string())
+                                        continue;
+
+                                auto verificationStatus =
+                                  cache::verificationStatus(local_user.to_string());
+
+                                if (!verificationStatus)
+                                        continue;
+
+                                auto deviceKeys = cache::userKeys(local_user.to_string());
+                                if (!deviceKeys)
+                                        continue;
+
+                                for (auto &[dev, key] : deviceKeys->device_keys) {
+                                        if (key.keys["curve25519:" + dev] == msg.sender_key) {
+                                                if (std::find(
+                                                      verificationStatus->verified_devices.begin(),
+                                                      verificationStatus->verified_devices.end(),
+                                                      dev) ==
+                                                    verificationStatus->verified_devices.end())
+                                                        break;
+
+                                                // this is a verified device
+                                                mtx::events::DeviceEvent<
+                                                  mtx::events::msg::SecretSend>
+                                                  secretSend;
+                                                secretSend.type = EventType::SecretSend;
+                                                secretSend.content.request_id =
+                                                  e->content.request_id;
+
+                                                auto secret =
+                                                  cache::client()->secret(e->content.name);
+                                                if (!secret)
+                                                        break;
+
+                                                secretSend.content.secret = secret.value();
+
+                                                send_encrypted_to_device_messages(
+                                                  {{local_user.to_string(), {{dev}}}}, secretSend);
+
+                                                nhlog::net()->info("Sent secret to ({},{})",
+                                                                   local_user.to_string(),
+                                                                   dev);
+
+                                                break;
+                                        }
+                                }
                         }
 
                         return;
diff --git a/src/RoomList.h b/src/RoomList.h
index d50c7de15..02aac869f 100644
--- a/src/RoomList.h
+++ b/src/RoomList.h
@@ -43,7 +43,11 @@ public:
         void initialize(const QMap<QString, RoomInfo> &info);
         void sync(const std::map<QString, RoomInfo> &info);
 
-        void clear() { rooms_.clear(); };
+        void clear()
+        {
+                rooms_.clear();
+                rooms_sort_cache_.clear();
+        };
         void updateAvatar(const QString &room_id, const QString &url);
 
         void addRoom(const QString &room_id, const RoomInfo &info);
diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp
index a773af1c3..fe0145fe8 100644
--- a/src/UserSettingsPage.cpp
+++ b/src/UserSettingsPage.cpp
@@ -637,6 +637,15 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge
 
         deviceFingerprintValue_->setText(utils::humanReadableFingerprint(QString(44, 'X')));
 
+        backupSecretCached      = new QLabel{this};
+        masterSecretCached      = new QLabel{this};
+        selfSigningSecretCached = new QLabel{this};
+        userSigningSecretCached = new QLabel{this};
+        backupSecretCached->setFont(monospaceFont);
+        masterSecretCached->setFont(monospaceFont);
+        selfSigningSecretCached->setFont(monospaceFont);
+        userSigningSecretCached->setFont(monospaceFont);
+
         auto sessionKeysLabel = new QLabel{tr("Session Keys"), this};
         sessionKeysLabel->setFont(font);
         sessionKeysLabel->setMargin(OptionMargin);
@@ -801,6 +810,27 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge
         formLayout_->addRow(sessionKeysLabel, sessionKeysLayout);
         formLayout_->addRow(crossSigningKeysLabel, crossSigningKeysLayout);
 
+        boxWrap(tr("Master signing key"),
+                masterSecretCached,
+                tr("Your most important key. You don't need to have it cached, since not caching "
+                   "it makes it less likely it can be stolen and it is only needed to rotate your "
+                   "other signing keys."));
+        boxWrap(tr("User signing key"),
+                userSigningSecretCached,
+                tr("The key to verify other users. If it is cached, verifying a user will verify "
+                   "all their devices."));
+        boxWrap(
+          tr("Self signing key"),
+          selfSigningSecretCached,
+          tr("The key to verify your own devices. If it is cached, verifying one of your devices "
+             "will mark it verified for all your other devices and for users, that have verified "
+             "you."));
+        boxWrap(tr("Backup key"),
+                backupSecretCached,
+                tr("The key to decrypt online key backups. If it is cached, you can enable online "
+                   "key backup to store encryption keys securely encrypted on the server."));
+        updateSecretStatus();
+
         auto scrollArea_ = new QScrollArea{this};
         scrollArea_->setFrameShape(QFrame::NoFrame);
         scrollArea_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
@@ -1154,3 +1184,30 @@ UserSettingsPage::exportSessionKeys()
                 QMessageBox::warning(this, tr("Error"), e.what());
         }
 }
+
+void
+UserSettingsPage::updateSecretStatus()
+{
+        QString ok      = "QLabel { color : #00cc66; }";
+        QString notSoOk = "QLabel { color : #ff9933; }";
+
+        auto updateLabel = [&, this](QLabel *label, const std::string &secretName) {
+                if (cache::secret(secretName)) {
+                        label->setStyleSheet(ok);
+                        label->setText(tr("CACHED"));
+                } else {
+                        if (secretName == mtx::secret_storage::secrets::cross_signing_master)
+                                label->setStyleSheet(ok);
+                        else
+                                label->setStyleSheet(notSoOk);
+                        label->setText(tr("NOT CACHED"));
+                }
+        };
+
+        updateLabel(masterSecretCached, mtx::secret_storage::secrets::cross_signing_master);
+        updateLabel(userSigningSecretCached,
+                    mtx::secret_storage::secrets::cross_signing_user_signing);
+        updateLabel(selfSigningSecretCached,
+                    mtx::secret_storage::secrets::cross_signing_self_signing);
+        updateLabel(backupSecretCached, mtx::secret_storage::secrets::megolm_backup_v1);
+}
diff --git a/src/UserSettingsPage.h b/src/UserSettingsPage.h
index d1ae93f0a..c699fd594 100644
--- a/src/UserSettingsPage.h
+++ b/src/UserSettingsPage.h
@@ -253,6 +253,9 @@ signals:
         void themeChanged();
         void decryptSidebarChanged();
 
+public slots:
+        void updateSecretStatus();
+
 private slots:
         void importSessionKeys();
         void exportSessionKeys();
@@ -285,6 +288,10 @@ private:
         Toggle *mobileMode_;
         QLabel *deviceFingerprintValue_;
         QLabel *deviceIdValue_;
+        QLabel *backupSecretCached;
+        QLabel *masterSecretCached;
+        QLabel *selfSigningSecretCached;
+        QLabel *userSigningSecretCached;
 
         QComboBox *themeCombo_;
         QComboBox *scaleFactorCombo_;
diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h
index b9febf752..f346acf83 100644
--- a/src/timeline/TimelineViewManager.h
+++ b/src/timeline/TimelineViewManager.h
@@ -51,7 +51,12 @@ public:
         void sync(const mtx::responses::Rooms &rooms);
         void addRoom(const QString &room_id);
 
-        void clearAll() { models.clear(); }
+        void clearAll()
+        {
+                timeline_ = nullptr;
+                emit activeTimelineChanged(nullptr);
+                models.clear();
+        }
 
         Q_INVOKABLE TimelineModel *activeTimeline() const { return timeline_; }
         Q_INVOKABLE bool isInitialSync() const { return isInitialSync_; }
-- 
GitLab