diff --git a/.ci/macos/Brewfile b/.ci/macos/Brewfile
index 4ef9967c..7e9687c7 100644
--- a/.ci/macos/Brewfile
+++ b/.ci/macos/Brewfile
@@ -11,4 +11,5 @@ brew "nlohmann_json"
brew "gstreamer"
brew "gst-plugins-base"
brew "gst-plugins-good"
-brew "gst-plugins-bad"
\ No newline at end of file
+brew "gst-plugins-bad"
+brew "qtkeychain"
diff --git a/CMakeLists.txt b/CMakeLists.txt
index d2689a97..326e5794 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -41,6 +41,8 @@ option(USE_BUNDLED_LMDBXX "Use the bundled version of lmdb++."
${HUNTER_ENABLED})
option(USE_BUNDLED_TWEENY "Use the bundled version of tweeny."
${HUNTER_ENABLED})
+option(USE_BUNDLED_QTKEYCHAIN "Use the bundled version of Qt5Keychain."
+ ${HUNTER_ENABLED})
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
@@ -137,6 +139,24 @@ find_package(Qt5 COMPONENTS Core Widgets LinguistTools Concurrent Svg Multimedia
find_package(Qt5QuickCompiler)
find_package(Qt5DBus)
+if (USE_BUNDLED_QTKEYCHAIN)
+ include(FetchContent)
+ FetchContent_Declare(
+ qt5keychain
+ GIT_REPOSITORY https://github.com/frankosterfeld/qtkeychain.git
+ GIT_TAG v0.12.0
+ )
+ if (BUILD_SHARED_LIBS)
+ set(QTKEYCHAIN_STATIC OFF CACHE INTERNAL "")
+ else()
+ set(QTKEYCHAIN_STATIC ON CACHE INTERNAL "")
+ endif()
+ set(BUILD_TEST_APPLICATION OFF CACHE INTERNAL "")
+ FetchContent_MakeAvailable(qt5keychain)
+else()
+find_package(Qt5Keychain REQUIRED)
+endif()
+
if (APPLE)
find_package(Qt5MacExtras REQUIRED)
endif(APPLE)
@@ -333,25 +353,25 @@ endif()
find_package(OpenSSL 1.1.0 REQUIRED)
if(USE_BUNDLED_MTXCLIENT)
include(FetchContent)
- set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "")
- set(BUILD_LIB_TESTS OFF CACHE INTERNAL "")
FetchContent_Declare(
MatrixClient
GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git
- GIT_TAG ed6315563409ce9d47978ff2a2d771b863e375c5
+ GIT_TAG ce8bc9c3dd6bba432e716f55136133111b0186e7
)
+ set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "")
+ set(BUILD_LIB_TESTS OFF CACHE INTERNAL "")
FetchContent_MakeAvailable(MatrixClient)
else()
find_package(MatrixClient 0.3.1 REQUIRED)
endif()
if(USE_BUNDLED_OLM)
include(FetchContent)
- set(OLM_TESTS OFF CACHE INTERNAL "")
FetchContent_Declare(
Olm
GIT_REPOSITORY https://gitlab.matrix.org/matrix-org/olm.git
GIT_TAG 3.1.4
)
+ set(OLM_TESTS OFF CACHE INTERNAL "")
FetchContent_MakeAvailable(Olm)
else()
find_package(Olm 3)
@@ -573,6 +593,11 @@ else()
endif()
target_include_directories(nheko PRIVATE src includes third_party/blurhash third_party/cpp-httplib-0.5.12)
+# Fixup bundled keychain include dirs
+if (USE_BUNDLED_QTKEYCHAIN)
+target_include_directories(nheko PRIVATE ${qt5keychain_SOURCE_DIR} ${qt5keychain_BINARY_DIR})
+endif()
+
target_link_libraries(nheko PRIVATE
MatrixClient::MatrixClient
Boost::iostreams
@@ -587,6 +612,7 @@ target_link_libraries(nheko PRIVATE
Qt5::Qml
Qt5::QuickControls2
Qt5::QuickWidgets
+ qt5keychain
nlohmann_json::nlohmann_json
lmdbxx::lmdbxx
liblmdb::lmdb
diff --git a/io.github.NhekoReborn.Nheko.json b/io.github.NhekoReborn.Nheko.json
index 913e239a..34b0d7e7 100644
--- a/io.github.NhekoReborn.Nheko.json
+++ b/io.github.NhekoReborn.Nheko.json
@@ -95,6 +95,22 @@
]
},
{
+ "config-opts": [
+ "-DCMAKE_BUILD_TYPE=Release",
+ "-DBUILD_TEST_APPLICATION=OFF"
+ ],
+ "buildsystem": "cmake-ninja",
+ "name": "QtKeychain",
+ "sources": [
+ {
+ "commit": "815fe610353ff8ad7e2f1121c368a74df8db5eb7",
+ "tag": "v0.12.0",
+ "type": "git",
+ "url": "https://github.com/frankosterfeld/qtkeychain.git"
+ }
+ ]
+ },
+ {
"config-opts":[
"-DJSON_BuildTests=OFF"
],
@@ -145,7 +161,7 @@
"name": "mtxclient",
"sources": [
{
- "commit": "ed6315563409ce9d47978ff2a2d771b863e375c5",
+ "commit": "ce8bc9c3dd6bba432e716f55136133111b0186e7",
"type": "git",
"url": "https://github.com/Nheko-Reborn/mtxclient.git"
}
diff --git a/src/Cache.cpp b/src/Cache.cpp
index 05c2e486..674b5793 100644
--- a/src/Cache.cpp
+++ b/src/Cache.cpp
@@ -24,9 +24,14 @@
#include <QFile>
#include <QHash>
#include <QMap>
-#include <QSettings>
#include <QStandardPaths>
+#if __has_include(<keychain.h>)
+#include <keychain.h>
+#else
+#include <qt5keychain/keychain.h>
+#endif
+
#include <mtx/responses/common.hpp>
#include "Cache.h"
@@ -569,6 +574,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 +789,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 +4347,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 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<std::string>
+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<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 dab414a9..e3325c05 100644
--- a/src/ChatPage.cpp
+++ b/src/ChatPage.cpp
@@ -17,6 +17,7 @@
#include <QApplication>
#include <QImageReader>
+#include <QInputDialog>
#include <QMessageBox>
#include <QSettings>
#include <QShortcut>
@@ -64,6 +65,8 @@ constexpr size_t MAX_ONETIME_KEYS = 50;
Q_DECLARE_METATYPE(std::optional<mtx::crypto::EncryptedFile>)
Q_DECLARE_METATYPE(std::optional<RelatedInfo>)
Q_DECLARE_METATYPE(mtx::presence::PresenceState)
+Q_DECLARE_METATYPE(mtx::secret_storage::AesHmacSha2KeyDescription)
+Q_DECLARE_METATYPE(SecretsToDecrypt)
ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
: QWidget(parent)
@@ -79,6 +82,8 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
qRegisterMetaType<std::optional<mtx::crypto::EncryptedFile>>();
qRegisterMetaType<std::optional<RelatedInfo>>();
qRegisterMetaType<mtx::presence::PresenceState>();
+ qRegisterMetaType<mtx::secret_storage::AesHmacSha2KeyDescription>();
+ qRegisterMetaType<SecretsToDecrypt>();
topLayout_ = new QHBoxLayout(this);
topLayout_->setSpacing(0);
@@ -136,6 +141,12 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
splitter->addWidget(content_);
splitter->restoreSizes(parent->width());
+ connect(this,
+ &ChatPage::downloadedSecrets,
+ this,
+ &ChatPage::decryptDownloadedSecrets,
+ Qt::QueuedConnection);
+
connect(this, &ChatPage::connectionLost, this, [this]() {
nhlog::net()->info("connectivity lost");
isConnected_ = false;
@@ -372,9 +383,8 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
void
ChatPage::logout()
{
- deleteConfigs();
-
resetUI();
+ deleteConfigs();
emit closing();
connectivityTimer_.stop();
@@ -385,12 +395,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 +428,8 @@ ChatPage::deleteConfigs()
settings.remove("");
settings.endGroup();
+ http::client()->shutdown();
cache::deleteData();
- http::client()->clear();
}
void
@@ -1209,3 +1219,45 @@ ChatPage::connectCallMessage()
view_manager_,
qOverload<const QString &, const T &>(&TimelineViewManager::queueCallMessage));
}
+
+void
+ChatPage::decryptDownloadedSecrets(mtx::secret_storage::AesHmacSha2KeyDescription keyDesc,
+ const SecretsToDecrypt &secrets)
+{
+ QString text = QInputDialog::getText(
+ ChatPage::instance(),
+ QCoreApplication::translate("CrossSigningSecrets", "Decrypt secrets"),
+ keyDesc.name.empty()
+ ? QCoreApplication::translate(
+ "CrossSigningSecrets",
+ "Enter your recovery key or passphrase to decrypt your secrets:")
+ : QCoreApplication::translate(
+ "CrossSigningSecrets",
+ "Enter your recovery key or passphrase called %1 to decrypt your secrets:")
+ .arg(QString::fromStdString(keyDesc.name)),
+ QLineEdit::Password);
+
+ if (text.isEmpty())
+ return;
+
+ auto decryptionKey = mtx::crypto::key_from_recoverykey(text.toStdString(), keyDesc);
+
+ if (!decryptionKey)
+ decryptionKey = mtx::crypto::key_from_passphrase(text.toStdString(), keyDesc);
+
+ if (!decryptionKey) {
+ QMessageBox::information(
+ ChatPage::instance(),
+ QCoreApplication::translate("CrossSigningSecrets", "Decrytion failed"),
+ QCoreApplication::translate("CrossSigningSecrets",
+ "Failed to decrypt secrets with the "
+ "provided recovery key or passphrase"));
+ return;
+ }
+
+ for (const auto &[secretName, encryptedSecret] : secrets) {
+ auto decrypted = mtx::crypto::decrypt(encryptedSecret, *decryptionKey, secretName);
+ if (!decrypted.empty())
+ cache::storeSecret(secretName, decrypted);
+ }
+}
diff --git a/src/ChatPage.h b/src/ChatPage.h
index 5b336cbb..45a4ff63 100644
--- a/src/ChatPage.h
+++ b/src/ChatPage.h
@@ -27,6 +27,7 @@
#include <mtx/events/encrypted.hpp>
#include <mtx/events/member.hpp>
#include <mtx/events/presence.hpp>
+#include <mtx/secret_storage.hpp>
#include <QFrame>
#include <QHBoxLayout>
@@ -72,6 +73,8 @@ namespace popups {
class UserMentions;
}
+using SecretsToDecrypt = std::map<std::string, mtx::secret_storage::AesHmacSha2EncryptedData>;
+
class ChatPage : public QWidget
{
Q_OBJECT
@@ -117,6 +120,8 @@ public slots:
void unbanUser(QString userid, QString reason);
void receivedSessionKey(const std::string &room_id, const std::string &session_id);
+ void decryptDownloadedSecrets(mtx::secret_storage::AesHmacSha2KeyDescription keyDesc,
+ const SecretsToDecrypt &secrets);
signals:
void connectionLost();
@@ -185,6 +190,9 @@ signals:
void receivedDeviceVerificationReady(const mtx::events::msg::KeyVerificationReady &message);
void receivedDeviceVerificationDone(const mtx::events::msg::KeyVerificationDone &message);
+ void downloadedSecrets(mtx::secret_storage::AesHmacSha2KeyDescription keyDesc,
+ const SecretsToDecrypt &secrets);
+
private slots:
void logout();
void removeRoom(const QString &room_id);
diff --git a/src/DeviceVerificationFlow.cpp b/src/DeviceVerificationFlow.cpp
index 509fce8c..f692629e 100644
--- a/src/DeviceVerificationFlow.cpp
+++ b/src/DeviceVerificationFlow.cpp
@@ -275,11 +275,66 @@ DeviceVerificationFlow::DeviceVerificationFlow(QObject *,
req.signatures[utils::localUser().toStdString()]
[master_key.keys.at(mac.first)] =
master_key;
+ } else if (mac.first ==
+ "ed25519:" + this->deviceId.toStdString()) {
+ // Sign their device key with self signing key
+
+ auto device_id = this->deviceId.toStdString();
+
+ if (their_keys.device_keys.count(device_id)) {
+ json j =
+ their_keys.device_keys.at(device_id);
+ j.erase("signatures");
+ j.erase("unsigned");
+
+ auto secret = cache::secret(
+ mtx::secret_storage::secrets::
+ cross_signing_self_signing);
+ if (!secret)
+ continue;
+ auto ssk =
+ mtx::crypto::PkSigning::from_seed(
+ *secret);
+
+ mtx::crypto::DeviceKeys dev = j;
+ dev.signatures
+ [utils::localUser().toStdString()]
+ ["ed25519:" + ssk.public_key()] =
+ ssk.sign(j.dump());
+
+ req.signatures[utils::localUser()
+ .toStdString()]
+ [device_id] = dev;
+ }
}
}
- // TODO(Nico): Sign their device key with self signing key
} else {
- // TODO(Nico): Sign their master key with user signing key
+ // Sign their master key with user signing key
+ for (const auto &mac : msg.mac) {
+ if (their_keys.master_keys.keys.count(mac.first)) {
+ json j = their_keys.master_keys;
+ j.erase("signatures");
+ j.erase("unsigned");
+
+ auto secret =
+ cache::secret(mtx::secret_storage::secrets::
+ cross_signing_user_signing);
+ if (!secret)
+ continue;
+ auto usk =
+ mtx::crypto::PkSigning::from_seed(*secret);
+
+ mtx::crypto::CrossSigningKeys master_key = j;
+ master_key
+ .signatures[utils::localUser().toStdString()]
+ ["ed25519:" + usk.public_key()] =
+ usk.sign(j.dump());
+
+ req.signatures[toClient.to_string()]
+ [master_key.keys.at(mac.first)] =
+ master_key;
+ }
+ }
}
if (!req.signatures.empty()) {
@@ -706,6 +761,14 @@ DeviceVerificationFlow::acceptDevice()
cache::markDeviceVerified(this->toClient.to_string(), this->deviceId.toStdString());
this->sendVerificationDone();
setState(Success);
+
+ // Request secrets. We should probably check somehow, if a device knowns about the
+ // secrets.
+ if (utils::localUser().toStdString() == this->toClient.to_string() &&
+ (!cache::secret(mtx::secret_storage::secrets::cross_signing_self_signing) ||
+ !cache::secret(mtx::secret_storage::secrets::cross_signing_user_signing))) {
+ olm::request_cross_signing_keys();
+ }
}
}
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 <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 1f58758c..07fc49f6 100644
--- a/src/Olm.cpp
+++ b/src/Olm.cpp
@@ -1,9 +1,14 @@
#include "Olm.h"
#include <QObject>
+#include <QTimer>
+
#include <nlohmann/json.hpp>
#include <variant>
+#include <mtx/responses/common.hpp>
+#include <mtx/secret_storage.hpp>
+
#include "Cache.h"
#include "Cache_p.h"
#include "ChatPage.h"
@@ -13,11 +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 {
@@ -43,6 +50,54 @@ client()
return client_.get();
}
+static void
+handle_secret_request(const mtx::events::DeviceEvent<mtx::events::msg::SecretRequest> *e,
+ const std::string &sender)
+{
+ using namespace mtx::events;
+
+ if (e->content.action != mtx::events::msg::RequestAction::Request)
+ return;
+
+ auto local_user = http::client()->user_id();
+
+ if (sender != local_user.to_string())
+ return;
+
+ auto verificationStatus = cache::verificationStatus(local_user.to_string());
+
+ if (!verificationStatus)
+ return;
+
+ auto deviceKeys = cache::userKeys(local_user.to_string());
+ if (!deviceKeys)
+ return;
+
+ if (std::find(verificationStatus->verified_devices.begin(),
+ verificationStatus->verified_devices.end(),
+ e->content.requesting_device_id) ==
+ verificationStatus->verified_devices.end())
+ return;
+
+ // 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)
+ return;
+ secretSend.content.secret = secret.value();
+
+ send_encrypted_to_device_messages(
+ {{local_user.to_string(), {{e->content.requesting_device_id}}}}, secretSend);
+
+ nhlog::net()->info("Sent secret '{}' to ({},{})",
+ e->content.name,
+ local_user.to_string(),
+ e->content.requesting_device_id);
+}
+
void
handle_to_device_messages(const std::vector<mtx::events::collections::DeviceEvents> &msgs)
{
@@ -127,6 +182,10 @@ handle_to_device_messages(const std::vector<mtx::events::collections::DeviceEven
std::get<mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationDone>>(
msg);
ChatPage::instance()->receivedDeviceVerificationDone(message.content);
+ } else if (auto e =
+ std::get_if<mtx::events::DeviceEvent<mtx::events::msg::SecretRequest>>(
+ &msg)) {
+ handle_secret_request(e, e->sender);
} else {
nhlog::crypto()->warn("unhandled event: {}", j_msg.dump(2));
}
@@ -163,59 +222,137 @@ handle_olm_message(const OlmMessage &msg)
}
if (!payload.is_null()) {
- std::string msg_type = payload["type"];
-
- if (msg_type == to_string(mtx::events::EventType::KeyVerificationAccept)) {
- ChatPage::instance()->receivedDeviceVerificationAccept(
- payload["content"]);
- return;
- } else if (msg_type ==
- to_string(mtx::events::EventType::KeyVerificationRequest)) {
- ChatPage::instance()->receivedDeviceVerificationRequest(
- payload["content"], payload["sender"]);
- return;
- } else if (msg_type ==
- to_string(mtx::events::EventType::KeyVerificationCancel)) {
- ChatPage::instance()->receivedDeviceVerificationCancel(
- payload["content"]);
- return;
- } else if (msg_type ==
- to_string(mtx::events::EventType::KeyVerificationKey)) {
- ChatPage::instance()->receivedDeviceVerificationKey(
- payload["content"]);
- return;
- } else if (msg_type ==
- to_string(mtx::events::EventType::KeyVerificationMac)) {
- ChatPage::instance()->receivedDeviceVerificationMac(
- payload["content"]);
- return;
- } else if (msg_type ==
- to_string(mtx::events::EventType::KeyVerificationStart)) {
- ChatPage::instance()->receivedDeviceVerificationStart(
- payload["content"], payload["sender"]);
- return;
- } else if (msg_type ==
- to_string(mtx::events::EventType::KeyVerificationReady)) {
- ChatPage::instance()->receivedDeviceVerificationReady(
- payload["content"]);
- return;
- } else if (msg_type ==
- to_string(mtx::events::EventType::KeyVerificationDone)) {
- ChatPage::instance()->receivedDeviceVerificationDone(
- payload["content"]);
- return;
- } else if (msg_type == to_string(mtx::events::EventType::RoomKey)) {
- mtx::events::DeviceEvent<mtx::events::msg::RoomKey> roomKey =
- payload;
- create_inbound_megolm_session(roomKey, msg.sender_key);
- return;
- } else if (msg_type ==
- to_string(mtx::events::EventType::ForwardedRoomKey)) {
- mtx::events::DeviceEvent<mtx::events::msg::ForwardedRoomKey>
- roomKey = payload;
- import_inbound_megolm_session(roomKey);
- return;
+ mtx::events::collections::DeviceEvents device_event;
+
+ {
+ std::string msg_type = payload["type"];
+ json event_array = json::array();
+ event_array.push_back(payload);
+
+ std::vector<mtx::events::collections::DeviceEvents> temp_events;
+ mtx::responses::utils::parse_device_events(event_array,
+ temp_events);
+ if (temp_events.empty()) {
+ nhlog::crypto()->warn("Decrypted unknown event: {}",
+ payload.dump());
+ continue;
+ }
+ device_event = temp_events.at(0);
+ }
+
+ using namespace mtx::events;
+ if (auto e1 =
+ std::get_if<DeviceEvent<msg::KeyVerificationAccept>>(&device_event)) {
+ ChatPage::instance()->receivedDeviceVerificationAccept(e1->content);
+ } else if (auto e2 = std::get_if<DeviceEvent<msg::KeyVerificationRequest>>(
+ &device_event)) {
+ ChatPage::instance()->receivedDeviceVerificationRequest(e2->content,
+ e2->sender);
+ } else if (auto e3 = std::get_if<DeviceEvent<msg::KeyVerificationCancel>>(
+ &device_event)) {
+ ChatPage::instance()->receivedDeviceVerificationCancel(e3->content);
+ } else if (auto e4 = std::get_if<DeviceEvent<msg::KeyVerificationKey>>(
+ &device_event)) {
+ ChatPage::instance()->receivedDeviceVerificationKey(e4->content);
+ } else if (auto e5 = std::get_if<DeviceEvent<msg::KeyVerificationMac>>(
+ &device_event)) {
+ ChatPage::instance()->receivedDeviceVerificationMac(e5->content);
+ } else if (auto e6 = std::get_if<DeviceEvent<msg::KeyVerificationStart>>(
+ &device_event)) {
+ ChatPage::instance()->receivedDeviceVerificationStart(e6->content,
+ e6->sender);
+ } else if (auto e7 = std::get_if<DeviceEvent<msg::KeyVerificationReady>>(
+ &device_event)) {
+ ChatPage::instance()->receivedDeviceVerificationReady(e7->content);
+ } else if (auto e8 = std::get_if<DeviceEvent<msg::KeyVerificationDone>>(
+ &device_event)) {
+ ChatPage::instance()->receivedDeviceVerificationDone(e8->content);
+ } else if (auto roomKey =
+ std::get_if<DeviceEvent<msg::RoomKey>>(&device_event)) {
+ create_inbound_megolm_session(*roomKey, msg.sender_key);
+ } else if (auto forwardedRoomKey =
+ std::get_if<DeviceEvent<msg::ForwardedRoomKey>>(
+ &device_event)) {
+ import_inbound_megolm_session(*forwardedRoomKey);
+ } 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 sec_req =
+ std::get_if<DeviceEvent<msg::SecretRequest>>(&device_event)) {
+ handle_secret_request(sec_req, msg.sender);
}
+
+ return;
}
}
}
@@ -332,11 +469,13 @@ encrypt_group_message(const std::string &room_id, const std::string &device_id,
// new member, send them the session at this index
sendSessionTo[member_it->first] = {};
- for (const auto &dev : member_it->second->device_keys)
- if (member_it->first != own_user_id ||
- dev.first != device_id)
- sendSessionTo[member_it->first].push_back(
- dev.first);
+ if (member_it->second) {
+ for (const auto &dev : member_it->second->device_keys)
+ if (member_it->first != own_user_id ||
+ dev.first != device_id)
+ sendSessionTo[member_it->first].push_back(
+ dev.first);
+ }
++member_it;
} else {
@@ -1035,4 +1174,143 @@ send_encrypted_to_device_messages(const std::map<std::string, std::vector<std::s
}
}
+void
+request_cross_signing_keys()
+{
+ mtx::events::msg::SecretRequest secretRequest{};
+ secretRequest.action = mtx::events::msg::RequestAction::Request;
+ secretRequest.requesting_device_id = http::client()->device_id();
+
+ auto local_user = http::client()->user_id();
+
+ auto verificationStatus = cache::verificationStatus(local_user.to_string());
+
+ if (!verificationStatus)
+ return;
+
+ auto request = [&](std::string secretName) {
+ secretRequest.name = secretName;
+ secretRequest.request_id = "ss." + http::client()->generate_txn_id();
+
+ request_id_to_secret_name[secretRequest.request_id] = secretRequest.name;
+
+ 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)
+ body[local_user][dev] = secretRequest;
+ }
+
+ http::client()->send_to_device<mtx::events::msg::SecretRequest>(
+ http::client()->generate_txn_id(),
+ body,
+ [request_id = secretRequest.request_id, secretName](mtx::http::RequestErr err) {
+ if (err) {
+ request_id_to_secret_name.erase(request_id);
+ nhlog::net()->error("Failed to send request for secrect '{}'",
+ secretName);
+ return;
+ }
+ });
+
+ for (const auto &dev : verificationStatus->verified_devices) {
+ if (dev != secretRequest.requesting_device_id)
+ body[local_user][dev].action =
+ mtx::events::msg::RequestAction::Cancellation;
+ }
+
+ // timeout after 15 min
+ QTimer::singleShot(15 * 60 * 1000, [secretRequest, body]() {
+ if (request_id_to_secret_name.count(secretRequest.request_id)) {
+ request_id_to_secret_name.erase(secretRequest.request_id);
+ http::client()->send_to_device<mtx::events::msg::SecretRequest>(
+ http::client()->generate_txn_id(),
+ body,
+ [secretRequest](mtx::http::RequestErr err) {
+ if (err) {
+ nhlog::net()->error(
+ "Failed to cancel request for secrect '{}'",
+ secretRequest.name);
+ return;
+ }
+ });
+ }
+ });
+ };
+
+ request(mtx::secret_storage::secrets::cross_signing_self_signing);
+ request(mtx::secret_storage::secrets::cross_signing_user_signing);
+ request(mtx::secret_storage::secrets::megolm_backup_v1);
+}
+
+namespace {
+void
+unlock_secrets(const std::string &key,
+ const std::map<std::string, mtx::secret_storage::AesHmacSha2EncryptedData> &secrets)
+{
+ http::client()->secret_storage_key(
+ key,
+ [secrets](mtx::secret_storage::AesHmacSha2KeyDescription keyDesc,
+ mtx::http::RequestErr err) {
+ if (err) {
+ nhlog::net()->error("Failed to download secret storage key");
+ return;
+ }
+
+ emit ChatPage::instance()->downloadedSecrets(keyDesc, secrets);
+ });
+}
+}
+
+void
+download_cross_signing_keys()
+{
+ using namespace mtx::secret_storage;
+ http::client()->secret_storage_secret(
+ secrets::megolm_backup_v1, [](Secret secret, mtx::http::RequestErr err) {
+ std::optional<Secret> backup_key;
+ if (!err)
+ backup_key = secret;
+
+ http::client()->secret_storage_secret(
+ secrets::cross_signing_self_signing,
+ [backup_key](Secret secret, mtx::http::RequestErr err) {
+ std::optional<Secret> self_signing_key;
+ if (!err)
+ self_signing_key = secret;
+
+ http::client()->secret_storage_secret(
+ secrets::cross_signing_user_signing,
+ [backup_key, self_signing_key](Secret secret,
+ mtx::http::RequestErr err) {
+ std::optional<Secret> user_signing_key;
+ if (!err)
+ user_signing_key = secret;
+
+ std::map<std::string,
+ std::map<std::string, AesHmacSha2EncryptedData>>
+ secrets;
+
+ if (backup_key && !backup_key->encrypted.empty())
+ secrets[backup_key->encrypted.begin()->first]
+ [secrets::megolm_backup_v1] =
+ backup_key->encrypted.begin()->second;
+ if (self_signing_key && !self_signing_key->encrypted.empty())
+ secrets[self_signing_key->encrypted.begin()->first]
+ [secrets::cross_signing_self_signing] =
+ self_signing_key->encrypted.begin()->second;
+ if (user_signing_key && !user_signing_key->encrypted.empty())
+ secrets[user_signing_key->encrypted.begin()->first]
+ [secrets::cross_signing_user_signing] =
+ user_signing_key->encrypted.begin()->second;
+
+ for (const auto &[key, secrets] : secrets)
+ unlock_secrets(key, secrets);
+ });
+ });
+ });
+}
+
} // namespace olm
diff --git a/src/Olm.h b/src/Olm.h
index 3400f993..78c1e641 100644
--- a/src/Olm.h
+++ b/src/Olm.h
@@ -102,4 +102,11 @@ send_encrypted_to_device_messages(const std::map<std::string, std::vector<std::s
const mtx::events::collections::DeviceEvents &event,
bool force_new_session = false);
+//! Request backup and signing keys and cache them locally
+void
+request_cross_signing_keys();
+//! Download backup and signing keys and cache them locally
+void
+download_cross_signing_keys();
+
} // namespace olm
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<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 17d1adb8..708fb7fd 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);
@@ -649,6 +658,18 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge
sessionKeysLayout->addWidget(sessionKeysExportBtn, 0, Qt::AlignRight);
sessionKeysLayout->addWidget(sessionKeysImportBtn, 0, Qt::AlignRight);
+ auto crossSigningKeysLabel = new QLabel{tr("Cross Signing Keys"), this};
+ crossSigningKeysLabel->setFont(font);
+ crossSigningKeysLabel->setMargin(OptionMargin);
+
+ auto crossSigningRequestBtn = new QPushButton{tr("REQUEST"), this};
+ auto crossSigningDownloadBtn = new QPushButton{tr("DOWNLOAD"), this};
+
+ auto crossSigningKeysLayout = new QHBoxLayout;
+ crossSigningKeysLayout->addWidget(new QLabel{"", this}, 1, Qt::AlignRight);
+ crossSigningKeysLayout->addWidget(crossSigningRequestBtn, 0, Qt::AlignRight);
+ crossSigningKeysLayout->addWidget(crossSigningDownloadBtn, 0, Qt::AlignRight);
+
auto boxWrap = [this, &font](QString labelText, QWidget *field, QString tooltipText = "") {
auto label = new QLabel{labelText, this};
label->setFont(font);
@@ -787,6 +808,28 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge
tr("Automatically replies to key requests from other users, if they are verified."));
formLayout_->addRow(new HorizontalLine{this});
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);
@@ -982,6 +1025,14 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge
connect(
sessionKeysExportBtn, &QPushButton::clicked, this, &UserSettingsPage::exportSessionKeys);
+ connect(crossSigningRequestBtn, &QPushButton::clicked, this, []() {
+ olm::request_cross_signing_keys();
+ });
+
+ connect(crossSigningDownloadBtn, &QPushButton::clicked, this, []() {
+ olm::download_cross_signing_keys();
+ });
+
connect(backBtn_, &QPushButton::clicked, this, [this]() {
settings_->save();
emit moveBack();
@@ -1137,3 +1188,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 = [&ok, ¬SoOk](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/Utils.cpp b/src/Utils.cpp
index 7fcaf5e2..1d8fcd9c 100644
--- a/src/Utils.cpp
+++ b/src/Utils.cpp
@@ -54,8 +54,9 @@ bool
utils::codepointIsEmoji(uint code)
{
// TODO: Be more precise here.
- return (code >= 0x2600 && code <= 0x27bf) || (code >= 0x1f300 && code <= 0x1f3ff) ||
- (code >= 0x1f000 && code <= 0x1faff);
+ return (code >= 0x2600 && code <= 0x27bf) || (code >= 0x2b00 && code <= 0x2bff) ||
+ (code >= 0x1f300 && code <= 0x1f3ff) || (code >= 0x1f000 && code <= 0x1faff) ||
+ code == 0x200d;
}
QString
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_; }
|