From 94690ebd4c22c8928b92c4f1723d1c6c5b798698 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 2 Oct 2020 01:14:42 +0200 Subject: [PATCH] Clean up verification and key cache a bit --- resources/qml/UserProfile.qml | 2 +- src/Cache.cpp | 367 ++++++++++++++++++---------- src/Cache.h | 30 +-- src/CacheCryptoStructs.h | 50 ++-- src/Cache_p.h | 32 ++- src/ChatPage.cpp | 56 +++-- src/ChatPage.h | 6 +- src/DeviceVerificationFlow.cpp | 124 +++++----- src/DeviceVerificationFlow.h | 14 +- src/timeline/.TimelineModel.cpp.swn | Bin 237568 -> 0 bytes src/ui/UserProfile.cpp | 70 ++---- 11 files changed, 416 insertions(+), 335 deletions(-) delete mode 100644 src/timeline/.TimelineModel.cpp.swn diff --git a/resources/qml/UserProfile.qml b/resources/qml/UserProfile.qml index 1ca9dcc8..dc6bc165 100644 --- a/resources/qml/UserProfile.qml +++ b/resources/qml/UserProfile.qml @@ -136,7 +136,7 @@ ApplicationWindow{ model: profile.deviceList delegate: RowLayout{ - width: parent.width + width: devicelist.width spacing: 4 ColumnLayout{ diff --git a/src/Cache.cpp b/src/Cache.cpp index 667506c5..8b47c357 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -91,6 +91,7 @@ Q_DECLARE_METATYPE(RoomMember) Q_DECLARE_METATYPE(mtx::responses::Timeline) Q_DECLARE_METATYPE(RoomSearchResult) Q_DECLARE_METATYPE(RoomInfo) +Q_DECLARE_METATYPE(mtx::responses::QueryKeys) namespace { std::unique_ptr instance_ = nullptr; @@ -155,26 +156,7 @@ Cache::Cache(const QString &userId, QObject *parent) , localUserId_{userId} { setup(); - connect( - this, - &Cache::updateUserCacheFlag, - this, - [this](const std::string &user_id) { - std::optional cache_ = getUserCache(user_id); - if (cache_.has_value()) { - cache_.value().isUpdated = false; - setUserCache(user_id, cache_.value()); - } else { - setUserCache(user_id, UserCache{}); - } - }, - Qt::QueuedConnection); - connect( - this, - &Cache::deleteLeftUsers, - this, - [this](const std::string &user_id) { deleteUserCache(user_id); }, - Qt::QueuedConnection); + connect(this, &Cache::userKeysUpdate, this, &Cache::updateUserKeys, Qt::QueuedConnection); } void @@ -1017,6 +999,8 @@ Cache::saveState(const mtx::responses::Sync &res) using namespace mtx::events; auto user_id = this->localUserId_.toStdString(); + auto currentBatchToken = nextBatchToken(); + auto txn = lmdb::txn::begin(env_); setNextBatchToken(txn, res.next_batch); @@ -1034,6 +1018,8 @@ Cache::saveState(const mtx::responses::Sync &res) ev); } + auto userKeyCacheDb = getUserKeysDb(txn); + // Save joined rooms for (const auto &room : res.rooms.join) { auto statesdb = getStatesDb(txn, room.first); @@ -1107,7 +1093,8 @@ Cache::saveState(const mtx::responses::Sync &res) savePresence(txn, res.presence); - updateUserCache(res.device_lists); + markUserKeysOutOfDate(txn, userKeyCacheDb, res.device_lists.changed, currentBatchToken); + deleteUserKeys(txn, userKeyCacheDb, res.device_lists.left); removeLeftRooms(txn, res.rooms.leave); @@ -3098,126 +3085,246 @@ Cache::statusMessage(const std::string &user_id) } void -to_json(json &j, const UserCache &info) +to_json(json &j, const UserKeyCache &info) { - j["keys"] = info.keys; - j["isUpdated"] = info.isUpdated; + j["device_keys"] = info.device_keys; + j["master_keys"] = info.master_keys; + j["user_signing_keys"] = info.user_signing_keys; + j["self_signing_keys"] = info.self_signing_keys; + j["updated_at"] = info.updated_at; + j["last_changed"] = info.last_changed; } void -from_json(const json &j, UserCache &info) +from_json(const json &j, UserKeyCache &info) { - info.keys = j.at("keys").get(); - info.isUpdated = j.at("isUpdated").get(); + info.device_keys = j.value("device_keys", std::map{}); + info.master_keys = j.value("master_keys", mtx::crypto::CrossSigningKeys{}); + info.user_signing_keys = j.value("user_signing_keys", mtx::crypto::CrossSigningKeys{}); + info.self_signing_keys = j.value("self_signing_keys", mtx::crypto::CrossSigningKeys{}); + info.updated_at = j.value("updated_at", ""); + info.last_changed = j.value("last_changed", ""); } -std::optional -Cache::getUserCache(const std::string &user_id) +std::optional +Cache::userKeys(const std::string &user_id) { - lmdb::val verifiedVal; + lmdb::val keys; - auto txn = lmdb::txn::begin(env_); - auto db = getUserCacheDb(txn); - auto res = lmdb::dbi_get(txn, db, lmdb::val(user_id), verifiedVal); + try { + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + auto db = getUserKeysDb(txn); + auto res = lmdb::dbi_get(txn, db, lmdb::val(user_id), keys); - txn.commit(); - - UserCache verified_state; - if (res) { - verified_state = json::parse(std::string(verifiedVal.data(), verifiedVal.size())); - return verified_state; - } else { + if (res) { + return json::parse(std::string_view(keys.data(), keys.size())) + .get(); + } else { + return {}; + } + } catch (std::exception &) { return {}; } } -//! be careful when using make sure is_user_verified is not changed -int -Cache::setUserCache(const std::string &user_id, const UserCache &body) +void +Cache::updateUserKeys(const std::string &sync_token, const mtx::responses::QueryKeys &keyQuery) { auto txn = lmdb::txn::begin(env_); - auto db = getUserCacheDb(txn); + auto db = getUserKeysDb(txn); - auto res = lmdb::dbi_put(txn, db, lmdb::val(user_id), lmdb::val(json(body).dump())); + std::map updates; - txn.commit(); + for (const auto &[user, keys] : keyQuery.device_keys) + updates[user].device_keys = keys; + for (const auto &[user, keys] : keyQuery.master_keys) + updates[user].master_keys = keys; + for (const auto &[user, keys] : keyQuery.user_signing_keys) + updates[user].user_signing_keys = keys; + for (const auto &[user, keys] : keyQuery.self_signing_keys) + updates[user].self_signing_keys = keys; - return res; -} + for (auto &[user, update] : updates) { + lmdb::val oldKeys; + auto res = lmdb::dbi_get(txn, db, lmdb::val(user), oldKeys); -void -Cache::updateUserCache(const mtx::responses::DeviceLists body) -{ - for (std::string user_id : body.changed) { - emit updateUserCacheFlag(user_id); + if (res) { + auto last_changed = + json::parse(std::string_view(oldKeys.data(), oldKeys.size())) + .get() + .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) + continue; + } + lmdb::dbi_put(txn, db, lmdb::val(user), lmdb::val(json(update).dump())); } - for (std::string user_id : body.left) { - emit deleteLeftUsers(user_id); - } -} - -int -Cache::deleteUserCache(const std::string &user_id) -{ - auto txn = lmdb::txn::begin(env_); - auto db = getUserCacheDb(txn); - auto res = lmdb::dbi_del(txn, db, lmdb::val(user_id), nullptr); - txn.commit(); - - return res; } void -to_json(json &j, const DeviceVerifiedCache &info) +Cache::deleteUserKeys(lmdb::txn &txn, lmdb::dbi &db, const std::vector &user_ids) { - j["is_user_verified"] = info.is_user_verified; - j["cross_verified"] = info.cross_verified; - j["device_verified"] = info.device_verified; - j["device_blocked"] = info.device_blocked; + for (const auto &user_id : user_ids) + lmdb::dbi_del(txn, db, lmdb::val(user_id), nullptr); } void -from_json(const json &j, DeviceVerifiedCache &info) +Cache::markUserKeysOutOfDate(lmdb::txn &txn, + lmdb::dbi &db, + const std::vector &user_ids, + const std::string &sync_token) { - info.is_user_verified = j.at("is_user_verified"); - info.cross_verified = j.at("cross_verified").get>(); - info.device_verified = j.at("device_verified").get>(); - info.device_blocked = j.at("device_blocked").get>(); + mtx::requests::QueryKeys query; + query.token = sync_token; + + for (const auto &user : user_ids) { + lmdb::val oldKeys; + auto res = lmdb::dbi_get(txn, db, lmdb::val(user), oldKeys); + + if (!res) + continue; + + auto cacheEntry = + json::parse(std::string_view(oldKeys.data(), oldKeys.size())).get(); + cacheEntry.last_changed = sync_token; + lmdb::dbi_put(txn, db, lmdb::val(user), lmdb::val(json(cacheEntry).dump())); + + query.device_keys[user] = {}; + } + + if (!query.device_keys.empty()) + http::client()->query_keys(query, + [this, sync_token](const mtx::responses::QueryKeys &keys, + mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn( + "failed to query device keys: {} {}", + err->matrix_error.error, + static_cast(err->status_code)); + return; + } + + emit userKeysUpdate(sync_token, keys); + }); } -std::optional -Cache::getVerifiedCache(const std::string &user_id) +void +to_json(json &j, const VerificationCache &info) +{ + j["verified_master_key"] = info.verified_master_key; + j["cross_verified"] = info.cross_verified; + j["device_verified"] = info.device_verified; + j["device_blocked"] = info.device_blocked; +} + +void +from_json(const json &j, VerificationCache &info) +{ + info.verified_master_key = j.at("verified_master_key"); + info.cross_verified = j.at("cross_verified").get>(); + info.device_verified = j.at("device_verified").get>(); + info.device_blocked = j.at("device_blocked").get>(); +} + +std::optional +Cache::verificationStatus(const std::string &user_id) { lmdb::val verifiedVal; auto txn = lmdb::txn::begin(env_); - auto db = getDeviceVerifiedDb(txn); - auto res = lmdb::dbi_get(txn, db, lmdb::val(user_id), verifiedVal); + auto db = getVerificationDb(txn); - txn.commit(); - - DeviceVerifiedCache verified_state; - if (res) { - verified_state = json::parse(std::string(verifiedVal.data(), verifiedVal.size())); - return verified_state; - } else { + try { + VerificationCache verified_state; + auto res = lmdb::dbi_get(txn, db, lmdb::val(user_id), verifiedVal); + if (res) { + verified_state = + json::parse(std::string_view(verifiedVal.data(), verifiedVal.size())); + return verified_state; + } else { + return {}; + } + } catch (std::exception &) { return {}; } } -int -Cache::setVerifiedCache(const std::string &user_id, const DeviceVerifiedCache &body) +void +Cache::markDeviceVerified(const std::string &user_id, const std::string &key) { + lmdb::val val; + auto txn = lmdb::txn::begin(env_); - auto db = getDeviceVerifiedDb(txn); + auto db = getVerificationDb(txn); - auto res = lmdb::dbi_put(txn, db, lmdb::val(user_id), lmdb::val(json(body).dump())); + try { + VerificationCache verified_state; + auto res = lmdb::dbi_get(txn, db, lmdb::val(user_id), val); + if (res) { + verified_state = json::parse(std::string_view(val.data(), val.size())); + } - txn.commit(); + for (const auto &device : verified_state.device_verified) + if (device == key) + return; - return res; + verified_state.device_verified.push_back(key); + lmdb::dbi_put(txn, db, lmdb::val(user_id), lmdb::val(json(verified_state).dump())); + txn.commit(); + } catch (std::exception &) { + } +} + +void +Cache::markDeviceUnverified(const std::string &user_id, const std::string &key) +{ + lmdb::val val; + + auto txn = lmdb::txn::begin(env_); + auto db = getVerificationDb(txn); + + try { + VerificationCache verified_state; + auto res = lmdb::dbi_get(txn, db, lmdb::val(user_id), val); + if (res) { + verified_state = json::parse(std::string_view(val.data(), val.size())); + } + + verified_state.device_verified.erase( + std::remove(verified_state.device_verified.begin(), + verified_state.device_verified.end(), + key), + verified_state.device_verified.end()); + + lmdb::dbi_put(txn, db, lmdb::val(user_id), lmdb::val(json(verified_state).dump())); + txn.commit(); + } catch (std::exception &) { + } +} + +void +Cache::markMasterKeyVerified(const std::string &user_id, const std::string &key) +{ + lmdb::val val; + + auto txn = lmdb::txn::begin(env_); + auto db = getVerificationDb(txn); + + try { + VerificationCache verified_state; + auto res = lmdb::dbi_get(txn, db, lmdb::val(user_id), val); + if (res) { + verified_state = json::parse(std::string_view(val.data(), val.size())); + } + + verified_state.verified_master_key = key; + lmdb::dbi_put(txn, db, lmdb::val(user_id), lmdb::val(json(verified_state).dump())); + txn.commit(); + } catch (std::exception &) { + } } void @@ -3401,41 +3508,6 @@ statusMessage(const std::string &user_id) { return instance_->statusMessage(user_id); } -std::optional -getUserCache(const std::string &user_id) -{ - return instance_->getUserCache(user_id); -} - -void -updateUserCache(const mtx::responses::DeviceLists body) -{ - instance_->updateUserCache(body); -} - -int -setUserCache(const std::string &user_id, const UserCache &body) -{ - return instance_->setUserCache(user_id, body); -} - -int -deleteUserCache(const std::string &user_id) -{ - return instance_->deleteUserCache(user_id); -} - -std::optional -getVerifiedCache(const std::string &user_id) -{ - return instance_->getVerifiedCache(user_id); -} - -int -setVerifiedCache(const std::string &user_id, const DeviceVerifiedCache &body) -{ - return instance_->setVerifiedCache(user_id, body); -} //! Load saved data for the display names & avatars. void @@ -3444,6 +3516,43 @@ populateMembers() instance_->populateMembers(); } +// user cache stores user keys +std::optional +userKeys(const std::string &user_id) +{ + return instance_->userKeys(user_id); +} +void +updateUserKeys(const std::string &sync_token, const mtx::responses::QueryKeys &keyQuery) +{ + instance_->updateUserKeys(sync_token, keyQuery); +} + +// device & user verification cache +std::optional +verificationStatus(const std::string &user_id) +{ + return instance_->verificationStatus(user_id); +} + +void +markDeviceVerified(const std::string &user_id, const std::string &key) +{ + instance_->markDeviceVerified(user_id, key); +} + +void +markDeviceUnverified(const std::string &user_id, const std::string &key) +{ + instance_->markDeviceUnverified(user_id, key); +} + +void +markMasterKeyVerified(const std::string &user_id, const std::string &key) +{ + instance_->markMasterKeyVerified(user_id, key); +} + std::vector joinedRooms() { diff --git a/src/Cache.h b/src/Cache.h index 82d909ae..edad5993 100644 --- a/src/Cache.h +++ b/src/Cache.h @@ -60,25 +60,21 @@ presenceState(const std::string &user_id); std::string statusMessage(const std::string &user_id); -//! user Cache -std::optional -getUserCache(const std::string &user_id); - +// user cache stores user keys +std::optional +userKeys(const std::string &user_id); void -updateUserCache(const mtx::responses::DeviceLists body); +updateUserKeys(const std::string &sync_token, const mtx::responses::QueryKeys &keyQuery); -int -setUserCache(const std::string &user_id, const UserCache &body); - -int -deleteUserCache(const std::string &user_id); - -//! verified Cache -std::optional -getVerifiedCache(const std::string &user_id); - -int -setVerifiedCache(const std::string &user_id, const DeviceVerifiedCache &body); +// device & user verification cache +std::optional +verificationStatus(const std::string &user_id); +void +markDeviceVerified(const std::string &user_id, const std::string &key); +void +markDeviceUnverified(const std::string &user_id, const std::string &key); +void +markMasterKeyVerified(const std::string &user_id, const std::string &key); //! Load saved data for the display names & avatars. void diff --git a/src/CacheCryptoStructs.h b/src/CacheCryptoStructs.h index 1dde21ce..10636ac6 100644 --- a/src/CacheCryptoStructs.h +++ b/src/CacheCryptoStructs.h @@ -67,52 +67,38 @@ struct OlmSessionStorage }; // this will store the keys of the user with whom a encrypted room is shared with -struct UserCache +struct UserKeyCache { - //! map of public key key_ids and their public_key - mtx::responses::QueryKeys keys; - //! if the current cache is updated or not - bool isUpdated = false; - - UserCache(mtx::responses::QueryKeys res, bool isUpdated_ = false) - : keys(res) - , isUpdated(isUpdated_) - {} - UserCache() {} + //! Device id to device keys + std::map device_keys; + //! corss signing keys + mtx::crypto::CrossSigningKeys master_keys, user_signing_keys, self_signing_keys; + //! Sync token when nheko last fetched the keys + std::string updated_at; + //! Sync token when the keys last changed. updated != last_changed means they are outdated. + std::string last_changed; }; void -to_json(nlohmann::json &j, const UserCache &info); +to_json(nlohmann::json &j, const UserKeyCache &info); void -from_json(const nlohmann::json &j, UserCache &info); +from_json(const nlohmann::json &j, UserKeyCache &info); // the reason these are stored in a seperate cache rather than storing it in the user cache is -// UserCache stores only keys of users with which encrypted room is shared -struct DeviceVerifiedCache +// UserKeyCache stores only keys of users with which encrypted room is shared +struct VerificationCache { //! list of verified device_ids with device-verification std::vector device_verified; - //! list of verified device_ids with cross-signing + //! list of verified device_ids with cross-signing, calculated from master key std::vector cross_verified; //! list of devices the user blocks std::vector device_blocked; - //! this stores if the user is verified (with cross-signing) - bool is_user_verified = false; - - DeviceVerifiedCache(std::vector device_verified_, - std::vector cross_verified_, - std::vector device_blocked_, - bool is_user_verified_ = false) - : device_verified(device_verified_) - , cross_verified(cross_verified_) - , device_blocked(device_blocked_) - , is_user_verified(is_user_verified_) - {} - - DeviceVerifiedCache() {} + //! The verified master key. + std::string verified_master_key; }; void -to_json(nlohmann::json &j, const DeviceVerifiedCache &info); +to_json(nlohmann::json &j, const VerificationCache &info); void -from_json(const nlohmann::json &j, DeviceVerifiedCache &info); +from_json(const nlohmann::json &j, VerificationCache &info); diff --git a/src/Cache_p.h b/src/Cache_p.h index ce6414ab..034c6d76 100644 --- a/src/Cache_p.h +++ b/src/Cache_p.h @@ -55,14 +55,22 @@ public: std::string statusMessage(const std::string &user_id); // user cache stores user keys - std::optional getUserCache(const std::string &user_id); - void updateUserCache(const mtx::responses::DeviceLists body); - int setUserCache(const std::string &user_id, const UserCache &body); - int deleteUserCache(const std::string &user_id); + std::optional userKeys(const std::string &user_id); + void updateUserKeys(const std::string &sync_token, + const mtx::responses::QueryKeys &keyQuery); + void markUserKeysOutOfDate(lmdb::txn &txn, + lmdb::dbi &db, + const std::vector &user_ids, + const std::string &sync_token); + void deleteUserKeys(lmdb::txn &txn, + lmdb::dbi &db, + const std::vector &user_ids); - // device verified cache - std::optional getVerifiedCache(const std::string &user_id); - int setVerifiedCache(const std::string &user_id, const DeviceVerifiedCache &body); + // device & user verification cache + std::optional verificationStatus(const std::string &user_id); + void markDeviceVerified(const std::string &user_id, const std::string &key); + void markDeviceUnverified(const std::string &user_id, const std::string &key); + void markMasterKeyVerified(const std::string &user_id, const std::string &key); static void removeDisplayName(const QString &room_id, const QString &user_id); static void removeAvatarUrl(const QString &room_id, const QString &user_id); @@ -272,8 +280,8 @@ signals: void newReadReceipts(const QString &room_id, const std::vector &event_ids); void roomReadStatus(const std::map &status); void removeNotification(const QString &room_id, const QString &event_id); - void updateUserCacheFlag(const std::string &user_id); - void deleteLeftUsers(const std::string &user_id); + void userKeysUpdate(const std::string &sync_token, + const mtx::responses::QueryKeys &keyQuery); private: //! Save an invited room. @@ -539,12 +547,12 @@ private: return lmdb::dbi::open(txn, "presence", MDB_CREATE); } - lmdb::dbi getUserCacheDb(lmdb::txn &txn) + lmdb::dbi getUserKeysDb(lmdb::txn &txn) { - return lmdb::dbi::open(txn, "user_cache", MDB_CREATE); + return lmdb::dbi::open(txn, "user_key", MDB_CREATE); } - lmdb::dbi getDeviceVerifiedDb(lmdb::txn &txn) + lmdb::dbi getVerificationDb(lmdb::txn &txn) { return lmdb::dbi::open(txn, "verified", MDB_CREATE); } diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index c6978a59..6abe4078 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -1466,35 +1466,43 @@ ChatPage::initiateLogout() } void -ChatPage::query_keys( - const mtx::requests::QueryKeys &req, - std::function cb) +ChatPage::query_keys(const std::string &user_id, + std::function cb) { - std::string user_id = req.device_keys.begin()->first; - auto cache_ = cache::getUserCache(user_id); + auto cache_ = cache::userKeys(user_id); if (cache_.has_value()) { - if (cache_.value().isUpdated) { - cb(cache_.value().keys, {}); - } else { - http::client()->query_keys( - req, - [cb, user_id](const mtx::responses::QueryKeys &res, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to query device keys: {},{}", - err->matrix_error.errcode, - static_cast(err->status_code)); - return; - } - cache::setUserCache(std::move(user_id), - std::move(UserCache{res, true})); - cb(res, err); - }); + if (!cache_->updated_at.empty() && cache_->updated_at == cache_->last_changed) { + cb(cache_.value(), {}); + return; } - } else { - http::client()->query_keys(req, cb); } + + mtx::requests::QueryKeys req; + req.device_keys[user_id] = {}; + + std::string last_changed; + if (cache_) + last_changed = cache_->last_changed; + req.token = last_changed; + + http::client()->query_keys(req, + [cb, user_id, last_changed](const mtx::responses::QueryKeys &res, + mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn( + "failed to query device keys: {},{}", + err->matrix_error.errcode, + static_cast(err->status_code)); + cb({}, err); + return; + } + + cache::updateUserKeys(last_changed, res); + + auto keys = cache::userKeys(user_id); + cb(keys.value_or(UserKeyCache{}), err); + }); } template diff --git a/src/ChatPage.h b/src/ChatPage.h index 9d8abb24..f363c4fe 100644 --- a/src/ChatPage.h +++ b/src/ChatPage.h @@ -35,6 +35,7 @@ #include #include +#include "CacheCryptoStructs.h" #include "CacheStructs.h" #include "CallManager.h" #include "CommunitiesList.h" @@ -89,9 +90,8 @@ public: //! Show the room/group list (if it was visible). void showSideBars(); void initiateLogout(); - void query_keys( - const mtx::requests::QueryKeys &req, - std::function cb); + void query_keys(const std::string &req, + std::function cb); void focusMessageInput(); QString status() const; diff --git a/src/DeviceVerificationFlow.cpp b/src/DeviceVerificationFlow.cpp index 96fed55a..aa8b5b44 100644 --- a/src/DeviceVerificationFlow.cpp +++ b/src/DeviceVerificationFlow.cpp @@ -328,22 +328,14 @@ DeviceVerificationFlow::setTransactionId(QString transaction_id_) void DeviceVerificationFlow::setUserId(QString userID) { - this->userId = userID; - this->toClient = mtx::identifiers::parse(userID.toStdString()); - auto user_cache = cache::getUserCache(userID.toStdString()); + this->userId = userID; + this->toClient = mtx::identifiers::parse(userID.toStdString()); - if (user_cache.has_value()) { - this->callback_fn(user_cache->keys, {}, userID.toStdString()); - } else { - mtx::requests::QueryKeys req; - req.device_keys[userID.toStdString()] = {}; - http::client()->query_keys( - req, - [user_id = userID.toStdString(), this](const mtx::responses::QueryKeys &res, - mtx::http::RequestErr err) { - this->callback_fn(res, err, user_id); - }); - } + auto user_id = userID.toStdString(); + ChatPage::instance()->query_keys( + user_id, [user_id, this](const UserKeyCache &res, mtx::http::RequestErr err) { + this->callback_fn(res, err, user_id); + }); } void @@ -622,30 +614,52 @@ DeviceVerificationFlow::sendVerificationKey() (model_)->sendMessageEvent(req, mtx::events::EventType::KeyVerificationKey); } } + +mtx::events::msg::KeyVerificationMac +key_verification_mac(mtx::crypto::SAS *sas, + mtx::identifiers::User sender, + const std::string &senderDevice, + mtx::identifiers::User receiver, + const std::string &receiverDevice, + const std::string &transactionId, + std::map keys) +{ + mtx::events::msg::KeyVerificationMac req; + + std::string info = "MATRIX_KEY_VERIFICATION_MAC" + sender.to_string() + senderDevice + + receiver.to_string() + receiverDevice + transactionId; + + std::string key_list; + bool first = true; + for (const auto &[key_id, key] : keys) { + req.mac[key_id] = sas->calculate_mac(key, info + key_id); + + if (!first) + key_list += ","; + key_list += key_id; + first = false; + } + + req.keys = sas->calculate_mac(key_list, info + "KEY_IDS"); + + return req; +} + //! sends the mac of the keys void DeviceVerificationFlow::sendVerificationMac() { - mtx::events::msg::KeyVerificationMac req; + std::map key_list; + key_list["ed25519:" + http::client()->device_id()] = olm::client()->identity_keys().ed25519; - std::string info = "MATRIX_KEY_VERIFICATION_MAC" + http::client()->user_id().to_string() + - http::client()->device_id() + this->toClient.to_string() + - this->deviceId.toStdString() + this->transaction_id; - - //! this vector stores the type of the key and the key - std::vector> key_list; - key_list.push_back(make_pair("ed25519", olm::client()->identity_keys().ed25519)); - std::sort(key_list.begin(), key_list.end()); - for (auto x : key_list) { - req.mac.insert( - std::make_pair(x.first + ":" + http::client()->device_id(), - this->sas->calculate_mac( - x.second, info + x.first + ":" + http::client()->device_id()))); - req.keys += x.first + ":" + http::client()->device_id() + ","; - } - - req.keys = - this->sas->calculate_mac(req.keys.substr(0, req.keys.size() - 1), info + "KEY_IDS"); + mtx::events::msg::KeyVerificationMac req = + key_verification_mac(sas.get(), + http::client()->user_id(), + http::client()->device_id(), + this->toClient, + this->deviceId.toStdString(), + this->transaction_id, + key_list); if (this->type == DeviceVerificationFlow::Type::ToDevice) { mtx::requests::ToDeviceMessages body; @@ -673,27 +687,16 @@ DeviceVerificationFlow::sendVerificationMac() void DeviceVerificationFlow::acceptDevice() { - auto verified_cache = cache::getVerifiedCache(this->userId.toStdString()); - if (verified_cache.has_value()) { - verified_cache->device_verified.push_back(this->deviceId.toStdString()); - verified_cache->device_blocked.erase( - std::remove(verified_cache->device_blocked.begin(), - verified_cache->device_blocked.end(), - this->deviceId.toStdString()), - verified_cache->device_blocked.end()); - } else { - cache::setVerifiedCache( - this->userId.toStdString(), - DeviceVerifiedCache{{this->deviceId.toStdString()}, {}, {}}); - } + cache::markDeviceVerified(this->userId.toStdString(), this->deviceId.toStdString()); emit deviceVerified(); emit refreshProfile(); this->deleteLater(); } + //! callback function to keep track of devices void -DeviceVerificationFlow::callback_fn(const mtx::responses::QueryKeys &res, +DeviceVerificationFlow::callback_fn(const UserKeyCache &res, mtx::http::RequestErr err, std::string user_id) { @@ -704,35 +707,22 @@ DeviceVerificationFlow::callback_fn(const mtx::responses::QueryKeys &res, return; } - if (res.device_keys.empty() || (res.device_keys.find(user_id) == res.device_keys.end())) { + if (res.device_keys.empty() || + (res.device_keys.find(deviceId.toStdString()) == res.device_keys.end())) { nhlog::net()->warn("no devices retrieved {}", user_id); return; } - for (auto x : res.device_keys) { - for (auto y : x.second) { - auto z = y.second; - if (z.user_id == user_id && z.device_id == this->deviceId.toStdString()) { - for (auto a : z.keys) { - // TODO: Verify Signatures - this->device_keys[a.first] = a.second; - } - } - } + for (const auto &[algorithm, key] : res.device_keys.at(deviceId.toStdString()).keys) { + // TODO: Verify Signatures + this->device_keys[algorithm] = key; } } void DeviceVerificationFlow::unverify() { - auto verified_cache = cache::getVerifiedCache(this->userId.toStdString()); - if (verified_cache.has_value()) { - auto it = std::remove(verified_cache->device_verified.begin(), - verified_cache->device_verified.end(), - this->deviceId.toStdString()); - verified_cache->device_verified.erase(it); - cache::setVerifiedCache(this->userId.toStdString(), verified_cache.value()); - } + cache::markDeviceUnverified(this->userId.toStdString(), this->deviceId.toStdString()); emit refreshProfile(); } diff --git a/src/DeviceVerificationFlow.h b/src/DeviceVerificationFlow.h index b85cbec2..31d2facc 100644 --- a/src/DeviceVerificationFlow.h +++ b/src/DeviceVerificationFlow.h @@ -1,11 +1,13 @@ #pragma once -#include "Olm.h" - -#include "MatrixClient.h" -#include "mtx/responses/crypto.hpp" #include +#include + +#include "CacheCryptoStructs.h" +#include "MatrixClient.h" +#include "Olm.h" + class QTimer; using sas_ptr = std::unique_ptr; @@ -71,9 +73,7 @@ public: void setSender(bool sender_); void setEventId(std::string event_id); - void callback_fn(const mtx::responses::QueryKeys &res, - mtx::http::RequestErr err, - std::string user_id); + void callback_fn(const UserKeyCache &res, mtx::http::RequestErr err, std::string user_id); nlohmann::json canonical_json; diff --git a/src/timeline/.TimelineModel.cpp.swn b/src/timeline/.TimelineModel.cpp.swn deleted file mode 100644 index 9e96570264bc9cbab5e9b87ccf0e1880e0db3eea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 237568 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

`E(A&g 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 F{|AftumJ!7 diff --git a/src/ui/UserProfile.cpp b/src/ui/UserProfile.cpp index 08c30097..2ea3f7b6 100644 --- a/src/ui/UserProfile.cpp +++ b/src/ui/UserProfile.cpp @@ -89,12 +89,10 @@ UserProfile::fetchDeviceList(const QString &userID) { auto localUser = utils::localUser(); - mtx::requests::QueryKeys req; - req.device_keys[userID.toStdString()] = {}; ChatPage::instance()->query_keys( - req, - [user_id = userID.toStdString(), this](const mtx::responses::QueryKeys &res, - mtx::http::RequestErr err) { + userID.toStdString(), + [other_user_id = userID.toStdString(), this](const UserKeyCache &other_user_keys, + mtx::http::RequestErr err) { if (err) { nhlog::net()->warn("failed to query device keys: {},{}", err->matrix_error.errcode, @@ -102,20 +100,11 @@ UserProfile::fetchDeviceList(const QString &userID) return; } - if (res.device_keys.empty() || - (res.device_keys.find(user_id) == res.device_keys.end())) { - nhlog::net()->warn("no devices retrieved {}", user_id); - return; - } - // Finding if the User is Verified or not based on the Signatures - mtx::requests::QueryKeys req; - req.device_keys[utils::localUser().toStdString()] = {}; - ChatPage::instance()->query_keys( - req, - [user_id, other_res = res, this](const mtx::responses::QueryKeys &res, - mtx::http::RequestErr err) { + utils::localUser().toStdString(), + [other_user_id, other_user_keys, this](const UserKeyCache &res, + mtx::http::RequestErr err) { using namespace mtx; std::string local_user_id = utils::localUser().toStdString(); @@ -126,34 +115,28 @@ UserProfile::fetchDeviceList(const QString &userID) return; } - if (res.device_keys.empty() || - (res.device_keys.find(local_user_id) == res.device_keys.end())) { - nhlog::net()->warn("no devices retrieved {}", user_id); + if (res.device_keys.empty()) { + nhlog::net()->warn("no devices retrieved {}", local_user_id); return; } std::vector deviceInfo; - auto devices = other_res.device_keys.at(user_id); - auto device_verified = cache::getVerifiedCache(user_id); + auto devices = other_user_keys.device_keys; + auto device_verified = cache::verificationStatus(other_user_id); if (device_verified.has_value()) { - isUserVerified = device_verified.value().is_user_verified; + // TODO: properly check cross-signing signatures here + isUserVerified = !device_verified->verified_master_key.empty(); } std::optional lmk, lsk, luk, mk, sk, uk; - if (!res.master_keys.empty()) - lmk = res.master_keys.at(local_user_id); - if (!res.user_signing_keys.empty()) - luk = res.user_signing_keys.at(local_user_id); - if (!res.self_signing_keys.empty()) - lsk = res.self_signing_keys.at(local_user_id); - if (!other_res.master_keys.empty()) - mk = other_res.master_keys.at(user_id); - if (!other_res.user_signing_keys.empty()) - uk = other_res.user_signing_keys.at(user_id); - if (!other_res.self_signing_keys.empty()) - sk = other_res.self_signing_keys.at(user_id); + lmk = res.master_keys; + luk = res.user_signing_keys; + lsk = res.self_signing_keys; + mk = other_user_keys.master_keys; + uk = other_user_keys.user_signing_keys; + sk = other_user_keys.self_signing_keys; // First checking if the user is verified if (luk.has_value() && mk.has_value()) { @@ -202,7 +185,7 @@ UserProfile::fetchDeviceList(const QString &userID) device_verified->device_blocked.end()) verified = verification::Status::BLOCKED; } else if (isUserVerified) { - device_verified = DeviceVerifiedCache{}; + device_verified = VerificationCache{}; } // won't check for already verified devices @@ -211,7 +194,7 @@ UserProfile::fetchDeviceList(const QString &userID) if ((sk.has_value()) && (!device.signatures.empty())) { for (auto sign_key : sk.value().keys) { auto signs = - device.signatures.at(user_id); + device.signatures.at(other_user_id); try { if (olm::client() ->ed25519_verify_sig( @@ -232,12 +215,13 @@ UserProfile::fetchDeviceList(const QString &userID) } } - if (device_verified.has_value()) { - device_verified.value().is_user_verified = - isUserVerified; - cache::setVerifiedCache(user_id, - device_verified.value()); - } + // TODO(Nico): properly show cross-signing + // if (device_verified.has_value()) { + // device_verified.value().is_user_verified = + // isUserVerified; + // cache::setVerifiedCache(user_id, + // device_verified.value()); + //} deviceInfo.push_back( {QString::fromStdString(d.first),