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 @@ -94,6 +94,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 #include #include -#include #include +#if __has_include() +#include +#else +#include +#endif + #include #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 +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 +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..e3325c05 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -17,6 +17,7 @@ #include #include +#include #include #include #include @@ -64,6 +65,8 @@ constexpr size_t MAX_ONETIME_KEYS = 50; Q_DECLARE_METATYPE(std::optional) Q_DECLARE_METATYPE(std::optional) Q_DECLARE_METATYPE(mtx::presence::PresenceState) +Q_DECLARE_METATYPE(mtx::secret_storage::AesHmacSha2KeyDescription) +Q_DECLARE_METATYPE(SecretsToDecrypt) ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) : QWidget(parent) @@ -79,6 +82,8 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) qRegisterMetaType>(); qRegisterMetaType>(); qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); topLayout_ = new QHBoxLayout(this); topLayout_->setSpacing(0); @@ -136,6 +141,12 @@ ChatPage::ChatPage(QSharedPointer 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, 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(&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 #include #include +#include #include #include @@ -72,6 +73,8 @@ namespace popups { class UserMentions; } +using SecretsToDecrypt = std::map; + 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 #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 +#include + #include #include +#include +#include + #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(); + +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 { @@ -43,6 +50,54 @@ client() return client_.get(); } +static void +handle_secret_request(const mtx::events::DeviceEvent *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 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 &msgs) { @@ -127,6 +182,10 @@ handle_to_device_messages(const std::vector>( msg); ChatPage::instance()->receivedDeviceVerificationDone(message.content); + } else if (auto e = + std::get_if>( + &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"]; + mtx::events::collections::DeviceEvents device_event; - 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 roomKey = - payload; - create_inbound_megolm_session(roomKey, msg.sender_key); - return; - } else if (msg_type == - to_string(mtx::events::EventType::ForwardedRoomKey)) { - mtx::events::DeviceEvent - roomKey = payload; - import_inbound_megolm_session(roomKey); - return; + { + std::string msg_type = payload["type"]; + json event_array = json::array(); + event_array.push_back(payload); + + std::vector 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>(&device_event)) { + ChatPage::instance()->receivedDeviceVerificationAccept(e1->content); + } else if (auto e2 = std::get_if>( + &device_event)) { + ChatPage::instance()->receivedDeviceVerificationRequest(e2->content, + e2->sender); + } else if (auto e3 = std::get_if>( + &device_event)) { + ChatPage::instance()->receivedDeviceVerificationCancel(e3->content); + } else if (auto e4 = std::get_if>( + &device_event)) { + ChatPage::instance()->receivedDeviceVerificationKey(e4->content); + } else if (auto e5 = std::get_if>( + &device_event)) { + ChatPage::instance()->receivedDeviceVerificationMac(e5->content); + } else if (auto e6 = std::get_if>( + &device_event)) { + ChatPage::instance()->receivedDeviceVerificationStart(e6->content, + e6->sender); + } else if (auto e7 = std::get_if>( + &device_event)) { + ChatPage::instance()->receivedDeviceVerificationReady(e7->content); + } else if (auto e8 = std::get_if>( + &device_event)) { + ChatPage::instance()->receivedDeviceVerificationDone(e8->content); + } else if (auto roomKey = + std::get_if>(&device_event)) { + create_inbound_megolm_session(*roomKey, msg.sender_key); + } else if (auto forwardedRoomKey = + std::get_if>( + &device_event)) { + import_inbound_megolm_session(*forwardedRoomKey); + } 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 sec_req = + std::get_if>(&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::mapdevice_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> + body; + + for (const auto &dev : verificationStatus->verified_devices) { + if (dev != secretRequest.requesting_device_id) + body[local_user][dev] = secretRequest; + } + + http::client()->send_to_device( + 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( + 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 &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 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 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 user_signing_key; + if (!err) + user_signing_key = secret; + + std::map> + 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 &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 17d1adb8..708fb7fd 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); @@ -649,6 +658,18 @@ UserSettingsPage::UserSettingsPage(QSharedPointer 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 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 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_; }