Newer
Older
// SPDX-FileCopyrightText: 2017 Konstantinos Sideris <siderisk@auth.gr>
// SPDX-FileCopyrightText: 2021 Nheko Contributors
// SPDX-FileCopyrightText: 2022 Nheko Contributors
#include <QCryptographicHash>
#if __has_include(<keychain.h>)
#include <keychain.h>
#else
#include <mtx/responses/common.hpp>
#include "UserSettingsPage.h"
#include "encryption/Olm.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{"2022.04.08"};
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");
constexpr auto PRESENCE_DB("presence");
//! 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; }
};
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;
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)
// 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;
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 = std::vector{
EventType::Reaction,
EventType::CallCandidates,
EventType::Unsupported,
};
if (auto temp = getAccountData(txn, mtx::events::EventType::NhekoHiddenEvents, "")) {
auto h = std::get<
mtx::events::AccountDataEvent<mtx::events::account_data::nheko_extensions::HiddenEvents>>(
*temp);
if (h.content.hidden_event_types)
hiddenEvents = std::move(h.content);
}
if (auto temp = getAccountData(txn, mtx::events::EventType::NhekoHiddenEvents, room_id)) {
auto h = std::get<
mtx::events::AccountDataEvent<mtx::events::account_data::nheko_extensions::HiddenEvents>>(
*temp);
if (h.content.hidden_event_types)
hiddenEvents = std::move(h.content);
}
return std::visit(
[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}
connect(this, &Cache::userKeysUpdate, this, &Cache::updateUserKeys, Qt::QueuedConnection);
connect(
this,
&Cache::verificationStatusChanged,
this,
[this](const std::string &u) {
if (u == localUserId_.toStdString()) {
auto status = verificationStatus(u);
emit selfVerificationStatusChanged();
void
Cache::setup()
{
auto settings = UserSettings::instance();
// Previous location of the cache directory
QStringLiteral("%1/%2%3").arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation),
QString::fromUtf8(localUserId_.toUtf8().toHex()),
QString::fromUtf8(settings->profile().toUtf8().toHex()));
cacheDirectory_ = QStringLiteral("%1/%2%3").arg(
QStandardPaths::writableLocation(QStandardPaths::AppDataLocation),
QString::fromUtf8(localUserId_.toUtf8().toHex()),
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) {
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()));
}
nhlog::db()->warn("resetting cache due to LMDB version mismatch: {}", e.what());
auto eList = stateDir.entryList(QDir::NoDotAndDotDot);
for (const auto &file : qAsConst(eList)) {
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);
presenceDb_ = lmdb::dbi::open(txn, PRESENCE_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);
txn.commit();
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(
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
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)
{
auto settings = UserSettings::instance()->qsettings();
if (toLoad.empty()) {
this->databaseReady_ = true;
emit databaseReady();
nhlog::db()->debug("Database ready");
if (settings->value(QStringLiteral("run_without_secure_secrets_service"), false).toBool()) {
for (auto &[name_, internal] : toLoad) {
auto value = settings->value("secrets/" + name).toString();
if (value.isEmpty()) {
nhlog::db()->info("Restored empty secret '{}'.", name.toStdString());
} else {
std::unique_lock lock(secret_storage.mtx);
secret_storage.secrets[name.toStdString()] = value.toStdString();
}
}
// if we emit the databaseReady signal directly it won't be received
QTimer::singleShot(0, this, [this] { loadSecrets({}); });
auto [name_, internal] = toLoad.front();
auto job = new QKeychain::ReadPasswordJob(QCoreApplication::applicationName());
job->setAutoDelete(true);
job->setInsecureFallback(true);
job->setSettings(settings);
auto name = secretName(name_, internal);
job->setKey(name);
connect(job,
&QKeychain::ReadPasswordJob::finished,
this,
[this, name, toLoad, job](QKeychain::Job *) mutable {
nhlog::db()->debug("Finished reading '{}'", toLoad.begin()->first);
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, [this, toLoad] { loadSecrets(toLoad); });
nhlog::db()->debug("Reading '{}'", name_);
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()->qsettings();
if (settings->value(QStringLiteral("run_without_secure_secrets_service"), false).toBool()) {
settings->setValue("secrets/" + name, QString::fromStdString(secret));
// if we emit the signal directly it won't be received
QTimer::singleShot(0, this, [this, name_] { emit secretChanged(name_); });
nhlog::db()->info("Storing secret '{}' successful", name_);
return;
}
auto job = new QKeychain::WritePasswordJob(QCoreApplication::applicationName());
job->setAutoDelete(true);
job->setInsecureFallback(true);
job->setSettings(settings);
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, [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()->qsettings();
if (settings->value(QStringLiteral("run_without_secure_secrets_service"), false).toBool()) {
settings->remove("secrets/" + name_);
// if we emit the signal directly it won't be received
QTimer::singleShot(0, this, [this, name] { emit secretChanged(name); });
return;
}
auto job = new QKeychain::DeletePasswordJob(QCoreApplication::applicationName());
job->setAutoDelete(true);
job->setInsecureFallback(true);
job->setSettings(settings);
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_;
Cache::setEncryptedRoom(lmdb::txn &txn, const std::string &room_id)
nhlog::db()->info("mark room {} as encrypted", room_id);
}
bool
Cache::isRoomEncrypted(const std::string &room_id)
{
auto txn = ro_txn(env_);
auto res = encryptedRooms_.get(txn, room_id, unused);
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 =
nlohmann::json::parse(event).get<StateEvent<Encryption>>();
} catch (const nlohmann::json::exception &e) {
nhlog::db()->warn("failed to parse m.room.encryption event: {}", e.what());
return Encryption{};
}
mtx::crypto::ExportedSessionKeys
Cache::exportSessionKeys()
{
auto txn = ro_txn(env_);
auto cursor = lmdb::cursor::open(txn, inboundMegolmSessionDb_);
std::string_view key, value;
while (cursor.get(key, value, MDB_NEXT)) {
ExportedSession exported;
MegolmSessionIndex index;
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;
}
try {
using namespace mtx::crypto;
std::string_view v;
if (megolmSessionDataDb_.get(txn, nlohmann::json(index).dump(), v)) {
auto data = nlohmann::json::parse(v).get<GroupSessionData>();
exported.sender_key = data.sender_key;
if (!data.sender_claimed_ed25519_key.empty())
exported.sender_claimed_keys["ed25519"] = data.sender_claimed_ed25519_key;
exported.forwarding_curve25519_key_chain = data.forwarding_curve25519_key_chain;
}
} catch (std::exception &e) {
nhlog::db()->error("Failed to retrieve Megolm Session Data: {}", e.what());
continue;
}
exported.room_id = index.room_id;
exported.session_id = index.session_id;
exported.session_key = export_session(saved_session.get(), -1);
keys.sessions.push_back(exported);
}
}
void
Cache::importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys)
{
std::size_t importCount = 0;
auto txn = lmdb::txn::begin(env_);
for (const auto &s : keys.sessions) {
MegolmSessionIndex index;
index.room_id = s.room_id;
index.session_id = s.session_id;
data.sender_key = s.sender_key;
data.forwarding_curve25519_key_chain = s.forwarding_curve25519_key_chain;
data.trusted = false;
if (s.sender_claimed_keys.count("ed25519"))
data.sender_claimed_ed25519_key = s.sender_claimed_keys.at("ed25519");
try {
auto exported_session = mtx::crypto::import_session(s.session_key);
using namespace mtx::crypto;
const auto key = nlohmann::json(index).dump();
const auto pickled =
pickle<InboundSessionObject>(exported_session.get(), pickle_secret_);
std::string_view value;
if (inboundMegolmSessionDb_.get(txn, key, value)) {
auto oldSession =
unpickle<InboundSessionObject>(std::string(value), pickle_secret_);
if (olm_inbound_group_session_first_known_index(exported_session.get()) >=
olm_inbound_group_session_first_known_index(oldSession.get())) {
nhlog::crypto()->warn(
"Not storing inbound session with newer or equal first known index");
continue;
}
}
inboundMegolmSessionDb_.put(txn, key, pickled);
megolmSessionDataDb_.put(txn, key, nlohmann::json(data).dump());
ChatPage::instance()->receivedSessionKey(index.room_id, index.session_id);
importCount++;
} catch (const mtx::crypto::olm_exception &e) {
nhlog::crypto()->critical(
"failed to import inbound megolm session {}: {}", index.session_id, e.what());
continue;
} catch (const lmdb::error &e) {
nhlog::crypto()->critical(
"failed to save inbound megolm session {}: {}", index.session_id, e.what());
continue;
}
txn.commit();
nhlog::crypto()->info("Imported {} out of {} keys", importCount, keys.sessions.size());
//
// Session Management
//
void
Cache::saveInboundMegolmSession(const MegolmSessionIndex &index,
mtx::crypto::InboundGroupSessionPtr session,
const GroupSessionData &data)
const auto key = nlohmann::json(index).dump();
const auto pickled = pickle<InboundSessionObject>(session.get(), pickle_secret_);
Nicolas Werner
committed
std::string_view value;
if (inboundMegolmSessionDb_.get(txn, key, value)) {
auto oldSession = unpickle<InboundSessionObject>(std::string(value), pickle_secret_);
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;
Nicolas Werner
committed
}
Nicolas Werner
committed
inboundMegolmSessionDb_.put(txn, key, pickled);
megolmSessionDataDb_.put(txn, key, nlohmann::json(data).dump());
Cache::getInboundMegolmSession(const MegolmSessionIndex &index)
{
std::string key = nlohmann::json(index).dump();
std::string_view value;
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());
}
Cache::inboundMegolmSessionExists(const MegolmSessionIndex &index)
std::string key = nlohmann::json(index).dump();
return inboundMegolmSessionDb_.get(txn, key, value);
} catch (std::exception &e) {
nhlog::db()->error("Failed to get inbound megolm session {}", e.what());
}
Cache::updateOutboundMegolmSession(const std::string &room_id,
const GroupSessionData &data_,
mtx::crypto::OutboundGroupSessionPtr &ptr)
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.session_id = mtx::crypto::session_id(ptr.get());
// Save the updated pickled data for the session.
j["session"] = pickle<OutboundSessionObject>(ptr.get(), pickle_secret_);
auto txn = lmdb::txn::begin(env_);
outboundMegolmSessionDb_.put(txn, room_id, j.dump());
megolmSessionDataDb_.put(txn, nlohmann::json(index).dump(), nlohmann::json(data).dump());
void
Cache::dropOutboundMegolmSession(const std::string &room_id)
{
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.session_id = mtx::crypto::session_id(session.get());
auto txn = lmdb::txn::begin(env_);
outboundMegolmSessionDb_.put(txn, room_id, j.dump());
megolmSessionDataDb_.put(txn, nlohmann::json(index).dump(), nlohmann::json(data).dump());
Cache::outboundMegolmSessionExists(const std::string &room_id) noexcept
try {
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)
auto txn = ro_txn(env_);
std::string_view value;
outboundMegolmSessionDb_.get(txn, room_id, value);
auto obj = nlohmann::json::parse(value);
ref.session =
unpickle<OutboundSessionObject>(obj.at("session").get<std::string>(), pickle_secret_);
MegolmSessionIndex index;
index.room_id = room_id;
index.session_id = mtx::crypto::session_id(ref.session.get());
if (megolmSessionDataDb_.get(txn, nlohmann::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)
{
if (megolmSessionDataDb_.get(txn, nlohmann::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)
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, nlohmann::json(stored_session).dump());
Cache::getOlmSession(const std::string &curve25519, const std::string &session_id)
auto txn = lmdb::txn::begin(env_);
auto db = getOlmSessionsDb(txn, curve25519);
std::string_view pickled;
bool found = db.get(txn, session_id, pickled);
auto data = nlohmann::json::parse(pickled).get<StoredOlmSession>();
return unpickle<SessionObject>(data.pickled_session, pickle_secret_);
}
std::optional<mtx::crypto::OlmSessionPtr>
Cache::getLatestOlmSession(const std::string &curve25519)
{
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 = nlohmann::json::parse(pickled_session).get<StoredOlmSession>();
if (!currentNewest || currentNewest->last_message_ts < data.last_message_ts)
currentNewest = data;
}
cursor.close();
return currentNewest ? std::optional(unpickle<SessionObject>(currentNewest->pickled_session,
pickle_secret_))
: std::nullopt;
std::vector<std::string>
Cache::getOlmSessions(const std::string &curve25519)
auto txn = lmdb::txn::begin(env_);
auto db = getOlmSessionsDb(txn, curve25519);
std::string_view session_id, unused;
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();
}
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()
{
std::string_view pickled;
syncStateDb_.get(txn, OLM_ACCOUNT_KEY, pickled);