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);
+};
|