Merge pull request #355 from Nheko-Reborn/ssss

Cross-signing with self and user signing keys
This commit is contained in:
DeepBlueV7.X 2020-12-18 15:20:11 +01:00 committed by GitHub
commit 45701b0896
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 735 additions and 78 deletions

View file

@ -12,3 +12,4 @@ brew "gstreamer"
brew "gst-plugins-base" brew "gst-plugins-base"
brew "gst-plugins-good" brew "gst-plugins-good"
brew "gst-plugins-bad" brew "gst-plugins-bad"
brew "qtkeychain"

View file

@ -41,6 +41,8 @@ option(USE_BUNDLED_LMDBXX "Use the bundled version of lmdb++."
${HUNTER_ENABLED}) ${HUNTER_ENABLED})
option(USE_BUNDLED_TWEENY "Use the bundled version of tweeny." option(USE_BUNDLED_TWEENY "Use the bundled version of tweeny."
${HUNTER_ENABLED}) ${HUNTER_ENABLED})
option(USE_BUNDLED_QTKEYCHAIN "Use the bundled version of Qt5Keychain."
${HUNTER_ENABLED})
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake") 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(Qt5QuickCompiler)
find_package(Qt5DBus) 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) if (APPLE)
find_package(Qt5MacExtras REQUIRED) find_package(Qt5MacExtras REQUIRED)
endif(APPLE) endif(APPLE)
@ -333,25 +353,25 @@ endif()
find_package(OpenSSL 1.1.0 REQUIRED) find_package(OpenSSL 1.1.0 REQUIRED)
if(USE_BUNDLED_MTXCLIENT) if(USE_BUNDLED_MTXCLIENT)
include(FetchContent) include(FetchContent)
set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "")
set(BUILD_LIB_TESTS OFF CACHE INTERNAL "")
FetchContent_Declare( FetchContent_Declare(
MatrixClient MatrixClient
GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git 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) FetchContent_MakeAvailable(MatrixClient)
else() else()
find_package(MatrixClient 0.3.1 REQUIRED) find_package(MatrixClient 0.3.1 REQUIRED)
endif() endif()
if(USE_BUNDLED_OLM) if(USE_BUNDLED_OLM)
include(FetchContent) include(FetchContent)
set(OLM_TESTS OFF CACHE INTERNAL "")
FetchContent_Declare( FetchContent_Declare(
Olm Olm
GIT_REPOSITORY https://gitlab.matrix.org/matrix-org/olm.git GIT_REPOSITORY https://gitlab.matrix.org/matrix-org/olm.git
GIT_TAG 3.1.4 GIT_TAG 3.1.4
) )
set(OLM_TESTS OFF CACHE INTERNAL "")
FetchContent_MakeAvailable(Olm) FetchContent_MakeAvailable(Olm)
else() else()
find_package(Olm 3) find_package(Olm 3)
@ -573,6 +593,11 @@ else()
endif() endif()
target_include_directories(nheko PRIVATE src includes third_party/blurhash third_party/cpp-httplib-0.5.12) 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 target_link_libraries(nheko PRIVATE
MatrixClient::MatrixClient MatrixClient::MatrixClient
Boost::iostreams Boost::iostreams
@ -587,6 +612,7 @@ target_link_libraries(nheko PRIVATE
Qt5::Qml Qt5::Qml
Qt5::QuickControls2 Qt5::QuickControls2
Qt5::QuickWidgets Qt5::QuickWidgets
qt5keychain
nlohmann_json::nlohmann_json nlohmann_json::nlohmann_json
lmdbxx::lmdbxx lmdbxx::lmdbxx
liblmdb::lmdb liblmdb::lmdb

View file

@ -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":[ "config-opts":[
"-DJSON_BuildTests=OFF" "-DJSON_BuildTests=OFF"
@ -145,7 +161,7 @@
"name": "mtxclient", "name": "mtxclient",
"sources": [ "sources": [
{ {
"commit": "ed6315563409ce9d47978ff2a2d771b863e375c5", "commit": "ce8bc9c3dd6bba432e716f55136133111b0186e7",
"type": "git", "type": "git",
"url": "https://github.com/Nheko-Reborn/mtxclient.git" "url": "https://github.com/Nheko-Reborn/mtxclient.git"
} }

View file

@ -24,9 +24,14 @@
#include <QFile> #include <QFile>
#include <QHash> #include <QHash>
#include <QMap> #include <QMap>
#include <QSettings>
#include <QStandardPaths> #include <QStandardPaths>
#if __has_include(<keychain.h>)
#include <keychain.h>
#else
#include <qt5keychain/keychain.h>
#endif
#include <mtx/responses/common.hpp> #include <mtx/responses/common.hpp>
#include "Cache.h" #include "Cache.h"
@ -569,6 +574,64 @@ Cache::restoreOlmAccount()
return std::string(pickled.data(), pickled.size()); 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 // Media Management
// //
@ -726,10 +789,32 @@ void
Cache::deleteData() Cache::deleteData()
{ {
// TODO: We need to remove the env_ while not accepting new requests. // 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()) { if (!cacheDirectory_.isEmpty()) {
QDir(cacheDirectory_).removeRecursively(); QDir(cacheDirectory_).removeRecursively();
nhlog::db()->info("deleted cache files from disk"); 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 //! migrates db to the current format
@ -4262,4 +4347,15 @@ restoreOlmAccount()
{ {
return instance_->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 } // namespace cache

View file

@ -282,4 +282,9 @@ saveOlmAccount(const std::string &pickled);
std::string std::string
restoreOlmAccount(); restoreOlmAccount();
void
storeSecret(const std::string &name, const std::string &secret);
std::optional<std::string>
secret(const std::string &name);
} }

View file

@ -269,6 +269,10 @@ public:
void saveOlmAccount(const std::string &pickled); void saveOlmAccount(const std::string &pickled);
std::string restoreOlmAccount(); 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: signals:
void newReadReceipts(const QString &room_id, const std::vector<QString> &event_ids); void newReadReceipts(const QString &room_id, const std::vector<QString> &event_ids);
void roomReadStatus(const std::map<QString, bool> &status); void roomReadStatus(const std::map<QString, bool> &status);
@ -276,6 +280,7 @@ signals:
void userKeysUpdate(const std::string &sync_token, void userKeysUpdate(const std::string &sync_token,
const mtx::responses::QueryKeys &keyQuery); const mtx::responses::QueryKeys &keyQuery);
void verificationStatusChanged(const std::string &userid); void verificationStatusChanged(const std::string &userid);
void secretChanged(const std::string name);
private: private:
//! Save an invited room. //! Save an invited room.

View file

@ -17,6 +17,7 @@
#include <QApplication> #include <QApplication>
#include <QImageReader> #include <QImageReader>
#include <QInputDialog>
#include <QMessageBox> #include <QMessageBox>
#include <QSettings> #include <QSettings>
#include <QShortcut> #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<mtx::crypto::EncryptedFile>)
Q_DECLARE_METATYPE(std::optional<RelatedInfo>) Q_DECLARE_METATYPE(std::optional<RelatedInfo>)
Q_DECLARE_METATYPE(mtx::presence::PresenceState) Q_DECLARE_METATYPE(mtx::presence::PresenceState)
Q_DECLARE_METATYPE(mtx::secret_storage::AesHmacSha2KeyDescription)
Q_DECLARE_METATYPE(SecretsToDecrypt)
ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
: QWidget(parent) : QWidget(parent)
@ -79,6 +82,8 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
qRegisterMetaType<std::optional<mtx::crypto::EncryptedFile>>(); qRegisterMetaType<std::optional<mtx::crypto::EncryptedFile>>();
qRegisterMetaType<std::optional<RelatedInfo>>(); qRegisterMetaType<std::optional<RelatedInfo>>();
qRegisterMetaType<mtx::presence::PresenceState>(); qRegisterMetaType<mtx::presence::PresenceState>();
qRegisterMetaType<mtx::secret_storage::AesHmacSha2KeyDescription>();
qRegisterMetaType<SecretsToDecrypt>();
topLayout_ = new QHBoxLayout(this); topLayout_ = new QHBoxLayout(this);
topLayout_->setSpacing(0); topLayout_->setSpacing(0);
@ -136,6 +141,12 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
splitter->addWidget(content_); splitter->addWidget(content_);
splitter->restoreSizes(parent->width()); splitter->restoreSizes(parent->width());
connect(this,
&ChatPage::downloadedSecrets,
this,
&ChatPage::decryptDownloadedSecrets,
Qt::QueuedConnection);
connect(this, &ChatPage::connectionLost, this, [this]() { connect(this, &ChatPage::connectionLost, this, [this]() {
nhlog::net()->info("connectivity lost"); nhlog::net()->info("connectivity lost");
isConnected_ = false; isConnected_ = false;
@ -372,9 +383,8 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
void void
ChatPage::logout() ChatPage::logout()
{ {
deleteConfigs();
resetUI(); resetUI();
deleteConfigs();
emit closing(); emit closing();
connectivityTimer_.stop(); connectivityTimer_.stop();
@ -385,12 +395,12 @@ ChatPage::dropToLoginPage(const QString &msg)
{ {
nhlog::ui()->info("dropping to the login page: {}", msg.toStdString()); nhlog::ui()->info("dropping to the login page: {}", msg.toStdString());
deleteConfigs();
resetUI();
http::client()->shutdown(); http::client()->shutdown();
connectivityTimer_.stop(); connectivityTimer_.stop();
resetUI();
deleteConfigs();
emit showLoginPage(msg); emit showLoginPage(msg);
} }
@ -418,8 +428,8 @@ ChatPage::deleteConfigs()
settings.remove(""); settings.remove("");
settings.endGroup(); settings.endGroup();
http::client()->shutdown();
cache::deleteData(); cache::deleteData();
http::client()->clear();
} }
void void
@ -1209,3 +1219,45 @@ ChatPage::connectCallMessage()
view_manager_, view_manager_,
qOverload<const QString &, const T &>(&TimelineViewManager::queueCallMessage)); 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);
}
}

View file

@ -27,6 +27,7 @@
#include <mtx/events/encrypted.hpp> #include <mtx/events/encrypted.hpp>
#include <mtx/events/member.hpp> #include <mtx/events/member.hpp>
#include <mtx/events/presence.hpp> #include <mtx/events/presence.hpp>
#include <mtx/secret_storage.hpp>
#include <QFrame> #include <QFrame>
#include <QHBoxLayout> #include <QHBoxLayout>
@ -72,6 +73,8 @@ namespace popups {
class UserMentions; class UserMentions;
} }
using SecretsToDecrypt = std::map<std::string, mtx::secret_storage::AesHmacSha2EncryptedData>;
class ChatPage : public QWidget class ChatPage : public QWidget
{ {
Q_OBJECT Q_OBJECT
@ -117,6 +120,8 @@ public slots:
void unbanUser(QString userid, QString reason); void unbanUser(QString userid, QString reason);
void receivedSessionKey(const std::string &room_id, const std::string &session_id); void receivedSessionKey(const std::string &room_id, const std::string &session_id);
void decryptDownloadedSecrets(mtx::secret_storage::AesHmacSha2KeyDescription keyDesc,
const SecretsToDecrypt &secrets);
signals: signals:
void connectionLost(); void connectionLost();
@ -185,6 +190,9 @@ signals:
void receivedDeviceVerificationReady(const mtx::events::msg::KeyVerificationReady &message); void receivedDeviceVerificationReady(const mtx::events::msg::KeyVerificationReady &message);
void receivedDeviceVerificationDone(const mtx::events::msg::KeyVerificationDone &message); void receivedDeviceVerificationDone(const mtx::events::msg::KeyVerificationDone &message);
void downloadedSecrets(mtx::secret_storage::AesHmacSha2KeyDescription keyDesc,
const SecretsToDecrypt &secrets);
private slots: private slots:
void logout(); void logout();
void removeRoom(const QString &room_id); void removeRoom(const QString &room_id);

View file

@ -275,11 +275,66 @@ DeviceVerificationFlow::DeviceVerificationFlow(QObject *,
req.signatures[utils::localUser().toStdString()] req.signatures[utils::localUser().toStdString()]
[master_key.keys.at(mac.first)] = [master_key.keys.at(mac.first)] =
master_key; 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 { } 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()) { if (!req.signatures.empty()) {
@ -706,6 +761,14 @@ DeviceVerificationFlow::acceptDevice()
cache::markDeviceVerified(this->toClient.to_string(), this->deviceId.toStdString()); cache::markDeviceVerified(this->toClient.to_string(), this->deviceId.toStdString());
this->sendVerificationDone(); this->sendVerificationDone();
setState(Success); 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();
}
} }
} }

View file

@ -26,6 +26,7 @@
#include <mtx/responses/login.hpp> #include <mtx/responses/login.hpp>
#include "Cache.h" #include "Cache.h"
#include "Cache_p.h"
#include "ChatPage.h" #include "ChatPage.h"
#include "Config.h" #include "Config.h"
#include "Logging.h" #include "Logging.h"
@ -294,6 +295,10 @@ MainWindow::showChatPage()
login_page_->reset(); login_page_->reset();
chat_page_->bootstrap(userid, homeserver, token); chat_page_->bootstrap(userid, homeserver, token);
connect(cache::client(),
&Cache::secretChanged,
userSettingsPage_,
&UserSettingsPage::updateSecretStatus);
instance_ = this; instance_ = this;
} }

View file

@ -1,9 +1,14 @@
#include "Olm.h" #include "Olm.h"
#include <QObject> #include <QObject>
#include <QTimer>
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include <variant> #include <variant>
#include <mtx/responses/common.hpp>
#include <mtx/secret_storage.hpp>
#include "Cache.h" #include "Cache.h"
#include "Cache_p.h" #include "Cache_p.h"
#include "ChatPage.h" #include "ChatPage.h"
@ -13,11 +18,13 @@
#include "UserSettingsPage.h" #include "UserSettingsPage.h"
#include "Utils.h" #include "Utils.h"
static const std::string STORAGE_SECRET_KEY("secret");
constexpr auto MEGOLM_ALGO = "m.megolm.v1.aes-sha2";
namespace { namespace {
auto client_ = std::make_unique<mtx::crypto::OlmClient>(); 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 { namespace olm {
@ -43,6 +50,54 @@ client()
return client_.get(); 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 void
handle_to_device_messages(const std::vector<mtx::events::collections::DeviceEvents> &msgs) 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>>( std::get<mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationDone>>(
msg); msg);
ChatPage::instance()->receivedDeviceVerificationDone(message.content); 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 { } else {
nhlog::crypto()->warn("unhandled event: {}", j_msg.dump(2)); nhlog::crypto()->warn("unhandled event: {}", j_msg.dump(2));
} }
@ -163,59 +222,137 @@ handle_olm_message(const OlmMessage &msg)
} }
if (!payload.is_null()) { 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( std::string msg_type = payload["type"];
payload["content"]); json event_array = json::array();
return; event_array.push_back(payload);
} else if (msg_type ==
to_string(mtx::events::EventType::KeyVerificationRequest)) { std::vector<mtx::events::collections::DeviceEvents> temp_events;
ChatPage::instance()->receivedDeviceVerificationRequest( mtx::responses::utils::parse_device_events(event_array,
payload["content"], payload["sender"]); temp_events);
return; if (temp_events.empty()) {
} else if (msg_type == nhlog::crypto()->warn("Decrypted unknown event: {}",
to_string(mtx::events::EventType::KeyVerificationCancel)) { payload.dump());
ChatPage::instance()->receivedDeviceVerificationCancel( continue;
payload["content"]); }
return; device_event = temp_events.at(0);
} else if (msg_type == }
to_string(mtx::events::EventType::KeyVerificationKey)) {
ChatPage::instance()->receivedDeviceVerificationKey( using namespace mtx::events;
payload["content"]); if (auto e1 =
return; std::get_if<DeviceEvent<msg::KeyVerificationAccept>>(&device_event)) {
} else if (msg_type == ChatPage::instance()->receivedDeviceVerificationAccept(e1->content);
to_string(mtx::events::EventType::KeyVerificationMac)) { } else if (auto e2 = std::get_if<DeviceEvent<msg::KeyVerificationRequest>>(
ChatPage::instance()->receivedDeviceVerificationMac( &device_event)) {
payload["content"]); ChatPage::instance()->receivedDeviceVerificationRequest(e2->content,
return; e2->sender);
} else if (msg_type == } else if (auto e3 = std::get_if<DeviceEvent<msg::KeyVerificationCancel>>(
to_string(mtx::events::EventType::KeyVerificationStart)) { &device_event)) {
ChatPage::instance()->receivedDeviceVerificationStart( ChatPage::instance()->receivedDeviceVerificationCancel(e3->content);
payload["content"], payload["sender"]); } else if (auto e4 = std::get_if<DeviceEvent<msg::KeyVerificationKey>>(
return; &device_event)) {
} else if (msg_type == ChatPage::instance()->receivedDeviceVerificationKey(e4->content);
to_string(mtx::events::EventType::KeyVerificationReady)) { } else if (auto e5 = std::get_if<DeviceEvent<msg::KeyVerificationMac>>(
ChatPage::instance()->receivedDeviceVerificationReady( &device_event)) {
payload["content"]); ChatPage::instance()->receivedDeviceVerificationMac(e5->content);
return; } else if (auto e6 = std::get_if<DeviceEvent<msg::KeyVerificationStart>>(
} else if (msg_type == &device_event)) {
to_string(mtx::events::EventType::KeyVerificationDone)) { ChatPage::instance()->receivedDeviceVerificationStart(e6->content,
ChatPage::instance()->receivedDeviceVerificationDone( e6->sender);
payload["content"]); } else if (auto e7 = std::get_if<DeviceEvent<msg::KeyVerificationReady>>(
return; &device_event)) {
} else if (msg_type == to_string(mtx::events::EventType::RoomKey)) { ChatPage::instance()->receivedDeviceVerificationReady(e7->content);
mtx::events::DeviceEvent<mtx::events::msg::RoomKey> roomKey = } else if (auto e8 = std::get_if<DeviceEvent<msg::KeyVerificationDone>>(
payload; &device_event)) {
create_inbound_megolm_session(roomKey, msg.sender_key); ChatPage::instance()->receivedDeviceVerificationDone(e8->content);
return; } else if (auto roomKey =
} else if (msg_type == std::get_if<DeviceEvent<msg::RoomKey>>(&device_event)) {
to_string(mtx::events::EventType::ForwardedRoomKey)) { create_inbound_megolm_session(*roomKey, msg.sender_key);
mtx::events::DeviceEvent<mtx::events::msg::ForwardedRoomKey> } else if (auto forwardedRoomKey =
roomKey = payload; std::get_if<DeviceEvent<msg::ForwardedRoomKey>>(
import_inbound_megolm_session(roomKey); &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; 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 // new member, send them the session at this index
sendSessionTo[member_it->first] = {}; sendSessionTo[member_it->first] = {};
if (member_it->second) {
for (const auto &dev : member_it->second->device_keys) for (const auto &dev : member_it->second->device_keys)
if (member_it->first != own_user_id || if (member_it->first != own_user_id ||
dev.first != device_id) dev.first != device_id)
sendSessionTo[member_it->first].push_back( sendSessionTo[member_it->first].push_back(
dev.first); dev.first);
}
++member_it; ++member_it;
} else { } 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 } // namespace olm

View file

@ -102,4 +102,11 @@ send_encrypted_to_device_messages(const std::map<std::string, std::vector<std::s
const mtx::events::collections::DeviceEvents &event, const mtx::events::collections::DeviceEvents &event,
bool force_new_session = false); 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 } // namespace olm

View file

@ -43,7 +43,11 @@ public:
void initialize(const QMap<QString, RoomInfo> &info); void initialize(const QMap<QString, RoomInfo> &info);
void sync(const std::map<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 updateAvatar(const QString &room_id, const QString &url);
void addRoom(const QString &room_id, const RoomInfo &info); void addRoom(const QString &room_id, const RoomInfo &info);

View file

@ -637,6 +637,15 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge
deviceFingerprintValue_->setText(utils::humanReadableFingerprint(QString(44, 'X'))); 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}; auto sessionKeysLabel = new QLabel{tr("Session Keys"), this};
sessionKeysLabel->setFont(font); sessionKeysLabel->setFont(font);
sessionKeysLabel->setMargin(OptionMargin); sessionKeysLabel->setMargin(OptionMargin);
@ -649,6 +658,18 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge
sessionKeysLayout->addWidget(sessionKeysExportBtn, 0, Qt::AlignRight); sessionKeysLayout->addWidget(sessionKeysExportBtn, 0, Qt::AlignRight);
sessionKeysLayout->addWidget(sessionKeysImportBtn, 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 boxWrap = [this, &font](QString labelText, QWidget *field, QString tooltipText = "") {
auto label = new QLabel{labelText, this}; auto label = new QLabel{labelText, this};
label->setFont(font); 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.")); tr("Automatically replies to key requests from other users, if they are verified."));
formLayout_->addRow(new HorizontalLine{this}); formLayout_->addRow(new HorizontalLine{this});
formLayout_->addRow(sessionKeysLabel, sessionKeysLayout); 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}; auto scrollArea_ = new QScrollArea{this};
scrollArea_->setFrameShape(QFrame::NoFrame); scrollArea_->setFrameShape(QFrame::NoFrame);
@ -982,6 +1025,14 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge
connect( connect(
sessionKeysExportBtn, &QPushButton::clicked, this, &UserSettingsPage::exportSessionKeys); 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]() { connect(backBtn_, &QPushButton::clicked, this, [this]() {
settings_->save(); settings_->save();
emit moveBack(); emit moveBack();
@ -1137,3 +1188,30 @@ UserSettingsPage::exportSessionKeys()
QMessageBox::warning(this, tr("Error"), e.what()); QMessageBox::warning(this, tr("Error"), e.what());
} }
} }
void
UserSettingsPage::updateSecretStatus()
{
QString ok = "QLabel { color : #00cc66; }";
QString notSoOk = "QLabel { color : #ff9933; }";
auto updateLabel = [&ok, &notSoOk](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);
}

View file

@ -253,6 +253,9 @@ signals:
void themeChanged(); void themeChanged();
void decryptSidebarChanged(); void decryptSidebarChanged();
public slots:
void updateSecretStatus();
private slots: private slots:
void importSessionKeys(); void importSessionKeys();
void exportSessionKeys(); void exportSessionKeys();
@ -285,6 +288,10 @@ private:
Toggle *mobileMode_; Toggle *mobileMode_;
QLabel *deviceFingerprintValue_; QLabel *deviceFingerprintValue_;
QLabel *deviceIdValue_; QLabel *deviceIdValue_;
QLabel *backupSecretCached;
QLabel *masterSecretCached;
QLabel *selfSigningSecretCached;
QLabel *userSigningSecretCached;
QComboBox *themeCombo_; QComboBox *themeCombo_;
QComboBox *scaleFactorCombo_; QComboBox *scaleFactorCombo_;

View file

@ -54,8 +54,9 @@ bool
utils::codepointIsEmoji(uint code) utils::codepointIsEmoji(uint code)
{ {
// TODO: Be more precise here. // TODO: Be more precise here.
return (code >= 0x2600 && code <= 0x27bf) || (code >= 0x1f300 && code <= 0x1f3ff) || return (code >= 0x2600 && code <= 0x27bf) || (code >= 0x2b00 && code <= 0x2bff) ||
(code >= 0x1f000 && code <= 0x1faff); (code >= 0x1f300 && code <= 0x1f3ff) || (code >= 0x1f000 && code <= 0x1faff) ||
code == 0x200d;
} }
QString QString

View file

@ -51,7 +51,12 @@ public:
void sync(const mtx::responses::Rooms &rooms); void sync(const mtx::responses::Rooms &rooms);
void addRoom(const QString &room_id); 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 TimelineModel *activeTimeline() const { return timeline_; }
Q_INVOKABLE bool isInitialSync() const { return isInitialSync_; } Q_INVOKABLE bool isInitialSync() const { return isInitialSync_; }