mirror of
https://github.com/Nheko-Reborn/nheko.git
synced 2024-11-25 20:48:52 +03:00
Store secrets in keychain
This commit is contained in:
parent
7f1f747203
commit
7b46aa2a6e
11 changed files with 320 additions and 12 deletions
|
@ -136,6 +136,7 @@ endif()
|
|||
find_package(Qt5 COMPONENTS Core Widgets LinguistTools Concurrent Svg Multimedia Qml QuickControls2 QuickWidgets REQUIRED)
|
||||
find_package(Qt5QuickCompiler)
|
||||
find_package(Qt5DBus)
|
||||
find_package(Qt5Keychain REQUIRED)
|
||||
|
||||
if (APPLE)
|
||||
find_package(Qt5MacExtras REQUIRED)
|
||||
|
@ -587,6 +588,7 @@ target_link_libraries(nheko PRIVATE
|
|||
Qt5::Qml
|
||||
Qt5::QuickControls2
|
||||
Qt5::QuickWidgets
|
||||
qt5keychain
|
||||
nlohmann_json::nlohmann_json
|
||||
lmdbxx::lmdbxx
|
||||
liblmdb::lmdb
|
||||
|
|
|
@ -24,9 +24,10 @@
|
|||
#include <QFile>
|
||||
#include <QHash>
|
||||
#include <QMap>
|
||||
#include <QSettings>
|
||||
#include <QStandardPaths>
|
||||
|
||||
#include <qt5keychain/keychain.h>
|
||||
|
||||
#include <mtx/responses/common.hpp>
|
||||
|
||||
#include "Cache.h"
|
||||
|
@ -569,6 +570,64 @@ Cache::restoreOlmAccount()
|
|||
return std::string(pickled.data(), pickled.size());
|
||||
}
|
||||
|
||||
void
|
||||
Cache::storeSecret(const std::string &name, const std::string &secret)
|
||||
{
|
||||
QKeychain::WritePasswordJob job(QCoreApplication::applicationName());
|
||||
job.setAutoDelete(false);
|
||||
job.setInsecureFallback(true);
|
||||
job.setKey(QString::fromStdString(name));
|
||||
job.setTextData(QString::fromStdString(secret));
|
||||
QEventLoop loop;
|
||||
job.connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
|
||||
job.start();
|
||||
loop.exec();
|
||||
|
||||
if (job.error()) {
|
||||
nhlog::db()->warn(
|
||||
"Storing secret '{}' failed: {}", name, job.errorString().toStdString());
|
||||
} else {
|
||||
emit secretChanged(name);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
Cache::deleteSecret(const std::string &name)
|
||||
{
|
||||
QKeychain::DeletePasswordJob job(QCoreApplication::applicationName());
|
||||
job.setAutoDelete(false);
|
||||
job.setInsecureFallback(true);
|
||||
job.setKey(QString::fromStdString(name));
|
||||
QEventLoop loop;
|
||||
job.connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
|
||||
job.start();
|
||||
loop.exec();
|
||||
|
||||
emit secretChanged(name);
|
||||
}
|
||||
|
||||
std::optional<std::string>
|
||||
Cache::secret(const std::string &name)
|
||||
{
|
||||
QKeychain::ReadPasswordJob job(QCoreApplication::applicationName());
|
||||
job.setAutoDelete(false);
|
||||
job.setInsecureFallback(true);
|
||||
job.setKey(QString::fromStdString(name));
|
||||
QEventLoop loop;
|
||||
job.connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
|
||||
job.start();
|
||||
loop.exec();
|
||||
|
||||
const QString secret = job.textData();
|
||||
if (job.error()) {
|
||||
nhlog::db()->debug(
|
||||
"Restoring secret '{}' failed: {}", name, job.errorString().toStdString());
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return secret.toStdString();
|
||||
}
|
||||
|
||||
//
|
||||
// Media Management
|
||||
//
|
||||
|
@ -726,10 +785,32 @@ void
|
|||
Cache::deleteData()
|
||||
{
|
||||
// TODO: We need to remove the env_ while not accepting new requests.
|
||||
lmdb::dbi_close(env_, syncStateDb_);
|
||||
lmdb::dbi_close(env_, roomsDb_);
|
||||
lmdb::dbi_close(env_, invitesDb_);
|
||||
lmdb::dbi_close(env_, mediaDb_);
|
||||
lmdb::dbi_close(env_, readReceiptsDb_);
|
||||
lmdb::dbi_close(env_, notificationsDb_);
|
||||
|
||||
lmdb::dbi_close(env_, devicesDb_);
|
||||
lmdb::dbi_close(env_, deviceKeysDb_);
|
||||
|
||||
lmdb::dbi_close(env_, inboundMegolmSessionDb_);
|
||||
lmdb::dbi_close(env_, outboundMegolmSessionDb_);
|
||||
|
||||
env_.close();
|
||||
|
||||
verification_storage.status.clear();
|
||||
|
||||
if (!cacheDirectory_.isEmpty()) {
|
||||
QDir(cacheDirectory_).removeRecursively();
|
||||
nhlog::db()->info("deleted cache files from disk");
|
||||
}
|
||||
|
||||
deleteSecret(mtx::secret_storage::secrets::megolm_backup_v1);
|
||||
deleteSecret(mtx::secret_storage::secrets::cross_signing_master);
|
||||
deleteSecret(mtx::secret_storage::secrets::cross_signing_user_signing);
|
||||
deleteSecret(mtx::secret_storage::secrets::cross_signing_self_signing);
|
||||
}
|
||||
|
||||
//! migrates db to the current format
|
||||
|
@ -4262,4 +4343,15 @@ restoreOlmAccount()
|
|||
{
|
||||
return instance_->restoreOlmAccount();
|
||||
}
|
||||
|
||||
void
|
||||
storeSecret(const std::string &name, const std::string &secret)
|
||||
{
|
||||
instance_->storeSecret(name, secret);
|
||||
}
|
||||
std::optional<std::string>
|
||||
secret(const std::string &name)
|
||||
{
|
||||
return instance_->secret(name);
|
||||
}
|
||||
} // namespace cache
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -372,9 +372,8 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
|
|||
void
|
||||
ChatPage::logout()
|
||||
{
|
||||
deleteConfigs();
|
||||
|
||||
resetUI();
|
||||
deleteConfigs();
|
||||
|
||||
emit closing();
|
||||
connectivityTimer_.stop();
|
||||
|
@ -385,12 +384,12 @@ ChatPage::dropToLoginPage(const QString &msg)
|
|||
{
|
||||
nhlog::ui()->info("dropping to the login page: {}", msg.toStdString());
|
||||
|
||||
deleteConfigs();
|
||||
resetUI();
|
||||
|
||||
http::client()->shutdown();
|
||||
connectivityTimer_.stop();
|
||||
|
||||
resetUI();
|
||||
deleteConfigs();
|
||||
|
||||
emit showLoginPage(msg);
|
||||
}
|
||||
|
||||
|
@ -418,8 +417,8 @@ ChatPage::deleteConfigs()
|
|||
settings.remove("");
|
||||
settings.endGroup();
|
||||
|
||||
http::client()->shutdown();
|
||||
cache::deleteData();
|
||||
http::client()->clear();
|
||||
}
|
||||
|
||||
void
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
133
src/Olm.cpp
133
src/Olm.cpp
|
@ -18,13 +18,13 @@
|
|||
#include "UserSettingsPage.h"
|
||||
#include "Utils.h"
|
||||
|
||||
static const std::string STORAGE_SECRET_KEY("secret");
|
||||
constexpr auto MEGOLM_ALGO = "m.megolm.v1.aes-sha2";
|
||||
|
||||
namespace {
|
||||
auto client_ = std::make_unique<mtx::crypto::OlmClient>();
|
||||
|
||||
std::map<std::string, std::string> request_id_to_secret_name;
|
||||
|
||||
const std::string STORAGE_SECRET_KEY("secret");
|
||||
constexpr auto MEGOLM_ALGO = "m.megolm.v1.aes-sha2";
|
||||
}
|
||||
|
||||
namespace olm {
|
||||
|
@ -221,6 +221,133 @@ handle_olm_message(const OlmMessage &msg)
|
|||
} else if (auto roomKey = std::get_if<DeviceEvent<msg::ForwardedRoomKey>>(
|
||||
&device_event)) {
|
||||
import_inbound_megolm_session(*roomKey);
|
||||
} else if (auto e =
|
||||
std::get_if<DeviceEvent<msg::SecretSend>>(&device_event)) {
|
||||
auto local_user = http::client()->user_id();
|
||||
|
||||
if (msg.sender != local_user.to_string())
|
||||
continue;
|
||||
|
||||
auto secret_name =
|
||||
request_id_to_secret_name.find(e->content.request_id);
|
||||
|
||||
if (secret_name != request_id_to_secret_name.end()) {
|
||||
nhlog::crypto()->info("Received secret: {}",
|
||||
secret_name->second);
|
||||
|
||||
mtx::events::msg::SecretRequest secretRequest{};
|
||||
secretRequest.action =
|
||||
mtx::events::msg::RequestAction::Cancellation;
|
||||
secretRequest.requesting_device_id =
|
||||
http::client()->device_id();
|
||||
secretRequest.request_id = e->content.request_id;
|
||||
|
||||
auto verificationStatus =
|
||||
cache::verificationStatus(local_user.to_string());
|
||||
|
||||
if (!verificationStatus)
|
||||
continue;
|
||||
|
||||
auto deviceKeys = cache::userKeys(local_user.to_string());
|
||||
std::string sender_device_id;
|
||||
if (deviceKeys) {
|
||||
for (auto &[dev, key] : deviceKeys->device_keys) {
|
||||
if (key.keys["curve25519:" + dev] ==
|
||||
msg.sender_key) {
|
||||
sender_device_id = dev;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::map<
|
||||
mtx::identifiers::User,
|
||||
std::map<std::string, mtx::events::msg::SecretRequest>>
|
||||
body;
|
||||
|
||||
for (const auto &dev :
|
||||
verificationStatus->verified_devices) {
|
||||
if (dev != secretRequest.requesting_device_id &&
|
||||
dev != sender_device_id)
|
||||
body[local_user][dev] = secretRequest;
|
||||
}
|
||||
|
||||
http::client()
|
||||
->send_to_device<mtx::events::msg::SecretRequest>(
|
||||
http::client()->generate_txn_id(),
|
||||
body,
|
||||
[name =
|
||||
secret_name->second](mtx::http::RequestErr err) {
|
||||
if (err) {
|
||||
nhlog::net()->error(
|
||||
"Failed to send request cancellation "
|
||||
"for secrect "
|
||||
"'{}'",
|
||||
name);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
cache::client()->storeSecret(secret_name->second,
|
||||
e->content.secret);
|
||||
|
||||
request_id_to_secret_name.erase(secret_name);
|
||||
}
|
||||
|
||||
} else if (auto e =
|
||||
std::get_if<DeviceEvent<msg::SecretRequest>>(&device_event)) {
|
||||
if (e->content.action != mtx::events::msg::RequestAction::Request)
|
||||
continue;
|
||||
|
||||
auto local_user = http::client()->user_id();
|
||||
|
||||
if (msg.sender != local_user.to_string())
|
||||
continue;
|
||||
|
||||
auto verificationStatus =
|
||||
cache::verificationStatus(local_user.to_string());
|
||||
|
||||
if (!verificationStatus)
|
||||
continue;
|
||||
|
||||
auto deviceKeys = cache::userKeys(local_user.to_string());
|
||||
if (!deviceKeys)
|
||||
continue;
|
||||
|
||||
for (auto &[dev, key] : deviceKeys->device_keys) {
|
||||
if (key.keys["curve25519:" + dev] == msg.sender_key) {
|
||||
if (std::find(
|
||||
verificationStatus->verified_devices.begin(),
|
||||
verificationStatus->verified_devices.end(),
|
||||
dev) ==
|
||||
verificationStatus->verified_devices.end())
|
||||
break;
|
||||
|
||||
// this is a verified device
|
||||
mtx::events::DeviceEvent<
|
||||
mtx::events::msg::SecretSend>
|
||||
secretSend;
|
||||
secretSend.type = EventType::SecretSend;
|
||||
secretSend.content.request_id =
|
||||
e->content.request_id;
|
||||
|
||||
auto secret =
|
||||
cache::client()->secret(e->content.name);
|
||||
if (!secret)
|
||||
break;
|
||||
|
||||
secretSend.content.secret = secret.value();
|
||||
|
||||
send_encrypted_to_device_messages(
|
||||
{{local_user.to_string(), {{dev}}}}, secretSend);
|
||||
|
||||
nhlog::net()->info("Sent secret to ({},{})",
|
||||
local_user.to_string(),
|
||||
dev);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -637,6 +637,15 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge
|
|||
|
||||
deviceFingerprintValue_->setText(utils::humanReadableFingerprint(QString(44, 'X')));
|
||||
|
||||
backupSecretCached = new QLabel{this};
|
||||
masterSecretCached = new QLabel{this};
|
||||
selfSigningSecretCached = new QLabel{this};
|
||||
userSigningSecretCached = new QLabel{this};
|
||||
backupSecretCached->setFont(monospaceFont);
|
||||
masterSecretCached->setFont(monospaceFont);
|
||||
selfSigningSecretCached->setFont(monospaceFont);
|
||||
userSigningSecretCached->setFont(monospaceFont);
|
||||
|
||||
auto sessionKeysLabel = new QLabel{tr("Session Keys"), this};
|
||||
sessionKeysLabel->setFont(font);
|
||||
sessionKeysLabel->setMargin(OptionMargin);
|
||||
|
@ -801,6 +810,27 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge
|
|||
formLayout_->addRow(sessionKeysLabel, sessionKeysLayout);
|
||||
formLayout_->addRow(crossSigningKeysLabel, crossSigningKeysLayout);
|
||||
|
||||
boxWrap(tr("Master signing key"),
|
||||
masterSecretCached,
|
||||
tr("Your most important key. You don't need to have it cached, since not caching "
|
||||
"it makes it less likely it can be stolen and it is only needed to rotate your "
|
||||
"other signing keys."));
|
||||
boxWrap(tr("User signing key"),
|
||||
userSigningSecretCached,
|
||||
tr("The key to verify other users. If it is cached, verifying a user will verify "
|
||||
"all their devices."));
|
||||
boxWrap(
|
||||
tr("Self signing key"),
|
||||
selfSigningSecretCached,
|
||||
tr("The key to verify your own devices. If it is cached, verifying one of your devices "
|
||||
"will mark it verified for all your other devices and for users, that have verified "
|
||||
"you."));
|
||||
boxWrap(tr("Backup key"),
|
||||
backupSecretCached,
|
||||
tr("The key to decrypt online key backups. If it is cached, you can enable online "
|
||||
"key backup to store encryption keys securely encrypted on the server."));
|
||||
updateSecretStatus();
|
||||
|
||||
auto scrollArea_ = new QScrollArea{this};
|
||||
scrollArea_->setFrameShape(QFrame::NoFrame);
|
||||
scrollArea_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
|
@ -1154,3 +1184,30 @@ UserSettingsPage::exportSessionKeys()
|
|||
QMessageBox::warning(this, tr("Error"), e.what());
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
UserSettingsPage::updateSecretStatus()
|
||||
{
|
||||
QString ok = "QLabel { color : #00cc66; }";
|
||||
QString notSoOk = "QLabel { color : #ff9933; }";
|
||||
|
||||
auto updateLabel = [&, this](QLabel *label, const std::string &secretName) {
|
||||
if (cache::secret(secretName)) {
|
||||
label->setStyleSheet(ok);
|
||||
label->setText(tr("CACHED"));
|
||||
} else {
|
||||
if (secretName == mtx::secret_storage::secrets::cross_signing_master)
|
||||
label->setStyleSheet(ok);
|
||||
else
|
||||
label->setStyleSheet(notSoOk);
|
||||
label->setText(tr("NOT CACHED"));
|
||||
}
|
||||
};
|
||||
|
||||
updateLabel(masterSecretCached, mtx::secret_storage::secrets::cross_signing_master);
|
||||
updateLabel(userSigningSecretCached,
|
||||
mtx::secret_storage::secrets::cross_signing_user_signing);
|
||||
updateLabel(selfSigningSecretCached,
|
||||
mtx::secret_storage::secrets::cross_signing_self_signing);
|
||||
updateLabel(backupSecretCached, mtx::secret_storage::secrets::megolm_backup_v1);
|
||||
}
|
||||
|
|
|
@ -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_;
|
||||
|
|
|
@ -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_; }
|
||||
|
|
Loading…
Reference in a new issue