diff --git a/src/Cache.cpp b/src/Cache.cpp
index ee991dc2..8b8b2985 100644
--- a/src/Cache.cpp
+++ b/src/Cache.cpp
@@ -114,7 +114,13 @@ ro_txn(lmdb::env &env)
txn = lmdb::txn::begin(env, nullptr, MDB_RDONLY);
reuse_counter = 0;
} else if (reuse_counter > 0) {
- txn.renew();
+ try {
+ txn.renew();
+ } catch (...) {
+ txn.abort();
+ txn = lmdb::txn::begin(env, nullptr, MDB_RDONLY);
+ reuse_counter = 0;
+ }
}
reuse_counter++;
@@ -289,7 +295,9 @@ Cache::setup()
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);
+ encryptedRooms_ = lmdb::dbi::open(txn, ENCRYPTED_ROOMS_DB, MDB_CREATE);
+ [[maybe_unused]] auto verificationDb = getVerificationDb(txn);
+ [[maybe_unused]] auto userKeysDb = getUserKeysDb(txn);
txn.commit();
@@ -720,20 +728,35 @@ Cache::storeSecret(const std::string name, const std::string secret)
{
auto settings = UserSettings::instance();
auto job = new QKeychain::WritePasswordJob(QCoreApplication::applicationName());
+ job->setAutoDelete(true);
job->setInsecureFallback(true);
- job->setKey("matrix." +
- QString(QCryptographicHash::hash(settings->profile().toUtf8(),
- QCryptographicHash::Sha256)) +
- "." + name.c_str());
+ job->setSettings(UserSettings::instance()->qsettings());
+
+ job->setKey(
+ "matrix." +
+ QString(QCryptographicHash::hash(settings->profile().toUtf8(), QCryptographicHash::Sha256)
+ .toBase64()) +
+ "." + QString::fromStdString(name));
+
job->setTextData(QString::fromStdString(secret));
- QObject::connect(job, &QKeychain::Job::finished, job, [name, this](QKeychain::Job *job) {
- if (job->error()) {
- nhlog::db()->warn(
- "Storing secret '{}' failed: {}", name, job->errorString().toStdString());
- } else {
- emit secretChanged(name);
- }
- });
+
+ 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);
job->start();
}
@@ -744,10 +767,14 @@ Cache::deleteSecret(const std::string name)
QKeychain::DeletePasswordJob job(QCoreApplication::applicationName());
job.setAutoDelete(false);
job.setInsecureFallback(true);
- job.setKey("matrix." +
- QString(QCryptographicHash::hash(settings->profile().toUtf8(),
- QCryptographicHash::Sha256)) +
- "." + name.c_str());
+ job.setSettings(UserSettings::instance()->qsettings());
+
+ job.setKey(
+ "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;
@@ -765,10 +792,14 @@ Cache::secret(const std::string name)
QKeychain::ReadPasswordJob job(QCoreApplication::applicationName());
job.setAutoDelete(false);
job.setInsecureFallback(true);
- job.setKey("matrix." +
- QString(QCryptographicHash::hash(settings->profile().toUtf8(),
- QCryptographicHash::Sha256)) +
- "." + name.c_str());
+ job.setSettings(UserSettings::instance()->qsettings());
+
+ job.setKey(
+ "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;
@@ -838,6 +869,9 @@ Cache::setNextBatchToken(lmdb::txn &txn, const QString &token)
bool
Cache::isInitialized()
{
+ if (!env_.handle())
+ return false;
+
auto txn = ro_txn(env_);
std::string_view token;
@@ -1563,26 +1597,32 @@ Cache::roomsWithStateUpdates(const mtx::responses::Sync &res)
RoomInfo
Cache::singleRoomInfo(const std::string &room_id)
{
- auto txn = ro_txn(env_);
- auto statesdb = getStatesDb(txn, room_id);
+ auto txn = ro_txn(env_);
- std::string_view data;
+ try {
+ auto statesdb = getStatesDb(txn, room_id);
- // Check if the room is joined.
- if (roomsDb_.get(txn, room_id, data)) {
- try {
- RoomInfo tmp = json::parse(data);
- tmp.member_count = getMembersDb(txn, room_id).size(txn);
- tmp.join_rule = getRoomJoinRule(txn, statesdb);
- tmp.guest_access = getRoomGuestAccess(txn, statesdb);
+ std::string_view data;
- return tmp;
- } catch (const json::exception &e) {
- nhlog::db()->warn("failed to parse room info: room_id ({}), {}: {}",
- room_id,
- std::string(data.data(), data.size()),
- e.what());
+ // Check if the room is joined.
+ if (roomsDb_.get(txn, room_id, data)) {
+ try {
+ RoomInfo tmp = json::parse(data);
+ tmp.member_count = getMembersDb(txn, room_id).size(txn);
+ tmp.join_rule = getRoomJoinRule(txn, statesdb);
+ tmp.guest_access = getRoomGuestAccess(txn, statesdb);
+
+ return tmp;
+ } catch (const json::exception &e) {
+ nhlog::db()->warn("failed to parse room info: room_id ({}), {}: {}",
+ room_id,
+ std::string(data.data(), data.size()),
+ e.what());
+ }
}
+ } catch (const lmdb::error &e) {
+ nhlog::db()->warn(
+ "failed to read room info from db: room_id ({}), {}", room_id, e.what());
}
return RoomInfo();
@@ -3541,6 +3581,44 @@ Cache::roomMembers(const std::string &room_id)
return members;
}
+crypto::Trust
+Cache::roomVerificationStatus(const std::string &room_id)
+{
+ crypto::Trust trust = crypto::Verified;
+
+ try {
+ auto txn = lmdb::txn::begin(env_);
+
+ auto db = getMembersDb(txn, room_id);
+ auto keysDb = getUserKeysDb(txn);
+ std::vector<std::string> keysToRequest;
+
+ std::string_view user_id, unused;
+ auto cursor = lmdb::cursor::open(txn, db);
+ while (cursor.get(user_id, unused, MDB_NEXT)) {
+ auto verif = verificationStatus_(std::string(user_id), txn);
+ if (verif.unverified_device_count) {
+ trust = crypto::Unverified;
+ if (verif.verified_devices.empty() && verif.no_keys) {
+ // we probably don't have the keys yet, so query them
+ keysToRequest.push_back(std::string(user_id));
+ }
+ } else if (verif.user_verified == crypto::TOFU && trust == crypto::Verified)
+ trust = crypto::TOFU;
+ }
+
+ if (!keysToRequest.empty())
+ markUserKeysOutOfDate(txn, keysDb, keysToRequest, "");
+
+ } catch (std::exception &e) {
+ nhlog::db()->error(
+ "Failed to calculate verification status for {}: {}", room_id, e.what());
+ trust = crypto::Unverified;
+ }
+
+ return trust;
+}
+
std::map<std::string, std::optional<UserKeyCache>>
Cache::getMembersWithKeys(const std::string &room_id, bool verified_only)
{
@@ -3723,10 +3801,16 @@ from_json(const json &j, UserKeyCache &info)
std::optional<UserKeyCache>
Cache::userKeys(const std::string &user_id)
{
+ auto txn = ro_txn(env_);
+ return userKeys_(user_id, txn);
+}
+
+std::optional<UserKeyCache>
+Cache::userKeys_(const std::string &user_id, lmdb::txn &txn)
+{
std::string_view keys;
try {
- auto txn = ro_txn(env_);
auto db = getUserKeysDb(txn);
auto res = db.get(txn, user_id, keys);
@@ -3735,7 +3819,8 @@ Cache::userKeys(const std::string &user_id)
} else {
return {};
}
- } catch (std::exception &) {
+ } catch (std::exception &e) {
+ nhlog::db()->error("Failed to retrieve user keys for {}: {}", user_id, e.what());
return {};
}
}
@@ -3770,8 +3855,14 @@ Cache::updateUserKeys(const std::string &sync_token, const mtx::responses::Query
auto last_changed = updateToWrite.last_changed;
// skip if we are tracking this and expect it to be up to date with the last
// sync token
- if (!last_changed.empty() && last_changed != sync_token)
+ if (!last_changed.empty() && last_changed != sync_token) {
+ nhlog::db()->debug("Not storing update for user {}, because "
+ "last_changed {}, but we fetched update for {}",
+ user,
+ last_changed,
+ sync_token);
continue;
+ }
if (!updateToWrite.master_keys.keys.empty() &&
update.master_keys.keys != updateToWrite.master_keys.keys) {
@@ -3819,8 +3910,43 @@ Cache::updateUserKeys(const std::string &sync_token, const mtx::responses::Query
}
}
- if (!keyReused && !oldDeviceKeys.count(device_id))
+ if (!keyReused && !oldDeviceKeys.count(device_id)) {
+ // ensure the key has a valid signature from itself
+ std::string device_signing_key =
+ "ed25519:" + device_keys.device_id;
+ if (device_id != device_keys.device_id) {
+ nhlog::crypto()->warn(
+ "device {}:{} has a different device id "
+ "in the body: {}",
+ user,
+ device_id,
+ device_keys.device_id);
+ continue;
+ }
+ if (!device_keys.signatures.count(user) ||
+ !device_keys.signatures.at(user).count(
+ device_signing_key)) {
+ nhlog::crypto()->warn(
+ "device {}:{} has no signature",
+ user,
+ device_id);
+ continue;
+ }
+
+ if (!mtx::crypto::ed25519_verify_signature(
+ device_keys.keys.at(device_signing_key),
+ json(device_keys),
+ device_keys.signatures.at(user).at(
+ device_signing_key))) {
+ nhlog::crypto()->warn(
+ "device {}:{} has an invalid signature",
+ user,
+ device_id);
+ continue;
+ }
+
updateToWrite.device_keys[device_id] = device_keys;
+ }
}
for (const auto &[key_id, key] : device_keys.keys) {
@@ -3830,6 +3956,7 @@ Cache::updateUserKeys(const std::string &sync_token, const mtx::responses::Query
updateToWrite.seen_device_ids.insert(device_id);
}
}
+ updateToWrite.updated_at = sync_token;
db.put(txn, user, json(updateToWrite).dump());
}
@@ -3882,14 +4009,15 @@ Cache::markUserKeysOutOfDate(lmdb::txn &txn,
nhlog::db()->debug("Marking user keys out of date: {}", user);
std::string_view oldKeys;
- auto res = db.get(txn, user, oldKeys);
-
- if (!res)
- continue;
- auto cacheEntry =
- json::parse(std::string_view(oldKeys.data(), oldKeys.size())).get<UserKeyCache>();
+ UserKeyCache cacheEntry;
+ auto res = db.get(txn, user, oldKeys);
+ if (res) {
+ cacheEntry = json::parse(std::string_view(oldKeys.data(), oldKeys.size()))
+ .get<UserKeyCache>();
+ }
cacheEntry.last_changed = sync_token;
+
db.put(txn, user, json(cacheEntry).dump());
query.device_keys[user] = {};
@@ -3915,35 +4043,46 @@ void
Cache::query_keys(const std::string &user_id,
std::function<void(const UserKeyCache &, mtx::http::RequestErr)> cb)
{
- auto cache_ = cache::userKeys(user_id);
+ mtx::requests::QueryKeys req;
+ std::string last_changed;
+ {
+ auto txn = ro_txn(env_);
+ auto cache_ = userKeys_(user_id, txn);
- if (cache_.has_value()) {
- if (!cache_->updated_at.empty() && cache_->updated_at == cache_->last_changed) {
- cb(cache_.value(), {});
- return;
- }
- }
+ if (cache_.has_value()) {
+ if (cache_->updated_at == cache_->last_changed) {
+ cb(cache_.value(), {});
+ return;
+ } else
+ nhlog::db()->info("Keys outdated for {}: {} vs {}",
+ user_id,
+ cache_->updated_at,
+ cache_->last_changed);
+ } else
+ nhlog::db()->info("No keys found for {}", user_id);
- mtx::requests::QueryKeys req;
- req.device_keys[user_id] = {};
+ req.device_keys[user_id] = {};
- std::string last_changed;
- if (cache_)
- last_changed = cache_->last_changed;
- req.token = last_changed;
+ if (cache_)
+ last_changed = cache_->last_changed;
+ req.token = last_changed;
+ }
// use context object so that we can disconnect again
QObject *context{new QObject(this)};
- QObject::connect(this,
- &Cache::verificationStatusChanged,
- context,
- [cb, user_id, context_ = context](std::string updated_user) mutable {
- if (user_id == updated_user) {
- context_->deleteLater();
- auto keys = cache::userKeys(user_id);
- cb(keys.value_or(UserKeyCache{}), {});
- }
- });
+ QObject::connect(
+ this,
+ &Cache::verificationStatusChanged,
+ context,
+ [cb, user_id, context_ = context, this](std::string updated_user) mutable {
+ if (user_id == updated_user) {
+ context_->deleteLater();
+ auto txn = ro_txn(env_);
+ auto keys = this->userKeys_(user_id, txn);
+ cb(keys.value_or(UserKeyCache{}), {});
+ }
+ },
+ Qt::QueuedConnection);
http::client()->query_keys(
req,
@@ -3971,17 +4110,16 @@ to_json(json &j, const VerificationCache &info)
void
from_json(const json &j, VerificationCache &info)
{
- info.device_verified = j.at("device_verified").get<std::vector<std::string>>();
- info.device_blocked = j.at("device_blocked").get<std::vector<std::string>>();
+ info.device_verified = j.at("device_verified").get<std::set<std::string>>();
+ info.device_blocked = j.at("device_blocked").get<std::set<std::string>>();
}
std::optional<VerificationCache>
-Cache::verificationCache(const std::string &user_id)
+Cache::verificationCache(const std::string &user_id, lmdb::txn &txn)
{
std::string_view verifiedVal;
- auto txn = lmdb::txn::begin(env_);
- auto db = getVerificationDb(txn);
+ auto db = getVerificationDb(txn);
try {
VerificationCache verified_state;
@@ -4000,26 +4138,28 @@ Cache::verificationCache(const std::string &user_id)
void
Cache::markDeviceVerified(const std::string &user_id, const std::string &key)
{
- std::string_view val;
+ {
+ std::string_view val;
- auto txn = lmdb::txn::begin(env_);
- auto db = getVerificationDb(txn);
+ auto txn = lmdb::txn::begin(env_);
+ auto db = getVerificationDb(txn);
- try {
- VerificationCache verified_state;
- auto res = db.get(txn, user_id, val);
- if (res) {
- verified_state = json::parse(val);
- }
+ try {
+ VerificationCache verified_state;
+ auto res = db.get(txn, user_id, val);
+ if (res) {
+ verified_state = json::parse(val);
+ }
- for (const auto &device : verified_state.device_verified)
- if (device == key)
- return;
+ for (const auto &device : verified_state.device_verified)
+ if (device == key)
+ return;
- verified_state.device_verified.push_back(key);
- db.put(txn, user_id, json(verified_state).dump());
- txn.commit();
- } catch (std::exception &) {
+ verified_state.device_verified.insert(key);
+ db.put(txn, user_id, json(verified_state).dump());
+ txn.commit();
+ } catch (std::exception &) {
+ }
}
const auto local_user = utils::localUser().toStdString();
@@ -4057,11 +4197,7 @@ Cache::markDeviceUnverified(const std::string &user_id, const std::string &key)
verified_state = json::parse(val);
}
- verified_state.device_verified.erase(
- std::remove(verified_state.device_verified.begin(),
- verified_state.device_verified.end(),
- key),
- verified_state.device_verified.end());
+ verified_state.device_verified.erase(key);
db.put(txn, user_id, json(verified_state).dump());
txn.commit();
@@ -4091,13 +4227,25 @@ Cache::markDeviceUnverified(const std::string &user_id, const std::string &key)
VerificationStatus
Cache::verificationStatus(const std::string &user_id)
{
+ auto txn = ro_txn(env_);
+ return verificationStatus_(user_id, txn);
+}
+
+VerificationStatus
+Cache::verificationStatus_(const std::string &user_id, lmdb::txn &txn)
+{
std::unique_lock<std::mutex> lock(verification_storage.verification_storage_mtx);
if (verification_storage.status.count(user_id))
return verification_storage.status.at(user_id);
VerificationStatus status;
- if (auto verifCache = verificationCache(user_id)) {
+ // assume there is at least one unverified device until we have checked we have the device
+ // list for that user.
+ status.unverified_device_count = 1;
+ status.no_keys = true;
+
+ if (auto verifCache = verificationCache(user_id, txn)) {
status.verified_devices = verifCache->device_verified;
}
@@ -4105,12 +4253,10 @@ Cache::verificationStatus(const std::string &user_id)
crypto::Trust trustlevel = crypto::Trust::Unverified;
if (user_id == local_user) {
- status.verified_devices.push_back(http::client()->device_id());
+ status.verified_devices.insert(http::client()->device_id());
trustlevel = crypto::Trust::Verified;
}
- verification_storage.status[user_id] = status;
-
auto verifyAtLeastOneSig = [](const auto &toVerif,
const std::map<std::string, std::string> &keys,
const std::string &keyOwner) {
@@ -4128,6 +4274,16 @@ Cache::verificationStatus(const std::string &user_id)
return false;
};
+ auto updateUnverifiedDevices = [&status](auto &theirDeviceKeys) {
+ int currentVerifiedDevices = 0;
+ for (auto device_id : status.verified_devices) {
+ if (theirDeviceKeys.count(device_id))
+ currentVerifiedDevices++;
+ }
+ status.unverified_device_count =
+ static_cast<int>(theirDeviceKeys.size()) - currentVerifiedDevices;
+ };
+
try {
// for local user verify this device_key -> our master_key -> our self_signing_key
// -> our device_keys
@@ -4137,17 +4293,27 @@ Cache::verificationStatus(const std::string &user_id)
//
// This means verifying the other user adds 2 extra steps,verifying our user_signing
// key and their master key
- auto ourKeys = userKeys(local_user);
- auto theirKeys = userKeys(user_id);
- if (!ourKeys || !theirKeys)
+ auto ourKeys = userKeys_(local_user, txn);
+ auto theirKeys = userKeys_(user_id, txn);
+ if (theirKeys)
+ status.no_keys = false;
+
+ if (!ourKeys || !theirKeys) {
+ verification_storage.status[user_id] = status;
return status;
+ }
+
+ // Update verified devices count to count without cross-signing
+ updateUnverifiedDevices(theirKeys->device_keys);
if (!mtx::crypto::ed25519_verify_signature(
olm::client()->identity_keys().ed25519,
json(ourKeys->master_keys),
ourKeys->master_keys.signatures.at(local_user)
- .at("ed25519:" + http::client()->device_id())))
+ .at("ed25519:" + http::client()->device_id()))) {
+ verification_storage.status[user_id] = status;
return status;
+ }
auto master_keys = ourKeys->master_keys.keys;
@@ -4162,14 +4328,17 @@ Cache::verificationStatus(const std::string &user_id)
trustlevel = crypto::Trust::Verified;
else if (!theirKeys->master_key_changed)
trustlevel = crypto::Trust::TOFU;
- else
+ else {
+ verification_storage.status[user_id] = status;
return status;
+ }
master_keys = theirKeys->master_keys.keys;
}
status.user_verified = trustlevel;
+ verification_storage.status[user_id] = status;
if (!verifyAtLeastOneSig(theirKeys->self_signing_keys, master_keys, user_id))
return status;
@@ -4180,16 +4349,19 @@ Cache::verificationStatus(const std::string &user_id)
device_key.keys.at("curve25519:" + device_key.device_id);
if (verifyAtLeastOneSig(
device_key, theirKeys->self_signing_keys.keys, user_id)) {
- status.verified_devices.push_back(device_key.device_id);
+ status.verified_devices.insert(device_key.device_id);
status.verified_device_keys[identkey] = trustlevel;
}
} catch (...) {
}
}
+ updateUnverifiedDevices(theirKeys->device_keys);
verification_storage.status[user_id] = status;
return status;
- } catch (std::exception &) {
+ } catch (std::exception &e) {
+ nhlog::db()->error(
+ "Failed to calculate verification status of {}: {}", user_id, e.what());
return status;
}
}
|