You need to sign in or sign up before continuing.
Newer
Older
// SPDX-FileCopyrightText: 2017 Konstantinos Sideris <siderisk@auth.gr>
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#include <limits>
#include <QCryptographicHash>
#if __has_include(<keychain.h>)
#include <keychain.h>
#else
#include <mtx/responses/common.hpp>
#include "UserSettingsPage.h"
//! Should be changed when a breaking change occurs in the cache format.
//! This will reset client's data.
static const std::string CURRENT_CACHE_FORMAT_VERSION("2021.08.31");
static const std::string_view NEXT_BATCH_KEY("next_batch");
static const std::string_view OLM_ACCOUNT_KEY("olm_account");
static const std::string_view CACHE_FORMAT_VERSION_KEY("cache_format_version");
static const std::string_view CURRENT_ONLINE_BACKUP_VERSION("current_online_backup_version");
constexpr size_t MAX_RESTORED_MESSAGES = 30'000;
constexpr auto DB_SIZE = 32ULL * 1024ULL * 1024ULL * 1024ULL; // 32 GB
//! Cache databases and their format.
//!
//! Contains UI information for the joined rooms. (i.e name, topic, avatar url etc).
//! Format: room_id -> RoomInfo
constexpr auto ROOMS_DB("rooms");
constexpr auto INVITES_DB("invites");
//! maps each room to its parent space (id->id)
constexpr auto SPACES_PARENTS_DB("space_parents");
//! maps each space to its current children (id->id)
constexpr auto SPACES_CHILDREN_DB("space_children");
//! Information that must be kept between sync requests.
constexpr auto SYNC_STATE_DB("sync_state");
constexpr auto READ_RECEIPTS_DB("read_receipts");
constexpr auto NOTIFICATIONS_DB("sent_notifications");
//! Encryption related databases.
//! user_id -> list of devices
constexpr auto DEVICES_DB("devices");
//! device_id -> device keys
constexpr auto DEVICE_KEYS_DB("device_keys");
//! room_ids that have encryption enabled.
constexpr auto ENCRYPTED_ROOMS_DB("encrypted_rooms");
//! room_id -> pickled OlmInboundGroupSession
constexpr auto INBOUND_MEGOLM_SESSIONS_DB("inbound_megolm_sessions");
//! MegolmSessionIndex -> pickled OlmOutboundGroupSession
constexpr auto OUTBOUND_MEGOLM_SESSIONS_DB("outbound_megolm_sessions");
//! MegolmSessionIndex -> session data about which devices have access to this
constexpr auto MEGOLM_SESSIONS_DATA_DB("megolm_sessions_data_db");
using CachedReceipts = std::multimap<uint64_t, std::string, std::greater<uint64_t>>;
using Receipts = std::map<std::string, std::map<std::string, uint64_t>>;
Q_DECLARE_METATYPE(RoomMember)
Q_DECLARE_METATYPE(mtx::responses::Timeline)
Q_DECLARE_METATYPE(RoomSearchResult)
Q_DECLARE_METATYPE(RoomInfo)
Q_DECLARE_METATYPE(mtx::responses::QueryKeys)
std::unique_ptr<Cache> instance_ = nullptr;
struct RO_txn
{
~RO_txn() { txn.reset(); }
operator MDB_txn *() const noexcept { return txn.handle(); }
operator lmdb::txn &() noexcept { return txn; }
lmdb::txn &txn;
};
RO_txn
ro_txn(lmdb::env &env)
{
thread_local lmdb::txn txn = lmdb::txn::begin(env, nullptr, MDB_RDONLY);
thread_local int reuse_counter = 0;
if (reuse_counter >= 100 || txn.env() != env.handle()) {
txn.abort();
txn = lmdb::txn::begin(env, nullptr, MDB_RDONLY);
reuse_counter = 0;
} else if (reuse_counter > 0) {
try {
txn.renew();
} catch (...) {
txn.abort();
txn = lmdb::txn::begin(env, nullptr, MDB_RDONLY);
reuse_counter = 0;
}
}
reuse_counter++;
return RO_txn{txn};
}
template<class T>
bool
containsStateUpdates(const T &e)
{
return std::visit([](const auto &ev) { return Cache::isStateEvent_<decltype(ev)>; }, e);
}
bool
containsStateUpdates(const mtx::events::collections::StrippedEvents &e)
{
using namespace mtx::events;
using namespace mtx::events::state;
return std::holds_alternative<StrippedEvent<state::Avatar>>(e) ||
std::holds_alternative<StrippedEvent<CanonicalAlias>>(e) ||
std::holds_alternative<StrippedEvent<Name>>(e) ||
std::holds_alternative<StrippedEvent<Member>>(e) ||
std::holds_alternative<StrippedEvent<Topic>>(e);
}
bool
Cache::isHiddenEvent(lmdb::txn &txn,
mtx::events::collections::TimelineEvents e,
const std::string &room_id)
{
using namespace mtx::events;
// Always hide edits
if (mtx::accessors::relations(e).replaces())
return true;
if (auto encryptedEvent = std::get_if<EncryptedEvent<msg::Encrypted>>(&e)) {
MegolmSessionIndex index;
index.room_id = room_id;
index.session_id = encryptedEvent->content.session_id;
index.sender_key = encryptedEvent->content.sender_key;
auto result = olm::decryptEvent(index, *encryptedEvent, true);
if (!result.error)
e = result.event.value();
}
mtx::events::account_data::nheko_extensions::HiddenEvents hiddenEvents;
hiddenEvents.hidden_event_types = {
EventType::Reaction, EventType::CallCandidates, EventType::Unsupported};
if (auto temp = getAccountData(txn, mtx::events::EventType::NhekoHiddenEvents, ""))
hiddenEvents =
std::move(std::get<mtx::events::AccountDataEvent<
mtx::events::account_data::nheko_extensions::HiddenEvents>>(*temp)
.content);
if (auto temp = getAccountData(txn, mtx::events::EventType::NhekoHiddenEvents, room_id))
hiddenEvents =
std::move(std::get<mtx::events::AccountDataEvent<
mtx::events::account_data::nheko_extensions::HiddenEvents>>(*temp)
.content);
[hiddenEvents](const auto &ev) {
return std::any_of(hiddenEvents.hidden_event_types.begin(),
hiddenEvents.hidden_event_types.end(),
[ev](EventType type) { return type == ev.type; });
},
e);
}
Cache::Cache(const QString &userId, QObject *parent)
: QObject{parent}
, env_{nullptr}
{
setup();
connect(this, &Cache::userKeysUpdate, this, &Cache::updateUserKeys, Qt::QueuedConnection);
void
Cache::setup()
{
nhlog::db()->debug("setting up cache");
// Previous location of the cache directory
auto oldCache = QString("%1/%2%3")
.arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation))
.arg(QString::fromUtf8(localUserId_.toUtf8().toHex()))
.arg(QString::fromUtf8(settings->profile().toUtf8().toHex()));
cacheDirectory_ = QString("%1/%2%3")
.arg(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation))
.arg(QString::fromUtf8(settings->profile().toUtf8().toHex()));
bool isInitial = !QFile::exists(cacheDirectory_);
// NOTE: If both cache directories exist it's better to do nothing: it
// could mean a previous migration failed or was interrupted.
bool needsMigration = isInitial && QFile::exists(oldCache);
if (needsMigration) {
nhlog::db()->info("found old state directory, migrating");
if (!QDir().rename(oldCache, cacheDirectory_)) {
throw std::runtime_error(("Unable to migrate the old state directory (" +
oldCache + ") to the new location (" +
cacheDirectory_ + ")")
.toStdString()
.c_str());
}
nhlog::db()->info("completed state migration");
}
env_ = lmdb::env::create();
env_.set_mapsize(DB_SIZE);
env_.set_max_dbs(MAX_DBS);
if (isInitial) {
Konstantinos Sideris
committed
nhlog::db()->info("initializing LMDB");
if (!QDir().mkpath(cacheDirectory_)) {
throw std::runtime_error(
("Unable to create state directory:" + cacheDirectory_)
.toStdString()
.c_str());
}
}
try {
// NOTE(Nico): We may want to use (MDB_MAPASYNC | MDB_WRITEMAP) in the future, but
// it can really mess up our database, so we shouldn't. For now, hopefully
// NOMETASYNC is fast enough.
env_.open(cacheDirectory_.toStdString().c_str(), MDB_NOMETASYNC | MDB_NOSYNC);
} catch (const lmdb::error &e) {
if (e.code() != MDB_VERSION_MISMATCH && e.code() != MDB_INVALID) {
throw std::runtime_error("LMDB initialization failed" +
std::string(e.what()));
}
Konstantinos Sideris
committed
nhlog::db()->warn("resetting cache due to LMDB version mismatch: {}", e.what());
QDir stateDir(cacheDirectory_);
for (const auto &file : stateDir.entryList(QDir::NoDotAndDotDot)) {
if (!stateDir.remove(file))
throw std::runtime_error(
("Unable to delete file " + file).toStdString().c_str());
}
env_.open(cacheDirectory_.toStdString().c_str());
}
auto txn = lmdb::txn::begin(env_);
syncStateDb_ = lmdb::dbi::open(txn, SYNC_STATE_DB, MDB_CREATE);
roomsDb_ = lmdb::dbi::open(txn, ROOMS_DB, MDB_CREATE);
spacesChildrenDb_ = lmdb::dbi::open(txn, SPACES_CHILDREN_DB, MDB_CREATE | MDB_DUPSORT);
spacesParentsDb_ = lmdb::dbi::open(txn, SPACES_PARENTS_DB, MDB_CREATE | MDB_DUPSORT);
invitesDb_ = lmdb::dbi::open(txn, INVITES_DB, MDB_CREATE);
readReceiptsDb_ = lmdb::dbi::open(txn, READ_RECEIPTS_DB, MDB_CREATE);
notificationsDb_ = lmdb::dbi::open(txn, NOTIFICATIONS_DB, MDB_CREATE);
// Device management
devicesDb_ = lmdb::dbi::open(txn, DEVICES_DB, MDB_CREATE);
deviceKeysDb_ = lmdb::dbi::open(txn, DEVICE_KEYS_DB, MDB_CREATE);
// Session management
inboundMegolmSessionDb_ = lmdb::dbi::open(txn, INBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE);
outboundMegolmSessionDb_ = lmdb::dbi::open(txn, OUTBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE);
megolmSessionDataDb_ = lmdb::dbi::open(txn, MEGOLM_SESSIONS_DATA_DB, MDB_CREATE);
// What rooms are encrypted
encryptedRooms_ = lmdb::dbi::open(txn, ENCRYPTED_ROOMS_DB, MDB_CREATE);
[[maybe_unused]] auto verificationDb = getVerificationDb(txn);
[[maybe_unused]] auto userKeysDb = getUserKeysDb(txn);
Cache::setEncryptedRoom(lmdb::txn &txn, const std::string &room_id)
Konstantinos Sideris
committed
nhlog::db()->info("mark room {} as encrypted", room_id);
encryptedRooms_.put(txn, room_id, "0");
}
bool
Cache::isRoomEncrypted(const std::string &room_id)
{
auto txn = ro_txn(env_);
auto res = encryptedRooms_.get(txn, room_id, unused);
return res;
}
std::optional<mtx::events::state::Encryption>
Cache::roomEncryptionSettings(const std::string &room_id)
{
using namespace mtx::events;
using namespace mtx::events::state;
try {
auto txn = ro_txn(env_);
auto statesdb = getStatesDb(txn, room_id);
std::string_view event;
bool res =
statesdb.get(txn, to_string(mtx::events::EventType::RoomEncryption), event);
if (res) {
try {
StateEvent<Encryption> msg = json::parse(event);
return msg.content;
} catch (const json::exception &e) {
nhlog::db()->warn("failed to parse m.room.encryption event: {}",
e.what());
return Encryption{};
}
}
} catch (lmdb::error &) {
}
return std::nullopt;
}
mtx::crypto::ExportedSessionKeys
Cache::exportSessionKeys()
{
using namespace mtx::crypto;
ExportedSessionKeys keys;
auto txn = ro_txn(env_);
auto cursor = lmdb::cursor::open(txn, inboundMegolmSessionDb_);
while (cursor.get(key, value, MDB_NEXT)) {
ExportedSession exported;
auto saved_session =
unpickle<InboundSessionObject>(std::string(value), pickle_secret_);
try {
index = nlohmann::json::parse(key).get<MegolmSessionIndex>();
} catch (const nlohmann::json::exception &e) {
nhlog::db()->critical("failed to export megolm session: {}", e.what());
continue;
}
exported.room_id = index.room_id;
exported.sender_key = index.sender_key;
exported.session_id = index.session_id;
Nicolas Werner
committed
exported.session_key = export_session(saved_session.get(), -1);
keys.sessions.push_back(exported);
}
cursor.close();
return keys;
}
void
Cache::importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys)
{
for (const auto &s : keys.sessions) {
MegolmSessionIndex index;
index.room_id = s.room_id;
index.session_id = s.session_id;
index.sender_key = s.sender_key;
GroupSessionData data{};
data.forwarding_curve25519_key_chain = s.forwarding_curve25519_key_chain;
if (s.sender_claimed_keys.count("ed25519"))
data.sender_claimed_ed25519_key = s.sender_claimed_keys.at("ed25519");
auto exported_session = mtx::crypto::import_session(s.session_key);
saveInboundMegolmSession(index, std::move(exported_session), data);
ChatPage::instance()->receivedSessionKey(index.room_id, index.session_id);
//
// Session Management
//
void
Cache::saveInboundMegolmSession(const MegolmSessionIndex &index,
mtx::crypto::InboundGroupSessionPtr session,
const GroupSessionData &data)
{
using namespace mtx::crypto;
const auto key = json(index).dump();
const auto pickled = pickle<InboundSessionObject>(session.get(), pickle_secret_);
auto txn = lmdb::txn::begin(env_);
Nicolas Werner
committed
std::string_view value;
if (inboundMegolmSessionDb_.get(txn, key, value)) {
auto oldSession =
unpickle<InboundSessionObject>(std::string(value), pickle_secret_);
Nicolas Werner
committed
if (olm_inbound_group_session_first_known_index(session.get()) >
olm_inbound_group_session_first_known_index(oldSession.get())) {
nhlog::crypto()->warn(
"Not storing inbound session with newer first known index");
return;
}
}
inboundMegolmSessionDb_.put(txn, key, pickled);
megolmSessionDataDb_.put(txn, key, json(data).dump());
txn.commit();
}
Cache::getInboundMegolmSession(const MegolmSessionIndex &index)
{
using namespace mtx::crypto;
try {
auto txn = ro_txn(env_);
if (inboundMegolmSessionDb_.get(txn, key, value)) {
auto session =
unpickle<InboundSessionObject>(std::string(value), pickle_secret_);
return session;
}
} catch (std::exception &e) {
nhlog::db()->error("Failed to get inbound megolm session {}", e.what());
}
return nullptr;
Cache::inboundMegolmSessionExists(const MegolmSessionIndex &index)
using namespace mtx::crypto;
try {
auto txn = ro_txn(env_);
return inboundMegolmSessionDb_.get(txn, key, value);
} catch (std::exception &e) {
nhlog::db()->error("Failed to get inbound megolm session {}", e.what());
}
return false;
Cache::updateOutboundMegolmSession(const std::string &room_id,
const GroupSessionData &data_,
mtx::crypto::OutboundGroupSessionPtr &ptr)
{
using namespace mtx::crypto;
if (!outboundMegolmSessionExists(room_id))
return;
GroupSessionData data = data_;
data.message_index = olm_outbound_group_session_message_index(ptr.get());
MegolmSessionIndex index;
index.room_id = room_id;
index.sender_key = olm::client()->identity_keys().ed25519;
index.session_id = mtx::crypto::session_id(ptr.get());
// Save the updated pickled data for the session.
json j;
j["session"] = pickle<OutboundSessionObject>(ptr.get(), pickle_secret_);
auto txn = lmdb::txn::begin(env_);
outboundMegolmSessionDb_.put(txn, room_id, j.dump());
megolmSessionDataDb_.put(txn, json(index).dump(), json(data).dump());
txn.commit();
}
void
Cache::dropOutboundMegolmSession(const std::string &room_id)
{
using namespace mtx::crypto;
if (!outboundMegolmSessionExists(room_id))
return;
{
auto txn = lmdb::txn::begin(env_);
outboundMegolmSessionDb_.del(txn, room_id);
// don't delete session data, so that we can still share the session.
txn.commit();
}
}
Cache::saveOutboundMegolmSession(const std::string &room_id,
const GroupSessionData &data_,
mtx::crypto::OutboundGroupSessionPtr &session)
{
using namespace mtx::crypto;
const auto pickled = pickle<OutboundSessionObject>(session.get(), pickle_secret_);
GroupSessionData data = data_;
data.message_index = olm_outbound_group_session_message_index(session.get());
MegolmSessionIndex index;
index.room_id = room_id;
index.sender_key = olm::client()->identity_keys().ed25519;
index.session_id = mtx::crypto::session_id(session.get());
json j;
j["session"] = pickled;
auto txn = lmdb::txn::begin(env_);
outboundMegolmSessionDb_.put(txn, room_id, j.dump());
megolmSessionDataDb_.put(txn, json(index).dump(), json(data).dump());
txn.commit();
}
bool
Cache::outboundMegolmSessionExists(const std::string &room_id) noexcept
auto txn = ro_txn(env_);
std::string_view value;
return outboundMegolmSessionDb_.get(txn, room_id, value);
} catch (std::exception &e) {
nhlog::db()->error("Failed to retrieve outbound Megolm Session: {}", e.what());
return false;
}
}
OutboundGroupSessionDataRef
Cache::getOutboundMegolmSession(const std::string &room_id)
try {
using namespace mtx::crypto;
auto txn = ro_txn(env_);
std::string_view value;
outboundMegolmSessionDb_.get(txn, room_id, value);
auto obj = json::parse(value);
ref.session = unpickle<OutboundSessionObject>(obj.at("session"), pickle_secret_);
MegolmSessionIndex index;
index.room_id = room_id;
index.sender_key = olm::client()->identity_keys().ed25519;
index.session_id = mtx::crypto::session_id(ref.session.get());
if (megolmSessionDataDb_.get(txn, json(index).dump(), value)) {
ref.data = nlohmann::json::parse(value).get<GroupSessionData>();
}
return ref;
} catch (std::exception &e) {
nhlog::db()->error("Failed to retrieve outbound Megolm Session: {}", e.what());
return {};
}
std::optional<GroupSessionData>
Cache::getMegolmSessionData(const MegolmSessionIndex &index)
{
try {
using namespace mtx::crypto;
auto txn = ro_txn(env_);
std::string_view value;
if (megolmSessionDataDb_.get(txn, json(index).dump(), value)) {
return nlohmann::json::parse(value).get<GroupSessionData>();
}
return std::nullopt;
} catch (std::exception &e) {
nhlog::db()->error("Failed to retrieve Megolm Session Data: {}", e.what());
return std::nullopt;
}
}
//
// OLM sessions.
//
Cache::saveOlmSession(const std::string &curve25519,
mtx::crypto::OlmSessionPtr session,
uint64_t timestamp)
{
using namespace mtx::crypto;
auto txn = lmdb::txn::begin(env_);
auto db = getOlmSessionsDb(txn, curve25519);
const auto pickled = pickle<SessionObject>(session.get(), pickle_secret_);
const auto session_id = mtx::crypto::session_id(session.get());
StoredOlmSession stored_session;
stored_session.pickled_session = pickled;
stored_session.last_message_ts = timestamp;
db.put(txn, session_id, json(stored_session).dump());
Cache::getOlmSession(const std::string &curve25519, const std::string &session_id)
using namespace mtx::crypto;
auto txn = lmdb::txn::begin(env_);
auto db = getOlmSessionsDb(txn, curve25519);
std::string_view pickled;
bool found = db.get(txn, session_id, pickled);
txn.commit();
if (found) {
auto data = json::parse(pickled).get<StoredOlmSession>();
return unpickle<SessionObject>(data.pickled_session, pickle_secret_);
std::optional<mtx::crypto::OlmSessionPtr>
Cache::getLatestOlmSession(const std::string &curve25519)
{
using namespace mtx::crypto;
auto txn = lmdb::txn::begin(env_);
auto db = getOlmSessionsDb(txn, curve25519);
std::string_view session_id, pickled_session;
std::optional<StoredOlmSession> currentNewest;
auto cursor = lmdb::cursor::open(txn, db);
while (cursor.get(session_id, pickled_session, MDB_NEXT)) {
auto data = json::parse(pickled_session).get<StoredOlmSession>();
if (!currentNewest || currentNewest->last_message_ts < data.last_message_ts)
currentNewest = data;
}
cursor.close();
txn.commit();
return currentNewest ? std::optional(unpickle<SessionObject>(currentNewest->pickled_session,
pickle_secret_))
: std::nullopt;
std::vector<std::string>
Cache::getOlmSessions(const std::string &curve25519)
using namespace mtx::crypto;
auto txn = lmdb::txn::begin(env_);
auto db = getOlmSessionsDb(txn, curve25519);
std::vector<std::string> res;
auto cursor = lmdb::cursor::open(txn, db);
while (cursor.get(session_id, unused, MDB_NEXT))
res.emplace_back(session_id);
cursor.close();
txn.commit();
return res;
}
void
Cache::saveOlmAccount(const std::string &data)
{
auto txn = lmdb::txn::begin(env_);
syncStateDb_.put(txn, OLM_ACCOUNT_KEY, data);
txn.commit();
}
std::string
Cache::restoreOlmAccount()
{
auto txn = ro_txn(env_);
std::string_view pickled;
syncStateDb_.get(txn, OLM_ACCOUNT_KEY, pickled);
return std::string(pickled.data(), pickled.size());
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
void
Cache::saveBackupVersion(const OnlineBackupVersion &data)
{
auto txn = lmdb::txn::begin(env_);
syncStateDb_.put(txn, CURRENT_ONLINE_BACKUP_VERSION, nlohmann::json(data).dump());
txn.commit();
}
void
Cache::deleteBackupVersion()
{
auto txn = lmdb::txn::begin(env_);
syncStateDb_.del(txn, CURRENT_ONLINE_BACKUP_VERSION);
txn.commit();
}
std::optional<OnlineBackupVersion>
Cache::backupVersion()
{
try {
auto txn = ro_txn(env_);
std::string_view v;
syncStateDb_.get(txn, CURRENT_ONLINE_BACKUP_VERSION, v);
return nlohmann::json::parse(v).get<OnlineBackupVersion>();
} catch (...) {
return std::nullopt;
}
}
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 Secrets 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);
}
Cache::storeSecret(const std::string name, const std::string secret, bool internal)
auto job = new QKeychain::WritePasswordJob(QCoreApplication::applicationName());
job->setAutoDelete(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());
} 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);
Cache::deleteSecret(const std::string name, bool internal)
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)
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();
}
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)
{
invitesDb_.del(txn, room_id);
getInviteStatesDb(txn, room_id).drop(txn, true);
getInviteMembersDb(txn, room_id).drop(txn, true);
Cache::removeInvite(const std::string &room_id)
Max Sandholm
committed
void
Cache::removeRoom(lmdb::txn &txn, const std::string &roomid)
Max Sandholm
committed
{
roomsDb_.del(txn, roomid);
getStatesDb(txn, roomid).drop(txn, true);
getAccountDataDb(txn, roomid).drop(txn, true);
getMembersDb(txn, roomid).drop(txn, true);
Max Sandholm
committed
}
{
auto txn = lmdb::txn::begin(env_, nullptr, 0);
Cache::setNextBatchToken(lmdb::txn &txn, const std::string &token)
syncStateDb_.put(txn, NEXT_BATCH_KEY, token);
if (!env_.handle())
return false;
auto txn = ro_txn(env_);
bool res = syncStateDb_.get(txn, NEXT_BATCH_KEY, token);
return res;
if (!env_.handle())
throw lmdb::error("Env already closed", MDB_INVALID);
auto txn = ro_txn(env_);
bool result = syncStateDb_.get(txn, NEXT_BATCH_KEY, token);
if (result)
return std::string(token.data(), token.size());
else
return "";
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_, 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_);
lmdb::dbi_close(env_, megolmSessionDataDb_);
env_.close();
verification_storage.status.clear();
if (!cacheDirectory_.isEmpty()) {
QDir(cacheDirectory_).removeRecursively();
Konstantinos Sideris
committed
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);