Move away from using an event loop to access secrets

Fixes messages in room flickering and being stuck

fixes #760
relates to #770
relates to #789
This commit is contained in:
Nicolas Werner 2021-11-07 03:38:48 +01:00
parent d112d6b11f
commit 5ca1fb18bb
No known key found for this signature in database
GPG key ID: C8D75E610773F2D9
11 changed files with 267 additions and 208 deletions

View file

@ -199,7 +199,6 @@ Cache::Cache(const QString &userId, QObject *parent)
, env_{nullptr}
, localUserId_{userId}
{
setup();
connect(this, &Cache::userKeysUpdate, this, &Cache::updateUserKeys, Qt::QueuedConnection);
connect(
this,
@ -212,6 +211,7 @@ Cache::Cache(const QString &userId, QObject *parent)
}
},
Qt::QueuedConnection);
setup();
}
void
@ -308,7 +308,178 @@ Cache::setup()
txn.commit();
databaseReady_ = true;
loadSecrets({
{mtx::secret_storage::secrets::cross_signing_master, false},
{mtx::secret_storage::secrets::cross_signing_self_signing, false},
{mtx::secret_storage::secrets::cross_signing_user_signing, false},
{mtx::secret_storage::secrets::megolm_backup_v1, false},
{"pickle_secret", true},
});
}
static void
fatalSecretError()
{
QMessageBox::critical(
ChatPage::instance(),
QCoreApplication::translate("SecretStorage", "Failed to connect to secret storage"),
QCoreApplication::translate(
"SecretStorage",
"Nheko could not connect to the secure storage to save encryption secrets to. This can "
"have multiple reasons. Check if your D-Bus service is running and you have configured a "
"service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If "
"you are having trouble, feel free to open an issue here: "
"https://github.com/Nheko-Reborn/nheko/issues"));
QCoreApplication::exit(1);
exit(1);
}
static QString
secretName(std::string name, bool internal)
{
auto settings = UserSettings::instance();
return (internal ? "nheko." : "matrix.") +
QString(
QCryptographicHash::hash(settings->profile().toUtf8(), QCryptographicHash::Sha256)
.toBase64()) +
"." + QString::fromStdString(name);
}
void
Cache::loadSecrets(std::vector<std::pair<std::string, bool>> toLoad)
{
if (toLoad.empty()) {
this->databaseReady_ = true;
emit databaseReady();
return;
}
auto [name_, internal] = toLoad.front();
auto job = new QKeychain::ReadPasswordJob(QCoreApplication::applicationName());
job->setAutoDelete(true);
job->setInsecureFallback(true);
job->setSettings(UserSettings::instance()->qsettings());
auto name = secretName(name_, internal);
job->setKey(name);
connect(job,
&QKeychain::ReadPasswordJob::finished,
this,
[this, name, toLoad, job](QKeychain::Job *) mutable {
const QString secret = job->textData();
if (job->error() && job->error() != QKeychain::Error::EntryNotFound) {
nhlog::db()->error("Restoring secret '{}' failed ({}): {}",
name.toStdString(),
job->error(),
job->errorString().toStdString());
fatalSecretError();
}
if (secret.isEmpty()) {
nhlog::db()->debug("Restored empty secret '{}'.", name.toStdString());
} else {
std::unique_lock lock(secret_storage.mtx);
secret_storage.secrets[name.toStdString()] = secret.toStdString();
}
// load next secret
toLoad.erase(toLoad.begin());
// You can't start a job from the finish signal of a job.
QTimer::singleShot(0, [this, toLoad] { loadSecrets(toLoad); });
});
job->start();
}
std::optional<std::string>
Cache::secret(const std::string name_, bool internal)
{
auto name = secretName(name_, internal);
std::unique_lock lock(secret_storage.mtx);
if (auto secret = secret_storage.secrets.find(name.toStdString());
secret != secret_storage.secrets.end())
return secret->second;
else
return std::nullopt;
}
void
Cache::storeSecret(const std::string name_, const std::string secret, bool internal)
{
auto name = secretName(name_, internal);
{
std::unique_lock lock(secret_storage.mtx);
secret_storage.secrets[name.toStdString()] = secret;
}
auto settings = UserSettings::instance();
auto job = new QKeychain::WritePasswordJob(QCoreApplication::applicationName());
job->setAutoDelete(true);
job->setInsecureFallback(true);
job->setSettings(UserSettings::instance()->qsettings());
job->setKey(name);
job->setTextData(QString::fromStdString(secret));
QObject::connect(
job,
&QKeychain::WritePasswordJob::finished,
this,
[name_, this](QKeychain::Job *job) {
if (job->error()) {
nhlog::db()->warn(
"Storing secret '{}' failed: {}", name_, job->errorString().toStdString());
fatalSecretError();
} else {
// if we emit the signal directly, qtkeychain breaks and won't execute new
// jobs. You can't start a job from the finish signal of a job.
QTimer::singleShot(0, [this, name_] { emit secretChanged(name_); });
nhlog::db()->info("Storing secret '{}' successful", name_);
}
},
Qt::ConnectionType::DirectConnection);
job->start();
}
void
Cache::deleteSecret(const std::string name, bool internal)
{
auto name_ = secretName(name, internal);
{
std::unique_lock lock(secret_storage.mtx);
secret_storage.secrets.erase(name_.toStdString());
}
auto settings = UserSettings::instance();
auto job = new QKeychain::DeletePasswordJob(QCoreApplication::applicationName());
job->setAutoDelete(true);
job->setInsecureFallback(true);
job->setSettings(UserSettings::instance()->qsettings());
job->setKey(name_);
job->connect(
job, &QKeychain::Job::finished, this, [this, name]() { emit secretChanged(name); });
job->start();
}
std::string
Cache::pickleSecret()
{
if (pickle_secret_.empty()) {
auto s = secret("pickle_secret", true);
if (!s) {
this->pickle_secret_ = mtx::client::utils::random_token(64, true);
storeSecret("pickle_secret", pickle_secret_, true);
} else {
this->pickle_secret_ = *s;
}
}
return pickle_secret_;
}
void
@ -758,144 +929,6 @@ Cache::backupVersion()
}
}
static void
fatalSecretError()
{
QMessageBox::critical(
ChatPage::instance(),
QCoreApplication::translate("SecretStorage", "Failed to connect to secret storage"),
QCoreApplication::translate(
"SecretStorage",
"Nheko could not connect to the secure storage to save encryption secrets to. This can "
"have multiple reasons. Check if your D-Bus service is running and you have configured a "
"service like KWallet, Gnome Keyring, KeePassXC or the equivalent for your platform. If "
"you are having trouble, feel free to open an issue here: "
"https://github.com/Nheko-Reborn/nheko/issues"));
QCoreApplication::exit(1);
exit(1);
}
void
Cache::storeSecret(const std::string name, const std::string secret, bool internal)
{
auto settings = UserSettings::instance();
auto job = new QKeychain::WritePasswordJob(QCoreApplication::applicationName());
job->setAutoDelete(true);
job->setInsecureFallback(true);
job->setSettings(UserSettings::instance()->qsettings());
job->setKey(
(internal ? "nheko." : "matrix.") +
QString(QCryptographicHash::hash(settings->profile().toUtf8(), QCryptographicHash::Sha256)
.toBase64()) +
"." + QString::fromStdString(name));
job->setTextData(QString::fromStdString(secret));
QObject::connect(
job,
&QKeychain::WritePasswordJob::finished,
this,
[name, this](QKeychain::Job *job) {
if (job->error()) {
nhlog::db()->warn(
"Storing secret '{}' failed: {}", name, job->errorString().toStdString());
fatalSecretError();
} else {
// if we emit the signal directly, qtkeychain breaks and won't execute new
// jobs. You can't start a job from the finish signal of a job.
QTimer::singleShot(100, [this, name] { emit secretChanged(name); });
nhlog::db()->info("Storing secret '{}' successful", name);
}
},
Qt::ConnectionType::DirectConnection);
job->start();
}
void
Cache::deleteSecret(const std::string name, bool internal)
{
auto settings = UserSettings::instance();
QKeychain::DeletePasswordJob job(QCoreApplication::applicationName());
job.setAutoDelete(false);
job.setInsecureFallback(true);
job.setSettings(UserSettings::instance()->qsettings());
job.setKey(
(internal ? "nheko." : "matrix.") +
QString(QCryptographicHash::hash(settings->profile().toUtf8(), QCryptographicHash::Sha256)
.toBase64()) +
"." + QString::fromStdString(name));
// FIXME(Nico): Nested event loops are dangerous. Some other slots may resume in the mean
// time!
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, bool internal)
{
auto settings = UserSettings::instance();
QKeychain::ReadPasswordJob job(QCoreApplication::applicationName());
job.setAutoDelete(false);
job.setInsecureFallback(true);
job.setSettings(UserSettings::instance()->qsettings());
job.setKey(
(internal ? "nheko." : "matrix.") +
QString(QCryptographicHash::hash(settings->profile().toUtf8(), QCryptographicHash::Sha256)
.toBase64()) +
"." + QString::fromStdString(name));
// FIXME(Nico): Nested event loops are dangerous. Some other slots may resume in the mean
// time!
QEventLoop loop;
job.connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
job.start();
loop.exec();
const QString secret = job.textData();
if (job.error()) {
if (job.error() == QKeychain::Error::EntryNotFound)
return std::nullopt;
nhlog::db()->error("Restoring secret '{}' failed ({}): {}",
name,
job.error(),
job.errorString().toStdString());
fatalSecretError();
return std::nullopt;
}
if (secret.isEmpty()) {
nhlog::db()->debug("Restored empty secret '{}'.", name);
return std::nullopt;
}
return secret.toStdString();
}
std::string
Cache::pickleSecret()
{
if (pickle_secret_.empty()) {
auto s = secret("pickle_secret", true);
if (!s) {
this->pickle_secret_ = mtx::client::utils::random_token(64, true);
storeSecret("pickle_secret", pickle_secret_, true);
} else {
this->pickle_secret_ = *s;
}
}
return pickle_secret_;
}
void
Cache::removeInvite(lmdb::txn &txn, const std::string &room_id)
{