summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorDeepBlueV7.X <nicolas.werner@hotmail.de>2020-10-08 20:08:38 +0200
committerGitHub <noreply@github.com>2020-10-08 20:08:38 +0200
commit517a126a4427972668a97b21fc8684f7160976b9 (patch)
tree27142d719f848abe685897cd367e8d69a9bed719 /src
parentMerge pull request #294 from trilene/master (diff)
parentTry to fix windows build (diff)
downloadnheko-517a126a4427972668a97b21fc8684f7160976b9.tar.xz
Merge pull request #270 from Chethan2k1/device-verification
Device verification and Cross-Signing
Diffstat (limited to 'src')
-rw-r--r--src/Cache.cpp446
-rw-r--r--src/Cache.h16
-rw-r--r--src/CacheCryptoStructs.h50
-rw-r--r--src/Cache_p.h34
-rw-r--r--src/ChatPage.cpp45
-rw-r--r--src/ChatPage.h24
-rw-r--r--src/DeviceVerificationFlow.cpp794
-rw-r--r--src/DeviceVerificationFlow.h235
-rw-r--r--src/EventAccessors.cpp22
-rw-r--r--src/MainWindow.cpp9
-rw-r--r--src/MainWindow.h2
-rw-r--r--src/Olm.cpp211
-rw-r--r--src/Olm.h10
-rw-r--r--src/TextInputWidget.cpp2
-rw-r--r--src/TextInputWidget.h1
-rw-r--r--src/WebRTCSession.cpp1
-rw-r--r--src/dialogs/AcceptCall.cpp1
-rw-r--r--src/dialogs/AcceptCall.h1
-rw-r--r--src/dialogs/PlaceCall.cpp1
-rw-r--r--src/dialogs/PlaceCall.h1
-rw-r--r--src/emoji/EmojiModel.h1
-rw-r--r--src/emoji/EmojiSearchModel.h1
-rw-r--r--src/timeline/EventStore.cpp170
-rw-r--r--src/timeline/EventStore.h4
-rw-r--r--src/timeline/TimelineModel.cpp333
-rw-r--r--src/timeline/TimelineModel.h31
-rw-r--r--src/timeline/TimelineViewManager.cpp156
-rw-r--r--src/timeline/TimelineViewManager.h16
-rw-r--r--src/ui/UserProfile.cpp227
-rw-r--r--src/ui/UserProfile.h123
30 files changed, 2748 insertions, 220 deletions
diff --git a/src/Cache.cpp b/src/Cache.cpp

index 98fe64c0..08b6f155 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp
@@ -31,8 +31,10 @@ #include "Cache.h" #include "Cache_p.h" +#include "ChatPage.h" #include "EventAccessors.h" #include "Logging.h" +#include "MatrixClient.h" #include "Olm.h" #include "Utils.h" @@ -89,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<Cache> instance_ = nullptr; @@ -153,6 +156,7 @@ Cache::Cache(const QString &userId, QObject *parent) , localUserId_{userId} { setup(); + connect(this, &Cache::userKeysUpdate, this, &Cache::updateUserKeys, Qt::QueuedConnection); } void @@ -369,6 +373,25 @@ Cache::updateOutboundMegolmSession(const std::string &room_id, int message_index } void +Cache::dropOutboundMegolmSession(const std::string &room_id) +{ + using namespace mtx::crypto; + + if (!outboundMegolmSessionExists(room_id)) + return; + + { + std::unique_lock<std::mutex> lock(session_storage.group_outbound_mtx); + session_storage.group_outbound_session_data.erase(room_id); + session_storage.group_outbound_sessions.erase(room_id); + + auto txn = lmdb::txn::begin(env_); + lmdb::dbi_del(txn, outboundMegolmSessionDb_, lmdb::val(room_id), nullptr); + txn.commit(); + } +} + +void Cache::saveOutboundMegolmSession(const std::string &room_id, const OutboundGroupSessionData &data, mtx::crypto::OutboundGroupSessionPtr session) @@ -683,11 +706,14 @@ Cache::nextBatchToken() const auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); lmdb::val token; - lmdb::dbi_get(txn, syncStateDb_, NEXT_BATCH_KEY, token); + auto result = lmdb::dbi_get(txn, syncStateDb_, NEXT_BATCH_KEY, token); txn.commit(); - return std::string(token.data(), token.size()); + if (result) + return std::string(token.data(), token.size()); + else + return ""; } void @@ -995,6 +1021,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); @@ -1012,6 +1040,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); @@ -1085,6 +1115,9 @@ Cache::saveState(const mtx::responses::Sync &res) savePresence(txn, res.presence); + markUserKeysOutOfDate(txn, userKeyCacheDb, res.device_lists.changed, currentBatchToken); + deleteUserKeys(txn, userKeyCacheDb, res.device_lists.left); + removeLeftRooms(txn, res.rooms.leave); txn.commit(); @@ -3074,6 +3107,378 @@ Cache::statusMessage(const std::string &user_id) } void +to_json(json &j, const UserKeyCache &info) +{ + 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, UserKeyCache &info) +{ + info.device_keys = j.value("device_keys", std::map<std::string, mtx::crypto::DeviceKeys>{}); + 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<UserKeyCache> +Cache::userKeys(const std::string &user_id) +{ + lmdb::val keys; + + 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); + + if (res) { + return json::parse(std::string_view(keys.data(), keys.size())) + .get<UserKeyCache>(); + } else { + return {}; + } + } catch (std::exception &) { + return {}; + } +} + +void +Cache::updateUserKeys(const std::string &sync_token, const mtx::responses::QueryKeys &keyQuery) +{ + auto txn = lmdb::txn::begin(env_); + auto db = getUserKeysDb(txn); + + std::map<std::string, UserKeyCache> updates; + + 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; + + for (auto &[user, update] : updates) { + lmdb::val oldKeys; + auto res = lmdb::dbi_get(txn, db, lmdb::val(user), oldKeys); + + if (res) { + auto last_changed = + json::parse(std::string_view(oldKeys.data(), oldKeys.size())) + .get<UserKeyCache>() + .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())); + } + + txn.commit(); + + std::map<std::string, VerificationStatus> tmp; + const auto local_user = utils::localUser().toStdString(); + + { + std::unique_lock<std::mutex> lock(verification_storage.verification_storage_mtx); + for (auto &[user_id, update] : updates) { + (void)update; + if (user_id == local_user) { + std::swap(tmp, verification_storage.status); + } else { + verification_storage.status.erase(user_id); + } + } + } + for (auto &[user_id, update] : updates) { + (void)update; + if (user_id == local_user) { + for (const auto &[user, status] : tmp) { + (void)status; + emit verificationStatusChanged(user); + } + } else { + emit verificationStatusChanged(user_id); + } + } +} + +void +Cache::deleteUserKeys(lmdb::txn &txn, lmdb::dbi &db, const std::vector<std::string> &user_ids) +{ + for (const auto &user_id : user_ids) + lmdb::dbi_del(txn, db, lmdb::val(user_id), nullptr); +} + +void +Cache::markUserKeysOutOfDate(lmdb::txn &txn, + lmdb::dbi &db, + const std::vector<std::string> &user_ids, + const std::string &sync_token) +{ + 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<UserKeyCache>(); + 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<int>(err->status_code)); + return; + } + + emit userKeysUpdate(sync_token, keys); + }); +} + +void +to_json(json &j, const VerificationCache &info) +{ + j["device_verified"] = info.device_verified; + j["device_blocked"] = info.device_blocked; +} + +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>>(); +} + +std::optional<VerificationCache> +Cache::verificationCache(const std::string &user_id) +{ + lmdb::val verifiedVal; + + 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), verifiedVal); + if (res) { + verified_state = + json::parse(std::string_view(verifiedVal.data(), verifiedVal.size())); + return verified_state; + } else { + return {}; + } + } catch (std::exception &) { + return {}; + } +} + +void +Cache::markDeviceVerified(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())); + } + + for (const auto &device : verified_state.device_verified) + if (device == key) + return; + + 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 &) { + } + + const auto local_user = utils::localUser().toStdString(); + std::map<std::string, VerificationStatus> tmp; + { + std::unique_lock<std::mutex> lock(verification_storage.verification_storage_mtx); + if (user_id == local_user) { + std::swap(tmp, verification_storage.status); + } else { + verification_storage.status.erase(user_id); + } + } + if (user_id == local_user) { + for (const auto &[user, status] : tmp) { + (void)status; + emit verificationStatusChanged(user); + } + } else { + emit verificationStatusChanged(user_id); + } +} + +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 &) { + } + + const auto local_user = utils::localUser().toStdString(); + std::map<std::string, VerificationStatus> tmp; + { + std::unique_lock<std::mutex> lock(verification_storage.verification_storage_mtx); + if (user_id == local_user) { + std::swap(tmp, verification_storage.status); + } else { + verification_storage.status.erase(user_id); + } + } + if (user_id == local_user) { + for (const auto &[user, status] : tmp) { + (void)status; + emit verificationStatusChanged(user); + } + } else { + emit verificationStatusChanged(user_id); + } +} + +VerificationStatus +Cache::verificationStatus(const std::string &user_id) +{ + 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)) { + status.verified_devices = verifCache->device_verified; + } + + const auto local_user = utils::localUser().toStdString(); + + if (user_id == local_user) + status.verified_devices.push_back(http::client()->device_id()); + + verification_storage.status[user_id] = status; + + auto verifyAtLeastOneSig = [](const auto &toVerif, + const std::map<std::string, std::string> &keys, + const std::string &keyOwner) { + if (!toVerif.signatures.count(keyOwner)) + return false; + + for (const auto &[key_id, signature] : toVerif.signatures.at(keyOwner)) { + if (!keys.count(key_id)) + continue; + + if (mtx::crypto::ed25519_verify_signature( + keys.at(key_id), json(toVerif), signature)) + return true; + } + return false; + }; + + try { + // for local user verify this device_key -> our master_key -> our self_signing_key + // -> our device_keys + // + // for other user verify this device_key -> our master_key -> our user_signing_key + // -> their master_key -> their self_signing_key -> their device_keys + // + // 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) + return status; + + 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()))) + return status; + + auto master_keys = ourKeys->master_keys.keys; + + if (user_id != local_user) { + if (!verifyAtLeastOneSig( + ourKeys->user_signing_keys, master_keys, local_user)) + return status; + + if (!verifyAtLeastOneSig( + theirKeys->master_keys, ourKeys->user_signing_keys.keys, local_user)) + return status; + + master_keys = theirKeys->master_keys.keys; + } + + status.user_verified = true; + + if (!verifyAtLeastOneSig(theirKeys->self_signing_keys, master_keys, user_id)) + return status; + + for (const auto &[device, device_key] : theirKeys->device_keys) { + (void)device; + if (verifyAtLeastOneSig( + device_key, theirKeys->self_signing_keys.keys, user_id)) + status.verified_devices.push_back(device_key.device_id); + } + + verification_storage.status[user_id] = status; + return status; + } catch (std::exception &) { + return status; + } +} + +void to_json(json &j, const RoomInfo &info) { j["name"] = info.name; @@ -3195,6 +3600,7 @@ init(const QString &user_id) qRegisterMetaType<QMap<QString, RoomInfo>>(); qRegisterMetaType<std::map<QString, RoomInfo>>(); qRegisterMetaType<std::map<QString, mtx::responses::Timeline>>(); + qRegisterMetaType<mtx::responses::QueryKeys>(); instance_ = std::make_unique<Cache>(user_id); } @@ -3262,6 +3668,37 @@ populateMembers() instance_->populateMembers(); } +// user cache stores user keys +std::optional<UserKeyCache> +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> +verificationStatus(const std::string &user_id) +{ + return instance_->verificationStatus(user_id); +} + +void +markDeviceVerified(const std::string &user_id, const std::string &device) +{ + instance_->markDeviceVerified(user_id, device); +} + +void +markDeviceUnverified(const std::string &user_id, const std::string &device) +{ + instance_->markDeviceUnverified(user_id, device); +} + std::vector<std::string> joinedRooms() { @@ -3595,6 +4032,11 @@ updateOutboundMegolmSession(const std::string &room_id, int message_index) { instance_->updateOutboundMegolmSession(room_id, message_index); } +void +dropOutboundMegolmSession(const std::string &room_id) +{ + instance_->dropOutboundMegolmSession(room_id); +} void importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys) diff --git a/src/Cache.h b/src/Cache.h
index b5275623..cd96708e 100644 --- a/src/Cache.h +++ b/src/Cache.h
@@ -60,6 +60,20 @@ presenceState(const std::string &user_id); std::string statusMessage(const std::string &user_id); +// user cache stores user keys +std::optional<UserKeyCache> +userKeys(const std::string &user_id); +void +updateUserKeys(const std::string &sync_token, const mtx::responses::QueryKeys &keyQuery); + +// device & user verification cache +std::optional<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); + //! Load saved data for the display names & avatars. void populateMembers(); @@ -255,6 +269,8 @@ bool outboundMegolmSessionExists(const std::string &room_id) noexcept; void updateOutboundMegolmSession(const std::string &room_id, int message_index); +void +dropOutboundMegolmSession(const std::string &room_id); void importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys); diff --git a/src/CacheCryptoStructs.h b/src/CacheCryptoStructs.h
index 14c9c86b..935d6493 100644 --- a/src/CacheCryptoStructs.h +++ b/src/CacheCryptoStructs.h
@@ -65,3 +65,53 @@ struct OlmSessionStorage std::mutex group_outbound_mtx; std::mutex group_inbound_mtx; }; + +//! Verification status of a single user +struct VerificationStatus +{ + //! True, if the users master key is verified + bool user_verified = false; + //! List of all devices marked as verified + std::vector<std::string> verified_devices; +}; + +//! In memory cache of verification status +struct VerificationStorage +{ + //! mapping of user to verification status + std::map<std::string, VerificationStatus> status; + std::mutex verification_storage_mtx; +}; + +// this will store the keys of the user with whom a encrypted room is shared with +struct UserKeyCache +{ + //! Device id to device keys + std::map<std::string, mtx::crypto::DeviceKeys> 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 UserKeyCache &info); +void +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 +// UserKeyCache stores only keys of users with which encrypted room is shared +struct VerificationCache +{ + //! list of verified device_ids with device-verification + std::vector<std::string> device_verified; + //! list of devices the user blocks + std::vector<std::string> device_blocked; +}; + +void +to_json(nlohmann::json &j, const VerificationCache &info); +void +from_json(const nlohmann::json &j, VerificationCache &info); diff --git a/src/Cache_p.h b/src/Cache_p.h
index c57995a2..b3f4c58c 100644 --- a/src/Cache_p.h +++ b/src/Cache_p.h
@@ -54,6 +54,23 @@ public: mtx::presence::PresenceState presenceState(const std::string &user_id); std::string statusMessage(const std::string &user_id); + // user cache stores user keys + std::optional<UserKeyCache> 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<std::string> &user_ids, + const std::string &sync_token); + void deleteUserKeys(lmdb::txn &txn, + lmdb::dbi &db, + const std::vector<std::string> &user_ids); + + // device & user verification cache + 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); + static void removeDisplayName(const QString &room_id, const QString &user_id); static void removeAvatarUrl(const QString &room_id, const QString &user_id); @@ -233,6 +250,7 @@ public: OutboundGroupSessionDataRef getOutboundMegolmSession(const std::string &room_id); bool outboundMegolmSessionExists(const std::string &room_id) noexcept; void updateOutboundMegolmSession(const std::string &room_id, int message_index); + void dropOutboundMegolmSession(const std::string &room_id); void importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys); mtx::crypto::ExportedSessionKeys exportSessionKeys(); @@ -262,6 +280,9 @@ signals: void newReadReceipts(const QString &room_id, const std::vector<QString> &event_ids); void roomReadStatus(const std::map<QString, bool> &status); void removeNotification(const QString &room_id, const QString &event_id); + void userKeysUpdate(const std::string &sync_token, + const mtx::responses::QueryKeys &keyQuery); + void verificationStatusChanged(const std::string &userid); private: //! Save an invited room. @@ -527,6 +548,16 @@ private: return lmdb::dbi::open(txn, "presence", MDB_CREATE); } + lmdb::dbi getUserKeysDb(lmdb::txn &txn) + { + return lmdb::dbi::open(txn, "user_key", MDB_CREATE); + } + + lmdb::dbi getVerificationDb(lmdb::txn &txn) + { + return lmdb::dbi::open(txn, "verified", MDB_CREATE); + } + //! Retrieves or creates the database that stores the open OLM sessions between our device //! and the given curve25519 key which represents another device. //! @@ -545,6 +576,8 @@ private: return QString::fromStdString(event.state_key); } + std::optional<VerificationCache> verificationCache(const std::string &user_id); + void setNextBatchToken(lmdb::txn &txn, const std::string &token); void setNextBatchToken(lmdb::txn &txn, const QString &token); @@ -569,6 +602,7 @@ private: static QHash<QString, QString> AvatarUrls; OlmSessionStorage session_storage; + VerificationStorage verification_storage; }; namespace cache { diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index 59ae7ef2..8e93c0f4 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp
@@ -26,6 +26,7 @@ #include "Cache.h" #include "Cache_p.h" #include "ChatPage.h" +#include "DeviceVerificationFlow.h" #include "EventAccessors.h" #include "Logging.h" #include "MainWindow.h" @@ -159,6 +160,10 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) view_manager_, &TimelineViewManager::clearCurrentRoomTimeline); + connect(text_input_, &TextInputWidget::rotateMegolmSession, this, [this]() { + cache::dropOutboundMegolmSession(current_room_.toStdString()); + }); + connect( new QShortcut(QKeySequence("Ctrl+Down"), this), &QShortcut::activated, this, [this]() { if (isVisible()) @@ -1456,6 +1461,46 @@ ChatPage::initiateLogout() emit showOverlayProgressBar(); } +void +ChatPage::query_keys(const std::string &user_id, + std::function<void(const UserKeyCache &, mtx::http::RequestErr)> cb) +{ + auto cache_ = cache::userKeys(user_id); + + if (cache_.has_value()) { + if (!cache_->updated_at.empty() && cache_->updated_at == cache_->last_changed) { + cb(cache_.value(), {}); + return; + } + } + + 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<int>(err->status_code)); + cb({}, err); + return; + } + + cache::updateUserKeys(last_changed, res); + + auto keys = cache::userKeys(user_id); + cb(keys.value_or(UserKeyCache{}), err); + }); +} + template<typename T> void ChatPage::connectCallMessage() diff --git a/src/ChatPage.h b/src/ChatPage.h
index 6b3916a2..f0e12ab5 100644 --- a/src/ChatPage.h +++ b/src/ChatPage.h
@@ -19,6 +19,7 @@ #include <atomic> #include <optional> +#include <stack> #include <variant> #include <mtx/common.hpp> @@ -34,6 +35,7 @@ #include <QTimer> #include <QWidget> +#include "CacheCryptoStructs.h" #include "CacheStructs.h" #include "CallManager.h" #include "CommunitiesList.h" @@ -50,6 +52,8 @@ class TextInputWidget; class TimelineViewManager; class UserInfoWidget; class UserSettings; +class NotificationsManager; +class TimelineModel; constexpr int CONSENSUS_TIMEOUT = 1000; constexpr int SHOW_CONTENT_TIMEOUT = 3000; @@ -85,6 +89,8 @@ public: //! Show the room/group list (if it was visible). void showSideBars(); void initiateLogout(); + void query_keys(const std::string &req, + std::function<void(const UserKeyCache &, mtx::http::RequestErr)> cb); void focusMessageInput(); QString status() const; @@ -161,6 +167,24 @@ signals: void themeChanged(); void decryptSidebarChanged(); + //! Signals for device verificaiton + void receivedDeviceVerificationAccept( + const mtx::events::msg::KeyVerificationAccept &message); + void receivedDeviceVerificationRequest( + const mtx::events::msg::KeyVerificationRequest &message, + std::string sender); + void receivedRoomDeviceVerificationRequest( + const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> &message, + TimelineModel *model); + void receivedDeviceVerificationCancel( + const mtx::events::msg::KeyVerificationCancel &message); + void receivedDeviceVerificationKey(const mtx::events::msg::KeyVerificationKey &message); + void receivedDeviceVerificationMac(const mtx::events::msg::KeyVerificationMac &message); + void receivedDeviceVerificationStart(const mtx::events::msg::KeyVerificationStart &message, + std::string sender); + void receivedDeviceVerificationReady(const mtx::events::msg::KeyVerificationReady &message); + void receivedDeviceVerificationDone(const mtx::events::msg::KeyVerificationDone &message); + private slots: void showUnreadMessageNotification(int count); void logout(); diff --git a/src/DeviceVerificationFlow.cpp b/src/DeviceVerificationFlow.cpp new file mode 100644
index 00000000..aa1a9607 --- /dev/null +++ b/src/DeviceVerificationFlow.cpp
@@ -0,0 +1,794 @@ +#include "DeviceVerificationFlow.h" + +#include "Cache.h" +#include "ChatPage.h" +#include "Logging.h" +#include "timeline/TimelineModel.h" + +#include <QDateTime> +#include <QTimer> +#include <iostream> + +static constexpr int TIMEOUT = 2 * 60 * 1000; // 2 minutes + +namespace msgs = mtx::events::msg; + +static 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<std::string, std::string> keys); + +DeviceVerificationFlow::DeviceVerificationFlow(QObject *, + DeviceVerificationFlow::Type flow_type, + TimelineModel *model, + QString userID, + QString deviceId_) + : sender(false) + , type(flow_type) + , deviceId(deviceId_) + , model_(model) +{ + timeout = new QTimer(this); + timeout->setSingleShot(true); + this->sas = olm::client()->sas_init(); + this->isMacVerified = false; + + auto user_id = userID.toStdString(); + this->toClient = mtx::identifiers::parse<mtx::identifiers::User>(user_id); + ChatPage::instance()->query_keys( + user_id, [user_id, this](const UserKeyCache &res, mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to query device keys: {},{}", + err->matrix_error.errcode, + static_cast<int>(err->status_code)); + return; + } + + if (!this->deviceId.isEmpty() && + (res.device_keys.find(deviceId.toStdString()) == res.device_keys.end())) { + nhlog::net()->warn("no devices retrieved {}", user_id); + return; + } + + this->their_keys = res; + }); + + ChatPage::instance()->query_keys( + http::client()->user_id().to_string(), + [this](const UserKeyCache &res, mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to query device keys: {},{}", + err->matrix_error.errcode, + static_cast<int>(err->status_code)); + return; + } + + if (res.master_keys.keys.empty()) + return; + + if (auto status = + cache::verificationStatus(http::client()->user_id().to_string()); + status && status->user_verified) + this->our_trusted_master_key = res.master_keys.keys.begin()->second; + }); + + if (model) { + connect(this->model_, + &TimelineModel::updateFlowEventId, + this, + [this](std::string event_id_) { + this->relation.rel_type = mtx::common::RelationType::Reference; + this->relation.event_id = event_id_; + this->transaction_id = event_id_; + }); + } + + connect(timeout, &QTimer::timeout, this, [this]() { + if (state_ != Success && state_ != Failed) + this->cancelVerification(DeviceVerificationFlow::Error::Timeout); + }); + + connect(ChatPage::instance(), + &ChatPage::receivedDeviceVerificationStart, + this, + &DeviceVerificationFlow::handleStartMessage); + connect(ChatPage::instance(), + &ChatPage::receivedDeviceVerificationAccept, + this, + [this](const mtx::events::msg::KeyVerificationAccept &msg) { + if (msg.transaction_id.has_value()) { + if (msg.transaction_id.value() != this->transaction_id) + return; + } else if (msg.relates_to.has_value()) { + if (msg.relates_to.value().event_id != this->relation.event_id) + return; + } + if ((msg.key_agreement_protocol == "curve25519-hkdf-sha256") && + (msg.hash == "sha256") && + (msg.message_authentication_code == "hkdf-hmac-sha256")) { + this->commitment = msg.commitment; + if (std::find(msg.short_authentication_string.begin(), + msg.short_authentication_string.end(), + mtx::events::msg::SASMethods::Emoji) != + msg.short_authentication_string.end()) { + this->method = mtx::events::msg::SASMethods::Emoji; + } else { + this->method = mtx::events::msg::SASMethods::Decimal; + } + this->mac_method = msg.message_authentication_code; + this->sendVerificationKey(); + } else { + this->cancelVerification( + DeviceVerificationFlow::Error::UnknownMethod); + } + }); + + connect(ChatPage::instance(), + &ChatPage::receivedDeviceVerificationCancel, + this, + [this](const mtx::events::msg::KeyVerificationCancel &msg) { + if (msg.transaction_id.has_value()) { + if (msg.transaction_id.value() != this->transaction_id) + return; + } else if (msg.relates_to.has_value()) { + if (msg.relates_to.value().event_id != this->relation.event_id) + return; + } + error_ = User; + emit errorChanged(); + setState(Failed); + }); + + connect(ChatPage::instance(), + &ChatPage::receivedDeviceVerificationKey, + this, + [this](const mtx::events::msg::KeyVerificationKey &msg) { + if (msg.transaction_id.has_value()) { + if (msg.transaction_id.value() != this->transaction_id) + return; + } else if (msg.relates_to.has_value()) { + if (msg.relates_to.value().event_id != this->relation.event_id) + return; + } + + if (sender) { + if (state_ != WaitingForOtherToAccept) { + this->cancelVerification(OutOfOrder); + return; + } + } else { + if (state_ != WaitingForKeys) { + this->cancelVerification(OutOfOrder); + return; + } + } + + this->sas->set_their_key(msg.key); + std::string info; + if (this->sender == true) { + info = "MATRIX_KEY_VERIFICATION_SAS|" + + http::client()->user_id().to_string() + "|" + + http::client()->device_id() + "|" + this->sas->public_key() + + "|" + this->toClient.to_string() + "|" + + this->deviceId.toStdString() + "|" + msg.key + "|" + + this->transaction_id; + } else { + info = "MATRIX_KEY_VERIFICATION_SAS|" + this->toClient.to_string() + + "|" + this->deviceId.toStdString() + "|" + msg.key + "|" + + http::client()->user_id().to_string() + "|" + + http::client()->device_id() + "|" + this->sas->public_key() + + "|" + this->transaction_id; + } + + nhlog::ui()->info("Info is: '{}'", info); + + if (this->sender == false) { + this->sendVerificationKey(); + } else { + if (this->commitment != + mtx::crypto::bin2base64_unpadded( + mtx::crypto::sha256(msg.key + this->canonical_json.dump()))) { + this->cancelVerification( + DeviceVerificationFlow::Error::MismatchedCommitment); + return; + } + } + + if (this->method == mtx::events::msg::SASMethods::Emoji) { + this->sasList = this->sas->generate_bytes_emoji(info); + setState(CompareEmoji); + } else if (this->method == mtx::events::msg::SASMethods::Decimal) { + this->sasList = this->sas->generate_bytes_decimal(info); + setState(CompareNumber); + } + }); + + connect( + ChatPage::instance(), + &ChatPage::receivedDeviceVerificationMac, + this, + [this](const mtx::events::msg::KeyVerificationMac &msg) { + if (msg.transaction_id.has_value()) { + if (msg.transaction_id.value() != this->transaction_id) + return; + } else if (msg.relates_to.has_value()) { + if (msg.relates_to.value().event_id != this->relation.event_id) + return; + } + + std::map<std::string, std::string> key_list; + std::string key_string; + for (const auto &mac : msg.mac) { + for (const auto &[deviceid, key] : their_keys.device_keys) { + (void)deviceid; + if (key.keys.count(mac.first)) + key_list[mac.first] = key.keys.at(mac.first); + } + + if (their_keys.master_keys.keys.count(mac.first)) + key_list[mac.first] = their_keys.master_keys.keys[mac.first]; + if (their_keys.user_signing_keys.keys.count(mac.first)) + key_list[mac.first] = + their_keys.user_signing_keys.keys[mac.first]; + if (their_keys.self_signing_keys.keys.count(mac.first)) + key_list[mac.first] = + their_keys.self_signing_keys.keys[mac.first]; + } + auto macs = key_verification_mac(sas.get(), + toClient, + this->deviceId.toStdString(), + http::client()->user_id(), + http::client()->device_id(), + this->transaction_id, + key_list); + + for (const auto &[key, mac] : macs.mac) { + if (mac != msg.mac.at(key)) { + this->cancelVerification( + DeviceVerificationFlow::Error::KeyMismatch); + return; + } + } + + if (msg.keys == macs.keys) { + mtx::requests::KeySignaturesUpload req; + if (utils::localUser().toStdString() == this->toClient.to_string()) { + // self verification, sign master key with device key, if we + // verified it + for (const auto &mac : msg.mac) { + if (their_keys.master_keys.keys.count(mac.first)) { + json j = their_keys.master_keys; + j.erase("signatures"); + j.erase("unsigned"); + mtx::crypto::CrossSigningKeys master_key = j; + master_key + .signatures[utils::localUser().toStdString()] + ["ed25519:" + + http::client()->device_id()] = + olm::client()->sign_message(j.dump()); + req.signatures[utils::localUser().toStdString()] + [master_key.keys.at(mac.first)] = + master_key; + } + } + // TODO(Nico): Sign their device key with self signing key + } else { + // TODO(Nico): Sign their master key with user signing key + } + + if (!req.signatures.empty()) { + http::client()->keys_signatures_upload( + req, + [](const mtx::responses::KeySignaturesUpload &res, + mtx::http::RequestErr err) { + if (err) { + nhlog::net()->error( + "failed to upload signatures: {},{}", + err->matrix_error.errcode, + static_cast<int>(err->status_code)); + } + + for (const auto &[user_id, tmp] : res.errors) + for (const auto &[key_id, e] : tmp) + nhlog::net()->error( + "signature error for user {} and key " + "id {}: {}, {}", + user_id, + key_id, + e.errcode, + e.error); + }); + } + + this->isMacVerified = true; + this->acceptDevice(); + } else { + this->cancelVerification(DeviceVerificationFlow::Error::KeyMismatch); + } + }); + + connect(ChatPage::instance(), + &ChatPage::receivedDeviceVerificationReady, + this, + [this](const mtx::events::msg::KeyVerificationReady &msg) { + if (!sender) { + if (msg.from_device != http::client()->device_id()) { + error_ = User; + emit errorChanged(); + setState(Failed); + } + + return; + } + + if (msg.transaction_id.has_value()) { + if (msg.transaction_id.value() != this->transaction_id) + return; + } else if ((msg.relates_to.has_value() && sender)) { + if (msg.relates_to.value().event_id != this->relation.event_id) + return; + else { + this->deviceId = QString::fromStdString(msg.from_device); + } + } + this->startVerificationRequest(); + }); + + connect(ChatPage::instance(), + &ChatPage::receivedDeviceVerificationDone, + this, + [this](const mtx::events::msg::KeyVerificationDone &msg) { + if (msg.transaction_id.has_value()) { + if (msg.transaction_id.value() != this->transaction_id) + return; + } else if (msg.relates_to.has_value()) { + if (msg.relates_to.value().event_id != this->relation.event_id) + return; + } + nhlog::ui()->info("Flow done on other side"); + }); + + timeout->start(TIMEOUT); +} + +QString +DeviceVerificationFlow::state() +{ + switch (state_) { + case PromptStartVerification: + return "PromptStartVerification"; + case CompareEmoji: + return "CompareEmoji"; + case CompareNumber: + return "CompareNumber"; + case WaitingForKeys: + return "WaitingForKeys"; + case WaitingForOtherToAccept: + return "WaitingForOtherToAccept"; + case WaitingForMac: + return "WaitingForMac"; + case Success: + return "Success"; + case Failed: + return "Failed"; + default: + return ""; + } +} + +void +DeviceVerificationFlow::next() +{ + if (sender) { + switch (state_) { + case PromptStartVerification: + sendVerificationRequest(); + break; + case CompareEmoji: + case CompareNumber: + sendVerificationMac(); + break; + case WaitingForKeys: + case WaitingForOtherToAccept: + case WaitingForMac: + case Success: + case Failed: + nhlog::db()->error("verification: Invalid state transition!"); + break; + } + } else { + switch (state_) { + case PromptStartVerification: + if (canonical_json.is_null()) + sendVerificationReady(); + else // legacy path without request and ready + acceptVerificationRequest(); + break; + case CompareEmoji: + [[fallthrough]]; + case CompareNumber: + sendVerificationMac(); + break; + case WaitingForKeys: + case WaitingForOtherToAccept: + case WaitingForMac: + case Success: + case Failed: + nhlog::db()->error("verification: Invalid state transition!"); + break; + } + } +} + +QString +DeviceVerificationFlow::getUserId() +{ + return QString::fromStdString(this->toClient.to_string()); +} + +QString +DeviceVerificationFlow::getDeviceId() +{ + return this->deviceId; +} + +bool +DeviceVerificationFlow::getSender() +{ + return this->sender; +} + +std::vector<int> +DeviceVerificationFlow::getSasList() +{ + return this->sasList; +} + +void +DeviceVerificationFlow::setEventId(std::string event_id_) +{ + this->relation.rel_type = mtx::common::RelationType::Reference; + this->relation.event_id = event_id_; + this->transaction_id = event_id_; +} + +void +DeviceVerificationFlow::handleStartMessage(const mtx::events::msg::KeyVerificationStart &msg, + std::string) +{ + if (msg.transaction_id.has_value()) { + if (msg.transaction_id.value() != this->transaction_id) + return; + } else if (msg.relates_to.has_value()) { + if (msg.relates_to.value().event_id != this->relation.event_id) + return; + } + if ((std::find(msg.key_agreement_protocols.begin(), + msg.key_agreement_protocols.end(), + "curve25519-hkdf-sha256") != msg.key_agreement_protocols.end()) && + (std::find(msg.hashes.begin(), msg.hashes.end(), "sha256") != msg.hashes.end()) && + (std::find(msg.message_authentication_codes.begin(), + msg.message_authentication_codes.end(), + "hkdf-hmac-sha256") != msg.message_authentication_codes.end())) { + if (std::find(msg.short_authentication_string.begin(), + msg.short_authentication_string.end(), + mtx::events::msg::SASMethods::Emoji) != + msg.short_authentication_string.end()) { + this->method = mtx::events::msg::SASMethods::Emoji; + } else if (std::find(msg.short_authentication_string.begin(), + msg.short_authentication_string.end(), + mtx::events::msg::SASMethods::Decimal) != + msg.short_authentication_string.end()) { + this->method = mtx::events::msg::SASMethods::Decimal; + } else { + this->cancelVerification(DeviceVerificationFlow::Error::UnknownMethod); + return; + } + if (!sender) + this->canonical_json = nlohmann::json(msg); + else { + if (utils::localUser().toStdString() < this->toClient.to_string()) { + this->canonical_json = nlohmann::json(msg); + } + } + + if (state_ != PromptStartVerification) + this->acceptVerificationRequest(); + } else { + this->cancelVerification(DeviceVerificationFlow::Error::UnknownMethod); + } +} + +//! accepts a verification +void +DeviceVerificationFlow::acceptVerificationRequest() +{ + mtx::events::msg::KeyVerificationAccept req; + + req.method = mtx::events::msg::VerificationMethods::SASv1; + req.key_agreement_protocol = "curve25519-hkdf-sha256"; + req.hash = "sha256"; + req.message_authentication_code = "hkdf-hmac-sha256"; + if (this->method == mtx::events::msg::SASMethods::Emoji) + req.short_authentication_string = {mtx::events::msg::SASMethods::Emoji}; + else if (this->method == mtx::events::msg::SASMethods::Decimal) + req.short_authentication_string = {mtx::events::msg::SASMethods::Decimal}; + req.commitment = mtx::crypto::bin2base64_unpadded( + mtx::crypto::sha256(this->sas->public_key() + this->canonical_json.dump())); + + send(req); + setState(WaitingForKeys); +} +//! responds verification request +void +DeviceVerificationFlow::sendVerificationReady() +{ + mtx::events::msg::KeyVerificationReady req; + + req.from_device = http::client()->device_id(); + req.methods = {mtx::events::msg::VerificationMethods::SASv1}; + + send(req); + setState(WaitingForKeys); +} +//! accepts a verification +void +DeviceVerificationFlow::sendVerificationDone() +{ + mtx::events::msg::KeyVerificationDone req; + + send(req); +} +//! starts the verification flow +void +DeviceVerificationFlow::startVerificationRequest() +{ + mtx::events::msg::KeyVerificationStart req; + + req.from_device = http::client()->device_id(); + req.method = mtx::events::msg::VerificationMethods::SASv1; + req.key_agreement_protocols = {"curve25519-hkdf-sha256"}; + req.hashes = {"sha256"}; + req.message_authentication_codes = {"hkdf-hmac-sha256"}; + req.short_authentication_string = {mtx::events::msg::SASMethods::Decimal, + mtx::events::msg::SASMethods::Emoji}; + + if (this->type == DeviceVerificationFlow::Type::ToDevice) { + mtx::requests::ToDeviceMessages<mtx::events::msg::KeyVerificationStart> body; + req.transaction_id = this->transaction_id; + this->canonical_json = nlohmann::json(req); + } else if (this->type == DeviceVerificationFlow::Type::RoomMsg && model_) { + req.relates_to = this->relation; + this->canonical_json = nlohmann::json(req); + } + send(req); + setState(WaitingForOtherToAccept); +} +//! sends a verification request +void +DeviceVerificationFlow::sendVerificationRequest() +{ + mtx::events::msg::KeyVerificationRequest req; + + req.from_device = http::client()->device_id(); + req.methods = {mtx::events::msg::VerificationMethods::SASv1}; + + if (this->type == DeviceVerificationFlow::Type::ToDevice) { + QDateTime currentTime = QDateTime::currentDateTimeUtc(); + + req.timestamp = (uint64_t)currentTime.toMSecsSinceEpoch(); + + } else if (this->type == DeviceVerificationFlow::Type::RoomMsg && model_) { + req.to = this->toClient.to_string(); + req.msgtype = "m.key.verification.request"; + req.body = "User is requesting to verify keys with you. However, your client does " + "not support this method, so you will need to use the legacy method of " + "key verification."; + } + + send(req); + setState(WaitingForOtherToAccept); +} +//! cancels a verification flow +void +DeviceVerificationFlow::cancelVerification(DeviceVerificationFlow::Error error_code) +{ + mtx::events::msg::KeyVerificationCancel req; + + if (error_code == DeviceVerificationFlow::Error::UnknownMethod) { + req.code = "m.unknown_method"; + req.reason = "unknown method received"; + } else if (error_code == DeviceVerificationFlow::Error::MismatchedCommitment) { + req.code = "m.mismatched_commitment"; + req.reason = "commitment didn't match"; + } else if (error_code == DeviceVerificationFlow::Error::MismatchedSAS) { + req.code = "m.mismatched_sas"; + req.reason = "sas didn't match"; + } else if (error_code == DeviceVerificationFlow::Error::KeyMismatch) { + req.code = "m.key_match"; + req.reason = "keys did not match"; + } else if (error_code == DeviceVerificationFlow::Error::Timeout) { + req.code = "m.timeout"; + req.reason = "timed out"; + } else if (error_code == DeviceVerificationFlow::Error::User) { + req.code = "m.user"; + req.reason = "user cancelled the verification"; + } else if (error_code == DeviceVerificationFlow::Error::OutOfOrder) { + req.code = "m.unexpected_message"; + req.reason = "received messages out of order"; + } + + this->error_ = error_code; + emit errorChanged(); + this->setState(Failed); + + send(req); +} +//! sends the verification key +void +DeviceVerificationFlow::sendVerificationKey() +{ + mtx::events::msg::KeyVerificationKey req; + + req.key = this->sas->public_key(); + + send(req); +} + +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<std::string, std::string> 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() +{ + std::map<std::string, std::string> key_list; + key_list["ed25519:" + http::client()->device_id()] = olm::client()->identity_keys().ed25519; + + // send our master key, if we trust it + if (!this->our_trusted_master_key.empty()) + key_list["ed25519:" + our_trusted_master_key] = our_trusted_master_key; + + 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); + + send(req); + + setState(WaitingForMac); + acceptDevice(); +} +//! Completes the verification flow +void +DeviceVerificationFlow::acceptDevice() +{ + if (!isMacVerified) { + setState(WaitingForMac); + } else if (state_ == WaitingForMac) { + cache::markDeviceVerified(this->toClient.to_string(), this->deviceId.toStdString()); + this->sendVerificationDone(); + setState(Success); + } +} + +void +DeviceVerificationFlow::unverify() +{ + cache::markDeviceUnverified(this->toClient.to_string(), this->deviceId.toStdString()); + + emit refreshProfile(); +} + +QSharedPointer<DeviceVerificationFlow> +DeviceVerificationFlow::NewInRoomVerification(QObject *parent_, + TimelineModel *timelineModel_, + const mtx::events::msg::KeyVerificationRequest &msg, + QString other_user_, + QString event_id_) +{ + QSharedPointer<DeviceVerificationFlow> flow( + new DeviceVerificationFlow(parent_, + Type::RoomMsg, + timelineModel_, + other_user_, + QString::fromStdString(msg.from_device))); + + flow->setEventId(event_id_.toStdString()); + + if (std::find(msg.methods.begin(), + msg.methods.end(), + mtx::events::msg::VerificationMethods::SASv1) == msg.methods.end()) { + flow->cancelVerification(UnknownMethod); + } + + return flow; +} +QSharedPointer<DeviceVerificationFlow> +DeviceVerificationFlow::NewToDeviceVerification(QObject *parent_, + const mtx::events::msg::KeyVerificationRequest &msg, + QString other_user_, + QString txn_id_) +{ + QSharedPointer<DeviceVerificationFlow> flow(new DeviceVerificationFlow( + parent_, Type::ToDevice, nullptr, other_user_, QString::fromStdString(msg.from_device))); + flow->transaction_id = txn_id_.toStdString(); + + if (std::find(msg.methods.begin(), + msg.methods.end(), + mtx::events::msg::VerificationMethods::SASv1) == msg.methods.end()) { + flow->cancelVerification(UnknownMethod); + } + + return flow; +} +QSharedPointer<DeviceVerificationFlow> +DeviceVerificationFlow::NewToDeviceVerification(QObject *parent_, + const mtx::events::msg::KeyVerificationStart &msg, + QString other_user_, + QString txn_id_) +{ + QSharedPointer<DeviceVerificationFlow> flow(new DeviceVerificationFlow( + parent_, Type::ToDevice, nullptr, other_user_, QString::fromStdString(msg.from_device))); + flow->transaction_id = txn_id_.toStdString(); + + flow->handleStartMessage(msg, ""); + + return flow; +} +QSharedPointer<DeviceVerificationFlow> +DeviceVerificationFlow::InitiateUserVerification(QObject *parent_, + TimelineModel *timelineModel_, + QString userid) +{ + QSharedPointer<DeviceVerificationFlow> flow( + new DeviceVerificationFlow(parent_, Type::RoomMsg, timelineModel_, userid, "")); + flow->sender = true; + return flow; +} +QSharedPointer<DeviceVerificationFlow> +DeviceVerificationFlow::InitiateDeviceVerification(QObject *parent_, QString userid, QString device) +{ + QSharedPointer<DeviceVerificationFlow> flow( + new DeviceVerificationFlow(parent_, Type::ToDevice, nullptr, userid, device)); + + flow->sender = true; + flow->transaction_id = http::client()->generate_txn_id(); + + return flow; +} diff --git a/src/DeviceVerificationFlow.h b/src/DeviceVerificationFlow.h new file mode 100644
index 00000000..70b5d9b3 --- /dev/null +++ b/src/DeviceVerificationFlow.h
@@ -0,0 +1,235 @@ +#pragma once + +#include <QObject> + +#include <mtx/responses/crypto.hpp> + +#include "CacheCryptoStructs.h" +#include "Logging.h" +#include "MatrixClient.h" +#include "Olm.h" +#include "timeline/TimelineModel.h" + +class QTimer; + +using sas_ptr = std::unique_ptr<mtx::crypto::SAS>; + +// clang-format off +/* + * Stolen from fluffy chat :D + * + * State | +-------------+ +-----------+ | + * | | AliceDevice | | BobDevice | | + * | | (sender) | | | | + * | +-------------+ +-----------+ | + * promptStartVerify | | | | + * | o | (m.key.verification.request) | | + * | p |-------------------------------->| (ASK FOR VERIFICATION REQUEST) | + * waitForOtherAccept | t | | | promptStartVerify + * && | i | (m.key.verification.ready) | | + * no commitment | o |<--------------------------------| | + * && | n | | | + * no canonical_json | a | (m.key.verification.start) | | waitingForKeys + * | l |<--------------------------------| Not sending to prevent the glare resolve| && no commitment + * | | | | && no canonical_json + * | | m.key.verification.start | | + * waitForOtherAccept | |-------------------------------->| (IF NOT ALREADY ASKED, | + * && | | | ASK FOR VERIFICATION REQUEST) | promptStartVerify, if not accepted + * canonical_json | | m.key.verification.accept | | + * | |<--------------------------------| | + * waitForOtherAccept | | | | waitingForKeys + * && | | m.key.verification.key | | && canonical_json + * commitment | |-------------------------------->| | && commitment + * | | | | + * | | m.key.verification.key | | + * | |<--------------------------------| | + * compareEmoji/Number| | | | compareEmoji/Number + * | | COMPARE EMOJI / NUMBERS | | + * | | | | + * waitingForMac | | m.key.verification.mac | | waitingForMac + * | success |<------------------------------->| success | + * | | | | + * success/fail | | m.key.verification.done | | success/fail + * | |<------------------------------->| | + */ +// clang-format on +class DeviceVerificationFlow : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString state READ state NOTIFY stateChanged) + Q_PROPERTY(Error error READ error NOTIFY errorChanged) + Q_PROPERTY(QString userId READ getUserId CONSTANT) + Q_PROPERTY(QString deviceId READ getDeviceId CONSTANT) + Q_PROPERTY(bool sender READ getSender CONSTANT) + Q_PROPERTY(std::vector<int> sasList READ getSasList CONSTANT) + +public: + enum State + { + PromptStartVerification, + WaitingForOtherToAccept, + WaitingForKeys, + CompareEmoji, + CompareNumber, + WaitingForMac, + Success, + Failed, + }; + Q_ENUM(State) + + enum Type + { + ToDevice, + RoomMsg + }; + + enum Error + { + UnknownMethod, + MismatchedCommitment, + MismatchedSAS, + KeyMismatch, + Timeout, + User, + OutOfOrder, + }; + Q_ENUM(Error) + + static QSharedPointer<DeviceVerificationFlow> NewInRoomVerification( + QObject *parent_, + TimelineModel *timelineModel_, + const mtx::events::msg::KeyVerificationRequest &msg, + QString other_user_, + QString event_id_); + static QSharedPointer<DeviceVerificationFlow> NewToDeviceVerification( + QObject *parent_, + const mtx::events::msg::KeyVerificationRequest &msg, + QString other_user_, + QString txn_id_); + static QSharedPointer<DeviceVerificationFlow> NewToDeviceVerification( + QObject *parent_, + const mtx::events::msg::KeyVerificationStart &msg, + QString other_user_, + QString txn_id_); + static QSharedPointer<DeviceVerificationFlow> + InitiateUserVerification(QObject *parent_, TimelineModel *timelineModel_, QString userid); + static QSharedPointer<DeviceVerificationFlow> InitiateDeviceVerification(QObject *parent, + QString userid, + QString device); + + // getters + QString state(); + Error error() { return error_; } + QString getUserId(); + QString getDeviceId(); + bool getSender(); + std::vector<int> getSasList(); + QString transactionId() { return QString::fromStdString(this->transaction_id); } + // setters + void setDeviceId(QString deviceID); + void setEventId(std::string event_id); + + void callback_fn(const UserKeyCache &res, mtx::http::RequestErr err, std::string user_id); + +public slots: + //! unverifies a device + void unverify(); + //! Continues the flow + void next(); + //! Cancel the flow + void cancel() { cancelVerification(User); } + +signals: + void refreshProfile(); + void stateChanged(); + void errorChanged(); + +private: + DeviceVerificationFlow(QObject *, + DeviceVerificationFlow::Type flow_type, + TimelineModel *model, + QString userID, + QString deviceId_); + void setState(State state) + { + if (state != state_) { + state_ = state; + emit stateChanged(); + } + } + + void handleStartMessage(const mtx::events::msg::KeyVerificationStart &msg, std::string); + //! sends a verification request + void sendVerificationRequest(); + //! accepts a verification request + void sendVerificationReady(); + //! completes the verification flow(); + void sendVerificationDone(); + //! accepts a verification + void acceptVerificationRequest(); + //! starts the verification flow + void startVerificationRequest(); + //! cancels a verification flow + void cancelVerification(DeviceVerificationFlow::Error error_code); + //! sends the verification key + void sendVerificationKey(); + //! sends the mac of the keys + void sendVerificationMac(); + //! Completes the verification flow + void acceptDevice(); + + std::string transaction_id; + + bool sender; + Type type; + mtx::identifiers::User toClient; + QString deviceId; + + // public part of our master key, when trusted or empty + std::string our_trusted_master_key; + + mtx::events::msg::SASMethods method = mtx::events::msg::SASMethods::Emoji; + QTimer *timeout = nullptr; + sas_ptr sas; + std::string mac_method; + std::string commitment; + nlohmann::json canonical_json; + + std::vector<int> sasList; + UserKeyCache their_keys; + TimelineModel *model_; + mtx::common::RelatesTo relation; + + State state_ = PromptStartVerification; + Error error_ = UnknownMethod; + + bool isMacVerified = false; + + template<typename T> + void send(T msg) + { + if (this->type == DeviceVerificationFlow::Type::ToDevice) { + mtx::requests::ToDeviceMessages<T> body; + msg.transaction_id = this->transaction_id; + body[this->toClient][deviceId.toStdString()] = msg; + + http::client()->send_to_device<T>( + this->transaction_id, body, [](mtx::http::RequestErr err) { + if (err) + nhlog::net()->warn( + "failed to send verification to_device message: {} {}", + err->matrix_error.error, + static_cast<int>(err->status_code)); + }); + } else if (this->type == DeviceVerificationFlow::Type::RoomMsg && model_) { + if constexpr (!std::is_same_v<T, mtx::events::msg::KeyVerificationRequest>) + msg.relates_to = this->relation; + (model_)->sendMessageEvent(msg, mtx::events::to_device_content_to_type<T>); + } + + nhlog::net()->debug( + "Sent verification step: {} in state: {}", + mtx::events::to_string(mtx::events::to_device_content_to_type<T>), + state().toStdString()); + } +}; diff --git a/src/EventAccessors.cpp b/src/EventAccessors.cpp
index 88612b14..b62be9a5 100644 --- a/src/EventAccessors.cpp +++ b/src/EventAccessors.cpp
@@ -39,8 +39,15 @@ struct EventMsgType template<class T> mtx::events::MessageType operator()(const mtx::events::Event<T> &e) { - if constexpr (is_detected<msgtype_t, T>::value) - return mtx::events::getMessageType(e.content.msgtype); + if constexpr (is_detected<msgtype_t, T>::value) { + if constexpr (std::is_same_v<std::optional<std::string>, + std::remove_cv_t<decltype(e.content.msgtype)>>) + return mtx::events::getMessageType(e.content.msgtype.value()); + else if constexpr (std::is_same_v< + std::string, + std::remove_cv_t<decltype(e.content.msgtype)>>) + return mtx::events::getMessageType(e.content.msgtype); + } return mtx::events::MessageType::Unknown; } }; @@ -97,8 +104,15 @@ struct EventBody template<class T> std::string operator()(const mtx::events::Event<T> &e) { - if constexpr (is_detected<body_t, T>::value) - return e.content.body; + if constexpr (is_detected<body_t, T>::value) { + if constexpr (std::is_same_v<std::optional<std::string>, + std::remove_cv_t<decltype(e.content.body)>>) + return e.content.body ? e.content.body.value() : ""; + else if constexpr (std::is_same_v< + std::string, + std::remove_cv_t<decltype(e.content.body)>>) + return e.content.body; + } return ""; } }; diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp
index 330ba2a3..b6ad8bbe 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp
@@ -329,15 +329,6 @@ MainWindow::hasActiveUser() } void -MainWindow::openUserProfile(const QString &user_id, const QString &room_id) -{ - auto dialog = new dialogs::UserProfile(this); - dialog->init(user_id, room_id); - - showDialog(dialog); -} - -void MainWindow::openRoomSettings(const QString &room_id) { auto dialog = new dialogs::RoomSettings(room_id, this); diff --git a/src/MainWindow.h b/src/MainWindow.h
index 4f54a195..e66f299f 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h
@@ -25,7 +25,6 @@ #include <QSystemTrayIcon> #include "UserSettingsPage.h" -#include "dialogs/UserProfile.h" #include "ui/OverlayModal.h" #include "jdenticoninterface.h" @@ -76,7 +75,6 @@ public: void openLogoutDialog(); void openRoomSettings(const QString &room_id); void openMemberListDialog(const QString &room_id); - void openUserProfile(const QString &user_id, const QString &room_id); void openReadReceiptsDialog(const QString &event_id); void hideOverlay(); diff --git a/src/Olm.cpp b/src/Olm.cpp
index e38e9ef7..f4cb2209 100644 --- a/src/Olm.cpp +++ b/src/Olm.cpp
@@ -1,9 +1,12 @@ +#include <QObject> #include <variant> #include "Olm.h" #include "Cache.h" #include "Cache_p.h" +#include "ChatPage.h" +#include "DeviceVerificationFlow.h" #include "Logging.h" #include "MatrixClient.h" #include "Utils.h" @@ -28,7 +31,6 @@ handle_to_device_messages(const std::vector<mtx::events::collections::DeviceEven { if (msgs.empty()) return; - nhlog::crypto()->info("received {} to_device messages", msgs.size()); nlohmann::json j_msg; @@ -53,6 +55,10 @@ handle_to_device_messages(const std::vector<mtx::events::collections::DeviceEven nhlog::crypto()->warn("validation error for olm message: {} {}", e.what(), j_msg.dump(2)); + + nhlog::crypto()->warn("validation error for olm message: {} {}", + e.what(), + j_msg.dump(2)); } } else if (msg_type == to_string(mtx::events::EventType::RoomKeyRequest)) { @@ -71,6 +77,43 @@ handle_to_device_messages(const std::vector<mtx::events::collections::DeviceEven e.what(), j_msg.dump(2)); } + } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationAccept)) { + auto message = std::get< + mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationAccept>>(msg); + ChatPage::instance()->receivedDeviceVerificationAccept(message.content); + } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationRequest)) { + auto message = std::get< + mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationRequest>>(msg); + ChatPage::instance()->receivedDeviceVerificationRequest(message.content, + message.sender); + } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationCancel)) { + auto message = std::get< + mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationCancel>>(msg); + ChatPage::instance()->receivedDeviceVerificationCancel(message.content); + } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationKey)) { + auto message = + std::get<mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationKey>>( + msg); + ChatPage::instance()->receivedDeviceVerificationKey(message.content); + } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationMac)) { + auto message = + std::get<mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationMac>>( + msg); + ChatPage::instance()->receivedDeviceVerificationMac(message.content); + } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationStart)) { + auto message = std::get< + mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationStart>>(msg); + ChatPage::instance()->receivedDeviceVerificationStart(message.content, + message.sender); + } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationReady)) { + auto message = std::get< + mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationReady>>(msg); + ChatPage::instance()->receivedDeviceVerificationReady(message.content); + } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationDone)) { + auto message = + std::get<mtx::events::DeviceEvent<mtx::events::msg::KeyVerificationDone>>( + msg); + ChatPage::instance()->receivedDeviceVerificationDone(message.content); } else { nhlog::crypto()->warn("unhandled event: {}", j_msg.dump(2)); } @@ -95,23 +138,76 @@ handle_olm_message(const OlmMessage &msg) auto payload = try_olm_decryption(msg.sender_key, cipher.second); - if (!payload.is_null()) { - nhlog::crypto()->debug("decrypted olm payload: {}", payload.dump(2)); - create_inbound_megolm_session(msg.sender, msg.sender_key, payload); - return; + if (payload.is_null()) { + // Check for PRE_KEY message + if (cipher.second.type == 0) { + payload = handle_pre_key_olm_message( + msg.sender, msg.sender_key, cipher.second); + } else { + nhlog::crypto()->error("Undecryptable olm message!"); + continue; + } } - // Not a PRE_KEY message - if (cipher.second.type != 0) { - // TODO: log that it should have matched something - return; - } + if (!payload.is_null()) { + std::string msg_type = payload["type"]; - handle_pre_key_olm_message(msg.sender, msg.sender_key, cipher.second); + if (msg_type == to_string(mtx::events::EventType::KeyVerificationAccept)) { + ChatPage::instance()->receivedDeviceVerificationAccept( + payload["content"]); + return; + } else if (msg_type == + to_string(mtx::events::EventType::KeyVerificationRequest)) { + ChatPage::instance()->receivedDeviceVerificationRequest( + payload["content"], payload["sender"]); + return; + } else if (msg_type == + to_string(mtx::events::EventType::KeyVerificationCancel)) { + ChatPage::instance()->receivedDeviceVerificationCancel( + payload["content"]); + return; + } else if (msg_type == + to_string(mtx::events::EventType::KeyVerificationKey)) { + ChatPage::instance()->receivedDeviceVerificationKey( + payload["content"]); + return; + } else if (msg_type == + to_string(mtx::events::EventType::KeyVerificationMac)) { + ChatPage::instance()->receivedDeviceVerificationMac( + payload["content"]); + return; + } else if (msg_type == + to_string(mtx::events::EventType::KeyVerificationStart)) { + ChatPage::instance()->receivedDeviceVerificationStart( + payload["content"], payload["sender"]); + return; + } else if (msg_type == + to_string(mtx::events::EventType::KeyVerificationReady)) { + ChatPage::instance()->receivedDeviceVerificationReady( + payload["content"]); + return; + } else if (msg_type == + to_string(mtx::events::EventType::KeyVerificationDone)) { + ChatPage::instance()->receivedDeviceVerificationDone( + payload["content"]); + return; + } else if (msg_type == to_string(mtx::events::EventType::RoomKey)) { + mtx::events::DeviceEvent<mtx::events::msg::RoomKey> roomKey = + payload; + create_inbound_megolm_session(roomKey, msg.sender_key); + return; + } else if (msg_type == + to_string(mtx::events::EventType::ForwardedRoomKey)) { + mtx::events::DeviceEvent<mtx::events::msg::ForwardedRoomKey> + roomKey = payload; + import_inbound_megolm_session(roomKey); + return; + } + } } } -void +nlohmann::json handle_pre_key_olm_message(const std::string &sender, const std::string &sender_key, const mtx::events::msg::OlmCipherContent &content) @@ -129,14 +225,14 @@ handle_pre_key_olm_message(const std::string &sender, } catch (const mtx::crypto::olm_exception &e) { nhlog::crypto()->critical( "failed to create inbound session with {}: {}", sender, e.what()); - return; + return {}; } if (!mtx::crypto::matches_inbound_session_from( inbound_session.get(), sender_key, content.body)) { nhlog::crypto()->warn("inbound olm session doesn't match sender's key ({})", sender); - return; + return {}; } mtx::crypto::BinaryBuf output; @@ -146,7 +242,7 @@ handle_pre_key_olm_message(const std::string &sender, } catch (const mtx::crypto::olm_exception &e) { nhlog::crypto()->critical( "failed to decrypt olm message {}: {}", content.body, e.what()); - return; + return {}; } auto plaintext = json::parse(std::string((char *)output.data(), output.size())); @@ -159,7 +255,7 @@ handle_pre_key_olm_message(const std::string &sender, "failed to save inbound olm session from {}: {}", sender, e.what()); } - create_inbound_megolm_session(sender, sender_key, plaintext); + return plaintext; } mtx::events::msg::Encrypted @@ -169,10 +265,15 @@ encrypt_group_message(const std::string &room_id, const std::string &device_id, // relations shouldn't be encrypted... mtx::common::ReplyRelatesTo relation; + mtx::common::RelatesTo r_relation; + if (body["content"].contains("m.relates_to") && body["content"]["m.relates_to"].contains("m.in_reply_to")) { relation = body["content"]["m.relates_to"]; body["content"].erase("m.relates_to"); + } else if (body["content"]["m.relates_to"].contains("event_id")) { + r_relation = body["content"]["m.relates_to"]; + body["content"].erase("m.relates_to"); } // Always check before for existence. @@ -181,12 +282,13 @@ encrypt_group_message(const std::string &room_id, const std::string &device_id, // Prepare the m.room.encrypted event. msg::Encrypted data; - data.ciphertext = std::string((char *)payload.data(), payload.size()); - data.sender_key = olm::client()->identity_keys().curve25519; - data.session_id = res.data.session_id; - data.device_id = device_id; - data.algorithm = MEGOLM_ALGO; - data.relates_to = relation; + data.ciphertext = std::string((char *)payload.data(), payload.size()); + data.sender_key = olm::client()->identity_keys().curve25519; + data.session_id = res.data.session_id; + data.device_id = device_id; + data.algorithm = MEGOLM_ALGO; + data.relates_to = relation; + data.r_relates_to = r_relation; auto message_index = olm_outbound_group_session_message_index(res.session); nhlog::crypto()->debug("next message_index {}", message_index); @@ -229,10 +331,12 @@ try_olm_decryption(const std::string &sender_key, const mtx::events::msg::OlmCip } try { - return json::parse(std::string((char *)text.data(), text.size())); + return json::parse(std::string_view((char *)text.data(), text.size())); } catch (const json::exception &e) { - nhlog::crypto()->critical("failed to parse the decrypted session msg: {}", - e.what()); + nhlog::crypto()->critical( + "failed to parse the decrypted session msg: {} {}", + e.what(), + std::string_view((char *)text.data(), text.size())); } } @@ -240,39 +344,54 @@ try_olm_decryption(const std::string &sender_key, const mtx::events::msg::OlmCip } void -create_inbound_megolm_session(const std::string &sender, - const std::string &sender_key, - const nlohmann::json &payload) +create_inbound_megolm_session(const mtx::events::DeviceEvent<mtx::events::msg::RoomKey> &roomKey, + const std::string &sender_key) { - std::string room_id, session_id, session_key; + MegolmSessionIndex index; + index.room_id = roomKey.content.room_id; + index.session_id = roomKey.content.session_id; + index.sender_key = sender_key; try { - room_id = payload.at("content").at("room_id"); - session_id = payload.at("content").at("session_id"); - session_key = payload.at("content").at("session_key"); - } catch (const nlohmann::json::exception &e) { - nhlog::crypto()->critical( - "failed to parse plaintext olm message: {} {}", e.what(), payload.dump(2)); + auto megolm_session = + olm::client()->init_inbound_group_session(roomKey.content.session_key); + cache::saveInboundMegolmSession(index, std::move(megolm_session)); + } catch (const lmdb::error &e) { + nhlog::crypto()->critical("failed to save inbound megolm session: {}", e.what()); + return; + } catch (const mtx::crypto::olm_exception &e) { + nhlog::crypto()->critical("failed to create inbound megolm session: {}", e.what()); return; } + nhlog::crypto()->info( + "established inbound megolm session ({}, {})", roomKey.content.room_id, roomKey.sender); +} + +void +import_inbound_megolm_session( + const mtx::events::DeviceEvent<mtx::events::msg::ForwardedRoomKey> &roomKey) +{ MegolmSessionIndex index; - index.room_id = room_id; - index.session_id = session_id; - index.sender_key = sender_key; + index.room_id = roomKey.content.room_id; + index.session_id = roomKey.content.session_id; + index.sender_key = roomKey.content.sender_key; try { - auto megolm_session = olm::client()->init_inbound_group_session(session_key); + auto megolm_session = + olm::client()->import_inbound_group_session(roomKey.content.session_key); cache::saveInboundMegolmSession(index, std::move(megolm_session)); } catch (const lmdb::error &e) { nhlog::crypto()->critical("failed to save inbound megolm session: {}", e.what()); return; } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->critical("failed to create inbound megolm session: {}", e.what()); + nhlog::crypto()->critical("failed to import inbound megolm session: {}", e.what()); return; } - nhlog::crypto()->info("established inbound megolm session ({}, {})", room_id, sender); + // TODO(Nico): Reload messages encrypted with this key. + nhlog::crypto()->info( + "established inbound megolm session ({}, {})", roomKey.content.room_id, roomKey.sender); } void @@ -373,6 +492,10 @@ handle_key_request_message(const mtx::events::DeviceEvent<mtx::events::msg::KeyR if (!cache::outboundMegolmSessionExists(req.content.room_id)) { nhlog::crypto()->warn("requested session not found in room: {}", req.content.room_id); + + nhlog::crypto()->warn("requested session not found in room: {}", + req.content.room_id); + return; } @@ -477,9 +600,11 @@ send_megolm_key_to_device(const std::string &user_id, ->create_room_key_event(UserId(user_id), pks.ed25519, payload) .dump(); + mtx::requests::ClaimKeys claim_keys; + claim_keys.one_time_keys[user_id][device_id] = mtx::crypto::SIGNED_CURVE25519; + http::client()->claim_keys( - user_id, - {device_id}, + claim_keys, [room_key, user_id, device_id, pks](const mtx::responses::ClaimKeys &res, mtx::http::RequestErr err) { if (err) { diff --git a/src/Olm.h b/src/Olm.h
index 87f4e3ec..7b97039b 100644 --- a/src/Olm.h +++ b/src/Olm.h
@@ -71,11 +71,13 @@ handle_olm_message(const OlmMessage &msg); //! Establish a new inbound megolm session with the decrypted payload from olm. void -create_inbound_megolm_session(const std::string &sender, - const std::string &sender_key, - const nlohmann::json &payload); - +create_inbound_megolm_session(const mtx::events::DeviceEvent<mtx::events::msg::RoomKey> &roomKey, + const std::string &sender_key); void +import_inbound_megolm_session( + const mtx::events::DeviceEvent<mtx::events::msg::ForwardedRoomKey> &roomKey); + +nlohmann::json handle_pre_key_olm_message(const std::string &sender, const std::string &sender_key, const mtx::events::msg::OlmCipherContent &content); diff --git a/src/TextInputWidget.cpp b/src/TextInputWidget.cpp
index f0d23e21..22e8aafc 100644 --- a/src/TextInputWidget.cpp +++ b/src/TextInputWidget.cpp
@@ -709,6 +709,8 @@ TextInputWidget::command(QString command, QString args) emit sendTextMessage("ノ┬─┬ノ ︵ ( \\o°o)\\"); } else if (command == "clear-timeline") { emit clearRoomTimeline(); + } else if (command == "rotate-megolm-session") { + emit rotateMegolmSession(); } } diff --git a/src/TextInputWidget.h b/src/TextInputWidget.h
index f4ca980e..7cc73e98 100644 --- a/src/TextInputWidget.h +++ b/src/TextInputWidget.h
@@ -186,6 +186,7 @@ signals: void sendBanRoomRequest(const QString &userid, const QString &reason); void sendUnbanRoomRequest(const QString &userid, const QString &reason); void changeRoomNick(const QString &displayname); + void rotateMegolmSession(); void startedTyping(); void stoppedTyping(); diff --git a/src/WebRTCSession.cpp b/src/WebRTCSession.cpp
index 9c8c032d..1c11f750 100644 --- a/src/WebRTCSession.cpp +++ b/src/WebRTCSession.cpp
@@ -418,7 +418,6 @@ getPayloadType(const std::string &sdp, const std::string &name) } return -1; } - } bool diff --git a/src/dialogs/AcceptCall.cpp b/src/dialogs/AcceptCall.cpp
index be1eb0c9..2b47b7dc 100644 --- a/src/dialogs/AcceptCall.cpp +++ b/src/dialogs/AcceptCall.cpp
@@ -131,5 +131,4 @@ AcceptCall::AcceptCall(const QString &caller, emit close(); }); } - } diff --git a/src/dialogs/AcceptCall.h b/src/dialogs/AcceptCall.h
index 909605d0..5db8fcfa 100644 --- a/src/dialogs/AcceptCall.h +++ b/src/dialogs/AcceptCall.h
@@ -33,5 +33,4 @@ private: QPushButton *rejectBtn_; std::vector<std::string> audioDevices_; }; - } diff --git a/src/dialogs/PlaceCall.cpp b/src/dialogs/PlaceCall.cpp
index 4e70370a..8acdbe88 100644 --- a/src/dialogs/PlaceCall.cpp +++ b/src/dialogs/PlaceCall.cpp
@@ -100,5 +100,4 @@ PlaceCall::PlaceCall(const QString &callee, emit close(); }); } - } diff --git a/src/dialogs/PlaceCall.h b/src/dialogs/PlaceCall.h
index 5a1e982c..e178afc4 100644 --- a/src/dialogs/PlaceCall.h +++ b/src/dialogs/PlaceCall.h
@@ -33,5 +33,4 @@ private: QPushButton *cancelBtn_; std::vector<std::string> audioDevices_; }; - } diff --git a/src/emoji/EmojiModel.h b/src/emoji/EmojiModel.h
index 8d43e000..88bacdee 100644 --- a/src/emoji/EmojiModel.h +++ b/src/emoji/EmojiModel.h
@@ -60,5 +60,4 @@ private: EmojiCategory category_ = EmojiCategory::Search; emoji::Provider emoji_provider_; }; - } \ No newline at end of file diff --git a/src/emoji/EmojiSearchModel.h b/src/emoji/EmojiSearchModel.h
index 1ff5f4e9..13a03934 100644 --- a/src/emoji/EmojiSearchModel.h +++ b/src/emoji/EmojiSearchModel.h
@@ -33,5 +33,4 @@ private: return shortname.replace(" ", "-").replace(":", "-").replace("--", "-").toLower(); } }; - } diff --git a/src/timeline/EventStore.cpp b/src/timeline/EventStore.cpp
index 749d4092..d3c5c3fa 100644 --- a/src/timeline/EventStore.cpp +++ b/src/timeline/EventStore.cpp
@@ -5,6 +5,7 @@ #include "Cache.h" #include "Cache_p.h" +#include "ChatPage.h" #include "EventAccessors.h" #include "Logging.h" #include "MatrixClient.h" @@ -53,9 +54,8 @@ EventStore::EventStore(std::string room_id, QObject *) &EventStore::oldMessagesRetrieved, this, [this](const mtx::responses::Messages &res) { - // uint64_t newFirst = cache::client()->saveOldMessages(room_id_, res); - if (newFirst == first && !res.chunk.empty()) + if (newFirst == first) fetchMore(); else { emit beginInsertRows(toExternalIdx(newFirst), @@ -97,8 +97,8 @@ EventStore::EventStore(std::string room_id, QObject *) room_id_, txn_id, e.content, - [this, txn_id](const mtx::responses::EventId &event_id, - mtx::http::RequestErr err) { + [this, txn_id, e](const mtx::responses::EventId &event_id, + mtx::http::RequestErr err) { if (err) { const int status_code = static_cast<int>(err->status_code); @@ -110,7 +110,21 @@ EventStore::EventStore(std::string room_id, QObject *) emit messageFailed(txn_id); return; } + emit messageSent(txn_id, event_id.event_id.to_string()); + if constexpr (std::is_same_v< + decltype(e.content), + mtx::events::msg::Encrypted>) { + auto event = + decryptEvent({room_id_, e.event_id}, e); + if (auto dec = + std::get_if<mtx::events::RoomEvent< + mtx::events::msg:: + KeyVerificationRequest>>(event)) { + emit updateFlowEventId( + event_id.event_id.to_string()); + } + } }); }, event->data); @@ -265,9 +279,78 @@ EventStore::handleSync(const mtx::responses::Timeline &events) emit dataChanged(toExternalIdx(*idx), toExternalIdx(*idx)); } } + + // decrypting and checking some encrypted messages + if (auto encrypted = + std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>( + &event)) { + mtx::events::collections::TimelineEvents *d_event = + decryptEvent({room_id_, encrypted->event_id}, *encrypted); + if (std::visit( + [](auto e) { return (e.sender != utils::localUser().toStdString()); }, + *d_event)) { + handle_room_verification(*d_event); + } + // else { + // // only the key.verification.ready sent by localuser's other + // device + // // is of significance as it is used for detecting accepted request + // if (std::get_if<mtx::events::RoomEvent< + // mtx::events::msg::KeyVerificationReady>>(d_event)) { + // auto msg = std::get_if<mtx::events::RoomEvent< + // mtx::events::msg::KeyVerificationReady>>(d_event); + // ChatPage::instance()->receivedDeviceVerificationReady( + // msg->content); + // } + //} + } } } +namespace { +template<class... Ts> +struct overloaded : Ts... +{ + using Ts::operator()...; +}; +template<class... Ts> +overloaded(Ts...) -> overloaded<Ts...>; +} + +void +EventStore::handle_room_verification(mtx::events::collections::TimelineEvents event) +{ + std::visit( + overloaded{ + [this](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> &msg) { + emit startDMVerification(msg); + }, + [](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationCancel> &msg) { + ChatPage::instance()->receivedDeviceVerificationCancel(msg.content); + }, + [](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationAccept> &msg) { + ChatPage::instance()->receivedDeviceVerificationAccept(msg.content); + }, + [](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationKey> &msg) { + ChatPage::instance()->receivedDeviceVerificationKey(msg.content); + }, + [](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationMac> &msg) { + ChatPage::instance()->receivedDeviceVerificationMac(msg.content); + }, + [](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationReady> &msg) { + ChatPage::instance()->receivedDeviceVerificationReady(msg.content); + }, + [](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationDone> &msg) { + ChatPage::instance()->receivedDeviceVerificationDone(msg.content); + }, + [](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationStart> &msg) { + ChatPage::instance()->receivedDeviceVerificationStart(msg.content, msg.sender); + }, + [](const auto &) {}, + }, + event); +} + QVariantList EventStore::reactions(const std::string &event_id) { @@ -289,13 +372,14 @@ EventStore::reactions(const std::string &event_id) continue; if (auto reaction = std::get_if<mtx::events::RoomEvent<mtx::events::msg::Reaction>>( - related_event)) { - auto &agg = aggregation[reaction->content.relates_to.key]; + related_event); + reaction && reaction->content.relates_to.key) { + auto &agg = aggregation[reaction->content.relates_to.key.value()]; if (agg.count == 0) { Reaction temp{}; temp.key_ = - QString::fromStdString(reaction->content.relates_to.key); + QString::fromStdString(reaction->content.relates_to.key.value()); reactions.push_back(temp); } @@ -407,11 +491,12 @@ EventStore::decryptEvent(const IdIndex &idx, auto decryptionResult = olm::decryptEvent(index, e); + mtx::events::RoomEvent<mtx::events::msg::Notice> dummy; + dummy.origin_server_ts = e.origin_server_ts; + dummy.event_id = e.event_id; + dummy.sender = e.sender; + if (decryptionResult.error) { - mtx::events::RoomEvent<mtx::events::msg::Notice> dummy; - dummy.origin_server_ts = e.origin_server_ts; - dummy.event_id = e.event_id; - dummy.sender = e.sender; switch (*decryptionResult.error) { case olm::DecryptionErrorCode::MissingSession: dummy.content.body = @@ -484,6 +569,66 @@ EventStore::decryptEvent(const IdIndex &idx, return asCacheEntry(std::move(dummy)); } + std::string msg_str; + try { + auto session = cache::client()->getInboundMegolmSession(index); + auto res = olm::client()->decrypt_group_message(session, e.content.ciphertext); + msg_str = std::string((char *)res.data.data(), res.data.size()); + } catch (const lmdb::error &e) { + nhlog::db()->critical("failed to retrieve megolm session with index ({}, {}, {})", + index.room_id, + index.session_id, + index.sender_key, + e.what()); + dummy.content.body = + tr("-- Decryption Error (failed to retrieve megolm keys from db) --", + "Placeholder, when the message can't be decrypted, because the DB " + "access " + "failed.") + .toStdString(); + return asCacheEntry(std::move(dummy)); + } catch (const mtx::crypto::olm_exception &e) { + nhlog::crypto()->critical("failed to decrypt message with index ({}, {}, {}): {}", + index.room_id, + index.session_id, + index.sender_key, + e.what()); + dummy.content.body = + tr("-- Decryption Error (%1) --", + "Placeholder, when the message can't be decrypted. In this case, the " + "Olm " + "decrytion returned an error, which is passed as %1.") + .arg(e.what()) + .toStdString(); + return asCacheEntry(std::move(dummy)); + } + + // Add missing fields for the event. + json body = json::parse(msg_str); + body["event_id"] = e.event_id; + body["sender"] = e.sender; + body["origin_server_ts"] = e.origin_server_ts; + body["unsigned"] = e.unsigned_data; + + // relations are unencrypted in content... + if (json old_ev = e; old_ev["content"].count("m.relates_to") != 0) + body["content"]["m.relates_to"] = old_ev["content"]["m.relates_to"]; + + json event_array = json::array(); + event_array.push_back(body); + + std::vector<mtx::events::collections::TimelineEvents> temp_events; + mtx::responses::utils::parse_timeline_events(event_array, temp_events); + + if (temp_events.size() == 1) { + auto encInfo = mtx::accessors::file(temp_events[0]); + + if (encInfo) + emit newEncryptedImage(encInfo.value()); + + return asCacheEntry(std::move(temp_events[0])); + } + auto encInfo = mtx::accessors::file(decryptionResult.event.value()); if (encInfo) emit newEncryptedImage(encInfo.value()); @@ -515,7 +660,8 @@ EventStore::get(std::string_view id, std::string_view related_to, bool decrypt) mtx::http::RequestErr err) { if (err) { nhlog::net()->error( - "Failed to retrieve event with id {}, which was " + "Failed to retrieve event with id {}, which " + "was " "requested to show the replyTo for event {}", relatedTo, id); diff --git a/src/timeline/EventStore.h b/src/timeline/EventStore.h
index d4353a18..954e271c 100644 --- a/src/timeline/EventStore.h +++ b/src/timeline/EventStore.h
@@ -98,6 +98,9 @@ signals: void processPending(); void messageSent(std::string txn_id, std::string event_id); void messageFailed(std::string txn_id); + void startDMVerification( + const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> &msg); + void updateFlowEventId(std::string event_id); public slots: void addPending(mtx::events::collections::TimelineEvents event); @@ -107,6 +110,7 @@ private: mtx::events::collections::TimelineEvents *decryptEvent( const IdIndex &idx, const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e); + void handle_room_verification(mtx::events::collections::TimelineEvents event); std::string room_id_; diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 32e9f92c..359e95bc 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp
@@ -116,7 +116,46 @@ struct RoomEventType { return qml_mtx_events::EventType::VideoMessage; } - + qml_mtx_events::EventType operator()( + const mtx::events::Event<mtx::events::msg::KeyVerificationRequest> &) + { + return qml_mtx_events::EventType::KeyVerificationRequest; + } + qml_mtx_events::EventType operator()( + const mtx::events::Event<mtx::events::msg::KeyVerificationStart> &) + { + return qml_mtx_events::EventType::KeyVerificationStart; + } + qml_mtx_events::EventType operator()( + const mtx::events::Event<mtx::events::msg::KeyVerificationMac> &) + { + return qml_mtx_events::EventType::KeyVerificationMac; + } + qml_mtx_events::EventType operator()( + const mtx::events::Event<mtx::events::msg::KeyVerificationAccept> &) + { + return qml_mtx_events::EventType::KeyVerificationAccept; + } + qml_mtx_events::EventType operator()( + const mtx::events::Event<mtx::events::msg::KeyVerificationReady> &) + { + return qml_mtx_events::EventType::KeyVerificationReady; + } + qml_mtx_events::EventType operator()( + const mtx::events::Event<mtx::events::msg::KeyVerificationCancel> &) + { + return qml_mtx_events::EventType::KeyVerificationCancel; + } + qml_mtx_events::EventType operator()( + const mtx::events::Event<mtx::events::msg::KeyVerificationKey> &) + { + return qml_mtx_events::EventType::KeyVerificationKey; + } + qml_mtx_events::EventType operator()( + const mtx::events::Event<mtx::events::msg::KeyVerificationDone> &) + { + return qml_mtx_events::EventType::KeyVerificationDone; + } qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Redacted> &) { return qml_mtx_events::EventType::Redacted; @@ -211,6 +250,15 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj connect(&events, &EventStore::newEncryptedImage, this, &TimelineModel::newEncryptedImage); connect( &events, &EventStore::fetchedMore, this, [this]() { setPaginationInProgress(false); }); + connect(&events, + &EventStore::startDMVerification, + this, + [this](mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> msg) { + ChatPage::instance()->receivedRoomDeviceVerificationRequest(msg, this); + }); + connect(&events, &EventStore::updateFlowEventId, this, [this](std::string event_id) { + this->updateFlowEventId(event_id); + }); } QHash<int, QByteArray> @@ -743,9 +791,9 @@ TimelineModel::viewDecryptedRawMessage(QString id) const } void -TimelineModel::openUserProfile(QString userid) const +TimelineModel::openUserProfile(QString userid) { - MainWindow::instance()->openUserProfile(userid, room_id_); + emit openProfile(new UserProfile(room_id_, userid, manager_, this)); } void @@ -846,18 +894,18 @@ TimelineModel::markEventsAsRead(const std::vector<QString> &event_ids) } } +template<typename T> void -TimelineModel::sendEncryptedMessageEvent(const std::string &txn_id, - nlohmann::json content, - mtx::events::EventType eventType) +TimelineModel::sendEncryptedMessage(mtx::events::RoomEvent<T> msg, mtx::events::EventType eventType) { const auto room_id = room_id_.toStdString(); using namespace mtx::events; using namespace mtx::identifiers; - json doc = { - {"type", mtx::events::to_string(eventType)}, {"content", content}, {"room_id", room_id}}; + json doc = {{"type", mtx::events::to_string(eventType)}, + {"content", json(msg.content)}, + {"room_id", room_id}}; try { // Check if we have already an outbound megolm session then we can use. @@ -865,7 +913,7 @@ TimelineModel::sendEncryptedMessageEvent(const std::string &txn_id, mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> event; event.content = olm::encrypt_group_message(room_id, http::client()->device_id(), doc); - event.event_id = txn_id; + event.event_id = msg.event_id; event.room_id = room_id; event.sender = http::client()->user_id().to_string(); event.type = mtx::events::EventType::RoomEncrypted; @@ -893,32 +941,43 @@ TimelineModel::sendEncryptedMessageEvent(const std::string &txn_id, OutboundGroupSessionData session_data; session_data.session_id = session_id; session_data.session_key = session_key; - session_data.message_index = 0; // TODO Update me + session_data.message_index = 0; cache::saveOutboundMegolmSession( room_id, session_data, std::move(outbound_session)); + { + MegolmSessionIndex index; + index.room_id = room_id; + index.session_id = session_id; + index.sender_key = olm::client()->identity_keys().curve25519; + auto megolm_session = + olm::client()->init_inbound_group_session(session_key); + cache::saveInboundMegolmSession(index, std::move(megolm_session)); + } + const auto members = cache::roomMembers(room_id); nhlog::ui()->info("retrieved {} members for {}", members.size(), room_id); - auto keeper = std::make_shared<StateKeeper>([room_id, doc, txn_id, this]() { - try { - mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> event; - event.content = olm::encrypt_group_message( - room_id, http::client()->device_id(), doc); - event.event_id = txn_id; - event.room_id = room_id; - event.sender = http::client()->user_id().to_string(); - event.type = mtx::events::EventType::RoomEncrypted; - event.origin_server_ts = QDateTime::currentMSecsSinceEpoch(); + auto keeper = + std::make_shared<StateKeeper>([room_id, doc, txn_id = msg.event_id, this]() { + try { + mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> event; + event.content = olm::encrypt_group_message( + room_id, http::client()->device_id(), doc); + event.event_id = txn_id; + event.room_id = room_id; + event.sender = http::client()->user_id().to_string(); + event.type = mtx::events::EventType::RoomEncrypted; + event.origin_server_ts = QDateTime::currentMSecsSinceEpoch(); - emit this->addPendingMessageToStore(event); - } catch (const lmdb::error &e) { - nhlog::db()->critical("failed to save megolm outbound session: {}", - e.what()); - emit ChatPage::instance()->showNotification( - tr("Failed to encrypt event, sending aborted!")); - } - }); + emit this->addPendingMessageToStore(event); + } catch (const lmdb::error &e) { + nhlog::db()->critical( + "failed to save megolm outbound session: {}", e.what()); + emit ChatPage::instance()->showNotification( + tr("Failed to encrypt event, sending aborted!")); + } + }); mtx::requests::QueryKeys req; for (const auto &member : members) @@ -926,7 +985,7 @@ TimelineModel::sendEncryptedMessageEvent(const std::string &txn_id, http::client()->query_keys( req, - [keeper = std::move(keeper), megolm_payload, txn_id, this]( + [keeper = std::move(keeper), megolm_payload, txn_id = msg.event_id, this]( const mtx::responses::QueryKeys &res, mtx::http::RequestErr err) { if (err) { nhlog::net()->warn("failed to query device keys: {} {}", @@ -937,19 +996,23 @@ TimelineModel::sendEncryptedMessageEvent(const std::string &txn_id, return; } - for (const auto &user : res.device_keys) { - // Mapping from a device_id with valid identity keys to the - // generated room_key event used for sharing the megolm session. - std::map<std::string, std::string> room_key_msgs; - std::map<std::string, DevicePublicKeys> deviceKeys; + mtx::requests::ClaimKeys claim_keys; - room_key_msgs.clear(); - deviceKeys.clear(); + // Mapping from user id to a device_id with valid identity keys to the + // generated room_key event used for sharing the megolm session. + std::map<std::string, std::map<std::string, std::string>> room_key_msgs; + std::map<std::string, std::map<std::string, DevicePublicKeys>> deviceKeys; + for (const auto &user : res.device_keys) { for (const auto &dev : user.second) { const auto user_id = ::UserId(dev.second.user_id); const auto device_id = DeviceId(dev.second.device_id); + if (user_id.get() == + http::client()->user_id().to_string() && + device_id.get() == http::client()->device_id()) + continue; + const auto device_keys = dev.second.keys; const auto curveKey = "curve25519:" + device_id.get(); const auto edKey = "ed25519:" + device_id.get(); @@ -968,7 +1031,7 @@ TimelineModel::sendEncryptedMessageEvent(const std::string &txn_id, try { if (!mtx::crypto::verify_identity_signature( - json(dev.second), device_id, user_id)) { + dev.second, device_id, user_id)) { nhlog::crypto()->warn( "failed to verify identity keys: {}", json(dev.second).dump(2)); @@ -991,42 +1054,25 @@ TimelineModel::sendEncryptedMessageEvent(const std::string &txn_id, user_id, pks.ed25519, megolm_payload) .dump(); - room_key_msgs.emplace(device_id, room_key); - deviceKeys.emplace(device_id, pks); - } - - std::vector<std::string> valid_devices; - valid_devices.reserve(room_key_msgs.size()); - for (auto const &d : room_key_msgs) { - valid_devices.push_back(d.first); + room_key_msgs[user_id].emplace(device_id, room_key); + deviceKeys[user_id].emplace(device_id, pks); + claim_keys.one_time_keys[user.first][device_id] = + mtx::crypto::SIGNED_CURVE25519; - nhlog::net()->info("{}", d.first); - nhlog::net()->info(" curve25519 {}", - deviceKeys.at(d.first).curve25519); - nhlog::net()->info(" ed25519 {}", - deviceKeys.at(d.first).ed25519); + nhlog::net()->info("{}", device_id.get()); + nhlog::net()->info(" curve25519 {}", pks.curve25519); + nhlog::net()->info(" ed25519 {}", pks.ed25519); } - - nhlog::net()->info( - "sending claim request for user {} with {} devices", - user.first, - valid_devices.size()); - - http::client()->claim_keys( - user.first, - valid_devices, - std::bind(&TimelineModel::handleClaimedKeys, - this, - keeper, - room_key_msgs, - deviceKeys, - user.first, - std::placeholders::_1, - std::placeholders::_2)); - - // TODO: Wait before sending the next batch of requests. - std::this_thread::sleep_for(std::chrono::milliseconds(500)); } + + http::client()->claim_keys(claim_keys, + std::bind(&TimelineModel::handleClaimedKeys, + this, + keeper, + room_key_msgs, + deviceKeys, + std::placeholders::_1, + std::placeholders::_2)); }); // TODO: Let the user know about the errors. @@ -1044,12 +1090,12 @@ TimelineModel::sendEncryptedMessageEvent(const std::string &txn_id, } void -TimelineModel::handleClaimedKeys(std::shared_ptr<StateKeeper> keeper, - const std::map<std::string, std::string> &room_keys, - const std::map<std::string, DevicePublicKeys> &pks, - const std::string &user_id, - const mtx::responses::ClaimKeys &res, - mtx::http::RequestErr err) +TimelineModel::handleClaimedKeys( + std::shared_ptr<StateKeeper> keeper, + const std::map<std::string, std::map<std::string, std::string>> &room_keys, + const std::map<std::string, std::map<std::string, DevicePublicKeys>> &pks, + const mtx::responses::ClaimKeys &res, + mtx::http::RequestErr err) { if (err) { nhlog::net()->warn("claim keys error: {} {} {}", @@ -1059,65 +1105,59 @@ TimelineModel::handleClaimedKeys(std::shared_ptr<StateKeeper> keeper, return; } - nhlog::net()->debug("claimed keys for {}", user_id); - - if (res.one_time_keys.size() == 0) { - nhlog::net()->debug("no one-time keys found for user_id: {}", user_id); - return; - } - - if (res.one_time_keys.find(user_id) == res.one_time_keys.end()) { - nhlog::net()->debug("no one-time keys found for user_id: {}", user_id); - return; - } + // Payload with all the to_device message to be sent. + nlohmann::json body; - auto retrieved_devices = res.one_time_keys.at(user_id); + for (const auto &[user_id, retrieved_devices] : res.one_time_keys) { + nhlog::net()->debug("claimed keys for {}", user_id); + if (retrieved_devices.size() == 0) { + nhlog::net()->debug("no one-time keys found for user_id: {}", user_id); + return; + } - // Payload with all the to_device message to be sent. - json body; - body["messages"][user_id] = json::object(); + for (const auto &rd : retrieved_devices) { + const auto device_id = rd.first; - for (const auto &rd : retrieved_devices) { - const auto device_id = rd.first; - nhlog::net()->debug("{} : \n {}", device_id, rd.second.dump(2)); + nhlog::net()->debug("{} : \n {}", device_id, rd.second.dump(2)); - // TODO: Verify signatures - auto otk = rd.second.begin()->at("key"); + if (rd.second.empty() || !rd.second.begin()->contains("key")) { + nhlog::net()->warn("Skipping device {} as it has no key.", + device_id); + continue; + } - if (pks.find(device_id) == pks.end()) { - nhlog::net()->critical("couldn't find public key for device: {}", - device_id); - continue; - } + // TODO: Verify signatures + auto otk = rd.second.begin()->at("key"); - auto id_key = pks.at(device_id).curve25519; - auto s = olm::client()->create_outbound_session(id_key, otk); + auto id_key = pks.at(user_id).at(device_id).curve25519; + auto s = olm::client()->create_outbound_session(id_key, otk); - if (room_keys.find(device_id) == room_keys.end()) { - nhlog::net()->critical("couldn't find m.room_key for device: {}", - device_id); - continue; - } + auto device_msg = olm::client()->create_olm_encrypted_content( + s.get(), + room_keys.at(user_id).at(device_id), + pks.at(user_id).at(device_id).curve25519); - auto device_msg = olm::client()->create_olm_encrypted_content( - s.get(), room_keys.at(device_id), pks.at(device_id).curve25519); + try { + cache::saveOlmSession(id_key, std::move(s)); + } catch (const lmdb::error &e) { + nhlog::db()->critical("failed to save outbound olm session: {}", + e.what()); + } catch (const mtx::crypto::olm_exception &e) { + nhlog::crypto()->critical( + "failed to pickle outbound olm session: {}", e.what()); + } - try { - cache::saveOlmSession(id_key, std::move(s)); - } catch (const lmdb::error &e) { - nhlog::db()->critical("failed to save outbound olm session: {}", e.what()); - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->critical("failed to pickle outbound olm session: {}", - e.what()); + body["messages"][user_id][device_id] = device_msg; } - body["messages"][user_id][device_id] = device_msg; + nhlog::net()->info("send_to_device: {}", user_id); } - nhlog::net()->info("send_to_device: {}", user_id); - http::client()->send_to_device( - "m.room.encrypted", body, [keeper](mtx::http::RequestErr err) { + mtx::events::to_string(mtx::events::EventType::RoomEncrypted), + http::client()->generate_txn_id(), + body, + [keeper](mtx::http::RequestErr err) { if (err) { nhlog::net()->warn("failed to send " "send_to_device " @@ -1143,8 +1183,7 @@ struct SendMessageVisitor if (encInfo) emit model_->newEncryptedImage(encInfo.value()); - model_->sendEncryptedMessageEvent( - msg.event_id, nlohmann::json(msg.content), Event); + model_->sendEncryptedMessage(msg, Event); } else { msg.type = Event; emit model_->addPendingMessageToStore(msg); @@ -1199,6 +1238,54 @@ struct SendMessageVisitor event); } + void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> &msg) + { + sendRoomEvent<mtx::events::msg::KeyVerificationRequest, + mtx::events::EventType::RoomMessage>(msg); + } + + void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationReady> &msg) + { + sendRoomEvent<mtx::events::msg::KeyVerificationReady, + mtx::events::EventType::KeyVerificationReady>(msg); + } + + void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationStart> &msg) + { + sendRoomEvent<mtx::events::msg::KeyVerificationStart, + mtx::events::EventType::KeyVerificationStart>(msg); + } + + void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationAccept> &msg) + { + sendRoomEvent<mtx::events::msg::KeyVerificationAccept, + mtx::events::EventType::KeyVerificationAccept>(msg); + } + + void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationMac> &msg) + { + sendRoomEvent<mtx::events::msg::KeyVerificationMac, + mtx::events::EventType::KeyVerificationMac>(msg); + } + + void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationKey> &msg) + { + sendRoomEvent<mtx::events::msg::KeyVerificationKey, + mtx::events::EventType::KeyVerificationKey>(msg); + } + + void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationDone> &msg) + { + sendRoomEvent<mtx::events::msg::KeyVerificationDone, + mtx::events::EventType::KeyVerificationDone>(msg); + } + + void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationCancel> &msg) + { + sendRoomEvent<mtx::events::msg::KeyVerificationCancel, + mtx::events::EventType::KeyVerificationCancel>(msg); + } + TimelineModel *model_; }; diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index 6daaac1b..3234a20c 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h
@@ -10,6 +10,7 @@ #include "CacheCryptoStructs.h" #include "EventStore.h" +#include "ui/UserProfile.h" namespace mtx::http { using RequestErr = const std::optional<mtx::http::ClientError> &; @@ -87,6 +88,14 @@ enum EventType VideoMessage, Redacted, UnknownMessage, + KeyVerificationRequest, + KeyVerificationStart, + KeyVerificationMac, + KeyVerificationAccept, + KeyVerificationCancel, + KeyVerificationKey, + KeyVerificationDone, + KeyVerificationReady }; Q_ENUM_NS(EventType) @@ -199,7 +208,7 @@ public: Q_INVOKABLE void viewRawMessage(QString id) const; Q_INVOKABLE void viewDecryptedRawMessage(QString id) const; - Q_INVOKABLE void openUserProfile(QString userid) const; + Q_INVOKABLE void openUserProfile(QString userid); Q_INVOKABLE void replyAction(QString id); Q_INVOKABLE void readReceiptsAction(QString id) const; Q_INVOKABLE void redactEvent(QString id); @@ -275,23 +284,25 @@ signals: void paginationInProgressChanged(const bool); void newCallEvent(const mtx::events::collections::TimelineEvents &event); + void openProfile(UserProfile *profile); + void newMessageToSend(mtx::events::collections::TimelineEvents event); void addPendingMessageToStore(mtx::events::collections::TimelineEvents event); + void updateFlowEventId(std::string event_id); void roomNameChanged(); void roomTopicChanged(); void roomAvatarUrlChanged(); private: - void sendEncryptedMessageEvent(const std::string &txn_id, - nlohmann::json content, - mtx::events::EventType); - void handleClaimedKeys(std::shared_ptr<StateKeeper> keeper, - const std::map<std::string, std::string> &room_key, - const std::map<std::string, DevicePublicKeys> &pks, - const std::string &user_id, - const mtx::responses::ClaimKeys &res, - mtx::http::RequestErr err); + template<typename T> + void sendEncryptedMessage(mtx::events::RoomEvent<T> msg, mtx::events::EventType eventType); + void handleClaimedKeys( + std::shared_ptr<StateKeeper> keeper, + const std::map<std::string, std::map<std::string, std::string>> &room_keys, + const std::map<std::string, std::map<std::string, DevicePublicKeys>> &pks, + const mtx::responses::ClaimKeys &res, + mtx::http::RequestErr err); void readEvent(const std::string &id); void setPaginationInProgress(const bool paginationInProgress); diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index 2b453e56..7c81ca8f 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp
@@ -4,6 +4,7 @@ #include <QMetaType> #include <QPalette> #include <QQmlContext> +#include <QQmlEngine> #include <QString> #include "BlurhashProvider.h" @@ -19,7 +20,12 @@ #include "emoji/EmojiModel.h" #include "emoji/Provider.h" +#include <iostream> //only for debugging + Q_DECLARE_METATYPE(mtx::events::collections::TimelineEvents) +Q_DECLARE_METATYPE(std::vector<DeviceInfo>) + +namespace msgs = mtx::events::msg; void TimelineViewManager::updateEncryptedDescriptions() @@ -65,9 +71,13 @@ TimelineViewManager::userColor(QString id, QColor background) QString TimelineViewManager::userPresence(QString id) const { - return QString::fromStdString( - mtx::presence::to_string(cache::presenceState(id.toStdString()))); + if (id.isEmpty()) + return ""; + else + return QString::fromStdString( + mtx::presence::to_string(cache::presenceState(id.toStdString()))); } + QString TimelineViewManager::userStatus(QString id) const { @@ -83,15 +93,52 @@ TimelineViewManager::TimelineViewManager(QSharedPointer<UserSettings> userSettin , callManager_(callManager) , settings(userSettings) { + qRegisterMetaType<mtx::events::msg::KeyVerificationAccept>(); + qRegisterMetaType<mtx::events::msg::KeyVerificationCancel>(); + qRegisterMetaType<mtx::events::msg::KeyVerificationDone>(); + qRegisterMetaType<mtx::events::msg::KeyVerificationKey>(); + qRegisterMetaType<mtx::events::msg::KeyVerificationMac>(); + qRegisterMetaType<mtx::events::msg::KeyVerificationReady>(); + qRegisterMetaType<mtx::events::msg::KeyVerificationRequest>(); + qRegisterMetaType<mtx::events::msg::KeyVerificationStart>(); + qmlRegisterUncreatableMetaObject(qml_mtx_events::staticMetaObject, "im.nheko", 1, 0, "MtxEvent", "Can't instantiate enum!"); + qmlRegisterUncreatableMetaObject(verification::staticMetaObject, + "im.nheko", + 1, + 0, + "VerificationStatus", + "Can't instantiate enum!"); + qmlRegisterType<DelegateChoice>("im.nheko", 1, 0, "DelegateChoice"); qmlRegisterType<DelegateChooser>("im.nheko", 1, 0, "DelegateChooser"); + qmlRegisterUncreatableType<DeviceVerificationFlow>( + "im.nheko", 1, 0, "DeviceVerificationFlow", "Can't create verification flow from QML!"); + qmlRegisterUncreatableType<UserProfile>( + "im.nheko", + 1, + 0, + "UserProfileModel", + "UserProfile needs to be instantiated on the C++ side"); + + static auto self = this; + qmlRegisterSingletonType<TimelineViewManager>( + "im.nheko", 1, 0, "TimelineManager", [](QQmlEngine *, QJSEngine *) -> QObject * { + return self; + }); + qmlRegisterSingletonType<UserSettings>( + "im.nheko", 1, 0, "Settings", [](QQmlEngine *, QJSEngine *) -> QObject * { + return self->settings.data(); + }); + qRegisterMetaType<mtx::events::collections::TimelineEvents>(); + qRegisterMetaType<std::vector<DeviceInfo>>(); + qmlRegisterType<emoji::EmojiModel>("im.nheko.EmojiModel", 1, 0, "EmojiModel"); qmlRegisterType<emoji::EmojiProxyModel>("im.nheko.EmojiModel", 1, 0, "EmojiProxyModel"); qmlRegisterUncreatableType<QAbstractItemModel>( @@ -123,8 +170,6 @@ TimelineViewManager::TimelineViewManager(QSharedPointer<UserSettings> userSettin }); #endif container->setMinimumSize(200, 200); - view->rootContext()->setContextProperty("timelineManager", this); - view->rootContext()->setContextProperty("settings", settings.data()); updateColorPalette(); view->engine()->addImageProvider("MxcImage", imgProvider); view->engine()->addImageProvider("colorimage", colorImgProvider); @@ -136,6 +181,57 @@ TimelineViewManager::TimelineViewManager(QSharedPointer<UserSettings> userSettin &ChatPage::decryptSidebarChanged, this, &TimelineViewManager::updateEncryptedDescriptions); + connect( + dynamic_cast<ChatPage *>(parent), + &ChatPage::receivedRoomDeviceVerificationRequest, + this, + [this](const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> &message, + TimelineModel *model) { + auto event_id = QString::fromStdString(message.event_id); + if (!this->dvList.contains(event_id)) { + if (auto flow = DeviceVerificationFlow::NewInRoomVerification( + this, + model, + message.content, + QString::fromStdString(message.sender), + event_id)) { + dvList[event_id] = flow; + emit newDeviceVerificationRequest(flow.data()); + } + } + }); + connect(dynamic_cast<ChatPage *>(parent), + &ChatPage::receivedDeviceVerificationRequest, + this, + [this](const mtx::events::msg::KeyVerificationRequest &msg, std::string sender) { + if (!msg.transaction_id) + return; + + auto txnid = QString::fromStdString(msg.transaction_id.value()); + if (!this->dvList.contains(txnid)) { + if (auto flow = DeviceVerificationFlow::NewToDeviceVerification( + this, msg, QString::fromStdString(sender), txnid)) { + dvList[txnid] = flow; + emit newDeviceVerificationRequest(flow.data()); + } + } + }); + connect(dynamic_cast<ChatPage *>(parent), + &ChatPage::receivedDeviceVerificationStart, + this, + [this](const mtx::events::msg::KeyVerificationStart &msg, std::string sender) { + if (!msg.transaction_id) + return; + + auto txnid = QString::fromStdString(msg.transaction_id.value()); + if (!this->dvList.contains(txnid)) { + if (auto flow = DeviceVerificationFlow::NewToDeviceVerification( + this, msg, QString::fromStdString(sender), txnid)) { + dvList[txnid] = flow; + emit newDeviceVerificationRequest(flow.data()); + } + } + }); connect(parent, &ChatPage::loggedOut, this, [this]() { isInitialSync_ = true; emit initialSyncChanged(true); @@ -285,6 +381,58 @@ TimelineViewManager::openRoomSettings() const } void +TimelineViewManager::verifyUser(QString userid) +{ + auto joined_rooms = cache::joinedRooms(); + auto room_infos = cache::getRoomInfo(joined_rooms); + + for (std::string room_id : joined_rooms) { + if ((room_infos[QString::fromStdString(room_id)].member_count == 2) && + cache::isRoomEncrypted(room_id)) { + auto room_members = cache::roomMembers(room_id); + if (std::find(room_members.begin(), + room_members.end(), + (userid).toStdString()) != room_members.end()) { + auto model = models.value(QString::fromStdString(room_id)); + auto flow = DeviceVerificationFlow::InitiateUserVerification( + this, model.data(), userid); + connect(model.data(), + &TimelineModel::updateFlowEventId, + this, + [this, flow](std::string eventId) { + dvList[QString::fromStdString(eventId)] = flow; + }); + emit newDeviceVerificationRequest(flow.data()); + return; + } + } + } + + emit ChatPage::instance()->showNotification( + tr("No share room with this user found. Create an " + "encrypted room with this user and try again.")); +} + +void +TimelineViewManager::removeVerificationFlow(DeviceVerificationFlow *flow) +{ + for (auto it = dvList.keyValueBegin(); it != dvList.keyValueEnd(); ++it) { + if ((*it).second == flow) { + dvList.remove((*it).first); + return; + } + } +} + +void +TimelineViewManager::verifyDevice(QString userid, QString deviceid) +{ + auto flow = DeviceVerificationFlow::InitiateDeviceVerification(this, userid, deviceid); + this->dvList[flow->transactionId()] = flow; + emit newDeviceVerificationRequest(flow.data()); +} + +void TimelineViewManager::updateReadReceipts(const QString &room_id, const std::vector<QString> &event_ids) { diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h
index 9ff9adac..9a2a6467 100644 --- a/src/timeline/TimelineViewManager.h +++ b/src/timeline/TimelineViewManager.h
@@ -11,6 +11,7 @@ #include "Cache.h" #include "CallManager.h" +#include "DeviceVerificationFlow.h" #include "Logging.h" #include "TimelineModel.h" #include "Utils.h" @@ -71,6 +72,10 @@ public: Q_INVOKABLE void openMemberListDialog() const; Q_INVOKABLE void openLeaveRoomDialog() const; Q_INVOKABLE void openRoomSettings() const; + Q_INVOKABLE void removeVerificationFlow(DeviceVerificationFlow *flow); + + void verifyUser(QString userid); + void verifyDevice(QString userid, QString deviceid); signals: void clearRoomMessageCount(QString roomid); @@ -79,6 +84,7 @@ signals: void initialSyncChanged(bool isInitialSync); void replyingEventChanged(QString replyingEvent); void replyClosed(); + void newDeviceVerificationRequest(DeviceVerificationFlow *flow); void inviteUsers(QStringList users); void showRoomList(); void narrowViewChanged(); @@ -172,4 +178,14 @@ private: QSharedPointer<UserSettings> settings; QHash<QString, QColor> userColors; + + QHash<QString, QSharedPointer<DeviceVerificationFlow>> dvList; }; +Q_DECLARE_METATYPE(mtx::events::msg::KeyVerificationAccept) +Q_DECLARE_METATYPE(mtx::events::msg::KeyVerificationCancel) +Q_DECLARE_METATYPE(mtx::events::msg::KeyVerificationDone) +Q_DECLARE_METATYPE(mtx::events::msg::KeyVerificationKey) +Q_DECLARE_METATYPE(mtx::events::msg::KeyVerificationMac) +Q_DECLARE_METATYPE(mtx::events::msg::KeyVerificationReady) +Q_DECLARE_METATYPE(mtx::events::msg::KeyVerificationRequest) +Q_DECLARE_METATYPE(mtx::events::msg::KeyVerificationStart) diff --git a/src/ui/UserProfile.cpp b/src/ui/UserProfile.cpp new file mode 100644
index 00000000..2bb0370f --- /dev/null +++ b/src/ui/UserProfile.cpp
@@ -0,0 +1,227 @@ +#include "UserProfile.h" +#include "Cache_p.h" +#include "ChatPage.h" +#include "DeviceVerificationFlow.h" +#include "Logging.h" +#include "Utils.h" +#include "mtx/responses/crypto.hpp" +#include "timeline/TimelineModel.h" +#include "timeline/TimelineViewManager.h" + +UserProfile::UserProfile(QString roomid, + QString userid, + TimelineViewManager *manager_, + TimelineModel *parent) + : QObject(parent) + , roomid_(roomid) + , userid_(userid) + , manager(manager_) + , model(parent) +{ + fetchDeviceList(this->userid_); + + connect(cache::client(), + &Cache::verificationStatusChanged, + this, + [this](const std::string &user_id) { + if (user_id != this->userid_.toStdString()) + return; + + auto status = cache::verificationStatus(user_id); + if (!status) + return; + this->isUserVerified = status->user_verified; + emit userStatusChanged(); + + for (auto &deviceInfo : deviceList_.deviceList_) { + deviceInfo.verification_status = + std::find(status->verified_devices.begin(), + status->verified_devices.end(), + deviceInfo.device_id.toStdString()) == + status->verified_devices.end() + ? verification::UNVERIFIED + : verification::VERIFIED; + } + deviceList_.reset(deviceList_.deviceList_); + }); +} + +QHash<int, QByteArray> +DeviceInfoModel::roleNames() const +{ + return { + {DeviceId, "deviceId"}, + {DeviceName, "deviceName"}, + {VerificationStatus, "verificationStatus"}, + }; +} + +QVariant +DeviceInfoModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() >= (int)deviceList_.size() || index.row() < 0) + return {}; + + switch (role) { + case DeviceId: + return deviceList_[index.row()].device_id; + case DeviceName: + return deviceList_[index.row()].display_name; + case VerificationStatus: + return QVariant::fromValue(deviceList_[index.row()].verification_status); + default: + return {}; + } +} + +void +DeviceInfoModel::reset(const std::vector<DeviceInfo> &deviceList) +{ + beginResetModel(); + this->deviceList_ = std::move(deviceList); + endResetModel(); +} + +DeviceInfoModel * +UserProfile::deviceList() +{ + return &this->deviceList_; +} + +QString +UserProfile::userid() +{ + return this->userid_; +} + +QString +UserProfile::displayName() +{ + return cache::displayName(roomid_, userid_); +} + +QString +UserProfile::avatarUrl() +{ + return cache::avatarUrl(roomid_, userid_); +} + +bool +UserProfile::getUserStatus() +{ + return isUserVerified; +} + +void +UserProfile::fetchDeviceList(const QString &userID) +{ + auto localUser = utils::localUser(); + + ChatPage::instance()->query_keys( + 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, + static_cast<int>(err->status_code)); + return; + } + + // Finding if the User is Verified or not based on the Signatures + ChatPage::instance()->query_keys( + 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(); + + if (err) { + nhlog::net()->warn("failed to query device keys: {},{}", + err->matrix_error.errcode, + static_cast<int>(err->status_code)); + return; + } + + if (res.device_keys.empty()) { + nhlog::net()->warn("no devices retrieved {}", local_user_id); + return; + } + + std::vector<DeviceInfo> deviceInfo; + auto devices = other_user_keys.device_keys; + auto verificationStatus = + cache::client()->verificationStatus(other_user_id); + + isUserVerified = verificationStatus.user_verified; + emit userStatusChanged(); + + for (const auto &d : devices) { + auto device = d.second; + verification::Status verified = + verification::Status::UNVERIFIED; + + if (std::find(verificationStatus.verified_devices.begin(), + verificationStatus.verified_devices.end(), + device.device_id) != + verificationStatus.verified_devices.end() && + mtx::crypto::verify_identity_signature( + device, + DeviceId(device.device_id), + UserId(other_user_id))) + verified = verification::Status::VERIFIED; + + deviceInfo.push_back( + {QString::fromStdString(d.first), + QString::fromStdString( + device.unsigned_info.device_display_name), + verified}); + } + + this->deviceList_.queueReset(std::move(deviceInfo)); + }); + }); +} + +void +UserProfile::banUser() +{ + ChatPage::instance()->banUser(this->userid_, ""); +} + +// void ignoreUser(){ + +// } + +void +UserProfile::kickUser() +{ + ChatPage::instance()->kickUser(this->userid_, ""); +} + +void +UserProfile::startChat() +{ + mtx::requests::CreateRoom req; + req.preset = mtx::requests::Preset::PrivateChat; + req.visibility = mtx::requests::Visibility::Private; + if (utils::localUser() != this->userid_) + req.invite = {this->userid_.toStdString()}; + emit ChatPage::instance()->createRoom(req); +} + +void +UserProfile::verify(QString device) +{ + if (!device.isEmpty()) + manager->verifyDevice(userid_, device); + else { + manager->verifyUser(userid_); + } +} + +void +UserProfile::unverify(QString device) +{ + cache::markDeviceUnverified(userid_.toStdString(), device.toStdString()); +} diff --git a/src/ui/UserProfile.h b/src/ui/UserProfile.h new file mode 100644
index 00000000..77b22323 --- /dev/null +++ b/src/ui/UserProfile.h
@@ -0,0 +1,123 @@ +#pragma once + +#include <QAbstractListModel> +#include <QObject> +#include <QString> +#include <QVector> + +#include "MatrixClient.h" + +namespace verification { +Q_NAMESPACE + +enum Status +{ + VERIFIED, + UNVERIFIED, + BLOCKED +}; +Q_ENUM_NS(Status) +} + +class DeviceVerificationFlow; +class TimelineModel; +class TimelineViewManager; + +class DeviceInfo +{ +public: + DeviceInfo(const QString deviceID, + const QString displayName, + verification::Status verification_status_) + : device_id(deviceID) + , display_name(displayName) + , verification_status(verification_status_) + {} + DeviceInfo() + : verification_status(verification::UNVERIFIED) + {} + + QString device_id; + QString display_name; + + verification::Status verification_status; +}; + +class DeviceInfoModel : public QAbstractListModel +{ + Q_OBJECT +public: + enum Roles + { + DeviceId, + DeviceName, + VerificationStatus, + }; + + explicit DeviceInfoModel(QObject *parent = nullptr) + { + (void)parent; + connect(this, &DeviceInfoModel::queueReset, this, &DeviceInfoModel::reset); + }; + QHash<int, QByteArray> roleNames() const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override + { + (void)parent; + return (int)deviceList_.size(); + } + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + +signals: + void queueReset(const std::vector<DeviceInfo> &deviceList); +public slots: + void reset(const std::vector<DeviceInfo> &deviceList); + +private: + std::vector<DeviceInfo> deviceList_; + + friend class UserProfile; +}; + +class UserProfile : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString displayName READ displayName CONSTANT) + Q_PROPERTY(QString userid READ userid CONSTANT) + Q_PROPERTY(QString avatarUrl READ avatarUrl CONSTANT) + Q_PROPERTY(DeviceInfoModel *deviceList READ deviceList CONSTANT) + Q_PROPERTY(bool isUserVerified READ getUserStatus NOTIFY userStatusChanged) +public: + UserProfile(QString roomid, + QString userid, + TimelineViewManager *manager_, + TimelineModel *parent = nullptr); + + DeviceInfoModel *deviceList(); + + QString userid(); + QString displayName(); + QString avatarUrl(); + bool getUserStatus(); + + Q_INVOKABLE void verify(QString device = ""); + Q_INVOKABLE void unverify(QString device = ""); + Q_INVOKABLE void fetchDeviceList(const QString &userID); + Q_INVOKABLE void banUser(); + // Q_INVOKABLE void ignoreUser(); + Q_INVOKABLE void kickUser(); + Q_INVOKABLE void startChat(); + +signals: + void userStatusChanged(); + +private: + QString roomid_, userid_; + DeviceInfoModel deviceList_; + bool isUserVerified = false; + TimelineViewManager *manager; + TimelineModel *model; + + void callback_fn(const mtx::responses::QueryKeys &res, + mtx::http::RequestErr err, + std::string user_id); +};