diff --git a/CMakeLists.txt b/CMakeLists.txt index d2689a97..877d7d54 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 05c2e486..9da0d87d 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -24,9 +24,10 @@ #include #include #include -#include #include +#include + #include #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 +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 +secret(const std::string &name) +{ + return instance_->secret(name); +} } // namespace cache diff --git a/src/Cache.h b/src/Cache.h index f38f1960..91956725 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 +secret(const std::string &name); } diff --git a/src/Cache_p.h b/src/Cache_p.h index fab2d964..059c1461 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 secret(const std::string &name); + signals: void newReadReceipts(const QString &room_id, const std::vector &event_ids); void roomReadStatus(const std::map &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 dab414a9..2d223584 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -372,9 +372,8 @@ ChatPage::ChatPage(QSharedPointer 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 60b5168b..d056aca6 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -26,6 +26,7 @@ #include #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 22df3911..9dd4705e 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(); std::map 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>( &device_event)) { import_inbound_megolm_session(*roomKey); + } else if (auto e = + std::get_if>(&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> + 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( + 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>(&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 d50c7de1..02aac869 100644 --- a/src/RoomList.h +++ b/src/RoomList.h @@ -43,7 +43,11 @@ public: void initialize(const QMap &info); void sync(const std::map &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 a773af1c..fe0145fe 100644 --- a/src/UserSettingsPage.cpp +++ b/src/UserSettingsPage.cpp @@ -637,6 +637,15 @@ UserSettingsPage::UserSettingsPage(QSharedPointer 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 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 d1ae93f0..c699fd59 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 b9febf75..f346acf8 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_; }