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;
}
}
diff --git a/src/CacheCryptoStructs.h b/src/CacheCryptoStructs.h
index 69d64885..6c402674 100644
--- a/src/CacheCryptoStructs.h
+++ b/src/CacheCryptoStructs.h
@@ -112,9 +112,13 @@ struct VerificationStatus
//! True, if the users master key is verified
crypto::Trust user_verified = crypto::Trust::Unverified;
//! List of all devices marked as verified
- std::vector<std::string> verified_devices;
+ std::set<std::string> verified_devices;
//! Map from sender key/curve25519 to trust status
std::map<std::string, crypto::Trust> verified_device_keys;
+ //! Count of unverified devices
+ int unverified_device_count = 0;
+ // if the keys are not in cache
+ bool no_keys = false;
};
//! In memory cache of verification status
@@ -154,9 +158,9 @@ from_json(const nlohmann::json &j, UserKeyCache &info);
struct VerificationCache
{
//! list of verified device_ids with device-verification
- std::vector<std::string> device_verified;
+ std::set<std::string> device_verified;
//! list of devices the user blocks
- std::vector<std::string> device_blocked;
+ std::set<std::string> device_blocked;
};
void
diff --git a/src/Cache_p.h b/src/Cache_p.h
index 30c365a6..748404d1 100644
--- a/src/Cache_p.h
+++ b/src/Cache_p.h
@@ -46,7 +46,6 @@ public:
std::string statusMessage(const std::string &user_id);
// user cache stores user keys
- std::optional<UserKeyCache> userKeys(const std::string &user_id);
std::map<std::string, std::optional<UserKeyCache>> getMembersWithKeys(
const std::string &room_id,
bool verified_only);
@@ -63,9 +62,11 @@ public:
std::function<void(const UserKeyCache &, mtx::http::RequestErr)> cb);
// device & user verification cache
+ std::optional<UserKeyCache> userKeys(const std::string &user_id);
VerificationStatus verificationStatus(const std::string &user_id);
void markDeviceVerified(const std::string &user_id, const std::string &device);
void markDeviceUnverified(const std::string &user_id, const std::string &device);
+ crypto::Trust roomVerificationStatus(const std::string &room_id);
std::vector<std::string> joinedRooms();
@@ -414,24 +415,25 @@ private:
if constexpr (isStateEvent_<decltype(e)>) {
eventsDb.put(txn, e.event_id, json(e).dump());
- if (std::is_same_v<
- std::remove_cv_t<std::remove_reference_t<decltype(e)>>,
- StateEvent<mtx::events::msg::Redacted>>) {
- if (e.type == EventType::RoomMember)
- membersdb.del(txn, e.state_key, "");
- else if (e.state_key.empty())
- statesdb.del(txn, to_string(e.type));
- else
- stateskeydb.del(
- txn,
- to_string(e.type),
- json::object({
- {"key", e.state_key},
- {"id", e.event_id},
- })
- .dump());
- } else if (e.type != EventType::Unsupported) {
- if (e.state_key.empty())
+ if (e.type != EventType::Unsupported) {
+ if (std::is_same_v<
+ std::remove_cv_t<
+ std::remove_reference_t<decltype(e)>>,
+ StateEvent<mtx::events::msg::Redacted>>) {
+ if (e.type == EventType::RoomMember)
+ membersdb.del(txn, e.state_key, "");
+ else if (e.state_key.empty())
+ statesdb.del(txn, to_string(e.type));
+ else
+ stateskeydb.del(
+ txn,
+ to_string(e.type),
+ json::object({
+ {"key", e.state_key},
+ {"id", e.event_id},
+ })
+ .dump());
+ } else if (e.state_key.empty())
statesdb.put(
txn, to_string(e.type), json(e).dump());
else
@@ -680,7 +682,10 @@ private:
return QString::fromStdString(event.state_key);
}
- std::optional<VerificationCache> verificationCache(const std::string &user_id);
+ std::optional<VerificationCache> verificationCache(const std::string &user_id,
+ lmdb::txn &txn);
+ VerificationStatus verificationStatus_(const std::string &user_id, lmdb::txn &txn);
+ std::optional<UserKeyCache> userKeys_(const std::string &user_id, lmdb::txn &txn);
void setNextBatchToken(lmdb::txn &txn, const std::string &token);
void setNextBatchToken(lmdb::txn &txn, const QString &token);
diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index 42e3bc7b..8a0e891b 100644
--- a/src/ChatPage.cpp
+++ b/src/ChatPage.cpp
@@ -4,11 +4,9 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#include <QApplication>
-#include <QImageReader>
#include <QInputDialog>
#include <QMessageBox>
#include <QSettings>
-#include <QShortcut>
#include <mtx/responses.hpp>
diff --git a/src/ChatPage.h b/src/ChatPage.h
index 751e7074..c90b87f5 100644
--- a/src/ChatPage.h
+++ b/src/ChatPage.h
@@ -17,10 +17,8 @@
#include <mtx/events/presence.hpp>
#include <mtx/secret_storage.hpp>
-#include <QFrame>
#include <QHBoxLayout>
#include <QMap>
-#include <QPixmap>
#include <QPoint>
#include <QTimer>
#include <QWidget>
diff --git a/src/MemberList.cpp b/src/MemberList.cpp
index 196647fe..0c0f0cdd 100644
--- a/src/MemberList.cpp
+++ b/src/MemberList.cpp
@@ -53,6 +53,7 @@ MemberList::roleNames() const
{Mxid, "mxid"},
{DisplayName, "displayName"},
{AvatarUrl, "avatarUrl"},
+ {Trustlevel, "trustlevel"},
};
}
@@ -69,6 +70,17 @@ MemberList::data(const QModelIndex &index, int role) const
return m_memberList[index.row()].first.display_name;
case AvatarUrl:
return m_memberList[index.row()].second;
+ case Trustlevel: {
+ auto stat =
+ cache::verificationStatus(m_memberList[index.row()].first.user_id.toStdString());
+
+ if (!stat)
+ return crypto::Unverified;
+ if (stat->unverified_device_count)
+ return crypto::Unverified;
+ else
+ return stat->user_verified;
+ }
default:
return {};
}
diff --git a/src/MemberList.h b/src/MemberList.h
index e6522694..cffcd83d 100644
--- a/src/MemberList.h
+++ b/src/MemberList.h
@@ -25,6 +25,7 @@ public:
Mxid,
DisplayName,
AvatarUrl,
+ Trustlevel,
};
MemberList(const QString &room_id, QObject *parent = nullptr);
diff --git a/src/MxcImageProvider.cpp b/src/MxcImageProvider.cpp
index b8648269..056374a9 100644
--- a/src/MxcImageProvider.cpp
+++ b/src/MxcImageProvider.cpp
@@ -11,6 +11,8 @@
#include <QByteArray>
#include <QDir>
#include <QFileInfo>
+#include <QPainter>
+#include <QPainterPath>
#include <QStandardPaths>
#include "Logging.h"
@@ -22,14 +24,26 @@ QHash<QString, mtx::crypto::EncryptedFile> infos;
QQuickImageResponse *
MxcImageProvider::requestImageResponse(const QString &id, const QSize &requestedSize)
{
- auto id_ = id;
- bool crop = true;
- if (id.endsWith("?scale")) {
- crop = false;
- id_.remove("?scale");
+ auto id_ = id;
+ bool crop = true;
+ double radius = 0;
+
+ auto queryStart = id.lastIndexOf('?');
+ if (queryStart != -1) {
+ id_ = id.left(queryStart);
+ auto query = id.midRef(queryStart + 1);
+ auto queryBits = query.split('&');
+
+ for (auto b : queryBits) {
+ if (b == "scale") {
+ crop = false;
+ } else if (b.startsWith("radius=")) {
+ radius = b.mid(7).toDouble();
+ }
+ }
}
- MxcImageResponse *response = new MxcImageResponse(id_, crop, requestedSize);
+ MxcImageResponse *response = new MxcImageResponse(id_, crop, radius, requestedSize);
pool.start(response);
return response;
}
@@ -53,14 +67,35 @@ MxcImageResponse::run()
}
emit finished();
},
- m_crop);
+ m_crop,
+ m_radius);
+}
+
+static QImage
+clipRadius(QImage img, double radius)
+{
+ QImage out(img.size(), QImage::Format_ARGB32_Premultiplied);
+ out.fill(Qt::transparent);
+
+ QPainter painter(&out);
+ painter.setRenderHint(QPainter::Antialiasing, true);
+ painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
+
+ QPainterPath ppath;
+ ppath.addRoundedRect(img.rect(), radius, radius, Qt::SizeMode::RelativeSize);
+
+ painter.setClipPath(ppath);
+ painter.drawImage(img.rect(), img);
+
+ return out;
}
void
MxcImageProvider::download(const QString &id,
const QSize &requestedSize,
std::function<void(QString, QSize, QImage, QString)> then,
- bool crop)
+ bool crop,
+ double radius)
{
std::optional<mtx::crypto::EncryptedFile> encryptionInfo;
auto temp = infos.find("mxc://" + id);
@@ -69,12 +104,13 @@ MxcImageProvider::download(const QString &id,
if (requestedSize.isValid() && !encryptionInfo) {
QString fileName =
- QString("%1_%2x%3_%4")
+ QString("%1_%2x%3_%4_radius%5")
.arg(QString::fromUtf8(id.toUtf8().toBase64(QByteArray::Base64UrlEncoding |
QByteArray::OmitTrailingEquals)))
.arg(requestedSize.width())
.arg(requestedSize.height())
- .arg(crop ? "crop" : "scale");
+ .arg(crop ? "crop" : "scale")
+ .arg(radius);
QFileInfo fileInfo(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) +
"/media_cache",
fileName);
@@ -86,6 +122,10 @@ MxcImageProvider::download(const QString &id,
image = image.scaled(
requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
+ if (radius != 0) {
+ image = clipRadius(std::move(image), radius);
+ }
+
if (!image.isNull()) {
then(id, requestedSize, image, fileInfo.absoluteFilePath());
return;
@@ -100,8 +140,8 @@ MxcImageProvider::download(const QString &id,
opts.method = crop ? "crop" : "scale";
http::client()->get_thumbnail(
opts,
- [fileInfo, requestedSize, then, id](const std::string &res,
- mtx::http::RequestErr err) {
+ [fileInfo, requestedSize, radius, then, id](const std::string &res,
+ mtx::http::RequestErr err) {
if (err || res.empty()) {
then(id, QSize(), {}, "");
@@ -113,6 +153,10 @@ MxcImageProvider::download(const QString &id,
if (!image.isNull()) {
image = image.scaled(
requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
+
+ if (radius != 0) {
+ image = clipRadius(std::move(image), radius);
+ }
}
image.setText("mxc url", "mxc://" + id);
if (image.save(fileInfo.absoluteFilePath(), "png"))
@@ -126,8 +170,12 @@ MxcImageProvider::download(const QString &id,
});
} else {
try {
- QString fileName = QString::fromUtf8(id.toUtf8().toBase64(
- QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals));
+ QString fileName =
+ QString("%1_radius%2")
+ .arg(QString::fromUtf8(id.toUtf8().toBase64(
+ QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals)))
+ .arg(radius);
+
QFileInfo fileInfo(
QStandardPaths::writableLocation(QStandardPaths::CacheLocation) +
"/media_cache",
@@ -148,6 +196,11 @@ MxcImageProvider::download(const QString &id,
QImage image = utils::readImage(data);
image.setText("mxc url", "mxc://" + id);
if (!image.isNull()) {
+ if (radius != 0) {
+ image =
+ clipRadius(std::move(image), radius);
+ }
+
then(id,
requestedSize,
image,
@@ -158,6 +211,11 @@ MxcImageProvider::download(const QString &id,
QImage image =
utils::readImageFromFile(fileInfo.absoluteFilePath());
if (!image.isNull()) {
+ if (radius != 0) {
+ image =
+ clipRadius(std::move(image), radius);
+ }
+
then(id,
requestedSize,
image,
@@ -169,7 +227,7 @@ MxcImageProvider::download(const QString &id,
http::client()->download(
"mxc://" + id.toStdString(),
- [fileInfo, requestedSize, then, id, encryptionInfo](
+ [fileInfo, requestedSize, then, id, radius, encryptionInfo](
const std::string &res,
const std::string &,
const std::string &originalFilename,
@@ -195,6 +253,10 @@ MxcImageProvider::download(const QString &id,
auto data =
QByteArray(tempData.data(), (int)tempData.size());
QImage image = utils::readImage(data);
+ if (radius != 0) {
+ image = clipRadius(std::move(image), radius);
+ }
+
image.setText("original filename",
QString::fromStdString(originalFilename));
image.setText("mxc url", "mxc://" + id);
@@ -205,6 +267,10 @@ MxcImageProvider::download(const QString &id,
QImage image =
utils::readImageFromFile(fileInfo.absoluteFilePath());
+ if (radius != 0) {
+ image = clipRadius(std::move(image), radius);
+ }
+
image.setText("original filename",
QString::fromStdString(originalFilename));
image.setText("mxc url", "mxc://" + id);
diff --git a/src/MxcImageProvider.h b/src/MxcImageProvider.h
index 61d82852..6de83c0e 100644
--- a/src/MxcImageProvider.h
+++ b/src/MxcImageProvider.h
@@ -19,10 +19,11 @@ class MxcImageResponse
, public QRunnable
{
public:
- MxcImageResponse(const QString &id, bool crop, const QSize &requestedSize)
+ MxcImageResponse(const QString &id, bool crop, double radius, const QSize &requestedSize)
: m_id(id)
, m_requestedSize(requestedSize)
, m_crop(crop)
+ , m_radius(radius)
{
setAutoDelete(false);
}
@@ -39,6 +40,7 @@ public:
QSize m_requestedSize;
QImage m_image;
bool m_crop;
+ double m_radius;
};
class MxcImageProvider
@@ -54,7 +56,8 @@ public slots:
static void download(const QString &id,
const QSize &requestedSize,
std::function<void(QString, QSize, QImage, QString)> then,
- bool crop = true);
+ bool crop = true,
+ double radius = 0);
private:
QThreadPool pool;
diff --git a/src/Olm.cpp b/src/Olm.cpp
index e4ab0aa1..2c9ac5a3 100644
--- a/src/Olm.cpp
+++ b/src/Olm.cpp
@@ -425,6 +425,8 @@ handle_olm_message(const OlmMessage &msg, const UserKeyCache &otherUserDeviceKey
}
});
+ nhlog::crypto()->info("Storing secret {}",
+ secret_name->second);
cache::client()->storeSecret(secret_name->second,
e->content.secret);
@@ -1110,6 +1112,8 @@ send_encrypted_to_device_messages(const std::map<std::string, std::vector<std::s
const mtx::events::collections::DeviceEvents &event,
bool force_new_session)
{
+ static QMap<QPair<std::string, std::string>, qint64> rateLimit;
+
nlohmann::json ev_json = std::visit([](const auto &e) { return json(e); }, event);
std::map<std::string, std::vector<std::string>> keysToQuery;
@@ -1162,7 +1166,6 @@ send_encrypted_to_device_messages(const std::map<std::string, std::vector<std::s
auto session = cache::getLatestOlmSession(device_curve);
if (!session || force_new_session) {
- static QMap<QPair<std::string, std::string>, qint64> rateLimit;
auto currentTime = QDateTime::currentSecsSinceEpoch();
if (rateLimit.value(QPair(user, device)) + 60 * 60 * 10 <
currentTime) {
@@ -1318,7 +1321,8 @@ send_encrypted_to_device_messages(const std::map<std::string, std::vector<std::s
};
};
- http::client()->claim_keys(claims, BindPks(pks));
+ if (!claims.one_time_keys.empty())
+ http::client()->claim_keys(claims, BindPks(pks));
if (!keysToQuery.empty()) {
mtx::requests::QueryKeys req;
@@ -1395,9 +1399,25 @@ send_encrypted_to_device_messages(const std::map<std::string, std::vector<std::s
continue;
}
- deviceKeys[user_id].emplace(device_id, pks);
- claim_keys.one_time_keys[user.first][device_id] =
- mtx::crypto::SIGNED_CURVE25519;
+ auto currentTime = QDateTime::currentSecsSinceEpoch();
+ if (rateLimit.value(QPair(user.first, device_id.get())) +
+ 60 * 60 * 10 <
+ currentTime) {
+ deviceKeys[user_id].emplace(device_id, pks);
+ claim_keys.one_time_keys[user.first][device_id] =
+ mtx::crypto::SIGNED_CURVE25519;
+
+ rateLimit.insert(
+ QPair(user.first, device_id.get()),
+ currentTime);
+ } else {
+ nhlog::crypto()->warn(
+ "Not creating new session with {}:{} "
+ "because of rate limit",
+ user.first,
+ device_id.get());
+ continue;
+ }
nhlog::net()->info("{}", device_id.get());
nhlog::net()->info(" curve25519 {}", pks.curve25519);
@@ -1405,7 +1425,8 @@ send_encrypted_to_device_messages(const std::map<std::string, std::vector<std::s
}
}
- http::client()->claim_keys(claim_keys, BindPks(deviceKeys));
+ if (!claim_keys.one_time_keys.empty())
+ http::client()->claim_keys(claim_keys, BindPks(deviceKeys));
});
}
}
diff --git a/src/RegisterPage.cpp b/src/RegisterPage.cpp
index bae24df0..fb6a1b97 100644
--- a/src/RegisterPage.cpp
+++ b/src/RegisterPage.cpp
@@ -3,6 +3,7 @@
//
// SPDX-License-Identifier: GPL-3.0-or-later
+#include <QInputDialog>
#include <QLabel>
#include <QMetaType>
#include <QPainter>
@@ -481,6 +482,23 @@ RegisterPage::doUIA(const mtx::user_interactive::Unauthorized &unauthorized)
doRegistrationWithAuth(
mtx::user_interactive::Auth{session, mtx::user_interactive::auth::Dummy{}});
+ } else if (current_stage == mtx::user_interactive::auth_types::registration_token) {
+ bool ok;
+ QString token =
+ QInputDialog::getText(this,
+ tr("Registration token"),
+ tr("Please enter a valid registration token."),
+ QLineEdit::Normal,
+ QString(),
+ &ok);
+
+ if (ok) {
+ emit registrationWithAuth(mtx::user_interactive::Auth{
+ session,
+ mtx::user_interactive::auth::RegistrationToken{token.toStdString()}});
+ } else {
+ emit errorOccurred();
+ }
} else {
// use fallback
auto dialog = new dialogs::FallbackAuth(
diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp
index ab6ac492..f67c5e2d 100644
--- a/src/UserSettingsPage.cpp
+++ b/src/UserSettingsPage.cpp
@@ -19,7 +19,6 @@
#include <QResizeEvent>
#include <QScrollArea>
#include <QScroller>
-#include <QSettings>
#include <QSpinBox>
#include <QStandardPaths>
#include <QString>
@@ -63,7 +62,6 @@ UserSettings::initialize(std::optional<QString> profile)
void
UserSettings::load(std::optional<QString> profile)
{
- QSettings settings;
tray_ = settings.value("user/window/tray", false).toBool();
startInTray_ = settings.value("user/window/start_in_tray", false).toBool();
@@ -601,7 +599,6 @@ UserSettings::applyTheme()
void
UserSettings::save()
{
- QSettings settings;
settings.beginGroup("user");
settings.beginGroup("window");
diff --git a/src/UserSettingsPage.h b/src/UserSettingsPage.h
index 096aab81..84940e47 100644
--- a/src/UserSettingsPage.h
+++ b/src/UserSettingsPage.h
@@ -8,6 +8,7 @@
#include <QFontDatabase>
#include <QFrame>
#include <QProcessEnvironment>
+#include <QSettings>
#include <QSharedPointer>
#include <QWidget>
@@ -107,6 +108,8 @@ public:
static QSharedPointer<UserSettings> instance();
static void initialize(std::optional<QString> profile);
+ QSettings *qsettings() { return &settings; }
+
enum class Presence
{
AutomaticPresence,
@@ -316,6 +319,8 @@ private:
QString homeserver_;
QStringList hiddenTags_;
+ QSettings settings;
+
static QSharedPointer<UserSettings> instance_;
};
diff --git a/src/dialogs/ImageOverlay.cpp b/src/dialogs/ImageOverlay.cpp
index f38b29f5..12813d57 100644
--- a/src/dialogs/ImageOverlay.cpp
+++ b/src/dialogs/ImageOverlay.cpp
@@ -28,8 +28,10 @@ ImageOverlay::ImageOverlay(QPixmap image, QWidget *parent)
setAttribute(Qt::WA_TranslucentBackground, true);
setAttribute(Qt::WA_DeleteOnClose, true);
setWindowState(Qt::WindowFullScreen);
+ close_shortcut_ = new QShortcut(QKeySequence(Qt::Key_Escape), this);
- connect(this, SIGNAL(closing()), this, SLOT(close()));
+ connect(close_shortcut_, &QShortcut::activated, this, &ImageOverlay::closing);
+ connect(this, &ImageOverlay::closing, this, &ImageOverlay::close);
raise();
}
diff --git a/src/dialogs/ImageOverlay.h b/src/dialogs/ImageOverlay.h
index 93b6afdc..9d4187bf 100644
--- a/src/dialogs/ImageOverlay.h
+++ b/src/dialogs/ImageOverlay.h
@@ -8,6 +8,7 @@
#include <QDialog>
#include <QMouseEvent>
#include <QPixmap>
+#include <QShortcut>
namespace dialogs {
@@ -32,5 +33,6 @@ private:
QRect content_;
QRect close_button_;
QRect save_button_;
+ QShortcut *close_shortcut_;
};
} // dialogs
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 99e00a67..79c28edf 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -310,7 +310,7 @@ qml_mtx_events::fromRoomEventType(qml_mtx_events::EventType t)
return mtx::events::EventType::RoomMessage;
//! m.image_pack, currently im.ponies.room_emotes
case qml_mtx_events::ImagePackInRoom:
- return mtx::events::EventType::ImagePackRooms;
+ return mtx::events::EventType::ImagePackInRoom;
//! m.image_pack, currently im.ponies.user_emotes
case qml_mtx_events::ImagePackInAccountData:
return mtx::events::EventType::ImagePackInAccountData;
@@ -418,6 +418,14 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj
&events,
&EventStore::enableKeyRequests);
+ connect(this, &TimelineModel::encryptionChanged, this, &TimelineModel::trustlevelChanged);
+ connect(
+ this, &TimelineModel::roomMemberCountChanged, this, &TimelineModel::trustlevelChanged);
+ connect(cache::client(),
+ &Cache::verificationStatusChanged,
+ this,
+ &TimelineModel::trustlevelChanged);
+
showEventTimer.callOnTimeout(this, &TimelineModel::scrollTimerEvent);
}
@@ -1993,6 +2001,15 @@ TimelineModel::roomTopic() const
QString::fromStdString(info[room_id_].topic).toHtmlEscaped()));
}
+crypto::Trust
+TimelineModel::trustlevel() const
+{
+ if (!isEncrypted_)
+ return crypto::Trust::Unverified;
+
+ return cache::client()->roomVerificationStatus(room_id_.toStdString());
+}
+
int
TimelineModel::roomMemberCount() const
{
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index ad7cfbbb..aa07fe01 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -175,6 +175,7 @@ class TimelineModel : public QAbstractListModel
Q_PROPERTY(int roomMemberCount READ roomMemberCount NOTIFY roomMemberCountChanged)
Q_PROPERTY(bool isEncrypted READ isEncrypted NOTIFY encryptionChanged)
Q_PROPERTY(bool isSpace READ isSpace CONSTANT)
+ Q_PROPERTY(int trustlevel READ trustlevel NOTIFY trustlevelChanged)
Q_PROPERTY(InputBar *input READ input CONSTANT)
Q_PROPERTY(Permissions *permissions READ permissions NOTIFY permissionsChanged)
@@ -287,6 +288,7 @@ public:
DescInfo lastMessage() const { return lastMessage_; }
bool isSpace() const { return isSpace_; }
bool isEncrypted() const { return isEncrypted_; }
+ crypto::Trust trustlevel() const;
int roomMemberCount() const;
public slots:
@@ -372,6 +374,7 @@ signals:
void updateFlowEventId(std::string event_id);
void encryptionChanged();
+ void trustlevelChanged();
void roomNameChanged();
void plainRoomNameChanged();
void roomTopicChanged();
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index b23ed278..906e328f 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -375,10 +375,12 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
}
void
-TimelineViewManager::openRoomMembers(QString room_id)
+TimelineViewManager::openRoomMembers(TimelineModel *room)
{
- MemberList *memberList = new MemberList(room_id, this);
- emit openRoomMembersDialog(memberList);
+ if (!room)
+ return;
+ MemberList *memberList = new MemberList(room->roomId(), this);
+ emit openRoomMembersDialog(memberList, room);
}
void
diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h
index 54e3a935..4dd5e996 100644
--- a/src/timeline/TimelineViewManager.h
+++ b/src/timeline/TimelineViewManager.h
@@ -66,7 +66,7 @@ public:
Q_INVOKABLE QString userPresence(QString id) const;
Q_INVOKABLE QString userStatus(QString id) const;
- Q_INVOKABLE void openRoomMembers(QString room_id);
+ Q_INVOKABLE void openRoomMembers(TimelineModel *room);
Q_INVOKABLE void openRoomSettings(QString room_id);
Q_INVOKABLE void openInviteUsers(QString roomId);
Q_INVOKABLE void openGlobalUserProfile(QString userId);
@@ -92,7 +92,7 @@ signals:
void focusChanged();
void focusInput();
void openImageOverlayInternalCb(QString eventId, QImage img);
- void openRoomMembersDialog(MemberList *members);
+ void openRoomMembersDialog(MemberList *members, TimelineModel *room);
void openRoomSettingsDialog(RoomSettings *settings);
void openInviteUsersDialog(InviteesModel *invitees);
void openProfile(UserProfile *profile);
|