summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorJoseph Donofry <joedonofry@gmail.com>2021-08-16 17:16:17 -0400
committerJoseph Donofry <joedonofry@gmail.com>2021-08-16 17:16:17 -0400
commit093f9f9e338f7898a2e3e3ed5673952639015cab (patch)
tree4117da7ddaf1bb2c2e3dbd8b51277343142f799c /src
parentMerge origin/master and fix conflicts (diff)
parentUpdate qt5 path in macos deploy.sh script (diff)
downloadnheko-093f9f9e338f7898a2e3e3ed5673952639015cab.tar.xz
Merge remote-tracking branch 'nheko-im/master' into video_player_enhancements
Diffstat (limited to 'src')
-rw-r--r--src/Cache.cpp388
-rw-r--r--src/CacheCryptoStructs.h10
-rw-r--r--src/Cache_p.h45
-rw-r--r--src/ChatPage.cpp2
-rw-r--r--src/ChatPage.h2
-rw-r--r--src/MemberList.cpp12
-rw-r--r--src/MemberList.h1
-rw-r--r--src/MxcImageProvider.cpp96
-rw-r--r--src/MxcImageProvider.h7
-rw-r--r--src/Olm.cpp33
-rw-r--r--src/RegisterPage.cpp18
-rw-r--r--src/UserSettingsPage.cpp3
-rw-r--r--src/UserSettingsPage.h5
-rw-r--r--src/dialogs/ImageOverlay.cpp4
-rw-r--r--src/dialogs/ImageOverlay.h2
-rw-r--r--src/timeline/TimelineModel.cpp19
-rw-r--r--src/timeline/TimelineModel.h3
-rw-r--r--src/timeline/TimelineViewManager.cpp8
-rw-r--r--src/timeline/TimelineViewManager.h4
19 files changed, 494 insertions, 168 deletions
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);